@idealic/poker-engine
Version:
Poker game engine and hand evaluator
501 lines (406 loc) • 17.9 kB
text/typescript
import { describe, expect, it } from 'vitest';
import {
getActionAmount,
getActionCards,
getActionPlayerIndex,
getActionType,
} from '../../../game/position';
import * as Poker from '../../../index';
import { BASE_HAND } from './fixtures/baseHand';
/**
* Delegation Tests for Hand API
*
* Purpose: Test Hand methods that purely delegate to Game namespace:
* 1. applyAction - Appends action if Game.canApplyAction() returns true
* 2. advance - Creates Game, calls Game.advance(), returns resulting Hand
* 3. handleTimeOut - Creates Game, handles timeout logic, returns resulting Hand
* 4. canApplyAction - Determines if an action can be applied to the Hand
* 5. finish - Extracts finishing data from a completed game and updates the hand with final state
*
* These tests verify pure delegation without implementing game logic
*/
describe('Hand Delegation to Game', () => {
describe('Hand.applyAction', () => {
it('should append valid action to Hand', () => {
const hand = Poker.Hand({
...BASE_HAND,
actions: BASE_HAND.actions.slice(0, 3), // Only hole cards dealt
});
// Apply a valid action
const newHand = Poker.Hand.applyAction(hand, 'p1 cc');
// Should append action
expect(newHand.actions).toHaveLength(4);
expect(newHand.actions[3]).toBe('p1 cc');
// Verify action components
expect(getActionType(newHand.actions[3])).toBe('cc');
expect(getActionPlayerIndex(newHand.actions[3])).toBe(0); // 0-based
expect(getActionAmount(newHand.actions[3])).toBe(0);
// Original unchanged
expect(hand.actions).toHaveLength(3);
});
it('should not append invalid action', () => {
const hand = Poker.Hand({
...BASE_HAND,
actions: BASE_HAND.actions.slice(0, 3), // Only hole cards dealt
});
// Try to apply invalid action (player 4 doesn't exist)
expect(() => Poker.Hand.applyAction(hand, 'p4 f')).toThrow();
});
it('should delegate validation to Game.canApplyAction', () => {
const hand = Poker.Hand({
...BASE_HAND,
actions: [],
});
// Dealer action should be first
const dealerHand = Poker.Hand.applyAction(hand, 'd dh p1 AsKs');
// Verify dealer action components
const dealerAction = dealerHand.actions[0];
expect(getActionType(dealerAction)).toBe('dh');
expect(getActionPlayerIndex(dealerAction)).toBe(0);
expect(getActionCards(dealerAction)).toEqual(['As', 'Ks']);
// Player action without dealing should fail
expect(() => Poker.Hand.applyAction(hand, 'p1 f')).toThrow();
});
it('should maintain immutability', () => {
const hand = Poker.Hand({ ...BASE_HAND, actions: BASE_HAND.actions.slice(0, 3) });
const originalJson = JSON.stringify(hand);
Poker.Hand.applyAction(hand, 'p1 f');
expect(JSON.stringify(hand)).toBe(originalJson);
});
});
describe('Hand.advance', () => {
it('should advance hand through dealer actions', () => {
const hand = Poker.Hand({
variant: 'NT',
players: ['Alice', 'Bob', 'Charlie'],
antes: [0, 0, 0],
startingStacks: [1000, 1000, 1000],
blindsOrStraddles: [0, 10, 20],
minBet: 20,
seed: 12345,
actions: [],
});
// Advance should deal hole cards
const advanced = Poker.Hand.advance(hand);
// Should have dealer actions added
expect(advanced.actions.length).toBeGreaterThan(0);
// Verify first action is dealing hole cards
const firstAction = advanced.actions[0];
expect(getActionType(firstAction)).toBe('dh');
expect(getActionPlayerIndex(firstAction)).toBe(0);
expect(getActionCards(firstAction)).toBeDefined();
expect(getActionCards(firstAction)?.length).toBe(2);
});
it('should handle auto-actions like timeout', () => {
// Test non-showdown timeout (should fold)
const now = Date.now();
const handPreflop = Poker.Hand({
...BASE_HAND,
timeLimit: 30,
actions: [
BASE_HAND.actions[0], // d dh p1
BASE_HAND.actions[1], // d dh p2
BASE_HAND.actions[2], // d dh p3
`p1 cc #${now - 40000}`, // Alice calls, 40 seconds ago
// Bob is next to act and will timeout
],
});
const advancedPreflop = Poker.Hand.advance(handPreflop);
// Bob should fold due to timeout (not showdown)
const newAction = advancedPreflop.actions[advancedPreflop.actions.length - 1];
expect(getActionType(newAction)).toBe('f');
expect(getActionPlayerIndex(newAction)).toBe(1); // Bob (player index 1)
// Test showdown timeout (should show/muck)
const handShowdown = Poker.Hand({
...BASE_HAND,
timeLimit: 30,
actions: [
...BASE_HAND.actions.slice(0, 18), // All the way to showdown
// At showdown, players need to show/muck
],
});
const advancedShowdown = Poker.Hand.advance(handShowdown);
// Should add show/muck action
const showdownAction = advancedShowdown.actions[advancedShowdown.actions.length - 1];
expect(getActionType(showdownAction)).toBe('sm'); // Show cards at showdown
});
it('should maintain immutability', () => {
const hand = Poker.Hand({
...BASE_HAND,
actions: [],
});
const originalJson = JSON.stringify(hand);
Poker.Hand.advance(hand);
expect(JSON.stringify(hand)).toBe(originalJson);
});
it('should delegate to Game.advance', () => {
// Use a hand state where advance will deal the flop
const hand = Poker.Hand({
...BASE_HAND,
actions: BASE_HAND.actions.slice(0, 6), // Through preflop betting
});
const advanced = Poker.Hand.advance(hand);
// Should return new Hand with game progression
expect(advanced).not.toBe(hand);
expect(advanced.actions.length).toBeGreaterThan(hand.actions.length);
// Should have dealt the flop
const newAction = advanced.actions[6];
expect(getActionType(newAction)).toBe('db'); // Deal board
expect(getActionCards(newAction)).toBeDefined();
expect(getActionCards(newAction)?.length).toBe(3); // Flop has 3 cards
});
});
describe('Hand.handleTimeOut', () => {
it('should handle timeout for current player', () => {
const hand = Poker.Hand({
...BASE_HAND,
timeLimit: 30,
actions: [
BASE_HAND.actions[0], // d dh p1
BASE_HAND.actions[1], // d dh p2
BASE_HAND.actions[2], // d dh p3
// Alice is next to act and has timed out
],
});
const handled = Poker.Hand.handleTimeOut(hand);
// Should add fold action for Alice
expect(handled.actions.length).toBe(4);
const newAction = handled.actions[3];
expect(getActionType(newAction)).toBe('f');
expect(getActionPlayerIndex(newAction)).toBe(0); // Alice
});
it('should fold in betting round', () => {
const hand = Poker.Hand({
...BASE_HAND,
timeLimit: 30,
actions: BASE_HAND.actions.slice(0, 7), // After flop is dealt, Bob to act
});
const handled = Poker.Hand.handleTimeOut(hand);
// Bob should fold due to timeout
const lastAction = handled.actions[handled.actions.length - 1];
expect(getActionType(lastAction)).toBe('f');
expect(getActionPlayerIndex(lastAction)).toBe(1); // Bob
});
it('should show cards in showdown', () => {
const hand = Poker.Hand({
...BASE_HAND,
timeLimit: 30,
actions: BASE_HAND.actions.slice(0, 20), // At showdown, Alice to show
});
const handled = Poker.Hand.handleTimeOut(hand);
// Alice should show cards
const lastAction = handled.actions[handled.actions.length - 1];
expect(getActionType(lastAction)).toBe('sm'); // Show cards
expect(getActionPlayerIndex(lastAction)).toBe(0); // Alice
expect(getActionCards(lastAction)).toEqual(['6c', '5h']); // Alice's cards from BASE_HAND
});
it('should maintain immutability', () => {
const hand = Poker.Hand(BASE_HAND);
const originalJson = JSON.stringify(hand);
Poker.Hand.handleTimeOut(hand);
expect(JSON.stringify(hand)).toBe(originalJson);
});
});
describe('Hand.canApplyAction', () => {
it('should delegate directly to Game.canApplyAction', () => {
const hand = Poker.Hand({
...BASE_HAND,
actions: BASE_HAND.actions.slice(0, 3), // Hole cards dealt
});
// Valid action for current state
expect(Poker.Hand.canApplyAction(hand, 'p1 cc')).toBe(true);
// Invalid action (out of turn)
expect(Poker.Hand.canApplyAction(hand, 'p2 f')).toBe(false);
// Invalid action (player doesn't exist)
expect(Poker.Hand.canApplyAction(hand, 'p4 f')).toBe(false);
});
it('should validate dealer actions', () => {
const hand = Poker.Hand({
...BASE_HAND,
actions: [],
});
// Should deal hole cards first
const dealAction = 'd dh p1 AsKs';
expect(Poker.Hand.canApplyAction(hand, dealAction)).toBe(true);
// Can't deal board without hole cards
const boardAction = 'd db AhKhQd';
expect(Poker.Hand.canApplyAction(hand, boardAction)).toBe(false);
});
it('should validate board card sequence - flop must be 3 cards', () => {
const handAfterPreflopBetting = Poker.Hand({
...BASE_HAND,
actions: BASE_HAND.actions.slice(0, 6), // Hole cards + preflop betting complete
});
// Can't deal turn (1 card) before flop
const turnBeforeFlop = 'd db Ah';
expect(Poker.Hand.canApplyAction(handAfterPreflopBetting, turnBeforeFlop)).toBe(false);
// Can't deal river (2 cards) as first board cards
const twoCards = 'd db AhKh';
expect(Poker.Hand.canApplyAction(handAfterPreflopBetting, twoCards)).toBe(false);
// Must deal exactly 3 cards for flop
const validFlop = 'd db AhKhQd';
expect(Poker.Hand.canApplyAction(handAfterPreflopBetting, validFlop)).toBe(true);
// Can't deal 4+ cards at once
const tooManyCards = 'd db AhKhQdJc';
expect(Poker.Hand.canApplyAction(handAfterPreflopBetting, tooManyCards)).toBe(false);
});
it('should validate board card sequence - turn must be 1 card after flop', () => {
const handAfterFlopBetting = Poker.Hand({
...BASE_HAND,
actions: BASE_HAND.actions.slice(0, 10), // Through flop betting complete
});
// Can't deal 3 cards again after flop
const secondFlop = 'd db ThTdTc';
expect(Poker.Hand.canApplyAction(handAfterFlopBetting, secondFlop)).toBe(false);
// Can't deal 2 cards for turn
const twoCardTurn = 'd db ThTd';
expect(Poker.Hand.canApplyAction(handAfterFlopBetting, twoCardTurn)).toBe(false);
// Must deal exactly 1 card for turn
const validTurn = 'd db Th';
expect(Poker.Hand.canApplyAction(handAfterFlopBetting, validTurn)).toBe(true);
});
it('should validate board card sequence - river must be 1 card after turn', () => {
const handAfterTurnBetting = Poker.Hand({
...BASE_HAND,
actions: BASE_HAND.actions.slice(0, 14), // Through turn betting complete
});
// Can't deal multiple cards for river
const multiCardRiver = 'd db 9h9d';
expect(Poker.Hand.canApplyAction(handAfterTurnBetting, multiCardRiver)).toBe(false);
// Must deal exactly 1 card for river
const validRiver = 'd db 9h';
expect(Poker.Hand.canApplyAction(handAfterTurnBetting, validRiver)).toBe(true);
});
it('should prevent dealing board cards when not all active players have hole cards', () => {
const partiallyDealtHand = Poker.Hand({
...BASE_HAND,
actions: [
BASE_HAND.actions[0], // p1 has hole cards
BASE_HAND.actions[1], // p2 has hole cards
// p3 doesn't have hole cards yet
],
});
// Can't deal flop when not all players have hole cards
const prematureFlop = 'd db AhKhQd';
expect(Poker.Hand.canApplyAction(partiallyDealtHand, prematureFlop)).toBe(false);
// Should be able to deal remaining hole cards
const dealToP3 = 'd dh p3 TsTd';
expect(Poker.Hand.canApplyAction(partiallyDealtHand, dealToP3)).toBe(true);
});
it('should handle complex invalid board sequences', () => {
const emptyHand = Poker.Hand({
...BASE_HAND,
actions: [],
});
// Can't skip straight to dealing board
expect(Poker.Hand.canApplyAction(emptyHand, 'd db AhKhQd')).toBe(false);
// After dealing all hole cards
const holeCardsDealt = Poker.Hand({
...BASE_HAND,
actions: BASE_HAND.actions.slice(0, 3),
});
// Can't deal turn-sized board as first community cards
expect(Poker.Hand.canApplyAction(holeCardsDealt, 'd db Ah')).toBe(false);
// After valid flop
let handWithFlop = Poker.Hand.applyAction(holeCardsDealt, 'p1 cc');
handWithFlop = Poker.Hand.applyAction(handWithFlop, 'p2 cc');
handWithFlop = Poker.Hand.applyAction(handWithFlop, 'p3 cc');
handWithFlop = Poker.Hand.applyAction(handWithFlop, 'd db AhKhQd');
// Can't deal another flop-sized board
expect(Poker.Hand.canApplyAction(handWithFlop, 'd db JhJdJc')).toBe(false);
// Can't skip turn and go to river (dealing when board.length would be 6)
handWithFlop = Poker.Hand.applyAction(handWithFlop, 'p2 cc');
handWithFlop = Poker.Hand.applyAction(handWithFlop, 'p3 cc');
handWithFlop = Poker.Hand.applyAction(handWithFlop, 'p1 cc');
const turnAction = 'd db Th';
handWithFlop = Poker.Hand.applyAction(handWithFlop, turnAction);
// Now with 4 cards on board, can only deal 1 more (river)
expect(Poker.Hand.canApplyAction(handWithFlop, 'd db 9h8h')).toBe(false);
});
it('should validate betting actions', () => {
const hand = Poker.Hand({
...BASE_HAND,
actions: BASE_HAND.actions.slice(0, 3), // Hole cards dealt, Alice to act
});
// Valid actions for Alice
const foldAction = 'p1 f';
const checkAction = 'p1 cc';
const betAction = 'p1 cbr 60';
// Valid actions
expect(Poker.Hand.canApplyAction(hand, foldAction)).toBe(true);
expect(Poker.Hand.canApplyAction(hand, checkAction)).toBe(true);
expect(Poker.Hand.canApplyAction(hand, betAction)).toBe(true);
// Invalid - out of turn
const outOfTurnAction = 'p2 f';
expect(Poker.Hand.canApplyAction(hand, outOfTurnAction)).toBe(false);
});
it('should be pure pass-through without side effects', () => {
const hand = Poker.Hand(BASE_HAND);
const originalJson = JSON.stringify(hand);
// Multiple calls should not affect hand
Poker.Hand.canApplyAction(hand, 'p1 f');
Poker.Hand.canApplyAction(hand, 'p2 cc');
Poker.Hand.canApplyAction(hand, 'd db AhKhQd');
expect(JSON.stringify(hand)).toBe(originalJson);
});
});
describe('Hand.finish', () => {
it('should delegate to Game.finish and return hand with completion data', () => {
// Create a complete hand (all actions including showdown)
const completeHand = Poker.Hand(BASE_HAND);
// Call finish
const finishedHand = Poker.Hand.finish(completeHand);
// Should have finishing data
expect(finishedHand.finishingStacks).toBeDefined();
expect(Array.isArray(finishedHand.finishingStacks)).toBe(true);
expect(finishedHand.finishingStacks).toHaveLength(3); // 3 players
// Should have winnings
expect(finishedHand.winnings).toBeDefined();
expect(Array.isArray(finishedHand.winnings)).toBe(true);
// Rake should be a number or undefined
if (finishedHand.rake !== undefined) {
expect(typeof finishedHand.rake).toBe('number');
}
});
it('should maintain immutability', () => {
const hand = Poker.Hand(BASE_HAND);
const originalJson = JSON.stringify(hand);
const finished = Poker.Hand.finish(hand);
expect(JSON.stringify(hand)).toBe(originalJson);
expect(JSON.stringify(finished)).not.toBe(originalJson);
});
it('should handle incomplete hands', () => {
// Create an incomplete hand (only partial actions)
const incompleteHand = Poker.Hand({
...BASE_HAND,
actions: BASE_HAND.actions.slice(0, 10), // Partial hand
});
const finishedHand = Poker.Hand.finish(incompleteHand);
// Should still return a hand (Game.finish handles the logic)
expect(finishedHand).toBeDefined();
// May or may not have finishing data depending on game state
// We're just testing delegation, not the logic
});
it('should work with hands at showdown', () => {
// Use a hand that's at showdown
const showdownHand = Poker.Hand({
...BASE_HAND,
actions: BASE_HAND.actions.slice(0, 19), // At showdown
});
const finishedHand = Poker.Hand.finish(showdownHand);
// Should delegate and return result
expect(finishedHand).toBeDefined();
expect(finishedHand.actions).toEqual(showdownHand.actions);
});
it('should preserve all original hand data', () => {
const hand = Poker.Hand(BASE_HAND);
const finishedHand = Poker.Hand.finish(hand);
// All original fields should be preserved
expect(finishedHand.variant).toBe(hand.variant);
expect(finishedHand.players).toEqual(hand.players);
expect(finishedHand.startingStacks).toEqual(hand.startingStacks);
expect(finishedHand.blindsOrStraddles).toEqual(hand.blindsOrStraddles);
expect(finishedHand.actions).toEqual(hand.actions);
});
});
});