boardgame-core/tests/samples/tic-tac-toe.test.ts

362 lines
13 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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<typeof createTestContext>['ctx']): Entity<TicTacToeState> {
return ctx.state;
}
function waitForPrompt(ctx: ReturnType<typeof createTestContext>['ctx']): Promise<PromptEvent> {
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].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();
const state = getState(ctx);
placePiece(state, 0, 0, 'O');
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);
});
});
});
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].value.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;
// 没有验证器tryCommit 返回 null但游戏逻辑会 continue 并重新 prompt
const error1 = promptEvent1.tryCommit({ name: 'play', params: ['O', 1, 1], options: {}, flags: {} });
expect(error1).toBeNull();
const promptEvent2 = await waitForPrompt(ctx);
expect(promptEvent2).not.toBeNull();
const error2 = promptEvent2.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).toBeNull();
const promptEvent2 = await waitForPrompt(ctx);
expect(promptEvent2).not.toBeNull();
const error2 = promptEvent2.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');
});
});