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; 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']); }); }); });