boardgame-core/tests/samples/boop.test.ts

649 lines
23 KiB
TypeScript

import { describe, it, expect } from 'vitest';
import {
registry,
checkWinner,
isCellOccupied,
getPartAt,
placePiece,
applyBoops,
checkGraduation,
processGraduation,
hasWinningLine,
removePieceFromBoard,
createInitialState,
BoopState,
WinnerType,
PlayerType,
getBoardRegion,
} from '@/samples/boop';
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<BoopState> {
return ctx.state;
}
function waitForPrompt(ctx: ReturnType<typeof createTestContext>['ctx']): Promise<PromptEvent> {
return new Promise(resolve => {
ctx.commands.on('prompt', resolve);
});
}
function getParts(state: Entity<BoopState>) {
return state.value.board.value.children;
}
describe('Boop - helper functions', () => {
describe('isCellOccupied', () => {
it('should return false for empty cell', () => {
const { ctx } = createTestContext();
const state = getState(ctx);
expect(isCellOccupied(state, 3, 3)).toBe(false);
});
it('should return true for occupied cell', () => {
const { ctx } = createTestContext();
const state = getState(ctx);
placePiece(state, 3, 3, 'white', 'kitten');
expect(isCellOccupied(state, 3, 3)).toBe(true);
});
it('should return false for different cell', () => {
const { ctx } = createTestContext();
const state = getState(ctx);
placePiece(state, 0, 0, 'white', 'kitten');
expect(isCellOccupied(state, 1, 1)).toBe(false);
});
});
describe('getPartAt', () => {
it('should return null for empty cell', () => {
const { ctx } = createTestContext();
const state = getState(ctx);
expect(getPartAt(state, 2, 2)).toBeNull();
});
it('should return the part at occupied cell', () => {
const { ctx } = createTestContext();
const state = getState(ctx);
placePiece(state, 2, 2, 'black', 'kitten');
const part = getPartAt(state, 2, 2);
expect(part).not.toBeNull();
if (part) {
expect(part.value.player).toBe('black');
expect(part.value.pieceType).toBe('kitten');
}
});
});
describe('placePiece', () => {
it('should add a kitten to the board', () => {
const { ctx } = createTestContext();
const state = getState(ctx);
placePiece(state, 2, 3, 'white', 'kitten');
const parts = getParts(state);
expect(parts.length).toBe(1);
expect(parts[0].value.position).toEqual([2, 3]);
expect(parts[0].value.player).toBe('white');
expect(parts[0].value.pieceType).toBe('kitten');
});
it('should name piece white-kitten-1', () => {
const { ctx } = createTestContext();
const state = getState(ctx);
placePiece(state, 0, 0, 'white', 'kitten');
expect(getParts(state)[0].id).toBe('white-kitten-1');
});
it('should name piece white-kitten-2 for second white kitten', () => {
const { ctx } = createTestContext();
const state = getState(ctx);
placePiece(state, 0, 0, 'white', 'kitten');
placePiece(state, 0, 1, 'white', 'kitten');
expect(getParts(state)[1].id).toBe('white-kitten-2');
});
it('should name piece white-cat-1', () => {
const { ctx } = createTestContext();
const state = getState(ctx);
placePiece(state, 0, 0, 'white', 'cat');
expect(getParts(state)[0].id).toBe('white-cat-1');
});
it('should decrement the correct player kitten supply', () => {
const { ctx } = createTestContext();
const state = getState(ctx);
placePiece(state, 0, 0, 'white', 'kitten');
expect(state.value.players.white.kitten.supply).toBe(7);
expect(state.value.players.black.kitten.supply).toBe(8);
placePiece(state, 0, 1, 'black', 'kitten');
expect(state.value.players.white.kitten.supply).toBe(7);
expect(state.value.players.black.kitten.supply).toBe(7);
});
it('should decrement the correct player cat supply', () => {
const { ctx } = createTestContext();
const state = getState(ctx);
state.produce(s => {
s.players.white.cat.supply = 3;
});
placePiece(state, 0, 0, 'white', 'cat');
expect(state.value.players.white.cat.supply).toBe(2);
});
it('should add piece to board region children', () => {
const { ctx } = createTestContext();
const state = getState(ctx);
placePiece(state, 1, 1, 'white', 'kitten');
const board = getBoardRegion(state);
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, 'white', 'kitten');
placePiece(state, 0, 1, 'black', 'kitten');
const ids = getParts(state).map(p => p.id);
expect(new Set(ids).size).toBe(2);
});
});
describe('applyBoops', () => {
it('should boop adjacent kitten away from placed kitten', () => {
const { ctx } = createTestContext();
const state = getState(ctx);
placePiece(state, 3, 3, 'black', 'kitten');
placePiece(state, 2, 2, 'white', 'kitten');
const whitePart = getParts(state)[1];
expect(whitePart.value.position).toEqual([2, 2]);
applyBoops(state, 3, 3, 'kitten');
expect(whitePart.value.position).toEqual([1, 1]);
});
it('should not boop a cat when a kitten is placed', () => {
const { ctx } = createTestContext();
const state = getState(ctx);
placePiece(state, 3, 3, 'black', 'kitten');
const whitePart = getParts(state)[0];
whitePart.produce(p => {
p.pieceType = 'cat';
});
applyBoops(state, 3, 3, 'kitten');
expect(whitePart.value.position).toEqual([3, 3]);
});
it('should remove piece that is booped off the board', () => {
const { ctx } = createTestContext();
const state = getState(ctx);
placePiece(state, 0, 0, 'white', 'kitten');
placePiece(state, 1, 1, 'black', 'kitten');
applyBoops(state, 1, 1, 'kitten');
expect(getParts(state).length).toBe(1);
expect(getParts(state)[0].value.player).toBe('black');
expect(state.value.players.white.kitten.supply).toBe(8);
});
it('should not boop piece if target cell is occupied', () => {
const { ctx } = createTestContext();
const state = getState(ctx);
placePiece(state, 1, 1, 'white', 'kitten');
placePiece(state, 2, 1, 'black', 'kitten');
placePiece(state, 0, 1, 'black', 'kitten');
applyBoops(state, 0, 1, 'kitten');
const whitePart = getParts(state).find(p => p.value.player === 'white');
expect(whitePart).toBeDefined();
if (whitePart) {
expect(whitePart.value.position).toEqual([1, 1]);
}
});
it('should boop multiple adjacent pieces', () => {
const { ctx } = createTestContext();
const state = getState(ctx);
placePiece(state, 3, 3, 'white', 'kitten');
placePiece(state, 2, 2, 'black', 'kitten');
placePiece(state, 2, 3, 'black', 'kitten');
applyBoops(state, 3, 3, 'kitten');
expect(getParts(state)[1].value.position).toEqual([1, 1]);
expect(getParts(state)[2].value.position).toEqual([1, 3]);
});
it('should not boop the placed piece itself', () => {
const { ctx } = createTestContext();
const state = getState(ctx);
placePiece(state, 3, 3, 'white', 'kitten');
applyBoops(state, 3, 3, 'kitten');
expect(getParts(state)[0].value.position).toEqual([3, 3]);
});
});
describe('removePieceFromBoard', () => {
it('should remove piece from board children', () => {
const { ctx } = createTestContext();
const state = getState(ctx);
placePiece(state, 2, 2, 'white', 'kitten');
const part = getParts(state)[0];
removePieceFromBoard(state, part);
const board = getBoardRegion(state);
expect(board.value.children.length).toBe(0);
});
});
describe('checkGraduation', () => {
it('should return empty array when no kittens in a row', () => {
const { ctx } = createTestContext();
const state = getState(ctx);
placePiece(state, 0, 0, 'white', 'kitten');
placePiece(state, 2, 2, 'white', 'kitten');
const lines = checkGraduation(state, 'white');
expect(lines.length).toBe(0);
});
it('should detect horizontal line of 3 kittens', () => {
const { ctx } = createTestContext();
const state = getState(ctx);
placePiece(state, 1, 0, 'white', 'kitten');
placePiece(state, 1, 1, 'white', 'kitten');
placePiece(state, 1, 2, 'white', 'kitten');
const lines = checkGraduation(state, 'white');
expect(lines.length).toBe(1);
expect(lines[0]).toEqual([[1, 0], [1, 1], [1, 2]]);
});
it('should detect vertical line of 3 kittens', () => {
const { ctx } = createTestContext();
const state = getState(ctx);
placePiece(state, 0, 2, 'white', 'kitten');
placePiece(state, 1, 2, 'white', 'kitten');
placePiece(state, 2, 2, 'white', 'kitten');
const lines = checkGraduation(state, 'white');
expect(lines.length).toBe(1);
expect(lines[0]).toEqual([[0, 2], [1, 2], [2, 2]]);
});
it('should detect diagonal line of 3 kittens', () => {
const { ctx } = createTestContext();
const state = getState(ctx);
placePiece(state, 0, 0, 'white', 'kitten');
placePiece(state, 1, 1, 'white', 'kitten');
placePiece(state, 2, 2, 'white', 'kitten');
const lines = checkGraduation(state, 'white');
expect(lines.length).toBe(1);
expect(lines[0]).toEqual([[0, 0], [1, 1], [2, 2]]);
});
it('should detect anti-diagonal line of 3 kittens', () => {
const { ctx } = createTestContext();
const state = getState(ctx);
placePiece(state, 2, 0, 'white', 'kitten');
placePiece(state, 1, 1, 'white', 'kitten');
placePiece(state, 0, 2, 'white', 'kitten');
const lines = checkGraduation(state, 'white');
expect(lines.length).toBe(1);
expect(lines[0]).toEqual([[0, 2], [1, 1], [2, 0]]);
});
it('should not detect line with mixed piece types', () => {
const { ctx } = createTestContext();
const state = getState(ctx);
placePiece(state, 0, 0, 'white', 'kitten');
placePiece(state, 0, 1, 'white', 'kitten');
placePiece(state, 0, 2, 'white', 'kitten');
getParts(state)[1].produce(p => {
p.pieceType = 'cat';
});
const lines = checkGraduation(state, 'white');
expect(lines.length).toBe(0);
});
});
describe('processGraduation', () => {
it('should convert kittens to cats and update supply', () => {
const { ctx } = createTestContext();
const state = getState(ctx);
placePiece(state, 0, 0, 'white', 'kitten');
placePiece(state, 0, 1, 'white', 'kitten');
placePiece(state, 0, 2, 'white', 'kitten');
const lines = checkGraduation(state, 'white');
expect(lines.length).toBe(1);
processGraduation(state, 'white', lines);
expect(getParts(state).length).toBe(0);
expect(state.value.players.white.cat.supply).toBe(3);
});
it('should only graduate pieces on the winning lines', () => {
const { ctx } = createTestContext();
const state = getState(ctx);
placePiece(state, 0, 0, 'white', 'kitten');
placePiece(state, 0, 1, 'white', 'kitten');
placePiece(state, 0, 2, 'white', 'kitten');
placePiece(state, 3, 3, 'white', 'kitten');
const lines = checkGraduation(state, 'white');
processGraduation(state, 'white', lines);
expect(getParts(state).length).toBe(1);
expect(getParts(state)[0].value.position).toEqual([3, 3]);
expect(state.value.players.white.cat.supply).toBe(3);
});
});
describe('hasWinningLine', () => {
it('should return false for no line', () => {
expect(hasWinningLine([[0, 0], [1, 1], [3, 3]])).toBe(false);
});
it('should return true for horizontal line', () => {
expect(hasWinningLine([[0, 0], [0, 1], [0, 2]])).toBe(true);
});
it('should return true for vertical line', () => {
expect(hasWinningLine([[0, 0], [1, 0], [2, 0]])).toBe(true);
});
it('should return true for diagonal line', () => {
expect(hasWinningLine([[0, 0], [1, 1], [2, 2]])).toBe(true);
});
it('should return true for anti-diagonal line', () => {
expect(hasWinningLine([[2, 0], [1, 1], [0, 2]])).toBe(true);
});
});
describe('checkWinner', () => {
it('should return null for empty board', () => {
const { ctx } = createTestContext();
const state = getState(ctx);
expect(checkWinner(state)).toBeNull();
});
it('should return winner when player has 3 cats in a row', () => {
const { ctx } = createTestContext();
const state = getState(ctx);
placePiece(state, 0, 0, 'white', 'cat');
placePiece(state, 0, 1, 'white', 'cat');
placePiece(state, 0, 2, 'white', 'cat');
expect(checkWinner(state)).toBe('white');
});
it('should return draw when both players use all pieces', () => {
const { ctx } = createTestContext();
const state = getState(ctx);
for (let i = 0; i < 8; i++) {
placePiece(state, i % 6, Math.floor(i / 6) + (i % 2), 'white', 'kitten');
}
for (let i = 0; i < 8; i++) {
placePiece(state, i % 6, Math.floor(i / 6) + 3 + (i % 2), 'black', 'kitten');
}
const result = checkWinner(state);
expect(result === 'draw' || result === null).toBe(true);
});
});
});
describe('Boop - 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 white');
const promptEvent = await promptPromise;
expect(promptEvent).not.toBeNull();
expect(promptEvent.schema.name).toBe('play');
const error = promptEvent.tryCommit({ name: 'play', params: ['white', 2, 2], options: {}, flags: {} });
expect(error).toBeNull();
const result = await runPromise;
expect(result.success).toBe(true);
if (result.success) expect(result.result.winner).toBeNull();
expect(getParts(ctx.state).length).toBe(1);
expect(getParts(ctx.state)[0].value.position).toEqual([2, 2]);
expect(getParts(ctx.state)[0].id).toBe('white-kitten-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 white');
const promptEvent1 = await promptPromise;
// 验证器会拒绝错误的玩家
const error1 = promptEvent1.tryCommit({ name: 'play', params: ['black', 2, 2], options: {}, flags: {} });
expect(error1).toContain('Invalid player');
// 验证失败后,再次尝试有效输入
const error2 = promptEvent1.tryCommit({ name: 'play', params: ['white', 2, 2], 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, 2, 2, 'black', 'kitten');
const promptPromise = waitForPrompt(ctx);
const runPromise = ctx.commands.run<{winner: WinnerType}>('turn white');
const promptEvent1 = await promptPromise;
const error1 = promptEvent1.tryCommit({ name: 'play', params: ['white', 2, 2], options: {}, flags: {} });
expect(error1).toContain('occupied');
// 验证失败后,再次尝试有效输入
const error2 = promptEvent1.tryCommit({ name: 'play', params: ['white', 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 reject move when kitten supply is empty', async () => {
const { ctx } = createTestContext();
const state = getState(ctx);
state.produce(s => {
s.players.white.kitten.supply = 0;
});
const promptPromise = waitForPrompt(ctx);
const runPromise = ctx.commands.run<{winner: WinnerType}>('turn white');
const promptEvent1 = await promptPromise;
const error1 = promptEvent1.tryCommit({ name: 'play', params: ['white', 0, 0], options: {}, flags: {} });
expect(error1).toContain('No kittens');
// 验证失败后,取消
promptEvent1.cancel('test end');
const result = await runPromise;
expect(result.success).toBe(false);
});
it('should boop adjacent pieces after placement', async () => {
const { ctx } = createTestContext();
const state = getState(ctx);
let promptPromise = waitForPrompt(ctx);
let runPromise = ctx.commands.run<{winner: WinnerType}>('turn white');
let prompt = await promptPromise;
const error1 = prompt.tryCommit({ name: 'play', params: ['white', 3, 3], options: {}, flags: {} });
expect(error1).toBeNull();
let result = await runPromise;
expect(result.success).toBe(true);
expect(getParts(state).length).toBe(1);
promptPromise = waitForPrompt(ctx);
runPromise = ctx.commands.run<{winner: WinnerType}>('turn black');
prompt = await promptPromise;
const error2 = prompt.tryCommit({ name: 'play', params: ['black', 2, 2], options: {}, flags: {} });
expect(error2).toBeNull();
result = await runPromise;
expect(result.success).toBe(true);
expect(getParts(state).length).toBe(2);
const whitePart = getParts(state).find(p => p.value.player === 'white');
expect(whitePart).toBeDefined();
if (whitePart) {
expect(whitePart.value.position).not.toEqual([3, 3]);
}
});
it('should graduate kittens to cats and check for cat win', () => {
const { ctx } = createTestContext();
const state = getState(ctx);
placePiece(state, 1, 0, 'white', 'kitten');
placePiece(state, 1, 1, 'white', 'kitten');
placePiece(state, 1, 2, 'white', 'kitten');
const lines = checkGraduation(state, 'white');
expect(lines.length).toBeGreaterThanOrEqual(1);
processGraduation(state, 'white', lines);
expect(getParts(state).length).toBe(0);
expect(state.value.players.white.cat.supply).toBe(3);
});
it('should accept placing a cat via play command', async () => {
const { ctx } = createTestContext();
const state = getState(ctx);
state.produce(s => {
s.players.white.cat.supply = 3;
});
const promptPromise = waitForPrompt(ctx);
const runPromise = ctx.commands.run<{winner: WinnerType}>('turn white');
const promptEvent = await promptPromise;
const error = promptEvent.tryCommit({ name: 'play', params: ['white', 2, 2, 'cat'], options: {}, flags: {} });
expect(error).toBeNull();
const result = await runPromise;
expect(result.success).toBe(true);
expect(getParts(state).length).toBe(1);
expect(getParts(state)[0].id).toBe('white-cat-1');
expect(getParts(state)[0].value.pieceType).toBe('cat');
expect(state.value.players.white.cat.supply).toBe(2);
});
it('should reject placing a cat when supply is empty', async () => {
const { ctx } = createTestContext();
const state = getState(ctx);
state.produce(s => {
s.players.white.cat.supply = 0;
});
const promptPromise = waitForPrompt(ctx);
const runPromise = ctx.commands.run<{winner: WinnerType}>('turn white');
const promptEvent1 = await promptPromise;
const error1 = promptEvent1.tryCommit({ name: 'play', params: ['white', 0, 0, 'cat'], options: {}, flags: {} });
expect(error1).toContain('No cats');
// 验证失败后,取消
promptEvent1.cancel('test end');
const result = await runPromise;
expect(result.success).toBe(false);
});
});