pets1/public/game.js

913 lines
28 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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