@idealic/poker-engine
Version:
Poker game engine and hand evaluator
568 lines (459 loc) • 24.9 kB
text/typescript
// src/__tests__/api/command/error-scenarios.test.ts
import { beforeEach, describe, expect, it } from 'vitest';
import { Game } from '../../../Game';
import {
getActionAmount,
getActionMessage,
getActionPlayerIndex,
getActionTimestamp,
getActionType,
} from '../../../game/position';
import { applyAction } from '../../../game/progress';
import * as Poker from '../../../index';
import { BASE_HAND } from './fixtures/baseHand';
/**
* Command Error Scenarios Tests
*
* Purpose: Test Command behavior with invalid inputs, edge cases, and error conditions
* Focus: Commands should generate Actions gracefully but let applyAction() handle validation
* Critical: Commands never throw - they always return Action strings (valid or invalid)
* Base: All tests use BASE_HAND as the foundation for consistent, deterministic scenarios
*/
describe('Command Error Scenarios', () => {
let validGame: Game;
let completedGame: Game;
let foldedPlayersGame: Game;
let allInGame: Game;
let emptyStackGame: Game;
beforeEach(() => {
// Fixture 1: Valid active game state from BASE_HAND
const validHand = Poker.Hand({
...BASE_HAND,
actions: BASE_HAND.actions.slice(0, 3), // Only deal hole cards
});
validGame = Poker.Game(validHand);
// Fixture 2: Completed game - only one player remains
const completedHand = Poker.Hand({
...BASE_HAND,
actions: [
...BASE_HAND.actions.slice(0, 3), // Deal hole cards
'p1 cbr 100', // Alice raises
'p2 f', // Bob folds
'p3 f', // Charlie folds - Alice wins, game over
],
});
completedGame = Poker.Game(completedHand);
// Fixture 3: Game with folded players
const foldedHand = Poker.Hand({
...BASE_HAND,
actions: [
...BASE_HAND.actions.slice(0, 3), // Deal hole cards
'p1 cbr 60', // Alice raises
'p2 f', // Bob folds
// Charlie to act
],
});
foldedPlayersGame = Poker.Game(foldedHand);
// Fixture 4: All-in scenario with side pots
const allInHand = Poker.Hand({
...BASE_HAND,
startingStacks: [100, 500, 1000], // Different starting stacks
actions: [
'd dh p1 6c5h',
'd dh p2 Jc2s',
'd dh p3 Tc3c',
'p1 cbr 100', // Alice all-in (remaining: 0)
'p2 cc 100', // Bob calls (remaining: 400)
// Charlie to act
],
});
allInGame = Poker.Game(allInHand);
// Fixture 5: Players with zero stacks
const emptyStackHand = Poker.Hand({
...BASE_HAND,
startingStacks: [0, 1000, 1000], // Alice has no chips
blindsOrStraddles: [0, 10, 20], // Standard blinds (Alice can't post, but Bob=SB, Charlie=BB)
actions: ['d dh p1 6c5h', 'd dh p2 Jc2s', 'd dh p3 Tc3c'],
});
emptyStackGame = Poker.Game(emptyStackHand);
});
describe('Invalid Player Identifier Scenarios', () => {
it('should not allow actions for invalid numeric player indices', () => {
// Logic: Test Commands with out-of-range player indices
// Testing: Commands handle invalid indices gracefully without throwing
const negativeIndex = Poker.Command.fold(validGame, -1);
const zeroButInvalid = Poker.Command.fold(validGame, 999);
const wayOutOfRange = Poker.Command.fold(validGame, 1000000);
// Expectation: Commands generate Actions (may be invalid) but don't throw
expect(typeof negativeIndex).toBe('string');
expect(getActionTimestamp(negativeIndex)).toBeTypeOf('number');
expect(typeof zeroButInvalid).toBe('string');
expect(getActionTimestamp(zeroButInvalid)).toBeTypeOf('number');
expect(typeof wayOutOfRange).toBe('string');
expect(getActionTimestamp(wayOutOfRange)).toBeTypeOf('number');
// These Actions should fail when applied
expect(() => applyAction(validGame, negativeIndex)).toThrow();
expect(() => applyAction(validGame, zeroButInvalid)).toThrow();
expect(() => applyAction(validGame, wayOutOfRange)).toThrow();
});
it('should not allow actions for invalid player names', () => {
// Logic: Test Commands with non-existent player names
// Testing: Commands handle unknown player names without throwing
const unknownPlayer = Poker.Command.fold(validGame, 'Unknown');
const emptyName = Poker.Command.fold(validGame, '');
const numericString = Poker.Command.fold(validGame, '42');
expect(typeof unknownPlayer).toBe('string');
expect(getActionTimestamp(unknownPlayer)).toBeTypeOf('number');
expect(typeof emptyName).toBe('string');
expect(getActionTimestamp(emptyName)).toBeTypeOf('number');
expect(typeof numericString).toBe('string');
expect(getActionTimestamp(numericString)).toBeTypeOf('number');
// applyAction should reject these
expect(() => applyAction(validGame, unknownPlayer)).toThrow();
expect(() => applyAction(validGame, emptyName)).toThrow();
expect(() => applyAction(validGame, numericString)).toThrow();
});
it('should generate Actions for null/undefined player identifiers', () => {
// Logic: Test Commands with null/undefined player parameters
// Testing: Commands handle missing player parameters gracefully
const nullPlayer = Poker.Command.fold(validGame, null as any);
const undefinedPlayer = Poker.Command.fold(validGame, undefined as any);
expect(typeof nullPlayer).toBe('string');
expect(getActionTimestamp(nullPlayer)).toBeTypeOf('number');
expect(typeof undefinedPlayer).toBe('string');
expect(getActionTimestamp(undefinedPlayer)).toBeTypeOf('number');
expect(() => applyAction(validGame, nullPlayer)).toThrow();
expect(() => applyAction(validGame, undefinedPlayer)).toThrow();
});
it('should generate Actions for invalid player types', () => {
// Logic: Test Commands with wrong parameter types
// Testing: Commands handle type mismatches without throwing
const objectAsPlayer = Poker.Command.fold(validGame, {} as any);
const arrayAsPlayer = Poker.Command.fold(validGame, [] as any);
const functionAsPlayer = Poker.Command.fold(validGame, (() => {}) as any);
expect(typeof objectAsPlayer).toBe('string');
expect(getActionTimestamp(objectAsPlayer)).toBeTypeOf('number');
expect(typeof arrayAsPlayer).toBe('string');
expect(getActionTimestamp(arrayAsPlayer)).toBeTypeOf('number');
expect(typeof functionAsPlayer).toBe('string');
expect(getActionTimestamp(functionAsPlayer)).toBeTypeOf('number');
});
it('should return an empty string for a message from a non-existent player', () => {
const action = Poker.Command.message(validGame, 'UnknownPlayer', 'Hello there');
expect(action).toBe('');
});
});
describe('Invalid Betting Amount Scenarios', () => {
it('should generate bet Actions with negative amounts', () => {
// Logic: Test bet Commands with negative amounts
// Testing: Commands handle negative values without throwing
const negativeBet = Poker.Command.bet(validGame, 0, -100);
const negativeRaise = Poker.Command.raise(validGame, 0, -200);
expect(getActionPlayerIndex(negativeBet)).toBe(0); // Player 0 (p1)
expect(getActionType(negativeBet)).toBe('cc');
expect(getActionAmount(negativeBet)).toBe(20);
expect(getActionTimestamp(negativeBet)).toBeTypeOf('number');
expect(getActionPlayerIndex(negativeRaise)).toBe(0); // Player 0 (p1)
expect(getActionType(negativeRaise)).toBe('cbr');
expect(getActionAmount(negativeRaise)).toBe(40);
expect(getActionTimestamp(negativeRaise)).toBeTypeOf('number');
});
it('should generate bet Actions with zero amounts', () => {
// Logic: Test betting Commands with zero amounts
// Testing: Commands handle edge case amounts
const zeroBet = Poker.Command.bet(validGame, 0, 0);
const zeroRaise = Poker.Command.raise(validGame, 0, 0);
expect(getActionPlayerIndex(zeroBet)).toBe(0); // Player 0 (p1)
expect(getActionType(zeroBet)).toBe('cc');
expect(getActionAmount(zeroBet)).toBe(20);
expect(getActionTimestamp(zeroBet)).toBeTypeOf('number');
expect(getActionPlayerIndex(zeroRaise)).toBe(0); // Player 0 (p1)
expect(getActionType(zeroRaise)).toBe('cbr');
expect(getActionAmount(zeroRaise)).toBe(40);
expect(getActionTimestamp(zeroRaise)).toBeTypeOf('number');
// Zero bets may or may not be valid depending on game rules
// Let applyAction decide
const zeroBetGame = JSON.parse(JSON.stringify(validGame));
const zeroRaiseGame = JSON.parse(JSON.stringify(validGame));
applyAction(zeroBetGame, zeroBet);
applyAction(zeroRaiseGame, zeroRaise);
});
it('should generate bet Actions with non-numeric amounts', () => {
// Logic: Test betting Commands with invalid amount types
// Testing: Commands handle type mismatches in amount parameters
const stringAmount = Poker.Command.bet(validGame, 0, 'hundred' as any);
const nullAmount = Poker.Command.bet(validGame, 0, null as any);
const undefinedAmount = Poker.Command.bet(validGame, 0, undefined as any);
const objectAmount = Poker.Command.bet(validGame, 0, {} as any);
// Commands should generate Actions (may contain invalid amounts)
expect(getActionPlayerIndex(stringAmount)).toBe(0);
expect(getActionType(stringAmount)).toBe('cc');
expect(getActionAmount(stringAmount)).toBe(validGame.bigBlind);
expect(getActionTimestamp(stringAmount)).toBeTypeOf('number');
expect(getActionPlayerIndex(nullAmount)).toBe(0);
expect(getActionType(nullAmount)).toBe('cc');
expect(getActionAmount(nullAmount)).toBe(validGame.bigBlind);
expect(getActionTimestamp(nullAmount)).toBeTypeOf('number');
expect(getActionPlayerIndex(undefinedAmount)).toBe(0);
expect(getActionType(undefinedAmount)).toBe('cc');
expect(getActionAmount(undefinedAmount)).toBe(validGame.bigBlind);
expect(getActionTimestamp(undefinedAmount)).toBeTypeOf('number');
expect(getActionPlayerIndex(objectAmount)).toBe(0);
expect(getActionType(objectAmount)).toBe('cc');
expect(getActionAmount(objectAmount)).toBe(validGame.bigBlind);
expect(getActionTimestamp(objectAmount)).toBeTypeOf('number');
});
it('should generate bet Actions with extremely large amounts', () => {
// Logic: Test betting Commands with unrealistic amounts
// Testing: Commands handle overflow/extreme values
const largeBet = Poker.Command.bet(validGame, 0, 5684843);
const nanBet = Poker.Command.bet(validGame, 0, NaN);
expect(typeof largeBet).toBe('string');
expect(typeof nanBet).toBe('string');
expect(getActionPlayerIndex(largeBet)).toBe(0);
expect(getActionType(largeBet)).toBe('cbr');
expect(getActionAmount(largeBet)).toBe(1000);
expect(getActionTimestamp(largeBet)).toBeTypeOf('number');
expect(getActionPlayerIndex(nanBet)).toBe(0);
expect(getActionType(nanBet)).toBe('cbr');
expect(getActionAmount(nanBet)).toBe(0);
expect(getActionTimestamp(nanBet)).toBeTypeOf('number');
});
it('should generate bet Actions exceeding player stack', () => {
// Logic: Test betting more than player has available
// Testing: Commands generate overbet Actions for applyAction to handle
// Alice has 1000 from BASE_HAND - try to bet 2000
const overbet = Poker.Command.bet(validGame, 0, 2000);
expect(getActionPlayerIndex(overbet)).toBe(0); // Player 0 (p1)
expect(getActionType(overbet)).toBe('cbr');
expect(getActionAmount(overbet)).toBe(1000); // Capped at stack size
expect(getActionTimestamp(overbet)).toBeTypeOf('number');
// applyAction should convert this to all-in or reject it
const result = applyAction(validGame, overbet);
expect(result.players[0].isAllIn || result.players[0].roundBet <= 1000).toBe(true);
});
});
describe('Invalid Game State Scenarios', () => {
it('should generate Actions for completed games but throw when applied', () => {
// Logic: Test Commands when game is already over
// Testing: Commands work even when game state is invalid
const foldInCompletedGame = Poker.Command.fold(completedGame, 0);
const betInCompletedGame = Poker.Command.bet(completedGame, 0, 100);
const dealInCompletedGame = Poker.Command.deal(completedGame);
expect(typeof foldInCompletedGame).toBe('string');
expect(typeof betInCompletedGame).toBe('string');
// When game is completed, no dealer action is needed
expect(dealInCompletedGame).toBeNull();
// applyAction should reject these for completed games
expect(() => applyAction(completedGame, foldInCompletedGame)).toThrow();
expect(() => applyAction(completedGame, betInCompletedGame)).toThrow();
});
it('should generate Actions for folded players but throw when applied', () => {
// Logic: Test Commands for players who already folded
// Testing: Commands generate Actions for inactive players
const foldedPlayerCall = Poker.Command.call(foldedPlayersGame, 1); // Bob folded
const foldedPlayerBet = Poker.Command.bet(foldedPlayersGame, 1, 100); // Bob folded
const foldedPlayerShow = Poker.Command.showCards(foldedPlayersGame, 1);
expect(typeof foldedPlayerCall).toBe('string');
expect(typeof foldedPlayerBet).toBe('string');
expect(typeof foldedPlayerShow).toBe('string');
// applyAction should reject actions from folded players
expect(() => applyAction(foldedPlayersGame, foldedPlayerCall)).toThrow();
expect(() => applyAction(foldedPlayersGame, foldedPlayerBet)).toThrow();
});
it('should generate Actions for all-in players but throw when applied', () => {
// Logic: Test Commands for players who are already all-in
// Testing: Commands handle all-in player states
const allInPlayerBet = Poker.Command.bet(allInGame, 0, 50); // Alice is all-in
const allInPlayerCall = Poker.Command.call(allInGame, 0);
const allInPlayerFold = Poker.Command.fold(allInGame, 0);
expect(typeof allInPlayerBet).toBe('string');
expect(typeof allInPlayerCall).toBe('string');
expect(typeof allInPlayerFold).toBe('string');
// applyAction should reject actions from all-in players (except maybe fold)
expect(() => applyAction(allInGame, allInPlayerBet)).toThrow();
expect(() => applyAction(allInGame, allInPlayerCall)).toThrow();
});
it('should generate Actions for players with zero stacks but throw when applied', () => {
// Logic: Test Commands for players with no chips
// Testing: Commands handle empty stack scenarios
const zeroStackBet = Poker.Command.bet(emptyStackGame, 0, 100);
const zeroStackCall = Poker.Command.call(emptyStackGame, 0);
const zeroStackAllIn = Poker.Command.allIn(emptyStackGame, 0);
expect(typeof zeroStackBet).toBe('string');
expect(typeof zeroStackCall).toBe('string');
expect(getActionPlayerIndex(zeroStackAllIn)).toBe(0); // Player 0 (p1)
expect(getActionType(zeroStackAllIn)).toBe('cc');
expect(getActionAmount(zeroStackAllIn)).toBe(0);
expect(getActionTimestamp(zeroStackAllIn)).toBeTypeOf('number');
expect(() => applyAction(emptyStackGame, zeroStackBet)).toThrow();
});
});
describe('Turn Order Violation Scenarios', () => {
it("should generate Actions for players when it's not their turn but throw when applied", () => {
// Logic: Test Commands for non-active players
// Testing: Commands don't enforce turn order (applyAction does)
// In validGame, Alice (p1) is next to act
const bobOutOfTurn = Poker.Command.fold(validGame, 1); // Bob acts out of turn
const charlieOutOfTurn = Poker.Command.bet(validGame, 2, 100); // Charlie acts out of turn
expect(getActionPlayerIndex(bobOutOfTurn)).toBe(1); // Player 1 (p2 - Bob)
expect(getActionType(bobOutOfTurn)).toBe('f');
expect(getActionTimestamp(bobOutOfTurn)).toBeTypeOf('number');
expect(getActionPlayerIndex(charlieOutOfTurn)).toBe(2); // Player 2 (p3 - Charlie)
expect(getActionType(charlieOutOfTurn)).toBe('cbr');
expect(getActionAmount(charlieOutOfTurn)).toBe(100);
expect(getActionTimestamp(charlieOutOfTurn)).toBeTypeOf('number');
// applyAction should enforce turn order
expect(() => applyAction(validGame, bobOutOfTurn)).toThrow();
expect(() => applyAction(validGame, charlieOutOfTurn)).toThrow();
});
it('should generate player Actions when dealer action expected but throw when applied', () => {
// Fixture: Game state where dealer should act (deal flop)
const dealerTurnHand = Poker.Hand({
...BASE_HAND,
actions: [
...BASE_HAND.actions.slice(0, 3), // Deal hole cards
'p1 cc 20',
'p2 cc 0',
'p3 cc 0', // Preflop complete, dealer should deal flop
],
});
const dealerTurnGame = Poker.Game(dealerTurnHand);
// Logic: Try player actions when dealer should act
// Testing: Commands generate Actions regardless of expected actor
const playerWhenDealer = Poker.Command.fold(dealerTurnGame, 0);
const betWhenDealer = Poker.Command.bet(dealerTurnGame, 0, 100);
expect(typeof playerWhenDealer).toBe('string');
expect(typeof betWhenDealer).toBe('string');
// applyAction should enforce proper turn order
expect(() => applyAction(dealerTurnGame, playerWhenDealer)).toThrow();
});
it('should return null for dealer action when player action expected', () => {
// Logic: Try dealer actions when player should act
// Testing: Command.deal returns null when it's not dealer's turn
const dealerWhenPlayer = Poker.Command.deal(validGame);
const dealHoleWhenPlayer = Poker.Command.deal(validGame);
// When it's player's turn to act, no dealer action is needed
expect(dealerWhenPlayer).toBeNull();
expect(dealHoleWhenPlayer).toBeNull();
});
});
describe('Message Command Edge Cases', () => {
it('should generate message Actions with empty strings', () => {
// Logic: Test message Command with empty/minimal input
// Testing: Message Commands handle edge case content
const emptyMessage = Poker.Command.message(validGame, 0, '');
const spaceMessage = Poker.Command.message(validGame, 0, ' ');
const tabMessage = Poker.Command.message(validGame, 0, '\t');
expect(emptyMessage).toBe('');
expect(getActionType(spaceMessage)).toBe('m');
expect(getActionPlayerIndex(spaceMessage)).toBe(0);
expect(getActionMessage(spaceMessage)).toBe(' ');
expect(getActionTimestamp(spaceMessage)).toBeTypeOf('number');
expect(getActionType(tabMessage)).toBe('m');
expect(getActionPlayerIndex(tabMessage)).toBe(0);
expect(getActionMessage(tabMessage)).toBe('\t');
expect(getActionTimestamp(tabMessage)).toBeTypeOf('number');
});
it('should generate message Actions with special characters', () => {
// Logic: Test message Commands with various special characters
// Testing: Message content handling for unusual characters
const specialChars = Poker.Command.message(validGame, 0, '!@#$%^&*()');
const unicodeChars = Poker.Command.message(validGame, 0, '♠♥♦♣🃏');
const newlineChars = Poker.Command.message(validGame, 0, 'Line1\nLine2');
expect(getActionType(specialChars)).toBe('m');
expect(getActionPlayerIndex(specialChars)).toBe(0);
expect(getActionMessage(specialChars)).toBe('!@#$%^&*()');
expect(getActionTimestamp(specialChars)).toBeTypeOf('number');
expect(getActionType(unicodeChars)).toBe('m');
expect(getActionPlayerIndex(unicodeChars)).toBe(0);
expect(getActionMessage(unicodeChars)).toBe('♠♥♦♣🃏');
expect(getActionTimestamp(unicodeChars)).toBeTypeOf('number');
expect(getActionType(newlineChars)).toBe('m');
expect(getActionPlayerIndex(newlineChars)).toBe(0);
expect(getActionMessage(newlineChars)).toBe('Line1\nLine2');
expect(getActionTimestamp(newlineChars)).toBeTypeOf('number');
// verify actions are applicable
expect(() => applyAction(validGame, specialChars)).not.toThrow();
expect(() => applyAction(validGame, unicodeChars)).not.toThrow();
expect(() => applyAction(validGame, newlineChars)).not.toThrow();
});
it('should generate message Actions with extremely long text', () => {
// Logic: Test message Commands with very long content
// Testing: Message Commands handle large text input
const longText = 'A'.repeat(10000);
const longMessage = Poker.Command.message(validGame, 0, longText);
expect(getActionType(longMessage)).toBe('m');
expect(getActionPlayerIndex(longMessage)).toBe(0);
expect(getActionMessage(longMessage)).toBe(longText);
expect(getActionTimestamp(longMessage)).toBeTypeOf('number');
});
it('should generate message Actions with null/undefined text', () => {
// Logic: Test message Commands with invalid text parameters
// Testing: Message Commands handle missing text gracefully
const nullText = Poker.Command.message(validGame, 0, null as any);
const undefinedText = Poker.Command.message(validGame, 0, undefined as any);
expect(typeof nullText).toBe('string');
expect(typeof undefinedText).toBe('string');
});
});
describe('Auto Command Edge Cases', () => {
it('should generate auto Actions without playerIdentifier', () => {
// Logic: Test auto Command when no specific player provided
// Testing: Auto Commands handle implicit player detection
const autoNoPlayer = Poker.Command.auto(validGame);
expect(typeof autoNoPlayer).toBe('string');
const actionType = getActionType(autoNoPlayer);
expect(actionType === 'f' || actionType === 'sm').toBe(true);
expect(getActionTimestamp(autoNoPlayer)).toBeTypeOf('number');
});
it('should generate auto Actions for invalid players', () => {
// Logic: Test auto Command with non-existent players
// Testing: Auto Commands handle invalid player gracefully
const autoInvalidPlayer = Poker.Command.auto(validGame, 999);
const autoNullPlayer = Poker.Command.auto(validGame, null as any);
expect(typeof autoInvalidPlayer).toBe('string');
expect(getActionTimestamp(autoInvalidPlayer)).toBeTypeOf('number');
expect(typeof autoNullPlayer).toBe('string');
expect(getActionTimestamp(autoNullPlayer)).toBeTypeOf('number');
});
it('should generate auto Actions in different game phases', () => {
// Logic: Test auto Commands in various game phases
// Testing: Auto behavior adapts to game phase (fold vs muck)
const autoBetting = Poker.Command.auto(validGame, 0); // Should fold
const autoShowdown = Poker.Command.auto(completedGame, 0); // Should muck
expect(typeof autoBetting).toBe('string');
expect(getActionPlayerIndex(autoBetting)).toBe(0); // Player 0 (p1)
const bettingType = getActionType(autoBetting);
expect(bettingType === 'f' || bettingType === 'sm').toBe(true);
expect(getActionTimestamp(autoBetting)).toBeTypeOf('number');
expect(typeof autoShowdown).toBe('string');
expect(getActionPlayerIndex(autoShowdown)).toBe(0); // Player 0 (p1)
const showdownType = getActionType(autoShowdown);
expect(showdownType === 'f' || showdownType === 'sm').toBe(true);
expect(getActionTimestamp(autoShowdown)).toBeTypeOf('number');
});
});
describe('ForceShowCards Command Edge Cases', () => {
it('should return null for games where no showdown is needed', () => {
const result = Poker.Command.forceShowCards(completedGame);
expect(result === null || typeof result === 'string').toBe(true);
});
it('should be deterministic even with invalid game states', () => {
const result1 = Poker.Command.forceShowCards(emptyStackGame);
const result2 = Poker.Command.forceShowCards(emptyStackGame);
expect(result1).toBe(result2);
});
});
describe('Consistency Under Error Conditions', () => {
it('should generate identical Actions for identical invalid inputs', () => {
// Logic: Test determinism with invalid inputs
// Testing: Even error Actions are consistent
const invalid1 = Poker.Command.fold(validGame, 999);
const invalid2 = Poker.Command.fold(validGame, 999);
const invalid3 = Poker.Command.fold(validGame, 999);
expect(invalid1).toBe(invalid2);
expect(invalid2).toBe(invalid3);
});
});
});