UNPKG

@idealic/poker-engine

Version:

Professional poker game engine and hand evaluator with built-in iterator utilities

159 lines 7.05 kB
import { recordStatsAfter, recordStatsBefore } from '../stats/stats'; import { getActionAmount, getActionCards, getActionPlayerIndex, getActionTimestamp, getActionType, getPlayerIndex, getRemainingPlayers, } from '../utils/position'; import { makeBet, matchBet } from './betting'; import { completeHand, resetForNewStreet } from './showdown'; /** * @instructions * Button-SB-BB vs. seat indices: Don't hard-code that the BB always acts first postflop. Instead, figure out who the button is, then the first active seat to the left of the button. * Action order can change street by street: Preflop order often starts left of the BB; postflop starts left of the button. * Edge cases with short-handed tables: Heads-up is special; the SB is on the button. * All-in/fold skipping: Ensure you skip players who are all-in or folded. * Street transitions: When you move from one street to the next, track who should act first. * Detecting next player: Don't just "++index % numPlayers." * should NOT mutate data */ export function getCurrentPlayerIndex(game) { return getPlayerIndex(game) ?? -1; } /** * Determines if action is a dealer action */ export function isDealerAction(action) { return action.startsWith('d '); } /** * Determines if action is a player action */ export function isPlayerAction(action, index) { return action?.startsWith('p' + (index == null ? '' : index + 1)) ?? false; } // Parse & apply a single action line to the Game // Returns a new Game object without mutating the original export function applyAction(game, action) { // Create a deep clone of the game object to ensure immutability const newGame = JSON.parse(JSON.stringify(game)); const playerIndex = getActionPlayerIndex(action); const cards = getActionCards(action) || []; const actionType = getActionType(action); if (isDealerAction(action) || actionType === 'sm') { if (newGame.nextPlayerIndex != -1) { throw new Error(`It's not the dealer's turn to deal, nextPlayerIndex is ${newGame.nextPlayerIndex}. Action: ${action}`); } } else if (actionType != 'm') { if (newGame.nextPlayerIndex != playerIndex) { console.error('error', JSON.stringify(newGame, null, 2)); throw new Error(`It's not the player's turn to act, nextPlayerIndex is ${newGame.nextPlayerIndex} but the action is for player ${playerIndex}. Action: ${action}. Before: ${newGame.lastAction}`); } } // Store the action newGame.lastAction = action; if (isDealerAction(action)) { // Dealer action if (actionType === 'dh' && playerIndex != null) { // Deal hole cards if (playerIndex >= 0 && playerIndex < newGame.players.length) { newGame.players[playerIndex].cards = cards; newGame.usedCards += cards.length; } } else if (actionType === 'db') { // Deal board cards newGame.board = [...newGame.board, ...cards]; newGame.usedCards += cards.length; // Update street based on number of board cards if (newGame.board.length === 3) { newGame.street = 'flop'; } else if (newGame.board.length === 4) { newGame.street = 'turn'; } else if (newGame.board.length === 5) { newGame.street = 'river'; } // Reset state for new street resetForNewStreet(newGame); } } else if (actionType === 'sm' && playerIndex != null) { // Show cards at showdown const currentPlayer = newGame.players[playerIndex]; const cards = getActionCards(action); if (cards) { currentPlayer.hasShownCards = true; currentPlayer.cards = cards; } currentPlayer.hasActed = true; newGame.isShowdown = true; // Complete hand after all active players have shown cards const activePlayers = getRemainingPlayers(newGame); if (activePlayers.every(p => p.hasShownCards) && newGame.street === 'river') { completeHand(newGame); } } else if (playerIndex != null) { const currentPlayer = newGame.players[playerIndex]; if (actionType === 'f') { // Fold currentPlayer.hasFolded = true; currentPlayer.hasActed = true; newGame.lastPlayerAction = action; // Check if everyone but one player has folded const activePlayers = getRemainingPlayers(newGame); if (activePlayers.length === 1) { completeHand(newGame); } } else if (actionType === 'cc') { // Call or check matchBet(newGame, playerIndex, newGame.bet); newGame.lastPlayerAction = action; } else if (actionType === 'cbr') { // Bet or raise makeBet(newGame, playerIndex, getActionAmount(action) ?? 0); newGame.lastPlayerAction = action; } } // After any player action, check if betting is complete const activePlayers = getRemainingPlayers(newGame); const allPlayersHaveActed = activePlayers.every(p => p.hasActed || p.isAllIn) || // only one non all-in player left in showdown activePlayers.filter(p => !p.isAllIn && p.currentBet === newGame.bet).length == 1; const allPlayersHaveMatchedBet = activePlayers.every(p => p.isAllIn || p.currentBet === newGame.bet); // Update bettingComplete state and check for hand completion newGame.isBettingComplete = (allPlayersHaveActed && allPlayersHaveMatchedBet) || activePlayers.length == 1; // Complete hand if: // 1. Only one player remains // 2. All players are all-in and have acted // 3. Betting is complete on the river and all active players have shown cards if (newGame.board.length === 5 || activePlayers.length === 1) { if (activePlayers.length === 1 || // (activePlayers.every(p => p.isAllIn) && allPlayersHaveActed) || (newGame.isBettingComplete && newGame.street === 'river' && activePlayers.every(p => p.hasShownCards))) { completeHand(newGame); } } newGame.nextPlayerIndex = getCurrentPlayerIndex(newGame); if (isPlayerAction(action)) { // Record stats after the action is applied recordStatsAfter(newGame, action); } // First, record stats BEFORE the action is applied if (newGame.nextPlayerIndex != -1) { recordStatsBefore(newGame, newGame.nextPlayerIndex); } // Record the timestamp of the action newGame.lastTimestamp = getActionTimestamp(action) || Date.now(); return newGame; } /** * Determines if the game is in its initial state with no actions taken */ export function isGameJustStarted(hand) { return hand.actions.length === 0 && hand.blindsOrStraddles.length === 0; } //# sourceMappingURL=processor.js.map