fix: module 10 review round 1 (module-local SpellGrid cards, cast target check, CLI without panic hook)

- SpellGrid cards move to course/10-spellgrid/cards; start-module and
  verify-references stage course/cards plus module-local cards up to the module,
  so M5-M9 workspaces get exactly the pre-M10 card set.
- cast() rejects a chosen-target spell without a target (checkCast/checkPlay);
  new test_10_targeted_spell_needs_target.
- Rust CLI pre-validates instead of swapping the global panic hook.
- Catalog: CRIER noun; DEATHRATTLE/BATTLECRY note on lexicon vs catalog role.
- Lesson: design note on mid-movement reparse.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
Lucas Winther
2026-10-29 05:11:09 +01:00
co-authored by Claude Opus 5.5
parent a0cf9be214
commit 6f576940b7
34 changed files with 295 additions and 79 deletions
@@ -0,0 +1,9 @@
{
"name": "Module Fixture",
"type": "minion",
"cost": 1,
"attack": 1,
"health": 1,
"split": 0,
"text": []
}
+15 -3
View File
@@ -55,14 +55,17 @@ test("card_schema_accepts_fixtures", () => {
}
});
// Card data the course ships: course/cards (every card the references load) and the
// Card data the course ships: course/cards (every module's cards), module-local cards in
// course/NN-*/cards (Module 10's SpellGrid cards, staged from that module on), and the
// Expansion Drop answers in course/NN-*/expansion/solution/cards.
function courseCardFiles() {
const courseDir = join(here, "../../course");
const dirs = [join(courseDir, "cards")];
for (const m of readdirSync(courseDir).filter((d) => /^\d\d-/.test(d))) {
const d = join(courseDir, m, "expansion/solution/cards");
if (existsSync(d)) dirs.push(d);
for (const sub of ["cards", "expansion/solution/cards"]) {
const d = join(courseDir, m, sub);
if (existsSync(d)) dirs.push(d);
}
}
return dirs.flatMap((d) => readdirSync(d).filter((f) => f.endsWith(".json")).map((f) => join(d, f)));
}
@@ -141,3 +144,12 @@ test("course_levels_valid", () => {
for (const card of level.hand ?? []) assert.ok(cards.has(card), `${f}: no card data for ${card}`);
}
});
test("spellgrid_cards_are_module_local", () => {
// course/cards reaches every workspace from Module 5; SpellGrid's cards must not reach
// Modules 5-9 (their spawnText effect and their place in Assemble pools are Module 10's).
const shared = join(here, "../../course/cards");
for (const f of readdirSync(shared).filter((f) => f.endsWith(".json"))) {
assert.notEqual(json(join(shared, f)).game, "spellgrid", `${f} belongs in course/10-spellgrid/cards`);
}
});
+14
View File
@@ -21,6 +21,20 @@ test("start_module_copies_course_cards", () => {
assert.ok(existsSync(join(ws, "cards/test-fixture.json")));
});
test("start_module_copies_module_cards_up_to_the_module", () => {
const early = copyFixture("ws-start");
assert.equal(run("start-module.mjs", ["01", early], env).code, 0);
assert.ok(existsSync(join(early, "cards/test-fixture.json")));
assert.ok(!existsSync(join(early, "cards/module-fixture.json")));
const later = copyFixture("ws-start");
mkdirSync(join(later, "tests"), { recursive: true });
writeFileSync(join(later, "tests/m02.rs"), "");
const r = run("start-module.mjs", ["03", later], env);
assert.equal(r.code, 0, r.out);
assert.ok(existsSync(join(later, "cards/test-fixture.json")));
assert.ok(existsSync(join(later, "cards/module-fixture.json")));
});
test("start_module_copies_module_levels", () => {
const ws = copyFixture("ws-start");
mkdirSync(join(ws, "tests"), { recursive: true });
+15
View File
@@ -76,3 +76,18 @@ test("verify_stage_copies_module_levels", async () => {
assert.ok(existsSync(join(stage(plans[2], "ts"), "levels", "fixture-level.json")));
assert.ok(!existsSync(join(stage(plans[1], "ts"), "levels")));
});
test("verify_stage_copies_module_cards_up_to_the_module", async () => {
// Module-local cards (Module 10's SpellGrid cards) reach that module and later ones only.
process.env.COURSE_ROOT = root;
const { planVerification, stage } = await import("../verify-references.mjs");
const { existsSync } = await import("node:fs");
const plans = planVerification(moduleDirs(), 3);
for (const lang of ["rust", "ts"]) {
assert.ok(!existsSync(join(stage(plans[0], lang), "cards", "module-fixture.json")), lang);
assert.ok(existsSync(join(stage(plans[1], lang), "cards", "module-fixture.json")), lang);
const later = stage(plans[2], lang);
assert.ok(existsSync(join(later, "cards", "module-fixture.json")), lang);
assert.ok(existsSync(join(later, "cards", "test-fixture.json")), lang);
}
});