UNPKG

@idealic/poker-engine

Version:

Poker game engine and hand evaluator

874 lines (730 loc) 31.4 kB
import { describe, expect, it } from 'vitest'; import { getActionPlayerIndex, getActionType } from '../../../game/position'; import * as Poker from '../../../index'; /** * Edge Cases Tests for Hand.advance with Player State Management * * Tests focus on player joining, pausing, and state transitions during game flow * Uses Hand methods (joinHand, pauseHand, quitHand, etc.) and merge for state changes */ beforeEach(() => { vi.setSystemTime(new Date(1715616000000)); }); afterEach(() => { vi.useRealTimers(); }); describe('Hand.advance - Player State Edge Cases', () => { describe('Empty table scenarios', () => { it('should not advance when table is completely empty', () => { // SCENARIO: Empty table with no players // INPUT: Hand with no players // EXPECTED: advance() returns unchanged hand (waits for players) const emptyHand = Poker.Hand({ variant: 'NT', players: [], startingStacks: [], blindsOrStraddles: [], antes: [], minBet: 20, actions: [], seed: 12345, }); const advanced = Poker.Hand.advance(emptyHand); // Should return unchanged - waiting for players expect(advanced.actions).toHaveLength(0); expect(advanced.players).toHaveLength(0); expect(Poker.Hand.isEqual(advanced, emptyHand)).toBe(true); }); it('should wait when single player joins empty table', () => { // SCENARIO: First player joins empty table // INPUT: Empty hand -> player joins with joinHand // EXPECTED: Player is inactive with intent 2, hand waits for second player const emptyHand = Poker.Hand({ variant: 'NT', players: [], startingStacks: [], blindsOrStraddles: [], antes: [], minBet: 20, actions: [], seed: 12345, }); // Alice joins the table const withAlice = Poker.Hand.join(emptyHand, { playerName: 'Alice', buyIn: 1000, }); // Merge to simulate server processing const serverHand = Poker.Hand.merge(emptyHand, withAlice, true); const advanced = Poker.Hand.advance(serverHand); // Should have Alice but not start game expect(advanced.players).toEqual(['Alice']); expect(advanced._intents).toEqual([0]); // Ready to play expect(advanced._inactive).toEqual([2]); // But inactive (new player) expect(advanced.actions).toHaveLength(0); // No actions yet }); }); describe('Single player scenarios', () => { it('should start game when second player joins', () => { // SCENARIO: Second player joins table with one waiting player // INPUT: 1 player waiting -> second player joins // EXPECTED: Both players activate and game starts // Start with one player const onePlayerHand = Poker.Hand({ variant: 'NT', players: ['Alice'], startingStacks: [1000], blindsOrStraddles: [0], // Alice inactive = 0 blind antes: [0], minBet: 20, _inactive: [1], _intents: [0], // Ready to play _deadBlinds: [0], actions: [], seed: 12345, }); // Bob joins const withBob = Poker.Hand.join(onePlayerHand, { playerName: 'Bob', buyIn: 1000, }); // Server merges the hands const mergedHand = Poker.Hand.merge(onePlayerHand, withBob, true); // Advance should activate both players and start const advanced = Poker.Hand.advance(mergedHand); // Both should be active now expect(advanced._inactive).toEqual([0, 0]); expect(advanced._deadBlinds).toEqual([0, 0]); expect(advanced.actions.length).toBeGreaterThan(0); // Game started expect(getActionType(advanced.actions[0])).toBe('dh'); // Dealing hole cards }); it('should handle player with waitForBB intent when second joins', () => { // SCENARIO: Player waiting for BB, second player joins // INPUT: Alice with waitForBB intent, Bob joins // EXPECTED: Both become active and start playing const aliceWaiting = Poker.Hand({ variant: 'NT', players: ['Alice'], startingStacks: [1000], blindsOrStraddles: [0], // Alice inactive = 0 blind antes: [0], minBet: 20, _inactive: [1], _intents: [1], // waitForBB _deadBlinds: [0], actions: [], seed: 12345, }); // Set Alice to resume (intent 0) first const aliceResumed = Poker.Hand.resume(aliceWaiting, 'Alice'); // Bob joins const withBob = Poker.Hand.join(aliceResumed, { playerName: 'Bob', buyIn: 1000, }); const mergedHand = Poker.Hand.merge(aliceResumed, withBob, true); const advanced = Poker.Hand.advance(mergedHand); // Both should be active expect(advanced._inactive).toEqual([0, 0]); expect(advanced.actions.length).toBeGreaterThan(0); }); }); describe('Player joining mid-game', () => { it('should keep new player inactive when joining active game', () => { // SCENARIO: New player joins during active hand // INPUT: Active 2-player game, third player joins // EXPECTED: New player stays inactive without dead blinds (will pay in next hand) const activeGame = Poker.Hand({ variant: 'NT', players: ['Alice', 'Bob'], startingStacks: [1000, 1000], blindsOrStraddles: [10, 20], antes: [0, 0], minBet: 20, _inactive: [0, 0], _intents: [0, 0], _deadBlinds: [0, 0], actions: ['d dh p1 AsKs', 'd dh p2 7c7d', 'p1 cc'], seed: 12345, }); // Charlie joins const withCharlie = Poker.Hand.join(activeGame, { playerName: 'Charlie', buyIn: 1000, }); // Server processes the join const mergedHand = Poker.Hand.merge(activeGame, withCharlie, true); // Advance the game const advanced = Poker.Hand.advance(mergedHand); // Charlie should remain inactive expect(advanced.players).toContain('Charlie'); expect(advanced._intents?.[2]).toBe(0); expect(advanced._inactive?.[2]).toBe(2); // Charlie is inactive expect(advanced._deadBlinds?.[2]).toBe(0); // Will have to pay dead blinds next hand // Game should NOT continue automatically - waiting for Bob's action (p2) // After 'p1 cc', it's Bob's turn to act, so advance doesn't add any actions expect(advanced.actions).toEqual(activeGame.actions); }); it('should handle multiple players joining simultaneously', () => { // SCENARIO: Multiple players join at once // INPUT: Active 2-player game, 2 new players join // EXPECTED: New players inactive, game continues const activeGame = Poker.Hand({ variant: 'NT', players: ['Alice', 'Bob'], startingStacks: [1000, 1000], blindsOrStraddles: [10, 20], antes: [0, 0], minBet: 20, _inactive: [0, 0], _intents: [0, 0], _deadBlinds: [0, 0], actions: ['d dh p1 AsKs', 'd dh p2 7c7d'], seed: 12345, }); // Charlie joins const withCharlie = Poker.Hand.join(activeGame, { playerName: 'Charlie', buyIn: 1500, }); // David joins const withDavid = Poker.Hand.join(withCharlie, { playerName: 'David', buyIn: 2000, }); // Server merges both joins const merged1 = Poker.Hand.merge(activeGame, withCharlie, true); const merged2 = Poker.Hand.merge(merged1, withDavid, true); const advanced = Poker.Hand.advance(merged2); // Both new players should be inactive expect(advanced.players).toHaveLength(4); expect(advanced._inactive?.[2]).toBe(2); // Charlie inactive expect(advanced._inactive?.[3]).toBe(2); // David inactive expect(advanced._deadBlinds?.[2]).toBe(0); expect(advanced._deadBlinds?.[3]).toBe(0); }); }); describe('Player pausing during game', () => { it('should continue the hand when a player pauses in a 2-player game', () => { // SCENARIO: 2-player game, one pauses mid-hand post-flop // INPUT: Active game post-flop, Bob pauses // EXPECTED: Hand continues normally, Bob is not auto-folded and can finish the hand. const activeGame = Poker.Hand({ variant: 'NT', players: ['Alice', 'Bob'], startingStacks: [990, 980], blindsOrStraddles: [10, 20], antes: [0, 0], minBet: 20, _inactive: [0, 0], _intents: [0, 0], _deadBlinds: [0, 0], actions: ['d dh p1 AsKs', 'd dh p2 7c7d', 'p1 cc 10', 'p2 cc', 'd db 2c3c4c'], seed: 12345, author: undefined, }); // Post-flop, it's Bob's turn (BB). Bob decides to pause for the next hand. Bob will play this hand to the end. const bobPaused = Poker.Hand.pause(activeGame, 'Bob'); const mergedHand = Poker.Hand.merge(activeGame, bobPaused, false); expect(mergedHand._intents?.[1]).toBe(2); // Verify Bob's intent is to pause // Bob checks. const bobChecks = Poker.Hand.applyAction( mergedHand, Poker.Command.check(Poker.Game(mergedHand), 'Bob') ); // The hand should not be complete, it's now Bob's turn. expect(Poker.Hand.isComplete(bobChecks)).toBe(false); // Bob, despite having paused, can still check. const aliceChecks = Poker.Hand.applyAction( bobChecks, Poker.Command.check(Poker.Game(bobChecks), 'Alice') ); // Betting round is done. Hand is still in progress. expect(Poker.Hand.isComplete(aliceChecks)).toBe(false); // Advance the game to deal the turn. const afterTurn = Poker.Hand.advance(aliceChecks); expect(Poker.Hand.isComplete(afterTurn)).toBe(false); // Play it out to showdown. const aliceChecksTurn = Poker.Hand.applyAction( afterTurn, Poker.Command.check(Poker.Game(afterTurn), 'Bob') ); const bobChecksTurn = Poker.Hand.applyAction( aliceChecksTurn, Poker.Command.check(Poker.Game(aliceChecksTurn), 'Alice') ); const afterRiver = Poker.Hand.advance(bobChecksTurn); const aliceChecksRiver = Poker.Hand.applyAction( afterRiver, Poker.Command.check(Poker.Game(afterRiver), 'Bob') ); const bobChecksRiver = Poker.Hand.applyAction( aliceChecksRiver, Poker.Command.check(Poker.Game(aliceChecksRiver), 'Alice') ); // Advance to showdown. const finalHand = Poker.Hand.advance(bobChecksRiver); // Now the hand should be complete. expect(Poker.Hand.isComplete(finalHand)).toBe(true); expect(finalHand.finishingStacks).toBeDefined(); // Bob's intent to pause should persist for the next hand. expect(finalHand._intents?.[1]).toBe(2); }); it('should continue when player pauses in 3+ player game', () => { // SCENARIO: 3-player game, one pauses // INPUT: Active 3-player game, Charlie pauses // EXPECTED: Game continues with all 3 players for the current hand const threePlayerGame = Poker.Hand({ variant: 'NT', players: ['Alice', 'Bob', 'Charlie'], startingStacks: [1000, 1000, 1000], blindsOrStraddles: [0, 10, 20], antes: [0, 0, 0], minBet: 20, _inactive: [0, 0, 0], _intents: [0, 0, 0], _deadBlinds: [0, 0, 0], actions: ['d dh p1 AsKs', 'd dh p2 7c7d', 'd dh p3 QhQd', 'p1 cc', 'p2 cc', 'p3 cc'], seed: 12345, author: undefined, }); // Charlie pauses const charliePaused = Poker.Hand.pause(threePlayerGame, 'Charlie'); const mergedHand = Poker.Hand.merge(threePlayerGame, charliePaused, false); expect(mergedHand._intents?.[2]).toBe(2); // First advance - dealer should deal flop let advanced = Poker.Hand.advance(mergedHand); expect(advanced.actions.length).toBeGreaterThan(threePlayerGame.actions.length); // After flop, it's Bob's turn first (p2, small blind) - add check const bobCheck = Poker.Command.check(Poker.Game(advanced), 'Bob'); advanced = Poker.Hand.applyAction(advanced, bobCheck); // Now it's Charlie's turn (p3, big blind). He should be able to act. const charlieCheck = Poker.Command.check(Poker.Game(advanced), 'Charlie'); advanced = Poker.Hand.applyAction(advanced, charlieCheck); // Charlie should not have been folded. const charlieFold = advanced.actions.find( action => getActionType(action) === 'f' && getActionPlayerIndex(action) === 2 ); expect(charlieFold).toBeUndefined(); expect(advanced._inactive?.[2]).toBe(0); // Now it's Alice's turn (p1) - add check const aliceCheck = Poker.Command.check(Poker.Game(advanced), 'Alice'); advanced = Poker.Hand.applyAction(advanced, aliceCheck); // Advance for turn card advanced = Poker.Hand.advance(advanced); // Bob checks turn const bobCheckTurn = Poker.Command.check(Poker.Game(advanced), 'Bob'); advanced = Poker.Hand.applyAction(advanced, bobCheckTurn); // Charlie checks turn const charlieCheckTurn = Poker.Command.check(Poker.Game(advanced), 'Charlie'); advanced = Poker.Hand.applyAction(advanced, charlieCheckTurn); // Alice checks turn const aliceCheckTurn = Poker.Command.check(Poker.Game(advanced), 'Alice'); advanced = Poker.Hand.applyAction(advanced, aliceCheckTurn); // Advance for river card advanced = Poker.Hand.advance(advanced); // Bob checks river const bobCheckRiver = Poker.Command.check(Poker.Game(advanced), 'Bob'); advanced = Poker.Hand.applyAction(advanced, bobCheckRiver); // Charlie checks river const charlieCheckRiver = Poker.Command.check(Poker.Game(advanced), 'Charlie'); advanced = Poker.Hand.applyAction(advanced, charlieCheckRiver); // Alice checks river const aliceCheckRiver = Poker.Command.check(Poker.Game(advanced), 'Alice'); advanced = Poker.Hand.applyAction(advanced, aliceCheckRiver); // Advance to showdown advanced = Poker.Hand.advance(advanced); // Game should complete with Alice and Bob expect(Poker.Hand.isComplete(advanced)).toBe(true); }); }); describe('Player quitting scenarios', () => { it('should handle player quitting mid-game', () => { // SCENARIO: Player quits during hand // INPUT: 3-player game, one quits // EXPECTED: Player marked with intent 3, game continues const activeGame = Poker.Hand({ variant: 'NT', players: ['Alice', 'Bob', 'Charlie'], startingStacks: [1000, 1000, 1000], blindsOrStraddles: [0, 10, 20], antes: [0, 0, 0], minBet: 20, _inactive: [0, 0, 0], _intents: [0, 0, 0], _deadBlinds: [0, 0, 0], actions: ['d dh p1 AsKs', 'd dh p2 7c7d', 'd dh p3 QhQd'], seed: 12345, author: undefined, }); // Bob quits const bobQuit = Poker.Hand.quit(activeGame, 'Bob'); // Server processes const mergedHand = Poker.Hand.merge(activeGame, bobQuit, false); // Advance const advanced = Poker.Hand.advance(mergedHand); // Bob should have quit intent expect(advanced._intents?.[1]).toBe(3); // Game should NOT continue automatically - waiting for Alice's action (p1) // After dealing hole cards, it's Alice's turn to act, advance doesn't add actions expect(advanced.actions).toEqual(activeGame.actions); expect(advanced._inactive?.[1]).toBe(0); // Bob stays active until his turn }); it('should handle multiple players quitting leaving one', async () => { // SCENARIO: Multiple players quit leaving one // INPUT: 3-player game, 1 quit, 1 paused(from the next hand) // EXPECTED: Hand is finished using timeout for both players (quit intent doesn't auto-fold) const activeGame = Poker.Hand({ variant: 'NT', players: ['Alice', 'Bob', 'Charlie'], startingStacks: [1000, 1000, 1000], blindsOrStraddles: [0, 10, 20], antes: [0, 0, 0], minBet: 20, timeLimit: 3, _inactive: [0, 0, 0], _intents: [0, 0, 0], _deadBlinds: [0, 0, 0], actions: [ 'd dh p1 AsKs #1715616000000', 'd dh p2 7c7d #1715616000000', 'd dh p3 QhQd #1715616000000', ], seed: 12345, author: undefined, }); // Bob quits const bobQuit = Poker.Hand.quit(activeGame, 'Bob'); const merged1 = Poker.Hand.merge(activeGame, bobQuit, false); // Charlie pauses const charliePaused = Poker.Hand.pause(merged1, 'Charlie'); const merged2 = Poker.Hand.merge(merged1, charliePaused, false); // Advance should not finish, waiting for Charlie's action const advanced = Poker.Hand.advance(merged2); // Should not be complete, allowing Charlie to play started game to the end expect(Poker.Hand.isComplete(advanced)).toBe(false); // Set system time to ensure Alice's call gets a consistent timestamp vi.setSystemTime(new Date(1715616001000)); // 1 second after deal // Alice must call (can't check preflop with BB=20) const aliceCalls = Poker.Hand.applyAction( { ...advanced, author: 'Alice' }, Poker.Command.call(Poker.Game(advanced), 'Alice') ); const merged3 = Poker.Hand.merge(advanced, aliceCalls, false); // Set time far into future to trigger Bob's timeout vi.setSystemTime(new Date(1717616000000)); // Bob folds due to timeout (not auto-fold - quit intent doesn't auto-fold) const advanced2 = Poker.Hand.advance(merged3); // Advance time again for Charlie's timeout (time must pass since Bob's fold) vi.setSystemTime(new Date(1718616000000)); // Charlie folds due to timeout const advanced3 = Poker.Hand.advance(advanced2); expect(Poker.Hand.isComplete(advanced3)).toBe(true); expect(advanced3.winnings).toEqual([50, 0, 0]); }); }); describe('Complex state transitions', () => { it('should handle player resuming after pause', () => { // SCENARIO: Paused player resumes // INPUT: Player was paused, sets intent to resume // EXPECTED: Player stays inactive until next hand const gameWithPaused = Poker.Hand({ variant: 'NT', players: ['Alice', 'Bob', 'Charlie'], startingStacks: [1000, 1000, 1000], blindsOrStraddles: [20, 10, 0], // Alice=BB, Bob=SB, Charlie=0 (inactive) antes: [0, 0, 0], minBet: 20, _inactive: [0, 0, 1], // Charlie paused _intents: [0, 0, 2], // Charlie has pause intent _deadBlinds: [0, 0, 20], actions: ['d dh p1 AsKs', 'd dh p2 7c7d'], seed: 12345, author: undefined, }); // Charlie resumes const charlieResumed = Poker.Hand.resume(gameWithPaused, 'Charlie'); const merged = Poker.Hand.merge(gameWithPaused, charlieResumed, false); const advanced = Poker.Hand.advance(merged); // Charlie should have resume intent but stay inactive expect(advanced._intents?.[2]).toBe(0); // Intent to resume expect(advanced._inactive?.[2]).toBe(1); // But still inactive this hand expect(advanced._deadBlinds?.[2]).toBe(20); // Dead blinds preserved }); it('should handle waitForBB correctly', () => { // SCENARIO: Player sets waitForBB intent // INPUT: Active player wants to wait for BB // EXPECTED: Player can still act (no auto-fold), waitForBB only affects next hand const activeGame = Poker.Hand({ variant: 'NT', players: ['Alice', 'Bob', 'Charlie'], startingStacks: [1000, 1000, 1000], blindsOrStraddles: [0, 10, 20], antes: [0, 0, 0], minBet: 20, _inactive: [0, 0, 0], _intents: [0, 0, 0], _deadBlinds: [0, 0, 0], actions: [], seed: 12345, author: undefined, }); // Alice wants to wait for BB const aliceWaitBB = Poker.Hand.waitForBB(activeGame, 'Alice'); let merged = Poker.Hand.merge(activeGame, aliceWaitBB, false); // Should have the intent set expect(merged._intents?.[0]).toBe(1); expect(merged._inactive?.[0]).toBe(0); // Advance deals cards, Alice is next to act (no auto-fold with new behavior) let advanced = Poker.Hand.advance(merged); const game = Poker.Game(advanced); expect(game.nextPlayerIndex).toBe(0); // Alice is next to act // Alice folds explicitly (not auto-fold - she can still act) const aliceFolds = Poker.Hand.applyAction( { ...advanced, author: 'Alice' }, Poker.Command.fold(game, 'Alice') ); merged = Poker.Hand.merge(advanced, aliceFolds, false); advanced = Poker.Hand.advance(merged); // Bob checks (calls 10 more to match BB) const bobChecks = Poker.Hand.applyAction( { ...advanced, author: 'Bob' }, Poker.Command.check(Poker.Game(advanced), 'Bob') ); merged = Poker.Hand.merge(advanced, bobChecks, false); advanced = Poker.Hand.advance(merged); // Charlie folds const charlieFolds = Poker.Hand.applyAction( { ...advanced, author: 'Charlie' }, Poker.Command.fold(Poker.Game(advanced), 'Charlie') ); merged = Poker.Hand.merge(advanced, charlieFolds, false); advanced = Poker.Hand.advance(merged); expect(Poker.Hand.isComplete(advanced)).toBe(true); expect(advanced.winnings).toEqual([0, 40, 0]); }); }); describe('Dead blinds scenarios', () => { it('should when player joins he have no dead blinds', () => { // SCENARIO: Player joins active table // INPUT: 2-player game, new player joins // EXPECTED: New player have no dead blinds in this hand const activeGame = Poker.Hand({ variant: 'NT', players: ['Alice', 'Bob'], startingStacks: [1000, 1000], blindsOrStraddles: [10, 20], antes: [0, 0], minBet: 20, _inactive: [0, 0], _intents: [0, 0], _deadBlinds: [0, 0], actions: ['d dh p1 AsKs', 'd dh p2 7c7d'], seed: 12345, }); // Charlie joins const withCharlie = Poker.Hand.join(activeGame, { playerName: 'Charlie', buyIn: 1000, }); const merged = Poker.Hand.merge(activeGame, withCharlie, true); // Charlie should have dead blinds, but only in the next hand expect(merged._deadBlinds?.[2]).toBe(0); expect(merged._inactive?.[2]).toBe(2); }); it('should preserve dead blinds for inactive players', () => { // SCENARIO: Inactive player with dead blinds // INPUT: Player inactive with dead blinds // EXPECTED: Dead blinds preserved through advance const gameWithInactive = Poker.Hand({ variant: 'NT', players: ['Alice', 'Bob', 'Charlie'], startingStacks: [1000, 1000, 1000], blindsOrStraddles: [20, 10, 0], // Alice=BB (shifted), Bob=SB, Charlie=0 (inactive) antes: [0, 0, 0], minBet: 20, _inactive: [0, 0, 1], _intents: [0, 0, 2], // Charlie paused _deadBlinds: [0, 0, 30], actions: ['d dh p1 AsKs', 'd dh p2 7c7d'], seed: 12345, }); const advanced = Poker.Hand.advance(gameWithInactive); // Dead blinds should be preserved expect(advanced._deadBlinds?.[2]).toBe(30); expect(advanced._inactive?.[2]).toBe(1); }); }); describe('Edge cases with empty and single player', () => { it('should activate players when enough join with intents to play', () => { // SCENARIO: Multiple just joined players become ready to play // INPUT: 2 just joined players ready to play, 1 waiting for BB // EXPECTED: 2 players are being activated and game starts, 1 player stays inactive const allInactive = Poker.Hand({ variant: 'NT', players: ['Alice', 'Bob', 'Charlie'], startingStacks: [1000, 1000, 1000], blindsOrStraddles: [0, 0, 0], // All inactive = 0 blinds antes: [0, 0, 0], minBet: 20, _inactive: [2, 2, 2], _intents: [0, 0, 1], // Alice and Bob ready to play, Charlie waiting for BB _deadBlinds: [20, 10, 0], actions: [], seed: 12345, }); const advanced = Poker.Hand.advance(allInactive); // Alice and Bob activate, Charlie stays inactive (waitForBB) expect(advanced._inactive).toEqual([0, 0, 2]); // Charlie stays inactive expect(advanced._deadBlinds).toEqual([0, 0, 0]); // Dead blinds cleared expect(advanced.actions.length).toBeGreaterThan(0); // Game starts with 2 players }); it('should activate waitForBB player when minimum players available', () => { // SCENARIO: Minimum players for game with waitForBB intent // INPUT: 3 inactive players - Alice ready (intent 0), Bob waitForBB (intent 1), Charlie paused (intent 2) // EXPECTED: Alice and Bob activate (Bob despite waitForBB since minimum players met), Charlie stays inactive const notEnoughReady = Poker.Hand({ variant: 'NT', players: ['Alice', 'Bob', 'Charlie'], startingStacks: [1000, 1000, 1000], blindsOrStraddles: [0, 0, 0], // All inactive = 0 blinds antes: [0, 0, 0], minBet: 20, _inactive: [1, 1, 1], _intents: [0, 1, 2], // Only Alice ready _deadBlinds: [20, 10, 0], actions: [], seed: 12345, }); const advanced = Poker.Hand.advance(notEnoughReady); // Alice and Bob should activate (Bob activates despite waitForBB intent to meet minimum) expect(advanced._inactive).toEqual([0, 0, 1]); // Charlie stays inactive expect(advanced._intents).toEqual([0, 0, 2]); // Intents for Charlie preserved expect(advanced.actions).toHaveLength(2); // Game starts with hole cards dealt }); }); }); describe('Hand.advance simplification', () => { describe('quit should NOT auto-fold', () => { it('should NOT auto-fold when player quits during their turn', () => { // SCENARIO: Player quits during active game, it's their turn // INPUT: Alice (SB) and Bob (BB) playing, preflop, Alice's turn, Alice quits // EXPECTED: advance() should NOT auto-fold for Alice - she continues playing // (fold should only happen on timeout, not on quit intent) // Setup: 2-player game, preflop, Alice's turn to act let hand = Poker.Hand({ variant: 'NT', players: ['Alice', 'Bob'], startingStacks: [1000, 1000], blindsOrStraddles: [10, 20], antes: [0, 0], minBet: 20, actions: [], _inactive: [0, 0], _intents: [0, 0], _deadBlinds: [0, 0], seed: 12345, }); // Start the game - deal cards hand = Poker.Hand.advance(hand); expect(hand.actions.length).toBeGreaterThan(0); // Cards dealt // Verify it's Alice's turn (SB acts first preflop in heads-up) let game = Poker.Game(hand); expect(game.nextPlayerIndex).toBe(0); // Alice's turn // Alice quits (sets intent to 3) const aliceQuit = Poker.Hand.quit({ ...hand, author: 'Alice' }); hand = Poker.Hand.merge(hand, aliceQuit, false); expect(hand._intents).toEqual([3, 0]); // Alice wants to quit // Count actions before advance const actionsBefore = hand.actions.length; // advance() should NOT auto-fold for Alice hand = Poker.Hand.advance(hand); // Actions should be unchanged - no auto-fold added expect(hand.actions.length).toBe(actionsBefore); // Game should still be waiting for Alice's action game = Poker.Game(hand); expect(game.nextPlayerIndex).toBe(0); // Still Alice's turn expect(Poker.Hand.isComplete(hand)).toBe(false); // Game not complete }); it('should NOT auto-fold when player with quit intent is not next to act', () => { // SCENARIO: Alice quits, but it's Bob's turn // INPUT: Alice and Bob playing, Alice already called, Bob's turn, Alice quits // EXPECTED: advance() should NOT interfere - Bob's turn continues let hand = Poker.Hand({ variant: 'NT', players: ['Alice', 'Bob'], startingStacks: [1000, 1000], blindsOrStraddles: [10, 20], antes: [0, 0], minBet: 20, actions: [], _inactive: [0, 0], _intents: [0, 0], _deadBlinds: [0, 0], seed: 12345, }); // Start game hand = Poker.Hand.advance(hand); // Alice calls (completes SB to BB) let game = Poker.Game(hand); const aliceCall = Poker.Command.call(game, 0); hand = Poker.Hand.applyAction(hand, aliceCall); // Now it's Bob's turn game = Poker.Game(hand); expect(game.nextPlayerIndex).toBe(1); // Bob's turn // Alice quits (even though it's not her turn) const aliceQuit = Poker.Hand.quit({ ...hand, author: 'Alice' }); hand = Poker.Hand.merge(hand, aliceQuit, false); expect(hand._intents).toEqual([3, 0]); // Alice wants to quit const actionsBefore = hand.actions.length; // advance() should not add any actions hand = Poker.Hand.advance(hand); // No auto-fold should be added expect(hand.actions.length).toBe(actionsBefore); // Still Bob's turn game = Poker.Game(hand); expect(game.nextPlayerIndex).toBe(1); }); }); describe('quit should NOT auto-complete game', () => { it('should NOT auto-complete when one player has quit intent', () => { // SCENARIO: Alice quits, Bob is only "continuing" player // INPUT: Alice and Bob playing, Alice quits (intent=3), Bob's turn // EXPECTED: Game should NOT auto-complete - Bob should act normally let hand = Poker.Hand({ variant: 'NT', players: ['Alice', 'Bob'], startingStacks: [1000, 1000], blindsOrStraddles: [10, 20], antes: [0, 0], minBet: 20, actions: [], _inactive: [0, 0], _intents: [0, 0], _deadBlinds: [0, 0], seed: 12345, }); // Start game hand = Poker.Hand.advance(hand); // Alice calls let game = Poker.Game(hand); const aliceCall = Poker.Command.call(game, 0); hand = Poker.Hand.applyAction(hand, aliceCall); // Alice quits const aliceQuit = Poker.Hand.quit({ ...hand, author: 'Alice' }); hand = Poker.Hand.merge(hand, aliceQuit, false); expect(hand._intents).toEqual([3, 0]); // Alice wants to quit // Now Bob is the only "continuing" player (Alice has intent=3) // But game should NOT auto-complete const actionsBefore = hand.actions.length; hand = Poker.Hand.advance(hand); // Game should NOT be auto-completed expect(Poker.Hand.isComplete(hand)).toBe(false); // Bob should still need to act game = Poker.Game(hand); expect(game.nextPlayerIndex).toBe(1); // Bob's turn // No auto-actions should be added expect(hand.actions.length).toBe(actionsBefore); }); }); });