UNPKG

@idealic/poker-engine

Version:

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

242 lines 9.47 kB
import { parseHand } from './formats/pokerstars'; import { phhToNarrative } from './formats/narrative'; import { mergeGames } from './utils/state'; /** * Hand constructor/guard function that creates a validated Hand object. * Acts as a factory function with sensible defaults and validation. * * @param props - Partial Hand properties to create a complete Hand * @returns A validated Hand object * * @example * const hand = Hand({ * variant: 'NT', * players: ['Alice', 'Bob'], * startingStacks: [1000, 1000], * blindsOrStraddles: [10, 20], * minBet: 20 * }); */ export function Hand(props) { // Provide sensible defaults const defaults = { variant: 'NT', currency: 'USD', actions: [], time: new Date().toISOString(), timeZone: 'UTC', }; // Merge props with defaults const hand = { ...defaults, ...props }; // Validate required fields if (!hand.players || !Array.isArray(hand.players) || hand.players.length === 0) { throw new Error('Hand requires a non-empty players array'); } if (!hand.startingStacks || !Array.isArray(hand.startingStacks)) { throw new Error('Hand requires startingStacks array'); } if (hand.startingStacks.length !== hand.players.length) { throw new Error('startingStacks must match players array length'); } if (!hand.blindsOrStraddles || !Array.isArray(hand.blindsOrStraddles)) { throw new Error('Hand requires blindsOrStraddles array'); } if (hand.blindsOrStraddles.length !== hand.players.length) { throw new Error('blindsOrStraddles must match players array length'); } if (!hand.antes || !Array.isArray(hand.antes)) { // Provide default antes if not specified hand.antes = new Array(hand.players.length).fill(0); } if (hand.antes.length !== hand.players.length) { throw new Error('antes must match players array length'); } // Variant-specific validation const noLimitVariants = ['NT', 'NS', 'PO', 'N2L1D']; const fixedLimitVariants = [ 'FT', 'FO/8', 'F7S', 'F7S/8', 'FR', 'F2L3D', 'FB', ]; const studVariants = ['F7S', 'F7S/8', 'FR']; if (noLimitVariants.includes(hand.variant)) { if (typeof hand.minBet !== 'number' || hand.minBet <= 0) { throw new Error(`No-limit variant ${hand.variant} requires positive minBet`); } // Ensure other betting structure fields are not present delete hand.smallBet; delete hand.bigBet; delete hand.bringIn; } else if (studVariants.includes(hand.variant)) { if (typeof hand.smallBet !== 'number' || hand.smallBet <= 0) { throw new Error(`Stud variant ${hand.variant} requires positive smallBet`); } if (typeof hand.bigBet !== 'number' || hand.bigBet <= 0) { throw new Error(`Stud variant ${hand.variant} requires positive bigBet`); } if (typeof hand.bringIn !== 'number' || hand.bringIn <= 0) { throw new Error(`Stud variant ${hand.variant} requires positive bringIn`); } // Ensure minBet is not present delete hand.minBet; } else if (fixedLimitVariants.includes(hand.variant)) { if (typeof hand.smallBet !== 'number' || hand.smallBet <= 0) { throw new Error(`Fixed-limit variant ${hand.variant} requires positive smallBet`); } if (typeof hand.bigBet !== 'number' || hand.bigBet <= 0) { throw new Error(`Fixed-limit variant ${hand.variant} requires positive bigBet`); } // Ensure other betting structure fields are not present delete hand.minBet; delete hand.bringIn; } else { throw new Error(`Unknown variant: ${hand.variant}`); } return hand; } /** * Hand namespace for methods that operate on Hand objects. */ (function (Hand) { /** * Parse a hand from string format (PokerStars or JSON) * @param input - String representation of the hand * @param format - Format type ('pokerstars' or 'json') * @returns Parsed Hand object */ Hand.parse = parseHand; /** * Export a hand with an optional player perspective, censoring cards appropriately * @param hand - The hand to export * @param playerId - Optional player ID for perspective (shows only their cards and shown cards) * @returns The hand with cards censored from the player's perspective */ function output(hand, playerId) { if (playerId === undefined) { return hand; } // Add author field const result = { ...hand, author: playerId }; // Find the player's position (p1, p2, etc.) const authorPlayerIndex = hand.players.indexOf(playerId); if (authorPlayerIndex === -1) { // Invalid player ID, return unchanged but with author return result; } const authorPlayerPosition = authorPlayerIndex + 1; // Convert to 1-based position // Track which players have shown their cards const playersWhoShowed = new Set(); for (const action of hand.actions) { // Look for show/muck actions: "p{n} sm {cards}" const showMatch = action.match(/^p(\d+)\s+sm\s+/); if (showMatch) { const playerPosition = parseInt(showMatch[1]); playersWhoShowed.add(playerPosition); } } // Process actions to censor cards const censoredActions = hand.actions.map(action => { // Look for dealer hole card actions: "d dh p{n} {cards}" const dealMatch = action.match(/^d\s+dh\s+p(\d+)\s+(.+)$/); if (dealMatch) { const playerPosition = parseInt(dealMatch[1]); const cards = dealMatch[2]; // Show cards if: // 1. It's the author player's cards // 2. The player has shown their cards if (playerPosition === authorPlayerPosition || playersWhoShowed.has(playerPosition)) { return action; // Keep original action } else { // Hide cards with ???? return `d dh p${playerPosition} ????`; } } // Return all other actions unchanged return action; }); return { ...result, actions: censoredActions }; } Hand.output = output; /** * Check if two hands are equal * @param oldHand - First hand to compare * @param newHand - Second hand to compare * @returns True if hands are equal, false otherwise */ function isHandsEqual(oldHand, newHand) { // If both hands are the same reference, they're identical if (oldHand === newHand) { return true; } // If either hand is null/undefined, they're equal only if both are exactly the same falsy value if (!oldHand || !newHand) { return oldHand === newHand; } // Compare the entire object structure for equality // This is more comprehensive than just checking specific fields try { const oldHandStr = JSON.stringify(oldHand); const newHandStr = JSON.stringify(newHand); return oldHandStr === newHandStr; } catch (error) { // If JSON.stringify fails, consider them different return false; } } Hand.isHandsEqual = isHandsEqual; /** * Serialize a hand to string format * @param input - Hand to serialize * @param format - Output format ('pokerstars' or 'json') * @returns Serialized hand as string */ function serialize(input, format = 'pokerstars') { if (format === 'pokerstars') { return phhToNarrative(input); } if (format === 'json') { try { // Use a Set to track circular references more reliably const seen = new WeakSet(); const result = JSON.stringify(input, (key, value) => { if (typeof value === 'object' && value !== null) { if (seen.has(value)) { return '[Circular Reference]'; } seen.add(value); } return value; }); return result; } catch (error) { // If JSON.stringify fails completely, return error info return JSON.stringify({ error: 'Failed to serialize hand to JSON', message: error instanceof Error ? error.message : 'Unknown error', inputType: typeof input, inputConstructor: input?.constructor?.name || 'unknown', }); } } throw new Error(`Unsupported format: ${format}`); } Hand.serialize = serialize; })(Hand || (Hand = {})); // Add 'export' alias for backward compatibility with the original API // This must be done after the namespace declaration Hand.export = Hand.output; Hand.merge = mergeGames; // Import extensions to register them // This demonstrates the module augmentation pattern import './formats/custom'; //# sourceMappingURL=Hand.js.map