Engine events carry the nesting depth they were recorded at; the harness prints the log as an indented tree. New tests test_02_log_tree_shows_nesting and test_03_log_tree_death_phase; both SpellGrid CLIs print the tree of the last command's Sequences under a Log: line. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
139 lines
5.4 KiB
TypeScript
139 lines
5.4 KiB
TypeScript
// Test harness: the thin adapter the course tests call. It translates each
|
|
// call into engine operations and holds no rules of its own.
|
|
|
|
import { cards } from "./content";
|
|
import {
|
|
assemble,
|
|
CardLibrary,
|
|
registerAssembled,
|
|
runSequence,
|
|
summon,
|
|
triggerLabel,
|
|
World,
|
|
type CardDef,
|
|
type Chooser,
|
|
type Event,
|
|
type Half,
|
|
type Zone,
|
|
} from "./engine";
|
|
|
|
export { AssembleError } from "./engine";
|
|
export type { CardDef, Chooser, Half, Zone };
|
|
|
|
export type Id = number;
|
|
export type Player = 0 | 1;
|
|
|
|
/** The events the tests read from `log()` (see course/harness.md). */
|
|
export type GameEvent =
|
|
| { type: "Summoned"; id: Id }
|
|
| { type: "Damaged"; target: Id; amount: number }
|
|
| { type: "Triggered"; source: Id; trigger: string }
|
|
| { type: "Destroyed"; id: Id }
|
|
| { type: "DeathPhase"; round: number }
|
|
| { type: "Drew"; player: Player; id: Id }
|
|
| { type: "Fatigue"; player: Player }
|
|
| { type: "ArmorGained"; player: Player; amount: number }
|
|
| { type: "StatsChanged"; id: Id; attack: number; health: number }
|
|
| { type: "SpellCast"; card: string };
|
|
|
|
/** The course's view of an engine event; `undefined` for engine events the course log doesn't show. */
|
|
function toGameEvent(event: Event): GameEvent | undefined {
|
|
if (event.type === "Triggered") return { type: "Triggered", source: event.source, trigger: triggerLabel(event.on) };
|
|
if (event.type === "Destroyed") return { type: "Destroyed", id: event.id };
|
|
// A played minion's `Summoned` already shows it entering the board.
|
|
if (event.type === "Played") return undefined;
|
|
return { ...event };
|
|
}
|
|
|
|
/** One event as `scenarios.md` writes it, such as `Triggered{source: 3, trigger: "deathrattle"}`. */
|
|
function eventText(event: GameEvent): string {
|
|
const { type, ...fields } = event;
|
|
const shown = Object.entries(fields).map(
|
|
([key, value]) => `${key}: ${typeof value === "string" ? JSON.stringify(value) : value}`,
|
|
);
|
|
return `${type}{${shown.join(", ")}}`;
|
|
}
|
|
|
|
/** The log as a tree: one event per line, two spaces of indent per level of nesting. */
|
|
function logTree(world: World, view: (event: Event) => GameEvent | undefined): string {
|
|
const depths = world.logDepths();
|
|
return world
|
|
.log()
|
|
.map((event, i) => {
|
|
const shown = view(event);
|
|
return shown === undefined ? "" : `${" ".repeat(depths[i])}${eventText(shown)}\n`;
|
|
})
|
|
.join("");
|
|
}
|
|
|
|
export interface Game {
|
|
summon(card: string, player?: Player): Id; // to right end of board
|
|
addToHand(card: string, player?: Player): Id;
|
|
addToDeck(card: string, player?: Player): Id;
|
|
play(handId: Id): void; // Module 1 Expansion Drop; required from Module 4 (Battlecry)
|
|
zoneOf(id: Id): Zone;
|
|
board(player: Player): Id[]; // left to right
|
|
hand(player: Player): Id[];
|
|
orderOfPlay(id: Id): number;
|
|
nameOf(id: Id): string;
|
|
// Module 2
|
|
hero(player: Player): Id;
|
|
dealDamage(target: Id, amount: number): void; // one Sequence
|
|
cast(card: string, player?: Player, target?: Id): void;
|
|
health(id: Id): number;
|
|
attack(id: Id): number;
|
|
armor(player: Player): number;
|
|
log(): GameEvent[];
|
|
clearLog(): void;
|
|
logTree(): string; // the log as an indented tree
|
|
// Module 3
|
|
destroy(target: Id): void; // marks pending destroy, one Sequence
|
|
// Module 5
|
|
registerCards(defs: CardDef[]): void; // add card definitions, replacing any with the same name
|
|
assemble(base: string, donor: string, half: Half): string; // registers "<base> (Assembled)"; throws AssembleError
|
|
cost(id: Id): number;
|
|
setChooser(choose: Chooser): void; // makes Assemble's choices: kind "base" | "donor" | "half"
|
|
}
|
|
|
|
export function newGame(opts?: { decks?: [string[], string[]] }): Game {
|
|
const world = new World(new CardLibrary(cards()));
|
|
opts?.decks?.forEach((deck, player) => {
|
|
for (const card of deck) world.create(card, player as Player, "deck");
|
|
});
|
|
|
|
return {
|
|
summon: (card, player = 0) => summon(world, card, player),
|
|
addToHand: (card, player = 0) => world.create(card, player, "hand"),
|
|
addToDeck: (card, player = 0) => world.create(card, player, "deck"),
|
|
// Play a card from hand, as one Sequence: it enters the right end of the board, then its Battlecry resolves.
|
|
play: (handId) => runSequence(world, { type: "play", id: handId }),
|
|
zoneOf: (id) => world.zoneOf(id),
|
|
board: (player) => [...world.zone(player, "board")],
|
|
hand: (player) => [...world.zone(player, "hand")],
|
|
orderOfPlay: (id) => {
|
|
const order = world.orderOfPlay(id);
|
|
if (order === undefined) throw new Error(`entity ${id} has not entered play`);
|
|
return order;
|
|
},
|
|
nameOf: (id) => world.cardName(id),
|
|
|
|
hero: (player) => world.hero(player),
|
|
dealDamage: (target, amount) => runSequence(world, { type: "damage", targets: [target], amount }),
|
|
cast: (card, player = 0, target) => runSequence(world, { type: "cast", card, player, target }),
|
|
health: (id) => world.health(id),
|
|
attack: (id) => world.attack(id),
|
|
armor: (player) => world.armor(player),
|
|
log: () => world.log().flatMap((event) => toGameEvent(event) ?? []),
|
|
clearLog: () => world.clearLog(),
|
|
logTree: () => logTree(world, toGameEvent),
|
|
|
|
destroy: (target) => runSequence(world, { type: "destroy", targets: [target] }),
|
|
|
|
registerCards: (defs) => defs.forEach((def) => world.register(def)),
|
|
assemble: (base, donor, half) =>
|
|
registerAssembled(world, assemble(world.card(base).def, world.card(donor).def, half)),
|
|
cost: (id) => world.cost(id),
|
|
setChooser: (choose) => world.setChooser(choose),
|
|
};
|
|
}
|