Implement engine core: board assembly, movement, turns, combat, victory
Pure deterministic game core in @wizwar/engine: seeded RNG (mulberry32, state in GameState so seed+commands replays identically), sector assembly with rotation, junction merging, and wraparound warps (configurable pairings; the 2p diagram crosses its side openings), movement (3 + one number card), geometric line of sight, deck building from the verified card data (asserts 125/200 totals), and the command-to-event reducer: setup with TRAP! redraw and die-roll first player, punching (no combat round 1, no self-attack, once per turn), damage/death with killer-takes-cards and forced discard, treasure stealing with both victory conditions, pick-up-ends-turn, and end-of-turn draw. Events carry full spatial detail for future replay rendering; private card knowledge rides on visibleTo events with a redaction helper. 21 tests passing. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5
parent
11209cdc5d
commit
a8884592a4
@@ -0,0 +1,86 @@
|
||||
// Card definitions loaded from data/cards.json (verified against the owner's
|
||||
// physical 6th edition + Expansion Set #1), and physical-deck construction:
|
||||
// each printed copy of a card becomes one CardInstance with a stable id.
|
||||
|
||||
import cardsData from "../data/cards.json";
|
||||
|
||||
export type CardSet = "basic" | "expansion1" | "expansion2";
|
||||
export type CardType =
|
||||
| "attack"
|
||||
| "neutral"
|
||||
| "counteraction"
|
||||
| "neutral/counteraction"
|
||||
| "number"
|
||||
| "object"
|
||||
| "trap"
|
||||
| "artifact"
|
||||
| "special";
|
||||
|
||||
export interface CardDef {
|
||||
id: string;
|
||||
name: string;
|
||||
set: CardSet;
|
||||
cardType: CardType | null;
|
||||
los: boolean | null;
|
||||
text: string | null;
|
||||
quantity: number | null;
|
||||
value?: number; // number cards
|
||||
alsoIn?: { set: CardSet; quantity: number }[];
|
||||
faqRulings: string[];
|
||||
}
|
||||
|
||||
export interface CardInstance {
|
||||
/** e.g. "fireball#2" — stable across the whole game. */
|
||||
instanceId: string;
|
||||
cardId: string;
|
||||
}
|
||||
|
||||
const defs: CardDef[] = (cardsData as { cards: CardDef[] }).cards;
|
||||
const byId = new Map(defs.map((d) => [d.id, d]));
|
||||
|
||||
export function cardDef(cardId: string): CardDef {
|
||||
const def = byId.get(cardId);
|
||||
if (!def) throw new Error(`unknown card: ${cardId}`);
|
||||
return def;
|
||||
}
|
||||
|
||||
export function allCardDefs(): readonly CardDef[] {
|
||||
return defs;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build the physical deck for the chosen sets. The 6e basic deck is exactly
|
||||
* 125 cards; adding Expansion Set #1 adds exactly 75 more (including its own
|
||||
* number cards) — both counts verified against the owner's rulebook lists.
|
||||
*/
|
||||
export function buildDeck(sets: CardSet[]): CardInstance[] {
|
||||
const instances: CardInstance[] = [];
|
||||
for (const def of defs) {
|
||||
let copies = 0;
|
||||
if (sets.includes(def.set) && def.quantity != null) copies += def.quantity;
|
||||
for (const extra of def.alsoIn ?? []) {
|
||||
if (sets.includes(extra.set)) copies += extra.quantity;
|
||||
}
|
||||
for (let i = 1; i <= copies; i++) {
|
||||
instances.push({ instanceId: `${def.id}#${i}`, cardId: def.id });
|
||||
}
|
||||
}
|
||||
return instances;
|
||||
}
|
||||
|
||||
export function isNumberCard(cardId: string): boolean {
|
||||
return cardDef(cardId).cardType === "number";
|
||||
}
|
||||
|
||||
export function numberValue(cardId: string): number {
|
||||
const def = cardDef(cardId);
|
||||
if (def.cardType !== "number" || def.value == null) {
|
||||
throw new Error(`${cardId} is not a number card`);
|
||||
}
|
||||
return def.value;
|
||||
}
|
||||
|
||||
/** TRAP! is discarded and redrawn if it comes up during the initial deal. */
|
||||
export function isTrap(cardId: string): boolean {
|
||||
return cardId === "trap";
|
||||
}
|
||||
Reference in New Issue
Block a user