From 975d363769bb89a89f650920e229bbc7c80b3385 Mon Sep 17 00:00:00 2001 From: hypercross Date: Thu, 2 Apr 2026 17:36:42 +0800 Subject: [PATCH] refactor: fix boop placement --- src/samples/boop/index.ts | 115 ++++++++------- tests/samples/boop.test.ts | 281 ++++++++++++++++++++++++------------- 2 files changed, 247 insertions(+), 149 deletions(-) diff --git a/src/samples/boop/index.ts b/src/samples/boop/index.ts index bdfb0dd..5df2bf6 100644 --- a/src/samples/boop/index.ts +++ b/src/samples/boop/index.ts @@ -4,18 +4,26 @@ const BOARD_SIZE = 6; const MAX_PIECES_PER_PLAYER = 8; const WIN_LENGTH = 3; -const DIRECTIONS = [ - [-1, -1], [-1, 0], [-1, 1], - [0, -1], [0, 1], - [1, -1], [1, 0], [1, 1], -]; - export type PlayerType = 'white' | 'black'; export type PieceType = 'kitten' | 'cat'; export type WinnerType = PlayerType | 'draw' | null; type BoopPart = Part & { player: PlayerType; pieceType: PieceType }; +type PieceSupply = { supply: number; placed: number }; + +type PlayerSupply = { + kitten: PieceSupply; + cat: PieceSupply; +}; + +function createPlayerSupply(): PlayerSupply { + return { + kitten: { supply: MAX_PIECES_PER_PLAYER, placed: 0 }, + cat: { supply: 0, placed: 0 }, + }; +} + export function createInitialState() { return { board: new RegionEntity('board', { @@ -26,13 +34,12 @@ export function createInitialState() { ], children: [], }), - parts: [] as Entity[], currentPlayer: 'white' as PlayerType, winner: null as WinnerType, - whiteKittensInSupply: MAX_PIECES_PER_PLAYER, - blackKittensInSupply: MAX_PIECES_PER_PLAYER, - whiteCatsInSupply: 0, - blackCatsInSupply: 0, + players: { + white: createPlayerSupply(), + black: createPlayerSupply(), + }, }; } export type BoopState = ReturnType; @@ -65,23 +72,23 @@ registration.add('turn ', async function(cmd) { while (retries < maxRetries) { retries++; - const playCmd = await this.prompt('play '); - const [player, row, col] = playCmd.params as [PlayerType, number, number]; + const playCmd = await this.prompt('play [type:string]'); + const [player, row, col, type] = playCmd.params as [PlayerType, number, number, PieceType?]; + const pieceType = type === 'cat' ? 'cat' : 'kitten'; if (player !== turnPlayer) continue; if (!isValidMove(row, col)) continue; if (isCellOccupied(this.context, row, col)) continue; - const state = this.context.value; - const kittensInSupply = player === 'white' ? state.whiteKittensInSupply : state.blackKittensInSupply; - if (kittensInSupply <= 0) continue; + const supply = this.context.value.players[player][pieceType].supply; + if (supply <= 0) continue; - placeKitten(this.context, row, col, turnPlayer); - applyBoops(this.context, row, col, 'kitten'); + placePiece(this.context, row, col, turnPlayer, pieceType); + applyBoops(this.context, row, col, pieceType); - const graduatedRows = checkGraduation(this.context, turnPlayer); - if (graduatedRows.length > 0) { - processGraduation(this.context, turnPlayer, graduatedRows); + const graduatedLines = checkGraduation(this.context, turnPlayer); + if (graduatedLines.length > 0) { + processGraduation(this.context, turnPlayer, graduatedLines); } const winner = checkWinner(this.context); @@ -111,21 +118,21 @@ export function getPartAt(host: Entity, row: number, col: number): En return (board.partsMap.value[`${row},${col}`] as Entity | undefined) || null; } -export function placeKitten(host: Entity, row: number, col: number, player: PlayerType) { +export function placePiece(host: Entity, row: number, col: number, player: PlayerType, pieceType: PieceType) { const board = getBoardRegion(host); - const moveNumber = host.value.parts.length + 1; + const count = host.value.players[player][pieceType].placed + 1; + const piece: BoopPart = { - id: `piece-${player}-${moveNumber}`, + id: `${player}-${pieceType}-${count}`, region: board, position: [row, col], player, - pieceType: 'kitten', + pieceType, }; - host.produce(state => { + host.produce(s => { const e = entity(piece.id, piece); - state.parts.push(e); - if (player === 'white') state.whiteKittensInSupply--; - else state.blackKittensInSupply--; + s.players[player][pieceType].supply--; + s.players[player][pieceType].placed++; board.produce(draft => { draft.children.push(e); }); @@ -162,11 +169,11 @@ export function applyBoops(host: Entity, placedRow: number, placedCol const newCol = c + dc; if (newRow < 0 || newRow >= BOARD_SIZE || newCol < 0 || newCol >= BOARD_SIZE) { + const pt = part.value.pieceType; + const pl = part.value.player; removePieceFromBoard(host, part); - const player = part.value.player; host.produce(state => { - if (player === 'white') state.whiteKittensInSupply++; - else state.blackKittensInSupply++; + state.players[pl][pt].supply++; }); continue; } @@ -181,17 +188,22 @@ export function applyBoops(host: Entity, placedRow: number, placedCol export function removePieceFromBoard(host: Entity, part: Entity) { const board = getBoardRegion(host); - host.produce(state => { - state.parts = state.parts.filter(p => p.id !== part.id); - board.produce(draft => { - draft.children = draft.children.filter(p => p.id !== part.id); - }); + board.produce(draft => { + draft.children = draft.children.filter(p => p.id !== part.id); }); } export function checkGraduation(host: Entity, player: PlayerType): number[][][] { - const parts = host.value.parts.filter(p => p.value.player === player && p.value.pieceType === 'kitten'); - const positions = parts.map(p => p.value.position); + const board = getBoardRegion(host); + const partsMap = board.partsMap.value; + const positions: number[][] = []; + + for (const key in partsMap) { + const part = partsMap[key] as Entity; + if (part.value.player === player && part.value.pieceType === 'kitten') { + positions.push(part.value.position); + } + } const winningLines: number[][][] = []; @@ -271,25 +283,30 @@ export function processGraduation(host: Entity, player: PlayerType, l const count = partsToRemove.length; host.produce(state => { - const catsInSupply = player === 'white' ? state.whiteCatsInSupply : state.blackCatsInSupply; - if (player === 'white') state.whiteCatsInSupply = catsInSupply + count; - else state.blackCatsInSupply = catsInSupply + count; + state.players[player].cat.supply += count; }); } export function checkWinner(host: Entity): WinnerType { - for (const player of ['white', 'black'] as PlayerType[]) { - const parts = host.value.parts.filter(p => p.value.player === player && p.value.pieceType === 'cat'); - const positions = parts.map(p => p.value.position); + const board = getBoardRegion(host); + const partsMap = board.partsMap.value; + for (const player of ['white', 'black'] as PlayerType[]) { + const positions: number[][] = []; + for (const key in partsMap) { + const part = partsMap[key] as Entity; + if (part.value.player === player && part.value.pieceType === 'cat') { + positions.push(part.value.position); + } + } if (hasWinningLine(positions)) return player; } - const totalParts = host.value.parts.length; - const whiteParts = host.value.parts.filter(p => p.value.player === 'white').length; - const blackParts = host.value.parts.filter(p => p.value.player === 'black').length; + const state = host.value; + const whiteTotal = MAX_PIECES_PER_PLAYER - state.players.white.kitten.supply + state.players.white.cat.supply; + const blackTotal = MAX_PIECES_PER_PLAYER - state.players.black.kitten.supply + state.players.black.cat.supply; - if (whiteParts >= MAX_PIECES_PER_PLAYER && blackParts >= MAX_PIECES_PER_PLAYER) { + if (whiteTotal >= MAX_PIECES_PER_PLAYER && blackTotal >= MAX_PIECES_PER_PLAYER) { return 'draw'; } diff --git a/tests/samples/boop.test.ts b/tests/samples/boop.test.ts index c8e23e4..c845485 100644 --- a/tests/samples/boop.test.ts +++ b/tests/samples/boop.test.ts @@ -4,7 +4,7 @@ import { checkWinner, isCellOccupied, getPartAt, - placeKitten, + placePiece, applyBoops, checkGraduation, processGraduation, @@ -35,6 +35,10 @@ function waitForPrompt(ctx: ReturnType['ctx']): Promis }); } +function getParts(state: Entity) { + return state.value.board.value.children; +} + describe('Boop - helper functions', () => { describe('isCellOccupied', () => { it('should return false for empty cell', () => { @@ -47,7 +51,7 @@ describe('Boop - helper functions', () => { it('should return true for occupied cell', () => { const { ctx } = createTestContext(); const state = getState(ctx); - placeKitten(state, 3, 3, 'white'); + placePiece(state, 3, 3, 'white', 'kitten'); expect(isCellOccupied(state, 3, 3)).toBe(true); }); @@ -55,7 +59,7 @@ describe('Boop - helper functions', () => { it('should return false for different cell', () => { const { ctx } = createTestContext(); const state = getState(ctx); - placeKitten(state, 0, 0, 'white'); + placePiece(state, 0, 0, 'white', 'kitten'); expect(isCellOccupied(state, 1, 1)).toBe(false); }); @@ -72,7 +76,7 @@ describe('Boop - helper functions', () => { it('should return the part at occupied cell', () => { const { ctx } = createTestContext(); const state = getState(ctx); - placeKitten(state, 2, 2, 'black'); + placePiece(state, 2, 2, 'black', 'kitten'); const part = getPartAt(state, 2, 2); expect(part).not.toBeNull(); @@ -83,35 +87,72 @@ describe('Boop - helper functions', () => { }); }); - describe('placeKitten', () => { + describe('placePiece', () => { it('should add a kitten to the board', () => { const { ctx } = createTestContext(); const state = getState(ctx); - placeKitten(state, 2, 3, 'white'); + placePiece(state, 2, 3, 'white', 'kitten'); - expect(state.value.parts.length).toBe(1); - expect(state.value.parts[0].value.position).toEqual([2, 3]); - expect(state.value.parts[0].value.player).toBe('white'); - expect(state.value.parts[0].value.pieceType).toBe('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); - placeKitten(state, 0, 0, 'white'); - expect(state.value.whiteKittensInSupply).toBe(7); - expect(state.value.blackKittensInSupply).toBe(8); + placePiece(state, 0, 0, 'white', 'kitten'); + expect(state.value.players.white.kitten.supply).toBe(7); + expect(state.value.players.black.kitten.supply).toBe(8); - placeKitten(state, 0, 1, 'black'); - expect(state.value.whiteKittensInSupply).toBe(7); - expect(state.value.blackKittensInSupply).toBe(7); + 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); - placeKitten(state, 1, 1, 'white'); + placePiece(state, 1, 1, 'white', 'kitten'); const board = getBoardRegion(state); expect(board.value.children.length).toBe(1); @@ -120,10 +161,10 @@ describe('Boop - helper functions', () => { it('should generate unique IDs for pieces', () => { const { ctx } = createTestContext(); const state = getState(ctx); - placeKitten(state, 0, 0, 'white'); - placeKitten(state, 0, 1, 'black'); + placePiece(state, 0, 0, 'white', 'kitten'); + placePiece(state, 0, 1, 'black', 'kitten'); - const ids = state.value.parts.map(p => p.id); + const ids = getParts(state).map(p => p.id); expect(new Set(ids).size).toBe(2); }); }); @@ -133,22 +174,23 @@ describe('Boop - helper functions', () => { const { ctx } = createTestContext(); const state = getState(ctx); - placeKitten(state, 3, 3, 'black'); - placeKitten(state, 2, 2, 'white'); + placePiece(state, 3, 3, 'black', 'kitten'); + placePiece(state, 2, 2, 'white', 'kitten'); - expect(state.value.parts[1].value.position).toEqual([2, 2]); + const whitePart = getParts(state)[1]; + expect(whitePart.value.position).toEqual([2, 2]); applyBoops(state, 3, 3, 'kitten'); - expect(state.value.parts[1].value.position).toEqual([1, 1]); + 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); - placeKitten(state, 3, 3, 'black'); - const whitePart = state.value.parts[0]; + placePiece(state, 3, 3, 'black', 'kitten'); + const whitePart = getParts(state)[0]; whitePart.produce(p => { p.pieceType = 'cat'; }); @@ -162,27 +204,27 @@ describe('Boop - helper functions', () => { const { ctx } = createTestContext(); const state = getState(ctx); - placeKitten(state, 0, 0, 'white'); - placeKitten(state, 1, 1, 'black'); + placePiece(state, 0, 0, 'white', 'kitten'); + placePiece(state, 1, 1, 'black', 'kitten'); applyBoops(state, 1, 1, 'kitten'); - expect(state.value.parts.length).toBe(1); - expect(state.value.parts[0].value.player).toBe('black'); - expect(state.value.whiteKittensInSupply).toBe(8); + 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); - placeKitten(state, 1, 1, 'white'); - placeKitten(state, 2, 1, 'black'); - placeKitten(state, 0, 1, 'black'); + 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 = state.value.parts.find(p => p.value.player === 'white'); + const whitePart = getParts(state).find(p => p.value.player === 'white'); expect(whitePart).toBeDefined(); if (whitePart) { expect(whitePart.value.position).toEqual([1, 1]); @@ -193,38 +235,37 @@ describe('Boop - helper functions', () => { const { ctx } = createTestContext(); const state = getState(ctx); - placeKitten(state, 3, 3, 'white'); - placeKitten(state, 2, 2, 'black'); - placeKitten(state, 2, 3, 'black'); + placePiece(state, 3, 3, 'white', 'kitten'); + placePiece(state, 2, 2, 'black', 'kitten'); + placePiece(state, 2, 3, 'black', 'kitten'); applyBoops(state, 3, 3, 'kitten'); - expect(state.value.parts[1].value.position).toEqual([1, 1]); - expect(state.value.parts[2].value.position).toEqual([1, 3]); + 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); - placeKitten(state, 3, 3, 'white'); + placePiece(state, 3, 3, 'white', 'kitten'); applyBoops(state, 3, 3, 'kitten'); - expect(state.value.parts[0].value.position).toEqual([3, 3]); + expect(getParts(state)[0].value.position).toEqual([3, 3]); }); }); describe('removePieceFromBoard', () => { - it('should remove piece from parts and board children', () => { + it('should remove piece from board children', () => { const { ctx } = createTestContext(); const state = getState(ctx); - placeKitten(state, 2, 2, 'white'); - const part = state.value.parts[0]; + placePiece(state, 2, 2, 'white', 'kitten'); + const part = getParts(state)[0]; removePieceFromBoard(state, part); - expect(state.value.parts.length).toBe(0); const board = getBoardRegion(state); expect(board.value.children.length).toBe(0); }); @@ -235,8 +276,8 @@ describe('Boop - helper functions', () => { const { ctx } = createTestContext(); const state = getState(ctx); - placeKitten(state, 0, 0, 'white'); - placeKitten(state, 2, 2, 'white'); + placePiece(state, 0, 0, 'white', 'kitten'); + placePiece(state, 2, 2, 'white', 'kitten'); const lines = checkGraduation(state, 'white'); expect(lines.length).toBe(0); @@ -246,9 +287,9 @@ describe('Boop - helper functions', () => { const { ctx } = createTestContext(); const state = getState(ctx); - placeKitten(state, 1, 0, 'white'); - placeKitten(state, 1, 1, 'white'); - placeKitten(state, 1, 2, 'white'); + 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); @@ -259,9 +300,9 @@ describe('Boop - helper functions', () => { const { ctx } = createTestContext(); const state = getState(ctx); - placeKitten(state, 0, 2, 'white'); - placeKitten(state, 1, 2, 'white'); - placeKitten(state, 2, 2, 'white'); + 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); @@ -272,9 +313,9 @@ describe('Boop - helper functions', () => { const { ctx } = createTestContext(); const state = getState(ctx); - placeKitten(state, 0, 0, 'white'); - placeKitten(state, 1, 1, 'white'); - placeKitten(state, 2, 2, 'white'); + 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); @@ -285,9 +326,9 @@ describe('Boop - helper functions', () => { const { ctx } = createTestContext(); const state = getState(ctx); - placeKitten(state, 2, 0, 'white'); - placeKitten(state, 1, 1, 'white'); - placeKitten(state, 0, 2, 'white'); + 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); @@ -298,11 +339,11 @@ describe('Boop - helper functions', () => { const { ctx } = createTestContext(); const state = getState(ctx); - placeKitten(state, 0, 0, 'white'); - placeKitten(state, 0, 1, 'white'); - placeKitten(state, 0, 2, 'white'); + placePiece(state, 0, 0, 'white', 'kitten'); + placePiece(state, 0, 1, 'white', 'kitten'); + placePiece(state, 0, 2, 'white', 'kitten'); - state.value.parts[1].produce(p => { + getParts(state)[1].produce(p => { p.pieceType = 'cat'; }); @@ -316,34 +357,34 @@ describe('Boop - helper functions', () => { const { ctx } = createTestContext(); const state = getState(ctx); - placeKitten(state, 0, 0, 'white'); - placeKitten(state, 0, 1, 'white'); - placeKitten(state, 0, 2, 'white'); + 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(state.value.parts.length).toBe(0); - expect(state.value.whiteCatsInSupply).toBe(3); + 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); - placeKitten(state, 0, 0, 'white'); - placeKitten(state, 0, 1, 'white'); - placeKitten(state, 0, 2, 'white'); - placeKitten(state, 3, 3, 'white'); + 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(state.value.parts.length).toBe(1); - expect(state.value.parts[0].value.position).toEqual([3, 3]); - expect(state.value.whiteCatsInSupply).toBe(3); + expect(getParts(state).length).toBe(1); + expect(getParts(state)[0].value.position).toEqual([3, 3]); + expect(state.value.players.white.cat.supply).toBe(3); }); }); @@ -381,15 +422,9 @@ describe('Boop - helper functions', () => { const { ctx } = createTestContext(); const state = getState(ctx); - placeKitten(state, 0, 0, 'white'); - placeKitten(state, 0, 1, 'white'); - placeKitten(state, 0, 2, 'white'); - - state.value.parts.forEach(p => { - p.produce(part => { - part.pieceType = 'cat'; - }); - }); + placePiece(state, 0, 0, 'white', 'cat'); + placePiece(state, 0, 1, 'white', 'cat'); + placePiece(state, 0, 2, 'white', 'cat'); expect(checkWinner(state)).toBe('white'); }); @@ -399,10 +434,10 @@ describe('Boop - helper functions', () => { const state = getState(ctx); for (let i = 0; i < 8; i++) { - placeKitten(state, i % 6, Math.floor(i / 6) + (i % 2), 'white'); + placePiece(state, i % 6, Math.floor(i / 6) + (i % 2), 'white', 'kitten'); } for (let i = 0; i < 8; i++) { - placeKitten(state, i % 6, Math.floor(i / 6) + 3 + (i % 2), 'black'); + placePiece(state, i % 6, Math.floor(i / 6) + 3 + (i % 2), 'black', 'kitten'); } const result = checkWinner(state); @@ -450,8 +485,9 @@ describe('Boop - game flow', () => { 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([2, 2]); + 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 () => { @@ -477,7 +513,7 @@ describe('Boop - game flow', () => { const { ctx } = createTestContext(); const state = getState(ctx); - placeKitten(state, 2, 2, 'black'); + placePiece(state, 2, 2, 'black', 'kitten'); const promptPromise = waitForPrompt(ctx); const runPromise = ctx.commands.run<{winner: WinnerType}>('turn white'); @@ -500,7 +536,7 @@ describe('Boop - game flow', () => { const state = getState(ctx); state.produce(s => { - s.whiteKittensInSupply = 0; + s.players.white.kitten.supply = 0; }); const promptPromise = waitForPrompt(ctx); @@ -528,7 +564,7 @@ describe('Boop - game flow', () => { prompt.resolve({ name: 'play', params: ['white', 3, 3], options: {}, flags: {} }); let result = await runPromise; expect(result.success).toBe(true); - expect(state.value.parts.length).toBe(1); + expect(getParts(state).length).toBe(1); promptPromise = waitForPrompt(ctx); runPromise = ctx.commands.run<{winner: WinnerType}>('turn black'); @@ -536,29 +572,74 @@ describe('Boop - game flow', () => { prompt.resolve({ name: 'play', params: ['black', 2, 2], options: {}, flags: {} }); result = await runPromise; expect(result.success).toBe(true); - expect(state.value.parts.length).toBe(2); + expect(getParts(state).length).toBe(2); - const whitePart = state.value.parts.find(p => p.value.player === 'white'); + 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', async () => { + it('should graduate kittens to cats and check for cat win', () => { const { ctx } = createTestContext(); const state = getState(ctx); - placeKitten(state, 1, 0, 'white'); - placeKitten(state, 1, 1, 'white'); - placeKitten(state, 1, 2, 'white'); + 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(state.value.parts.length).toBe(0); - expect(state.value.whiteCatsInSupply).toBe(3); + 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; + promptEvent.resolve({ name: 'play', params: ['white', 2, 2, 'cat'], options: {}, flags: {} }); + + 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; + promptEvent1.resolve({ name: 'play', params: ['white', 0, 0, 'cat'], options: {}, flags: {} }); + + const promptEvent2 = await waitForPrompt(ctx); + expect(promptEvent2).not.toBeNull(); + + promptEvent2.reject(new Error('test end')); + + const result = await runPromise; + expect(result.success).toBe(false); }); });