From 793c7d834b5fea62b3c8f710cfd4691ad102f428 Mon Sep 17 00:00:00 2001 From: hyper Date: Thu, 2 Apr 2026 19:46:49 +0800 Subject: [PATCH] refactor: update boop implementation --- src/samples/boop/index.ts | 61 +++++++++++++++++++++++++------------- tests/samples/boop.test.ts | 34 +++++++++------------ 2 files changed, 55 insertions(+), 40 deletions(-) diff --git a/src/samples/boop/index.ts b/src/samples/boop/index.ts index 42bd831..0e79873 100644 --- a/src/samples/boop/index.ts +++ b/src/samples/boop/index.ts @@ -12,17 +12,20 @@ type BoopPart = Part & { player: PlayerType; pieceType: PieceType }; type PieceSupply = { supply: number; placed: number }; -type PlayerSupply = { +type Player = { + id: PlayerType; kitten: PieceSupply; cat: PieceSupply; }; -// TODO refactor this into an Entity -function createPlayerSupply(): PlayerSupply { - return { +type PlayerEntity = Entity; + +function createPlayer(id: PlayerType): PlayerEntity { + return entity(id, { + id, kitten: { supply: MAX_PIECES_PER_PLAYER, placed: 0 }, cat: { supply: 0, placed: 0 }, - }; + }); } export function createInitialState() { @@ -38,8 +41,8 @@ export function createInitialState() { currentPlayer: 'white' as PlayerType, winner: null as WinnerType, players: { - white: createPlayerSupply(), - black: createPlayerSupply(), + white: createPlayer('white'), + black: createPlayer('black'), }, }; } @@ -47,6 +50,24 @@ export type BoopState = ReturnType; const registration = createGameCommandRegistry(); export const registry = registration.registry; +// Player Entity helper functions +export function getPlayer(host: Entity, player: PlayerType): PlayerEntity { + return host.value.players[player]; +} + +export function decrementSupply(player: PlayerEntity, pieceType: PieceType) { + player.produce(p => { + p[pieceType].supply--; + p[pieceType].placed++; + }); +} + +export function incrementSupply(player: PlayerEntity, pieceType: PieceType, count?: number) { + player.produce(p => { + p[pieceType].supply += count ?? 1; + }); +} + registration.add('setup', async function() { const {context} = this; while (true) { @@ -85,7 +106,8 @@ registration.add('turn ', async function(cmd) { return `Cell (${row}, ${col}) is already occupied.`; } - const supply = this.context.value.players[player][pieceType].supply; + const playerEntity = getPlayer(this.context, player); + const supply = playerEntity.value[pieceType].supply; if (supply <= 0) { return `No ${pieceType}s left in ${player}'s supply.`; } @@ -129,7 +151,8 @@ export function getPartAt(host: Entity, row: number, col: number): En export function placePiece(host: Entity, row: number, col: number, player: PlayerType, pieceType: PieceType) { const board = getBoardRegion(host); - const count = host.value.players[player][pieceType].placed + 1; + const playerEntity = getPlayer(host, player); + const count = playerEntity.value[pieceType].placed + 1; const piece: BoopPart = { id: `${player}-${pieceType}-${count}`, @@ -140,12 +163,11 @@ export function placePiece(host: Entity, row: number, col: number, pl }; host.produce(s => { const e = entity(piece.id, piece); - s.players[player][pieceType].supply--; - s.players[player][pieceType].placed++; board.produce(draft => { draft.children.push(e); }); }); + decrementSupply(playerEntity, pieceType); } export function applyBoops(host: Entity, placedRow: number, placedCol: number, placedType: PieceType) { @@ -180,10 +202,9 @@ export function applyBoops(host: Entity, placedRow: number, placedCol if (newRow < 0 || newRow >= BOARD_SIZE || newCol < 0 || newCol >= BOARD_SIZE) { const pt = part.value.pieceType; const pl = part.value.player; + const playerEntity = getPlayer(host, pl); removePieceFromBoard(host, part); - host.produce(state => { - state.players[pl][pt].supply++; - }); + incrementSupply(playerEntity, pt); continue; } @@ -298,9 +319,8 @@ export function processGraduation(host: Entity, player: PlayerType, l } const count = partsToRemove.length; - host.produce(state => { - state.players[player].cat.supply += count; - }); + const playerEntity = getPlayer(host, player); + incrementSupply(playerEntity, 'cat', count); } export function checkWinner(host: Entity): WinnerType { @@ -318,9 +338,10 @@ export function checkWinner(host: Entity): WinnerType { if (hasWinningLine(positions)) return player; } - 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; + const whitePlayer = getPlayer(host, 'white'); + const blackPlayer = getPlayer(host, 'black'); + const whiteTotal = MAX_PIECES_PER_PLAYER - whitePlayer.value.kitten.supply + whitePlayer.value.cat.supply; + const blackTotal = MAX_PIECES_PER_PLAYER - blackPlayer.value.kitten.supply + blackPlayer.value.cat.supply; 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 597cacc..f2bf9fb 100644 --- a/tests/samples/boop.test.ts +++ b/tests/samples/boop.test.ts @@ -130,23 +130,23 @@ describe('Boop - helper functions', () => { 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); + expect(state.value.players.white.value.kitten.supply).toBe(7); + expect(state.value.players.black.value.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); + expect(state.value.players.white.value.kitten.supply).toBe(7); + expect(state.value.players.black.value.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; + s.players.white.value.cat.supply = 3; }); placePiece(state, 0, 0, 'white', 'cat'); - expect(state.value.players.white.cat.supply).toBe(2); + expect(state.value.players.white.value.cat.supply).toBe(2); }); it('should add piece to board region children', () => { @@ -211,7 +211,7 @@ describe('Boop - helper functions', () => { expect(getParts(state).length).toBe(1); expect(getParts(state)[0].value.player).toBe('black'); - expect(state.value.players.white.kitten.supply).toBe(8); + expect(state.value.players.white.value.kitten.supply).toBe(8); }); it('should not boop piece if target cell is occupied', () => { @@ -367,7 +367,7 @@ describe('Boop - helper functions', () => { processGraduation(state, 'white', lines); expect(getParts(state).length).toBe(0); - expect(state.value.players.white.cat.supply).toBe(3); + expect(state.value.players.white.value.cat.supply).toBe(3); }); it('should only graduate pieces on the winning lines', () => { @@ -384,7 +384,7 @@ describe('Boop - helper functions', () => { expect(getParts(state).length).toBe(1); expect(getParts(state)[0].value.position).toEqual([3, 3]); - expect(state.value.players.white.cat.supply).toBe(3); + expect(state.value.players.white.value.cat.supply).toBe(3); }); }); @@ -537,9 +537,7 @@ describe('Boop - game flow', () => { const { ctx } = createTestContext(); const state = getState(ctx); - state.produce(s => { - s.players.white.kitten.supply = 0; - }); + state.value.players.white.value.kitten.supply = 0; const promptPromise = waitForPrompt(ctx); const runPromise = ctx.commands.run<{winner: WinnerType}>('turn white'); @@ -598,16 +596,14 @@ describe('Boop - game flow', () => { processGraduation(state, 'white', lines); expect(getParts(state).length).toBe(0); - expect(state.value.players.white.cat.supply).toBe(3); + expect(state.value.players.white.value.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; - }); + state.value.players.white.value.cat.supply = 3; const promptPromise = waitForPrompt(ctx); const runPromise = ctx.commands.run<{winner: WinnerType}>('turn white'); @@ -621,16 +617,14 @@ describe('Boop - game flow', () => { 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); + expect(state.value.players.white.value.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; - }); + state.value.players.white.value.cat.supply = 0; const promptPromise = waitForPrompt(ctx); const runPromise = ctx.commands.run<{winner: WinnerType}>('turn white');