UNPKG

@idealic/poker-engine

Version:

Poker game engine and hand evaluator

343 lines (286 loc) 14.7 kB
// src/__tests__/api/command/core-contract.test.ts import { beforeEach, describe, expect, it } from 'vitest'; import { getActionAmount, getActionCards, getActionPlayerIndex, getActionTimestamp, getActionType, } from '../../../game/position'; import { applyAction } from '../../../game/progress'; import * as Poker from '../../../index'; import { BASE_HAND } from './fixtures/baseHand'; /** * Core Contract Tests for Command API * * Purpose: Validate that Command methods are pure functions that: * 1. Never mutate input Game state * 2. Return consistent Action strings for identical inputs * 3. Generate Actions that can be validated via applyAction() * * Architecture: Command -> Action string -> applyAction() -> New Poker.Game | Error * Base: All tests use BASE_HAND as the foundation for consistent, deterministic scenarios */ describe('Command Core Contracts', () => { let baseGame: Poker.Game; let gameSnapshot: string; let preflopGame: Poker.Game; let readyForFlopGame: Poker.Game; beforeEach(() => { // Fixture: Standard 3-player game from BASE_HAND where Alice is next to act preflop const hand = Poker.Hand({ ...BASE_HAND, actions: BASE_HAND.actions.slice(0, 3), // Only deal hole cards }); baseGame = Poker.Game(hand); gameSnapshot = JSON.stringify(baseGame); // Additional fixture: Clean preflop game for dealer operations testing const preflopHand = Poker.Hand({ ...BASE_HAND, actions: [], // No actions yet }); preflopGame = Poker.Game(preflopHand); // Fixture: Game ready for flop (all players have acted preflop) const readyForFlopHand = Poker.Hand({ ...BASE_HAND, actions: BASE_HAND.actions.slice(0, 6), // Hole cards dealt and all players checked }); readyForFlopGame = Poker.Game(readyForFlopHand); }); describe('State Immutability Contract', () => { it('should never mutate input Poker.Game object during Action generation', () => { // Logic: Generate Actions and verify original Game unchanged // Testing: Commands are pure functions without side effects const commands = [ () => Poker.Command.fold(baseGame, 0), () => Poker.Command.call(baseGame, 0), () => Poker.Command.bet(baseGame, 0, 100), () => Poker.Command.raise(baseGame, 0, 200), () => Poker.Command.allIn(baseGame, 0), ]; commands.forEach(commandFn => { const action = commandFn(); // Expectation: Original Game object must remain unchanged expect(JSON.stringify(baseGame)).toBe(gameSnapshot); // Verify we got a valid Action string expect(getActionPlayerIndex(action)).toBe(0); expect(getActionTimestamp(action)).toBeTypeOf('number'); expect(typeof action).toBe('string'); expect(action.length).toBeGreaterThan(0); }); }); it('should never mutate Game during dealer Action generation', () => { // Logic: Generate dealer Actions without affecting input Game // Testing: Dealer commands also respect immutability const dealerCommands = [ () => Poker.Command.deal(baseGame), () => Poker.Command.deal(preflopGame), () => Poker.Command.forceShowCards(baseGame), ]; dealerCommands.forEach(commandFn => { commandFn(); // Expectation: Game state unchanged after dealer Action generation expect(JSON.stringify(baseGame)).toBe(gameSnapshot); }); }); }); describe('Deterministic Output Contract', () => { it('should return identical Actions for identical inputs', () => { // Logic: Call same Command multiple times // Testing: Commands produce consistent output const action1 = Poker.Command.fold(baseGame, 0); const action2 = Poker.Command.fold(baseGame, 0); const action3 = Poker.Command.bet(baseGame, 0, 150); const action4 = Poker.Command.bet(baseGame, 0, 150); // Expectation: Identical inputs produce identical Actions expect(action1).toBe(action2); expect(action3).toBe(action4); }); it('should return different Actions for different parameters', () => { // Logic: Test Commands with different parameters // Testing: Parameter changes affect Action generation const alice_fold = Poker.Command.fold(baseGame, 0); const bob_fold = Poker.Command.fold(baseGame, 1); const bet_50 = Poker.Command.bet(baseGame, 0, 50); const bet_100 = Poker.Command.bet(baseGame, 0, 100); // Expectation: Different parameters produce different Actions expect(alice_fold).not.toBe(bob_fold); expect(bet_50).not.toBe(bet_100); }); }); describe('Action Applicability Contract', () => { it('should generate Actions that can be processed by applyAction', () => { // Logic: Generate Action then attempt to apply it // Testing: Commands generate processable Actions for valid game states const validAction = Poker.Command.fold(baseGame, 0); // Alice can fold // Expectation: Generated Action should be applicable without throwing expect(() => { const newGame = applyAction(baseGame, validAction); expect(newGame).toBeDefined(); expect(newGame.players[0].hasFolded).toBe(true); }).not.toThrow(); }); it('should generate Actions that fail appropriately for invalid game states', () => { // Fixture: Create game where player already folded const foldedGame = applyAction(baseGame, Poker.Command.fold(baseGame, 0)); // Logic: Try to generate Action for folded player // Testing: Commands generate Actions even for invalid states, but applyAction rejects them const invalidAction = Poker.Command.call(foldedGame, 0); // Folded player can't call // Expectation: Action generates but applyAction should throw expect(typeof invalidAction).toBe('string'); expect(() => { applyAction(foldedGame, invalidAction); }).toThrow(); }); it('should generate valid Actions for different player identifiers', () => { // Logic: Test Commands work with both numeric and string player identifiers // Testing: PlayerIdentifier resolution in Commands const numericAction = Poker.Command.fold(baseGame, 0); const stringAction = Poker.Command.fold(baseGame, 'Alice'); // Both should produce valid, applicable Actions expect(() => applyAction(JSON.parse(JSON.stringify(baseGame)), numericAction)).not.toThrow(); expect(() => applyAction(JSON.parse(JSON.stringify(baseGame)), stringAction)).not.toThrow(); // And they should be identical expect(getActionPlayerIndex(numericAction)).toBe(getActionPlayerIndex(stringAction)); expect(getActionType(numericAction)).toBe(getActionType(stringAction)); }); }); describe('Return Value Contract', () => { it('should always return string Actions, never null or undefined', () => { // Logic: Test Commands with various parameters including edge cases // Testing: Commands handle all inputs gracefully const commands = [ () => Poker.Command.fold(baseGame, 0), () => Poker.Command.call(baseGame, 0), () => Poker.Command.bet(baseGame, 0, 100), () => Poker.Command.message(baseGame, 0, 'Hello'), ]; commands.forEach(commandFn => { const result = commandFn(); // Expectation: All Commands return valid Action strings expect(typeof result).toBe('string'); expect(result).toBeDefined(); expect(result).not.toBeNull(); }); }); it('should return Action strings even for problematic inputs', () => { // Logic: Test Commands with edge case parameters // Testing: Commands handle boundary conditions by returning valid Actions const edgeCaseCommands = [ () => Poker.Command.fold(baseGame, 0), // Valid player (Alice) () => Poker.Command.bet(baseGame, 0, 0), // Zero bet amount () => Poker.Command.call(baseGame, 'Alice'), // Valid player name ]; edgeCaseCommands.forEach(commandFn => { const result = commandFn(); // Expectation: Commands return valid Action strings for edge cases expect(getActionPlayerIndex(result)).toBe(0); expect(getActionTimestamp(result)).toBeTypeOf('number'); expect(typeof result).toBe('string'); expect(result).toBeDefined(); }); }); }); describe('Action Format Contract', () => { it('should generate Actions in expected string format', () => { // Logic: Check that generated Actions follow expected patterns // Testing: Action string format compliance const fold_action = Poker.Command.fold(baseGame, 0); const call_action = Poker.Command.call(baseGame, 0); const bet_action = Poker.Command.bet(baseGame, 0, 150); // Expectation: Actions should have valid format and specific structure expect(getActionPlayerIndex(fold_action)).toBe(0); // Player 0 (p1) expect(getActionType(fold_action)).toBe('f'); expect(getActionTimestamp(fold_action)).toBeTypeOf('number'); expect(getActionPlayerIndex(call_action)).toBe(0); // Player 0 (p1) expect(getActionType(call_action)).toBe('cc'); expect(getActionAmount(call_action)).toBeGreaterThan(0); expect(getActionTimestamp(call_action)).toBeTypeOf('number'); expect(getActionPlayerIndex(bet_action)).toBe(0); // Player 0 (p1) expect(getActionType(bet_action)).toBe('cbr'); expect(getActionAmount(bet_action)).toBe(150); expect(getActionTimestamp(bet_action)).toBeTypeOf('number'); }); it('should generate dealer Actions in expected format', () => { // Logic: Test dealer Action format patterns // Testing: Dealer Action string formatting const hole_action = Poker.Command.deal(preflopGame)!; const board_action = Poker.Command.deal(readyForFlopGame)!; const no_action = Poker.Command.deal(baseGame); // Players need to act first const force_show_action = Poker.Command.forceShowCards(baseGame); // Expectation: Dealer Actions should have valid format and expected structure expect(getActionType(hole_action)).toBe('dh'); expect(getActionPlayerIndex(hole_action)).toBe(0); // Player 0 (p1) gets first cards expect(getActionCards(hole_action)).toEqual(['Qs', '5h']); expect(getActionPlayerIndex(hole_action)).toBe(0); expect(getActionTimestamp(hole_action)).toBeGreaterThan(0); expect(getActionType(board_action)).toBe('db'); expect(getActionCards(board_action)).toEqual(['7h', 'Ac', '5c']); expect(getActionPlayerIndex(board_action)).toBe(undefined); expect(getActionTimestamp(board_action)).toBeGreaterThan(0); // When it's not dealer's turn, should return null expect(no_action).toBeNull(); // forceShowCards should return null for preflop game state (no showdown yet) expect(getActionPlayerIndex(force_show_action ?? '')).toBe(0); // Player 0 (p1) expect(getActionType(force_show_action ?? '')).toBe('sm'); expect(getActionCards(force_show_action ?? '')).toEqual(['6c', '5h']); expect(getActionTimestamp(force_show_action ?? '')).toBeGreaterThan(0); }); }); describe('BASE_HAND Integration Contract', () => { it('should generate consistent Actions using BASE_HAND data', () => { // Logic: Verify Commands work consistently with BASE_HAND foundation // Testing: BASE_HAND provides reliable test foundation const game = JSON.parse(JSON.stringify(baseGame)); const charlieAction = Poker.Command.fold(game, 'Charlie'); const aliceAction = Poker.Command.fold(game, 'Alice'); const bobAction = Poker.Command.fold(game, 'Bob'); // Expectation: Actions should reference correct players from BASE_HAND expect(getActionPlayerIndex(charlieAction)).toBe(2); // Player 2 (p3 - Charlie) expect(getActionType(charlieAction)).toBe('f'); expect(getActionTimestamp(charlieAction)).toBeTypeOf('number'); expect(getActionAmount(charlieAction)).toEqual(0); expect(getActionPlayerIndex(aliceAction)).toBe(0); // Player 0 (p1 - Alice) expect(getActionType(aliceAction)).toBe('f'); expect(getActionTimestamp(aliceAction)).toBeTypeOf('number'); expect(getActionAmount(aliceAction)).toEqual(0); expect(getActionPlayerIndex(bobAction)).toBe(1); // Player 1 (p2 - Bob) expect(getActionType(bobAction)).toBe('f'); expect(getActionTimestamp(bobAction)).toBeTypeOf('number'); expect(getActionAmount(bobAction)).toEqual(0); // Verify all actions are applicable expect(() => applyAction(game, aliceAction)).not.toThrow(); expect(() => applyAction(game, bobAction)).not.toThrow(); // Charlie should throw because he is the only player left, game is over expect(() => applyAction(game, charlieAction)).toThrow(); }); it('should generate deterministic dealer Actions with BASE_HAND seed', () => { // Logic: Verify dealer Actions are deterministic with BASE_HAND seed // Testing: BASE_HAND seed ensures reproducible card dealing const game = JSON.parse(JSON.stringify(preflopGame)); const dealAction1 = Poker.Command.deal(game)!; if (dealAction1) applyAction(game, dealAction1); const dealAction2 = Poker.Command.deal(game)!; if (dealAction2) applyAction(game, dealAction2); const dealAction3 = Poker.Command.deal(game)!; if (dealAction3) applyAction(game, dealAction3); // Actions should be deterministic and match expected format expect(getActionType(dealAction1)).toBe('dh'); expect(getActionPlayerIndex(dealAction1)).toBe(0); // Player 0 (p1) expect(getActionCards(dealAction1)).toEqual(['Qs', '5h']); expect(getActionTimestamp(dealAction1)).toBeTypeOf('number'); expect(getActionType(dealAction2)).toBe('dh'); expect(getActionPlayerIndex(dealAction2)).toBe(1); // Player 1 (p2) expect(getActionCards(dealAction2)).toEqual(['Kh', 'Jd']); expect(getActionTimestamp(dealAction2)).toBeTypeOf('number'); expect(getActionType(dealAction3)).toBe('dh'); expect(getActionPlayerIndex(dealAction3)).toBe(2); // Player 2 (p3) expect(getActionCards(dealAction3)).toEqual(['8h', '6s']); expect(getActionTimestamp(dealAction3)).toBeTypeOf('number'); // Exectation: Game players should have the correct hole cards expect(game.players[0].cards).toEqual(['Qs', '5h']); expect(game.players[1].cards).toEqual(['Kh', 'Jd']); expect(game.players[2].cards).toEqual(['8h', '6s']); }); }); });