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