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['ctx']): Entity { return ctx.state; } function waitForPrompt(ctx: ReturnType['ctx']): Promise { return new Promise(resolve => { ctx.commands.on('prompt', resolve); }); } function getParts(state: Entity) { return state.value.pieces; } 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.player).toBe('black'); expect(part.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].position).toEqual([2, 3]); expect(parts[0].player).toBe('white'); expect(parts[0].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.childIds.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.position).toEqual([2, 2]); applyBoops(state, 3, 3, 'kitten'); expect(whitePart.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.pieceType = 'cat'; applyBoops(state, 3, 3, 'kitten'); expect(whitePart.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].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.player === 'white'); expect(whitePart).toBeDefined(); if (whitePart) { expect(whitePart.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].position).toEqual([1, 1]); expect(getParts(state)[2].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].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.childIds.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].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].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].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.player === 'white'); expect(whitePart).toBeDefined(); if (whitePart) { expect(whitePart.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].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); }); });