913 lines
28 KiB
JavaScript
913 lines
28 KiB
JavaScript
/**
|
||
* =============================================================================
|
||
* Texas Hold'em - Игровая логика (клиентская сторона)
|
||
* Колода, раздача, оценка рук, определение победителя
|
||
* =============================================================================
|
||
*/
|
||
|
||
// =============================================================================
|
||
// КОНСТАНТЫ
|
||
// =============================================================================
|
||
|
||
const SUITS = ['hearts', 'diamonds', 'clubs', 'spades'];
|
||
const RANKS = ['2', '3', '4', '5', '6', '7', '8', '9', '10', 'J', 'Q', 'K', 'A'];
|
||
|
||
const SUIT_SYMBOLS = {
|
||
hearts: '♥',
|
||
diamonds: '♦',
|
||
clubs: '♣',
|
||
spades: '♠'
|
||
};
|
||
|
||
const HAND_RANKINGS = {
|
||
1: 'Старшая карта',
|
||
2: 'Пара',
|
||
3: 'Две пары',
|
||
4: 'Сет',
|
||
5: 'Стрит',
|
||
6: 'Флеш',
|
||
7: 'Фулл-хаус',
|
||
8: 'Каре',
|
||
9: 'Стрит-флеш',
|
||
10: 'Роял-флеш'
|
||
};
|
||
|
||
// =============================================================================
|
||
// КЛАСС КАРТЫ
|
||
// =============================================================================
|
||
|
||
class Card {
|
||
constructor(suit, rank) {
|
||
this.suit = suit;
|
||
this.rank = rank;
|
||
this.value = RANKS.indexOf(rank) + 2; // 2-14 (A=14)
|
||
}
|
||
|
||
/**
|
||
* Получить HTML элемент карты
|
||
*/
|
||
toHTML(isSmall = false, isBack = false) {
|
||
const card = document.createElement('div');
|
||
card.className = `card ${this.suit}${isSmall ? ' card-small' : ''}${isBack ? ' card-back' : ''}`;
|
||
|
||
if (!isBack) {
|
||
card.innerHTML = `
|
||
<span class="card-rank">${this.rank}</span>
|
||
<span class="card-suit">${SUIT_SYMBOLS[this.suit]}</span>
|
||
`;
|
||
}
|
||
|
||
return card;
|
||
}
|
||
|
||
toString() {
|
||
return `${this.rank}${SUIT_SYMBOLS[this.suit]}`;
|
||
}
|
||
}
|
||
|
||
// =============================================================================
|
||
// КЛАСС КОЛОДЫ
|
||
// =============================================================================
|
||
|
||
class Deck {
|
||
constructor() {
|
||
this.reset();
|
||
}
|
||
|
||
/**
|
||
* Сбросить и перемешать колоду
|
||
*/
|
||
reset() {
|
||
this.cards = [];
|
||
for (const suit of SUITS) {
|
||
for (const rank of RANKS) {
|
||
this.cards.push(new Card(suit, rank));
|
||
}
|
||
}
|
||
this.shuffle();
|
||
}
|
||
|
||
/**
|
||
* Перемешать колоду (Fisher-Yates)
|
||
*/
|
||
shuffle() {
|
||
for (let i = this.cards.length - 1; i > 0; i--) {
|
||
const j = Math.floor(Math.random() * (i + 1));
|
||
[this.cards[i], this.cards[j]] = [this.cards[j], this.cards[i]];
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Раздать карты
|
||
*/
|
||
deal(count = 1) {
|
||
return this.cards.splice(0, count);
|
||
}
|
||
}
|
||
|
||
// =============================================================================
|
||
// КЛАСС ИГРОКА
|
||
// =============================================================================
|
||
|
||
class Player {
|
||
constructor(id, name, chips = 1000, isAI = false, aiLevel = 1) {
|
||
this.id = id;
|
||
this.name = name;
|
||
this.chips = chips;
|
||
this.hand = [];
|
||
this.bet = 0;
|
||
this.totalBet = 0;
|
||
this.folded = false;
|
||
this.allIn = false;
|
||
this.isDealer = false;
|
||
this.isSmallBlind = false;
|
||
this.isBigBlind = false;
|
||
this.isAI = isAI;
|
||
this.aiLevel = aiLevel;
|
||
this.lastAction = null;
|
||
this.isConnected = true;
|
||
}
|
||
|
||
/**
|
||
* Сбросить состояние для новой раздачи
|
||
*/
|
||
reset() {
|
||
this.hand = [];
|
||
this.bet = 0;
|
||
this.totalBet = 0;
|
||
this.folded = false;
|
||
this.allIn = false;
|
||
this.isDealer = false;
|
||
this.isSmallBlind = false;
|
||
this.isBigBlind = false;
|
||
this.lastAction = null;
|
||
}
|
||
}
|
||
|
||
// =============================================================================
|
||
// КЛАСС ИГРЫ
|
||
// =============================================================================
|
||
|
||
class PokerGame {
|
||
constructor(options = {}) {
|
||
this.players = [];
|
||
this.deck = new Deck();
|
||
this.communityCards = [];
|
||
this.pot = 0;
|
||
this.sidePots = [];
|
||
this.currentBet = 0;
|
||
this.dealerIndex = -1;
|
||
this.currentPlayerIndex = 0;
|
||
this.gamePhase = 'waiting'; // waiting, preflop, flop, turn, river, showdown
|
||
this.smallBlind = options.smallBlind || 5;
|
||
this.bigBlind = options.bigBlind || 10;
|
||
this.minRaise = this.bigBlind;
|
||
this.lastRaiseAmount = this.bigBlind;
|
||
this.isGameStarted = false;
|
||
this.onUpdate = options.onUpdate || (() => {});
|
||
this.onAction = options.onAction || (() => {});
|
||
this.onHandEnd = options.onHandEnd || (() => {});
|
||
}
|
||
|
||
/**
|
||
* Добавить игрока
|
||
*/
|
||
addPlayer(player) {
|
||
this.players.push(player);
|
||
}
|
||
|
||
/**
|
||
* Получить активных игроков
|
||
*/
|
||
getActivePlayers() {
|
||
return this.players.filter(p => !p.folded && p.chips >= 0);
|
||
}
|
||
|
||
/**
|
||
* Получить игроков с фишками
|
||
*/
|
||
getPlayersWithChips() {
|
||
return this.players.filter(p => p.chips > 0);
|
||
}
|
||
|
||
/**
|
||
* Начать новую раздачу
|
||
*/
|
||
startNewHand() {
|
||
if (this.getPlayersWithChips().length < 2) {
|
||
return false;
|
||
}
|
||
|
||
// Сброс состояния
|
||
this.deck.reset();
|
||
this.communityCards = [];
|
||
this.pot = 0;
|
||
this.sidePots = [];
|
||
this.currentBet = 0;
|
||
this.minRaise = this.bigBlind;
|
||
this.lastRaiseAmount = this.bigBlind;
|
||
|
||
// Сброс игроков
|
||
for (const player of this.players) {
|
||
player.reset();
|
||
}
|
||
|
||
// Определение позиций
|
||
this.moveDealer();
|
||
this.assignBlinds();
|
||
|
||
// Раздача карт
|
||
this.dealHoleCards();
|
||
|
||
// Начало игры
|
||
this.gamePhase = 'preflop';
|
||
this.isGameStarted = true;
|
||
|
||
// Ставки блайндов
|
||
this.postBlinds();
|
||
|
||
// Первый ход
|
||
this.setFirstPlayer();
|
||
|
||
this.onUpdate();
|
||
|
||
// Если первый ход - ИИ
|
||
this.checkAITurn();
|
||
|
||
return true;
|
||
}
|
||
|
||
/**
|
||
* Передвинуть баттон дилера
|
||
*/
|
||
moveDealer() {
|
||
const playersWithChips = this.getPlayersWithChips();
|
||
if (playersWithChips.length < 2) return;
|
||
|
||
// Находим следующего дилера
|
||
do {
|
||
this.dealerIndex = (this.dealerIndex + 1) % this.players.length;
|
||
} while (this.players[this.dealerIndex].chips <= 0);
|
||
|
||
this.players[this.dealerIndex].isDealer = true;
|
||
}
|
||
|
||
/**
|
||
* Назначить блайнды
|
||
*/
|
||
assignBlinds() {
|
||
const playersWithChips = this.getPlayersWithChips();
|
||
|
||
if (playersWithChips.length === 2) {
|
||
// Heads-up: дилер = SB
|
||
this.players[this.dealerIndex].isSmallBlind = true;
|
||
let bbIndex = this.getNextActivePlayerIndex(this.dealerIndex);
|
||
this.players[bbIndex].isBigBlind = true;
|
||
} else {
|
||
// 3+ игроков
|
||
let sbIndex = this.getNextActivePlayerIndex(this.dealerIndex);
|
||
this.players[sbIndex].isSmallBlind = true;
|
||
|
||
let bbIndex = this.getNextActivePlayerIndex(sbIndex);
|
||
this.players[bbIndex].isBigBlind = true;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Поставить блайнды
|
||
*/
|
||
postBlinds() {
|
||
const sbPlayer = this.players.find(p => p.isSmallBlind);
|
||
const bbPlayer = this.players.find(p => p.isBigBlind);
|
||
|
||
if (sbPlayer) {
|
||
const sbAmount = Math.min(sbPlayer.chips, this.smallBlind);
|
||
sbPlayer.chips -= sbAmount;
|
||
sbPlayer.bet = sbAmount;
|
||
sbPlayer.totalBet = sbAmount;
|
||
this.pot += sbAmount;
|
||
sbPlayer.lastAction = `SB ${sbAmount}`;
|
||
}
|
||
|
||
if (bbPlayer) {
|
||
const bbAmount = Math.min(bbPlayer.chips, this.bigBlind);
|
||
bbPlayer.chips -= bbAmount;
|
||
bbPlayer.bet = bbAmount;
|
||
bbPlayer.totalBet = bbAmount;
|
||
this.pot += bbAmount;
|
||
this.currentBet = bbAmount;
|
||
bbPlayer.lastAction = `BB ${bbAmount}`;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Раздать карманные карты
|
||
*/
|
||
dealHoleCards() {
|
||
for (const player of this.getPlayersWithChips()) {
|
||
player.hand = this.deck.deal(2);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Установить первого игрока для хода
|
||
*/
|
||
setFirstPlayer() {
|
||
if (this.gamePhase === 'preflop') {
|
||
// UTG - после BB
|
||
const bbPlayer = this.players.find(p => p.isBigBlind);
|
||
const bbIndex = this.players.indexOf(bbPlayer);
|
||
this.currentPlayerIndex = this.getNextActivePlayerIndex(bbIndex);
|
||
} else {
|
||
// После флопа - первый активный после дилера
|
||
this.currentPlayerIndex = this.getNextActivePlayerIndex(this.dealerIndex);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Получить индекс следующего активного игрока
|
||
*/
|
||
getNextActivePlayerIndex(fromIndex) {
|
||
let index = (fromIndex + 1) % this.players.length;
|
||
let attempts = 0;
|
||
|
||
while (attempts < this.players.length) {
|
||
const player = this.players[index];
|
||
if (!player.folded && player.chips >= 0 && !player.allIn) {
|
||
return index;
|
||
}
|
||
index = (index + 1) % this.players.length;
|
||
attempts++;
|
||
}
|
||
|
||
return -1;
|
||
}
|
||
|
||
/**
|
||
* Текущий игрок
|
||
*/
|
||
getCurrentPlayer() {
|
||
return this.players[this.currentPlayerIndex];
|
||
}
|
||
|
||
/**
|
||
* Получить доступные действия для игрока
|
||
*/
|
||
getAvailableActions(player) {
|
||
const actions = [];
|
||
const toCall = this.currentBet - player.bet;
|
||
|
||
// Фолд всегда доступен
|
||
actions.push('fold');
|
||
|
||
if (toCall === 0) {
|
||
// Можно чекнуть
|
||
actions.push('check');
|
||
|
||
// Можно поставить
|
||
if (player.chips > 0) {
|
||
actions.push('bet');
|
||
}
|
||
} else {
|
||
// Нужно колл или рейз
|
||
if (player.chips > toCall) {
|
||
actions.push('call');
|
||
actions.push('raise');
|
||
} else {
|
||
// Можно только колл (олл-ин)
|
||
actions.push('call');
|
||
}
|
||
}
|
||
|
||
// All-in всегда доступен если есть фишки
|
||
if (player.chips > 0) {
|
||
actions.push('allin');
|
||
}
|
||
|
||
return actions;
|
||
}
|
||
|
||
/**
|
||
* Обработать действие игрока
|
||
*/
|
||
processAction(playerId, action, amount = 0) {
|
||
const player = this.players.find(p => p.id === playerId);
|
||
if (!player) return { success: false, error: 'Игрок не найден' };
|
||
|
||
const currentPlayer = this.players[this.currentPlayerIndex];
|
||
if (currentPlayer.id !== playerId) {
|
||
return { success: false, error: 'Сейчас не ваш ход' };
|
||
}
|
||
|
||
const result = { success: true, action, amount: 0 };
|
||
|
||
switch (action) {
|
||
case 'fold':
|
||
player.folded = true;
|
||
player.lastAction = 'Фолд';
|
||
break;
|
||
|
||
case 'check':
|
||
if (player.bet < this.currentBet) {
|
||
return { success: false, error: 'Нельзя чекнуть' };
|
||
}
|
||
player.lastAction = 'Чек';
|
||
break;
|
||
|
||
case 'call':
|
||
const callAmount = Math.min(this.currentBet - player.bet, player.chips);
|
||
player.chips -= callAmount;
|
||
player.bet += callAmount;
|
||
player.totalBet += callAmount;
|
||
this.pot += callAmount;
|
||
result.amount = callAmount;
|
||
player.lastAction = `Колл ${callAmount}`;
|
||
|
||
if (player.chips === 0) {
|
||
player.allIn = true;
|
||
player.lastAction = 'Олл-ин';
|
||
}
|
||
break;
|
||
|
||
case 'bet':
|
||
if (this.currentBet > 0) {
|
||
return { success: false, error: 'Используйте рейз' };
|
||
}
|
||
if (amount < this.bigBlind) {
|
||
amount = this.bigBlind;
|
||
}
|
||
if (amount > player.chips) {
|
||
amount = player.chips;
|
||
}
|
||
|
||
player.chips -= amount;
|
||
player.bet = amount;
|
||
player.totalBet += amount;
|
||
this.pot += amount;
|
||
this.currentBet = amount;
|
||
this.lastRaiseAmount = amount;
|
||
this.minRaise = amount;
|
||
result.amount = amount;
|
||
player.lastAction = `Бет ${amount}`;
|
||
|
||
if (player.chips === 0) {
|
||
player.allIn = true;
|
||
player.lastAction = 'Олл-ин';
|
||
}
|
||
break;
|
||
|
||
case 'raise':
|
||
const toCall = this.currentBet - player.bet;
|
||
const minRaiseTotal = this.currentBet + this.lastRaiseAmount;
|
||
|
||
if (amount < minRaiseTotal && amount < player.chips + player.bet) {
|
||
amount = minRaiseTotal;
|
||
}
|
||
|
||
const raiseTotal = Math.min(amount, player.chips + player.bet);
|
||
const raiseAmount = raiseTotal - player.bet;
|
||
|
||
player.chips -= raiseAmount;
|
||
this.pot += raiseAmount;
|
||
this.lastRaiseAmount = raiseTotal - this.currentBet;
|
||
this.currentBet = raiseTotal;
|
||
player.bet = raiseTotal;
|
||
player.totalBet += raiseAmount;
|
||
result.amount = raiseTotal;
|
||
player.lastAction = `Рейз до ${raiseTotal}`;
|
||
|
||
if (player.chips === 0) {
|
||
player.allIn = true;
|
||
player.lastAction = 'Олл-ин';
|
||
}
|
||
break;
|
||
|
||
case 'allin':
|
||
const allInAmount = player.chips;
|
||
this.pot += allInAmount;
|
||
player.bet += allInAmount;
|
||
player.totalBet += allInAmount;
|
||
result.amount = player.bet;
|
||
|
||
if (player.bet > this.currentBet) {
|
||
this.lastRaiseAmount = player.bet - this.currentBet;
|
||
this.currentBet = player.bet;
|
||
}
|
||
|
||
player.chips = 0;
|
||
player.allIn = true;
|
||
player.lastAction = `Олл-ин ${player.bet}`;
|
||
break;
|
||
|
||
default:
|
||
return { success: false, error: 'Неизвестное действие' };
|
||
}
|
||
|
||
// Уведомляем о действии
|
||
this.onAction(player, action, result.amount);
|
||
|
||
// Проверка завершения раунда ставок
|
||
if (this.isBettingRoundComplete()) {
|
||
this.nextPhase();
|
||
} else {
|
||
this.moveToNextPlayer();
|
||
this.onUpdate();
|
||
this.checkAITurn();
|
||
}
|
||
|
||
return result;
|
||
}
|
||
|
||
/**
|
||
* Перейти к следующему игроку
|
||
*/
|
||
moveToNextPlayer() {
|
||
const nextIndex = this.getNextActivePlayerIndex(this.currentPlayerIndex);
|
||
if (nextIndex !== -1) {
|
||
this.currentPlayerIndex = nextIndex;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Проверить, ход ли ИИ
|
||
*/
|
||
checkAITurn() {
|
||
if (!this.isGameStarted) return;
|
||
|
||
const currentPlayer = this.getCurrentPlayer();
|
||
if (currentPlayer && currentPlayer.isAI) {
|
||
// Задержка для реалистичности
|
||
setTimeout(() => {
|
||
this.makeAIMove(currentPlayer);
|
||
}, 1000 + Math.random() * 1500);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Ход ИИ
|
||
*/
|
||
makeAIMove(player) {
|
||
if (!this.isGameStarted || player.folded || player.allIn) return;
|
||
|
||
const decision = pokerAI.makeDecision(player, this);
|
||
this.processAction(player.id, decision.action, decision.amount);
|
||
}
|
||
|
||
/**
|
||
* Проверить завершение раунда ставок
|
||
*/
|
||
isBettingRoundComplete() {
|
||
const activePlayers = this.getActivePlayers().filter(p => !p.allIn);
|
||
|
||
// Все фолднули кроме одного
|
||
if (this.getActivePlayers().length === 1) {
|
||
return true;
|
||
}
|
||
|
||
// Все активные (не all-in) игроки уравняли ставку
|
||
if (activePlayers.length === 0) {
|
||
return true;
|
||
}
|
||
|
||
// Все поставили одинаково и сделали действие
|
||
const allEqual = activePlayers.every(p => p.bet === this.currentBet);
|
||
const allActed = activePlayers.every(p => p.lastAction !== null &&
|
||
!p.lastAction.includes('SB') && !p.lastAction.includes('BB'));
|
||
|
||
return allEqual && allActed;
|
||
}
|
||
|
||
/**
|
||
* Переход к следующей фазе
|
||
*/
|
||
nextPhase() {
|
||
// Сброс ставок для новой улицы
|
||
for (const player of this.players) {
|
||
player.bet = 0;
|
||
if (!player.folded && !player.allIn) {
|
||
player.lastAction = null;
|
||
}
|
||
}
|
||
this.currentBet = 0;
|
||
|
||
const activePlayers = this.getActivePlayers();
|
||
|
||
// Один игрок остался - он победитель
|
||
if (activePlayers.length === 1) {
|
||
this.endHand([activePlayers[0]]);
|
||
return;
|
||
}
|
||
|
||
switch (this.gamePhase) {
|
||
case 'preflop':
|
||
this.gamePhase = 'flop';
|
||
this.communityCards = this.deck.deal(3);
|
||
break;
|
||
case 'flop':
|
||
this.gamePhase = 'turn';
|
||
this.communityCards.push(...this.deck.deal(1));
|
||
break;
|
||
case 'turn':
|
||
this.gamePhase = 'river';
|
||
this.communityCards.push(...this.deck.deal(1));
|
||
break;
|
||
case 'river':
|
||
this.gamePhase = 'showdown';
|
||
this.determineWinner();
|
||
return;
|
||
}
|
||
|
||
// Установка первого игрока для новой улицы
|
||
this.setFirstPlayer();
|
||
|
||
this.onUpdate();
|
||
|
||
// Если все в all-in или только один активный, автоматически переходим
|
||
const nonAllInPlayers = this.getActivePlayers().filter(p => !p.allIn);
|
||
if (nonAllInPlayers.length <= 1) {
|
||
setTimeout(() => this.nextPhase(), 1500);
|
||
} else {
|
||
this.checkAITurn();
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Определить победителя
|
||
*/
|
||
determineWinner() {
|
||
const activePlayers = this.getActivePlayers();
|
||
|
||
if (activePlayers.length === 1) {
|
||
this.endHand([activePlayers[0]]);
|
||
return;
|
||
}
|
||
|
||
// Оценка рук
|
||
const playerHands = activePlayers.map(player => ({
|
||
player,
|
||
handResult: evaluateHand([...player.hand, ...this.communityCards])
|
||
}));
|
||
|
||
// Сортировка по силе руки
|
||
playerHands.sort((a, b) => {
|
||
if (b.handResult.rank !== a.handResult.rank) {
|
||
return b.handResult.rank - a.handResult.rank;
|
||
}
|
||
for (let i = 0; i < a.handResult.kickers.length; i++) {
|
||
if (b.handResult.kickers[i] !== a.handResult.kickers[i]) {
|
||
return b.handResult.kickers[i] - a.handResult.kickers[i];
|
||
}
|
||
}
|
||
return 0;
|
||
});
|
||
|
||
// Находим победителей (может быть сплит)
|
||
const winners = [playerHands[0]];
|
||
for (let i = 1; i < playerHands.length; i++) {
|
||
const cmp = this.compareHands(playerHands[0].handResult, playerHands[i].handResult);
|
||
if (cmp === 0) {
|
||
winners.push(playerHands[i]);
|
||
} else {
|
||
break;
|
||
}
|
||
}
|
||
|
||
this.endHand(
|
||
winners.map(w => w.player),
|
||
playerHands.map(ph => ({ player: ph.player, hand: ph.handResult }))
|
||
);
|
||
}
|
||
|
||
/**
|
||
* Сравнение двух рук
|
||
*/
|
||
compareHands(h1, h2) {
|
||
if (h1.rank !== h2.rank) return h2.rank - h1.rank;
|
||
for (let i = 0; i < h1.kickers.length; i++) {
|
||
if (h1.kickers[i] !== h2.kickers[i]) {
|
||
return h2.kickers[i] - h1.kickers[i];
|
||
}
|
||
}
|
||
return 0;
|
||
}
|
||
|
||
/**
|
||
* Завершить раздачу
|
||
*/
|
||
endHand(winners, allHands = null) {
|
||
const winAmount = Math.floor(this.pot / winners.length);
|
||
|
||
for (const winner of winners) {
|
||
winner.chips += winAmount;
|
||
}
|
||
|
||
// Остаток при нечётном сплите
|
||
const remainder = this.pot % winners.length;
|
||
if (remainder > 0) {
|
||
winners[0].chips += remainder;
|
||
}
|
||
|
||
this.gamePhase = 'showdown';
|
||
this.isGameStarted = false;
|
||
|
||
const result = {
|
||
winners: winners.map(w => ({
|
||
id: w.id,
|
||
name: w.name,
|
||
amount: winAmount,
|
||
hand: allHands ? allHands.find(h => h.player.id === w.id)?.hand : null
|
||
})),
|
||
pot: this.pot,
|
||
hands: allHands
|
||
};
|
||
|
||
this.onHandEnd(result);
|
||
this.onUpdate();
|
||
|
||
return result;
|
||
}
|
||
}
|
||
|
||
// =============================================================================
|
||
// ОЦЕНКА РУК
|
||
// =============================================================================
|
||
|
||
/**
|
||
* Оценить покерную руку из 7 карт
|
||
*/
|
||
function evaluateHand(cards) {
|
||
const allCombinations = getCombinations(cards, 5);
|
||
let bestHand = null;
|
||
|
||
for (const combo of allCombinations) {
|
||
const hand = evaluateFiveCards(combo);
|
||
if (!bestHand || compareHandResults(hand, bestHand) > 0) {
|
||
bestHand = hand;
|
||
}
|
||
}
|
||
|
||
return bestHand;
|
||
}
|
||
|
||
/**
|
||
* Получить все комбинации из n элементов
|
||
*/
|
||
function getCombinations(arr, n) {
|
||
if (n === 0) return [[]];
|
||
if (arr.length === 0) return [];
|
||
|
||
const [first, ...rest] = arr;
|
||
const withFirst = getCombinations(rest, n - 1).map(c => [first, ...c]);
|
||
const withoutFirst = getCombinations(rest, n);
|
||
|
||
return [...withFirst, ...withoutFirst];
|
||
}
|
||
|
||
/**
|
||
* Оценить 5 карт
|
||
*/
|
||
function evaluateFiveCards(cards) {
|
||
const sortedCards = [...cards].sort((a, b) => b.value - a.value);
|
||
const values = sortedCards.map(c => c.value);
|
||
const suits = sortedCards.map(c => c.suit);
|
||
|
||
const isFlush = suits.every(s => s === suits[0]);
|
||
const isStraight = checkStraight(values);
|
||
const isWheel = values.join(',') === '14,5,4,3,2'; // A-2-3-4-5
|
||
|
||
const valueCounts = {};
|
||
for (const v of values) {
|
||
valueCounts[v] = (valueCounts[v] || 0) + 1;
|
||
}
|
||
|
||
const counts = Object.values(valueCounts).sort((a, b) => b - a);
|
||
const uniqueValues = Object.keys(valueCounts)
|
||
.map(Number)
|
||
.sort((a, b) => {
|
||
if (valueCounts[b] !== valueCounts[a]) {
|
||
return valueCounts[b] - valueCounts[a];
|
||
}
|
||
return b - a;
|
||
});
|
||
|
||
// Рояль-флеш
|
||
if (isFlush && isStraight && values[0] === 14) {
|
||
return { rank: 10, name: 'Роял-флеш', kickers: values };
|
||
}
|
||
|
||
// Стрит-флеш
|
||
if (isFlush && (isStraight || isWheel)) {
|
||
return { rank: 9, name: 'Стрит-флеш', kickers: isWheel ? [5, 4, 3, 2, 1] : values };
|
||
}
|
||
|
||
// Каре
|
||
if (counts[0] === 4) {
|
||
return { rank: 8, name: 'Каре', kickers: uniqueValues };
|
||
}
|
||
|
||
// Фулл-хаус
|
||
if (counts[0] === 3 && counts[1] === 2) {
|
||
return { rank: 7, name: 'Фулл-хаус', kickers: uniqueValues };
|
||
}
|
||
|
||
// Флеш
|
||
if (isFlush) {
|
||
return { rank: 6, name: 'Флеш', kickers: values };
|
||
}
|
||
|
||
// Стрит
|
||
if (isStraight || isWheel) {
|
||
return { rank: 5, name: 'Стрит', kickers: isWheel ? [5, 4, 3, 2, 1] : values };
|
||
}
|
||
|
||
// Сет
|
||
if (counts[0] === 3) {
|
||
return { rank: 4, name: 'Сет', kickers: uniqueValues };
|
||
}
|
||
|
||
// Две пары
|
||
if (counts[0] === 2 && counts[1] === 2) {
|
||
return { rank: 3, name: 'Две пары', kickers: uniqueValues };
|
||
}
|
||
|
||
// Пара
|
||
if (counts[0] === 2) {
|
||
return { rank: 2, name: 'Пара', kickers: uniqueValues };
|
||
}
|
||
|
||
// Старшая карта
|
||
return { rank: 1, name: 'Старшая карта', kickers: values };
|
||
}
|
||
|
||
/**
|
||
* Проверка на стрит
|
||
*/
|
||
function checkStraight(values) {
|
||
for (let i = 0; i < values.length - 1; i++) {
|
||
if (values[i] - values[i + 1] !== 1) {
|
||
return false;
|
||
}
|
||
}
|
||
return true;
|
||
}
|
||
|
||
/**
|
||
* Сравнение результатов рук
|
||
*/
|
||
function compareHandResults(h1, h2) {
|
||
if (h1.rank !== h2.rank) return h1.rank - h2.rank;
|
||
for (let i = 0; i < h1.kickers.length; i++) {
|
||
if (h1.kickers[i] !== h2.kickers[i]) {
|
||
return h1.kickers[i] - h2.kickers[i];
|
||
}
|
||
}
|
||
return 0;
|
||
}
|
||
|
||
/**
|
||
* Получить силу руки для отображения
|
||
*/
|
||
function getHandStrength(cards, communityCards) {
|
||
if (cards.length < 2) return null;
|
||
|
||
const allCards = [...cards];
|
||
if (communityCards && communityCards.length > 0) {
|
||
allCards.push(...communityCards);
|
||
}
|
||
|
||
if (allCards.length < 5) {
|
||
// Только карманные карты - оцениваем префлоп силу
|
||
return getPreflopHandName(cards);
|
||
}
|
||
|
||
const hand = evaluateHand(allCards);
|
||
return hand.name;
|
||
}
|
||
|
||
/**
|
||
* Получить название префлоп руки
|
||
*/
|
||
function getPreflopHandName(cards) {
|
||
if (cards.length !== 2) return '';
|
||
|
||
const [c1, c2] = cards;
|
||
const highCard = c1.value > c2.value ? c1 : c2;
|
||
const lowCard = c1.value > c2.value ? c2 : c1;
|
||
const suited = c1.suit === c2.suit;
|
||
|
||
let name = '';
|
||
|
||
if (c1.value === c2.value) {
|
||
name = `Пара ${c1.rank}`;
|
||
} else {
|
||
name = `${highCard.rank}${lowCard.rank}${suited ? 's' : 'o'}`;
|
||
}
|
||
|
||
return name;
|
||
}
|
||
|
||
// Экспорт для использования в других модулях
|
||
if (typeof module !== 'undefined' && module.exports) {
|
||
module.exports = { Card, Deck, Player, PokerGame, evaluateHand, getHandStrength };
|
||
}
|