UNPKG

@idealic/poker-engine

Version:

Poker game engine and hand evaluator

315 lines (262 loc) 10.7 kB
/** * @instructions Actions should be applied to the table immediately after they are generated */ import { describe, expect, it } from 'vitest'; import { Game } from '../../Game'; import { bet, call, check, dealBoard, fold, raise } from '../../game/commands'; import { applyAction } from '../../game/progress'; import { canBet, canCall, canCheck, canFold, canRaise } from '../../game/validation'; import type { Hand } from '../../Hand'; const baseHand: Hand = { variant: 'NT', currency: 'USD', players: ['Hero', 'Villain', 'BB'], startingStacks: [1000, 1000, 1000], blindsOrStraddles: [0, 10, 20], antes: [], time: new Date().toISOString(), timeZone: 'UTC', minBet: 20, actions: ['d dh p1 AhKh', 'd dh p2 QhJh', 'd dh p3 ThTd'], }; describe('Table State', () => { describe('canCheck', () => { it('allows BB to check after SB calls preflop', () => { const game = Game(baseHand); expect(canCall(game, 0)).toBe(true); applyAction(game, call(game, 0)); // Big blind player should be able to check expect(game.players[2].totalBet).toBe(20); // BB player expect(game.players[2].hasActed).toBe(false); expect(game.players[2].hasFolded).toBe(false); expect(canCheck(game, 2)).toBe(true); // Verify check action works applyAction(game, check(game, 1)); expect(game.players[1].hasActed).toBe(true); expect(game.players[1].totalBet).toBe(20); }); it('prevents SB from checking when facing BB', () => { const game = Game(baseHand); // First player has only posted small blind expect(game.players[1].totalBet).toBe(10); expect(game.players[1].hasActed).toBe(false); expect(game.players[1].hasFolded).toBe(false); expect(canCheck(game, 1)).toBe(false); }); }); describe('canCall', () => { it('allows UTG to call BB preflop', () => { const game = Game(baseHand); // First player faces BB of 20 and has only posted 0 expect(game.players[0].totalBet).toBe(0); expect(game.players[0].stack).toBe(1000); expect(game.players[0].hasActed).toBe(false); expect(game.players[0].hasFolded).toBe(false); expect(canCall(game, 0)).toBe(true); // Verify call action works applyAction(game, call(game, 0)); expect(game.players[0].hasActed).toBe(true); expect(game.players[0].totalBet).toBe(20); }); it('prevents BB from calling when no bets to call', () => { const game = Game(baseHand); applyAction(game, call(game, 0)); // Player who posted BB should not be able to call expect(game.players[2].totalBet).toBe(20); expect(game.players[2].hasActed).toBe(false); expect(game.players[2].hasFolded).toBe(false); expect(canCall(game, 2)).toBe(false); }); }); describe('canBet', () => { it('allows first player to bet on flop', () => { // Complete preflop action const game = Game(baseHand); applyAction(game, call(game, 0)); applyAction(game, call(game, 1)); applyAction(game, check(game, 2)); // Deal flop applyAction(game, dealBoard(game, ['Ac', 'Kc', 'Qc'])); // First player should be able to bet on flop expect(game.players[0].hasActed).toBe(false); expect(game.players[0].hasFolded).toBe(false); expect(game.isComplete).toBe(false); expect(game.bet).toBe(0); expect(canBet(game, 0)).toBe(true); // Verify bet action works applyAction(game, bet(game, 1, 50)); expect(game.players[1].hasActed).toBe(true); expect(game.players[1].totalBet).toBe(70); // 20 from preflop call + 50 from flop bet expect(game.bet).toBe(50); }); it('prevents betting preflop when BB is posted', () => { const game = Game(baseHand); // Can't bet preflop because BB is posted expect(game.bet).toBe(20); expect(canBet(game, 0)).toBe(false); }); }); describe('canRaise', () => { it('allows UTG to raise over BB preflop', () => { const game = Game(baseHand); // First player can raise over BB expect(game.players[0].stack).toBe(1000); expect(game.bet).toBe(20); expect(game.players[0].hasActed).toBe(false); expect(game.players[0].hasFolded).toBe(false); expect(canRaise(game, 0)).toBe(true); // Verify raise action works applyAction(game, raise(game, 0, 60)); expect(game.players[0].hasActed).toBe(true); expect(game.players[0].totalBet).toBe(60); expect(game.bet).toBe(60); }); it('prevents raising on flop with no bets', () => { // Complete preflop action const game = Game(baseHand); applyAction(game, call(game, 0)); applyAction(game, call(game, 1)); applyAction(game, check(game, 2)); // Deal flop applyAction(game, dealBoard(game, ['Ac', 'Kc', 'Qc'])); // Can't raise on flop with no bets expect(game.bet).toBe(0); expect(canRaise(game, 0)).toBe(false); }); it('allows raising all-in with insufficient stack for min-raise', () => { const smallStackHand = { ...baseHand, // Player 0 (UTG) has only 30 chips. // BB is 20. Min raise is to 40 (20 call + 20 raise). // Player 0 needs 40 chips total to make a valid raise. // Stack 30 is less than 40. startingStacks: [30, 1000, 1000], }; const game = Game(smallStackHand); // First player can't make a FULL raise over BB with only 30 chips (needs 40) expect(game.players[0].stack).toBe(30); expect(game.bet).toBe(20); expect(game.minBet).toBe(20); // But they CAN raise all-in (incomplete raise) expect(canRaise(game, 0)).toBe(true); // Verify specific amounts // Cannot raise to min-raise (40) because stack is 30 expect(canRaise(game, 0, 40)).toBe(false); // Can raise all-in (30) expect(canRaise(game, 0, 30)).toBe(true); }); it('prevents capped player from re-raising after incomplete raise (all-in)', () => { // Scenario: // P1 bets 100 // P2 calls 100 // P3 goes all-in for 120 (incomplete raise, only +20) // Action back to P1. // P1 has already acted on the 100 bet. // The new raise is incomplete (less than min raise of 100). // P1 should be CAPPED (cannot raise, can only call or fold). const cappedHand = { ...baseHand, startingStacks: [1000, 1000, 120], // P3 short stack // implicitly uses baseHand.actions which deals cards }; const game = Game(cappedHand); // Preflop: BB is 20. // P1 (UTG) raises to 100 applyAction(game, raise(game, 0, 100)); expect(game.bet).toBe(100); expect(game.lastCompleteBet).toBe(100); expect(game.minBet).toBe(80); // Raise was 80 (20->100) // P2 (SB) calls 100 applyAction(game, call(game, 1)); expect(game.players[1].roundBet).toBe(100); // P3 (BB) goes all-in for 120 // This is a raise of 20 (120 - 100). // Min raise required was 80 (to 180). // So this is an INCOMPLETE raise. applyAction(game, 'p3 cbr 120'); // Manual all-in command expect(game.bet).toBe(120); expect(game.players[2].isAllIn).toBe(true); // Check state updates // The raise was incomplete, so minBet and lastCompleteBet should NOT have updated from P1's action? // P1's action: minBet = 80, lastCompleteBet = 100. // P3's action: incomplete. expect(game.lastCompleteBet).toBe(100); expect(game.minBet).toBe(80); // Action returns to P1 (UTG) expect(game.nextPlayerIndex).toBe(0); // P1 has already bet 100 (matches lastCompleteBet). // P1 is facing 120 (incomplete raise). // P1 should be capped. expect(canRaise(game, 0)).toBe(false); expect(canCall(game, 0)).toBe(true); expect(canFold(game, 0)).toBe(true); }); it('allows uncapped player to raise after incomplete raise', () => { // Scenario: // P1 bets 100 // P2 calls 100 // P3 goes all-in for 120 (incomplete raise) // Suppose there was a P4 who hasn't acted yet. // P4 is NOT capped and can raise. const fourPlayerHand = { ...baseHand, players: ['P1', 'P2', 'P3', 'P4'], startingStacks: [1000, 1000, 120, 1000], blindsOrStraddles: [0, 0, 10, 20], // P3=SB, P4=BB actions: ['d dh p1 AhKh', 'd dh p2 QhJh', 'd dh p3 ThTd', 'd dh p4 2s3s'], }; // Order: P1, P2, P3(SB), P4(BB) const game = Game(fourPlayerHand); // P1 bets 100 applyAction(game, raise(game, 0, 100)); // P2 calls 100 applyAction(game, call(game, 1)); // P3 (SB) goes all-in for 120 (incomplete raise) applyAction(game, 'p3 cbr 120'); expect(game.bet).toBe(120); expect(game.lastCompleteBet).toBe(100); // Action moves to P4 (BB). P4 has NOT acted on the 100 bet yet. // P4 should be able to raise. expect(game.nextPlayerIndex).toBe(3); expect(canRaise(game, 3)).toBe(true); }); it('enforces minimum raise amount correctly', () => { const game = Game(baseHand); // BB is 20. Min raise is to 40. // canRaise just returns boolean. expect(canRaise(game, 0)).toBe(true); // Can raise generally // The 'raise' command helper calculates the min correctly: const raiseCmd = raise(game, 0, 30); // Should auto-correct 30 to min (40) expect(raiseCmd).toContain('40'); }); }); describe('canFold', () => { it('allows UTG to fold facing BB preflop', () => { const game = Game(baseHand); // First player can fold facing BB expect(game.players[0].totalBet).toBe(0); expect(game.bet).toBe(20); expect(game.players[0].hasActed).toBe(false); expect(game.players[0].hasFolded).toBe(false); expect(canFold(game, 0)).toBe(true); // Verify fold action works applyAction(game, fold(game, 0)); expect(game.players[0].hasActed).toBe(true); expect(game.players[0].hasFolded).toBe(true); }); it('allows folding on flop even with no bets', () => { // Complete preflop action const game = Game(baseHand); applyAction(game, call(game, 0)); applyAction(game, call(game, 1)); applyAction(game, check(game, 2)); // Deal flop applyAction(game, dealBoard(game, ['Ac', 'Kc', 'Qc'])); // No bet on the flop yet expect(game.bet).toBe(0); // Player 0 should still be allowed to fold (even though it's a bad play) expect(canFold(game, 0)).toBe(true); }); }); });