import { describe, it, expect } from 'vitest'; import { registry, checkWinner, isCellOccupied, placePiece, createInitialState, TicTacToeState, WinnerType, PlayerType } from '@/samples/tic-tac-toe'; import {Entity} from "@/utils/entity"; import {createGameContext} from "@/"; import type { PromptEvent } from '@/utils/command'; function createTestContext() { const ctx = createGameContext(registry, createInitialState); return { registry, ctx }; } function getState(ctx: ReturnType['ctx']): Entity { return ctx.state; } function waitForPrompt(ctx: ReturnType['ctx']): Promise { return new Promise(resolve => { ctx.commands.on('prompt', resolve); }); } describe('TicTacToe - helper functions', () => { describe('checkWinner', () => { it('should return null for empty board', () => { const { ctx } = createTestContext(); const state = getState(ctx); expect(checkWinner(state)).toBeNull(); }); it('should detect horizontal win for X', () => { const { ctx } = createTestContext(); const state = getState(ctx); placePiece(state, 0, 0, 'X'); placePiece(state, 1, 0, 'O'); placePiece(state, 0, 1, 'X'); placePiece(state, 1, 1, 'O'); placePiece(state, 0, 2, 'X'); expect(checkWinner(state)).toBe('X'); }); it('should detect horizontal win for O', () => { const { ctx } = createTestContext(); const state = getState(ctx); placePiece(state, 2, 0, 'X'); placePiece(state, 1, 0, 'O'); placePiece(state, 2, 1, 'X'); placePiece(state, 1, 1, 'O'); placePiece(state, 0, 0, 'X'); placePiece(state, 1, 2, 'O'); expect(checkWinner(state)).toBe('O'); }); it('should detect vertical win', () => { const { ctx } = createTestContext(); const state = getState(ctx); placePiece(state, 0, 0, 'X'); placePiece(state, 0, 1, 'O'); placePiece(state, 1, 0, 'X'); placePiece(state, 1, 1, 'O'); placePiece(state, 2, 0, 'X'); expect(checkWinner(state)).toBe('X'); }); it('should detect diagonal win (top-left to bottom-right)', () => { const { ctx } = createTestContext(); const state = getState(ctx); placePiece(state, 0, 0, 'X'); placePiece(state, 0, 1, 'O'); placePiece(state, 1, 1, 'X'); placePiece(state, 0, 2, 'O'); placePiece(state, 2, 2, 'X'); expect(checkWinner(state)).toBe('X'); }); it('should detect diagonal win (top-right to bottom-left)', () => { const { ctx } = createTestContext(); const state = getState(ctx); placePiece(state, 0, 0, 'X'); placePiece(state, 0, 2, 'O'); placePiece(state, 1, 0, 'X'); placePiece(state, 1, 1, 'O'); placePiece(state, 1, 2, 'X'); placePiece(state, 2, 0, 'O'); expect(checkWinner(state)).toBe('O'); }); it('should return null for no winner', () => { const { ctx } = createTestContext(); const state = getState(ctx); placePiece(state, 0, 0, 'X'); placePiece(state, 0, 1, 'O'); placePiece(state, 1, 2, 'X'); expect(checkWinner(state)).toBeNull(); }); it('should return draw when board is full with no winner', () => { const { ctx } = createTestContext(); const state = getState(ctx); const drawPositions = [ [0, 0, 'X'], [0, 1, 'O'], [0, 2, 'X'], [1, 0, 'X'], [1, 1, 'O'], [1, 2, 'O'], [2, 0, 'O'], [2, 1, 'X'], [2, 2, 'X'], ] as [number, number, PlayerType][]; drawPositions.forEach(([r, c, p], i) => { placePiece(state, r, c, p); }); expect(checkWinner(state)).toBe('draw'); }); }); describe('isCellOccupied', () => { it('should return false for empty cell', () => { const { ctx } = createTestContext(); const state = getState(ctx); expect(isCellOccupied(state, 1, 1)).toBe(false); }); it('should return true for occupied cell', () => { const { ctx } = createTestContext(); const state = getState(ctx); placePiece(state, 1, 1, 'X'); expect(isCellOccupied(state, 1, 1)).toBe(true); }); it('should return false for different cell', () => { const { ctx } = createTestContext(); const state = getState(ctx); placePiece(state, 0, 0, 'X'); expect(isCellOccupied(state, 1, 1)).toBe(false); }); }); describe('placePiece', () => { it('should add a piece to the board', () => { const { ctx } = createTestContext(); const state = getState(ctx); placePiece(state, 1, 1, 'X'); expect(state.value.parts.length).toBe(1); expect(state.value.parts[0].position).toEqual([1, 1]); expect(state.value.parts[0].player).toBe('X'); }); it('should add piece to board region children', () => { const { ctx } = createTestContext(); const state = getState(ctx); placePiece(state, 0, 0, 'O'); const board = state.value.board; expect(board.childIds.length).toBe(1); }); it('should generate unique IDs for pieces', () => { const { ctx } = createTestContext(); const state = getState(ctx); placePiece(state, 0, 0, 'X'); placePiece(state, 0, 1, 'O'); const ids = state.value.parts.map(p => p.id); expect(new Set(ids).size).toBe(2); }); }); }); describe('TicTacToe - game flow', () => { it('should have setup and turn commands registered', () => { const { registry: reg } = createTestContext(); expect(reg.has('setup')).toBe(true); expect(reg.has('turn')).toBe(true); }); it('should setup board when setup command runs', async () => { const { ctx } = createTestContext(); const promptPromise = waitForPrompt(ctx); const runPromise = ctx.commands.run('setup'); const promptEvent = await promptPromise; expect(promptEvent).not.toBeNull(); expect(promptEvent.schema.name).toBe('play'); promptEvent.cancel('test end'); const result = await runPromise; expect(result.success).toBe(false); }); it('should accept valid move via turn command', async () => { const { ctx } = createTestContext(); const promptPromise = waitForPrompt(ctx); const runPromise = ctx.commands.run<{winner: WinnerType}>('turn X 1'); const promptEvent = await promptPromise; expect(promptEvent).not.toBeNull(); expect(promptEvent.schema.name).toBe('play'); const error = promptEvent.tryCommit({ name: 'play', params: ['X', 1, 1], options: {}, flags: {} }); expect(error).toBeNull(); const result = await runPromise; expect(result.success).toBe(true); if (result.success) expect(result.result.winner).toBeNull(); expect(ctx.state.value.parts.length).toBe(1); expect(ctx.state.value.parts[0].position).toEqual([1, 1]); }); it('should reject move for wrong player and re-prompt', async () => { const { ctx } = createTestContext(); const promptPromise = waitForPrompt(ctx); const runPromise = ctx.commands.run<{winner: WinnerType}>('turn X 1'); const promptEvent1 = await promptPromise; // 验证器会拒绝错误的玩家 const error1 = promptEvent1.tryCommit({ name: 'play', params: ['O', 1, 1], options: {}, flags: {} }); expect(error1).toContain('Invalid player'); // 验证失败后,再次尝试有效输入 const error2 = promptEvent1.tryCommit({ name: 'play', params: ['X', 1, 1], options: {}, flags: {} }); expect(error2).toBeNull(); const result = await runPromise; expect(result.success).toBe(true); if (result.success) expect(result.result.winner).toBeNull(); }); it('should reject move to occupied cell and re-prompt', async () => { const { ctx } = createTestContext(); const state = getState(ctx); placePiece(state, 1, 1, 'O'); const promptPromise = waitForPrompt(ctx); const runPromise = ctx.commands.run<{winner: WinnerType}>('turn X 1'); const promptEvent1 = await promptPromise; const error1 = promptEvent1.tryCommit({ name: 'play', params: ['X', 1, 1], options: {}, flags: {} }); expect(error1).toContain('occupied'); // 验证失败后,再次尝试有效输入 const error2 = promptEvent1.tryCommit({ name: 'play', params: ['X', 0, 0], options: {}, flags: {} }); expect(error2).toBeNull(); const result = await runPromise; expect(result.success).toBe(true); if (result.success) expect(result.result.winner).toBeNull(); }); it('should detect winner after winning move', async () => { const { ctx } = createTestContext(); let promptPromise = waitForPrompt(ctx); let runPromise = ctx.commands.run<{winner: WinnerType}>('turn X 1'); let prompt = await promptPromise; const error1 = prompt.tryCommit({ name: 'play', params: ['X', 0, 0], options: {}, flags: {} }); expect(error1).toBeNull(); let result = await runPromise; expect(result.success).toBe(true); if (result.success) expect(result.result.winner).toBeNull(); promptPromise = waitForPrompt(ctx); runPromise = ctx.commands.run('turn O 2'); prompt = await promptPromise; const error2 = prompt.tryCommit({ name: 'play', params: ['O', 0, 1], options: {}, flags: {} }); expect(error2).toBeNull(); result = await runPromise; expect(result.success).toBe(true); if (result.success) expect(result.result.winner).toBeNull(); promptPromise = waitForPrompt(ctx); runPromise = ctx.commands.run('turn X 3'); prompt = await promptPromise; const error3 = prompt.tryCommit({ name: 'play', params: ['X', 1, 0], options: {}, flags: {} }); expect(error3).toBeNull(); result = await runPromise; expect(result.success).toBe(true); if (result.success) expect(result.result.winner).toBeNull(); promptPromise = waitForPrompt(ctx); runPromise = ctx.commands.run('turn O 4'); prompt = await promptPromise; const error4 = prompt.tryCommit({ name: 'play', params: ['O', 0, 2], options: {}, flags: {} }); expect(error4).toBeNull(); result = await runPromise; expect(result.success).toBe(true); if (result.success) expect(result.result.winner).toBeNull(); promptPromise = waitForPrompt(ctx); runPromise = ctx.commands.run('turn X 5'); prompt = await promptPromise; const error5 = prompt.tryCommit({ name: 'play', params: ['X', 2, 0], options: {}, flags: {} }); expect(error5).toBeNull(); result = await runPromise; expect(result.success).toBe(true); if (result.success) expect(result.result.winner).toBe('X'); }); it('should detect draw after 9 moves', async () => { const { ctx } = createTestContext(); const state = getState(ctx); const pieces = [ { id: 'p1', pos: [0, 0], player: 'X' }, { id: 'p2', pos: [2, 2], player: 'O' }, { id: 'p3', pos: [0, 2], player: 'X' }, { id: 'p4', pos: [2, 0], player: 'O' }, { id: 'p5', pos: [1, 0], player: 'X' }, { id: 'p6', pos: [0, 1], player: 'O' }, { id: 'p7', pos: [2, 1], player: 'X' }, { id: 'p8', pos: [1, 2], player: 'O' }, ] as { id: string, pos: [number, number], player: PlayerType}[]; for (const { id, pos, player } of pieces) { placePiece(state, pos[0], pos[1], player); } expect(checkWinner(state)).toBeNull(); const promptPromise = waitForPrompt(ctx); const runPromise = ctx.commands.run<{winner: WinnerType}>('turn X 9'); const prompt = await promptPromise; const error = prompt.tryCommit({ name: 'play', params: ['X', 1, 1], options: {}, flags: {} }); expect(error).toBeNull(); const result = await runPromise; expect(result.success).toBe(true); if (result.success) expect(result.result.winner).toBe('draw'); }); });