From 467a56bd84cf0345ccccf529c387f443112ba844 Mon Sep 17 00:00:00 2001 From: hypercross Date: Sat, 4 Apr 2026 18:42:21 +0800 Subject: [PATCH] fix: fix tic tac toe tests --- src/core/part-factory.ts | 16 +++ src/index.ts | 2 +- tests/samples/tic-tac-toe.test.ts | 173 +++++++++++++----------------- 3 files changed, 94 insertions(+), 97 deletions(-) diff --git a/src/core/part-factory.ts b/src/core/part-factory.ts index 8fa0c2c..e78111e 100644 --- a/src/core/part-factory.ts +++ b/src/core/part-factory.ts @@ -111,3 +111,19 @@ function createEmptyPartPool(): PartPool { }, }; } +export function createPartsFromTable(items: T[], getId: (item: T, index: number) => string, getCount?: ((item: T) => number) | number){ + const pool: Record> = {}; + for (const entry of items) { + const count = getCount ? (typeof getCount === 'function' ? getCount(entry) : getCount) : 1; + for (let i = 0; i < count; i++) { + const id = getId(entry, i); + pool[id] = { + id, + regionId: '', + position: [], + ...entry + }; + } + } + return pool; +} diff --git a/src/index.ts b/src/index.ts index d8f4dd4..1c6b273 100644 --- a/src/index.ts +++ b/src/index.ts @@ -14,7 +14,7 @@ export type { Part } from './core/part'; export { flip, flipTo, roll, findPartById, isCellOccupied, getPartAtPosition, isCellOccupiedByRegion, getPartAtPositionInRegion } from './core/part'; export type { PartTemplate, PartPool } from './core/part-factory'; -export { createPart, createParts, createPartPool, mergePartPools } from './core/part-factory'; +export { createPart, createParts, createPartPool, mergePartPools, createPartsFromTable } from './core/part-factory'; export type { Region, RegionAxis } from './core/region'; export { createRegion, applyAlign, shuffle, moveToRegion } from './core/region'; diff --git a/tests/samples/tic-tac-toe.test.ts b/tests/samples/tic-tac-toe.test.ts index 67e3805..46892e0 100644 --- a/tests/samples/tic-tac-toe.test.ts +++ b/tests/samples/tic-tac-toe.test.ts @@ -6,10 +6,11 @@ import { placePiece, createInitialState, TicTacToeState, - WinnerType, PlayerType + TicTacToeGame, + WinnerType, + PlayerType } from '@/samples/tic-tac-toe'; -import {MutableSignal} from "@/utils/mutable-signal"; -import {createGameContext} from "@/"; +import { createGameContext } from '@/index'; import type { PromptEvent } from '@/utils/command'; function createTestContext() { @@ -17,13 +18,9 @@ function createTestContext() { return { registry, ctx }; } -function getState(ctx: ReturnType['ctx']): MutableSignal { - return ctx.state; -} - function waitForPrompt(ctx: ReturnType['ctx']): Promise { return new Promise(resolve => { - ctx.commands.on('prompt', resolve); + ctx._commands.on('prompt', resolve); }); } @@ -31,92 +28,84 @@ describe('TicTacToe - helper functions', () => { describe('checkWinner', () => { it('should return null for empty board', () => { const { ctx } = createTestContext(); - const state = getState(ctx); - expect(checkWinner(state)).toBeNull(); + expect(checkWinner(ctx)).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'); + placePiece(ctx, 0, 0, 'X'); + placePiece(ctx, 1, 0, 'O'); + placePiece(ctx, 0, 1, 'X'); + placePiece(ctx, 1, 1, 'O'); + placePiece(ctx, 0, 2, 'X'); - expect(checkWinner(state)).toBe('X'); + expect(checkWinner(ctx)).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'); + placePiece(ctx, 2, 0, 'X'); + placePiece(ctx, 1, 0, 'O'); + placePiece(ctx, 2, 1, 'X'); + placePiece(ctx, 1, 1, 'O'); + placePiece(ctx, 0, 0, 'X'); + placePiece(ctx, 1, 2, 'O'); - expect(checkWinner(state)).toBe('O'); + expect(checkWinner(ctx)).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'); + placePiece(ctx, 0, 0, 'X'); + placePiece(ctx, 0, 1, 'O'); + placePiece(ctx, 1, 0, 'X'); + placePiece(ctx, 1, 1, 'O'); + placePiece(ctx, 2, 0, 'X'); - expect(checkWinner(state)).toBe('X'); + expect(checkWinner(ctx)).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'); + placePiece(ctx, 0, 0, 'X'); + placePiece(ctx, 0, 1, 'O'); + placePiece(ctx, 1, 1, 'X'); + placePiece(ctx, 0, 2, 'O'); + placePiece(ctx, 2, 2, 'X'); - expect(checkWinner(state)).toBe('X'); + expect(checkWinner(ctx)).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'); + placePiece(ctx, 0, 0, 'X'); + placePiece(ctx, 0, 2, 'O'); + placePiece(ctx, 1, 0, 'X'); + placePiece(ctx, 1, 1, 'O'); + placePiece(ctx, 1, 2, 'X'); + placePiece(ctx, 2, 0, 'O'); - expect(checkWinner(state)).toBe('O'); + expect(checkWinner(ctx)).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'); + placePiece(ctx, 0, 0, 'X'); + placePiece(ctx, 0, 1, 'O'); + placePiece(ctx, 1, 2, 'X'); - expect(checkWinner(state)).toBeNull(); + expect(checkWinner(ctx)).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'], @@ -124,66 +113,60 @@ describe('TicTacToe - helper functions', () => { [2, 0, 'O'], [2, 1, 'X'], [2, 2, 'X'], ] as [number, number, PlayerType][]; - drawPositions.forEach(([r, c, p], i) => { - placePiece(state, r, c, p); + drawPositions.forEach(([r, c, p]) => { + placePiece(ctx, r, c, p); }); - expect(checkWinner(state)).toBe('draw'); + expect(checkWinner(ctx)).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); + expect(isCellOccupied(ctx, 1, 1)).toBe(false); }); it('should return true for occupied cell', () => { const { ctx } = createTestContext(); - const state = getState(ctx); - placePiece(state, 1, 1, 'X'); + placePiece(ctx, 1, 1, 'X'); - expect(isCellOccupied(state, 1, 1)).toBe(true); + expect(isCellOccupied(ctx, 1, 1)).toBe(true); }); it('should return false for different cell', () => { const { ctx } = createTestContext(); - const state = getState(ctx); - placePiece(state, 0, 0, 'X'); + placePiece(ctx, 0, 0, 'X'); - expect(isCellOccupied(state, 1, 1)).toBe(false); + expect(isCellOccupied(ctx, 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'); + placePiece(ctx, 1, 1, 'X'); - expect(Object.keys(state.value.parts).length).toBe(1); - expect(state.value.parts['piece-X-1']!.position).toEqual([1, 1]); - expect(state.value.parts['piece-X-1']!.player).toBe('X'); + expect(Object.keys(ctx.value.parts).length).toBe(1); + expect(ctx.value.parts['piece-X-1']!.position).toEqual([1, 1]); + expect(ctx.value.parts['piece-X-1']!.player).toBe('X'); }); it('should add piece to board region children', () => { const { ctx } = createTestContext(); - const state = getState(ctx); - placePiece(state, 0, 0, 'O'); + placePiece(ctx, 0, 0, 'O'); - const board = state.value.board; + const board = ctx.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'); + placePiece(ctx, 0, 0, 'X'); + placePiece(ctx, 0, 1, 'O'); - const ids = Object.keys(state.value.parts); + const ids = Object.keys(ctx.value.parts); expect(new Set(ids).size).toBe(2); }); }); @@ -201,7 +184,7 @@ describe('TicTacToe - game flow', () => { const { ctx } = createTestContext(); const promptPromise = waitForPrompt(ctx); - const runPromise = ctx.commands.run('setup'); + const runPromise = ctx.run('setup'); const promptEvent = await promptPromise; expect(promptEvent).not.toBeNull(); @@ -217,7 +200,7 @@ describe('TicTacToe - game flow', () => { const { ctx } = createTestContext(); const promptPromise = waitForPrompt(ctx); - const runPromise = ctx.commands.run<{winner: WinnerType}>('turn X 1'); + const runPromise = ctx.run<{winner: WinnerType}>('turn X 1'); const promptEvent = await promptPromise; expect(promptEvent).not.toBeNull(); @@ -229,22 +212,22 @@ describe('TicTacToe - game flow', () => { const result = await runPromise; expect(result.success).toBe(true); if (result.success) expect(result.result.winner).toBeNull(); - expect(Object.keys(ctx.state.value.parts).length).toBe(1); - expect(ctx.state.value.parts['piece-X-1']!.position).toEqual([1, 1]); + expect(Object.keys(ctx.value.parts).length).toBe(1); + expect(ctx.value.parts['piece-X-1']!.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 runPromise = ctx.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(); @@ -255,18 +238,17 @@ describe('TicTacToe - game flow', () => { it('should reject move to occupied cell and re-prompt', async () => { const { ctx } = createTestContext(); - const state = getState(ctx); - placePiece(state, 1, 1, 'O'); + placePiece(ctx, 1, 1, 'O'); const promptPromise = waitForPrompt(ctx); - const runPromise = ctx.commands.run<{winner: WinnerType}>('turn X 1'); + const runPromise = ctx.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(); @@ -279,7 +261,7 @@ describe('TicTacToe - game flow', () => { const { ctx } = createTestContext(); let promptPromise = waitForPrompt(ctx); - let runPromise = ctx.commands.run<{winner: WinnerType}>('turn X 1'); + let runPromise = ctx.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(); @@ -288,7 +270,7 @@ describe('TicTacToe - game flow', () => { if (result.success) expect(result.result.winner).toBeNull(); promptPromise = waitForPrompt(ctx); - runPromise = ctx.commands.run('turn O 2'); + runPromise = ctx.run('turn O 2'); prompt = await promptPromise; const error2 = prompt.tryCommit({ name: 'play', params: ['O', 0, 1], options: {}, flags: {} }); expect(error2).toBeNull(); @@ -297,7 +279,7 @@ describe('TicTacToe - game flow', () => { if (result.success) expect(result.result.winner).toBeNull(); promptPromise = waitForPrompt(ctx); - runPromise = ctx.commands.run('turn X 3'); + runPromise = ctx.run('turn X 3'); prompt = await promptPromise; const error3 = prompt.tryCommit({ name: 'play', params: ['X', 1, 0], options: {}, flags: {} }); expect(error3).toBeNull(); @@ -306,7 +288,7 @@ describe('TicTacToe - game flow', () => { if (result.success) expect(result.result.winner).toBeNull(); promptPromise = waitForPrompt(ctx); - runPromise = ctx.commands.run('turn O 4'); + runPromise = ctx.run('turn O 4'); prompt = await promptPromise; const error4 = prompt.tryCommit({ name: 'play', params: ['O', 0, 2], options: {}, flags: {} }); expect(error4).toBeNull(); @@ -315,7 +297,7 @@ describe('TicTacToe - game flow', () => { if (result.success) expect(result.result.winner).toBeNull(); promptPromise = waitForPrompt(ctx); - runPromise = ctx.commands.run('turn X 5'); + runPromise = ctx.run('turn X 5'); prompt = await promptPromise; const error5 = prompt.tryCommit({ name: 'play', params: ['X', 2, 0], options: {}, flags: {} }); expect(error5).toBeNull(); @@ -326,7 +308,6 @@ describe('TicTacToe - game flow', () => { it('should detect draw after 9 moves', async () => { const { ctx } = createTestContext(); - const state = getState(ctx); const pieces = [ { id: 'p1', pos: [0, 0], player: 'X' }, @@ -339,14 +320,14 @@ describe('TicTacToe - game flow', () => { { 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); + for (const { pos, player } of pieces) { + placePiece(ctx, pos[0], pos[1], player); } - expect(checkWinner(state)).toBeNull(); + expect(checkWinner(ctx)).toBeNull(); const promptPromise = waitForPrompt(ctx); - const runPromise = ctx.commands.run<{winner: WinnerType}>('turn X 9'); + const runPromise = ctx.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();