/**
* =============================================================================
* 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 };
}