UNPKG

@idealic/poker-engine

Version:

Poker game engine and hand evaluator

1,080 lines (879 loc) 113 kB
import { beforeEach, describe, expect, it } from 'vitest'; import { Hand } from '../../../Hand'; import type { NoLimitHand } from '../../../types'; import { BASE_HAND } from './fixtures/baseHand'; describe('next hand logic', () => { let completedHand: NoLimitHand; beforeEach(() => { // Deep copy of BASE_HAND and mark as completed completedHand = JSON.parse(JSON.stringify(BASE_HAND)) as NoLimitHand; // Add fields to make it a completed hand completedHand.finishingStacks = [980, 1010, 1010]; // Alice lost 20, Bob/Charlie split pot completedHand.winnings = [0, 30, 30]; completedHand.rake = 0; completedHand.totalPot = 60; // Add sit-in/out fields completedHand._inactive = completedHand._inactive || [0, 0, 0]; completedHand._intents = completedHand._intents || [0, 0, 0]; completedHand._deadBlinds = completedHand._deadBlinds || [0, 0, 0]; completedHand.seatCount = 6; }); describe('Order of operations verification', () => { it('should execute removal BEFORE rotation', () => { // Scenario: Verify removal happens first, then rotation // Input: Player to remove at index 1, verify rotation on filtered data // Expected: Arrays filtered first, then rotated completedHand._intents = [0, 3, 0]; // Bob wants to leave completedHand.blindsOrStraddles = [0, 10, 20]; const nextHand = Hand.next(completedHand); // Bob removed first, then blinds rotated expect(nextHand.players).toEqual(['Alice', 'Charlie']); // Rotation happens on [0, 10, 20] -> [10, 20] expect(nextHand.blindsOrStraddles).toEqual([10, 20]); }); it('should execute removal BEFORE dead blind calculation', () => { // Scenario: Dead blinds calculated on already-filtered players // Input: Remove player, then calculate dead blinds for remaining // Expected: Dead blind positions based on filtered array completedHand._intents = [2, 3, 0]; // Alice paused, Bob leaving, Charlie active completedHand._inactive = [1, 0, 0]; completedHand.blindsOrStraddles = [0, 10, 20]; completedHand._deadBlinds = [0, 0, 0]; const nextHand = Hand.next(completedHand); // Bob removed, Alice and Charlie remain expect(nextHand.players).toEqual(['Alice', 'Charlie']); // Dead blinds calculated on filtered positions expect(nextHand._deadBlinds?.length).toBe(2); }); it('should filter ALL arrays before ANY other operation', () => { // Scenario: All player-related arrays filtered in sync // Input: Remove player at index 1 from 3-player game // Expected: All arrays have length 2 before rotation/calculation completedHand._intents = [0, 3, 0]; // Bob leaving completedHand.seats = [1, 3, 5]; completedHand._venueIds = ['id1', 'id2', 'id3']; const nextHand = Hand.next(completedHand); // All arrays filtered to length 2 expect(nextHand.players).toHaveLength(2); expect(nextHand.startingStacks).toHaveLength(2); expect(nextHand.blindsOrStraddles).toHaveLength(2); expect(nextHand.antes).toHaveLength(2); expect(nextHand._intents).toHaveLength(2); expect(nextHand._inactive).toHaveLength(2); expect(nextHand._deadBlinds).toHaveLength(2); expect(nextHand.seats).toHaveLength(2); expect(nextHand._venueIds).toHaveLength(2); }); it('should base blind positions on filtered players', () => { // Scenario: After removal, blind positions recalculated // Input: Remove middle player, verify blind assignments // Expected: Correct SB/BB positions on remaining players completedHand._intents = [0, 3, 0]; // Bob (SB) leaving completedHand.blindsOrStraddles = [0, 10, 20]; // Alice UTG, Bob SB, Charlie BB const nextHand = Hand.next(completedHand); // After Bob removed and rotation expect(nextHand.players).toEqual(['Alice', 'Charlie']); // Blinds rotate on 2-player: [0, 10, 20] -> [10, 20] expect(nextHand.blindsOrStraddles).toEqual([10, 20]); }); }); describe('Player removal (Step 1 - happens first)', () => { describe('removing players who want to leave', () => { it('should remove player with leave intent (_intents: 3)', () => { // Scenario: Bob wants to leave the table // Input: _intents: [0, 3, 0] // Expected: Bob removed from all arrays in nextHand completedHand._intents = [0, 3, 0]; const nextHand = Hand.next(completedHand); expect(nextHand.players).toEqual(['Alice', 'Charlie']); expect(nextHand.startingStacks).toEqual([980, 1010]); expect(nextHand._intents).toEqual([0, 0]); expect(nextHand._inactive).toEqual([0, 0]); expect(nextHand._deadBlinds).toEqual([0, 0]); }); it('should remove multiple players with leave intent', () => { // Scenario: Alice and Charlie both leaving // Input: _intents: [3, 0, 3] // Expected: Only Bob remains in nextHand completedHand._intents = [3, 0, 3]; const nextHand = Hand.next(completedHand); expect(nextHand.players).toEqual(['Bob']); expect(nextHand.startingStacks).toEqual([1010]); expect(nextHand._intents).toEqual([0]); expect(nextHand._inactive).toEqual([0]); expect(nextHand._deadBlinds).toEqual([0]); }); it('should handle all players leaving - return error or empty table', () => { // Scenario: Everyone wants to leave // Input: _intents: [3, 3, 3] // Expected: Empty table - all player arrays empty completedHand._intents = [3, 3, 3]; const nextHand = Hand.next(completedHand); expect(nextHand.players).toEqual([]); expect(nextHand.startingStacks).toEqual([]); expect(nextHand.blindsOrStraddles).toEqual([]); expect(nextHand.antes).toEqual([]); expect(nextHand._intents).toEqual([]); expect(nextHand._inactive).toEqual([]); expect(nextHand._deadBlinds).toEqual([]); }); it('should handle single remaining player after others leave', () => { // Scenario: Two players leave, one remains // Input: _intents: [3, 3, 0] // Expected: Single player remains (heads-up not possible) completedHand._intents = [3, 3, 0]; const nextHand = Hand.next(completedHand); expect(nextHand.players).toEqual(['Charlie']); expect(nextHand.startingStacks).toEqual([1010]); expect(nextHand._intents).toEqual([0]); }); }); describe('removing players with zero chips', () => { it('should remove player with zero finishing stack', () => { // Scenario: Alice busted out // Input: finishingStacks: [0, 1050, 950] // Expected: Alice removed from nextHand completedHand.finishingStacks = [0, 1050, 950]; const nextHand = Hand.next(completedHand); expect(nextHand.players).toEqual(['Bob', 'Charlie']); expect(nextHand.startingStacks).toEqual([1050, 950]); }); it('should remove player with negative finishing stack', () => { // Scenario: Player went negative (shouldn't happen but handle) // Input: finishingStacks: [-5, 1050, 955] // Expected: Alice removed from nextHand completedHand.finishingStacks = [-5, 1050, 955]; const nextHand = Hand.next(completedHand); expect(nextHand.players).toEqual(['Bob', 'Charlie']); expect(nextHand.startingStacks).toEqual([1050, 955]); }); it('should remove multiple busted players', () => { // Scenario: Two players busted in all-in // Input: finishingStacks: [0, 2000, 0] // Expected: Only Bob in nextHand completedHand.finishingStacks = [0, 2000, 0]; const nextHand = Hand.next(completedHand); expect(nextHand.players).toEqual(['Bob']); expect(nextHand.startingStacks).toEqual([2000]); }); }); describe('removing players who cannot afford blinds', () => { it('should remove player who cannot afford upcoming BB', () => { // Scenario: Charlie next BB but only has 15 chips (BB is 20) // Input: finishingStacks: [1000, 1000, 15], next blinds would be [0, 10, 20] // Expected: Charlie removed from nextHand completedHand.finishingStacks = [1000, 1000, 15]; completedHand.blindsOrStraddles = [10, 20, 0]; // Alice (SB), Bob (BB), Charlie (BTN) // After rotation: [0, 10, 20] - Alice (BTN), Bob (SB), Charlie (BB) const nextHand = Hand.next(completedHand); expect(nextHand.players).toEqual(['Alice', 'Bob']); expect(nextHand.startingStacks).toEqual([1000, 1000]); }); it('should remove player who cannot afford upcoming SB', () => { // Scenario: Bob next SB but has 5 chips (SB is 10) // Input: finishingStacks: [1000, 5, 1000], next blinds would be [10, 20, 0] // Expected: Bob removed from nextHand completedHand.finishingStacks = [1000, 5, 1000]; completedHand.blindsOrStraddles = [20, 0, 10]; // After rotation, Bob would be SB const nextHand = Hand.next(completedHand); expect(nextHand.players).toEqual(['Alice', 'Charlie']); expect(nextHand.startingStacks).toEqual([1000, 1000]); }); it('should remove player who cannot afford antes', () => { // Scenario: Player has 1 chip but ante is 2 // Input: finishingStacks: [1000, 1000, 1], antes: [2, 2, 2] // Expected: Charlie removed from nextHand completedHand.finishingStacks = [1000, 1000, 1]; completedHand.antes = [2, 2, 2]; const nextHand = Hand.next(completedHand); expect(nextHand.players).toEqual(['Alice', 'Bob']); expect(nextHand.startingStacks).toEqual([1000, 1000]); }); it('should keep player who can exactly afford blind', () => { // Scenario: Player has exactly BB amount // Input: finishingStacks: [1000, 1000, 20], next BB is 20 // Expected: Charlie remains (can go all-in) completedHand.finishingStacks = [1000, 1000, 20]; completedHand.blindsOrStraddles = [20, 0, 10]; // Alice (BB), Bob (BTN), Charlie (SB) const nextHand = Hand.next(completedHand); expect(nextHand.players).toEqual(['Alice', 'Bob', 'Charlie']); expect(nextHand.startingStacks).toEqual([1000, 1000, 20]); }); }); describe('Player removal with complete chip requirements', () => { it('should remove player who can afford blind but NOT ante', () => { // Scenario: Player has exactly enough for blind, but needs ante too // Input: finishingStacks: [10, 200, 300], blinds: [0,1,10], antes: [2,2,2] // Expected: Alice removed (has 10, needs 10+2=12) completedHand.finishingStacks = [10, 200, 300]; completedHand.blindsOrStraddles = [0, 1, 10]; completedHand.antes = [2, 2, 2]; const nextHand = Hand.next(completedHand); // Alice needs blind(10) + ante(2) = 12 total expect(nextHand.players).toEqual(['Bob', 'Charlie']); expect(nextHand.startingStacks).toEqual([200, 300]); }); it('should keep player who has exactly blind+ante', () => { // SCENARIO: Player has precisely the required amount // INPUT: finishingStacks: [22, 200, 300], blinds: [0,10,20], antes: [2,2,2] // EXPECTED: Alice kept (has 22, needs BB=20 + ante=2 = 22 after rotation) completedHand.finishingStacks = [22, 200, 300]; completedHand.blindsOrStraddles = [0, 10, 20]; // minBet=20 → SB=10, BB=20 completedHand.antes = [2, 2, 2]; const nextHand = Hand.next(completedHand); // Alice has exactly 22, needs 22 (BB+ante), stays in game expect(nextHand.players).toContain('Alice'); expect(nextHand.startingStacks[0]).toBe(22); }); it('should remove inactive player who cannot afford blind+ante+deadBlinds', () => { // SCENARIO: Returning player needs to pay accumulated dead blinds // INPUT: finishingStacks: [35, 200, 300], _deadBlinds: [15, 0, 0], BB=20, ante=2 // EXPECTED: Alice removed (has 35, needs BB=20 + ante=2 + dead=15 = 37) completedHand.finishingStacks = [35, 200, 300]; completedHand.blindsOrStraddles = [0, 10, 20]; // minBet=20 → SB=10, BB=20 completedHand.antes = [2, 2, 2]; completedHand._inactive = [1, 0, 0]; completedHand._intents = [0, 0, 0]; // Alice wants to return completedHand._deadBlinds = [15, 0, 0]; const nextHand = Hand.next(completedHand); // Alice needs BB(20) + ante(2) + dead(15) = 37, has only 35 expect(nextHand.players).toEqual(['Bob', 'Charlie']); }); it('should handle player with intent=1 at BB position with insufficient chips', () => { // Scenario: Wait-for-BB player arrives at BB but cannot afford it // Input: finishingStacks: [8, 200, 300], at BB position, needs 10 // Expected: Alice removed (has 8, needs 10 for BB) completedHand.finishingStacks = [8, 200, 300]; completedHand.blindsOrStraddles = [0, 10, 20]; // After rotation: [20,0,10] completedHand._inactive = [1, 0, 0]; completedHand._intents = [1, 0, 0]; // Alice waiting for BB const nextHand = Hand.next(completedHand); // Alice would be at BB(20) but only has 8 chips expect(nextHand.players).not.toContain('Alice'); expect(nextHand.players).toEqual(['Bob', 'Charlie']); }); it('should keep player with intent=1 NOT at BB with low chips', () => { // Scenario: Wait-for-BB player not yet at BB, insufficient chips // Input: finishingStacks: [5, 200, 300], NOT at BB position // Expected: Alice kept (waiting, no chip requirement yet) completedHand.finishingStacks = [5, 200, 300]; completedHand.blindsOrStraddles = [0, 20, 10]; // Alice=0 (inactive), Bob=BB, Charlie=SB. After rotation: [10,0,20] completedHand._inactive = [1, 0, 0]; completedHand._intents = [1, 0, 0]; // Alice waiting for BB const nextHand = Hand.next(completedHand); // Alice not at BB, stays inactive with low chips expect(nextHand.players).toContain('Alice'); expect(nextHand._inactive![0]).toBe(1); // Still inactive }); it('should handle paused player with exact blind amount but not ante', () => { // Scenario: Paused player (intent=2) needs blind+ante // Input: finishingStacks: [10, 200, 300], blind: 10, ante: 1, intent: 2 // Expected: Alice removed (has 10, needs 10+1=11) completedHand.finishingStacks = [10, 200, 300]; completedHand.blindsOrStraddles = [0, 1, 10]; completedHand.antes = [1, 1, 1]; completedHand._inactive = [1, 0, 0]; completedHand._intents = [2, 0, 0]; // Alice paused const nextHand = Hand.next(completedHand); // Paused player needs blind(10) + ante(1) = 11, has only 10 expect(nextHand.players).toEqual(['Bob', 'Charlie']); }); }); describe('removal with dead blinds', () => { it('should not charge dead blinds to leaving player', () => { // Scenario: Player leaving with accumulated debt // Input: _intents: [3, 0, 0], _deadBlinds: [30, 0, 0] // Expected: Alice removed, debt not collected completedHand._intents = [3, 0, 0]; completedHand._deadBlinds = [30, 0, 0]; const nextHand = Hand.next(completedHand); expect(nextHand.players).toEqual(['Bob', 'Charlie']); // Alice's stack not reduced by dead blinds expect(nextHand.startingStacks).toEqual([1010, 1010]); }); it('should remove player who cannot afford dead blinds plus blinds', () => { // Scenario: Returning player can't cover debt + blind // Input: finishingStacks: [25, 1000, 1000], _deadBlinds: [20, 0, 0], upcoming BB // Expected: Alice removed (can't pay 20 debt + 20 BB) completedHand.finishingStacks = [25, 1000, 1000]; completedHand._deadBlinds = [20, 0, 0]; completedHand._inactive = [1, 0, 0]; completedHand._intents = [0, 0, 0]; // Alice wants to return completedHand.blindsOrStraddles = [0, 10, 20]; // Alice (BTN), Bob (SB), Charlie (BB) // After rotation: [20, 0, 10] - Alice (BB), Bob (BTN), Charlie (SB) const nextHand = Hand.next(completedHand); expect(nextHand.players).toEqual(['Bob', 'Charlie']); }); }); describe('auto-removal scenarios', () => { it('should auto-remove player who cannot afford dead blinds + blinds', () => { // Scenario: Insufficient chips for return to play // Input: finishingStack: 15, _deadBlinds: 10, upcoming BB: 20 // Expected: Auto-set _intents: 3 and remove player completedHand.finishingStacks = [15, 1000, 1000]; // Alice has only 15 completedHand._inactive = [1, 0, 0]; completedHand._intents = [0, 0, 0]; // Alice wants to return completedHand._deadBlinds = [10, 0, 0]; // Owes 10 completedHand.blindsOrStraddles = [0, 10, 20]; // Alice (BTN), Bob (SB), Charlie (BB) // After rotation: [20, 0, 10] - Alice (BB), Bob (BTN), Charlie (SB) const nextHand = Hand.next(completedHand); // Alice auto-removed (15 < 10 debt + 20 BB) expect(nextHand.players).toEqual(['Bob', 'Charlie']); expect(nextHand.startingStacks).toEqual([1000, 1000]); }); it('should auto-remove inactive player with insufficient chips', () => { // Scenario: Paused player runs out of money // Input: _inactive: 1, finishingStack: 5, needs 10 for SB // Expected: Auto-marked for removal with _intents: 3 completedHand.finishingStacks = [1000, 5, 1000]; // Bob has only 5 completedHand._inactive = [0, 1, 0]; // Bob is inactive completedHand._intents = [0, 2, 0]; // Bob is paused completedHand.blindsOrStraddles = [20, 0, 10]; // Alice (BB), Bob (BTN), Charlie (SB) // After rotation: [10, 20, 0] - Alice (SB), Bob (BB), Charlie (BTN) const nextHand = Hand.next(completedHand); // Bob auto-removed (5 < 10 SB) expect(nextHand.players).toEqual(['Alice', 'Charlie']); expect(nextHand.startingStacks).toEqual([1000, 1000]); }); it('should handle multiple auto-removals in one operation', () => { // Scenario: Multiple players insufficient funds // Input: 2 players can't afford blinds/debts // Expected: Player with insufficient funds removed completedHand.finishingStacks = [5, 8, 2000]; // Alice low with dead blinds completedHand._inactive = [1, 0, 0]; completedHand._intents = [0, 0, 0]; completedHand._deadBlinds = [10, 0, 0]; // Alice owes more than she has (5 < 10) completedHand.blindsOrStraddles = [0, 20, 10]; // Alice=0 (inactive), Bob=BB, Charlie=SB // After rotation: [10, 0, 20] - Alice at SB (but inactive, still 0), Bob at BTN, Charlie at BB const nextHand = Hand.next(completedHand); // Alice removed (can't afford dead blinds), Bob stays (at BTN, doesn't need blinds) expect(nextHand.players).toEqual(['Bob', 'Charlie']); expect(nextHand.startingStacks).toEqual([8, 2000]); }); }); }); describe('Dead blind calculations', () => { describe('dead blind formula verification', () => { it('should calculate SB as exactly 0.5 * BB in chips', () => { // Scenario: Verify exact formula for SB // Input: BB = 20, player will miss SB in next hand // Expected: _deadBlinds += 10 (0.5 * 20) completedHand._inactive = [0, 0, 1]; // Charlie inactive completedHand._intents = [0, 0, 2]; // Charlie paused completedHand.blindsOrStraddles = [20, 10, 0]; // Alice=BB, Bob=SB, Charlie=0 (inactive). Charlie was at theoretical BB position. completedHand._deadBlinds = [0, 0, 0]; const nextHand = Hand.next(completedHand); // After rotation: Charlie at theoretical SB position, will miss SB(10) // Should add 0.5 * 20 = 10 expect(nextHand._deadBlinds![2]).toBe(10); }); it('should calculate BB as exactly 1.0 * BB in chips', () => { // Scenario: Verify exact formula for BB // Input: BB = 20, player will miss BB in next hand // Expected: _deadBlinds += 20 (1.0 * 20) completedHand._inactive = [1, 0, 0]; // Alice inactive completedHand._intents = [2, 0, 0]; // Alice paused completedHand.blindsOrStraddles = [0, 10, 20]; // Alice at UTG, after rotation will be at BB completedHand._deadBlinds = [0, 0, 0]; const nextHand = Hand.next(completedHand); // After rotation: [20, 0, 10] - Alice will miss BB(20) // Should add 1.0 * 20 = 20 expect(nextHand._deadBlinds![0]).toBe(20); }); it('should use absolute chip values not coefficients', () => { // SCENARIO: Dead blinds in chips, not multipliers // INPUT: minBet=100 → BB=100, SB=50, will miss SB in next hand // EXPECTED: _deadBlinds += 50 chips (not 0.5) completedHand.minBet = 100; // BB=100, SB=50 completedHand.blindsOrStraddles = [100, 50, 0]; // Alice=BB, Bob=SB, Charlie=BTN (inactive) completedHand._inactive = [0, 0, 1]; // Charlie inactive completedHand._intents = [0, 0, 2]; completedHand._deadBlinds = [0, 0, 0]; const nextHand = Hand.next(completedHand); // After rotation: Charlie will miss SB(50) // Should add 50 chips (0.5 * 100), not 0.5 expect(nextHand._deadBlinds![2]).toBe(50); expect(typeof nextHand._deadBlinds![2]).toBe('number'); }); it('should calculate based on NEXT hand positions', () => { // Scenario: Use positions after rotation // Input: Player was BTN in completed hand, will be SB in next // Expected: Calculate based on next position after rotation completedHand.blindsOrStraddles = [10, 0, 20]; // Alice=SB (shifted from Bob), Bob=0 (inactive), Charlie=BB completedHand._inactive = [0, 1, 0]; completedHand._intents = [0, 2, 0]; completedHand._deadBlinds = [0, 0, 0]; const nextHand = Hand.next(completedHand); // After rotation: [20, 10, 0] - Bob would be at SB position (index 1) // Bob as inactive at SB position accumulates dead blinds = 0.5*BB = 10 expect(nextHand._deadBlinds![1]).toBe(10); // With skip-inactive, actual blinds shift: Alice=BB, Charlie=SB expect(nextHand.blindsOrStraddles).toEqual([20, 0, 10]); }); it('should cap at exactly 1.5 * BB in chips', () => { // Scenario: Maximum cap verification // Input: BB = 20, accumulate past 1.5 // Expected: Caps at 30 chips (1.5 * 20) completedHand.blindsOrStraddles = [20, 0, 10]; // Alice at BB completedHand._inactive = [1, 0, 0]; // Alice inactive completedHand._intents = [2, 0, 0]; completedHand._deadBlinds = [30, 0, 0]; // Already at max (1.5 * 20) const nextHand = Hand.next(completedHand); // Should remain capped at 30, not increase expect(nextHand._deadBlinds![0]).toBe(30); }); }); describe('accumulating dead blinds for inactive players', () => { it('should add 0.5BB for missed SB position', () => { // Scenario: Inactive player will be in SB position after rotation // Input: _inactive: [0, 0, 1], blindsOrStraddles: [20, 10, 0] // Expected: _deadBlinds increases by 10 (0.5 * BB of 20) completedHand._inactive = [0, 0, 1]; // Charlie is inactive completedHand._intents = [0, 0, 2]; // Charlie paused completedHand.blindsOrStraddles = [20, 10, 0]; // Alice=BB (shifted), Bob=SB, Charlie=0 (inactive, at theoretical BB) completedHand._deadBlinds = [0, 0, 0]; const nextHand = Hand.next(completedHand); // After rotation: [20, 0, 10] - Charlie will be at SB // Charlie's dead blinds should increase by 10 (0.5 * 20) expect(nextHand._deadBlinds).toEqual([0, 0, 10]); }); it('should add 1BB for missed BB position', () => { // Scenario: Inactive player will be in BB position after rotation // Input: _inactive: [1, 0, 0], blindsOrStraddles: [0, 10, 20] // Expected: _deadBlinds increases by 20 (1 * BB of 20) completedHand._inactive = [1, 0, 0]; // Alice is inactive completedHand._intents = [2, 0, 0]; // Alice paused completedHand.blindsOrStraddles = [0, 10, 20]; // Alice at UTG completedHand._deadBlinds = [0, 0, 0]; const nextHand = Hand.next(completedHand); // After rotation: [20, 0, 10] - Alice will be at BB // Alice's dead blinds should increase by 20 (1.0 * 20) expect(nextHand._deadBlinds).toEqual([20, 0, 0]); }); it('should not accumulate for non-blind position', () => { // Scenario: Inactive player will be in non-blind position after rotation // Input: _inactive: [0, 1, 0], blindsOrStraddles: [20, 0, 10] // Expected: _deadBlinds unchanged (Bob at BTN after rotation) completedHand._inactive = [0, 1, 0]; // Bob is inactive completedHand._intents = [0, 2, 0]; // Bob paused completedHand.blindsOrStraddles = [20, 0, 10]; // Alice=BB, Bob=0 (inactive), Charlie=SB completedHand._deadBlinds = [0, 0, 0]; const nextHand = Hand.next(completedHand); // After rotation: [10, 20, 0] - Bob at theoretical BB position // But with skip-inactive, actual blinds shift: Alice=SB(10), Charlie=BB(20) // Bob as inactive at BB position still accumulates dead blinds = 1.0*BB = 20 expect(nextHand._deadBlinds).toEqual([0, 20, 0]); }); it('should not accumulate for any non-blind positions', () => { // Scenario: Inactive players check future positions after rotation // Input: 6 players, P1,P2,P3 inactive // Expected: Only those who WILL BE on blind accumulate const sixPlayerHand = { ...completedHand, players: ['P1', 'P2', 'P3', 'P4', 'P5', 'P6'], finishingStacks: [1000, 1000, 1000, 1000, 1000, 1000], blindsOrStraddles: [0, 0, 0, 0, 10, 20], // P5=SB, P6=BB antes: [0, 0, 0, 0, 0, 0], _inactive: [1, 1, 1, 0, 0, 0], // P1,P2,P3 inactive _intents: [2, 2, 2, 0, 0, 0], _deadBlinds: [0, 0, 0, 0, 0, 0], }; const nextHand = Hand.next(sixPlayerHand); // After rotation: [20, 0, 0, 0, 0, 10] - P1 at BB, P2-P5 not on blind, P6 at SB expect(nextHand._deadBlinds![0]).toBe(20); // P1 will miss BB expect(nextHand._deadBlinds![1]).toBe(0); // P2 not in blind position expect(nextHand._deadBlinds![2]).toBe(0); // P3 not in blind position }); it('should accumulate for button if will be on blind next', () => { // Scenario: Inactive player on button will be BB after rotation // Input: _inactive: [1, 0, 0], blindsOrStraddles: [0, 10, 20] // Expected: _deadBlinds accumulate for future BB position completedHand.blindsOrStraddles = [0, 10, 20]; // Alice=BTN, Bob=SB, Charlie=BB completedHand._inactive = [1, 0, 0]; // Alice (button) inactive completedHand._intents = [2, 0, 0]; completedHand._deadBlinds = [0, 0, 0]; const nextHand = Hand.next(completedHand); // After rotation: [20, 0, 10] - Alice will be at BB expect(nextHand._deadBlinds![0]).toBe(20); // Alice will miss BB }); it('should cap dead blinds at 1.5BB maximum', () => { // Scenario: Player already at max debt // Input: _deadBlinds: [30, 0, 0] (1.5 * 20), missed another BB // Expected: _deadBlinds remains at 30 completedHand._inactive = [1, 0, 0]; // Alice is inactive completedHand._intents = [2, 0, 0]; // Alice paused completedHand.blindsOrStraddles = [0, 20, 10]; // Alice=0 (inactive), Bob=BB, Charlie=SB completedHand._deadBlinds = [30, 0, 0]; // Already at max (1.5 * 20) const nextHand = Hand.next(completedHand); // Should remain capped at 30 expect(nextHand._deadBlinds).toEqual([30, 0, 0]); }); it('should accumulate correctly over multiple hands', () => { // Scenario: Track accumulation pattern with shift-to-active logic // Input: Player inactive through multiple rotations // Expected: Accumulate based on THEORETICAL position after rotation // Note: With shift-to-active, blinds shift to active players, // which changes the actual blind array but dead blinds use theoretical positions // First hand: Charlie at BB, theoretical position after rotation is SB completedHand.blindsOrStraddles = [20, 10, 0]; // Alice=BB, Bob=SB, Charlie=0 (inactive) completedHand._inactive = [0, 0, 1]; // Charlie inactive completedHand._intents = [0, 0, 2]; // Charlie paused completedHand._deadBlinds = [0, 0, 0]; let hand1 = Hand.next(completedHand); // Theoretical rotation: [20, 0, 10] - Charlie at SB position // Dead blinds: Charlie misses SB = 0.5 * 20 = 10 // Actual blindsOrStraddles with shift: [10, 20, 0] (Alice SB, Bob BB, Charlie 0) expect(hand1._deadBlinds![2]).toBe(10); // Second hand: hand1.blindsOrStraddles = [10, 20, 0] hand1.finishingStacks = hand1.startingStacks; hand1._inactive = [0, 0, 1]; // Charlie still inactive let hand2 = Hand.next(hand1); // Theoretical rotation of [10, 20, 0]: [0, 10, 20] - Charlie at BB position! // Dead blinds: Charlie misses BB = 1.0 * 20 = 20 // Total: 10 + 20 = 30 (capped at 1.5 * 20 = 30) expect(hand2._deadBlinds![2]).toBe(30); // Third hand: hand2.blindsOrStraddles = [10, 20, 0] (shifted) hand2.finishingStacks = hand2.startingStacks; hand2._inactive = [0, 0, 1]; // Charlie still inactive let hand3 = Hand.next(hand2); // Dead blinds already at cap (30), no further accumulation expect(hand3._deadBlinds![2]).toBe(30); }); it('should handle different blind amounts', () => { // SCENARIO: 50/100 blinds instead of 10/20 // INPUT: minBet=100 → BB=100, SB=50, player will be on SB after rotation // EXPECTED: _deadBlinds increases by 50 (0.5 * 100) completedHand.minBet = 100; // BB=100, SB=50 completedHand.blindsOrStraddles = [100, 50, 0]; // Alice=BB, Bob=SB, Charlie=BTN (inactive) completedHand._inactive = [0, 0, 1]; // Charlie inactive completedHand._intents = [0, 0, 2]; // Charlie paused completedHand._deadBlinds = [0, 0, 0]; const nextHand = Hand.next(completedHand); // After rotation: Charlie will miss SB(50) // Should add 50 (0.5 * 100) expect(nextHand._deadBlinds![2]).toBe(50); }); }); describe('Dead blind accumulation across multiple hands', () => { it('should accumulate 0.5×BB when inactive player misses SB', () => { // Scenario: Track dead blind accumulation over 2 hands with shift-to-active logic // Input: Hand1: Charlie inactive, will be at SB in theoretical rotation // Expected: Charlie accumulates dead blinds based on theoretical positions // === HAND 1 COMPLETION === completedHand.blindsOrStraddles = [20, 10, 0]; // Alice=BB, Bob=SB, Charlie=0 (inactive) completedHand._inactive = [0, 0, 1]; // Charlie inactive completedHand._intents = [0, 0, 2]; // Charlie paused completedHand._deadBlinds = [0, 0, 0]; // No prior debt const hand2 = Hand.next(completedHand); // Theoretical rotation: [20, 10, 0] -> [0, 20, 10] - Charlie would be at SB // Dead blinds: Charlie misses SB = 0.5 * 20 = 10 // Actual blindsOrStraddles with shift: [20, 10, 0] (Alice BB, Bob SB) expect(hand2._deadBlinds![2]).toBe(10); // === HAND 2 COMPLETION === const hand2Completed = { ...hand2, finishingStacks: hand2.startingStacks, }; const hand3 = Hand.next(hand2Completed); // hand2.blindsOrStraddles = [10, 20, 0] // Theoretical rotation: [0, 10, 20] - Charlie would be at BB! // Dead blinds: Charlie misses BB = 1.0 * 20 = 20 // Total: 10 + 20 = 30 (capped at 1.5 * 20 = 30) expect(hand3._deadBlinds![2]).toBe(30); }); it('should accumulate 1.0×BB when inactive player misses BB', () => { // Scenario: Track dead blind accumulation for missed BB position // Input: Hand1: Alice at UTG inactive, will be at BB in next; Hand2: check accumulation // Expected: Alice accumulates 20 chips (1.0×20) dead blind // === HAND 1 COMPLETION === completedHand.blindsOrStraddles = [0, 10, 20]; // Alice at UTG completedHand._inactive = [1, 0, 0]; // Alice inactive completedHand._intents = [2, 0, 0]; // Alice paused completedHand._deadBlinds = [0, 0, 0]; const hand2 = Hand.next(completedHand); // After rotation: [20, 0, 10] - Alice will miss BB(20) // Adds 1.0×20 = 20 expect(hand2._deadBlinds![0]).toBe(20); }); it('should accumulate correctly over 3 hands: various positions', () => { // Scenario: Player accumulates dead blinds based on NEXT hand positions // Input: Track through 3 hands with different positions // Expected: Accumulate only when will be on blind in next hand // === HAND 1: Alice at UTG, will be BB after rotation === completedHand.blindsOrStraddles = [0, 10, 20]; // Alice UTG, Bob SB, Charlie BB completedHand._inactive = [1, 0, 0]; // Alice inactive completedHand._intents = [2, 0, 0]; // Alice paused completedHand._deadBlinds = [0, 0, 0]; const hand2 = Hand.next(completedHand); // After rotation: [20, 0, 10] - Alice at BB expect(hand2._deadBlinds![0]).toBe(20); // Alice will miss BB = +1.0×20 = 20 // === HAND 2: Alice at BB, will be SB after rotation === hand2.finishingStacks = hand2.startingStacks; hand2._inactive = [1, 0, 0]; // Alice still inactive // hand2.blindsOrStraddles is already [20, 0, 10] from rotation const hand3 = Hand.next(hand2); // After rotation: [10, 20, 0] - Alice at SB expect(hand3._deadBlinds![0]).toBe(30); // 20 + 0.5×20 = 30 // === HAND 3: Alice at SB, will be UTG after rotation (no blind) === hand3.finishingStacks = hand3.startingStacks; hand3._inactive = [1, 0, 0]; // Alice still inactive // hand3.blindsOrStraddles is already [10, 20, 0] from rotation const hand4 = Hand.next(hand3); // After rotation: [0, 10, 20] - Alice at UTG (no blind) expect(hand4._deadBlinds![0]).toBe(30); // No change - not on blind next hand }); it('should track separate accumulation for multiple inactive players', () => { // Scenario: Two players inactive, check their NEXT positions // Input: Alice at UTG, Bob at SB, both inactive // Expected: Based on next positions after rotation completedHand.blindsOrStraddles = [0, 10, 20]; // Alice UTG, Bob SB, Charlie BB completedHand._inactive = [1, 1, 0]; // Alice and Bob inactive completedHand._intents = [2, 2, 0]; // Both paused completedHand._deadBlinds = [0, 0, 0]; const nextHand = Hand.next(completedHand); // After rotation: [20, 0, 10] - Alice at BB, Bob at UTG, Charlie at SB expect(nextHand._deadBlinds![0]).toBe(20); // Alice: will miss BB = 1.0×20 expect(nextHand._deadBlinds![1]).toBe(0); // Bob: not on blind expect(nextHand._deadBlinds![2]).toBe(0); // Charlie: active }); it('should NOT accumulate when player returns and becomes active', () => { // Scenario: Previously inactive player returns, stops accumulating // Input: Alice was inactive with debt, now intent=0 (returning) // Expected: Dead blinds preserved for Game() to charge, stack unchanged completedHand.finishingStacks = [100, 200, 300]; completedHand.blindsOrStraddles = [10, 20, 0]; // Alice at SB completedHand._inactive = [1, 0, 0]; // Alice was inactive completedHand._intents = [0, 0, 0]; // Alice returns completedHand._deadBlinds = [10, 0, 0]; // Has debt const nextHand = Hand.next(completedHand); // Alice returns, debt preserved for Game() to charge expect(nextHand._inactive![0]).toBe(0); // Now active expect(nextHand._deadBlinds![0]).toBe(10); // Debt preserved for Game() expect(nextHand.startingStacks[0]).toBe(100); // Stack unchanged }); }); describe('dead blind payment scenarios', () => { it('should preserve dead blinds when player returns early (Game() will charge)', () => { // Scenario: Player stops pause, debt preserved for Game() to charge // Input: _intents: [0, 0, 0], _deadBlinds: [20, 0, 0], finishingStacks: [1000, 1000, 1000] // Expected: startingStacks: [1000, 1000, 1000], _deadBlinds: [20, 0, 0] completedHand.finishingStacks = [1000, 1000, 1000]; completedHand._inactive = [1, 0, 0]; // Alice was inactive completedHand._intents = [0, 0, 0]; // Alice wants to return completedHand._deadBlinds = [20, 0, 0]; // Alice owes 20 const nextHand = Hand.next(completedHand); expect(nextHand.startingStacks).toEqual([1000, 1000, 1000]); // Stack unchanged expect(nextHand._deadBlinds).toEqual([20, 0, 0]); // Debt preserved for Game() expect(nextHand._inactive).toEqual([0, 0, 0]); }); it('should clear dead blinds when reaching BB position', () => { // Scenario: Player waited for BB, no payment // Input: _intents: [1, 0, 0], at BB position, _deadBlinds: [20, 0, 0] // Expected: _deadBlinds: [0, 0, 0], stack unchanged completedHand.finishingStacks = [1000, 1000, 1000]; completedHand._inactive = [1, 0, 0]; // Alice was inactive completedHand._intents = [1, 0, 0]; // Alice waiting for BB completedHand._deadBlinds = [20, 0, 0]; // Alice has debt completedHand.blindsOrStraddles = [0, 10, 20]; // Alice (BTN), Bob (SB), Charlie (BB) // After rotation: [20, 0, 10] - Alice (BB), Bob (BTN), Charlie (SB) const nextHand = Hand.next(completedHand); // Check rotated positions - Alice should now be at BB expect(nextHand.blindsOrStraddles).toEqual([20, 0, 10]); expect(nextHand.startingStacks).toEqual([1000, 1000, 1000]); // No deduction expect(nextHand._deadBlinds).toEqual([0, 0, 0]); // Debt cleared expect(nextHand._inactive).toEqual([0, 0, 0]); // Activated expect(nextHand._intents).toEqual([0, 0, 0]); // Intent reset }); it('should handle partial payment if insufficient chips', () => { // Scenario: Player has 10 chips, owes 30 // Input: finishingStacks: [10, 1000, 1000], _deadBlinds: [30, 0, 0] // Expected: Player removed (can't afford) completedHand.finishingStacks = [10, 1000, 1000]; completedHand._inactive = [1, 0, 0]; completedHand._intents = [0, 0, 0]; // Alice wants to return completedHand._deadBlinds = [30, 0, 0]; const nextHand = Hand.next(completedHand); // Alice removed - can't afford dead blinds expect(nextHand.players).toEqual(['Bob', 'Charlie']); expect(nextHand.startingStacks).toEqual([1000, 1000]); }); }); }); describe('Player activation states', () => { describe('state transition coverage', () => { it('should transition active player with pause intent to inactive', () => { // Scenario: Active player requested pause // Input: _inactive: 0, _intents: 1 (or 2) // Expected: nextHand has _inactive: 1 completedHand._inactive = [0, 0, 0]; // All active completedHand._intents = [1, 0, 0]; // Alice wants to pause completedHand.blindsOrStraddles = [10, 20, 0]; // Alice=SB, Bob=BB, Charlie=BTN // After rotation: [0, 10, 20] - Alice=BTN (not BB), so becomes inactive const nextHand = Hand.next(completedHand); expect(nextHand._inactive).toEqual([1, 0, 0]); // Alice now inactive expect(nextHand._intents).toEqual([1, 0, 0]); // Intent preserved }); it('should transition from _intents: 1 to inactive', () => { // Scenario: Wait-for-BB intent takes effect // Input: _inactive: 0, _intents: 1 // Expected: _inactive: 1, _intents: 1 (preserved) completedHand._inactive = [0, 0, 0]; completedHand._intents = [1, 0, 0]; // Alice wait-for-BB completedHand.blindsOrStraddles = [10, 20, 0]; // Alice=SB, not BB // After rotation: [0, 10, 20] - Alice=BTN (not BB), so becomes inactive const nextHand = Hand.next(completedHand); expect(nextHand._inactive).toEqual([1, 0, 0]); expect(nextHand._intents).toEqual([1, 0, 0]); // Preserved until BB }); it('should transition from _intents: 2 to inactive', () => { // Scenario: Simple pause intent takes effect // Input: _inactive: 0, _intents: 2 // Expected: _inactive: 1, _intents: 2 (preserved) completedHand._inactive = [0, 0, 0]; completedHand._intents = [2, 0, 0]; // Alice simple pause const nextHand = Hand.next(completedHand); expect(nextHand._inactive).toEqual([1, 0, 0]); expect(nextHand._intents).toEqual([2, 0, 0]); // Preserved }); it('should not transition if already inactive', () => { // Scenario: Already paused player // Input: _inactive: 1, _intents: 1 // Expected: Remains _inactive: 1 completedHand._inactive = [1, 0, 0]; // Already inactive completedHand._intents = [1, 0, 0]; completedHand.blindsOrStraddles = [10, 20, 0]; // Alice=SB // After rotation: [0, 10, 20] - Alice=BTN (not BB) const nextHand = Hand.next(completedHand); expect(nextHand._inactive).toEqual([1, 0, 0]); // Stays inactive expect(nextHand._intents).toEqual([1, 0, 0]); }); it('should handle transition during hand completion', () => { // Scenario: Intent changed during hand // Input: Changed from 0 to 1 during play // Expected: Becomes inactive in next hand completedHand._inactive = [0, 0, 0]; // Was active during hand completedHand._intents = [1, 0, 0]; // Changed intent during hand completedHand.blindsOrStraddles = [10, 20, 0]; // Alice=SB // After rotation: [0, 10, 20] - Alice=BTN (not BB) const nextHand = Hand.next(completedHand); expect(nextHand._inactive).toEqual([1, 0, 0]); // Now inactive expect(nextHand._intents).toEqual([1, 0, 0]); }); }); describe('new players joining next hand', () => { it('should activate player waiting to join', () => { // Scenario: Player joined mid-hand, now active // Input: _inactive: [0, 0, 1], _intents: [0, 0, 0] // Expected: _inactive: [0, 0, 0] in nextHand completedHand._inactive = [0, 0, 1]; // Charlie waiting to join completedHand._intents = [0, 0, 0]; // Charlie wants to play const nextHand = Hand.next(completedHand); expect(nextHand._inactive).toEqual([0, 0, 0]); expect(nextHand.players).toEqual(['Alice', 'Bob', 'Charlie']); }); it('should not activate if insufficient chips', () => { // Scenario: New player doesn't have enough for blinds // Input: _inactive: [0, 0, 1], finishingStacks: [1000, 1000, 0] // Expected: Player removed instead of activated completedHand._inactive = [0, 0, 1]; completedHand._intents = [0, 0, 0]; completedHand.finishingStacks = [1000, 1000, 0]; const nextHand = Hand.next(completedHand); expect(nextHand.players).toEqual(['Alice', 'Bob']); expect(nextHand.startingStacks).toEqual([1000, 1000]); }); }); describe('paused players returning', () => { it('should activate player returning at BB without payment', () => { // Scenario: Waited for BB position // Input: _intents: [1, 0, 0], now at BB, _deadBlinds: [20, 0, 0] // Expected: _inactive: [0, 0, 0], _deadBlinds: [0, 0, 0] completedHand._inactive = [1, 0, 0]; completedHand._intents = [1, 0, 0]; // Alice waiting for BB completedHand._deadBlinds = [20, 0, 0]; completedHand.blindsOrStraddles = [0, 10, 20]; // Next rotation puts Alice at BB (right rotation) const nextHand = Hand.next(completedHand); expect(nextHand._inactive).toEqual([0, 0, 0]); expect(nextHand._deadBlinds).toEqual([0, 0, 0]); }); it('should activate player returning early with debt preserved', () => { // Scenario: Stops pause, debt preserved for Game() to charge // Input: _intents: [0, 0, 0], _inactive: [1, 0, 0], _deadBlinds: [10, 0, 0] // Expected: _inactive: [0, 0, 0], stack unchanged, debt preserved completedHand.finishingStacks = [1000, 1000, 1000]; completedHand._inactive = [1, 0, 0]; completedHand._intents = [0, 0, 0]; // Alice wants to return completedHand._deadBlinds = [10, 0, 0]; const nextHand = Hand.next(completedHand); expect(nextHand._inactive).toEqual([0, 0, 0]); expect(nextHand.startingStacks).toEqual([1000, 1000, 1000]); // Stack unchanged expect(nextHand._deadBlinds).toEqual([10, 0, 0]); // Debt preserved for Game() }); it('should not activate if intent still paused', () => { // Scenario: Still on pause // Input: _intents: [2, 0, 0], _inactive: [1, 0, 0] // Expected: _inactive: [1, 0, 0] unchanged completedHand._inactive = [1, 0, 0]; completedHand._intents = [2, 0, 0]; // Alice still paused const nextHand = Hand.next(completedHand); expect(nextHand._inactive).toEqual([1, 0, 0]); }); }); describe('intent state transitions', () => { it('should handle pause request taking effect', () => { // Scenario: Was active with pause intent, now inactive // Input: _inactive: [0, 0, 0], _intents: [1, 0, 0] // Expected: _inactive: [1, 0, 0] in nextHand completedHand._inactive = [0, 0, 0]; completedHand._intents = [1, 0, 0]; // Alice wants to pause completedHand.blindsOrStraddles = [10, 20, 0]; // Alice=SB // After rotation: [0, 10, 20] - Alice=BTN (not BB) const nextHand = Hand.next(completedHand); expect(nextHand._inactive).toEqual([1, 0, 0]); expect(nextHand._intents).toEqual([1, 0, 0]); // Intent preserved }); it('should reset intents when returning at BB', () => { // Scenario: Reached BB position // Input: _intents: [1, 0, 0] at BB // Expected: _intents: [0, 0, 0] in nextHand completedHand._inactive = [1, 0, 0]; completedHand._intents = [1, 0, 0]; completedHand.blindsOrStraddles = [0, 10, 20]; // Next rotation puts Alice at BB (right rotation) const nextHand = Hand.next(completedHand); expect(nextHand._intents).toEqual([0, 0, 0]); expect(nextHand._inactive).toEqual([0, 0, 0]); }); it('should preserve pause intent if not at BB', () => { // Scenario: Still waiting for BB // Input: _intents: [1, 0, 0] not at BB // Expected: _intents: [1, 0, 0] unchanged completedHand._inactive = [1, 0, 0]; completedHand._intents = [1, 0, 0]; completedHand.blindsOrStraddles = [10, 20, 0]; // Alice at SB, not BB // After rotation: [0, 10, 20] - Alice=BTN (still not BB) const nextHand = Hand.next(completedHand); expect(nextHand._intents).toEqual([1, 0, 0]); // Preserved expect(nextHand._inactive).toEqual([1, 0, 0]); // Still inactive }); }); describe('State transitions based on intents', () => { it('should activate player with intent=1 when reaching BB position', () => { // Scenario: Player waiting for BB reaches BB position after rotation // Input: _intents=[1,0,0], _inactive=[1,0,0], blinds=[0,10,20] // Expected: Player becomes active and intent is cleared completedHand._intents = [1, 0, 0]; // Alice waiting for BB completedHand._inactive = [1, 0, 0]; // Alice inactive completedHand.blindsOrStraddles = [0, 10, 20]; // Alice at button const nextHand = Hand.next(completedHand); // After rotation: [20,0,10], Alice at BB position expect(nextHand._intents).toEqual([0, 0, 0]); // Intent cleared per spec expect(nextHand._inactive).toEqual([0, 0, 0]); // Alice becomes active at BB expect(nextHand.blindsOrStraddles).toEqual([20, 0, 10]); }); it('should keep player with intent=1 inactive when NOT at BB position', () => { // Scenario: Player waiting for BB, but not at BB position yet // Input: After rotation, player with intent=1 is NOT at BB // Expected: Player remains inactive with intent preserved completedHand._intents = [0, 1, 0]; // Bob waiting for BB completedHand._inactive = [0, 1, 0]; // Bob inactive completedHand.blindsOrStraddles = [10, 0, 20]; //