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:
co-authored by
Claude Opus 5.5
parent
a0cf9be214
commit
6f576940b7
@@ -48,6 +48,19 @@ export function scenarioNames(path, file = "scenarios.md") {
|
||||
return [...names].sort();
|
||||
}
|
||||
|
||||
/**
|
||||
* The card data a workspace at module `upTo` has: course/cards (every module's, from
|
||||
* Module 5), then the cards/ of each module up to `upTo` that has its own (Module 10's
|
||||
* SpellGrid cards), so earlier modules never see later modules' cards.
|
||||
*/
|
||||
export function cardDirs(upTo) {
|
||||
const shared = join(courseRoot(), "cards");
|
||||
const local = moduleDirs()
|
||||
.filter((m) => Number(m.num) <= Number(upTo))
|
||||
.map((m) => join(m.path, "cards"));
|
||||
return [shared, ...local].filter((d) => existsSync(d));
|
||||
}
|
||||
|
||||
export function loadCatalog() {
|
||||
const f = join(courseRoot(), "catalog.json");
|
||||
if (!existsSync(f)) throw new Error(`catalog not found: ${f}`);
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
// Copies module NN's tests for the learner's track into <workspace>/tests/.
|
||||
import { cpSync, existsSync, mkdirSync, readFileSync } from "node:fs";
|
||||
import { basename, join } from "node:path";
|
||||
import { moduleDirs, courseRoot, testFiles, fail } from "./lib.mjs";
|
||||
import { cardDirs, moduleDirs, courseRoot, testFiles, fail } from "./lib.mjs";
|
||||
|
||||
const [num, ws] = process.argv.slice(2);
|
||||
if (!/^\d\d$/.test(num || "") || !ws) fail("usage: start-module.mjs <NN> <workspaceDir>");
|
||||
@@ -32,13 +32,12 @@ mkdirSync(join(ws, "tests"), { recursive: true });
|
||||
for (const f of files) cpSync(f, join(ws, "tests", basename(f)));
|
||||
console.log(`Copied ${files.length} test file(s) for module ${num} into ${join(ws, "tests")}`);
|
||||
|
||||
// The course's card data (read by your content from Module 5). Refreshed on every start;
|
||||
// card files you add yourself are kept.
|
||||
const cards = join(courseRoot(), "cards");
|
||||
if (existsSync(cards)) {
|
||||
cpSync(cards, join(ws, "cards"), { recursive: true });
|
||||
console.log(`Copied the course card data into ${join(ws, "cards")}`);
|
||||
}
|
||||
// The course's card data (read by your content from Module 5): course/cards, plus the cards
|
||||
// of each module up to this one that has its own (Module 10's SpellGrid cards). Refreshed on
|
||||
// every start; card files you add yourself are kept.
|
||||
const cardSources = cardDirs(num);
|
||||
for (const cards of cardSources) cpSync(cards, join(ws, "cards"), { recursive: true });
|
||||
if (cardSources.length) console.log(`Copied the course card data into ${join(ws, "cards")}`);
|
||||
|
||||
// The module's puzzle levels (Module 10's tests load them from levels/).
|
||||
const levels = join(mod.path, "levels");
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"name": "Module Fixture",
|
||||
"type": "minion",
|
||||
"cost": 1,
|
||||
"attack": 1,
|
||||
"health": 1,
|
||||
"split": 0,
|
||||
"text": []
|
||||
}
|
||||
@@ -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`);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -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 });
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
#!/usr/bin/env node
|
||||
// For each module <= N with a reference/: copy reference + tests of modules 01..module into a temp dir, run tests.
|
||||
// course/cards (the card data, loaded by the references from Module 5) is staged as <dir>/cards,
|
||||
// and each tested module's levels/ (the puzzle levels its tests load, from Module 10) as <dir>/levels.
|
||||
// The card data (loaded by the references from Module 5) is staged as <dir>/cards: course/cards
|
||||
// plus the cards/ of each module up to the one verified. Each tested module's levels/ (the
|
||||
// puzzle levels its tests load, from Module 10) is staged as <dir>/levels.
|
||||
import { cpSync, existsSync, mkdirSync, mkdtempSync, rmSync } from "node:fs";
|
||||
import { spawnSync } from "node:child_process";
|
||||
import { tmpdir } from "node:os";
|
||||
import { basename, join } from "node:path";
|
||||
import { courseRoot, moduleDirs, testFiles, fail } from "./lib.mjs";
|
||||
import { cardDirs, moduleDirs, testFiles, fail } from "./lib.mjs";
|
||||
|
||||
// A staged reference has no node_modules, so TS installs the reference's own
|
||||
// pinned devDependencies first (a bare `npx vitest` would fetch an unrelated version).
|
||||
@@ -38,8 +39,7 @@ export function stage(plan, lang) {
|
||||
for (const f of testFiles(t.path, lang)) cpSync(f, join(dir, "tests", basename(f)));
|
||||
if (existsSync(join(t.path, "levels"))) cpSync(join(t.path, "levels"), join(dir, "levels"), { recursive: true });
|
||||
}
|
||||
const cards = join(courseRoot(), "cards");
|
||||
if (existsSync(cards)) cpSync(cards, join(dir, "cards"), { recursive: true });
|
||||
for (const cards of cardDirs(plan.num)) cpSync(cards, join(dir, "cards"), { recursive: true });
|
||||
return dir;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user