UNPKG

@idealic/poker-engine

Version:

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

198 lines 7.89 kB
export function cloneState(state) { return { ...state, players: state.players.map(p => ({ ...p, cards: [...(p.cards || [])] })), board: [...state.board], }; } /** * Creates a sanitized copy of the table state where cards are hidden for players who haven't shown them. * This is used when sharing table state with players to maintain game integrity. * Cards are only visible if: * 1. The player has shown their cards (hasShownCards is true) * 2. The hand is complete and the player hasn't folded * @param table The table state to sanitize * @param playerPosition Optional position of the player viewing the table. If provided, their cards will be visible. */ export function sanitizeTable(game, playerPosition) { const sanitized = cloneState(game); sanitized.players = sanitized.players.map((player, index) => { // Hide cards unless: // 1. This is the viewing player's position // 2. Player has shown cards // 3. Hand is complete and player hasn't folded const shouldShowCards = index === playerPosition || player.hasShownCards || (game.isHandComplete && !player.hasFolded); return { ...player, cards: shouldShowCards ? player.cards : player.cards?.map(() => '??'), }; }); return sanitized; } /** @todo: implement this */ export function formatAction(action) { return String(action); } /** * Applies an action to a game state. * @param game - The game state to apply the action to * @param action - The action to apply * @returns The updated game state */ export function applyGameAction(hand, action) { return { ...hand, actions: [...hand.actions, action], }; } /** * Intelligently merges hole card actions, combining card information * @param action1 - First action (may have ???? for hidden cards) * @param action2 - Second action (may have actual cards) * @returns Merged action with most complete card information */ function mergeHoleCardActions(action1, action2) { const match1 = action1.match(/^d\s+dh\s+p(\d+)\s+(.+)$/); const match2 = action2.match(/^d\s+dh\s+p(\d+)\s+(.+)$/); if (!match1 || !match2 || match1[1] !== match2[1]) { // Not hole card actions or different players - return the second action return action2; } const cards1 = match1[2].split('#')[0].trim(); const cards2 = match2[2].split('#')[0].trim(); // If one has ???? and the other has actual cards, use the actual cards if (cards1 === '????') { return action2; } if (cards2 === '????') { return action1; } // Both have actual cards - use the second one (newer information) return action2; } /** * Tries to merge two game states, assuming they are from the same table, have common players and are in the same hand. * @param oldGame - The first game state * @param newGame - The second game state * @returns The merged game state */ export function mergeGames(oldHand, newHand) { // If games are from different hands – keep the most recent one based on hand number if (oldHand.hand !== newHand.hand) { return (oldHand.hand ?? 0) > (newHand.hand ?? 0) ? oldHand : newHand; } const oldActions = oldHand.actions; const newActions = newHand.actions; // Create a map to track the best version of each action const actionMap = new Map(); // Helper function to generate consistent action keys with position context const getActionKey = (action, actionIndex) => { const baseAction = action.split('#')[0].trim(); // Remove timestamp/comments // For hole card actions, use player-based key (d dh pX) to enable merging const holeCardMatch = baseAction.match(/^d\s+dh\s+p(\d+)\s+/); if (holeCardMatch) { return `d dh p${holeCardMatch[1]}`; } // For other actions, include position to differentiate identical actions return `${actionIndex}:${baseAction}`; }; // Process old actions first for (let i = 0; i < oldActions.length; i++) { const action = oldActions[i]; const actionKey = getActionKey(action, i); actionMap.set(actionKey, action); } // Process new actions, intelligently merging hole card actions for (let i = 0; i < newActions.length; i++) { const action = newActions[i]; const actionKey = getActionKey(action, i); const existingAction = actionMap.get(actionKey); if (existingAction && actionKey.startsWith('d dh p')) { // This is a hole card action - merge intelligently const mergedAction = mergeHoleCardActions(existingAction, action); actionMap.set(actionKey, mergedAction); } else if (existingAction) { // For non-hole-card actions that exist, keep the existing version // (already in the map, no action needed) } else { // For new actions, add them actionMap.set(actionKey, action); } } // Convert back to ordered list, always starting with old actions first const mergedActions = []; const usedKeys = new Set(); // First, add all old actions in order for (let i = 0; i < oldActions.length; i++) { const action = oldActions[i]; const actionKey = getActionKey(action, i); if (!usedKeys.has(actionKey)) { mergedActions.push(actionMap.get(actionKey) || action); usedKeys.add(actionKey); } } // Then, add new actions that aren't already included for (let i = 0; i < newActions.length; i++) { const action = newActions[i]; const actionKey = getActionKey(action, i); if (!usedKeys.has(actionKey)) { mergedActions.push(actionMap.get(actionKey) || action); usedKeys.add(actionKey); } } // Remove consecutive duplicate actions const deduplicatedActions = []; for (let i = 0; i < mergedActions.length; i++) { const currentAction = mergedActions[i]; const previousAction = deduplicatedActions[deduplicatedActions.length - 1]; // Only add if it's different from the previous action if (currentAction !== previousAction) { deduplicatedActions.push(currentAction); } } // Start with the newer game (it might contain fresher meta-data) and override the // actions with the merged list. const mergedHand = { ...newHand, actions: deduplicatedActions, }; // If the two games were authored by different people, clear the author field to // avoid mis-attribution. if (oldHand.author && newHand.author && oldHand.author !== newHand.author) { delete mergedHand.author; } return mergedHand; } export function getGameId(hand) { if (hand.id) return hand.id; return String((hand.table ? hand.table + '-' : '') + hand.hand); } /** * Replaces the cards of a player in the game actions. * @param game - The game to modify * @param playerId - The ID of the player to replace cards for * @param playerName - The name of the player to replace cards for * @param cards - The new cards to replace with */ export function replacePlayerCards(hand, playerId, playerName, cards) { const heroIndex = !!hand?._venueIds ? ((hand?._venueIds).findIndex(pId => pId === playerId) ?? -1) : -1; const heroDhAction = `d dh p${heroIndex + 1} ${cards.join('')} #0 ${playerName}`; return { ...hand, actions: hand.actions.map(action => { // wtf: replacing all actions by all players? at least scope it to d dh action if (action.includes(`p${heroIndex + 1}`)) { return heroDhAction; } return action; }), }; } //# sourceMappingURL=state.js.map