Scaffold Entity
Use when introducing a new game entity (e.g. Enemy, Item, Projectile).
Rules
- All entity types are
Readonly<{…}>— no mutable fields. - Every entity has a branded ID:
Brand<string, "EntityNameId">— never a plainstring. - IDs are deterministically generated via the seeded PRNG stored in state — never
Math.random(), nevercrypto.randomUUID(). - IDs are never reused within a run; spawn order is tracked for deterministic tie-breaking.
- Board actors use
GridCellfor gameplay position. Do not add actorxPos/yPos; pixel coordinates belong only to render commands and raw browser input events. - Composition over nesting: share sub-shapes via type aliases, not inheritance.
Steps
Declare the branded ID type in
packages/game/lib/core/types.ts:import type { Brand } from "@bruff/utils"; export type EnemyId = Brand<string, "EnemyId">;Declare the entity type as a
Readonlyshape:import type { GridCell } from "./types.ts"; export type Enemy = Readonly<{ cell: GridCell; id: EnemyId; spawnOrder: number; size: number; }>;Add a factory function (pure, no side effects) that accepts PRNG state and returns the entity plus next PRNG state:
const createEnemy = (
prng: PrngState,
spawnOrder: number,
cell: GridCell,
): { enemy: Enemy; prng: PrngState } => {
const step = nextId(prng);
return {
enemy: {
cell,
id: brand<"EnemyId">(step.value),
spawnOrder,
size: ENEMY_SIZE,
},
prng: step.prng,
};
};
Add the entity collection to
GameStateinpackages/game/lib/core/types.tsasReadonlyArray<Enemy>.Write unit tests in a co-located
*.test.tscovering:- ID is branded (compile-time, no runtime check needed).
- Two calls with the same PRNG seed produce the same ID.
- Consecutive calls produce different IDs.
Tie-breaking
When multiple entities act simultaneously in a tick, order by:
spawnOrderascending (earlier spawn wins)idlexicographic ascending as a secondary key