UNPKG

@idealic/poker-engine

Version:

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

257 lines 9.36 kB
import { isAwaitingDealer, isPlayerEligibleToAct } from './validation'; /** * Extracts specific player index from action (1-based) * @param action - The action string containing a player index * @returns Player index or undefined if no player index is present */ export function getActionPlayerIndex(action) { const prefix = 'p'; const prefixIndex = action?.indexOf(prefix); if (prefixIndex !== -1) { const numberStart = prefixIndex + 1; const numberEnd = action.indexOf(' ', numberStart); const playerNumber = action.substring(numberStart, numberEnd === -1 ? action.length : numberEnd); return parseInt(playerNumber, 10) - 1; } return undefined; } /** * Extracts phh action type from action * @param action - The action string containing an action type * @returns Action type or '?' if no action type is present */ export function getActionType(action) { if (action != null) { if (action.startsWith('d ')) { return action.substring(2, action.indexOf(' ', 2) === -1 ? action.length : action.indexOf(' ', 2)); } const spaceIndex = action.indexOf(' '); if (spaceIndex !== -1) { const typeStart = spaceIndex + 1; const typeEnd = action.indexOf(' ', typeStart); return action.substring(typeStart, typeEnd === -1 ? action.length : typeEnd); } } return '?'; } /** * Extracts comment from action * @param action - The action string containing a comment * @returns Comment or undefined if no comment is present */ export function getActionComment(action) { const commentIndex = action.indexOf('#'); if (commentIndex !== -1) { return action.substring(commentIndex + 1); } return undefined; } /** * Extracts timestamp from action and its comment * @param action - The action string containing a comment * @returns Timestamp or undefined if no timestamp is present */ export function getActionTimestamp(action, useDefault = true) { const comment = getActionComment(action); const timestamp = parseInt(comment?.match(/^\d{13}$/)?.[0] ?? '0'); return timestamp || (useDefault ? Date.now() : null); } /** * Adds or replaces timestamp in an action * @param action - The action string to timestamp * @param timestamp - Optional timestamp (defaults to Date.now()) * @returns Action with timestamp appended as comment */ export function timestampAction(action, timestamp) { if (action == null || typeof action !== 'string') { return action; } // Remove existing timestamp comment if present and trim whitespace const commentIndex = action.indexOf('#'); const baseAction = commentIndex !== -1 ? action.substring(0, commentIndex).trim() : action.trim(); // Use provided timestamp or current time const ts = timestamp ?? Date.now(); // Handle empty base action case if (baseAction === '') { return `#${ts}`; } // Append timestamp as comment return `${baseAction} #${ts}`; } /** * Extracts cards from dealer actions * @param action - The action string containing dealer actions * @returns Array of cards or undefined if no cards are present */ export function getActionCards(action) { const cards = []; const matches = action.match(/(?:d dh p\d+|d db|p\d+ sm) ([A-Z0-9?]{2}(?:[A-Z0-9?]{2})*)/i); if (matches) { const cardString = matches[1]; for (let i = 0; i < cardString.length; i += 2) { cards.push(cardString.substring(i, i + 2)); } } return cards.length ? cards : undefined; } /** * Extracts amount from action * @param action - The action string containing an amount * @returns Amount or 0 if no amount is present */ export function getActionAmount(action) { const amountString = action.match(/^[^#]+ ([\d.]+)/)?.[1]; const amount = parseFloat(amountString ?? '0'); return isNaN(amount) ? 0 : amount; } /** * Finds next eligible player to act starting from given index. * This function ONLY checks player flags and does not deal with betting logic. * Returns -1 if no eligible player found. * * @instructions * This function ONLY checks player eligibility based on: * 1. Not folded (hasFolded flag) * 2. Not all-in (isAllIn flag) * 3. Circular search (wraps around table) * 4. Stops if full circle made (no eligible found) * * It does NOT: * - Check betting amounts * - Handle betting logic * - Reset any flags * - Deal with positions * - Consider street or game state * * The function relies on: * - Player flags being accurate (hasFolded, isAllIn) * - Valid startIndex within table size * - isPlayerEligibleToAct helper for flag checks */ export function getNextEligiblePlayerIndex(game, startIndex) { const numPlayers = game.players.length; // Condition 1 & 2: Start search from next position let pos = (startIndex + 1) % numPlayers; // Condition 3: Search circularly until we're back to start while (pos !== startIndex) { const player = game.players[pos]; // Check eligibility using helper (not folded, not all-in) if (isPlayerEligibleToAct(player)) { return pos; } // Move to next position circularly pos = (pos + 1) % numPlayers; } // Condition 4: No eligible player found after full circle return -1; } export function getCurrentEligiblePlayerIndex(game, playerIndex) { if (playerIndex === -1) { return -1; } if (isPlayerEligibleToAct(game.players[playerIndex])) { return playerIndex; } return getNextEligiblePlayerIndex(game, playerIndex); } /** * @instructions * Returns the theoretical first player to act for a given street based ONLY on positions. * This function ignores current betting state, folded status, or all-in status. * For preflop: * - In heads-up: BTN/SB acts first * - In ring game: UTG (next after BB) acts first * For postflop streets: first position after button. */ export function findFirstToActForStreet(game, street) { const isHeadsUp = getActivePlayers(game).length === 2; if (street === 'preflop') { // In heads-up, BTN/SB acts first if (isHeadsUp) { return game.buttonIndex; } // In ring games, UTG (next after BB) acts first const bigBlindIndex = game.bigBlindIndex; return (bigBlindIndex + 1) % game.players.length; } // For postflop streets, first position after button return (game.buttonIndex + 1) % game.players.length; } /** * Gets the index of the next player who should act * Returns -1 if no player can act or if a dealer action should happen next * * @instructions * This function ONLY determines next player to act based on: * 1. Betting complete (no one can act) * 2. Dealer intervention needed (no one can act) * 3. Single player or all all-in (no one can act) * 4. New street start (first active player after button) * 5. After last action (next after last acted) * * It does NOT: * - Reset player flags * - Handle betting logic * - Compare bet amounts * - Deal with side pots * * The function relies on: * - hasActed flag being properly managed by action handlers * - bettingComplete flag being set correctly * - Player states (hasFolded, isAllIn) being accurate * - Last action being tracked correctly */ export function getPlayerIndex(game) { // Condition 1: Betting complete (no one can act) if (game.isBettingComplete) { return -1; } // Condition 2: Dealer intervention needed (no one can act) if (isAwaitingDealer(game)) { return -1; } // Get active players (not folded) const activePlayers = getRemainingPlayers(game); // Condition 3: Single player or all all-in (no one can act) if (activePlayers.length <= 1 || activePlayers.every(p => p.isAllIn)) { return -1; } // Condition 4: New street start (first active player after button) if (activePlayers.every(p => !p.hasActed) || !game.lastPlayerAction) { return getCurrentEligiblePlayerIndex(game, findFirstToActForStreet(game, game.street)); } // Condition 5: After last action (next after last acted) const lastActedIndex = getActionPlayerIndex(game.lastPlayerAction); if (lastActedIndex === undefined) return -1; return getNextEligiblePlayerIndex(game, lastActedIndex); } export function getRemainingPlayers(game) { return getActivePlayers(game).filter(p => !p.hasFolded); } export function getActivePlayers(game) { return game.players.filter(p => !p.isInactive); } /** * Returns the index of the player with the given name */ export function getPlayerIndexFromName(game, name) { return game.players.findIndex(p => p.name === name); } /** * Determines the button position based on blinds structure. * In heads-up, button is SB. Otherwise, button is one position before SB. */ export function getButtonIndex(players, blinds) { const sbIndex = blinds.indexOf(Math.min(...blinds.filter(b => b > 0))); if (players === 2) { // In heads-up, button is SB position return sbIndex; } // Find SB position (smallest non-zero blind) if (sbIndex === -1) return 0; // Button is one position before SB return (sbIndex - 1 + players) % players; } //# sourceMappingURL=position.js.map