320 lines
13 KiB
TypeScript
320 lines
13 KiB
TypeScript
import { describe, it, expect, beforeEach } from 'vitest';
|
|
import { createGameState } from '../../src/core/GameState';
|
|
import { RuleEngine } from '../../src/rules/RuleEngine';
|
|
import { RegionType } from '../../src/core/Region';
|
|
import type { Player, TicTacToeMetadata } from '../../src/games/tictactoe/TicTacToeState';
|
|
import {
|
|
getCellId,
|
|
getAllCellIds,
|
|
getWinningCombinations,
|
|
} from '../../src/games/tictactoe/TicTacToeState';
|
|
import {
|
|
ticTacToeRules,
|
|
startGameCommand,
|
|
createMarkCellCommand,
|
|
resetGameCommand,
|
|
} from '../../src/games/tictactoe';
|
|
|
|
describe('Tic Tac Toe', () => {
|
|
let gameState: ReturnType<typeof createGameState>;
|
|
let ruleEngine: RuleEngine;
|
|
|
|
beforeEach(async () => {
|
|
gameState = createGameState({
|
|
id: 'tictactoe-game',
|
|
name: 'Tic Tac Toe',
|
|
metadata: {
|
|
ticTacToe: {
|
|
currentPlayer: 'X' as Player,
|
|
gameEnded: false,
|
|
winner: null,
|
|
moveHistory: [],
|
|
totalMoves: 0,
|
|
},
|
|
},
|
|
});
|
|
|
|
ruleEngine = new RuleEngine(gameState, { gameType: 'tictactoe' });
|
|
ruleEngine.registerRules(ticTacToeRules);
|
|
|
|
// Start the game
|
|
await ruleEngine.executeCommand(startGameCommand);
|
|
});
|
|
|
|
describe('game initialization', () => {
|
|
it('should create the board region', () => {
|
|
const board = gameState.getRegion('board');
|
|
expect(board).toBeDefined();
|
|
expect(board?.type).toBe(RegionType.Keyed);
|
|
});
|
|
|
|
it('should initialize all cells', () => {
|
|
const cellIds = getAllCellIds(3);
|
|
expect(cellIds.length).toBe(9);
|
|
|
|
const board = gameState.getRegion('board');
|
|
expect(board).toBeDefined();
|
|
});
|
|
|
|
it('should set initial game state', () => {
|
|
const metadata = gameState.data.value.metadata?.ticTacToe as TicTacToeMetadata;
|
|
expect(metadata.currentPlayer).toBe('X');
|
|
expect(metadata.gameEnded).toBe(false);
|
|
expect(metadata.winner).toBe(null);
|
|
});
|
|
});
|
|
|
|
describe('marking cells', () => {
|
|
it('should allow player X to mark an empty cell', async () => {
|
|
const command = createMarkCellCommand('cell-A1', 'X');
|
|
const result = await ruleEngine.executeCommand(command);
|
|
|
|
expect(result.success).toBe(true);
|
|
|
|
const placement = gameState.getPlacement('cell-A1');
|
|
expect(placement).toBeDefined();
|
|
expect(placement?.metadata?.player).toBe('X');
|
|
});
|
|
|
|
it('should switch to player O after X moves', async () => {
|
|
const command = createMarkCellCommand('cell-A1', 'X');
|
|
await ruleEngine.executeCommand(command);
|
|
|
|
const metadata = gameState.data.value.metadata?.ticTacToe as TicTacToeMetadata;
|
|
expect(metadata.currentPlayer).toBe('O');
|
|
});
|
|
|
|
it('should record move history', async () => {
|
|
const command = createMarkCellCommand('cell-A1', 'X');
|
|
await ruleEngine.executeCommand(command);
|
|
|
|
const metadata = gameState.data.value.metadata?.ticTacToe as TicTacToeMetadata;
|
|
expect(metadata.moveHistory.length).toBe(1);
|
|
expect(metadata.moveHistory[0].player).toBe('X');
|
|
expect(metadata.moveHistory[0].cellId).toBe('cell-A1');
|
|
});
|
|
|
|
it('should not allow marking an occupied cell', async () => {
|
|
const command1 = createMarkCellCommand('cell-A1', 'X');
|
|
await ruleEngine.executeCommand(command1);
|
|
|
|
const command2 = createMarkCellCommand('cell-A1', 'O');
|
|
const result = await ruleEngine.executeCommand(command2);
|
|
|
|
expect(result.success).toBe(false);
|
|
expect(result.error).toContain('already occupied');
|
|
});
|
|
|
|
it('should not allow wrong player to move', async () => {
|
|
// Try to place O when it's X's turn
|
|
const command = createMarkCellCommand('cell-A1', 'O');
|
|
const result = await ruleEngine.executeCommand(command);
|
|
|
|
expect(result.success).toBe(false);
|
|
expect(result.error).toContain("It is X's turn");
|
|
});
|
|
|
|
it('should not allow moves after game ends', async () => {
|
|
// Set up a winning scenario
|
|
await ruleEngine.executeCommand(createMarkCellCommand('cell-A1', 'X'));
|
|
await ruleEngine.executeCommand(createMarkCellCommand('cell-B1', 'O'));
|
|
await ruleEngine.executeCommand(createMarkCellCommand('cell-A2', 'X'));
|
|
await ruleEngine.executeCommand(createMarkCellCommand('cell-B2', 'O'));
|
|
await ruleEngine.executeCommand(createMarkCellCommand('cell-A3', 'X'));
|
|
|
|
// Game should end with X winning
|
|
const metadata = gameState.data.value.metadata?.ticTacToe as TicTacToeMetadata;
|
|
expect(metadata.gameEnded).toBe(true);
|
|
expect(metadata.winner).toBe('X');
|
|
|
|
// Try to make another move
|
|
const command = createMarkCellCommand('cell-C1', 'O');
|
|
const result = await ruleEngine.executeCommand(command);
|
|
|
|
expect(result.success).toBe(false);
|
|
expect(result.error).toContain('Game has already ended');
|
|
});
|
|
});
|
|
|
|
describe('win conditions', () => {
|
|
it('should detect horizontal win for X', async () => {
|
|
await ruleEngine.executeCommand(createMarkCellCommand('cell-A1', 'X'));
|
|
await ruleEngine.executeCommand(createMarkCellCommand('cell-B1', 'O'));
|
|
await ruleEngine.executeCommand(createMarkCellCommand('cell-A2', 'X'));
|
|
await ruleEngine.executeCommand(createMarkCellCommand('cell-B2', 'O'));
|
|
await ruleEngine.executeCommand(createMarkCellCommand('cell-A3', 'X'));
|
|
|
|
const metadata = gameState.data.value.metadata?.ticTacToe as TicTacToeMetadata;
|
|
expect(metadata.gameEnded).toBe(true);
|
|
expect(metadata.winner).toBe('X');
|
|
expect(metadata.winningCombination).toEqual(['cell-A1', 'cell-A2', 'cell-A3']);
|
|
});
|
|
|
|
it('should detect horizontal win for O', async () => {
|
|
await ruleEngine.executeCommand(createMarkCellCommand('cell-A1', 'X'));
|
|
await ruleEngine.executeCommand(createMarkCellCommand('cell-B1', 'O'));
|
|
await ruleEngine.executeCommand(createMarkCellCommand('cell-C1', 'X'));
|
|
await ruleEngine.executeCommand(createMarkCellCommand('cell-B2', 'O'));
|
|
await ruleEngine.executeCommand(createMarkCellCommand('cell-C2', 'X'));
|
|
await ruleEngine.executeCommand(createMarkCellCommand('cell-B3', 'O'));
|
|
|
|
const metadata = gameState.data.value.metadata?.ticTacToe as TicTacToeMetadata;
|
|
expect(metadata.gameEnded).toBe(true);
|
|
expect(metadata.winner).toBe('O');
|
|
expect(metadata.winningCombination).toEqual(['cell-B1', 'cell-B2', 'cell-B3']);
|
|
});
|
|
|
|
it('should detect vertical win', async () => {
|
|
await ruleEngine.executeCommand(createMarkCellCommand('cell-A1', 'X'));
|
|
await ruleEngine.executeCommand(createMarkCellCommand('cell-B1', 'O'));
|
|
await ruleEngine.executeCommand(createMarkCellCommand('cell-C1', 'X'));
|
|
await ruleEngine.executeCommand(createMarkCellCommand('cell-C2', 'O'));
|
|
await ruleEngine.executeCommand(createMarkCellCommand('cell-B2', 'X'));
|
|
await ruleEngine.executeCommand(createMarkCellCommand('cell-C3', 'O'));
|
|
|
|
const metadata = gameState.data.value.metadata?.ticTacToe as TicTacToeMetadata;
|
|
expect(metadata.gameEnded).toBe(true);
|
|
expect(metadata.winner).toBe('O');
|
|
expect(metadata.winningCombination).toEqual(['cell-C1', 'cell-C2', 'cell-C3']);
|
|
});
|
|
|
|
it('should detect main diagonal win', async () => {
|
|
await ruleEngine.executeCommand(createMarkCellCommand('cell-A1', 'X'));
|
|
await ruleEngine.executeCommand(createMarkCellCommand('cell-B1', 'O'));
|
|
await ruleEngine.executeCommand(createMarkCellCommand('cell-B2', 'X'));
|
|
await ruleEngine.executeCommand(createMarkCellCommand('cell-C1', 'O'));
|
|
await ruleEngine.executeCommand(createMarkCellCommand('cell-C3', 'X'));
|
|
|
|
const metadata = gameState.data.value.metadata?.ticTacToe as TicTacToeMetadata;
|
|
expect(metadata.gameEnded).toBe(true);
|
|
expect(metadata.winner).toBe('X');
|
|
expect(metadata.winningCombination).toEqual(['cell-A1', 'cell-B2', 'cell-C3']);
|
|
});
|
|
|
|
it('should detect anti-diagonal win', async () => {
|
|
await ruleEngine.executeCommand(createMarkCellCommand('cell-C1', 'X'));
|
|
await ruleEngine.executeCommand(createMarkCellCommand('cell-A1', 'O'));
|
|
await ruleEngine.executeCommand(createMarkCellCommand('cell-B2', 'X'));
|
|
await ruleEngine.executeCommand(createMarkCellCommand('cell-A2', 'O'));
|
|
await ruleEngine.executeCommand(createMarkCellCommand('cell-A3', 'X'));
|
|
|
|
const metadata = gameState.data.value.metadata?.ticTacToe as TicTacToeMetadata;
|
|
expect(metadata.gameEnded).toBe(true);
|
|
expect(metadata.winner).toBe('X');
|
|
expect(metadata.winningCombination).toEqual(['cell-A3', 'cell-B2', 'cell-C1']);
|
|
});
|
|
});
|
|
|
|
describe('draw condition', () => {
|
|
it('should detect a draw when all cells are filled without winner', async () => {
|
|
// Fill the board with no winner
|
|
const moves = [
|
|
['cell-A1', 'X'],
|
|
['cell-A2', 'O'],
|
|
['cell-A3', 'X'],
|
|
['cell-B1', 'O'],
|
|
['cell-B3', 'X'],
|
|
['cell-B2', 'O'],
|
|
['cell-C2', 'X'],
|
|
['cell-C1', 'O'],
|
|
['cell-C3', 'X'],
|
|
] as [string, Player][];
|
|
|
|
for (const [cell, player] of moves) {
|
|
const command = createMarkCellCommand(cell, player);
|
|
await ruleEngine.executeCommand(command);
|
|
}
|
|
|
|
const metadata = gameState.data.value.metadata?.ticTacToe as TicTacToeMetadata;
|
|
expect(metadata.gameEnded).toBe(true);
|
|
expect(metadata.winner).toBe(null); // Draw
|
|
expect(metadata.totalMoves).toBe(9);
|
|
});
|
|
});
|
|
|
|
describe('reset game', () => {
|
|
it('should reset the board for a new game', async () => {
|
|
// Make some moves
|
|
await ruleEngine.executeCommand(createMarkCellCommand('cell-A1', 'X'));
|
|
await ruleEngine.executeCommand(createMarkCellCommand('cell-B1', 'O'));
|
|
|
|
// Reset
|
|
await ruleEngine.executeCommand(resetGameCommand);
|
|
|
|
// Check that cells are empty
|
|
const board = gameState.getRegion('board');
|
|
expect(board).toBeDefined();
|
|
|
|
// Game should be reset
|
|
const metadata = gameState.data.value.metadata?.ticTacToe as TicTacToeMetadata;
|
|
expect(metadata.currentPlayer).toBe('X');
|
|
expect(metadata.gameEnded).toBe(false);
|
|
expect(metadata.winner).toBe(null);
|
|
});
|
|
});
|
|
|
|
describe('rule engine integration', () => {
|
|
it('should execute all rules in correct order', async () => {
|
|
const command = createMarkCellCommand('cell-B2', 'X');
|
|
const result = await ruleEngine.executeCommand(command);
|
|
|
|
expect(result.success).toBe(true);
|
|
expect(result.validationRules.length).toBeGreaterThan(0);
|
|
expect(result.effectRules.length).toBeGreaterThan(0);
|
|
|
|
// Check that validation rules ran
|
|
const validationRuleIds = result.validationRules.map((r) => r.ruleId);
|
|
expect(validationRuleIds).toContain('tictactoe-validate-turn');
|
|
expect(validationRuleIds).toContain('tictactoe-validate-cell-empty');
|
|
expect(validationRuleIds).toContain('tictactoe-validate-game-not-ended');
|
|
|
|
// Check that effect rules ran
|
|
const effectRuleIds = result.effectRules.map((r) => r.ruleId);
|
|
expect(effectRuleIds).toContain('tictactoe-switch-turn');
|
|
expect(effectRuleIds).toContain('tictactoe-record-history');
|
|
});
|
|
|
|
it('should trigger win condition check after each move', async () => {
|
|
// Set up a winning scenario
|
|
await ruleEngine.executeCommand(createMarkCellCommand('cell-A1', 'X'));
|
|
await ruleEngine.executeCommand(createMarkCellCommand('cell-B1', 'O'));
|
|
await ruleEngine.executeCommand(createMarkCellCommand('cell-A2', 'X'));
|
|
await ruleEngine.executeCommand(createMarkCellCommand('cell-B2', 'O'));
|
|
|
|
const winningMove = createMarkCellCommand('cell-A3', 'X');
|
|
const result = await ruleEngine.executeCommand(winningMove);
|
|
|
|
// Check that trigger rules ran
|
|
const triggerRuleIds = result.triggerRules.map((r) => r.ruleId);
|
|
expect(triggerRuleIds).toContain('tictactoe-check-win');
|
|
});
|
|
});
|
|
|
|
describe('helper functions', () => {
|
|
it('should generate correct cell IDs', () => {
|
|
expect(getCellId(0, 0)).toBe('cell-A1');
|
|
expect(getCellId(1, 1)).toBe('cell-B2');
|
|
expect(getCellId(2, 2)).toBe('cell-C3');
|
|
});
|
|
|
|
it('should return correct winning combinations', () => {
|
|
const combinations = getWinningCombinations(3);
|
|
expect(combinations.length).toBe(8); // 3 rows + 3 columns + 2 diagonals
|
|
|
|
// Check rows
|
|
expect(combinations[0]).toEqual(['cell-A1', 'cell-A2', 'cell-A3']);
|
|
expect(combinations[1]).toEqual(['cell-B1', 'cell-B2', 'cell-B3']);
|
|
expect(combinations[2]).toEqual(['cell-C1', 'cell-C2', 'cell-C3']);
|
|
|
|
// Check columns
|
|
expect(combinations[3]).toEqual(['cell-A1', 'cell-B1', 'cell-C1']);
|
|
expect(combinations[4]).toEqual(['cell-A2', 'cell-B2', 'cell-C2']);
|
|
expect(combinations[5]).toEqual(['cell-A3', 'cell-B3', 'cell-C3']);
|
|
|
|
// Check diagonals
|
|
expect(combinations[6]).toEqual(['cell-A1', 'cell-B2', 'cell-C3']);
|
|
expect(combinations[7]).toEqual(['cell-A3', 'cell-B2', 'cell-C1']);
|
|
});
|
|
});
|
|
});
|