Files
systemic-game-design-course/docs/superpowers/plans/2026-11-11-course-v2-extensions.md
T
2026-11-11 09:45:01 +01:00

21 KiB
Raw Blame History

Course v2 Extensions Implementation Plan

For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (- [ ]) syntax for tracking.

Goal: Close the course's admitted gaps, add two modules and one seam, and add the tools, levels and site page described in the spec.

Architecture: Engine changes are made in the cumulative Rust and TypeScript references, earliest module first, so later tasks copy finished engines. New tools reuse tools/play-spellgrid.mjs's staging. New modules 11 and 12 are full modules; the old Module 11 becomes 13.

Tech Stack: Rust (cargo, serde), TypeScript (vitest, tsx), Node 22+ tooling (node --test), Astro/Starlight site.

Spec: docs/superpowers/specs/2026-11-11-course-v2-extensions-design.md. Every task implements the spec sections it names; the spec's text is the requirement and this plan adds order, files and steps.

Global Constraints

Spec §1, all eight points, bind every task. In short: both tracks, every cumulative reference copy; test names identical across Rust, TS and scenarios; engine names no card or noun; the full verification list of §1.4 passes before reporting; lessons follow §1.5 (fetched sources, exact quotes of at most 125 characters, course choices labelled, counts and quoted code match the files); harness.md, catalog.json, README.md and tool tests travel with the code; commit on extend/course-v2, never push, commit trailer Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>; no subagents.

Review Focus

  1. A change made in one track or one module copy and missed in another: the differential fuzzer and verify-references must both run on the final tree of every engine task.
  2. A random level or script that crashes a CLI (out-of-range coordinates, a card not in hand, a chooser asked with no options): both tracks must print the same error line and carry on; Task 1 tests this.
  3. An existing learner test that silently changes meaning: Armor absorption (Task 3) and the nesting guard (Task 4) must not alter any existing test's expected log; if one would, stop and report.
  4. Undo after a turn that destroyed objects and created others: ids must not be reused or renumbered; Task 5's test asserts ids.
  5. A lesson that quotes a source nobody fetched, or a test count that no longer matches: reviewers check every new quotation has a URL and every count against the test files.

Task 1: Rename Module 11 to 13; differential fuzzer; fix what it finds

Implements spec §4 (first paragraph, rename only) and §2.1.

Files:

  • Move: course/11-break-your-engine/ → course/13-break-your-engine/ (title becomes "Module 13"); update every link and mention in course/**/lesson.md, README.md, mentor/SKILL.md.
  • Create: tools/diff-tracks.mjs, tools/test/diff-tracks.test.mjs
  • Modify: tools/start-module.mjs (clear message for a module with no tests), package.json (script diff:tracks), README.md
  • Modify as needed: references and tests of any module where a divergence is fixed

Interfaces:

  • Produces: node tools/diff-tracks.mjs [--seeds N] [--seed S] [--module NN] [--dir D] [--keep], exit 0 on agreement. Exports parseArgs(argv), generateCase(seed, vocab) -> { level, script } (pure, deterministic) and rng(seed) for tests. Later tasks run it as verification.

  • Step 1: git mv the module, fix links and the H1, run node tools/check-links.mjs (relative links only matter) and npm run test:site.

  • Step 2: Write tools/test/diff-tracks.test.mjs: generateCase is deterministic for a seed and differs across seeds; every generated level validates against course/schema/level.schema.json; every script line matches the CLI grammar (w|a|s|d, c <card> [x y], a chooser answer); parseArgs defaults. Run: fails (no module).

  • Step 3: Implement the tool per §2.1. Compare the two CLIs' output formats first; where the Rust and TS CLIs print the same state differently, fix the CLI (both must print identical text for identical state) and add a CLI test.

  • Step 4: Run node tools/diff-tracks.mjs --seeds 500. For each distinct divergence: minimise by hand to a scenario, decide the right behaviour (spec §2.1), write the regression test in both tracks and scenarios.md in the module that owns the behaviour, fix every affected reference copy. Repeat until 500 seeds agree. Record each divergence and its ruling in the report.

  • Step 5: Full verification (§1.4). Commit.

Task 2: Log tree

Implements spec §3.1.

Files:

  • Modify: engine/event.*, engine/queue.*, engine/world.*, src/harness.* in the references of Modules 02–10, both tracks; cli.* in Module 10
  • Modify: course/02-action-queue/tests/*, scenarios.md, lesson.md; course/03-death-step/tests/*, scenarios.md, lesson.md; course/harness.md; workspace templates' harness stubs if they list functions

Interfaces:

  • Produces: harness logTree(): string (TS) / log_tree(&self) -> String (Rust), on Game from Module 2 and on SpellGrid in Module 10. Format per §3.1; event text exactly as scenarios.md writes events. Tasks 10 and 11 consume it.

  • Step 1: Write test_02_log_tree_shows_nesting (Echo, Armorsmith, Reactor; dealDamage(armorsmith, 1)) asserting the exact string: Damaged at depth 0; Echo's Triggered at 0 with its hero Damaged at 1 and Reactor's Triggered at 1 with StatsChanged at 2; Armorsmith's Triggered at 0 with ArmorGained at 1. Write test_03_log_tree_death_phase (the chained Golem/Ghoul scenario): Destroyed and DeathPhase at 0, each Deathrattle's Triggered at 0, its effects at 1. Add both to scenarios. Run: fail.

  • Step 2: Add depth to engine events and logTree to the harness, Module 2 first, then propagate to every later copy.

  • Step 3: Module 10 CLIs print Log: and the tree of the last command's Sequences (both tracks, identical text); update the CLI tests.

  • Step 4: Update Lesson 2 (Spec table, test list and count, Stress it question 3), Lesson 3 (test list and count), harness.md.

  • Step 5: Full verification including diff-tracks --seeds 200. Commit.

Task 3: Replacement effects in Module 4

Implements spec §3.2.

Files:

  • Modify: engine/aura.*, engine/queue.*, engine/card.*, engine/event.*, src/harness.*, content, in the references of Modules 04–10, both tracks
  • Modify: course/schema/card.schema.json, course/catalog.json, course/cards/ (the redirect card, from Module 5's data format on), course/04-interceptors/tests/*, scenarios.md, lesson.md, course/02-action-queue/lesson.md, course/harness.md, tools/test/schema.test.mjs if it enumerates modifier types

Interfaces:

  • Produces: aura data { type: "damageRedirect", from: "friendlyHero", to: "self" }; GameEvent ArmorLost{player, amount}; engine list DAMAGE_STEPS = [redirect, armor] (TS readonly array / Rust const). Task 9 (combat) and Task 10 (mutant) consume these names.

  • Step 1: Fetch the Bolf Ramshield page; settle the card per §3.2 and record the quotation.

  • Step 2: Write the four tests of §3.2 with exact logs, and their scenarios. Run: fail.

  • Step 3: Implement in Module 4 (card as code there, as that module's cards are), then propagate; from Module 5 the card is a JSON file and the interpreter needs no new effect.

  • Step 4: Confirm no existing test's expected log changed. If one must, stop and report (Review Focus 3).

  • Step 5: Lesson 4: new section "A second interceptor: replacement" after Name it, test list and count, further reading; Lesson 2: the "How the real game differs" sentence about Armor and Stress it question 4.

  • Step 6: Full verification including diff-tracks. Commit.

Task 4: Robustness in Module 5

Implements spec §3.3.

Files:

  • Modify: engine/queue.*, engine/assemble.*, engine/effect.*, engine/card.*, src/harness.* in the references of Modules 05–10, both tracks
  • Modify: course/05-assemble/tests/*, scenarios.md, lesson.md; Stress it answers in course/02-action-queue/lesson.md, course/03-death-step/lesson.md; course/harness.md
  • Create: a test in tools/test/ that every shipped card is well-formed (reimplement the rule in the tool, in JS, against the JSON)

Interfaces:

  • Produces: engine wellFormed / well_formed; error text containing Death Phase limit; DEATH_PHASE_LIMIT = 64. Nothing later depends on these names except lessons.

  • Step 1: Write test_05_trigger_does_not_nest_in_itself, test_05_death_phase_limit, test_05_assemble_rejects_ill_formed_text, test_05_multi_summon_order (cards registered inline, as test_05_own_aura_misses_own_battlecry does) and scenarios. For the Bomb test, work the expected Triggered sequence out by hand in the scenario text before running anything. Run: fail (the first two crash or hang: use a timeout).

  • Step 2: Implement the four changes in Module 5, propagate.

  • Step 3: Confirm Module 10's Graft level and every existing Assemble test still offer the options their tests expect.

  • Step 4: Lessons 2, 3, 5 per §3.3; test counts.

  • Step 5: Full verification including diff-tracks. Commit.

Task 5: Undo and stackAt

Implements spec §3.4, the stackAt half of §2.3, and one leftover.

Files:

  • Modify: engine/puzzle/grid.*, engine/puzzle/level.*, src/harness.* in the references of Modules 08–10 (stackAt) and 09–10 (undo), both tracks
  • Modify: course/08-push-chains/tests/*, scenarios.md, lesson.md; course/09-turn-pipeline/tests/*, scenarios.md, lesson.md; course/harness.md
  • Fix: course/08-push-chains/lesson.md quotes try_move / tryMove with an inline comment the reference lacks: make the quote match the reference exactly

Interfaces:

  • Produces: stackAt(x, y): string[] / stack_at(&self, x, y) -> Vec<String> (kinds on the tile, oldest first) on Baba from Module 8 and on SpellGrid; undo(): boolean / undo(&mut self) -> bool on Baba from Module 9; SpellGrid's undo errors with not supported. Task 7's solver consumes stackAt.

  • Step 1: Write test_08_stack_is_age_ordered and test_09_undo_restores_everything (§3.4) and test_09_undo_walks_back_and_stops (two steps, three undos, third returns false). Scenarios. Run: fail.

  • Step 2: Implement; propagate.

  • Step 3: Lessons 8 and 9 (Spec tables, test lists and counts; Lesson 9's Stress it question 3 now compares the learner's undo with the reference's snapshot contents), harness.md, the Lesson 8 quote fix.

  • Step 4: Full verification including diff-tracks. Commit.

Task 6: Summoned minions enter the grid

Implements spec §3.5.

Files:

  • Modify: engine/spellgrid.*, engine/event.*, engine/queue.* (the summoner on the internal event), src/harness.*, src/content/words.json in the Module 10 reference, both tracks
  • Create: course/10-spellgrid/cards/clay-golem.json, clay-shard.json; catalog entries; words GOLEM, SHARD
  • Modify: course/10-spellgrid/tests/*, scenarios.md, lesson.md, course/harness.md

Interfaces:

  • Produces: cards Clay Golem (noun GOLEM) and Clay Shard (noun SHARD) available to Task 7's levels.

  • Step 1: Write test_10_summoned_minion_enters_grid and test_10_summon_without_noun_stays_off_grid with scenarios. Run: fail.

  • Step 2: Implement the seam; the order inside "a Sequence ends" is: write spawned text, place summoned minions, remove the dead, reparse, sync rule auras.

  • Step 3: Lesson 10: three seams become four everywhere they are counted (Spec, Build diagram and hints, Name it, "How the real game differs", Stress it question 3), card table, test list.

  • Step 4: Full verification including diff-tracks (the generator now sees the new cards). Commit.

Task 7: Solver upgrade, level proofs, seven new levels

Implements spec §2.3 and §3.6.

Files:

  • Modify: tools/solver/solve.ts (age-aware key via stackAt), tools/solve-spellgrid.mjs if needed
  • Create: tools/verify-levels.mjs, tools/test/verify-levels.test.mjs, course/10-spellgrid/levels/proofs.json, course/10-spellgrid/levels/l4-….json … l10-….json
  • Modify: course/10-spellgrid/tests/*, scenarios.md, lesson.md, course/schema/level.schema.json only if a level needs nothing new (it should not), package.json (script verify:levels), README.md, both CLIs' tests if they enumerate levels, tools/test/play-spellgrid.test.mjs (it lists level names)

Interfaces:

  • Produces: node tools/verify-levels.mjs exit 0 when every proof in proofs.json holds; proofs.json entries { "level": "l4-…", "shortest": N, "neverRule": "X IS Y" } or "naiveUnsolvable": true. Task 10 adds it to CI; Task 11 lists the levels.

  • Step 1: Solver key uses stackAt; re-run on l1–l3 and confirm the same results as recorded in Lesson 10's Stress it question 4 (10 inputs; exhausted without the rule; 4; 6; naive exhausted). Update the numbers in Lessons 10 and 13 if position counts changed.

  • Step 2: Write verify-levels and its test (arg parsing; a fixture proof that must fail is reported as failing). Add proofs for l1–l3.

  • Step 3: Design l4–l10, one per idea in §3.6. For each: write the level, run the solver with --all and read every solution, run it with the idea forbidden, and iterate on the layout until the proof holds and no cook exists. Keep each level's intended solution and proof in the report.

  • Step 4: test_10_level_N_solution for N = 4…10 in both tracks, scripted from the solver's shortest solution, with one assertion mid-script that shows the idea happening; scenarios; Lesson 10's level list (ideas, no solutions) and test count.

  • Step 5: Full verification, node tools/verify-levels.mjs, diff-tracks. Commit.

Task 8: Module 11, Conditions

Implements spec §4 (module shape) and §4.1.

Files:

  • Create: course/11-conditions/ with lesson.md, scenarios.md, tests/{rust,ts}/test_11.*, reference/{rust,ts}/ (copied from Module 10's final reference, then changed), expansion/ (README.md, tests/{rust,ts}/test_11x.*, solution/ with words.json and the content file that registers NEAR)
  • Modify: course/harness.md, course/catalog.json (words ON, NEAR with a condition role), tools/coupling-audit.mjs only if its word-role handling needs the new role, tools/lib.mjs/verify-references.mjs only if a module after 10 needs staging changes (levels and SpellGrid cards must still be staged for 11+)

Interfaces:

  • Consumes: Module 10's final reference (Tasks 1–7).

  • Produces: word role "condition"; content registry of condition functions (level, objectId, nouns) -> bool; rule text format BABA ON LAVA IS MELT; Rule.conditions. Task 9 copies this reference; Task 10 adds a mutant "conditions evaluated at parse time instead of at use" against it.

  • Step 1: Fetch the Baba Is You Wiki pages for ON, NEAR and Order of Operations (the course's docs/research/baba-is-you.md lists how earlier fetches were done); append what was fetched, with quotations, to that research file.

  • Step 2: Create the module skeleton and reference copy; make verify-references 11 pass with zero new tests.

  • Step 3: Write the tests of §4.1 (at least the seven listed) and scenarios. Run: fail.

  • Step 4: Implement: lexer role, parser, rule conditions, evaluation at use in hasProperty, conversion, hazards' sharing-based queries and the win check, resolution pairing, rule text.

  • Step 5: Expansion Drop NEAR: test, README scenario, solution; verify-references --expansion passes and the solution touches no engine file.

  • Step 6: Write the lesson (§4.1 theme; all beats; three hint tiers; "Decisions in the reference"; "How the real game differs"; a Stress it section with at least three questions, one of which asks which existing mutant-style change the new tests catch).

  • Step 7: harness.md, mentor/README mentions of the module list, full verification, diff-tracks --module 11. Commit.

Task 9: Module 12, Combat

Implements spec §4 (module shape) and §4.2.

Files:

  • Create: course/12-combat/ with the full module shape; reference copied from Module 11's final reference
  • Modify: course/harness.md, course/catalog.json, course/cards/ or course/12-combat/cards/ for any new card (follow how Module 10 scopes its cards so earlier modules never see them), course/01-handles/lesson.md and course/02-action-queue/lesson.md ("defender triggers first" is no longer unmodelled: point forward)

Interfaces:

  • Consumes: DAMAGE_STEPS, ArmorLost (Task 3); Module 11's reference.

  • Produces: harness attack(attacker, defender), canAttack(attacker, defender): string | null, endTurn(player) on Game; a per-queue-entry priority in react. Task 10's mutant list may add "defender priority removed".

  • Step 1: Fetch the Advanced rulebook's combat and end-of-turn passages and the Drop card's page; append to docs/research/hearthstone.md.

  • Step 2: Skeleton and reference copy; verify-references 12 green with no new tests.

  • Step 3: Write the tests of §4.2 (at least the six listed) and scenarios. Run: fail.

  • Step 4: Implement: per-target damage amounts, the attack action and its legality, queue priority, endTurn and the endOfTurn event.

  • Step 5: Expansion Drop: one card file, one test, README; content only.

  • Step 6: Lesson (§4.2 theme; all beats; hints; decisions; differences; Stress it with at least three questions).

  • Step 7: harness.md, earlier lessons' forward pointers, full verification, diff-tracks --module 12. Commit.

Task 10: Mutation check, scenario runner, designer capstone, CI, closing docs

Implements spec §2.2, §2.4, §2.5, §5.2 and the Module 13 updates.

Files:

  • Create: tools/mutation-check.mjs, tools/mutants.json, tools/test/mutation-check.test.mjs, tools/run-scenario.mjs, tools/scenario/run.ts, tools/test/run-scenario.test.mjs, course/10-spellgrid/designer-capstone.md, course/10-spellgrid/designer-example/ (cards, word, scenario.json, output.txt)
  • Modify: .gitea/workflows/ci.yml, package.json scripts (mutation:check, run:scenario), README.md (tools; Designer path), mentor/SKILL.md (module list, designer capstone), course/13-break-your-engine/lesson.md, course/10-spellgrid/lesson.md (link to the capstone)

Interfaces:

  • Consumes: logTree (Task 2), verify-levels (Task 7), diff-tracks (Task 1), final references (Task 9).

  • Step 1: mutation-check: tests first (a fixture mutant that must be killed; a stale find is an error; a surviving mutant exits 1). Implement. Fill mutants.json per §2.2 and run it; a mutant that survives gets a test in the owning module (both tracks, scenario) in this task.

  • Step 2: run-scenario: tests first (schema error is readable; merged words appear in the rules; output has the tree). Implement.

  • Step 3: Designer capstone document and worked example; output.txt is the tool's real output, regenerated in a tools test so it cannot go stale.

  • Step 4: CI jobs per §2.5; run each job's commands locally and paste the results in the report.

  • Step 5: Module 13: the mutation table matches mutants.json and names the tool; "Two engines, one specification" reports what diff-tracks found in Task 1 and how to run it; "State, undo and replay" and "Solving your own levels" match Tasks 5 and 7; the course outline in README and mentor lists Modules 11–13.

  • Step 6: Full verification, mutation-check, verify-levels, diff-tracks. Commit.

Task 11: Playable SpellGrid on the site

Implements spec §5.1.

Files:

  • Modify: site/scripts/sync-content.mjs (generate the game sources), site/scripts/sync-content.test.mjs, site/package.json if a script changes, site/.gitignore (generated directory)
  • Create: site/src/game/content.browser.ts (static imports of cards, words, seams, levels, and the reference's hazards and movement), site/src/game/play.ts (DOM glue), site/src/pages/play.astro or a Starlight page embedding it, site/scripts/game.test.mjs
  • Modify: site/astro.config.mjs (sidebar entry "Play SpellGrid"), course/10-spellgrid/lesson.md, course/11-conditions/lesson.md, course/12-combat/lesson.md (link), site/README.md

Interfaces:

  • Consumes: the Module 12 TypeScript reference's src/engine and src/harness.ts unmodified (if the harness imports ./content in a way that cannot be redirected by the bundler, the sync step may rewrite that one import line, and nothing else); logTree; all levels.

  • Step 1: site/scripts/game.test.mjs: the generated game module, loaded under Node, plays l3 with its scripted inputs to won(). Run: fail.

  • Step 2: Sync step and browser content module; test passes.

  • Step 3: The page per §5.1. Build the site, serve dist locally, and exercise the page in a headless browser if one is available (load, play l3 to the win, open the log tree, Undo, switch level, at 375 px and 1280 px, light and dark); if none is available, say so in the report and describe what was checked instead.

  • Step 4: Links from the lessons; site/README.md.

  • Step 5: npm run test:site, site build, full verification. Commit.