@idealic/poker-engine
Version:
Poker game engine and hand evaluator
574 lines (460 loc) • 19.6 kB
text/typescript
import { describe, expect } from 'vitest';
import { Game } from '../../Game';
import { getCurrentPlayerIndex } from '../../game/position';
import { applyAction } from '../../game/progress';
import type { Hand } from '../../Hand';
/**
* @instructions
*
* Testing principles demonstrated in this file:
*
* 1. State Validation First:
* - Verify initial betting state before actions
* - Check getPlayerIndex to confirm correct action order
* - Validate betting completion conditions
*
* 2. Complete Action Sequences:
* - Each test shows complete sequence of actions
* - Actions are commented to show their meaning
* - Include all necessary setup (dealing cards, etc)
*
* 3. State Verification:
* - Check betting state after each action
* - Verify both direct effects (bettingComplete) and side effects (player states)
* - Confirm correct action order transitions
*
* 4. Edge Cases:
* - Test special scenarios (heads-up, all-in)
* - Verify folded player handling
* - Check boundary conditions
*
* 5. Clear Test Organization:
* - Group by betting scenario type
* - Each test focuses on one specific behavior
* - Descriptive test names
*/
const sampleGame: Hand = {
variant: 'NT',
players: ['p1', 'p2', 'p3'],
startingStacks: [1000, 1000, 1000],
blindsOrStraddles: [10, 20, 0],
antes: [0, 0, 0],
actions: [],
minBet: 20,
};
describe('Betting State', () => {
describe('standard betting rounds', () => {
it('should not complete hand until showdown', () => {
const hand: Hand = {
variant: 'NT',
minBet: 100,
actions: [
'd dh p1 3hTc',
'd dh p2 3sQc',
'p1 cbr 250',
'p2 cbr 600',
'p1 cbr 2500',
'p2 cbr 3500',
'p1 cbr 10000',
'p2 cbr 22000',
'p1 f',
],
table: 'croupierTable_220182',
antes: [0, 0],
blindsOrStraddles: [50, 100],
startingStacks: [100000, 100000],
players: ['Leleka', 'Romulus'],
seats: [0, 3],
author: '',
timeLimit: 10,
venue: 'pokerrrr',
_heroIds: ['Leleka', 'Romulus'],
_venueIds: ['Leleka', 'Romulus'],
_managerUid: 'manager_123',
hand: 1,
seed: 459717,
time: '2025-04-21T15:05:21.537Z',
_timestamp: 1745247921537,
_croupierId: 'croupier_123',
};
const game = Game(hand);
// no showdown, should be true
expect(game.isComplete).toBe(true);
});
it('should complete betting round when all players call a bet', () => {
const game = Game(sampleGame);
// Initial state - awaiting dealer
expect(game.isBettingComplete).toBe(false);
expect(getCurrentPlayerIndex(game)).toBe(-1); // Awaiting initial deal
// Deal cards one by one, should still await dealer
applyAction(game, 'd dh p1 AhKh');
expect(getCurrentPlayerIndex(game)).toBe(-1); // Still dealing
applyAction(game, 'd dh p2 QhJh');
expect(getCurrentPlayerIndex(game)).toBe(-1); // Still dealing
applyAction(game, 'd dh p3 ThTd');
expect(getCurrentPlayerIndex(game)).toBe(2); // UTG acts first preflop
// Complete preflop action
applyAction(game, 'p3 cc'); // UTG calls
expect(getCurrentPlayerIndex(game)).toBe(0); // Action to BTN
applyAction(game, 'p1 cc'); // BTN calls
expect(getCurrentPlayerIndex(game)).toBe(1); // Action to SB
applyAction(game, 'p2 cc'); // SB checks
expect(getCurrentPlayerIndex(game)).toBe(-1); // Betting complete, dealer to act
// Deal flop
applyAction(game, 'd db AcKcQc');
// Verify initial flop state
expect(game.isBettingComplete).toBe(false);
expect(getCurrentPlayerIndex(game)).toBe(0); // First after button
expect(game.buttonIndex).toBe(2);
expect(game.lastBetAction).toBeUndefined();
// Player 1 checks
applyAction(game, 'p1 cc');
expect(getCurrentPlayerIndex(game)).toBe(1); // Action to SB
// Player 2 bets
applyAction(game, 'p2 cbr 100');
expect(game.isBettingComplete).toBe(false);
expect(game.lastBetAction).toBe('p2 cbr 100');
expect(getCurrentPlayerIndex(game)).toBe(2); // Action to BB
// Player 3 calls
applyAction(game, 'p3 cc');
expect(game.isBettingComplete).toBe(false);
expect(getCurrentPlayerIndex(game)).toBe(0); // Back to BTN
// Player 1 calls, completing the betting round
applyAction(game, 'p1 cc');
expect(game.isBettingComplete).toBe(true);
expect(getCurrentPlayerIndex(game)).toBe(-1); // Awaiting dealer
expect(game.players.every(p => p.hasActed)).toBe(true);
});
it('should complete betting round when all remaining players fold to a bet', () => {
const game = Game(sampleGame);
// Initial state - awaiting dealer
expect(getCurrentPlayerIndex(game)).toBe(-1);
// Setup game state
applyAction(game, 'd dh p1 AhKh');
expect(getCurrentPlayerIndex(game)).toBe(-1); // Still dealing
applyAction(game, 'd dh p2 QhJh');
expect(getCurrentPlayerIndex(game)).toBe(-1); // Still dealing
applyAction(game, 'd dh p3 ThTd');
expect(getCurrentPlayerIndex(game)).toBe(2); // UTG acts first preflop
// Complete preflop action
applyAction(game, 'p3 cc'); // UTG calls
expect(getCurrentPlayerIndex(game)).toBe(0); // Action to BTN
applyAction(game, 'p1 cc'); // BTN calls
expect(getCurrentPlayerIndex(game)).toBe(1); // Action to SB
applyAction(game, 'p2 cc'); // SB checks
expect(getCurrentPlayerIndex(game)).toBe(-1); // Betting complete, dealer to act
// Deal flop
applyAction(game, 'd db AcKcQc');
// Verify initial flop state
expect(game.isBettingComplete).toBe(false);
expect(getCurrentPlayerIndex(game)).toBe(0); // First after button
// Player 1 checks
applyAction(game, 'p1 cc');
expect(getCurrentPlayerIndex(game)).toBe(1); // Action to SB
// Player 2 bets
applyAction(game, 'p2 cbr 100');
expect(game.isBettingComplete).toBe(false);
expect(game.lastBetAction).toBe('p2 cbr 100');
expect(getCurrentPlayerIndex(game)).toBe(2); // Action to BB
// Player 3 folds
applyAction(game, 'p3 f');
expect(game.isBettingComplete).toBe(false);
expect(game.players[2].hasFolded).toBe(true);
expect(getCurrentPlayerIndex(game)).toBe(0); // Action to BTN
// Player 1 folds, completing the betting round
applyAction(game, 'p1 f');
expect(game.isBettingComplete).toBe(true);
expect(game.players[0].hasFolded).toBe(true);
expect(getCurrentPlayerIndex(game)).toBe(-1); // Awaiting dealer
});
});
describe('position and action order', () => {
it('should handle heads-up position rules with SB acting first preflop and BB first postflop', () => {
const headsUpGame: Hand = {
...sampleGame,
players: ['p1', 'p2'],
startingStacks: [1000, 1000],
blindsOrStraddles: [10, 20],
antes: [0, 0],
};
const game = Game(headsUpGame);
expect(getCurrentPlayerIndex(game)).toBe(-1); // Awaiting initial deal
// Setup initial state
applyAction(game, 'd dh p1 AhKh');
expect(getCurrentPlayerIndex(game)).toBe(-1); // Still dealing
applyAction(game, 'd dh p2 QhJh');
expect(getCurrentPlayerIndex(game)).toBe(0); // SB acts first in heads-up preflop
// Verify preflop state (SB acts first in heads-up)
expect(game.buttonIndex).toBe(0); // Button is SB in heads-up
// Complete preflop action
applyAction(game, 'p1 cc'); // SB calls
expect(getCurrentPlayerIndex(game)).toBe(1); // Action to BB
applyAction(game, 'p2 cc'); // BB checks
expect(getCurrentPlayerIndex(game)).toBe(-1); // Betting complete, dealer to act
// Deal flop
applyAction(game, 'd db AcKcQc');
// Verify postflop state (BB acts first in heads-up)
expect(getCurrentPlayerIndex(game)).toBe(1); // BB acts first postflop
expect(game.isBettingComplete).toBe(false);
// BB checks
applyAction(game, 'p2 cc');
expect(getCurrentPlayerIndex(game)).toBe(0); // Action to SB/BTN
// SB/BTN checks
applyAction(game, 'p1 cc');
expect(getCurrentPlayerIndex(game)).toBe(-1); // Betting complete, dealer to act
});
it('should maintain correct action order when players fold preflop', () => {
const game = Game(sampleGame);
expect(getCurrentPlayerIndex(game)).toBe(-1); // Awaiting initial deal
// Setup initial state
applyAction(game, 'd dh p1 AhKh');
expect(getCurrentPlayerIndex(game)).toBe(-1); // Still dealing
applyAction(game, 'd dh p2 QhJh');
expect(getCurrentPlayerIndex(game)).toBe(-1); // Still dealing
applyAction(game, 'd dh p3 ThTd');
expect(getCurrentPlayerIndex(game)).toBe(2); // UTG acts first preflop
// Verify preflop state
expect(game.buttonIndex).toBe(2);
// Player 3 (UTG) folds
applyAction(game, 'p3 f');
expect(game.players[2].hasFolded).toBe(true);
expect(getCurrentPlayerIndex(game)).toBe(0); // Action to BTN
// Complete preflop action
applyAction(game, 'p1 cc'); // BTN calls
expect(getCurrentPlayerIndex(game)).toBe(1); // Action to SB
applyAction(game, 'p2 cc'); // SB checks
expect(getCurrentPlayerIndex(game)).toBe(-1); // Betting complete, dealer to act
// Deal flop
applyAction(game, 'd db AcKcQc');
// Verify postflop state
expect(getCurrentPlayerIndex(game)).toBe(0); // First active player after button
expect(game.isBettingComplete).toBe(false);
// BTN checks
applyAction(game, 'p1 cc');
expect(getCurrentPlayerIndex(game)).toBe(1); // Action to SB
// SB checks
applyAction(game, 'p2 cc');
expect(getCurrentPlayerIndex(game)).toBe(-1); // Betting complete, dealer to act
});
it('should handle betting completion when player goes all-in and others call', () => {
const smallStackGame = {
...sampleGame,
startingStacks: [50, 1000, 1000],
};
const game = Game(smallStackGame);
expect(getCurrentPlayerIndex(game)).toBe(-1); // Awaiting initial deal
// Setup initial state
applyAction(game, 'd dh p1 AhKh');
expect(getCurrentPlayerIndex(game)).toBe(-1); // Still dealing
applyAction(game, 'd dh p2 QhJh');
expect(getCurrentPlayerIndex(game)).toBe(-1); // Still dealing
applyAction(game, 'd dh p3 ThTd');
expect(getCurrentPlayerIndex(game)).toBe(2); // UTG acts first preflop
// Verify preflop state
expect(game.players[0].stack).toBe(40); // After SB
expect(game.players[0].roundBet).toBe(10);
// UTG folds
applyAction(game, 'p3 f');
expect(getCurrentPlayerIndex(game)).toBe(0); // Action to BTN
// Player 1 goes all-in
applyAction(game, 'p1 cbr 50');
expect(game.isBettingComplete).toBe(false);
expect(game.lastBetAction).toBe('p1 cbr 50');
expect(game.players[0].isAllIn).toBe(true);
expect(game.players[0].stack).toBe(0);
expect(getCurrentPlayerIndex(game)).toBe(1); // Action to SB
// SB calls
applyAction(game, 'p2 cc');
expect(game.isBettingComplete).toBe(true); // No more action possible
expect(getCurrentPlayerIndex(game)).toBe(-1); // Betting complete, dealer to act
expect(game.players.every(p => p.hasActed || p.hasFolded)).toBe(true);
});
it('should handle betting completion when last player folds to all-in after others called', () => {
const game = Game(sampleGame);
// Setup initial state
applyAction(game, 'd dh p1 AhKh');
applyAction(game, 'd dh p2 QhJh');
applyAction(game, 'd dh p3 ThTd');
expect(getCurrentPlayerIndex(game)).toBe(2); // UTG acts first preflop
// Preflop action
applyAction(game, 'p3 cbr 200'); // UTG raises
expect(getCurrentPlayerIndex(game)).toBe(0);
applyAction(game, 'p1 cc'); // BTN calls
expect(getCurrentPlayerIndex(game)).toBe(1);
applyAction(game, 'p2 cc'); // SB calls
expect(game.isBettingComplete).toBe(true);
expect(getCurrentPlayerIndex(game)).toBe(-1);
// Deal flop
applyAction(game, 'd db AcKcQc');
expect(getCurrentPlayerIndex(game)).toBe(0);
// BTN goes all-in
applyAction(game, 'p1 cbr 800');
expect(getCurrentPlayerIndex(game)).toBe(1);
expect(game.isBettingComplete).toBe(false);
// SB calls all-in
applyAction(game, 'p2 cc');
expect(getCurrentPlayerIndex(game)).toBe(2);
expect(game.isBettingComplete).toBe(false);
// BB folds to all-in after others called
applyAction(game, 'p3 f');
expect(game.isBettingComplete).toBe(true);
expect(getCurrentPlayerIndex(game)).toBe(-1);
});
it('should handle betting when multiple players go all-in with different stack sizes', () => {
const shortStackGame = {
...sampleGame,
startingStacks: [200, 400, 800],
};
const game = Game(shortStackGame);
// Setup initial state
applyAction(game, 'd dh p1 AhKh');
applyAction(game, 'd dh p2 QhJh');
applyAction(game, 'd dh p3 ThTd');
expect(getCurrentPlayerIndex(game)).toBe(2); // UTG acts first preflop
// UTG goes all-in with largest stack
applyAction(game, 'p3 cbr 800');
expect(getCurrentPlayerIndex(game)).toBe(0);
expect(game.isBettingComplete).toBe(false);
// BTN goes all-in with smallest stack
applyAction(game, 'p1 cc');
expect(game.players[0].isAllIn).toBe(true);
expect(game.players[0].stack).toBe(0);
expect(getCurrentPlayerIndex(game)).toBe(1);
expect(game.isBettingComplete).toBe(false);
// SB goes all-in with medium stack
applyAction(game, 'p2 cc');
expect(game.players[1].isAllIn).toBe(true);
expect(game.players[1].stack).toBe(0);
expect(game.isBettingComplete).toBe(true);
expect(getCurrentPlayerIndex(game)).toBe(-1);
});
it('should handle betting when player goes all-in for less than previous bet', () => {
const shortStackGame = {
...sampleGame,
startingStacks: [50, 1000, 1000],
};
const game = Game(shortStackGame);
// Setup initial state
applyAction(game, 'd dh p1 AhKh');
applyAction(game, 'd dh p2 QhJh');
applyAction(game, 'd dh p3 ThTd');
expect(getCurrentPlayerIndex(game)).toBe(2); // UTG acts first preflop
// UTG raises big
applyAction(game, 'p3 cbr 200');
expect(getCurrentPlayerIndex(game)).toBe(0);
expect(game.isBettingComplete).toBe(false);
// BTN all-in for less than the raise
applyAction(game, 'p1 cc');
expect(game.players[0].isAllIn).toBe(true);
expect(game.players[0].stack).toBe(0);
expect(getCurrentPlayerIndex(game)).toBe(1);
expect(game.isBettingComplete).toBe(false);
// SB calls full amount
applyAction(game, 'p2 cc');
expect(game.isBettingComplete).toBe(true);
expect(getCurrentPlayerIndex(game)).toBe(-1);
});
it('should handle betting when all players but one are all-in', () => {
const shortStackGame = {
...sampleGame,
startingStacks: [50, 60, 1000],
};
const game = Game(shortStackGame);
// Setup initial state
applyAction(game, 'd dh p1 AhKh');
applyAction(game, 'd dh p2 QhJh');
applyAction(game, 'd dh p3 ThTd');
expect(getCurrentPlayerIndex(game)).toBe(2); // UTG acts first preflop
// UTG raises
applyAction(game, 'p3 cbr 100');
expect(getCurrentPlayerIndex(game)).toBe(0);
expect(game.isBettingComplete).toBe(false);
// BTN all-in for less
applyAction(game, 'p1 cc');
expect(game.players[0].isAllIn).toBe(true);
expect(getCurrentPlayerIndex(game)).toBe(1);
expect(game.isBettingComplete).toBe(false);
// SB all-in for less
applyAction(game, 'p2 cc');
expect(game.players[1].isAllIn).toBe(true);
expect(game.isBettingComplete).toBe(true);
expect(getCurrentPlayerIndex(game)).toBe(-1);
});
});
describe('complex betting scenarios', () => {
it('should track correct betting amounts in flush vs pair showdown hand', () => {
const hand: Hand = {
variant: 'NT',
players: ['dddocky', 'Duke Croix', 'color_singleton', 'Klemtonius', 'HighCardJasper'],
startingStacks: [4768, 8950, 10000, 68607, 18608],
blindsOrStraddles: [0, 50, 100, 0, 0],
antes: [0, 0, 0, 0, 0],
minBet: 100,
actions: [],
rake: 938,
};
const game = Game(hand);
// Deal cards
applyAction(game, 'd dh p1 ????');
applyAction(game, 'd dh p2 Td8s');
applyAction(game, 'd dh p3 ????');
applyAction(game, 'd dh p4 QcTh');
applyAction(game, 'd dh p5 JhAc');
// Preflop
applyAction(game, 'p4 cbr 200');
expect(game.players[3].totalBet).toBe(200);
applyAction(game, 'p5 cc');
expect(game.players[4].totalBet).toBe(200);
applyAction(game, 'p1 f');
expect(game.players[0].hasFolded).toBe(true);
applyAction(game, 'p2 f');
expect(game.players[1].hasFolded).toBe(true);
expect(game.players[1].totalBet).toBe(50); // SB remains
applyAction(game, 'p3 cc');
expect(game.players[2].totalBet).toBe(200);
expect(game.pot).toBe(650); // SB(50) + BB(200) + Klemtonius(200) + HighCardJasper(200)
// Flop
applyAction(game, 'd db Kc6c8c');
applyAction(game, 'p3 cc');
expect(game.players[2].roundBet).toBe(0);
applyAction(game, 'p4 cbr 543');
expect(game.players[3].roundBet).toBe(543);
expect(game.players[3].totalBet).toBe(743);
applyAction(game, 'p5 cc');
expect(game.players[4].roundBet).toBe(543);
expect(game.players[4].totalBet).toBe(743);
applyAction(game, 'p3 f');
expect(game.players[2].hasFolded).toBe(true);
expect(game.pot).toBe(1736); // Previous 650 + 2 * 543
// Turn
applyAction(game, 'd db 2s');
applyAction(game, 'p4 cc');
expect(game.players[3].roundBet).toBe(0);
applyAction(game, 'p5 cc');
expect(game.players[4].roundBet).toBe(0);
expect(game.pot).toBe(1736); // No new bets
// River
applyAction(game, 'd db 9c');
applyAction(game, 'p4 cbr 1830');
expect(game.players[3].roundBet).toBe(1830);
expect(game.players[3].totalBet).toBe(2573);
applyAction(game, 'p5 cbr 7560');
expect(game.players[4].roundBet).toBe(7560);
expect(game.players[4].totalBet).toBe(8303);
applyAction(game, 'p4 cc');
expect(game.players[3].roundBet).toBe(7560);
expect(game.players[3].totalBet).toBe(8303);
// Final pot should be 16118 (16856 - 938 rake)
expect(game.pot).toBe(16856); // Total bets: preflop(650) + flop(1086) + river(15120)
expect(game.rake).toBe(938);
// Show cards
applyAction(game, 'p5 sm JhAc');
applyAction(game, 'p4 sm QcTh');
expect(game.isBettingComplete).toBe(true);
expect(game.isShowdown).toBe(true);
// Verify final state
expect(game.isComplete).toBe(true);
expect(game.pot).toBe(15918);
});
});
});