@idealic/poker-engine
Version:
Professional poker game engine and hand evaluator with built-in iterator utilities
257 lines • 9.36 kB
JavaScript
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