Files
systemic-game-design-course/tools/test/play-spellgrid.test.mjs
T
Lucas WintherandClaude Opus 5.5 570e5d6826 feat: age-aware solver, verify-levels, seven solver-proved SpellGrid levels (l4-l10)
The solver's position key reads stackAt (each tile oldest first). New tool
tools/verify-levels.mjs checks course/10-spellgrid/levels/proofs.json: each
level's shortest solution, and that it exhausts without its idea (neverRule)
or on the naive engine. Levels 4-10, one course idea each, with
test_10_level_N_solution in both tracks, scenarios and Lesson 10's list;
l1 no longer matches l10, and proofs.json is not listed as a level.

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

50 lines
2.6 KiB
JavaScript

import { test } from "node:test";
import assert from "node:assert/strict";
import { existsSync, mkdtempSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { run, toolsDir } from "./helpers.mjs";
import { levelNames, parseArgs, resolveLevel } from "../play-spellgrid.mjs";
test("play_spellgrid_parses_args", () => {
assert.deepEqual(parseArgs(["rust"]), { lang: "rust", level: "l1", dir: null, stageOnly: false });
assert.deepEqual(parseArgs(["ts", "l2", "--dir", "x", "--stage-only"]), { lang: "ts", level: "l2", dir: "x", stageOnly: true });
});
test("play_spellgrid_stages_reference_cards_and_levels", () => {
// Uses the real course: the Module 10 Rust reference with every card it reads and the levels.
const dir = join(mkdtempSync(join(tmpdir(), "sdc-play-")), "ws");
const r = run("play-spellgrid.mjs", ["rust", "l3", "--dir", dir, "--stage-only"]);
assert.equal(r.code, 0, r.out);
for (const f of ["Cargo.toml", "src/cli.rs", "cards/whirlwind.json", "cards/powder-keg.json", "levels/l1-double-tap.json"]) {
assert.ok(existsSync(join(dir, f)), f);
}
assert.match(r.out, /cargo run --quiet --bin spellgrid -- levels\/l3-simultaneous-paradox\.json/);
// Re-staging the same directory is allowed.
assert.equal(run("play-spellgrid.mjs", ["rust", "--dir", dir, "--stage-only"]).code, 0);
});
test("play_spellgrid_refuses_unknown_level_and_foreign_dir", () => {
const foreign = mkdtempSync(join(tmpdir(), "sdc-play-"));
writeFileSync(join(foreign, "mine.txt"), "keep me");
const r = run("play-spellgrid.mjs", ["rust", "--dir", foreign, "--stage-only"]);
assert.equal(r.code, 1);
assert.match(r.out, /not empty/);
const bad = run("play-spellgrid.mjs", ["rust", "l99", "--dir", join(foreign, "ws"), "--stage-only"]);
assert.equal(bad.code, 1);
assert.match(
bad.out,
/no level "l99"\. Levels: l1-double-tap, l2-spliced-corpse, l3-simultaneous-paradox, l4-crossed-out, l5-sealed-keg, l6-melt-before-defeat, l7-clay-key, l8-nothing-dies, l9-skull-river, l10-friendly-fire\n/,
);
assert.equal(run("play-spellgrid.mjs", ["java"]).code, 1);
});
test("play_spellgrid_resolves_levels_by_number", () => {
// l1 is level 1, not level 10; proofs.json beside the levels is not a level.
const levels = join(toolsDir, "..", "course", "10-spellgrid", "levels");
assert.equal(resolveLevel(levels, "l1").path, join("levels", "l1-double-tap.json"));
assert.equal(resolveLevel(levels, "l10").path, join("levels", "l10-friendly-fire.json"));
assert.equal(resolveLevel(levels, "proofs"), null);
assert.ok(!levelNames(levels).includes("proofs"));
});