From b7c5312b60f20a88807474029f57f8e332581d9b Mon Sep 17 00:00:00 2001 From: hypercross Date: Thu, 2 Apr 2026 15:06:04 +0800 Subject: [PATCH] fix: fix tic tac toe tests --- tests/samples/tic-tac-toe.test.ts | 304 ++++++++++++++++-------------- 1 file changed, 166 insertions(+), 138 deletions(-) diff --git a/tests/samples/tic-tac-toe.test.ts b/tests/samples/tic-tac-toe.test.ts index 333c05f..d27c5a5 100644 --- a/tests/samples/tic-tac-toe.test.ts +++ b/tests/samples/tic-tac-toe.test.ts @@ -1,169 +1,200 @@ import { describe, it, expect } from 'vitest'; -import {registry, checkWinner, isCellOccupied, placePiece, createInitialState} from '../../src/samples/tic-tac-toe'; -import type { TicTacToeContext } from '../../src/samples/tic-tac-toe'; -import type { Part } from '../../src/core/part'; +import {registry, checkWinner, isCellOccupied, placePiece, createInitialState, TicTacToeState} from '../../src/samples/tic-tac-toe'; +import {Entity, entity} from "../../src/utils/entity"; import {createGameContext} from "../../src"; +import type { PromptEvent } from '../../src/utils/command'; function createTestContext() { const ctx = createGameContext(registry, createInitialState); return { registry, ctx }; } -function setupBoard(ctx: TicTacToeContext) { - ctx.regions.add({ - id: 'board', - axes: [ - { name: 'x', min: 0, max: 2 }, - { name: 'y', min: 0, max: 2 }, - ], - children: [], +function getState(ctx: ReturnType['ctx']): Entity { + return ctx.state; +} + +function addPiece(state: Entity, id: string, row: number, col: number, player: 'X' | 'O') { + const board = state.value.board; + const part = { + id, + region: board, + position: [row, col], + player, + }; + const e = entity(id, part); + state.produce(draft => { + draft.parts.push(e); + board.produce(boardDraft => { + boardDraft.children.push(e); + }); }); } -function addPiece(ctx: TicTacToeContext, 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)); +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(); - setupBoard(ctx); + const state = getState(ctx); - expect(checkWinner(ctx)).toBeNull(); + expect(checkWinner(state)).toBeNull(); }); it('should detect horizontal win for X', () => { const { ctx } = createTestContext(); - setupBoard(ctx); + const state = getState(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); + addPiece(state, 'piece-1', 0, 0, 'X'); + addPiece(state, 'piece-2', 1, 0, 'O'); + addPiece(state, 'piece-3', 0, 1, 'X'); + addPiece(state, 'piece-4', 1, 1, 'O'); + addPiece(state, 'piece-5', 0, 2, 'X'); - expect(checkWinner(ctx)).toBe('X'); + expect(checkWinner(state)).toBe('X'); }); it('should detect horizontal win for O', () => { const { ctx } = createTestContext(); - setupBoard(ctx); + const state = getState(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); + addPiece(state, 'piece-1', 2, 0, 'X'); + addPiece(state, 'piece-2', 1, 0, 'O'); + addPiece(state, 'piece-3', 2, 1, 'X'); + addPiece(state, 'piece-4', 1, 1, 'O'); + addPiece(state, 'piece-5', 0, 0, 'X'); + addPiece(state, 'piece-6', 1, 2, 'O'); - expect(checkWinner(ctx)).toBe('O'); + expect(checkWinner(state)).toBe('O'); }); it('should detect vertical win', () => { const { ctx } = createTestContext(); - setupBoard(ctx); + const state = getState(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); + addPiece(state, 'piece-1', 0, 0, 'X'); + addPiece(state, 'piece-2', 0, 1, 'O'); + addPiece(state, 'piece-3', 1, 0, 'X'); + addPiece(state, 'piece-4', 1, 1, 'O'); + addPiece(state, 'piece-5', 2, 0, 'X'); - expect(checkWinner(ctx)).toBe('X'); + expect(checkWinner(state)).toBe('X'); }); it('should detect diagonal win (top-left to bottom-right)', () => { const { ctx } = createTestContext(); - setupBoard(ctx); + const state = getState(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); + addPiece(state, 'piece-1', 0, 0, 'X'); + addPiece(state, 'piece-2', 0, 1, 'O'); + addPiece(state, 'piece-3', 1, 1, 'X'); + addPiece(state, 'piece-4', 0, 2, 'O'); + addPiece(state, 'piece-5', 2, 2, 'X'); - expect(checkWinner(ctx)).toBe('X'); + expect(checkWinner(state)).toBe('X'); }); it('should detect diagonal win (top-right to bottom-left)', () => { const { ctx } = createTestContext(); - setupBoard(ctx); + const state = getState(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); + addPiece(state, 'piece-1', 0, 0, 'X'); + addPiece(state, 'piece-2', 0, 2, 'O'); + addPiece(state, 'piece-3', 1, 0, 'X'); + addPiece(state, 'piece-4', 1, 1, 'O'); + addPiece(state, 'piece-5', 1, 2, 'X'); + addPiece(state, 'piece-6', 2, 0, 'O'); - expect(checkWinner(ctx)).toBe('O'); + expect(checkWinner(state)).toBe('O'); }); it('should return null for no winner', () => { const { ctx } = createTestContext(); - setupBoard(ctx); + const state = getState(ctx); - addPiece(ctx, 'piece-1', 0, 0); - addPiece(ctx, 'piece-2', 0, 1); - addPiece(ctx, 'piece-3', 1, 2); + addPiece(state, 'piece-1', 0, 0, 'X'); + addPiece(state, 'piece-2', 0, 1, 'O'); + addPiece(state, 'piece-3', 1, 2, 'X'); - expect(checkWinner(ctx)).toBeNull(); + 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'], + ]; + + drawPositions.forEach(([r, c, p], i) => { + addPiece(state, 'piece-' + (i + 1), r, c, p); + }); + + expect(checkWinner(state)).toBe('draw'); }); }); describe('isCellOccupied', () => { it('should return false for empty cell', () => { const { ctx } = createTestContext(); - setupBoard(ctx); + const state = getState(ctx); - expect(isCellOccupied(ctx, 1, 1)).toBe(false); + expect(isCellOccupied(state, 1, 1)).toBe(false); }); it('should return true for occupied cell', () => { const { ctx } = createTestContext(); - setupBoard(ctx); - addPiece(ctx, 'piece-1', 1, 1); + const state = getState(ctx); + addPiece(state, 'piece-1', 1, 1, 'X'); - expect(isCellOccupied(ctx, 1, 1)).toBe(true); + expect(isCellOccupied(state, 1, 1)).toBe(true); }); it('should return false for different cell', () => { const { ctx } = createTestContext(); - setupBoard(ctx); - addPiece(ctx, 'piece-1', 0, 0); + const state = getState(ctx); + addPiece(state, 'piece-1', 0, 0, 'X'); - expect(isCellOccupied(ctx, 1, 1)).toBe(false); + expect(isCellOccupied(state, 1, 1)).toBe(false); }); }); describe('placePiece', () => { it('should add a piece to the board', () => { const { ctx } = createTestContext(); - setupBoard(ctx); - placePiece(ctx, 1, 1, 1); + const state = getState(ctx); + placePiece(state, 1, 1, 'X'); - expect(ctx.parts.get('piece-1')).not.toBeNull(); - expect(ctx.parts.get('piece-1').value.position).toEqual([1, 1]); + expect(state.value.parts.length).toBe(1); + expect(state.value.parts[0].value.position).toEqual([1, 1]); + expect(state.value.parts[0].value.player).toBe('X'); }); it('should add piece to board region children', () => { const { ctx } = createTestContext(); - setupBoard(ctx); - placePiece(ctx, 0, 0, 1); + const state = getState(ctx); + placePiece(state, 0, 0, 'O'); - const board = ctx.regions.get('board'); + const board = state.value.board; expect(board.value.children.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); + }); }); }); @@ -178,9 +209,10 @@ describe('TicTacToe - game flow', () => { 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 ctx.prompts.pop(); + const promptEvent = await promptPromise; expect(promptEvent).not.toBeNull(); expect(promptEvent.schema.name).toBe('play'); @@ -192,139 +224,135 @@ describe('TicTacToe - game flow', () => { it('should accept valid move via turn command', async () => { const { ctx } = createTestContext(); - setupBoard(ctx); + const promptPromise = waitForPrompt(ctx); const runPromise = ctx.commands.run('turn X 1'); - const promptEvent = await ctx.prompts.pop(); + const promptEvent = await promptPromise; expect(promptEvent).not.toBeNull(); expect(promptEvent.schema.name).toBe('play'); promptEvent.resolve({ name: 'play', params: ['X', 1, 1], options: {}, flags: {} }); - 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]); + 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].value.position).toEqual([1, 1]); }); it('should reject move for wrong player and re-prompt', async () => { const { ctx } = createTestContext(); - setupBoard(ctx); + const promptPromise = waitForPrompt(ctx); const runPromise = ctx.commands.run('turn X 1'); - const promptEvent1 = await ctx.prompts.pop(); + const promptEvent1 = await promptPromise; promptEvent1.resolve({ name: 'play', params: ['O', 1, 1], options: {}, flags: {} }); - const promptEvent2 = await ctx.prompts.pop(); + const promptEvent2 = await waitForPrompt(ctx); expect(promptEvent2).not.toBeNull(); promptEvent2.resolve({ name: 'play', params: ['X', 1, 1], options: {}, flags: {} }); - const promptEvent3 = await ctx.prompts.pop(); - promptEvent3.reject(new Error('done')); - const result = await runPromise; - expect(result.success).toBe(false); + 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(); - setupBoard(ctx); + const state = getState(ctx); - addPiece(ctx, 'piece-0', 1, 1); + addPiece(state, 'piece-0', 1, 1, 'O'); + const promptPromise = waitForPrompt(ctx); const runPromise = ctx.commands.run('turn X 1'); - const promptEvent1 = await ctx.prompts.pop(); + const promptEvent1 = await promptPromise; promptEvent1.resolve({ name: 'play', params: ['X', 1, 1], options: {}, flags: {} }); - const promptEvent2 = await ctx.prompts.pop(); + const promptEvent2 = await waitForPrompt(ctx); expect(promptEvent2).not.toBeNull(); promptEvent2.resolve({ name: 'play', params: ['X', 0, 0], options: {}, flags: {} }); - const promptEvent3 = await ctx.prompts.pop(); - promptEvent3.reject(new Error('done')); - const result = await runPromise; - expect(result.success).toBe(false); + expect(result.success).toBe(true); + if (result.success) expect(result.result.winner).toBeNull(); }); it('should detect winner after winning move', async () => { const { ctx } = createTestContext(); - setupBoard(ctx); + let promptPromise = waitForPrompt(ctx); let runPromise = ctx.commands.run('turn X 1'); - let prompt = await ctx.prompts.pop(); + let prompt = await promptPromise; 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); + 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 ctx.prompts.pop(); + prompt = await promptPromise; 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); + 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 ctx.prompts.pop(); + prompt = await promptPromise; 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); + 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 ctx.prompts.pop(); + prompt = await promptPromise; 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); + 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 ctx.prompts.pop(); + prompt = await promptPromise; 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'); + if (result.success) expect(result.result.winner).toBe('X'); }); it('should detect draw after 9 moves', async () => { const { ctx } = createTestContext(); - setupBoard(ctx); + const state = getState(ctx); const pieces = [ - { id: 'p1', pos: [0, 0] }, - { id: 'p2', pos: [2, 2] }, - { id: 'p3', pos: [0, 2] }, - { id: 'p4', pos: [2, 0] }, - { id: 'p5', pos: [1, 0] }, - { id: 'p6', pos: [0, 1] }, - { id: 'p7', pos: [2, 1] }, - { id: 'p8', pos: [1, 2] }, + { 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' }, ]; - for (const { id, pos } of pieces) { - addPiece(ctx, id, pos[0], pos[1]); + for (const { id, pos, player } of pieces) { + addPiece(state, id, pos[0], pos[1], player); } - expect(checkWinner(ctx)).toBeNull(); + expect(checkWinner(state)).toBeNull(); + const promptPromise = waitForPrompt(ctx); const runPromise = ctx.commands.run('turn X 9'); - const prompt = await ctx.prompts.pop(); + const prompt = await promptPromise; 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'); + if (result.success) expect(result.result.winner).toBe('draw'); }); });