UNPKG

@idealic/poker-engine

Version:

Poker game engine and hand evaluator

518 lines (426 loc) 17.8 kB
import { describe, expect, it } from 'vitest'; import * as Poker from '../../../index'; import { getActionAmount, getActionCards, getActionPlayerIndex } from '../../../index'; import type { Action } from '../../../types'; import { BASE_HAND } from './fixtures/baseGame'; describe('Game API - Integration', () => { describe('Complete Hand Flow', () => { it('should play complete hand from start to finish', () => { const hand: Poker.Hand = { variant: 'NT', players: ['Alice', 'Bob', 'Charlie'], startingStacks: [1000, 1000, 1000], blindsOrStraddles: [0, 10, 20], antes: [0, 0, 0], minBet: 20, actions: [], }; const completedHand: Poker.Hand = { ...hand, actions: [ 'd dh p1 AsKs', 'd dh p2 QhQc', 'd dh p3 JdTd', 'p1 cbr 60', 'p2 cc 60', 'p3 cc 60', 'd db AhKhQd', 'p2 cc', 'p3 cc', 'p1 cc', 'd db Td', 'p2 cc', 'p3 cc', 'p1 cc', 'd db 9s', 'p2 cc', 'p3 cc', 'p1 cc', 'p1 sm', 'p2 sm', 'p3 sm', ], }; let game = Poker.Game(hand); // Deal hole cards Poker.Game.applyAction(game, 'd dh p1 AsKs'); Poker.Game.applyAction(game, 'd dh p2 QhQc'); Poker.Game.applyAction(game, 'd dh p3 JdTd'); // Preflop betting expect(game.street).toBe('preflop'); Poker.Game.applyAction(game, 'p1 cbr 60'); Poker.Game.applyAction(game, 'p2 cc 60'); Poker.Game.applyAction(game, 'p3 cc 60'); // Flop Poker.Game.applyAction(game, 'd db AhKhQd'); expect(game.street).toBe('flop'); expect(game.board).toEqual(['Ah', 'Kh', 'Qd']); // Flop betting Poker.Game.applyAction(game, 'p2 cc'); Poker.Game.applyAction(game, 'p3 cbr 100'); Poker.Game.applyAction(game, 'p1 cc 100'); Poker.Game.applyAction(game, 'p2 cc 100'); // Turn Poker.Game.applyAction(game, 'd db Td'); expect(game.street).toBe('turn'); // Turn betting Poker.Game.applyAction(game, 'p2 cc'); Poker.Game.applyAction(game, 'p3 cc'); Poker.Game.applyAction(game, 'p1 cc'); // River Poker.Game.applyAction(game, 'd db 9s'); expect(game.street).toBe('river'); // River betting Poker.Game.applyAction(game, 'p2 cc'); Poker.Game.applyAction(game, 'p3 cc'); Poker.Game.applyAction(game, 'p1 cc'); // Showdown Poker.Game.applyAction(game, 'p2 sm QhQc'); Poker.Game.applyAction(game, 'p3 sm JdTd'); Poker.Game.applyAction(game, 'p1 sm AsKs'); expect(game.isShowdown).toBe(true); expect(game.isComplete).toBe(true); // Verify pot and winnings const finished = Poker.Game.finish(game, completedHand); expect(finished.finishingStacks).toBeDefined(); expect(finished.finishingStacks).toEqual([840, 840, 1320]); expect(finished.winnings).toBeDefined(); expect(finished.totalPot).toBeGreaterThan(0); }); it('should handle all-in scenarios through completion', () => { const hand: Poker.Hand = { variant: 'NT', players: ['Alice', 'Bob'], antes: [0, 0], startingStacks: [100, 500], blindsOrStraddles: [10, 20], minBet: 20, actions: [], }; let game = Poker.Game(hand); // Deal and go all-in preflop Poker.Game.applyAction(game, 'd dh p1 AsKs'); Poker.Game.applyAction(game, 'd dh p2 7h2d'); Poker.Game.applyAction(game, 'p1 cbr 100'); // Alice all-in Poker.Game.applyAction(game, 'p2 cc 100'); // Bob calls expect(game.players[0].isAllIn).toBe(true); // Should auto-deal remaining streets Poker.Game.applyAction(game, 'd db AhKhQd'); Poker.Game.applyAction(game, 'd db Td'); Poker.Game.applyAction(game, 'd db 9s'); // Direct to showdown Poker.Game.applyAction(game, 'p2 sm 7h2d'); Poker.Game.applyAction(game, 'p1 sm AsKs'); expect(game.isShowdown).toBe(true); expect(game.isComplete).toBe(true); }); }); describe('Validation and Application Flow', () => { it('should validate then apply actions correctly', () => { let game = Poker.Game(BASE_HAND); const actions: Action[] = ['p3 cc', 'p4 cbr 100']; for (const action of actions) { // First validate const canApply = Poker.Game.canApplyAction(game, action); expect(canApply).toBe(true); // Then apply const newGame = Poker.Game.applyAction(game, action); expect(newGame).toMatchObject(game); // New instance // Verify state changed expect(newGame.nextPlayerIndex).toBe(game.nextPlayerIndex); expect(getActionPlayerIndex(action)).toEqual(getActionPlayerIndex(newGame.lastAction!)); expect(getActionCards(action)).toEqual(getActionCards(newGame.lastAction!)); expect(getActionAmount(action)).toEqual(getActionAmount(newGame.lastAction!)); game = newGame; } }); it('should reject invalid actions without state corruption', () => { const game = Poker.Game(BASE_HAND); // Try invalid action const invalidAction = 'p1 cbr 100'; // Wrong player expect(Poker.Game.canApplyAction(game, invalidAction)).toBe(false); // Should throw when trying to apply expect(() => Poker.Game.applyAction(game, invalidAction)).toThrow(); // Original game should be unchanged expect(game.nextPlayerIndex).toBe(2); // Still Charlie's turn }); }); describe('Player Management Integration', () => { it('should track player states through hand progression', () => { const hand: Poker.Hand = { ...BASE_HAND, author: 'Alice', }; let game = Poker.Game(hand); // Track who has acted expect(Poker.Game.hasActed(game, 'Charlie')).toBe(false); Poker.Game.applyAction(game, 'p3 cc'); expect(Poker.Game.hasActed(game, 'Charlie')).toBe(true); Poker.Game.applyAction(game, 'p4 cc'); // New street resets hasActed Poker.Game.applyAction(game, 'd db Td'); expect(Poker.Game.hasActed(game, 'Charlie')).toBe(false); }); it('should handle player elimination correctly', () => { const hand: Poker.Hand = { variant: 'NT', players: ['Alice', 'Bob', 'Charlie', 'David'], startingStacks: [100, 100, 100, 100], blindsOrStraddles: [10, 20, 0, 0], antes: [0, 0, 0, 0], minBet: 20, actions: [], }; const completedHand: Poker.Hand = { ...hand, actions: [ 'd dh p1 AsKs', 'd dh p2 7h2d', 'd dh p3 QhQc', 'd dh p4 JdTd', 'p3 cbr 100', 'p4 f', 'p1 f', 'p2 f', 'p3 sm QhQc', 'p4 sm JdTd', ], }; let game = Poker.Game(hand); // Deal cards Poker.Game.applyAction(game, 'd dh p1 AsKs'); Poker.Game.applyAction(game, 'd dh p2 7h2d'); Poker.Game.applyAction(game, 'd dh p3 QhQc'); Poker.Game.applyAction(game, 'd dh p4 JdTd'); // Charlie goes all-in, others fold Poker.Game.applyAction(game, 'p3 cbr 100'); Poker.Game.applyAction(game, 'p4 f'); Poker.Game.applyAction(game, 'p1 f'); Poker.Game.applyAction(game, 'p2 f'); expect(game.isComplete).toBe(true); // Charlie wins const finished = Poker.Game.finish(game, completedHand); if (finished.winnings) { expect(finished.winnings[2]).toBeGreaterThan(0); } }); }); describe('Timing Integration', () => { it('should track timing through hand progression', () => { const now = Date.now(); const hand: Poker.Hand = { ...BASE_HAND, timeLimit: 30, actions: [], }; let game = Poker.Game(hand); // Apply actions with timestamps Poker.Game.applyAction(game, `d dh p1 AsKs #${now}`); expect(game.lastTimestamp).toBe(now); Poker.Game.applyAction(game, `d dh p2 7h2d #${now + 1000}`); expect(game.lastTimestamp).toBe(now + 1000); // Check elapsed time const elapsed = Poker.Game.getElapsedTime(game); expect(elapsed).toBeGreaterThanOrEqual(-1000); // Check time left const timeLeft = Poker.Game.getTimeLeft(game); expect(timeLeft).toBeLessThanOrEqual(30000 + 5000); }); }); describe('Complex Multi-Way Scenarios', () => { it('should handle side pots with multiple all-ins', () => { const hand: Poker.Hand = { variant: 'NT', players: ['Alice', 'Bob', 'Charlie', 'David'], startingStacks: [100, 200, 300, 400], blindsOrStraddles: [10, 20, 0, 0], antes: [0, 0, 0, 0], minBet: 20, actions: [], }; let game = Poker.Game(hand); // Deal cards to produce a specific outcome for testing side pots: // - p1 (shortest stack) gets the best hand (Full House) to win the Main Pot. // - p2 (second shortest) gets the second-best hand (Flush) to win Side Pot 1. // - p3 (third shortest) gets the third-best hand (Straight) to win Side Pot 2. // - p4 (largest stack) gets the fourth-best hand and wins nothing. Poker.Game.applyAction(game, 'd dh p1 AdAc'); // p1: Full House Poker.Game.applyAction(game, 'd dh p2 QhQc'); // p2: Flush Poker.Game.applyAction(game, 'd dh p3 JdTd'); // p3: Straight Poker.Game.applyAction(game, 'd dh p4 9h8h'); // p4: Two Pair // Pre-flop action: Multiple players go all-in Poker.Game.applyAction(game, 'p3 cbr 300'); // Charlie (UTG) is all-in for $300 Poker.Game.applyAction(game, 'p4 cc 300'); // David (BTN) calls Poker.Game.applyAction(game, 'p1 cbr 100'); // Alice (SB) is all-in for $100 Poker.Game.applyAction(game, 'p2 cbr 200'); // Bob (BB) is all-in for $200 // The board is dealt Poker.Game.applyAction(game, 'd db AsKs5s'); // Flop Poker.Game.applyAction(game, 'd db 5c'); // Turn Poker.Game.applyAction(game, 'd db Jd'); // River expect(game.isShowdown).toBe(true); // --- Showdown Sequence --- // Pots are resolved from the outside in. Since there was no river betting, // the showdown order for each pot starts with the first eligible player // to the left of the button. The correct order of new showings is p3 -> p4 -> p2 -> p1. // Pot 1 (Side Pot 2: $200): Contested by Charlie (p3) and David (p4) // The first eligible player to act left of the button is Charlie. Poker.Game.applyAction(game, 'p3 sm JdTd'); // The next eligible player is David. Poker.Game.applyAction(game, 'p4 sm 9h8h'); // Pot 2 (Side Pot 1: $300): Contested by Bob (p2), Charlie, and David // The next eligible player who hasn't shown is Bob. Poker.Game.applyAction(game, 'p2 sm QhQc'); // Pot 3 (Main Pot: $400): Contested by all players // The final player to show is Alice. Poker.Game.applyAction(game, 'p1 sm AdAc'); expect(game.isComplete).toBe(true); // Hand Results: // Alice: Full House, Aces full of Fives (wins Main Pot) // Bob: King-high Flush (wins Side Pot 1) // Charlie: King-high Straight (wins Side Pot 2) // David: Two Pair, Kings and Fives expect(game.players.map(p => p.winnings)).toEqual([400, 300, 200, 0]); expect(game.players.map(p => p.stack)).toEqual([400, 300, 200, 100]); }); it('should correctly process a real PokerStars hand history', () => { const hand: Poker.Hand = { variant: 'NT', players: ['aby100', 'Fridrih34', 'avstero', 'machosaliba22', 'ka100pka527', 'Riesa82'], startingStacks: [177.21, 124.69, 100.22, 21.46, 100, 106.54], blindsOrStraddles: [0, 0, 0, 0.5, 1, 0], // SB: p4, BB: p5 minBet: 1, actions: [], antes: [], rakePercentage: 0.05, }; let game = Poker.Game(hand); // Hole Cards are dealt to all players Poker.Game.applyAction(game, 'd dh p1 ??'); Poker.Game.applyAction(game, 'd dh p2 ??'); Poker.Game.applyAction(game, 'd dh p3 ??'); Poker.Game.applyAction(game, 'd dh p4 KdJd'); Poker.Game.applyAction(game, 'd dh p5 ??'); Poker.Game.applyAction(game, 'd dh p6 9d9s'); // Pre-flop Actions Poker.Game.applyAction(game, 'p6 cbr 2.3'); // Riesa82 raises Poker.Game.applyAction(game, 'p1 f'); // aby100 folds Poker.Game.applyAction(game, 'p2 f'); // Fridrih34 folds Poker.Game.applyAction(game, 'p3 f'); // avstero folds Poker.Game.applyAction(game, 'p4 cbr 3.6'); // machosaliba22 re-raises Poker.Game.applyAction(game, 'p5 f'); // ka100pka527 folds Poker.Game.applyAction(game, 'p6 cc 3.6'); // Riesa82 calls // Check pre-flop state expect(game.street).toBe('preflop'); expect(game.pot).toBeCloseTo(8.2); expect(game.players[3].totalBet).toBe(3.6); expect(game.players[5].totalBet).toBe(3.6); // Flop Poker.Game.applyAction(game, 'd db 6cKc6s'); expect(game.street).toBe('flop'); expect(game.board).toEqual(['6c', 'Kc', '6s']); // Flop Actions Poker.Game.applyAction(game, 'p4 cbr 7.79'); // machosaliba22 bets Poker.Game.applyAction(game, 'p6 cbr 23.21'); // Riesa82 raises Poker.Game.applyAction(game, 'p4 cc 23.21'); // machosaliba22 calls all-in // Turn and River are dealt automatically as a player is all-in Poker.Game.applyAction(game, 'd db 9c'); Poker.Game.applyAction(game, 'd db 2s'); expect(game.street).toBe('river'); expect(game.board).toEqual(['6c', 'Kc', '6s', '9c', '2s']); expect(game.isShowdown).toBe(true); // Showdown Poker.Game.applyAction(game, 'p4 sm KdJd'); Poker.Game.applyAction(game, 'p6 sm 9d9s'); expect(game.isComplete).toBe(true); // Final Assertions expect(game.rake).toBeCloseTo(2.2); expect(game.pot).toBeCloseTo(41.72); const finalStacks = game.players.map(p => p.stack); expect(finalStacks[0]).toBe(177.21); // aby100 - folded expect(finalStacks[1]).toBe(124.69); // Fridrih34 - folded expect(finalStacks[2]).toBe(100.22); // avstero - folded expect(finalStacks[3]).toBe(0); // machosaliba22 - lost all-in expect(finalStacks[4]).toBe(99); // ka100pka527 - folded BB expect(finalStacks[5]).toBeCloseTo(126.8); // Riesa82 - won pot (106.54 - 21.46 + 41.72) }); }); describe('Error Recovery', () => { it('should maintain consistency after failed action', () => { let game = Poker.Game(BASE_HAND); const originalState = JSON.parse(JSON.stringify(game)); // Try invalid action try { Poker.Game.applyAction(game, 'p1 cbr 100'); // Wrong player } catch (error) { // Expected to throw } // Game should still be in original state expect(JSON.stringify(game)).toBe(JSON.stringify(originalState)); // Should be able to continue with valid action Poker.Game.applyAction(game, 'p3 cc'); expect(game.nextPlayerIndex).toBe(3); }); }); describe('Query Methods Consistency', () => { it('should provide consistent results across methods', () => { const hand: Poker.Hand = { ...BASE_HAND, author: 'Charlie', }; const game = Poker.Game(hand); const authorIndex = Poker.Hand.getAuthorPlayerIndex(hand); // All methods should work together consistently const charlieIndex = Poker.Game.getPlayerIndex(game, 'Charlie'); expect(charlieIndex).toBe(authorIndex); const indexByNumber = Poker.Game.getPlayerIndex(game, 2); expect(indexByNumber).toBe(charlieIndex); // Validate current player to act if (game.nextPlayerIndex === charlieIndex) { expect(Poker.Game.canApplyAction(game, 'p3 cc')).toBe(true); } }); }); describe('Performance Scenarios', () => { it('should handle many rapid state changes', () => { const hand: Poker.Hand = { variant: 'NT', players: ['A', 'B'], startingStacks: [10000, 10000], blindsOrStraddles: [10, 20], antes: [0, 0], minBet: 20, actions: [], }; let game = Poker.Game(hand); // Rapid back-and-forth betting Poker.Game.applyAction(game, 'd dh p1 AsKs'); Poker.Game.applyAction(game, 'd dh p2 QhQc'); // Many raises back and forth Poker.Game.applyAction(game, 'p1 cbr 60'); Poker.Game.applyAction(game, 'p2 cbr 120'); Poker.Game.applyAction(game, 'p1 cbr 240'); Poker.Game.applyAction(game, 'p2 cbr 480'); Poker.Game.applyAction(game, 'p1 cbr 960'); Poker.Game.applyAction(game, 'p2 cc 960'); // Continue through streets Poker.Game.applyAction(game, 'd db AhKhQd'); Poker.Game.applyAction(game, 'p2 cbr 1000'); Poker.Game.applyAction(game, 'p1 cc 1000'); // Game should maintain consistency expect(game.pot).toBeGreaterThan(0); expect(game.street).toBe('flop'); }); it('should efficiently query large game states', () => { const game = Poker.Game(BASE_HAND); // Multiple queries shouldn't degrade for (let i = 0; i < 100; i++) { Poker.Game.getPlayerIndex(game, 'Alice'); Poker.Game.hasActed(game, 'Charlie'); Poker.Game.getTimeLeft(game); Poker.Game.canApplyAction(game, 'p3 cc'); } // Game should remain unchanged expect(game.nextPlayerIndex).toBe(2); }); }); });