- 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>
89 lines
3.4 KiB
TypeScript
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();
|
|
}
|