fix: fix tic tac toe tests

This commit is contained in:
hypercross 2026-04-04 18:42:21 +08:00
parent de7006ef19
commit 467a56bd84
3 changed files with 94 additions and 97 deletions

View File

@ -111,3 +111,19 @@ function createEmptyPartPool<TMeta>(): PartPool<TMeta> {
}, },
}; };
} }
export function createPartsFromTable<T>(items: T[], getId: (item: T, index: number) => string, getCount?: ((item: T) => number) | number){
const pool: Record<string, Part<T>> = {};
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;
}

View File

@ -14,7 +14,7 @@ export type { Part } from './core/part';
export { flip, flipTo, roll, findPartById, isCellOccupied, getPartAtPosition, isCellOccupiedByRegion, getPartAtPositionInRegion } from './core/part'; export { flip, flipTo, roll, findPartById, isCellOccupied, getPartAtPosition, isCellOccupiedByRegion, getPartAtPositionInRegion } from './core/part';
export type { PartTemplate, PartPool } from './core/part-factory'; 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 type { Region, RegionAxis } from './core/region';
export { createRegion, applyAlign, shuffle, moveToRegion } from './core/region'; export { createRegion, applyAlign, shuffle, moveToRegion } from './core/region';

View File

@ -6,10 +6,11 @@ import {
placePiece, placePiece,
createInitialState, createInitialState,
TicTacToeState, TicTacToeState,
WinnerType, PlayerType TicTacToeGame,
WinnerType,
PlayerType
} from '@/samples/tic-tac-toe'; } from '@/samples/tic-tac-toe';
import {MutableSignal} from "@/utils/mutable-signal"; import { createGameContext } from '@/index';
import {createGameContext} from "@/";
import type { PromptEvent } from '@/utils/command'; import type { PromptEvent } from '@/utils/command';
function createTestContext() { function createTestContext() {
@ -17,13 +18,9 @@ function createTestContext() {
return { registry, ctx }; return { registry, ctx };
} }
function getState(ctx: ReturnType<typeof createTestContext>['ctx']): MutableSignal<TicTacToeState> {
return ctx.state;
}
function waitForPrompt(ctx: ReturnType<typeof createTestContext>['ctx']): Promise<PromptEvent> { function waitForPrompt(ctx: ReturnType<typeof createTestContext>['ctx']): Promise<PromptEvent> {
return new Promise(resolve => { return new Promise(resolve => {
ctx.commands.on('prompt', resolve); ctx._commands.on('prompt', resolve);
}); });
} }
@ -31,92 +28,84 @@ describe('TicTacToe - helper functions', () => {
describe('checkWinner', () => { describe('checkWinner', () => {
it('should return null for empty board', () => { it('should return null for empty board', () => {
const { ctx } = createTestContext(); const { ctx } = createTestContext();
const state = getState(ctx);
expect(checkWinner(state)).toBeNull(); expect(checkWinner(ctx)).toBeNull();
}); });
it('should detect horizontal win for X', () => { it('should detect horizontal win for X', () => {
const { ctx } = createTestContext(); const { ctx } = createTestContext();
const state = getState(ctx);
placePiece(state, 0, 0, 'X'); placePiece(ctx, 0, 0, 'X');
placePiece(state, 1, 0, 'O'); placePiece(ctx, 1, 0, 'O');
placePiece(state, 0, 1, 'X'); placePiece(ctx, 0, 1, 'X');
placePiece(state, 1, 1, 'O'); placePiece(ctx, 1, 1, 'O');
placePiece(state, 0, 2, 'X'); placePiece(ctx, 0, 2, 'X');
expect(checkWinner(state)).toBe('X'); expect(checkWinner(ctx)).toBe('X');
}); });
it('should detect horizontal win for O', () => { it('should detect horizontal win for O', () => {
const { ctx } = createTestContext(); const { ctx } = createTestContext();
const state = getState(ctx);
placePiece(state, 2, 0, 'X'); placePiece(ctx, 2, 0, 'X');
placePiece(state, 1, 0, 'O'); placePiece(ctx, 1, 0, 'O');
placePiece(state, 2, 1, 'X'); placePiece(ctx, 2, 1, 'X');
placePiece(state, 1, 1, 'O'); placePiece(ctx, 1, 1, 'O');
placePiece(state, 0, 0, 'X'); placePiece(ctx, 0, 0, 'X');
placePiece(state, 1, 2, 'O'); placePiece(ctx, 1, 2, 'O');
expect(checkWinner(state)).toBe('O'); expect(checkWinner(ctx)).toBe('O');
}); });
it('should detect vertical win', () => { it('should detect vertical win', () => {
const { ctx } = createTestContext(); const { ctx } = createTestContext();
const state = getState(ctx);
placePiece(state, 0, 0, 'X'); placePiece(ctx, 0, 0, 'X');
placePiece(state, 0, 1, 'O'); placePiece(ctx, 0, 1, 'O');
placePiece(state, 1, 0, 'X'); placePiece(ctx, 1, 0, 'X');
placePiece(state, 1, 1, 'O'); placePiece(ctx, 1, 1, 'O');
placePiece(state, 2, 0, 'X'); 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)', () => { it('should detect diagonal win (top-left to bottom-right)', () => {
const { ctx } = createTestContext(); const { ctx } = createTestContext();
const state = getState(ctx);
placePiece(state, 0, 0, 'X'); placePiece(ctx, 0, 0, 'X');
placePiece(state, 0, 1, 'O'); placePiece(ctx, 0, 1, 'O');
placePiece(state, 1, 1, 'X'); placePiece(ctx, 1, 1, 'X');
placePiece(state, 0, 2, 'O'); placePiece(ctx, 0, 2, 'O');
placePiece(state, 2, 2, 'X'); 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)', () => { it('should detect diagonal win (top-right to bottom-left)', () => {
const { ctx } = createTestContext(); const { ctx } = createTestContext();
const state = getState(ctx);
placePiece(state, 0, 0, 'X'); placePiece(ctx, 0, 0, 'X');
placePiece(state, 0, 2, 'O'); placePiece(ctx, 0, 2, 'O');
placePiece(state, 1, 0, 'X'); placePiece(ctx, 1, 0, 'X');
placePiece(state, 1, 1, 'O'); placePiece(ctx, 1, 1, 'O');
placePiece(state, 1, 2, 'X'); placePiece(ctx, 1, 2, 'X');
placePiece(state, 2, 0, 'O'); placePiece(ctx, 2, 0, 'O');
expect(checkWinner(state)).toBe('O'); expect(checkWinner(ctx)).toBe('O');
}); });
it('should return null for no winner', () => { it('should return null for no winner', () => {
const { ctx } = createTestContext(); const { ctx } = createTestContext();
const state = getState(ctx);
placePiece(state, 0, 0, 'X'); placePiece(ctx, 0, 0, 'X');
placePiece(state, 0, 1, 'O'); placePiece(ctx, 0, 1, 'O');
placePiece(state, 1, 2, 'X'); placePiece(ctx, 1, 2, 'X');
expect(checkWinner(state)).toBeNull(); expect(checkWinner(ctx)).toBeNull();
}); });
it('should return draw when board is full with no winner', () => { it('should return draw when board is full with no winner', () => {
const { ctx } = createTestContext(); const { ctx } = createTestContext();
const state = getState(ctx);
const drawPositions = [ const drawPositions = [
[0, 0, 'X'], [0, 1, 'O'], [0, 2, 'X'], [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'], [2, 0, 'O'], [2, 1, 'X'], [2, 2, 'X'],
] as [number, number, PlayerType][]; ] as [number, number, PlayerType][];
drawPositions.forEach(([r, c, p], i) => { drawPositions.forEach(([r, c, p]) => {
placePiece(state, r, c, p); placePiece(ctx, r, c, p);
}); });
expect(checkWinner(state)).toBe('draw'); expect(checkWinner(ctx)).toBe('draw');
}); });
}); });
describe('isCellOccupied', () => { describe('isCellOccupied', () => {
it('should return false for empty cell', () => { it('should return false for empty cell', () => {
const { ctx } = createTestContext(); 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', () => { it('should return true for occupied cell', () => {
const { ctx } = createTestContext(); const { ctx } = createTestContext();
const state = getState(ctx); placePiece(ctx, 1, 1, 'X');
placePiece(state, 1, 1, 'X');
expect(isCellOccupied(state, 1, 1)).toBe(true); expect(isCellOccupied(ctx, 1, 1)).toBe(true);
}); });
it('should return false for different cell', () => { it('should return false for different cell', () => {
const { ctx } = createTestContext(); const { ctx } = createTestContext();
const state = getState(ctx); placePiece(ctx, 0, 0, 'X');
placePiece(state, 0, 0, 'X');
expect(isCellOccupied(state, 1, 1)).toBe(false); expect(isCellOccupied(ctx, 1, 1)).toBe(false);
}); });
}); });
describe('placePiece', () => { describe('placePiece', () => {
it('should add a piece to the board', () => { it('should add a piece to the board', () => {
const { ctx } = createTestContext(); const { ctx } = createTestContext();
const state = getState(ctx); placePiece(ctx, 1, 1, 'X');
placePiece(state, 1, 1, 'X');
expect(Object.keys(state.value.parts).length).toBe(1); expect(Object.keys(ctx.value.parts).length).toBe(1);
expect(state.value.parts['piece-X-1']!.position).toEqual([1, 1]); expect(ctx.value.parts['piece-X-1']!.position).toEqual([1, 1]);
expect(state.value.parts['piece-X-1']!.player).toBe('X'); expect(ctx.value.parts['piece-X-1']!.player).toBe('X');
}); });
it('should add piece to board region children', () => { it('should add piece to board region children', () => {
const { ctx } = createTestContext(); const { ctx } = createTestContext();
const state = getState(ctx); placePiece(ctx, 0, 0, 'O');
placePiece(state, 0, 0, 'O');
const board = state.value.board; const board = ctx.value.board;
expect(board.childIds.length).toBe(1); expect(board.childIds.length).toBe(1);
}); });
it('should generate unique IDs for pieces', () => { it('should generate unique IDs for pieces', () => {
const { ctx } = createTestContext(); const { ctx } = createTestContext();
const state = getState(ctx); placePiece(ctx, 0, 0, 'X');
placePiece(state, 0, 0, 'X'); placePiece(ctx, 0, 1, 'O');
placePiece(state, 0, 1, 'O');
const ids = Object.keys(state.value.parts); const ids = Object.keys(ctx.value.parts);
expect(new Set(ids).size).toBe(2); expect(new Set(ids).size).toBe(2);
}); });
}); });
@ -201,7 +184,7 @@ describe('TicTacToe - game flow', () => {
const { ctx } = createTestContext(); const { ctx } = createTestContext();
const promptPromise = waitForPrompt(ctx); const promptPromise = waitForPrompt(ctx);
const runPromise = ctx.commands.run('setup'); const runPromise = ctx.run('setup');
const promptEvent = await promptPromise; const promptEvent = await promptPromise;
expect(promptEvent).not.toBeNull(); expect(promptEvent).not.toBeNull();
@ -217,7 +200,7 @@ describe('TicTacToe - game flow', () => {
const { ctx } = createTestContext(); const { ctx } = createTestContext();
const promptPromise = waitForPrompt(ctx); 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; const promptEvent = await promptPromise;
expect(promptEvent).not.toBeNull(); expect(promptEvent).not.toBeNull();
@ -229,22 +212,22 @@ describe('TicTacToe - game flow', () => {
const result = await runPromise; const result = await runPromise;
expect(result.success).toBe(true); expect(result.success).toBe(true);
if (result.success) expect(result.result.winner).toBeNull(); if (result.success) expect(result.result.winner).toBeNull();
expect(Object.keys(ctx.state.value.parts).length).toBe(1); expect(Object.keys(ctx.value.parts).length).toBe(1);
expect(ctx.state.value.parts['piece-X-1']!.position).toEqual([1, 1]); expect(ctx.value.parts['piece-X-1']!.position).toEqual([1, 1]);
}); });
it('should reject move for wrong player and re-prompt', async () => { it('should reject move for wrong player and re-prompt', async () => {
const { ctx } = createTestContext(); const { ctx } = createTestContext();
const promptPromise = waitForPrompt(ctx); 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 promptEvent1 = await promptPromise;
// 验证器会拒绝错误的玩家 // 验证器会拒绝错误的玩家
const error1 = promptEvent1.tryCommit({ name: 'play', params: ['O', 1, 1], options: {}, flags: {} }); const error1 = promptEvent1.tryCommit({ name: 'play', params: ['O', 1, 1], options: {}, flags: {} });
expect(error1).toContain('Invalid player'); expect(error1).toContain('Invalid player');
// 验证失败后再次尝试有效输入 // 验证失败后,再次尝试有效输入
const error2 = promptEvent1.tryCommit({ name: 'play', params: ['X', 1, 1], options: {}, flags: {} }); const error2 = promptEvent1.tryCommit({ name: 'play', params: ['X', 1, 1], options: {}, flags: {} });
expect(error2).toBeNull(); expect(error2).toBeNull();
@ -255,18 +238,17 @@ describe('TicTacToe - game flow', () => {
it('should reject move to occupied cell and re-prompt', async () => { it('should reject move to occupied cell and re-prompt', async () => {
const { ctx } = createTestContext(); const { ctx } = createTestContext();
const state = getState(ctx);
placePiece(state, 1, 1, 'O'); placePiece(ctx, 1, 1, 'O');
const promptPromise = waitForPrompt(ctx); 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 promptEvent1 = await promptPromise;
const error1 = promptEvent1.tryCommit({ name: 'play', params: ['X', 1, 1], options: {}, flags: {} }); const error1 = promptEvent1.tryCommit({ name: 'play', params: ['X', 1, 1], options: {}, flags: {} });
expect(error1).toContain('occupied'); expect(error1).toContain('occupied');
// 验证失败后再次尝试有效输入 // 验证失败后,再次尝试有效输入
const error2 = promptEvent1.tryCommit({ name: 'play', params: ['X', 0, 0], options: {}, flags: {} }); const error2 = promptEvent1.tryCommit({ name: 'play', params: ['X', 0, 0], options: {}, flags: {} });
expect(error2).toBeNull(); expect(error2).toBeNull();
@ -279,7 +261,7 @@ describe('TicTacToe - game flow', () => {
const { ctx } = createTestContext(); const { ctx } = createTestContext();
let promptPromise = waitForPrompt(ctx); 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; let prompt = await promptPromise;
const error1 = prompt.tryCommit({ name: 'play', params: ['X', 0, 0], options: {}, flags: {} }); const error1 = prompt.tryCommit({ name: 'play', params: ['X', 0, 0], options: {}, flags: {} });
expect(error1).toBeNull(); expect(error1).toBeNull();
@ -288,7 +270,7 @@ describe('TicTacToe - game flow', () => {
if (result.success) expect(result.result.winner).toBeNull(); if (result.success) expect(result.result.winner).toBeNull();
promptPromise = waitForPrompt(ctx); promptPromise = waitForPrompt(ctx);
runPromise = ctx.commands.run('turn O 2'); runPromise = ctx.run('turn O 2');
prompt = await promptPromise; prompt = await promptPromise;
const error2 = prompt.tryCommit({ name: 'play', params: ['O', 0, 1], options: {}, flags: {} }); const error2 = prompt.tryCommit({ name: 'play', params: ['O', 0, 1], options: {}, flags: {} });
expect(error2).toBeNull(); expect(error2).toBeNull();
@ -297,7 +279,7 @@ describe('TicTacToe - game flow', () => {
if (result.success) expect(result.result.winner).toBeNull(); if (result.success) expect(result.result.winner).toBeNull();
promptPromise = waitForPrompt(ctx); promptPromise = waitForPrompt(ctx);
runPromise = ctx.commands.run('turn X 3'); runPromise = ctx.run('turn X 3');
prompt = await promptPromise; prompt = await promptPromise;
const error3 = prompt.tryCommit({ name: 'play', params: ['X', 1, 0], options: {}, flags: {} }); const error3 = prompt.tryCommit({ name: 'play', params: ['X', 1, 0], options: {}, flags: {} });
expect(error3).toBeNull(); expect(error3).toBeNull();
@ -306,7 +288,7 @@ describe('TicTacToe - game flow', () => {
if (result.success) expect(result.result.winner).toBeNull(); if (result.success) expect(result.result.winner).toBeNull();
promptPromise = waitForPrompt(ctx); promptPromise = waitForPrompt(ctx);
runPromise = ctx.commands.run('turn O 4'); runPromise = ctx.run('turn O 4');
prompt = await promptPromise; prompt = await promptPromise;
const error4 = prompt.tryCommit({ name: 'play', params: ['O', 0, 2], options: {}, flags: {} }); const error4 = prompt.tryCommit({ name: 'play', params: ['O', 0, 2], options: {}, flags: {} });
expect(error4).toBeNull(); expect(error4).toBeNull();
@ -315,7 +297,7 @@ describe('TicTacToe - game flow', () => {
if (result.success) expect(result.result.winner).toBeNull(); if (result.success) expect(result.result.winner).toBeNull();
promptPromise = waitForPrompt(ctx); promptPromise = waitForPrompt(ctx);
runPromise = ctx.commands.run('turn X 5'); runPromise = ctx.run('turn X 5');
prompt = await promptPromise; prompt = await promptPromise;
const error5 = prompt.tryCommit({ name: 'play', params: ['X', 2, 0], options: {}, flags: {} }); const error5 = prompt.tryCommit({ name: 'play', params: ['X', 2, 0], options: {}, flags: {} });
expect(error5).toBeNull(); expect(error5).toBeNull();
@ -326,7 +308,6 @@ describe('TicTacToe - game flow', () => {
it('should detect draw after 9 moves', async () => { it('should detect draw after 9 moves', async () => {
const { ctx } = createTestContext(); const { ctx } = createTestContext();
const state = getState(ctx);
const pieces = [ const pieces = [
{ id: 'p1', pos: [0, 0], player: 'X' }, { id: 'p1', pos: [0, 0], player: 'X' },
@ -339,14 +320,14 @@ describe('TicTacToe - game flow', () => {
{ id: 'p8', pos: [1, 2], player: 'O' }, { id: 'p8', pos: [1, 2], player: 'O' },
] as { id: string, pos: [number, number], player: PlayerType}[]; ] as { id: string, pos: [number, number], player: PlayerType}[];
for (const { id, pos, player } of pieces) { for (const { pos, player } of pieces) {
placePiece(state, pos[0], pos[1], player); placePiece(ctx, pos[0], pos[1], player);
} }
expect(checkWinner(state)).toBeNull(); expect(checkWinner(ctx)).toBeNull();
const promptPromise = waitForPrompt(ctx); 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 prompt = await promptPromise;
const error = prompt.tryCommit({ name: 'play', params: ['X', 1, 1], options: {}, flags: {} }); const error = prompt.tryCommit({ name: 'play', params: ['X', 1, 1], options: {}, flags: {} });
expect(error).toBeNull(); expect(error).toBeNull();