import { describe, it, expect } from 'vitest'; import { createGameContext } from '../../src/core/game'; import { createCommandRegistry } from '../../src/utils/command'; import { registerTicTacToeCommands, checkWinner, isCellOccupied, placePiece } from '../../src/samples/tic-tac-toe'; import type { IGameContext } from '../../src/core/game'; import type { Part } from '../../src/core/part'; function createTestContext() { const registry = createCommandRegistry(); registerTicTacToeCommands(registry); const ctx = createGameContext(registry); return { registry, ctx }; } function setupBoard(ctx: IGameContext) { ctx.regions.add({ id: 'board', axes: [ { name: 'x', min: 0, max: 2 }, { name: 'y', min: 0, max: 2 }, ], children: [], }); } function addPiece(ctx: IGameContext, id: string, row: number, col: number) { const board = ctx.regions.get('board'); const part: Part = { id, sides: 1, side: 0, region: board, position: [row, col], }; ctx.parts.add(part); board.value.children.push(ctx.parts.get(id)); } describe('TicTacToe - helper functions', () => { describe('checkWinner', () => { it('should return null for empty board', () => { const { ctx } = createTestContext(); setupBoard(ctx); expect(checkWinner(ctx)).toBeNull(); }); it('should detect horizontal win for X', () => { const { ctx } = createTestContext(); setupBoard(ctx); addPiece(ctx, 'piece-1', 0, 0); addPiece(ctx, 'piece-2', 1, 0); addPiece(ctx, 'piece-3', 0, 1); addPiece(ctx, 'piece-4', 1, 1); addPiece(ctx, 'piece-5', 0, 2); expect(checkWinner(ctx)).toBe('X'); }); it('should detect horizontal win for O', () => { const { ctx } = createTestContext(); setupBoard(ctx); addPiece(ctx, 'piece-1', 2, 0); addPiece(ctx, 'piece-2', 1, 0); addPiece(ctx, 'piece-3', 2, 1); addPiece(ctx, 'piece-4', 1, 1); addPiece(ctx, 'piece-5', 0, 0); addPiece(ctx, 'piece-6', 1, 2); expect(checkWinner(ctx)).toBe('O'); }); it('should detect vertical win', () => { const { ctx } = createTestContext(); setupBoard(ctx); addPiece(ctx, 'piece-1', 0, 0); addPiece(ctx, 'piece-2', 0, 1); addPiece(ctx, 'piece-3', 1, 0); addPiece(ctx, 'piece-4', 1, 1); addPiece(ctx, 'piece-5', 2, 0); expect(checkWinner(ctx)).toBe('X'); }); it('should detect diagonal win (top-left to bottom-right)', () => { const { ctx } = createTestContext(); setupBoard(ctx); addPiece(ctx, 'piece-1', 0, 0); addPiece(ctx, 'piece-2', 0, 1); addPiece(ctx, 'piece-3', 1, 1); addPiece(ctx, 'piece-4', 0, 2); addPiece(ctx, 'piece-5', 2, 2); expect(checkWinner(ctx)).toBe('X'); }); it('should detect diagonal win (top-right to bottom-left)', () => { const { ctx } = createTestContext(); setupBoard(ctx); addPiece(ctx, 'piece-1', 0, 0); addPiece(ctx, 'piece-2', 0, 2); addPiece(ctx, 'piece-3', 1, 0); addPiece(ctx, 'piece-4', 1, 1); addPiece(ctx, 'piece-5', 1, 2); addPiece(ctx, 'piece-6', 2, 0); expect(checkWinner(ctx)).toBe('O'); }); it('should return null for no winner', () => { const { ctx } = createTestContext(); setupBoard(ctx); addPiece(ctx, 'piece-1', 0, 0); addPiece(ctx, 'piece-2', 0, 1); addPiece(ctx, 'piece-3', 1, 2); expect(checkWinner(ctx)).toBeNull(); }); }); describe('isCellOccupied', () => { it('should return false for empty cell', () => { const { ctx } = createTestContext(); setupBoard(ctx); expect(isCellOccupied(ctx, 1, 1)).toBe(false); }); it('should return true for occupied cell', () => { const { ctx } = createTestContext(); setupBoard(ctx); addPiece(ctx, 'piece-1', 1, 1); expect(isCellOccupied(ctx, 1, 1)).toBe(true); }); it('should return false for different cell', () => { const { ctx } = createTestContext(); setupBoard(ctx); addPiece(ctx, 'piece-1', 0, 0); expect(isCellOccupied(ctx, 1, 1)).toBe(false); }); }); describe('placePiece', () => { it('should add a piece to the board', () => { const { ctx } = createTestContext(); setupBoard(ctx); placePiece(ctx, 1, 1, 1); expect(ctx.parts.get('piece-1')).not.toBeNull(); expect(ctx.parts.get('piece-1').value.position).toEqual([1, 1]); }); it('should add piece to board region children', () => { const { ctx } = createTestContext(); setupBoard(ctx); placePiece(ctx, 0, 0, 1); const board = ctx.regions.get('board'); expect(board.value.children.length).toBe(1); }); }); }); describe('TicTacToe - game flow', () => { it('should have setup and turn commands registered', () => { const { registry } = createTestContext(); expect(registry.has('setup')).toBe(true); expect(registry.has('turn')).toBe(true); }); it('should setup board when setup command runs', async () => { const { ctx } = createTestContext(); const runPromise = ctx.commands.run('setup'); const promptEvent = await ctx.prompts.pop(); expect(promptEvent).not.toBeNull(); expect(promptEvent.schema.name).toBe('play'); promptEvent.reject(new Error('test end')); const result = await runPromise; expect(result.success).toBe(false); }); it('should accept valid move via turn command', async () => { const { ctx } = createTestContext(); setupBoard(ctx); const runPromise = ctx.commands.run('turn X 1'); const promptEvent = await ctx.prompts.pop(); expect(promptEvent).not.toBeNull(); expect(promptEvent.schema.name).toBe('play'); promptEvent.resolve({ name: 'play', params: ['X', 1, 1], options: {}, flags: {} }); // After valid non-winning move, turn command prompts again, reject to stop const promptEvent2 = await ctx.prompts.pop(); promptEvent2.reject(new Error('done')); const result = await runPromise; expect(result.success).toBe(false); expect(ctx.parts.get('piece-1')).not.toBeNull(); expect(ctx.parts.get('piece-1').value.position).toEqual([1, 1]); }); it('should reject move for wrong player and re-prompt', async () => { const { ctx } = createTestContext(); setupBoard(ctx); const runPromise = ctx.commands.run('turn X 1'); const promptEvent1 = await ctx.prompts.pop(); promptEvent1.resolve({ name: 'play', params: ['O', 1, 1], options: {}, flags: {} }); const promptEvent2 = await ctx.prompts.pop(); expect(promptEvent2).not.toBeNull(); promptEvent2.resolve({ name: 'play', params: ['X', 1, 1], options: {}, flags: {} }); // After valid non-winning move, reject next prompt const promptEvent3 = await ctx.prompts.pop(); promptEvent3.reject(new Error('done')); const result = await runPromise; expect(result.success).toBe(false); }); it('should reject move to occupied cell and re-prompt', async () => { const { ctx } = createTestContext(); setupBoard(ctx); addPiece(ctx, 'piece-0', 1, 1); const runPromise = ctx.commands.run('turn X 1'); const promptEvent1 = await ctx.prompts.pop(); promptEvent1.resolve({ name: 'play', params: ['X', 1, 1], options: {}, flags: {} }); const promptEvent2 = await ctx.prompts.pop(); expect(promptEvent2).not.toBeNull(); promptEvent2.resolve({ name: 'play', params: ['X', 0, 0], options: {}, flags: {} }); // After valid non-winning move, reject next prompt const promptEvent3 = await ctx.prompts.pop(); promptEvent3.reject(new Error('done')); const result = await runPromise; expect(result.success).toBe(false); }); it('should detect winner after winning move', async () => { const { ctx } = createTestContext(); setupBoard(ctx); // X plays (0,0) let runPromise = ctx.commands.run('turn X 1'); let prompt = await ctx.prompts.pop(); prompt.resolve({ name: 'play', params: ['X', 0, 0], options: {}, flags: {} }); let promptNext = await ctx.prompts.pop(); promptNext.reject(new Error('next turn')); let result = await runPromise; expect(result.success).toBe(false); // O plays (0,1) runPromise = ctx.commands.run('turn O 2'); prompt = await ctx.prompts.pop(); prompt.resolve({ name: 'play', params: ['O', 0, 1], options: {}, flags: {} }); promptNext = await ctx.prompts.pop(); promptNext.reject(new Error('next turn')); result = await runPromise; expect(result.success).toBe(false); // X plays (1,0) runPromise = ctx.commands.run('turn X 3'); prompt = await ctx.prompts.pop(); prompt.resolve({ name: 'play', params: ['X', 1, 0], options: {}, flags: {} }); promptNext = await ctx.prompts.pop(); promptNext.reject(new Error('next turn')); result = await runPromise; expect(result.success).toBe(false); // O plays (0,2) runPromise = ctx.commands.run('turn O 4'); prompt = await ctx.prompts.pop(); prompt.resolve({ name: 'play', params: ['O', 0, 2], options: {}, flags: {} }); promptNext = await ctx.prompts.pop(); promptNext.reject(new Error('next turn')); result = await runPromise; expect(result.success).toBe(false); // X plays (2,0) - wins with vertical line runPromise = ctx.commands.run('turn X 5'); prompt = await ctx.prompts.pop(); prompt.resolve({ name: 'play', params: ['X', 2, 0], options: {}, flags: {} }); result = await runPromise; expect(result.success).toBe(true); if (result.success) expect((result.result as any).winner).toBe('X'); }); it('should detect draw after 9 moves', async () => { const { ctx } = createTestContext(); setupBoard(ctx); // Pre-place 8 pieces that don't form any winning line for either player // Using positions that clearly don't form lines // X pieces at even indices, O pieces at odd indices const pieces = [ { id: 'p1', pos: [0, 0] }, // X { id: 'p2', pos: [2, 2] }, // O { id: 'p3', pos: [0, 2] }, // X { id: 'p4', pos: [2, 0] }, // O { id: 'p5', pos: [1, 0] }, // X { id: 'p6', pos: [0, 1] }, // O { id: 'p7', pos: [2, 1] }, // X { id: 'p8', pos: [1, 2] }, // O ]; for (const { id, pos } of pieces) { addPiece(ctx, id, pos[0], pos[1]); } // Verify no winner before 9th move expect(checkWinner(ctx)).toBeNull(); // Now X plays (1,1) for the 9th move -> draw const runPromise = ctx.commands.run('turn X 9'); const prompt = await ctx.prompts.pop(); prompt.resolve({ name: 'play', params: ['X', 1, 1], options: {}, flags: {} }); const result = await runPromise; expect(result.success).toBe(true); if (result.success) expect((result.result as any).winner).toBe('draw'); }); });