fix: fix tic tac toe tests
This commit is contained in:
parent
e945d28fc3
commit
b7c5312b60
|
|
@ -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<typeof createTestContext>['ctx']): Entity<TicTacToeState> {
|
||||
return ctx.state;
|
||||
}
|
||||
|
||||
function addPiece(state: Entity<TicTacToeState>, 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<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();
|
||||
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');
|
||||
});
|
||||
});
|
||||
|
|
|
|||
Loading…
Reference in New Issue