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