feat: log tree from Module 2 (logTree / log_tree); SpellGrid CLIs print Log:
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>
This commit is contained in:
1 parent
06a1b34c5d
commit
97c64ebee8
86 files changed
+1375
-55
No files matched your search
@@ -1,7 +1,8 @@
|
||||
// SpellGrid on the command line: `npx tsx src/cli.ts levels/l1-double-tap.json`.
|
||||
//
|
||||
// A thin loop over the harness: it draws the level, reads a command, and calls
|
||||
// `step`, `cast` or `play`. No rules live here.
|
||||
// `step`, `cast` or `play`. No rules live here. After a command that ran Part I
|
||||
// Sequences, it prints their events under `Log:` as an indented tree.
|
||||
//
|
||||
// w a s d one turn: up, left, down, right
|
||||
// c <card> [x y] cast a spell from your hand (at the minion on (x, y), if given),
|
||||
@@ -87,6 +88,8 @@ export function run(levelText: string, readLine: () => string | undefined, write
|
||||
const line = readLine();
|
||||
if (line === undefined) return "quit";
|
||||
const [command, ...args] = line.trim().split(/\s+/);
|
||||
// The log holds only what this command does.
|
||||
g.clearLog();
|
||||
try {
|
||||
if (command === "q") return "quit";
|
||||
if (command in DIRS) g.step(DIRS[command as keyof typeof DIRS]);
|
||||
@@ -100,6 +103,8 @@ export function run(levelText: string, readLine: () => string | undefined, write
|
||||
} catch (e) {
|
||||
write(`${e instanceof Error ? e.message : String(e)}\n`);
|
||||
}
|
||||
const tree = g.logTree();
|
||||
if (tree !== "") write(`Log:\n${tree}`);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
// What happened, in order. The engine records an event each time it changes the game.
|
||||
// The world also keeps the nesting depth each event was recorded at (`World.logDepths`).
|
||||
|
||||
import type { TriggerOn } from "./card";
|
||||
import type { EntityId, PlayerId } from "./world";
|
||||
|
||||
@@ -223,7 +223,10 @@ function react(world: World, events: Event[]): void {
|
||||
event.type === "Destroyed" && event.id === source
|
||||
? positionLeftOf(world, world.ownerOf(source), event.before)
|
||||
: world.boardIndex(source);
|
||||
// The trigger's consequences are one level deeper in the log tree.
|
||||
world.openBlock();
|
||||
runEffects(world, trigger.effects, new Ctx(world, world.ownerOf(source), source, undefined, position));
|
||||
world.closeBlock();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -47,6 +47,10 @@ export class World {
|
||||
private readonly sides: [Side, Side];
|
||||
private nextOrderOfPlay = 0;
|
||||
private events: Event[] = [];
|
||||
/** The nesting depth at which each event in `events` was recorded, index for index. */
|
||||
private depths: number[] = [];
|
||||
/** How many triggers are resolving right now, one inside another. */
|
||||
private depth = 0;
|
||||
/** The auras in force as of the last Aura Update. Changes only there (see `aura.ts`). */
|
||||
private auras: ActiveAura[] = [];
|
||||
/** Until a chooser is set, take the first option. */
|
||||
@@ -235,10 +239,26 @@ export class World {
|
||||
return this.events;
|
||||
}
|
||||
|
||||
/** The nesting depth of each event in `log()`, index for index: 0 for the outermost action's own events. */
|
||||
logDepths(): readonly number[] {
|
||||
return this.depths;
|
||||
}
|
||||
|
||||
// ---- Changes: each one records its event ----
|
||||
|
||||
record(event: Event): void {
|
||||
this.events.push(event);
|
||||
this.depths.push(this.depth);
|
||||
}
|
||||
|
||||
/** A trigger starts resolving: the events it causes are recorded one level deeper. */
|
||||
openBlock(): void {
|
||||
this.depth++;
|
||||
}
|
||||
|
||||
/** The trigger that last opened a block has finished resolving. */
|
||||
closeBlock(): void {
|
||||
this.depth--;
|
||||
}
|
||||
|
||||
/** Add a card to the library, or replace the card with the same name. */
|
||||
@@ -281,6 +301,7 @@ export class World {
|
||||
|
||||
clearLog(): void {
|
||||
this.events = [];
|
||||
this.depths = [];
|
||||
}
|
||||
|
||||
damage(target: EntityId, amount: number): void {
|
||||
|
||||
@@ -50,6 +50,27 @@ function toGameEvent(event: Event): GameEvent | 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;
|
||||
@@ -69,6 +90,7 @@ export interface Game {
|
||||
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
|
||||
@@ -108,6 +130,7 @@ export function newGame(opts?: { decks?: [string[], string[]] }): Game {
|
||||
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] }),
|
||||
|
||||
@@ -185,6 +208,7 @@ export interface SpellGrid extends Baba {
|
||||
hand(): string[]; // card names in player 0's hand, in order
|
||||
log(): GameEvent[]; // Part I's log, with grid ids
|
||||
clearLog(): void;
|
||||
logTree(): string; // Part I's log as an indented tree, with grid ids
|
||||
setChooser(choose: Chooser): void; // makes Assemble's choices
|
||||
withNaiveAuraTiming(): void; // test hook: Aura Update before the Death Creation Step
|
||||
// For the command line: why `cast`/`play` would fail, or null if they wouldn't
|
||||
@@ -246,6 +270,11 @@ export function newSpellGrid(level: puzzle.LevelDef, extra: CardDef[]): SpellGri
|
||||
.flatMap((event) => toGameEvent(event) ?? [])
|
||||
.map(withIds),
|
||||
clearLog: () => g.world.clearLog(),
|
||||
logTree: () =>
|
||||
logTree(g.world, (event) => {
|
||||
const shown = toGameEvent(event);
|
||||
return shown === undefined ? undefined : withIds(shown);
|
||||
}),
|
||||
setChooser: (choose) => g.world.setChooser(choose),
|
||||
// The naive engine reads the auras before the dead leave play: the same steps, reversed.
|
||||
withNaiveAuraTiming: () => g.world.setEndOfPhase([...END_OF_PHASE].reverse()),
|
||||
|
||||
Reference in new issue
Block a user