feat: module 01 handles

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Lucas WintherandClaude Opus 5.5 committed 2026-10-29 00:17:31 +01:00
1 parent 1453a00b9c
commit 78dc09b303
32 files changed
+2561

No files matched your search

@@ -0,0 +1,21 @@
// Card definitions as the engine sees them. The definitions themselves live in `content`.
/** What the engine needs to know about a card. Module 1: only its name. */
export interface CardDef {
name: string;
}
/** Every card the game can create, looked up by name. */
export class CardLibrary {
private readonly cards = new Map<string, CardDef>();
constructor(defs: CardDef[]) {
for (const def of defs) this.cards.set(def.name, def);
}
get(name: string): CardDef {
const def = this.cards.get(name);
if (!def) throw new Error(`unknown card "${name}": define it in src/content`);
return def;
}
}
@@ -0,0 +1,8 @@
// Engine core: generic rules machinery. Never name a specific card here.
//
// Module 1: every card in the game is an entity addressed by an id handle.
// The engine knows which zone each entity is in, who owns it, and when it
// entered play.
export { CardLibrary, type CardDef } from "./card";
export { World, type EntityId, type PlayerId, type Zone } from "./world";
@@ -0,0 +1,81 @@
// Entities, zones and order of play.
import type { CardLibrary } from "./card";
/** A handle to an entity. Ids are never reused and stay valid when the entity changes zone. */
export type EntityId = number;
export type PlayerId = 0 | 1;
export type Zone = "deck" | "hand" | "board" | "graveyard";
interface Entity {
card: string;
owner: PlayerId;
zone: Zone;
/** Stamped each time the entity enters the board; undefined until it first does. */
orderOfPlay?: number;
}
/** One player's zones. Each is an ordered list of ids (the board is left to right). */
type Side = Record<Zone, EntityId[]>;
const emptySide = (): Side => ({ deck: [], hand: [], board: [], graveyard: [] });
export class World {
/** Indexed by EntityId. */
private readonly entities: Entity[] = [];
private readonly sides: [Side, Side] = [emptySide(), emptySide()];
private nextOrderOfPlay = 0;
constructor(private readonly library: CardLibrary) {}
/** Create an entity of `card` for `owner` at the end of `zone` (the right end for the board). */
create(card: string, owner: PlayerId, zone: Zone): EntityId {
if (owner !== 0 && owner !== 1) throw new Error(`no player ${owner}`);
const name = this.library.get(card).name;
const id = this.entities.length;
this.entities.push({ card: name, owner, zone });
this.enter(id, zone);
return id;
}
/** Move an entity to the end of `zone` on its owner's side, keeping its id. */
moveTo(id: EntityId, zone: Zone): void {
const { owner, zone: from } = this.entity(id);
const list = this.sides[owner][from];
list.splice(list.indexOf(id), 1);
this.enter(id, zone);
}
/** The single place an entity arrives in a zone. Entering the board starts a new order of play. */
private enter(id: EntityId, zone: Zone): void {
const entity = this.entity(id);
this.sides[entity.owner][zone].push(id);
entity.zone = zone;
if (zone === "board") entity.orderOfPlay = ++this.nextOrderOfPlay;
}
private entity(id: EntityId): Entity {
const entity = this.entities[id];
if (!entity) throw new Error(`unknown entity id ${id}`);
return entity;
}
zoneOf(id: EntityId): Zone {
return this.entity(id).zone;
}
cardName(id: EntityId): string {
return this.entity(id).card;
}
/** When the entity last entered the board (larger is later); undefined if it never has. */
orderOfPlay(id: EntityId): number | undefined {
return this.entity(id).orderOfPlay;
}
/** The ids in one player's zone, in zone order (left to right for the board). */
zone(owner: PlayerId, zone: Zone): readonly EntityId[] {
if (owner !== 0 && owner !== 1) throw new Error(`no player ${owner}`);
return this.sides[owner][zone];
}
}