Files
systemic-game-design-course/tools/scenario/run.ts
T
Lucas WintherandClaude Opus 5.5 78862d1a1f feat: mutation check, scenario runner and designer capstone; CI jobs; Module 13 brought up to date
- tools/mutation-check.mjs with tools/mutants.json: thirteen mutants against the
  Module 12 TypeScript reference (the eight of Module 13's first table, plus
  Armor before redirect, conditions at parse time, no defender priority, and the
  summon seam's noun check); all killed.
- tools/run-scenario.mjs with tools/scenario/run.ts: a designer's cards, words
  and seam words as data, played on the Module 10 TypeScript reference.
- course/10-spellgrid/designer-capstone.md and designer-example/ (output.txt
  is checked against the tool's output in the tools tests).
- CI: diff-tracks (300 seeds), mutation-check, verify-levels.
- Module 13: the mutation table is the tool's output; differential testing,
  undo and the solver sections describe the repository as it is. Lesson 10
  says what the level proofs prove for levels 6, 8 and 9.
- schema.mjs: a value that matches no oneOf branch is reported against the
  branch its const fields pick.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-11 13:58:45 +01:00

89 lines
3.4 KiB
TypeScript

// Play a designer's scenario on a staged SpellGrid reference (tools/run-scenario.mjs copies
// this file into the staged directory and runs it there with tsx).
//
// npx tsx run.ts <level.json> <inputs.json>
//
// The inputs are command-line commands, as `src/cli.ts` reads them: `w a s d`, `c <card> [x y]`,
// and the answers to Assemble's questions, one input each. After the start and after each
// input it prints the input, the Part I log of that input as an indented tree, the board, the
// rules, the minions, the hand and both heroes' Health. No rules live here: it calls the
// harness, as the command line does, and draws with the command line's `render`.
import { readFileSync } from "node:fs";
import { render } from "./src/cli";
import { newSpellGrid, type LevelDef } from "./src/harness";
const DIRS = { w: "up", a: "left", s: "down", d: "right" } as const;
/** A coordinate or a chooser's number, as the command line reads one. */
const NUMBER = /^\d{1,9}$/;
const level = JSON.parse(readFileSync(process.argv[2], "utf8")) as LevelDef;
const inputs = JSON.parse(readFileSync(process.argv[3], "utf8")) as string[];
let next = 0;
const out = (s: string) => process.stdout.write(`${s}\n`);
/** The next input, echoed after its prompt; undefined when there are none left. */
function read(prompt: string): string | undefined {
const line = inputs[next];
if (line === undefined) return undefined;
next++;
out(`${prompt}${line}`);
return line;
}
/** The level, or a readable error: an unknown card, a card with no noun on the grid, ... */
function start() {
try {
return newSpellGrid(level, []);
} catch (e) {
process.stderr.write(`The level does not start: ${e instanceof Error ? e.message : String(e)}\n`);
process.exit(1);
}
}
const g = start();
g.setChooser((kind, options) => {
out(`Choose the ${kind}:\n${options.map((o, i) => ` ${i + 1}) ${o}`).join("\n")}`);
const answer = read("? ")?.trim() ?? "";
if (options.includes(answer)) return answer;
return (NUMBER.test(answer) ? options[Number(answer) - 1] : undefined) ?? options[0];
});
const show = () => out(`${render(g, level)}\nHeroes: ${g.health(g.hero(0))} Health (yours), ${g.health(g.hero(1))} Health (enemy)`);
out("== start");
show();
for (;;) {
if (g.won() || g.lost()) {
out(g.won() ? "You win!" : "Nothing is YOU. You lose.");
const left = inputs.length - next;
if (left > 0) out(`${left} input${left === 1 ? "" : "s"} not run: the level is over.`);
break;
}
out("");
const line = read("> ");
if (line === undefined) {
out("(end of inputs)");
break;
}
const [command, ...args] = line.trim().split(/\s+/);
g.clearLog();
try {
if (command in DIRS) g.step(DIRS[command as keyof typeof DIRS]);
else if (command === "c" && args.length > 0) {
const n = args.length;
const at = n >= 3 && NUMBER.test(args[n - 2]) && NUMBER.test(args[n - 1]) ? ([Number(args[n - 2]), Number(args[n - 1])] as const) : undefined;
const card = (at === undefined ? args : args.slice(0, n - 2)).join(" ");
const target = at === undefined ? null : g.minionAt(...at);
if (at === undefined) g.cast(card);
else if (target !== null) g.cast(card, target);
else g.play(card, ...at);
} else out("Commands: w a s d (move), c <card> [x y] (cast or play)");
} catch (e) {
out(`Error: ${e instanceof Error ? e.message : String(e)}`);
}
const tree = g.logTree();
if (tree !== "") out(`Log:\n${tree.trimEnd()}`);
show();
}