refactor: transition to non child entity format

This commit is contained in:
2026-04-03 12:46:50 +08:00
parent cf7cfcd86b
commit eb0ebf5411
8 changed files with 425 additions and 478 deletions
+81 -111
View File
@@ -1,4 +1,4 @@
import {createGameCommandRegistry, Part, Entity, entity, RegionEntity} from '@/index';
import {createGameCommandRegistry, Part, Entity, createRegion} from '@/index';
const BOARD_SIZE = 6;
const MAX_PIECES_PER_PLAYER = 8;
@@ -18,26 +18,15 @@ type Player = {
cat: PieceSupply;
};
type PlayerEntity = Entity<Player>;
function createPlayer(id: PlayerType): PlayerEntity {
return entity<Player>(id, {
id,
kitten: { supply: MAX_PIECES_PER_PLAYER, placed: 0 },
cat: { supply: 0, placed: 0 },
});
}
type PlayerData = Record<PlayerType, Player>;
export function createInitialState() {
return {
board: new RegionEntity('board', {
id: 'board',
axes: [
{ name: 'x', min: 0, max: BOARD_SIZE - 1 },
{ name: 'y', min: 0, max: BOARD_SIZE - 1 },
],
children: [],
}),
board: createRegion('board', [
{ name: 'x', min: 0, max: BOARD_SIZE - 1 },
{ name: 'y', min: 0, max: BOARD_SIZE - 1 },
]),
pieces: [] as BoopPart[],
currentPlayer: 'white' as PlayerType,
winner: null as WinnerType,
players: {
@@ -46,26 +35,30 @@ export function createInitialState() {
},
};
}
function createPlayer(id: PlayerType): Player {
return {
id,
kitten: { supply: MAX_PIECES_PER_PLAYER, placed: 0 },
cat: { supply: 0, placed: 0 },
};
}
export type BoopState = ReturnType<typeof createInitialState>;
const registration = createGameCommandRegistry<BoopState>();
export const registry = registration.registry;
// Player Entity helper functions
export function getPlayer(host: Entity<BoopState>, player: PlayerType): PlayerEntity {
export function getPlayer(host: Entity<BoopState>, player: PlayerType): Player {
return host.value.players[player];
}
export function decrementSupply(player: PlayerEntity, pieceType: PieceType) {
player.produce(p => {
p[pieceType].supply--;
p[pieceType].placed++;
});
export function decrementSupply(player: Player, pieceType: PieceType) {
player[pieceType].supply--;
player[pieceType].placed++;
}
export function incrementSupply(player: PlayerEntity, pieceType: PieceType, count?: number) {
player.produce(p => {
p[pieceType].supply += count ?? 1;
});
export function incrementSupply(player: Player, pieceType: PieceType, count?: number) {
player[pieceType].supply += count ?? 1;
}
registration.add('setup', async function() {
@@ -106,8 +99,8 @@ registration.add('turn <player>', async function(cmd) {
return `Cell (${row}, ${col}) is already occupied.`;
}
const playerEntity = getPlayer(this.context, player);
const supply = playerEntity.value[pieceType].supply;
const playerData = getPlayer(this.context, player);
const supply = playerData[pieceType].supply;
if (supply <= 0) {
return `No ${pieceType}s left in ${player}'s supply.`;
}
@@ -126,15 +119,9 @@ registration.add('turn <player>', async function(cmd) {
}
if (countPiecesOnBoard(this.context, turnPlayer) >= MAX_PIECES_PER_PLAYER) {
const board = getBoardRegion(this.context);
const partsMap = board.partsMap.value;
const availableKittens: Entity<BoopPart>[] = [];
for (const key in partsMap) {
const part = partsMap[key] as Entity<BoopPart>;
if (part.value.player === turnPlayer && part.value.pieceType === 'kitten') {
availableKittens.push(part);
}
}
const availableKittens = this.context.value.pieces.filter(
p => p.player === turnPlayer && p.pieceType === 'kitten'
);
if (availableKittens.length > 0) {
const graduateCmd = await this.prompt(
@@ -142,16 +129,16 @@ registration.add('turn <player>', async function(cmd) {
(command) => {
const [row, col] = command.params as [number, number];
const posKey = `${row},${col}`;
const part = availableKittens.find(p => `${p.value.position[0]},${p.value.position[1]}` === posKey);
const part = availableKittens.find(p => `${p.position[0]},${p.position[1]}` === posKey);
if (!part) return `No kitten at (${row}, ${col}).`;
return null;
}
);
const [row, col] = graduateCmd.params as [number, number];
const part = availableKittens.find(p => p.value.position[0] === row && p.value.position[1] === col)!;
const part = availableKittens.find(p => p.position[0] === row && p.position[1] === col)!;
removePieceFromBoard(this.context, part);
const playerEntity = getPlayer(this.context, turnPlayer);
incrementSupply(playerEntity, 'cat', 1);
const playerData = getPlayer(this.context, turnPlayer);
incrementSupply(playerData, 'cat', 1);
}
}
@@ -171,44 +158,44 @@ export function getBoardRegion(host: Entity<BoopState>) {
export function isCellOccupied(host: Entity<BoopState>, row: number, col: number): boolean {
const board = getBoardRegion(host);
return board.partsMap.value[`${row},${col}`] !== undefined;
return board.partMap[`${row},${col}`] !== undefined;
}
export function getPartAt(host: Entity<BoopState>, row: number, col: number): Entity<BoopPart> | null {
export function getPartAt(host: Entity<BoopState>, row: number, col: number): BoopPart | null {
const board = getBoardRegion(host);
return (board.partsMap.value[`${row},${col}`] as Entity<BoopPart> | undefined) || null;
const partId = board.partMap[`${row},${col}`];
if (!partId) return null;
return host.value.pieces.find(p => p.id === partId) || null;
}
export function placePiece(host: Entity<BoopState>, row: number, col: number, player: PlayerType, pieceType: PieceType) {
const board = getBoardRegion(host);
const playerEntity = getPlayer(host, player);
const count = playerEntity.value[pieceType].placed + 1;
const playerData = getPlayer(host, player);
const count = playerData[pieceType].placed + 1;
const piece: BoopPart = {
id: `${player}-${pieceType}-${count}`,
region: board,
regionId: 'board',
position: [row, col],
player,
pieceType,
};
host.produce(s => {
const e = entity(piece.id, piece);
board.produce(draft => {
draft.children.push(e);
});
s.pieces.push(piece);
board.childIds.push(piece.id);
board.partMap[`${row},${col}`] = piece.id;
});
decrementSupply(playerEntity, pieceType);
decrementSupply(playerData, pieceType);
}
export function applyBoops(host: Entity<BoopState>, placedRow: number, placedCol: number, placedType: PieceType) {
const board = getBoardRegion(host);
const partsMap = board.partsMap.value;
const pieces = host.value.pieces;
const piecesToBoop: { part: Entity<BoopPart>; dr: number; dc: number }[] = [];
const piecesToBoop: { part: BoopPart; dr: number; dc: number }[] = [];
for (const key in partsMap) {
const part = partsMap[key] as Entity<BoopPart>;
const [r, c] = part.value.position;
for (const part of pieces) {
const [r, c] = part.position;
if (r === placedRow && c === placedCol) continue;
const dr = Math.sign(r - placedRow);
@@ -216,7 +203,7 @@ export function applyBoops(host: Entity<BoopState>, placedRow: number, placedCol
if (Math.abs(r - placedRow) <= 1 && Math.abs(c - placedCol) <= 1) {
const booperIsKitten = placedType === 'kitten';
const targetIsCat = part.value.pieceType === 'cat';
const targetIsCat = part.pieceType === 'cat';
if (booperIsKitten && targetIsCat) continue;
@@ -225,36 +212,38 @@ export function applyBoops(host: Entity<BoopState>, placedRow: number, placedCol
}
for (const { part, dr, dc } of piecesToBoop) {
const [r, c] = part.value.position;
const [r, c] = part.position;
const newRow = r + dr;
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;
const playerEntity = getPlayer(host, pl);
const pt = part.pieceType;
const pl = part.player;
const playerData = getPlayer(host, pl);
removePieceFromBoard(host, part);
incrementSupply(playerEntity, pt);
incrementSupply(playerData, pt);
continue;
}
if (isCellOccupied(host, newRow, newCol)) continue;
part.produce(p => {
p.position = [newRow, newCol];
});
part.position = [newRow, newCol];
board.partMap = Object.fromEntries(
board.childIds.map(id => {
const p = pieces.find(x => x.id === id)!;
return [p.position.join(','), id];
})
);
}
}
export function removePieceFromBoard(host: Entity<BoopState>, part: Entity<BoopPart>) {
export function removePieceFromBoard(host: Entity<BoopState>, part: BoopPart) {
const board = getBoardRegion(host);
const playerEntity = getPlayer(host, part.value.player);
board.produce(draft => {
draft.children = draft.children.filter(p => p.id !== part.id);
});
playerEntity.produce(p => {
p[part.value.pieceType].placed--;
});
const playerData = getPlayer(host, part.player);
board.childIds = board.childIds.filter(id => id !== part.id);
delete board.partMap[part.position.join(',')];
host.value.pieces = host.value.pieces.filter(p => p.id !== part.id);
playerData[part.pieceType].placed--;
}
const DIRECTIONS: [number, number][] = [
@@ -309,14 +298,12 @@ export function hasWinningLine(positions: number[][]): boolean {
}
export function checkGraduation(host: Entity<BoopState>, player: PlayerType): number[][][] {
const board = getBoardRegion(host);
const partsMap = board.partsMap.value;
const pieces = host.value.pieces;
const posSet = new Set<string>();
for (const key in partsMap) {
const part = partsMap[key] as Entity<BoopPart>;
if (part.value.player === player && part.value.pieceType === 'kitten') {
posSet.add(`${part.value.position[0]},${part.value.position[1]}`);
for (const part of pieces) {
if (part.player === player && part.pieceType === 'kitten') {
posSet.add(`${part.position[0]},${part.position[1]}`);
}
}
@@ -338,48 +325,31 @@ export function processGraduation(host: Entity<BoopState>, player: PlayerType, l
}
const board = getBoardRegion(host);
const partsMap = board.partsMap.value;
const partsToRemove: Entity<BoopPart>[] = [];
for (const key in partsMap) {
const part = partsMap[key] as Entity<BoopPart>;
if (part.value.player === player && part.value.pieceType === 'kitten' && allPositions.has(`${part.value.position[0]},${part.value.position[1]}`)) {
partsToRemove.push(part);
}
}
const partsToRemove = host.value.pieces.filter(
p => p.player === player && p.pieceType === 'kitten' && allPositions.has(`${p.position[0]},${p.position[1]}`)
);
for (const part of partsToRemove) {
removePieceFromBoard(host, part);
}
const count = partsToRemove.length;
const playerEntity = getPlayer(host, player);
incrementSupply(playerEntity, 'cat', count);
const playerData = getPlayer(host, player);
incrementSupply(playerData, 'cat', count);
}
export function countPiecesOnBoard(host: Entity<BoopState>, player: PlayerType): number {
const board = getBoardRegion(host);
const partsMap = board.partsMap.value;
let count = 0;
for (const key in partsMap) {
const part = partsMap[key] as Entity<BoopPart>;
if (part.value.player === player) count++;
}
return count;
const pieces = host.value.pieces;
return pieces.filter(p => p.player === player).length;
}
export function checkWinner(host: Entity<BoopState>): WinnerType {
const board = getBoardRegion(host);
const partsMap = board.partsMap.value;
const pieces = host.value.pieces;
for (const player of ['white', 'black'] as PlayerType[]) {
const positions: number[][] = [];
for (const key in partsMap) {
const part = partsMap[key] as Entity<BoopPart>;
if (part.value.player === player && part.value.pieceType === 'cat') {
positions.push(part.value.position);
}
}
const positions = pieces
.filter(p => p.player === player && p.pieceType === 'cat')
.map(p => p.position);
if (hasWinningLine(positions)) return player;
}