@idealic/poker-engine
Version:
Poker game engine and hand evaluator
364 lines (301 loc) • 15.7 kB
text/typescript
// src/__tests__/api/command/player-actions.test.ts
import { beforeEach, describe, expect, it } from 'vitest';
import { Game } from '../../../Game';
import {
getActionAmount,
getActionPlayerIndex,
getActionTimestamp,
getActionType,
} from '../../../game/position';
import { applyAction } from '../../../game/progress';
import * as Poker from '../../../index';
import { BASE_HAND } from './fixtures/baseHand';
/**
* Player Actions Command Tests
*
* Purpose: Test specific player action Commands (fold, call, check, bet, raise, allIn, auto, message)
* Focus: Verify each Command generates correct Action strings and handles parameters properly
* Critical: All-in amounts must reflect TOTAL bet amount (currentBet + remaining stack)
* Base: All tests use BASE_HAND as the foundation for consistent, deterministic scenarios
*/
describe('Player Action Commands', () => {
let preflopGame: Game;
let flopGame: Game;
let turnGame: Game;
let showdownGame: Game;
let bettingGame: Game;
beforeEach(() => {
// Fixture 1: Preflop state - hole cards dealt, players ready to act
const preflopHand = Poker.Hand({
...BASE_HAND,
actions: BASE_HAND.actions.slice(0, 3), // Only deal hole cards
});
preflopGame = Poker.Game(preflopHand);
// Fixture 2: Flop state - preflop complete, flop dealt
const flopHand = Poker.Hand({
...BASE_HAND,
actions: BASE_HAND.actions.slice(0, 7), // Through flop
});
flopGame = Poker.Game(flopHand);
// Fixture 3: Turn state - flop complete, turn dealt
const turnHand = Poker.Hand({
...BASE_HAND,
actions: BASE_HAND.actions.slice(0, 11), // Through turn
});
turnGame = Poker.Game(turnHand);
// Fixture 4: Complete hand at showdown
const showdownHand = Poker.Hand(BASE_HAND);
showdownGame = Poker.Game({ ...showdownHand, actions: [...showdownHand.actions.slice(0, -2)] });
// Fixture 5: Game with betting action - create from BASE_HAND with bet
const bettingHand = Poker.Hand({
...BASE_HAND,
actions: [
...BASE_HAND.actions.slice(0, 3), // Deal hole cards
'p1 cbr 60', // Alice bets
'p2 cc 60', // Bob calls
// Charlie to act
],
});
bettingGame = Poker.Game(bettingHand);
// using the variables
if (!turnGame || !flopGame || !preflopGame || !showdownGame || !bettingGame)
throw new Error('Game is undefined');
});
describe('allIn Command - Critical Stack Calculations', () => {
it('should generate all-in for total bet amount from BASE_HAND', () => {
// Logic: Test all-in calculation using BASE_HAND foundation
// Testing: All-in returns total bet amount (currentBet + remainingStack)
// From BASE_HAND: Alice, Bob, Charlie all have 1000 starting stacks
// After preflop calls, all players invested 20 (currentBet = 20, stack = 980)
const aliceAllIn = Poker.Command.allIn(flopGame, 0);
const bobAllIn = Poker.Command.allIn(flopGame, 1);
const charlieAllIn = Poker.Command.allIn(flopGame, 2);
// Expectation: All-in amounts should be total bet amounts (currentBet + stack = 1000 - 20 = 980)
expect(getActionPlayerIndex(aliceAllIn)).toBe(0); // Player 0 (p1)
expect(getActionType(aliceAllIn)).toBe('cbr');
expect(getActionAmount(aliceAllIn)).toBe(980);
expect(getActionTimestamp(aliceAllIn)).toBeTypeOf('number');
expect(getActionPlayerIndex(bobAllIn)).toBe(1); // Player 1 (p2)
expect(getActionType(bobAllIn)).toBe('cbr');
expect(getActionAmount(bobAllIn)).toBe(980);
expect(getActionTimestamp(bobAllIn)).toBeTypeOf('number');
expect(getActionPlayerIndex(charlieAllIn)).toBe(2); // Player 2 (p3)
expect(getActionType(charlieAllIn)).toBe('cbr');
expect(getActionAmount(charlieAllIn)).toBe(980);
expect(getActionTimestamp(charlieAllIn)).toBeTypeOf('number');
// Verify actions are applicable
expect(() => applyAction(flopGame, bobAllIn)).not.toThrow();
expect(() => applyAction(flopGame, charlieAllIn)).not.toThrow();
expect(() => applyAction(flopGame, aliceAllIn)).not.toThrow();
});
it('should generate all-in after betting from BASE_HAND state', () => {
// Logic: Test all-in calculation after betting action from BASE_HAND
// Testing: All-in calculation with player investments
// From bettingGame: Alice bet 60, Bob called 60, Charlie to act
// Charlie still has 1000 - 0 = 1000 remaining (hasn't acted yet)
const charlieAllIn = Poker.Command.allIn(bettingGame, 2);
expect(getActionPlayerIndex(charlieAllIn)).toBe(2); // Player 2 (p3)
expect(getActionType(charlieAllIn)).toBe('cbr');
expect(getActionAmount(charlieAllIn)).toBe(1000); // Full stack available
expect(getActionTimestamp(charlieAllIn)).toBeTypeOf('number');
// Apply the bet and test Alice's remaining stack
const afterCharlieCall = applyAction(bettingGame, 'p3 cc 60');
const aliceAllIn = Poker.Command.allIn(afterCharlieCall, 0);
expect(getActionPlayerIndex(aliceAllIn)).toBe(0); // Player 0 (p1)
expect(getActionType(aliceAllIn)).toBe('cbr');
expect(getActionAmount(aliceAllIn)).toBe(1000); // 1000 = 60 + 940 remaining
expect(getActionTimestamp(aliceAllIn)).toBeTypeOf('number');
});
it('should handle all-in scenarios with different stack investments', () => {
// Logic: Create scenario with varied investments from BASE_HAND
// Testing: All-in calculation with different player investments
// Create a scenario where players have different investments
const variedInvestmentHand = Poker.Hand({
...BASE_HAND,
startingStacks: [500, 800, 1200], // Different starting stacks
actions: [
'd dh p1 6c5h',
'd dh p2 Jc2s',
'd dh p3 Tc3c',
'p1 cbr 100', // Alice raises to 100
'p2 cc 100', // Bob calls
// Charlie to act
],
});
const variedGame = Poker.Game(variedInvestmentHand);
const aliceAllIn = Poker.Command.allIn(variedGame, 0);
const bobAllIn = Poker.Command.allIn(variedGame, 1);
const charlieAllIn = Poker.Command.allIn(variedGame, 2);
expect(getActionPlayerIndex(aliceAllIn)).toBe(0); // Player 0 (p1)
expect(getActionType(aliceAllIn)).toBe('cbr');
expect(getActionAmount(aliceAllIn)).toBe(500); // currentBet (100) + stack (400) = 500 total
expect(getActionTimestamp(aliceAllIn)).toBeTypeOf('number');
expect(getActionPlayerIndex(bobAllIn)).toBe(1); // Player 1 (p2)
expect(getActionType(bobAllIn)).toBe('cbr');
expect(getActionAmount(bobAllIn)).toBe(800); // currentBet (100) + stack (700) = 800 total
expect(getActionTimestamp(bobAllIn)).toBeTypeOf('number');
expect(getActionPlayerIndex(charlieAllIn)).toBe(2); // Player 2 (p3)
expect(getActionType(charlieAllIn)).toBe('cbr');
expect(getActionAmount(charlieAllIn)).toBe(1200); // currentBet (0) + stack (1200) = 1200 total
expect(getActionTimestamp(charlieAllIn)).toBeTypeOf('number');
});
});
describe('fold Command', () => {
it('should generate fold Action for active player', () => {
const foldAction = Poker.Command.fold(preflopGame, 0);
expect(getActionPlayerIndex(foldAction)).toBe(0); // Player 0 (p1)
expect(getActionType(foldAction)).toBe('f');
expect(getActionTimestamp(foldAction)).toBeTypeOf('number');
const newGame = applyAction(preflopGame, foldAction);
expect(newGame.players[0].hasFolded).toBe(true);
});
it('should generate fold Actions for different players', () => {
const aliceFold = Poker.Command.fold(preflopGame, 0);
const bobFold = Poker.Command.fold(preflopGame, 1);
const charlieFold = Poker.Command.fold(preflopGame, 2);
expect(getActionPlayerIndex(aliceFold)).toBe(0); // Player 0 (p1)
expect(getActionType(aliceFold)).toBe('f');
expect(getActionTimestamp(aliceFold)).toBeTypeOf('number');
expect(getActionAmount(aliceFold)).toBe(0);
expect(getActionPlayerIndex(bobFold)).toBe(1); // Player 1 (p2)
expect(getActionType(bobFold)).toBe('f');
expect(getActionTimestamp(bobFold)).toBeTypeOf('number');
expect(getActionAmount(bobFold)).toBe(0);
expect(getActionPlayerIndex(charlieFold)).toBe(2); // Player 2 (p3)
expect(getActionType(charlieFold)).toBe('f');
expect(getActionTimestamp(charlieFold)).toBeTypeOf('number');
expect(getActionAmount(charlieFold)).toBe(0);
});
it('should generate fold Action using string player identifier', () => {
const foldByName = Poker.Command.fold(preflopGame, 'Alice');
const foldByIndex = Poker.Command.fold(preflopGame, 0);
// Both should target the same player with same action type
expect(getActionPlayerIndex(foldByName)).toBe(getActionPlayerIndex(foldByIndex));
expect(getActionType(foldByName)).toBe(getActionType(foldByIndex));
expect(getActionPlayerIndex(foldByName)).toBe(0); // Player 0 (p1 - Alice)
expect(getActionType(foldByName)).toBe('f');
expect(getActionTimestamp(foldByName)).toBeTypeOf('number');
expect(getActionTimestamp(foldByIndex)).toBeTypeOf('number');
});
});
describe('call Command', () => {
it('should generate call Action with correct amount from BASE_HAND', () => {
const callAction = Poker.Command.call(bettingGame, 2); // Charlie calling the bet
expect(getActionPlayerIndex(callAction)).toBe(2); // Player 2 (p3 - Charlie)
expect(getActionType(callAction)).toBe('cc');
expect(getActionAmount(callAction)).toBeGreaterThan(0);
expect(getActionTimestamp(callAction)).toBeTypeOf('number');
const newGame = applyAction(bettingGame, callAction);
expect(newGame.players[2].roundBet).toBe(bettingGame.bet);
});
it('should generate call Action for preflop big blind scenario', () => {
const callAction = Poker.Command.call(preflopGame, 0);
expect(getActionPlayerIndex(callAction)).toBe(0); // Player 0 (p1)
expect(getActionType(callAction)).toBe('cc');
expect(getActionAmount(callAction)).toBe(20);
expect(getActionTimestamp(callAction)).toBeTypeOf('number');
const newGame = applyAction(preflopGame, callAction);
expect(newGame.players[0].roundBet).toBe(20);
});
});
describe('bet Command', () => {
it('should generate bet Action with specified amount', () => {
const betAction = Poker.Command.bet(preflopGame, 0, 150);
expect(getActionPlayerIndex(betAction)).toBe(0); // Player 0 (p1)
expect(getActionType(betAction)).toBe('cbr');
expect(getActionAmount(betAction)).toBe(150);
expect(getActionTimestamp(betAction)).toBeTypeOf('number');
const newGame = applyAction(preflopGame, betAction);
expect(newGame.bet).toBe(150);
expect(newGame.players[0].roundBet).toBe(150);
});
it('should generate bet Action for minimum bet', () => {
const minBetAction = Poker.Command.bet(preflopGame, 0, 20);
expect(getActionPlayerIndex(minBetAction)).toBe(0); // Player 0 (p1)
expect(getActionType(minBetAction)).toBe('cc');
expect(getActionAmount(minBetAction)).toBe(20);
expect(getActionTimestamp(minBetAction)).toBeTypeOf('number');
expect(() => applyAction(preflopGame, minBetAction)).not.toThrow();
});
});
describe('raise Command', () => {
it('should generate raise Action to specified total amount', () => {
const raiseAction = Poker.Command.raise(bettingGame, 2, 120);
expect(getActionPlayerIndex(raiseAction)).toBe(2); // Player 2 (p3)
expect(getActionType(raiseAction)).toBe('cbr');
expect(getActionAmount(raiseAction)).toBe(120);
expect(getActionTimestamp(raiseAction)).toBeTypeOf('number');
const newGame = applyAction(bettingGame, raiseAction);
expect(newGame.bet).toBe(120);
expect(newGame.players[2].roundBet).toBe(120);
});
});
describe('check Command', () => {
it('should generate check Action when no bet to call', () => {
const game = JSON.parse(JSON.stringify(flopGame));
const checkAction = Poker.Command.check(game, 1);
expect(getActionPlayerIndex(checkAction)).toBe(1); // Player 1 (p2)
expect(getActionType(checkAction)).toBe('cc');
expect(getActionAmount(checkAction)).toBe(0);
expect(getActionTimestamp(checkAction)).toBeTypeOf('number');
const newGame = applyAction(game, checkAction);
expect(newGame.players[1].hasActed).toBe(true);
});
});
describe('auto Command', () => {
it('should generate fold Action for timeout in betting round', () => {
const autoAction = Poker.Command.auto(preflopGame, 0);
expect(getActionPlayerIndex(autoAction)).toBe(0); // Player 0 (p1)
expect(getActionType(autoAction)).toBe('f');
expect(getActionTimestamp(autoAction)).toBeTypeOf('number');
const newGame = applyAction(preflopGame, autoAction);
expect(newGame.players[0].hasFolded).toBe(true);
});
it('should generate muck Action for timeout in showdown', () => {
const autoShowdownAction = Poker.Command.auto(showdownGame, 2);
expect(getActionPlayerIndex(autoShowdownAction)).toBe(2); // Player 2 (p3)
expect(getActionType(autoShowdownAction)).toBe('sm');
expect(getActionTimestamp(autoShowdownAction)).toBeTypeOf('number');
const newGame = applyAction(showdownGame, autoShowdownAction);
expect(newGame.players[1].hasShownCards).toBe(true);
});
});
describe('message Command', () => {
it('should generate message Action with player and text', () => {
const messageAction = Poker.Command.message(preflopGame, 0, 'Good luck everyone!');
// Message actions are player actions with type 'm'
expect(getActionType(messageAction)).toBe('m');
expect(getActionPlayerIndex(messageAction)).toBe(0);
expect(getActionTimestamp(messageAction)).toBeTypeOf('number');
expect(messageAction).toContain('Good luck everyone!');
expect(() => {
return applyAction(showdownGame, messageAction);
}).not.toThrow();
});
it('should generate message Action with different players and messages', () => {
const aliceMsg = Poker.Command.message(preflopGame, 'Alice', 'Hello');
const bobMsg = Poker.Command.message(preflopGame, 1, 'Nice hand');
const charlieMsg = Poker.Command.message(preflopGame, 2, 'GG');
// Message actions are player actions with type 'm'
expect(getActionType(aliceMsg)).toBe('m');
expect(getActionPlayerIndex(aliceMsg)).toBe(0);
expect(getActionTimestamp(aliceMsg)).toBeTypeOf('number');
expect(getActionAmount(aliceMsg)).toBe(0);
expect(aliceMsg).toContain('Hello');
expect(getActionType(bobMsg)).toBe('m');
expect(getActionPlayerIndex(bobMsg)).toBe(1);
expect(getActionTimestamp(bobMsg)).toBeTypeOf('number');
expect(getActionAmount(bobMsg)).toBe(0);
expect(bobMsg).toContain('Nice hand');
expect(getActionType(charlieMsg)).toBe('m');
expect(getActionPlayerIndex(charlieMsg)).toBe(2);
expect(getActionTimestamp(charlieMsg)).toBeTypeOf('number');
expect(getActionAmount(charlieMsg)).toBe(0);
expect(charlieMsg).toContain('GG');
// Verify actions are applicable
expect(() => applyAction(preflopGame, aliceMsg)).not.toThrow();
expect(() => applyAction(preflopGame, bobMsg)).not.toThrow();
expect(() => applyAction(preflopGame, charlieMsg)).not.toThrow();
});
});
});