refactor: update boop implementation

This commit is contained in:
hyper 2026-04-02 19:46:49 +08:00
parent 15122defcc
commit 793c7d834b
2 changed files with 55 additions and 40 deletions

View File

@ -12,17 +12,20 @@ type BoopPart = Part & { player: PlayerType; pieceType: PieceType };
type PieceSupply = { supply: number; placed: number }; type PieceSupply = { supply: number; placed: number };
type PlayerSupply = { type Player = {
id: PlayerType;
kitten: PieceSupply; kitten: PieceSupply;
cat: PieceSupply; cat: PieceSupply;
}; };
// TODO refactor this into an Entity type PlayerEntity = Entity<Player>;
function createPlayerSupply(): PlayerSupply {
return { function createPlayer(id: PlayerType): PlayerEntity {
return entity<Player>(id, {
id,
kitten: { supply: MAX_PIECES_PER_PLAYER, placed: 0 }, kitten: { supply: MAX_PIECES_PER_PLAYER, placed: 0 },
cat: { supply: 0, placed: 0 }, cat: { supply: 0, placed: 0 },
}; });
} }
export function createInitialState() { export function createInitialState() {
@ -38,8 +41,8 @@ export function createInitialState() {
currentPlayer: 'white' as PlayerType, currentPlayer: 'white' as PlayerType,
winner: null as WinnerType, winner: null as WinnerType,
players: { players: {
white: createPlayerSupply(), white: createPlayer('white'),
black: createPlayerSupply(), black: createPlayer('black'),
}, },
}; };
} }
@ -47,6 +50,24 @@ export type BoopState = ReturnType<typeof createInitialState>;
const registration = createGameCommandRegistry<BoopState>(); const registration = createGameCommandRegistry<BoopState>();
export const registry = registration.registry; export const registry = registration.registry;
// Player Entity helper functions
export function getPlayer(host: Entity<BoopState>, 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() { registration.add('setup', async function() {
const {context} = this; const {context} = this;
while (true) { while (true) {
@ -85,7 +106,8 @@ registration.add('turn <player>', async function(cmd) {
return `Cell (${row}, ${col}) is already occupied.`; 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) { if (supply <= 0) {
return `No ${pieceType}s left in ${player}'s supply.`; return `No ${pieceType}s left in ${player}'s supply.`;
} }
@ -129,7 +151,8 @@ export function getPartAt(host: Entity<BoopState>, row: number, col: number): En
export function placePiece(host: Entity<BoopState>, row: number, col: number, player: PlayerType, pieceType: PieceType) { export function placePiece(host: Entity<BoopState>, row: number, col: number, player: PlayerType, pieceType: PieceType) {
const board = getBoardRegion(host); 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 = { const piece: BoopPart = {
id: `${player}-${pieceType}-${count}`, id: `${player}-${pieceType}-${count}`,
@ -140,12 +163,11 @@ export function placePiece(host: Entity<BoopState>, row: number, col: number, pl
}; };
host.produce(s => { host.produce(s => {
const e = entity(piece.id, piece); const e = entity(piece.id, piece);
s.players[player][pieceType].supply--;
s.players[player][pieceType].placed++;
board.produce(draft => { board.produce(draft => {
draft.children.push(e); draft.children.push(e);
}); });
}); });
decrementSupply(playerEntity, pieceType);
} }
export function applyBoops(host: Entity<BoopState>, placedRow: number, placedCol: number, placedType: PieceType) { export function applyBoops(host: Entity<BoopState>, placedRow: number, placedCol: number, placedType: PieceType) {
@ -180,10 +202,9 @@ export function applyBoops(host: Entity<BoopState>, placedRow: number, placedCol
if (newRow < 0 || newRow >= BOARD_SIZE || newCol < 0 || newCol >= BOARD_SIZE) { if (newRow < 0 || newRow >= BOARD_SIZE || newCol < 0 || newCol >= BOARD_SIZE) {
const pt = part.value.pieceType; const pt = part.value.pieceType;
const pl = part.value.player; const pl = part.value.player;
const playerEntity = getPlayer(host, pl);
removePieceFromBoard(host, part); removePieceFromBoard(host, part);
host.produce(state => { incrementSupply(playerEntity, pt);
state.players[pl][pt].supply++;
});
continue; continue;
} }
@ -298,9 +319,8 @@ export function processGraduation(host: Entity<BoopState>, player: PlayerType, l
} }
const count = partsToRemove.length; const count = partsToRemove.length;
host.produce(state => { const playerEntity = getPlayer(host, player);
state.players[player].cat.supply += count; incrementSupply(playerEntity, 'cat', count);
});
} }
export function checkWinner(host: Entity<BoopState>): WinnerType { export function checkWinner(host: Entity<BoopState>): WinnerType {
@ -318,9 +338,10 @@ export function checkWinner(host: Entity<BoopState>): WinnerType {
if (hasWinningLine(positions)) return player; if (hasWinningLine(positions)) return player;
} }
const state = host.value; const whitePlayer = getPlayer(host, 'white');
const whiteTotal = MAX_PIECES_PER_PLAYER - state.players.white.kitten.supply + state.players.white.cat.supply; const blackPlayer = getPlayer(host, 'black');
const blackTotal = MAX_PIECES_PER_PLAYER - state.players.black.kitten.supply + state.players.black.cat.supply; 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) { if (whiteTotal >= MAX_PIECES_PER_PLAYER && blackTotal >= MAX_PIECES_PER_PLAYER) {
return 'draw'; return 'draw';

View File

@ -130,23 +130,23 @@ describe('Boop - helper functions', () => {
const state = getState(ctx); const state = getState(ctx);
placePiece(state, 0, 0, 'white', 'kitten'); placePiece(state, 0, 0, 'white', 'kitten');
expect(state.value.players.white.kitten.supply).toBe(7); expect(state.value.players.white.value.kitten.supply).toBe(7);
expect(state.value.players.black.kitten.supply).toBe(8); expect(state.value.players.black.value.kitten.supply).toBe(8);
placePiece(state, 0, 1, 'black', 'kitten'); placePiece(state, 0, 1, 'black', 'kitten');
expect(state.value.players.white.kitten.supply).toBe(7); expect(state.value.players.white.value.kitten.supply).toBe(7);
expect(state.value.players.black.kitten.supply).toBe(7); expect(state.value.players.black.value.kitten.supply).toBe(7);
}); });
it('should decrement the correct player cat supply', () => { it('should decrement the correct player cat supply', () => {
const { ctx } = createTestContext(); const { ctx } = createTestContext();
const state = getState(ctx); const state = getState(ctx);
state.produce(s => { state.produce(s => {
s.players.white.cat.supply = 3; s.players.white.value.cat.supply = 3;
}); });
placePiece(state, 0, 0, 'white', 'cat'); 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', () => { 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).length).toBe(1);
expect(getParts(state)[0].value.player).toBe('black'); 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', () => { it('should not boop piece if target cell is occupied', () => {
@ -367,7 +367,7 @@ describe('Boop - helper functions', () => {
processGraduation(state, 'white', lines); processGraduation(state, 'white', lines);
expect(getParts(state).length).toBe(0); 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', () => { 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).length).toBe(1);
expect(getParts(state)[0].value.position).toEqual([3, 3]); 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 { ctx } = createTestContext();
const state = getState(ctx); const state = getState(ctx);
state.produce(s => { state.value.players.white.value.kitten.supply = 0;
s.players.white.kitten.supply = 0;
});
const promptPromise = waitForPrompt(ctx); const promptPromise = waitForPrompt(ctx);
const runPromise = ctx.commands.run<{winner: WinnerType}>('turn white'); const runPromise = ctx.commands.run<{winner: WinnerType}>('turn white');
@ -598,16 +596,14 @@ describe('Boop - game flow', () => {
processGraduation(state, 'white', lines); processGraduation(state, 'white', lines);
expect(getParts(state).length).toBe(0); 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 () => { it('should accept placing a cat via play command', async () => {
const { ctx } = createTestContext(); const { ctx } = createTestContext();
const state = getState(ctx); const state = getState(ctx);
state.produce(s => { state.value.players.white.value.cat.supply = 3;
s.players.white.cat.supply = 3;
});
const promptPromise = waitForPrompt(ctx); const promptPromise = waitForPrompt(ctx);
const runPromise = ctx.commands.run<{winner: WinnerType}>('turn white'); 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).length).toBe(1);
expect(getParts(state)[0].id).toBe('white-cat-1'); expect(getParts(state)[0].id).toBe('white-cat-1');
expect(getParts(state)[0].value.pieceType).toBe('cat'); 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 () => { it('should reject placing a cat when supply is empty', async () => {
const { ctx } = createTestContext(); const { ctx } = createTestContext();
const state = getState(ctx); const state = getState(ctx);
state.produce(s => { state.value.players.white.value.cat.supply = 0;
s.players.white.cat.supply = 0;
});
const promptPromise = waitForPrompt(ctx); const promptPromise = waitForPrompt(ctx);
const runPromise = ctx.commands.run<{winner: WinnerType}>('turn white'); const runPromise = ctx.commands.run<{winner: WinnerType}>('turn white');