/** * ============================================================================= * 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 = ` ${this.rank} ${SUIT_SYMBOLS[this.suit]} `; } 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 }; }