Files
systemic-game-design-course/tools/lib.mjs
T
Lucas WintherandClaude Opus 5.5 6f576940b7 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]>
2026-10-29 05:11:09 +01:00

74 lines
2.6 KiB
JavaScript

import { existsSync, readdirSync, readFileSync, statSync } from "node:fs";
import { dirname, join, resolve } from "node:path";
import { fileURLToPath } from "node:url";
const here = dirname(fileURLToPath(import.meta.url));
/** Course directory; override with COURSE_ROOT (used by tests). */
export function courseRoot() {
return process.env.COURSE_ROOT
? resolve(process.env.COURSE_ROOT)
: join(here, "..", "course");
}
/** Module directories `course/NN-slug/`, sorted by number. */
export function moduleDirs() {
const root = courseRoot();
if (!existsSync(root)) return [];
return readdirSync(root)
.map((name) => ({ name, m: /^(\d\d)-(.+)$/.exec(name) }))
.filter(({ name, m }) => m && statSync(join(root, name)).isDirectory())
.map(({ name, m }) => ({ num: m[1], slug: m[2], path: join(root, name) }))
.sort((a, b) => a.num.localeCompare(b.num));
}
export function testFiles(path, lang) {
const dir = join(path, "tests", lang === "rust" ? "rust" : "ts");
if (!existsSync(dir)) return [];
const ext = lang === "rust" ? /\.rs$/ : /\.test\.ts$/;
return readdirSync(dir).filter((f) => ext.test(f)).sort().map((f) => join(dir, f));
}
/** Test names declared by a module's tests for `lang` ("rust" | "ts"). */
export function testNames(path, lang) {
const re = lang === "rust" ? /fn\s+(test_\w+)/g : /test\(\s*["'](test_\w+)/g;
const names = new Set();
for (const f of testFiles(path, lang)) {
for (const m of readFileSync(f, "utf8").matchAll(re)) names.add(m[1]);
}
return [...names].sort();
}
/** `### test_…` headings in a module's scenarios.md (or another file in `path`). */
export function scenarioNames(path, file = "scenarios.md") {
const f = join(path, file);
if (!existsSync(f)) return [];
const names = new Set();
for (const m of readFileSync(f, "utf8").matchAll(/^### (test_\w+)/gm)) names.add(m[1]);
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}`);
return JSON.parse(readFileSync(f, "utf8"));
}
export function fail(msg) {
console.error(msg);
process.exit(1);
}