Files
systemic-game-design-course/tools/start-module.mjs
T
Lucas WintherandClaude Opus 5.5 06a1b34c5d feat: Module 11 becomes 13; diff-tracks fuzzer; align the two reference tracks
- Rename course/11-break-your-engine to 13-break-your-engine and update every
  link and mention; start-module explains that Module 13 has no tests to start.
- tools/diff-tracks.mjs (npm run diff:tracks): stages both references of a
  module, generates a seeded random SpellGrid level and script per seed, pipes
  it to the Rust and TypeScript CLIs and diffs their output; crashes on one or
  both sides fail. stagePlay takes a module number.
- register_assembled: the TypeScript track compared only the text of an
  existing Assembled card, so a base re-registered with a new cost (or a
  different split) silently rewrote the earlier card. It now compares every
  field the engine reads, as Rust does (Modules 5-10).
  New test_05_assemble_never_rewrites_an_earlier_card.
- SpellGrid CLIs: coordinates and chooser numbers are 1-9 plain digits in both
  tracks (Rust accepted signs, TypeScript accepted "+2", "0x2", "1e0"); Rust
  quotes card names verbatim instead of with Debug escapes; Rust joins the
  minion list with "; " instead of rewriting ", ". Two new CLI tests per track.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-11 10:08:56 +01:00

64 lines
3.1 KiB
JavaScript

#!/usr/bin/env node
// 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 { cardDirs, moduleDirs, courseRoot, testFiles, fail, readCourseConfig } from "./lib.mjs";
const [num, ws] = process.argv.slice(2);
if (!/^\d\d$/.test(num || "") || !ws) fail("usage: start-module.mjs <NN> <workspaceDir>");
if (num === "00") fail("Module 00 is not startable: M0 uses the naive-engine/ project, not a workspace.");
const cfg = readCourseConfig(ws);
const track = cfg.track;
if (track !== "rust" && track !== "ts") fail(`bad track in .course.json: ${track}`);
const mods = moduleDirs();
const mod = mods.find((m) => m.num === num);
if (!mod) fail(`no module ${num} in ${courseRoot()}`);
if (!testFiles(mod.path, "rust").length && !testFiles(mod.path, "ts").length) {
fail(`Module ${num} has no tests to start: it is made of reading and exercises. Open ${join(mod.path, "lesson.md")}.`);
}
if (num !== "01") {
const prevNum = String(Number(num) - 1).padStart(2, "0");
const prev = mods.find((m) => m.num === prevNum);
if (prev) {
const missing = testFiles(prev.path, track).map((f) => basename(f))
.filter((f) => !existsSync(join(ws, "tests", f)));
if (missing.length) fail(`Cannot start module ${num}: finish module ${prevNum} first. Missing tests/${missing.join(", tests/")}`);
}
}
const files = testFiles(mod.path, track);
if (!files.length) fail(`module ${num} has no ${track} tests`);
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): course/cards from Module 5 on
// (Modules 1-4 keep their cards in your own code), 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");
if (existsSync(levels)) {
cpSync(levels, join(ws, "levels"), { recursive: true });
console.log(`Copied the module's levels into ${join(ws, "levels")}`);
}
// Harness additions: module-local note, else the "- M<N>:" line of course/harness.md.
const note = join(mod.path, "harness-adds.md");
let adds = existsSync(note) ? readFileSync(note, "utf8").trim() : "";
if (!adds) {
const h = join(courseRoot(), "harness.md");
if (existsSync(h)) {
const re = new RegExp(`^\\s*[-*]\\s*\\**M${Number(num)}\\b.*$`, "m");
adds = (re.exec(readFileSync(h, "utf8")) || [""])[0].trim();
}
}
console.log(adds ? `Harness functions added by module ${num}:\n${adds}` : `No harness additions recorded for module ${num} (see course/harness.md).`);