UNPKG

@idealic/poker-engine

Version:

Poker game engine and hand evaluator

499 lines (417 loc) 16.5 kB
import { describe, expect, it } from 'vitest'; import * as Poker from '../../../index'; import { BASE_HAND, MINIMAL_HAND } from './fixtures/baseGame'; describe('Game API - State Modification', () => { describe('applyAction', () => { describe('Betting Actions', () => { it('should update pot and player bets on raise', () => { const hand: Poker.Hand = { ...BASE_HAND, actions: ['d dh p1 AsKs', 'd dh p2 7h2d', 'd dh p3 QhQc', 'd dh p4 JdTd'], }; const game = Poker.Game(hand); const initialPot = game.pot; const updatedGame = Poker.Game.applyAction(game, 'p3 cbr 60'); expect(updatedGame.pot).toBe(initialPot + 60); expect(updatedGame.players[2].roundBet).toBe(60); expect(updatedGame.players[2].stack).toBe(800 - 60); expect(updatedGame.nextPlayerIndex).toBe(3); // David's turn }); it('should handle call actions correctly', () => { const hand: Poker.Hand = { ...BASE_HAND, actions: ['d dh p1 AsKs', 'd dh p2 7h2d', 'd dh p3 QhQc', 'd dh p4 JdTd', 'p3 cbr 60'], }; const game = Poker.Game(hand); const updatedGame = Poker.Game.applyAction(game, 'p4 cc 60'); expect(updatedGame.players[3].roundBet).toBe(60); expect(updatedGame.players[3].stack).toBe(1200 - 60); expect(updatedGame.pot).toBe(30 + 60 + 60); // Blinds + two bets }); it('should handle check actions', () => { const hand: Poker.Hand = { ...BASE_HAND, actions: [...BASE_HAND.actions], // On flop, Charlie to act }; const game = Poker.Game(hand); const initialPot = game.pot; const updatedGame = Poker.Game.applyAction(game, 'p3 cc'); expect(updatedGame.pot).toBe(initialPot); // No change expect(updatedGame.players[2].roundBet).toBe(0); // Reset for new street expect(updatedGame.nextPlayerIndex).toBe(3); // David's turn }); it('should handle fold actions', () => { const hand: Poker.Hand = { ...BASE_HAND, actions: ['d dh p1 AsKs', 'd dh p2 7h2d', 'd dh p3 QhQc', 'd dh p4 JdTd', 'p3 cbr 60'], }; const game = Poker.Game(hand); const updatedGame = Poker.Game.applyAction(game, 'p4 f'); expect(updatedGame.players[3].hasFolded).toBe(true); expect(updatedGame.players[3].roundBet).toBe(0); // Should advance to next player or complete hand expect(updatedGame.nextPlayerIndex).toBe(0); // Alice's turn }); it('should handle all-in bets', () => { const hand: Poker.Hand = { ...BASE_HAND, startingStacks: [100, 150, 80, 120], actions: ['d dh p1 AsKs', 'd dh p2 7h2d', 'd dh p3 QhQc', 'd dh p4 JdTd'], }; const game = Poker.Game(hand); const updatedGame = Poker.Game.applyAction(game, 'p3 cbr 80'); expect(updatedGame.players[2].isAllIn).toBe(true); expect(updatedGame.players[2].stack).toBe(0); expect(updatedGame.players[2].roundBet).toBe(80); expect(updatedGame.pot).toBe(30 + 80); // Blinds + all-in }); }); describe('Dealer Actions', () => { it('should deal hole cards', () => { const hand: Poker.Hand = { ...BASE_HAND, actions: [], }; const game = Poker.Game(hand); const updated1 = Poker.Game.applyAction(game, 'd dh p1 AsKs'); expect(updated1.players[0].cards).toEqual(['As', 'Ks']); const updated2 = Poker.Game.applyAction(updated1, 'd dh p2 7h2d'); expect(updated2.players[1].cards).toEqual(['7h', '2d']); }); it('should deal flop', () => { const hand: Poker.Hand = { ...BASE_HAND, actions: [ 'd dh p1 AsKs', 'd dh p2 7h2d', 'd dh p3 QhQc', 'd dh p4 JdTd', 'p3 cc 20', 'p4 cc 20', 'p1 cc', 'p2 cc', ], }; const game = Poker.Game(hand); const updatedGame = Poker.Game.applyAction(game, 'd db AhKhQd'); expect(updatedGame.board).toEqual(['Ah', 'Kh', 'Qd']); expect(updatedGame.street).toBe('flop'); // Should reset to first active player expect(updatedGame.players[2].roundBet).toBe(0); // Bets reset }); it('should deal turn', () => { const hand: Poker.Hand = { ...BASE_HAND, actions: [...BASE_HAND.actions, 'p3 cc', 'p4 cc'], }; const game = Poker.Game(hand); const updatedGame = Poker.Game.applyAction(game, 'd db Td'); expect(updatedGame.board).toEqual(['Ah', 'Kh', 'Qd', 'Td']); expect(updatedGame.street).toBe('turn'); }); it('should deal river', () => { const hand: Poker.Hand = { ...BASE_HAND, actions: [...BASE_HAND.actions, 'p3 cc', 'p4 cc', 'd db Td', 'p3 cc', 'p4 cc'], }; const game = Poker.Game(hand); const updatedGame = Poker.Game.applyAction(game, 'd db 9s'); expect(updatedGame.board).toEqual(['Ah', 'Kh', 'Qd', 'Td', '9s']); expect(updatedGame.street).toBe('river'); }); }); describe('Show/Muck Actions', () => { it('should handle show actions', () => { const hand: Poker.Hand = { ...BASE_HAND, actions: [ ...BASE_HAND.actions, 'p3 cc', 'p4 cc', 'd db Td', 'p3 cc', 'p4 cc', 'd db 9s', 'p3 cc', 'p4 cc', ], }; const game = Poker.Game(hand); const updatedGame = Poker.Game.applyAction(game, 'p3 sm QhQc'); // Cards should be revealed expect(updatedGame.players[2].cards).toEqual(['Qh', 'Qc']); expect(updatedGame.players[2].hasShownCards).toBe(true); }); it('should handle muck actions', () => { const hand: Poker.Hand = { ...BASE_HAND, actions: [ ...BASE_HAND.actions, 'p3 cc', 'p4 cc', 'd db Td', 'p3 cc', 'p4 cc', 'd db 9s', 'p3 cc', 'p4 cc', ], }; const game = Poker.Game(hand); const updatedGame = Poker.Game.applyAction(game, 'p3 sm QhQc'); expect(updatedGame.players[2].hasActed).toBe(true); // Cards should be visible expect(updatedGame.players[2].hasShownCards).toBe(true); const updatedGame2 = Poker.Game.applyAction(updatedGame, 'p4 sm'); expect(updatedGame2.players[3].hasActed).toBe(true); expect(updatedGame2.players[3].hasShownCards).toBe(false); }); }); describe('State Transitions', () => { it('should advance street after betting round completes', () => { const hand: Poker.Hand = { ...BASE_HAND, actions: [ 'd dh p1 AsKs', 'd dh p2 7h2d', 'd dh p3 QhQc', 'd dh p4 JdTd', 'p3 cc 20', 'p4 cc 20', 'p1 cc', ], }; const game = Poker.Game(hand); expect(game.street).toBe('preflop'); // Last action to complete preflop const updatedGame = Poker.Game.applyAction(game, 'p2 cc'); // Should be ready for flop expect(updatedGame.nextPlayerIndex).toBe(-1); // Dealer's turn }); it('should complete hand when all but one fold', () => { const hand: Poker.Hand = { ...MINIMAL_HAND, actions: ['d dh p1 AsKs', 'd dh p2 7h2d', 'p1 cbr 100'], }; const game = Poker.Game(hand); const updatedGame = Poker.Game.applyAction(game, 'p2 f'); expect(updatedGame.isComplete).toBe(true); // Alice wins expect(updatedGame.players[0].winnings).toBeGreaterThan(0); }); it('should enter showdown after final betting and card show/muck', () => { const hand: Poker.Hand = { ...BASE_HAND, actions: [ ...BASE_HAND.actions, 'p3 cc', 'p4 cc', 'd db Td', 'p3 cc', 'p4 cc', 'd db 9s', 'p3 cc', ], }; const game = Poker.Game(hand); let updatedGame = Poker.Game.applyAction(game, Poker.Command.check(game, 3)); updatedGame = Poker.Game.applyAction(game, Poker.Command.muckCards(game, 2)); updatedGame = Poker.Game.applyAction(game, Poker.Command.muckCards(game, 3)); expect(updatedGame.isShowdown).toBe(true); expect(updatedGame.nextPlayerIndex).toBeGreaterThanOrEqual(-1); // Someone needs to show }); }); describe('Player Position Updates', () => { it('should update nextPlayerIndex correctly', () => { const hand: Poker.Hand = { ...BASE_HAND, actions: ['d dh p1 AsKs', 'd dh p2 7h2d', 'd dh p3 QhQc', 'd dh p4 JdTd'], }; const game = Poker.Game(hand); expect(game.nextPlayerIndex).toBe(2); // Charlie const g1 = Poker.Game.applyAction(game, 'p3 cbr 60'); expect(g1.nextPlayerIndex).toBe(3); // David const g2 = Poker.Game.applyAction(g1, 'p4 cc 60'); expect(g2.nextPlayerIndex).toBe(0); // Alice const g3 = Poker.Game.applyAction(g2, 'p1 f'); expect(g3.nextPlayerIndex).toBe(1); // Bob }); it('should skip folded players', () => { const hand: Poker.Hand = { ...BASE_HAND, actions: [ 'd dh p1 AsKs', 'd dh p2 7h2d', 'd dh p3 QhQc', 'd dh p4 JdTd', 'p3 cbr 60', 'p4 cc 60', 'p1 f', // Alice folds 'p2 f', // Bob folds 'd db AhKhQd', ], }; const game = Poker.Game(hand); // Only Charlie and David active expect(game.nextPlayerIndex).toBe(2); // Charlie const g1 = Poker.Game.applyAction(game, 'p3 cc'); expect(g1.nextPlayerIndex).toBe(3); // David (skips folded players) }); it('should skip all-in players', () => { const hand: Poker.Hand = { ...BASE_HAND, startingStacks: [100, 1500, 50, 1200], actions: [ 'd dh p1 AsKs', 'd dh p2 7h2d', 'd dh p3 QhQc', 'd dh p4 JdTd', 'p3 cbr 50', // Charlie all-in 'p4 cc 50', 'p1 cc 50', 'p2 cc 50', 'd db AhKhQd', ], }; const game = Poker.Game(hand); // Charlie is all-in, should skip expect(game.players[2].isAllIn).toBe(true); expect(game.nextPlayerIndex).not.toBe(2); }); }); describe('Mutability', () => { it('should mutate the original game object', () => { const game = Poker.Game(BASE_HAND); const originalNextPlayerIndex = game.nextPlayerIndex; const updatedGame = Poker.Game.applyAction(game, 'p3 cc'); // Should return the same game reference expect(updatedGame).toBe(game); // Original game should be mutated expect(game.nextPlayerIndex).not.toBe(originalNextPlayerIndex); expect(game.nextPlayerIndex).toBe(3); // David's turn // Player has acted flag should be updated expect(game.players[2].hasActed).toBe(true); }); it('should mutate player objects in place', () => { const game = Poker.Game(BASE_HAND); const originalPlayer = game.players[2]; const originalPlayers = game.players; const updatedGame = Poker.Game.applyAction(game, 'p3 cbr 100'); // Should return same references expect(updatedGame.players).toBe(originalPlayers); expect(updatedGame.players[2]).toBe(originalPlayer); // But the player object should be mutated expect(game.players[2].roundBet).toBe(100); expect(game.players[2].stack).toBe(640); // 740 (after preflop 60 bet) - 100 }); }); describe('Error Handling', () => { it('should throw for invalid actions', () => { // Wrong player - Alice already folded, can't act const game1 = Poker.Game(BASE_HAND); expect(() => Poker.Game.applyAction(game1, 'p1 cc')).toThrow(); // Wrong turn - it's Charlie's turn (p3), not David's (p4) const game2 = Poker.Game(BASE_HAND); expect(() => Poker.Game.applyAction(game2, 'p4 cc')).toThrow(); }); it('should throw for actions after hand complete', () => { const hand: Poker.Hand = { ...MINIMAL_HAND, actions: [ 'd dh p1 AsKs', 'd dh p2 7h2d', 'p1 cbr 100', 'p2 f', // Hand complete ], }; const game = Poker.Game(hand); // Verify the hand is actually complete expect(game.isComplete).toBe(true); // Try to perform actions after completion expect(() => Poker.Game.applyAction(game, 'p1 cbr 200')).toThrow(); expect(() => Poker.Game.applyAction(game, 'p2 cc')).toThrow(); }); }); describe('Complex Scenarios', () => { it('should handle multi-way pots', () => { const hand: Poker.Hand = { ...BASE_HAND, actions: [ 'd dh p1 AsKs', 'd dh p2 7h2d', 'd dh p3 QhQc', 'd dh p4 JdTd', 'p3 cbr 60', 'p4 cc 60', 'p1 cc 60', ], }; const game = Poker.Game(hand); const updatedGame = Poker.Game.applyAction(game, 'p2 cc 60'); // All players in, ready for flop expect(updatedGame.pot).toBe(60 * 4); // 4 players at 60 each expect(updatedGame.nextPlayerIndex).toBe(-1); // Dealer's turn }); it('should handle side pots with all-ins', () => { const hand: Poker.Hand = { ...BASE_HAND, startingStacks: [100, 500, 300, 400], actions: [ 'd dh p1 AsKs', 'd dh p2 7h2d', 'd dh p3 QhQc', 'd dh p4 JdTd', 'p3 cbr 100', // Charlie bets 100 (his max with 300 stack after blinds) 'p4 cc 100', 'p1 cbr 100', // Alice all-in for 100 total (90 more + 10 blind) ], }; const game = Poker.Game(hand); // Bob calls 100 const g1 = Poker.Game.applyAction(game, 'p2 cc 100'); // Charlie already bet 100, doesn't need to act again // David already called 100, doesn't need to act again // Alice is all-in, main pot will be 100*4 = 400 expect(g1.players[0].isAllIn).toBe(true); expect(g1.pot).toBe(100 * 4); // All players contributed 100 expect(g1.nextPlayerIndex).toBe(-1); // Betting complete, dealer's turn }); it('should handle re-raises', () => { const hand: Poker.Hand = { ...BASE_HAND, actions: [ 'd dh p1 AsKs', 'd dh p2 7h2d', 'd dh p3 QhQc', 'd dh p4 JdTd', 'p3 cbr 60', 'p4 cbr 120', // Re-raise ], }; const game = Poker.Game(hand); const g1 = Poker.Game.applyAction(game, 'p1 f'); const g2 = Poker.Game.applyAction(g1, 'p2 f'); const g3 = Poker.Game.applyAction(g2, 'p3 cbr 240'); // Re-re-raise expect(g3.players[2].roundBet).toBe(240); expect(g3.nextPlayerIndex).toBe(3); // Back to David }); }); describe('Timestamp Handling', () => { it('should preserve timestamps in actions', () => { const now = Date.now(); const game = Poker.Game(BASE_HAND); const updatedGame = Poker.Game.applyAction(game, `p3 cc #${now}`); expect(updatedGame.lastTimestamp).toBe(now); }); it('should update lastTimestamp', () => { const game = Poker.Game(BASE_HAND); const originalTimestamp = game.lastTimestamp; const now = Date.now() + 1000; // Ensure it's different from any existing timestamp const updatedGame = Poker.Game.applyAction(game, `p3 cc #${now}`); expect(updatedGame).toBe(game); // Same object reference expect(updatedGame.lastTimestamp).toBe(now); expect(originalTimestamp).not.toBe(now); // Original value was different }); }); }); });