feat: Stress it beat for Modules 6-10, Module 11 (Break your own engine), level solver tool

- lessons 8 and 9 updated for the once-per-move rule, per-object negated
  conversion and the new ordering tests
- tools/solve-spellgrid.mjs: breadth-first search over a SpellGrid level
  with the TypeScript reference; --never-rule and --naive
- README, harness.md and the mentor skill know the new beat and module

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
This commit is contained in:
Lucas WintherandClaude Fable 5.2 committed 2026-11-11 09:32:22 +01:00
1 parent e907f17937
commit 4cb0f3a61f
13 files changed
+771 -19

No files matched your search

+55
View File
@@ -0,0 +1,55 @@
#!/usr/bin/env node
// Search a SpellGrid level (Module 10) for solutions, by brute force, with the course's
// TypeScript reference engine. Used in Module 11 to check that a level has the solution
// its designer meant and no other.
//
// node tools/solve-spellgrid.mjs [level] [--dir <dir>] [--max N] [--naive] [--never-rule "X IS Y"] [--all]
// npm run solve:spellgrid -- l1 --never-rule "DEATHRATTLE IS DOUBLE"
//
// Stages the Module 10 TypeScript reference with its cards and levels, as play-spellgrid
// does, copies tools/solver/solve.ts beside it and runs it there. `level` is a level of
// course/10-spellgrid/levels by name or prefix (`l1`), or a path to a level file; default `l1`.
// The other options are the solver's: see tools/solver/solve.ts.
import { cpSync, existsSync, readdirSync } from "node:fs";
import { spawnSync } from "node:child_process";
import { basename, dirname, join, resolve } from "node:path";
import { fileURLToPath } from "node:url";
import { fail, isMain } from "./lib.mjs";
import { resolveLevel, stagePlay } from "./play-spellgrid.mjs";
const here = dirname(fileURLToPath(import.meta.url));
const WITH_VALUE = ["--max", "--never-rule"];
const SWITCHES = ["--naive", "--all"];
export function parseArgs(argv) {
const opts = { level: "l1", dir: null, solver: [] };
const rest = [];
for (let i = 0; i < argv.length; i++) {
if (argv[i] === "--dir") opts.dir = argv[++i];
else if (WITH_VALUE.includes(argv[i])) opts.solver.push(argv[i], argv[++i]);
else if (SWITCHES.includes(argv[i])) opts.solver.push(argv[i]);
else rest.push(argv[i]);
}
[opts.level = "l1"] = rest;
return opts;
}
function main() {
const opts = parseArgs(process.argv.slice(2));
const dir = resolve(stagePlay("ts", opts.dir));
const level = resolveLevel(join(dir, "levels"), opts.level);
if (!level) {
const names = readdirSync(join(dir, "levels")).filter((f) => f.endsWith(".json")).map((f) => basename(f, ".json"));
fail(`no level "${opts.level}". Levels: ${names.join(", ")}`);
}
cpSync(join(here, "solver", "solve.ts"), join(dir, "solve.ts"));
if (!existsSync(join(dir, "node_modules"))) {
console.error(`Installing the TypeScript reference's dependencies in ${dir} ...`);
const r = spawnSync("npm", ["install", "--no-audit", "--no-fund", "--loglevel=error"], { cwd: dir, stdio: ["ignore", "inherit", "inherit"] });
if (r.status !== 0) fail("npm install failed");
}
const r = spawnSync("npx", ["tsx", "solve.ts", level.path, ...opts.solver], { cwd: dir, stdio: "inherit" });
process.exit(r.status ?? 1);
}
if (isMain(import.meta.url)) main();