UNPKG

@idealic/poker-engine

Version:

Poker game engine and hand evaluator

624 lines (530 loc) 20.1 kB
import { describe, expect, it } from 'vitest'; import * as Poker from '../../../index'; import type { Action } from '../../../types'; import { BASE_HAND, MINIMAL_HAND } from './fixtures/baseGame'; describe('Game API - Edge Cases', () => { describe('Extreme Stack Sizes', () => { it('should handle zero starting stacks', () => { const hand: Poker.Hand = { ...BASE_HAND, startingStacks: [5, 1000, 0, 1000], }; // game constructor should throw // because player with chips 0 or lower than big blind CAN'T act expect(() => Poker.Game(hand)).toThrow(); }); it('should handle negative starting stacks', () => { const hand: Poker.Hand = { ...BASE_HAND, startingStacks: [-5, -1000, 0, 1000], }; // game constructor should throw // because player with chips negative CAN'T act expect(() => Poker.Game(hand)).toThrow(); }); it('should handle fractional stacks if allowed', () => { const hand: Poker.Hand = { ...BASE_HAND, startingStacks: [100.5, 200.25, 300.75, 400.1], }; const game = Poker.Game(hand); expect(game.players[0].stack).toBe(90.5); expect(game.players[1].stack).toBe(180.25); }); }); describe('Player Count Extremes', () => { it('should set isValid=false for single player game', () => { const hand: Poker.Hand = { variant: 'NT', players: ['Lonely'], startingStacks: [1000], blindsOrStraddles: [0], minBet: 20, antes: [0], actions: [], }; const game = Poker.Game(hand); expect(game.isPlayable).toBe(false); }); it('should set isValid=true for normal game', () => { const game = Poker.Game(BASE_HAND); expect(game.isPlayable).toBe(true); }); it('should throw when applying action with less than 2 active players', () => { const hand: Poker.Hand = { variant: 'NT', players: ['Lonely'], startingStacks: [1000], blindsOrStraddles: [0], minBet: 20, antes: [0], actions: [], }; const game = Poker.Game(hand); expect(game.isPlayable).toBe(false); // Only 1 player, so activePlayers.length < 2 // Trying to apply an action should throw const action = 'p1 cc' as Action; expect(() => Poker.Game.applyAction(game, action)).toThrowError( /Cannot apply action to invalid game/ ); }); it('should throw when applying action after everyone else folded (1 active player)', () => { const hand: Poker.Hand = { ...BASE_HAND, actions: [ 'd dh p1 AsKs', 'd dh p2 7h2d', 'd dh p3 QhQc', 'd dh p4 JdTd', 'p3 f', // UTG folds 'p4 f', // BTN folds 'p1 f', // SB folds // p2 wins, hand complete ] as Action[], }; const game = Poker.Game(hand); // game started valid but now has < 2 active players expect(game.isPlayable).toBe(true); // Try to apply an action for p2 expect(() => Poker.Game.applyAction(game, 'p2 cc')).toThrowError( /Cannot apply action to invalid game/ ); }); it('should handle maximum players (9)', () => { const hand: Poker.Hand = { variant: 'NT', players: ['P1', 'P2', 'P3', 'P4', 'P5', 'P6', 'P7', 'P8', 'P9'], startingStacks: [1000, 1000, 1000, 1000, 1000, 1000, 1000, 1000, 1000], blindsOrStraddles: [0, 0, 0, 0, 0, 0, 0, 10, 20], antes: [0, 0, 0, 0, 0, 0, 0, 0, 0], minBet: 20, actions: [], }; const game = Poker.Game(hand); expect(game.players).toHaveLength(9); }); it('should throw when players exceed default seat count (9)', () => { const hand: Poker.Hand = { variant: 'NT', players: ['P1', 'P2', 'P3', 'P4', 'P5', 'P6', 'P7', 'P8', 'P9', 'P10'], startingStacks: [1000, 1000, 1000, 1000, 1000, 1000, 1000, 1000, 1000, 1000], blindsOrStraddles: [0, 0, 0, 0, 0, 0, 0, 10, 20, 0], antes: [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], minBet: 20, actions: [], }; expect(() => Poker.Game(hand)).toThrowError(/Game cannot have more players than seats/); }); it('should throw when players exceed explicit seat count', () => { const hand: Poker.Hand = { variant: 'NT', players: ['P1', 'P2', 'P3', 'P4', 'P5', 'P6'], startingStacks: [1000, 1000, 1000, 1000, 1000, 1000], blindsOrStraddles: [0, 0, 0, 0, 10, 20], antes: [0, 0, 0, 0, 0, 0], minBet: 20, seatCount: 5, // Limit to 5 actions: [], }; expect(() => Poker.Game(hand)).toThrowError(/Game cannot have more players than seats/); }); }); describe('Action Sequence Edge Cases', () => { it('should handle empty actions array', () => { const hand: Poker.Hand = { ...BASE_HAND, actions: [], }; const game = Poker.Game(hand); expect(game).toBeDefined(); expect(game.board).toEqual([]); expect(game.street).toBe('preflop'); }); it('should handle actions array with only dealer actions', () => { const hand: Poker.Hand = { ...BASE_HAND, actions: ['d dh p1 AsKs', 'd dh p2 7h2d', 'd dh p3 QhQc', 'd dh p4 JdTd'], }; const game = Poker.Game(hand); expect(game.players[0].cards).toEqual(['As', 'Ks']); expect(game.street).toBe('preflop'); expect(game.nextPlayerIndex).toBeGreaterThanOrEqual(0); }); it('should handle incomplete action sequences', () => { const hand: Poker.Hand = { ...BASE_HAND, actions: [ 'd dh p1 AsKs', 'd dh p2 7h2d', // Missing p3 and p4 hole cards 'p3 cbr 60', ], }; // Should either handle gracefully or throw expect(() => Poker.Game(hand)).toBeDefined(); }); it('should handle duplicate actions', () => { const hand: Poker.Hand = { ...BASE_HAND, actions: [ 'd dh p1 AsKs', 'd dh p1 AsKs', // Duplicate 'd dh p2 7h2d', ], }; // Should handle or reject duplicates const game = Poker.Game(hand); expect(game.players[0].cards).toEqual(['As', 'Ks']); }); it('should report illegal move when raising less than min-raise', () => { const hand: Poker.Hand = { ...BASE_HAND, actions: ['d dh p1 AsKs', 'd dh p2 7h2d', 'd dh p3 QhQc', 'd dh p4 JdTd'], startingStacks: [1000, 1500, 30, 1200], }; const game = Poker.Game(hand); // BB is 20. Min raise is to 40. const illegalAction: Action = 'p3 cbr 29'; // Attempting to raise less than min (29) when stack allows more should be invalid expect(Poker.Game.canApplyAction(game, illegalAction)).toBe(false); // 30 is fine since it's the whole stack - meaning going all in expect(Poker.Game.canApplyAction(game, 'p3 cbr 30')).toBe(true); }); }); describe('Illegal Moves', () => { it('should throw when capped player attempts to raise', () => { const cappedHand: Poker.Hand = { ...BASE_HAND, startingStacks: [1000, 1000, 120, 1000], blindsOrStraddles: [0, 0, 10, 20], players: ['P1', 'P2', 'P3', 'P4'], actions: [ 'd dh p1 AsKs', 'd dh p2 7h2d', 'd dh p3 QhQc', 'd dh p4 JdTd', 'p1 cbr 100', 'p2 cc', 'p3 cbr 120', // all-in, incomplete raise 'p4 f', // Action back on P1. P1 is capped. ], }; const game = Poker.Game(cappedHand); expect(game.nextPlayerIndex).toBe(0); // P1 tries to raise to 200 (Illegal) const illegalAction = 'p1 cbr 200' as Action; // Ensure canApplyAction returns false expect(Poker.Game.canApplyAction(game, illegalAction)).toBe(false); // Ensure applyAction throws // NOTE: This expectation might fail if the engine is permissive. // If it fails, we need to decide whether to enforce strictness in engine or update test. // The requirement was "it should throw". expect(() => Poker.Game.applyAction(game, illegalAction)).toThrow(); }); }); describe('Blind Structure Edge Cases', () => { it('should handle no blinds, should throw', () => { const hand: Poker.Hand = { ...BASE_HAND, blindsOrStraddles: [0, 0, 0, 0], }; // this game variant MUST have blinds expect(() => Poker.Game(hand)).toThrow(); }); it('should handle unusual blind positions, should throw', () => { const hand: Poker.Hand = { ...BASE_HAND, blindsOrStraddles: [0, 10, 0, 20], // Blinds not in typical positions }; // this game variant MUST have blinds in typical positions expect(() => Poker.Game(hand)).toThrow(); }); }); describe('Card Dealing Edge Cases', () => { it('should handle invalid card notations gracefully', () => { const hand: Poker.Hand = { ...BASE_HAND, actions: [ 'd dh p1 XxYy' as Action, // Invalid cards ], }; let game: Poker.Game; // Should either handle gracefully or throw expect(() => (game = Poker.Game(hand))).not.toThrow(); expect(game!.players[0].cards).toEqual(['Xx', 'Yy']); }); it('should handle duplicate cards in deck', () => { const hand: Poker.Hand = { ...BASE_HAND, actions: [ 'd dh p1 AsKs', 'd dh p2 AsKs', // Same cards as p1 ], }; let game: Poker.Game; // Do not check for duplicate cards, any cards is OK expect(() => (game = Poker.Game(hand))).not.toThrow(); expect(game!.players[0].cards).toEqual(['As', 'Ks']); expect(game!.players[1].cards).toEqual(['As', 'Ks']); }); it('should handle too many board cards, should throw', () => { const hand: Poker.Hand = { ...BASE_HAND, actions: [ ...BASE_HAND.actions, 'd db Td', 'd db 9s', 'd db 8h', // 6th card - invalid ], }; // Should have max 5 board cards expect(() => Poker.Game(hand)).toThrow(); }); }); describe('Timing Edge Cases', () => { it('should handle negative time limits', () => { const hand: Poker.Hand = { ...BASE_HAND, timeLimit: -30, // negative time limit is like no time limit }; const game = Poker.Game(hand); expect(Poker.Game.getTimeLeft(game)).toBe(Infinity); }); it('should handle zero time limit', () => { const hand: Poker.Hand = { ...BASE_HAND, timeLimit: 0, }; const game = Poker.Game(hand); expect(Poker.Game.getTimeLeft(game)).toBe(Infinity); }); it('should handle very large time limits', () => { const hand: Poker.Hand = { ...BASE_HAND, timeLimit: Number.MAX_SAFE_INTEGER, }; const game = Poker.Game(hand); const timeLeft = Poker.Game.getTimeLeft(game); expect(timeLeft).toBeLessThanOrEqual(Number.MAX_SAFE_INTEGER * 1000); }); it('should handle timestamps in the future', () => { const futureTime = Date.now() + 1000000; const hand: Poker.Hand = { ...BASE_HAND, actions: [`d dh p1 AsKs #${futureTime}`], }; const game = Poker.Game(hand); expect(game.lastTimestamp).toBe(futureTime); }); it('should handle very old timestamps', () => { const oldTime = 0; // Unix epoch const hand: Poker.Hand = { ...BASE_HAND, actions: [`d dh p1 AsKs #${oldTime}`], }; const now = Date.now(); // invalid timestamp is being replaced with current time const game = Poker.Game(hand); expect(game.lastTimestamp).toBeGreaterThan(now - 1000); expect(game.lastTimestamp).toBeLessThan(now + 1000); }); }); describe('Special Characters in Player Names', () => { it('should handle unicode characters', () => { const hand: Poker.Hand = { ...BASE_HAND, players: ['Alice', '🎮Player', '中文名', 'Пользователь'], }; const game = Poker.Game(hand); expect(game.players[1].name).toBe('🎮Player'); expect(game.players[2].name).toBe('中文名'); expect(Poker.Game.getPlayerIndex(game, '🎮Player')).toBe(1); }); it('should handle empty player names', () => { const hand: Poker.Hand = { ...BASE_HAND, players: ['', 'Bob', 'Ch arlie', 'David'], }; const game = Poker.Game(hand); expect(game.players[0].name).toBe(''); expect(Poker.Game.getPlayerIndex(game, '')).toBe(0); }); it('should handle very long player names', () => { const longName = 'A'.repeat(1000); const hand: Poker.Hand = { ...BASE_HAND, players: [longName, 'Bob', 'Charlie', 'David'], }; const game = Poker.Game(hand); expect(game.players[0].name).toBe(longName); expect(Poker.Game.getPlayerIndex(game, longName)).toBe(0); }); }); describe('Rake Edge Cases', () => { it('should handle 100% rake', () => { const hand: Poker.Hand = { ...BASE_HAND, rakePercentage: 1.0, }; const game = Poker.Game(hand); const finished = Poker.Game.finish(game, hand); if (finished.rake !== undefined) { // All pot goes to rake expect(finished.rake).toBeLessThanOrEqual(finished.totalPot || 0); } }); it('should handle negative rake', () => { const hand: Poker.Hand = { ...BASE_HAND, rakePercentage: -0.1, }; const game = Poker.Game(hand); const finished = Poker.Game.finish(game, hand); if (finished.rake !== undefined) { // Should treat as 0 or handle specially expect(finished.rake).toBeGreaterThanOrEqual(0); } }); it('should handle fixed rake amount', () => { const hand: Poker.Hand = { ...BASE_HAND, rake: 5, rakePercentage: undefined, }; const game = Poker.Game(hand); const finished = Poker.Game.finish(game, hand); if (finished.rake !== undefined) { expect(finished.rake).toBe(5); } }); }); describe('Action Validation Edge Cases', () => { it('should handle malformed action strings', () => { const game = Poker.Game(BASE_HAND); // Various malformed actions //expect(Poker.Game.canApplyAction(game, '' as Action)).toBe(false); //expect(Poker.Game.canApplyAction(game, ' ' as Action)).toBe(false); //expect(Poker.Game.canApplyAction(game, 'p' as Action)).toBe(false); expect(Poker.Game.canApplyAction(game, 'p3' as Action)).toBe(false); //expect(Poker.Game.canApplyAction(game, 'cbr 100' as Action)).toBe(false); //expect(Poker.Game.canApplyAction(game, '3 cbr 100' as Action)).toBe(false); }); it('should handle out-of-range player indices', () => { const game = Poker.Game(BASE_HAND); expect(Poker.Game.canApplyAction(game, 'p0 cc' as Action)).toBe(false); expect(Poker.Game.canApplyAction(game, 'p5 cc' as Action)).toBe(false); expect(Poker.Game.canApplyAction(game, 'p-1 cc' as Action)).toBe(false); expect(Poker.Game.canApplyAction(game, 'p99 cc' as Action)).toBe(false); }); it('should handle invalid action types', () => { const game = Poker.Game(BASE_HAND); expect(Poker.Game.canApplyAction(game, 'p3 xyz' as Action)).toBe(false); expect(Poker.Game.canApplyAction(game, 'p3 call' as Action)).toBe(false); expect(Poker.Game.canApplyAction(game, 'p3 raise 100' as Action)).toBe(false); expect(Poker.Game.canApplyAction(game, 'p3 CHECK' as Action)).toBe(false); }); it('should handle invalid amounts', () => { const game = Poker.Game(BASE_HAND); expect(Poker.Game.canApplyAction(game, 'p3 cbr -100' as Action)).toBe(false); expect(Poker.Game.canApplyAction(game, 'p3 cbr 0' as Action)).toBe(false); // Non-numeric or infinite should be rejected too if strict, but previous behavior accepted them? // getActionAmount returns 0 for NaN/invalid. // And 0 < bigBlind, so it should be rejected now. expect(Poker.Game.canApplyAction(game, 'p3 cbr abc' as Action)).toBe(false); // Infinity handling: getActionAmount might return Infinity or 0? // parseInt('Infinity') is NaN -> 0. So rejected. expect(Poker.Game.canApplyAction(game, 'p3 cbr Infinity' as Action)).toBe(false); expect(Poker.Game.canApplyAction(game, 'p3 cbr NaN' as Action)).toBe(false); }); }); describe('State Recovery Edge Cases', () => { it('should handle contradictory state', () => { const hand: Poker.Hand = { ...BASE_HAND, actions: [ 'd dh p1 AsKs', 'd dh p2 7h2d', 'p1 f', // Alice folds 'p1 cbr 100', // Alice bets after folding - contradiction ] as Action[], }; // Should handle or reject contradictory actions expect(() => Poker.Game(hand)).toBeDefined(); }); it('should handle actions after hand complete', () => { const hand: Poker.Hand = { ...MINIMAL_HAND, actions: [ 'd dh p1 AsKs', 'd dh p2 7h2d', 'p1 cbr 100', 'p2 f', // Hand complete 'p1 cc', // Action after complete ] as Action[], }; // Should handle or ignore actions after completion expect(() => Poker.Game(hand)).toBeDefined(); }); }); describe('Null and Undefined Handling', () => { it('should handle undefined optional fields', () => { const hand: Poker.Hand = { variant: 'NT', players: ['Alice', 'Bob'], startingStacks: [1000, 1000], antes: [0, 0], blindsOrStraddles: [10, 20], minBet: 20, actions: [], // All optional fields undefined seed: undefined, timeLimit: undefined, rake: undefined, rakePercentage: undefined, currency: undefined, author: undefined, }; const game = Poker.Game(hand); expect(game).toBeDefined(); expect(Poker.Game.getTimeLeft(game)).toBe(Infinity); }); it('should handle null values gracefully', () => { const game = Poker.Game(BASE_HAND); // Various null/undefined inputs expect(Poker.Game.getPlayerIndex(game, null as any)).toBe(-1); expect(Poker.Game.getPlayerIndex(game, undefined as any)).toBe(-1); expect(Poker.Game.hasActed(game, null as any)).toBe(false); expect(Poker.Game.hasActed(game, undefined as any)).toBe(false); expect(Poker.Game.canApplyAction(game, null as any)).toBe(false); expect(Poker.Game.canApplyAction(game, undefined as any)).toBe(false); }); }); describe('Concurrent Modifications', () => { it('should handle rapid successive actions', () => { let game = Poker.Game(BASE_HAND); // Simulate rapid action application const actions: Action[] = ['p3 cc', 'p4 cc']; for (const action of actions) { if (Poker.Game.canApplyAction(game, action)) { game = Poker.Game.applyAction(game, action); } } expect(game).toBeDefined(); expect(game.street).toBeDefined(); }); it('should maintain consistency across multiple operations', () => { const game = Poker.Game(BASE_HAND); // Multiple query operations shouldn't affect each other const index1 = Poker.Game.getPlayerIndex(game, 'Alice'); const showdown1 = game.isShowdown; const hasActed1 = Poker.Game.hasActed(game, 'Charlie'); const index2 = Poker.Game.getPlayerIndex(game, 'Alice'); const showdown2 = game.isShowdown; const hasActed2 = Poker.Game.hasActed(game, 'Charlie'); expect(index1).toBe(index2); expect(showdown1).toBe(showdown2); expect(hasActed1).toBe(hasActed2); }); }); });