@idealic/poker-engine
Version:
Poker game engine and hand evaluator
166 lines (138 loc) • 6.25 kB
text/typescript
import { describe, expect, it } from 'vitest';
import * as Poker from '../../../index';
import { BASE_HAND, SHOWDOWN_HAND } from './fixtures/baseGame';
describe('Game API - Core Contract', () => {
describe('Rules Engine Contract', () => {
it('should own all game logic and validation', () => {
const game = Poker.Game(BASE_HAND);
// Game should provide comprehensive validation
expect(typeof Poker.Game.canApplyAction).toBe('function');
expect(typeof Poker.Game.applyAction).toBe('function');
// Game should transform Hand to runtime state
expect(game).toHaveProperty('players');
expect(game).toHaveProperty('pot');
expect(game).toHaveProperty('street');
expect(game).toHaveProperty('nextPlayerIndex');
expect(game).toHaveProperty('isShowdown');
expect(game).toHaveProperty('isComplete');
});
it('should maintain deterministic state from identical Hand inputs', () => {
const game1 = Poker.Game(BASE_HAND);
const game2 = Poker.Game(BASE_HAND);
// Same Hand should produce identical Game states
expect(game1.pot).toBe(game2.pot);
expect(game1.street).toBe(game2.street);
expect(game1.nextPlayerIndex).toBe(game2.nextPlayerIndex);
expect(JSON.stringify(game1.players)).toBe(JSON.stringify(game2.players));
expect(game1.board).toEqual(game2.board);
});
it('should never modify input Hand data', () => {
const handCopy = JSON.parse(JSON.stringify(BASE_HAND));
const game = Poker.Game(BASE_HAND);
// Apply some action to game
if (game.nextPlayerIndex >= 0) {
try {
const action = `p${game.nextPlayerIndex + 1} cc`;
Poker.Game.applyAction(game, action);
} catch {
// Action might be invalid, that's ok for this test
}
}
// Original Hand should remain unchanged
expect(JSON.stringify(BASE_HAND)).toBe(JSON.stringify(handCopy));
});
});
describe('Query Methods Contract', () => {
it('should have no side effects for query operations', () => {
const game = Poker.Game(BASE_HAND);
const gameCopy = JSON.parse(JSON.stringify(game));
// Call all query methods
Poker.Game.getPlayerIndex(game, 'Alice');
Poker.Game.getTimeLeft(game);
Poker.Game.getElapsedTime(game);
Poker.Game.hasActed(game, 'Charlie');
Poker.Game.canApplyAction(game, 'p3 cc');
// Game state should remain unchanged (except for any cached computations)
// Compare key mutable properties
expect(game.pot).toBe(gameCopy.pot);
expect(game.street).toBe(gameCopy.street);
expect(game.nextPlayerIndex).toBe(gameCopy.nextPlayerIndex);
expect(JSON.stringify(game.players)).toBe(JSON.stringify(gameCopy.players));
});
it('should correctly use canApplyAction', () => {
const game = Poker.Game({ ...BASE_HAND, actions: [] });
// Apply all actions
for (const action of BASE_HAND.actions) {
expect(Poker.Game.canApplyAction(game, action)).toBe(true);
expect(() => Poker.Game.applyAction(game, action)).not.toThrow();
}
expect(Poker.Game(BASE_HAND).lastAction).toBe(
BASE_HAND.actions[BASE_HAND.actions.length - 1]
);
});
it('should return -1 for not found instead of throwing', () => {
const game = Poker.Game(BASE_HAND);
// Should return -1 for not found, not throw
expect(Poker.Game.getPlayerIndex(game, 'NonExistent')).toBe(-1);
expect(Poker.Game.getPlayerIndex(game, -1)).toBe(-1);
expect(Poker.Game.getPlayerIndex(game, 100)).toBe(-1);
// Should return false for invalid players
expect(Poker.Game.hasActed(game, 'NonExistent')).toBe(false);
expect(Poker.Game.hasActed(game, -1)).toBe(false);
expect(Poker.Game.hasActed(game, 100)).toBe(false);
});
});
describe('Separation of Concerns', () => {
it('should own all game logic while Hand owns data notation', () => {
const hand = BASE_HAND;
const game = Poker.Game(hand);
// Game owns runtime state and logic
expect(game).toHaveProperty('isShowdown');
expect(game).toHaveProperty('nextPlayerIndex');
expect(game).toHaveProperty('pot');
expect(game).toHaveProperty('street');
expect(game).toHaveProperty('board');
expect(game).toHaveProperty('players');
// Hand owns data notation
expect(hand).toHaveProperty('actions');
expect(hand).toHaveProperty('variant');
expect(hand).toHaveProperty('startingStacks');
expect(hand).not.toHaveProperty('isShowdown');
expect(hand).not.toHaveProperty('nextPlayerIndex');
expect(hand).not.toHaveProperty('pot');
});
it('should provide all necessary methods for game management', () => {
// Constructor
expect(typeof Poker.Game).toBe('function');
// Query methods
expect(typeof Poker.Game.getPlayerIndex).toBe('function');
expect(typeof Poker.Game.getTimeLeft).toBe('function');
expect(typeof Poker.Game.getElapsedTime).toBe('function');
expect(typeof Poker.Game.hasActed).toBe('function');
// Validation
expect(typeof Poker.Game.canApplyAction).toBe('function');
// State modification
expect(typeof Poker.Game.applyAction).toBe('function');
expect(typeof Poker.Game.finish).toBe('function');
});
});
describe('Immutability Contract', () => {
it('should not mutate Hand when creating Game', () => {
const originalActions = [...BASE_HAND.actions];
const originalStacks = [...BASE_HAND.startingStacks];
expect(() => Poker.Game(BASE_HAND)).not.toThrow();
expect(BASE_HAND.actions).toEqual(originalActions);
expect(BASE_HAND.startingStacks).toEqual(originalStacks);
});
it('should not mutate Hand when calling finish', () => {
const hand = { ...SHOWDOWN_HAND };
const originalHand = JSON.parse(JSON.stringify(hand));
const game = Poker.Game(hand);
expect(() => Poker.Game.finish(game, hand)).not.toThrow();
// Original hand should be unchanged if we didn't complete it
if (!game.isComplete) {
expect(hand).toEqual(originalHand);
}
});
});
});