Add course design draft and sourced research notes
Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
@@ -0,0 +1,240 @@
|
||||
# Systemic Game Design Course — Design
|
||||
|
||||
Status: **first draft for discussion** (2026-10-28)
|
||||
Repo: https://gitea.lucaswinther.info/lucasw89/systemic-game-design-course
|
||||
Research notes backing every game-rule claim: `docs/research/` (hearthstone.md, baba-is-you.md, architecture-readings.md)
|
||||
|
||||
Markers used below: **[decided]** = said by the course owner. **[my call]** = chosen while drafting; open to change in review. **[builder]** = left to whoever builds it.
|
||||
|
||||
---
|
||||
|
||||
## 1. Intent
|
||||
|
||||
A course that teaches how to build **systemic game rules that stay extensible**: new mechanics and interactions slot in as new pieces instead of forcing a rewrite. **[decided]**
|
||||
|
||||
The motivating failure: Deathrattles coded so tightly to their logic that adding "Deathrattles trigger twice" means rewriting the system. The real world provides both sides of that story, and the course uses them:
|
||||
|
||||
- *Before:* Fireplace (Python Hearthstone sim) implements Baron Rivendare as a hard-coded `if controller.extra_deathrattles:` branch inside its `Deathrattle` action. SabberStone (C#) sets one tag in the aura and reads a different tag in the Deathrattle task; its Rivendare test is skipped and, from reading the code, the doubling appears broken.
|
||||
- *After:* Argentum (Kotlin MTG engine) expresses Panharmonicon ("triggers an additional time") as pure data (`AdditionalETBOrLTBTriggers`) consumed by one generic routine in the trigger detector.
|
||||
|
||||
**Who it's for.** The course owner first, then anyone who finds it on git or a website. **[decided]** Assumed audience **[my call]**: programmers comfortable in Rust *or* TypeScript who play games; no prior engine experience required.
|
||||
|
||||
**What success looks like.** A graduate can look at a new mechanic and say which pattern it belongs to, and can add it to their engine by adding content, not by editing the engine core. **[decided, phrased by me]**
|
||||
|
||||
## 2. Goals, non-goals, anti-goals
|
||||
|
||||
**Goals**
|
||||
- Teach a small set of transferable patterns (§3) through two real games as case studies: **Hearthstone** and **Baba Is You**, then fuse them in a capstone. Case studies are central, not decoration — learners' love of the games is the hook. **[decided]**
|
||||
- Balance **reverse engineering and teaching**: each module starts from a real, observable behavior of the game and works backward to the architecture that explains it. **[decided]**
|
||||
- Two ways to take it, from one set of material: **solo** (read, run provided failing tests, compare to reference solution) and **AI-mentored** (an agent skill that teaches Socratically from the same files). **[decided]**
|
||||
- Two language tracks, **Rust and TypeScript, both from the start**, kept in sync. **[decided]**
|
||||
- Every module ships failing tests and a reference solution that passes them, in both languages. **[decided]**
|
||||
- Publishable as a git repo and as a website. **[decided]**
|
||||
|
||||
**Non-goals**
|
||||
- Not a faithful Hearthstone or Baba Is You clone. We model the slice of rules each lesson needs.
|
||||
- Not graphics, networking, AI opponents, or game feel. Output is rules engines driven by tests (plus a minimal playable CLI for the capstone).
|
||||
- "Small" is not a constraint. **[decided]**
|
||||
|
||||
**Anti-goals** (would make it a failure even if it technically works)
|
||||
- **Teaching false facts about the games.** Every claim about real game behavior cites a source we read; any deliberate deviation is marked as a simplification in the lesson.
|
||||
- **Tests that dictate internal architecture.** If the tests force one specific data layout, learners copy instead of designing. Tests observe behavior through a thin harness (§6.2).
|
||||
- **Material that drifts between paths or languages.** One lesson source; mentor reads the same files; Rust and TS tests are generated from or checked against one shared scenario spec (§6.3).
|
||||
- **A mentor that writes the learner's code.** In mentor mode the agent never writes the implementation.
|
||||
|
||||
## 3. The patterns (the spine of the course)
|
||||
|
||||
| # | Pattern | One-line idea | Taught in |
|
||||
|---|---------|---------------|-----------|
|
||||
| P1 | Handles, not references | Everything is an `EntityId`; order is data (order of play). | M1 |
|
||||
| P2 | Request ≠ resolution | Effects are queued actions, resolved by one loop in a defined order (depth-first). | M2 |
|
||||
| P3 | Deferred, state-based checks | "Is it dead?" is asked at defined checkpoints, not at the moment HP hits 0. | M3 |
|
||||
| P4 | Interception / modifier pipelines | Rules that bend other rules ("trigger twice") are data in a pipeline the engine consults, with defined timing. | M4 |
|
||||
| P5 | Behavior as data | Abilities are inspectable, splittable data (an AST), not closures. | M5 |
|
||||
| P6 | Rules derived from state | Active rules are recompiled from the world, then resolved (negation, locks) in a post-pass. | M6, M7 |
|
||||
| P7 | Validate-then-commit | Multi-object changes are checked as a whole before anything mutates. | M8 |
|
||||
| P8 | Ordered phase pipelines | A turn is a fixed sequence of phases with re-derivation between them. | M9 |
|
||||
| — | Integration | Two engines communicating only through these seams. | M10 |
|
||||
|
||||
The **Zero-Coupling Law** (from the drafts, kept): no card, keyword, or rule implementation may reference the identity of another specific card or keyword. Interactions emerge from queues, pipelines, state checks, data composition and compiled rules.
|
||||
|
||||
## 4. How every module is shaped
|
||||
|
||||
Each module follows the same seven beats. **[my call — reshaped from the drafts' Socratic → RED → GREEN → Refactor loop]**
|
||||
|
||||
1. **Observe.** A concrete, sourced scenario from the real game ("Cast Twisting Nether with Baron Rivendare and Loot Hoarder on board: you draw one card, not two"). Includes the citation.
|
||||
2. **Predict.** One or two Socratic questions. Solo learners write their answer down before opening the collapsible discussion.
|
||||
3. **Spec (RED).** The module's provided tests are added to the learner's workspace and fail.
|
||||
4. **Build (GREEN).** Learner implements. Hints are tiered (nudge → concept → pseudo-code), collapsible in the lesson; the mentor releases them one tier at a time.
|
||||
5. **Name it & audit.** The pattern is named and linked to readings. The learner runs the coupling audit (§6.5).
|
||||
6. **Expansion Drop.** A *new* card or rule arrives that uses the module's pattern (e.g. after M4, Brann Bronzebeard: "Your Battlecries trigger twice"). It must be added **without editing engine-core files** — only content. A provided check verifies this. This is the course's direct measure of its core goal. **[my call]**
|
||||
7. **Further reading.** Primary sources and reference engine pointers.
|
||||
|
||||
Each module's lesson also has a **"How the real game differs"** box wherever we simplify.
|
||||
|
||||
## 5. Curriculum
|
||||
|
||||
Reshaped from drafts 1–5 using the research findings. **[my call, owner free-to-reshape]** Each module lists: scenario (Observe), test scenarios, Expansion Drop, readings. Exact test values are **[builder]** within these constraints; game facts must match `docs/research/`.
|
||||
|
||||
### Module 0 — The Rewrite Trap (orientation)
|
||||
- Pick track; scaffold workspace; run the test runner.
|
||||
- Learner receives a deliberately **naive engine** (provided, ~100 lines per language): minions with HP, damage applied immediately, Deathrattles called recursively the instant HP ≤ 0.
|
||||
- Task: add Baron Rivendare. Learner experiences the coupling (the fix wants to live inside Deathrattle), and sees the naive engine fail the provided "Whirlwind + two deathrattle minions" test in a confusing order.
|
||||
- Read: Fireplace's `Deathrattle` branch and SabberStone's tag mismatch as real-world versions of the trap.
|
||||
- No reference solution "fixes" M0 — it is thrown away. The learner's real engine starts in M1.
|
||||
|
||||
### Part I — Hearthstone: the event engine
|
||||
|
||||
**M1 — Handles and order of play (P1)**
|
||||
- Observe: two copies of the same minion trigger in the order they were played, not board position.
|
||||
- Tests: summon A then B; IDs stable; order-of-play increases; zones (Deck, Hand, Board, Graveyard) correct; an entity moved to Graveyard keeps its ID and is not returned by board queries.
|
||||
- Expansion Drop: a "Hand" → "Board" play action reusing zone moves.
|
||||
- Read: Wagar (framing); Argentum `architecture-principles.md` §2.2.
|
||||
|
||||
**M2 — The action queue, depth-first (P2)**
|
||||
- Observe (sourced): AoE damage is dealt to all targets first, then damage triggers resolve in order of play; Acolyte of Pain draws even after being mortally wounded. Triggers resolve depth-first: one trigger's consequences finish before the next sibling.
|
||||
- Tests: (a) Whirlwind with Armorsmith + Acolyte → event log shows all damage, then triggers in play order; (b) a nested chain where trigger A's damage triggers C, and C resolves before sibling B.
|
||||
- Expansion Drop: Frothing Berserker ("Whenever a minion takes damage, gain +1 Attack").
|
||||
- How the real game differs: Redemption-last, Poisonous priority, defender-first combat triggers are out of scope (named, not modeled).
|
||||
- Read: Liquid Fire *Action System* (note: sorts by priority then order of play); Nystrom *Event Queue*; BasicMod *Action Queue* (Slay the Spire) as a second real example.
|
||||
|
||||
**M3 — Deferred death (P3)**
|
||||
- Observe (sourced): a mortally wounded minion stays in play until the outermost Phase ends; then the Death Creation Step removes all mortally wounded *and pending-destroy* minions **simultaneously**; a Death Phase resolves their Deathrattles in order of play; this repeats until no new deaths.
|
||||
- Tests: chained death phases (A's Deathrattle damage kills B, B dies in the next round and summons in its old position); "destroy" effects marking pending-destroy.
|
||||
- Expansion Drop: a "Destroy a minion" spell and a second Deathrattle card, content only.
|
||||
- Read: Hearthstone Advanced Rulebook (wiki.gg) — Sequences, Phases, Death Creation Step; MTG CR 704 (state-based actions) as the parallel; Fireplace `game.py` `action_end`/`process_deaths`.
|
||||
|
||||
**M4 — Interceptors and "trigger twice" (P4)**
|
||||
- Observe (sourced): Baron Rivendare (real stats 1/7) doubles your Deathrattles. If Baron dies *at the same time*, the Deathrattle is **not** doubled. The precise reason: at the end of the Phase the game runs Aura Update (Health/Attack) → Death Creation Step → Aura Update (Other); Baron's effect is an "Other" aura and is gone before Deathrattles resolve. Trigger multipliers are checked before an Event starts; the Deathrattle queues once and resolves twice in a row.
|
||||
- Tests: (A) Baron alive, Loot Hoarder dies → 2 draws; (B) Twisting Nether ("Destroy all minions") with Baron + Loot Hoarder → 1 draw (real cards, uses pending-destroy from M3); (C) two Barons → still 2 draws: "Rivendare's ability is not duplicated or multiplied by having more than one Baron Rivendare in play" (wiki.gg). Contrast in the lesson: MTG's Panharmonicon *is* additive (Argentum: "N copies add N extra copies") — stacking policy is a property of the modifier, i.e. data.
|
||||
- The drafts' "1-HP Baron dies to a 1-damage AoE" is replaced by Twisting Nether, which is real. **[my call]**
|
||||
- Expansion Drop: Brann Bronzebeard (Battlecries trigger twice) or Drakkari Enchanter (end-of-turn effects twice) — one modifier, no engine edit.
|
||||
- Read: Advanced Rulebook (auras section); MTG CR 603.2d (triggers an additional time), 613 (layers), 614 (replacement effects) from the official Wizards rules; Argentum Panharmonicon.kt + `TriggerDetector.duplicateETBOrLTBTriggers` as the "after" picture.
|
||||
|
||||
**M5 — Behavior as data and Assemble (P5)**
|
||||
- Observe (sourced): Assemble (Reign of the Black Empire, patch 37.0, Oct 2026): "Get a random minion. Change half its text." You get a random base minion and are shown two others; you replace the base minion's top **or** bottom half of its text with the corresponding half from one of them. The base keeps cost, stats, name and type; its text must change.
|
||||
- Course model: each card's text is stored as an ability AST plus a designer-authored split point dividing it into a top and bottom half. In our example cards the split falls between trigger and effect, but the engine must not assume that — halves are arbitrary AST fragments. **[my call; the wiki does not define "half" precisely — lesson says so]**
|
||||
- Prerequisite refactor: abilities written as closures in M1–M4 are converted to data. The pain of that refactor *is* the lesson.
|
||||
- Tests: (a) round-trip: every M1–M4 card expressed as data behaves identically (regression suite reused); (b) assemble a minion whose bottom half "Deathrattle: | Summon two 1/1s" becomes "Deathrattle: | Deal 2 damage to all other minions"; with Baron alive, other minions take 4; (c) assembled card keeps base cost/stats/name; (d) assembling must change text (no-op swap rejected).
|
||||
- Random selection is out of the engine tests (deterministic seeded RNG or injected choice). **[my call]**
|
||||
- Expansion Drop: a new Assemble card (e.g. Fleshshape: "Assemble a Deathrattle minion…") using a pool filter.
|
||||
- Read: Nystrom *Bytecode* (framed as "behavior as data", not "build a VM"); Argentum §1.1–1.2; Forge card-scripting wiki; Hearthstone wiki *Assemble*.
|
||||
|
||||
### Part II — Baba Is You: rules derived from state
|
||||
|
||||
Bridge lesson (short): in Part I rules lived inside cards; in Part II objects have no behavior at all and every rule is compiled from the board each time it changes.
|
||||
|
||||
**M6 — Grid, lexer and parser (P6)**
|
||||
- Observe (sourced): rows and columns both parse, so sentences can share an IS; AND distributes over subjects and complements; stacked text produces every possible rule; TEXT IS PUSH is a default rule not shown in the pause menu.
|
||||
- Tests: crossword (`BABA IS YOU` horizontal sharing IS with `FLAG IS WIN` vertical); `BABA AND ROCK IS PUSH`; stacked nouns on one tile; default `TEXT IS PUSH` present.
|
||||
- Expansion Drop: a new property word (e.g. `HOT`) with no parser change.
|
||||
- Read: baba-is-auto `Game::ParseRules` and rule-language doc (as rules-as-data example, not authority); Baba wiki rule pages.
|
||||
|
||||
**M7 — Resolving rules: NOT, identity, meta-nouns (P6, echoes P4)**
|
||||
- Observe (sourced): `X IS NOT Y` overrides `X IS Y`; `X IS X` stops X converting into anything; `X IS NOT X` deletes X and cancels the lock; `TEXT IS NOT PUSH` disables the default.
|
||||
- Tests: those four behaviours; property queries through a single `has_property(entity, prop)`.
|
||||
- Fixes the drafts' "X IS NOT X locks" error.
|
||||
- Expansion Drop: `ALL` meta-noun.
|
||||
- Read: Teikari's Game Developer article (quotes re-verified before publishing); Baba wiki NOT/TEXT pages; MTG 613 as the layered-resolution parallel.
|
||||
|
||||
**M8 — Push chains: validate then commit (P7)**
|
||||
- Observe (sourced): a push chain that hits STOP doesn't move at all, including YOU; OPEN/SHUT destroys both objects during movement, including pushed objects, overriding STOP.
|
||||
- Tests: 3-object chain moves together; same chain into STOP → nothing moves; PUSH+OPEN key pushed into STOP+SHUT door → both destroyed, Baba moves one tile (into the key's former tile). This landing tile is inferred from the documented rules, not shown by a source: the lesson marks it as inferred, and the builder confirms it in-game if possible.
|
||||
- Expansion Drop: `WEAK` or `PULL`.
|
||||
- Read: Baba wiki PUSH/STOP/OPEN/SHUT; esolangs page.
|
||||
|
||||
**M9 — The turn pipeline (P8)**
|
||||
- Observe (sourced): the real turn is movement → parse → conversions → parse → second movement (FOLLOW, BACK, TELE, SHIFT) → FALL → parse → status → FALL → parse → level checks → hazards → WIN → parse. The course implements a **labelled simplification**: Movement → Parse → Conversion → Parse → Hazards → Win, with the full order shown.
|
||||
- Tests: (1) BABA IS YOU/MELT, LAVA IS HOT; Baba steps onto lava while pushing MELT out of its sentence → survives; (2) a conversion completed mid-turn (`BABA IS ROCK` formed, all Babas become rocks the same turn; `BABA IS BABA` blocks it); (3) hazards only between same FLOAT level; SINK destroys overlapping objects and itself; (4) YOU+WIN wins; DEFEAT beats WIN.
|
||||
- Fixes the drafts' unfinished Scenario 2.
|
||||
- Expansion Drop: `SINK` or `FLOAT` added as a new phase rule only.
|
||||
- Read: Baba wiki Order of Operations; baba-is-auto `Game::MovePlayer` (reparse between phases).
|
||||
|
||||
### Part III — Capstone: SpellGrid (M10)
|
||||
|
||||
A small original game fusing both engines: minions with HP/Deathrattles live on a grid alongside word blocks. SpellGrid is **our** game, so its cards may differ from Hearthstone's (e.g. a 1-HP Baron-like card) — labelled as such. **[my call]**
|
||||
|
||||
Bridges (each is a test):
|
||||
1. **Spatial rules feed the interceptor pipeline.** `DEATHRATTLE IS DOUBLE` compiles into an M4 modifier; `DEATHRATTLE IS NOT DOUBLE` cancels it via M7, even with a Baron-like minion on board.
|
||||
2. **Spatial hazards feed the death step.** A minion destroyed by HOT/MELT or SINK is marked pending-destroy and processed by the M3 Death Creation Step.
|
||||
3. **Effects that change the grid trigger reparsing.** A Deathrattle that spawns a word block forces a reparse after the sequence.
|
||||
|
||||
Integration seam rule: the two engines talk only through (a) compiled rules → modifiers, (b) pending-destroy marks, (c) "grid changed" → reparse. **[my call]**
|
||||
|
||||
Puzzle levels (playable in a minimal CLI, and each one an integration test), reworked from Draft 5:
|
||||
- **L1 "Double-Tap"**: push DOUBLE to form `DEATHRATTLE IS DOUBLE`, Whirlwind a deterministic Deathrattle minion (fixed damage, not Boom Bot's random 1–4) whose doubled effect clears the way.
|
||||
- **L2 "Spliced Corpse"**: Assemble a Deathrattle "spawn WIN text" minion, push it into lava, WIN text completes `BABA IS WIN`.
|
||||
- **L3 "Simultaneous Paradox"**: a Baron-like card and a Deathrattle minion die together; the puzzle is only solvable if the Deathrattle fires once (correct aura timing). A naive engine produces a different board.
|
||||
- Exact layouts **[builder]**, each must be verified solvable by its test and *unsolvable* (or differently resolved) under the naive behavior it targets.
|
||||
|
||||
Closing synthesis question (kept from Draft 3): compare how Baron modifies Deathrattle (M4) with how `BABA IS NOT YOU` modifies `BABA IS YOU` (M7).
|
||||
|
||||
## 6. Structure and mechanics
|
||||
|
||||
### 6.1 Repo layout **[my call; builder may adjust names]**
|
||||
```
|
||||
course/
|
||||
00-rewrite-trap/ ... 10-spellgrid/
|
||||
lesson.md # the single source: Observe, Predict, hints, pattern, readings
|
||||
scenarios.md # language-neutral test scenarios (what each test asserts)
|
||||
tests/rust/ tests/ts/
|
||||
reference/rust/ reference/ts/ # full cumulative engine as of this module
|
||||
expansion/ # the Expansion Drop card + its tests
|
||||
naive-engine/rust/ naive-engine/ts/ # Module 0
|
||||
workspace-template/rust/ workspace-template/ts/
|
||||
mentor/SKILL.md (+ supporting files)
|
||||
site/ # website config; content read from course/
|
||||
tools/ # coupling audit, expansion check, test-sync check
|
||||
docs/research/ # sourced research notes (kept, public)
|
||||
```
|
||||
|
||||
### 6.2 Tests observe behavior through a harness
|
||||
- Each module defines a small **harness interface** (functions the learner implements: e.g. `new_game`, `summon`, `cast`, `deal_damage`, `query`, `event_log`). Tests call only the harness. The harness grows module by module; each lesson lists what it adds.
|
||||
- Primary assertion surface is an **event log** of named events in order, plus queries. Internal layout is the learner's.
|
||||
- Rust: tests for a module are copied into the workspace when the learner starts it (later-module tests would not compile against an unfinished harness). TS: same, for consistency. Mechanism **[builder]** (a small script or documented copy step).
|
||||
|
||||
### 6.2a Who owns the card format **[my call]**
|
||||
- **M1–M4:** cards are defined in the learner's own code (in their `content/` folder) via the harness; the course gives card text and behavior, not a data format. Learners are free to (and many will) write closures — that sets up M5.
|
||||
- **M5 onward:** the course defines a **language-neutral card and level format** (JSON, with a schema in `course/schema/`): ability AST nodes, top/bottom split point, and from M6 grid level layouts. Learners write a loader. Rationale: Assemble, Expansion Drops and capstone puzzles all need content the course can ship identically to both tracks, and "build an engine that runs this data" is exactly the P5 lesson. The engine's internal representation stays the learner's; only the input format is fixed.
|
||||
- Expansion Drops from M5 on are delivered as data files plus tests; before M5 they are card text plus tests.
|
||||
|
||||
### 6.3 Two languages, one truth
|
||||
- `scenarios.md` per module is the canonical statement of every test. Rust and TS tests implement it one-to-one, with matching test names.
|
||||
- `tools/` includes a check that both languages have the same test names per module and that both reference solutions pass. Runs in CI. **[my call]**
|
||||
|
||||
### 6.4 Reference solutions
|
||||
- Cumulative: `reference/<lang>/` in module N is the whole engine through module N, so a learner can diff module N-1 → N. Duplication across modules accepted for browsability. **[my call]**
|
||||
- Reference solutions must themselves pass the coupling audit and every Expansion Drop check — they are the proof the course's claims hold.
|
||||
|
||||
### 6.5 Audits
|
||||
- **Coupling audit:** flags engine-core code that names specific cards/keywords (string or identifier match against the content registry). Learner keeps `engine/` and `content/` separate from M1 onward.
|
||||
- **Expansion check:** after an Expansion Drop, confirms only `content/` (and tests) changed. Implemented over `git diff` against the module's starting commit. **[my call]**
|
||||
|
||||
### 6.6 Solo path
|
||||
Read `lesson.md` on the site or GitHub-style viewer; Predict answers in a learner notes file; hints and discussion collapsible; compare with reference after green.
|
||||
|
||||
### 6.7 Mentor path
|
||||
- `mentor/SKILL.md` is an Agent Skill (Claude Code compatible). It holds **how to teach**, never curriculum content — it reads `course/NN/lesson.md` and `scenarios.md`.
|
||||
- Rules (from drafts, kept): one module at a time; ask Predict questions and wait; add the tests and have the learner confirm RED; never write the implementation; release hints one tier at a time; run the audits and reject coupling; run the Expansion Drop.
|
||||
- Tracks progress in a file in the learner's workspace (current module, beat, hints used).
|
||||
|
||||
### 6.8 Website
|
||||
- Static docs site generated from `course/**/lesson.md` (no copied content). Rust/TS code shown in tabs. **[my call: Astro Starlight]**
|
||||
- Hosting **[open — see §8]**.
|
||||
|
||||
## 7. Accuracy policy
|
||||
- Every "the real game does X" statement links to a source recorded in `docs/research/` with a quote.
|
||||
- Simplifications are flagged in a "How the real game differs" box.
|
||||
- Unverified sources are excluded: the Patashu reddit post and both Assemble reddit threads couldn't be fetched (reddit blocked); the Medium ECS post 403'd. They're out unless the owner verifies them manually.
|
||||
- Replacements: MTG Fandom pages → official Wizards Comprehensive Rules; Liquid Fire *Drawing Cards* → dropped (Unity view code) in favor of *Damage*/other relevant parts; fandom Hearthstone rulebook → hearthstone.wiki.gg.
|
||||
- Teikari article quotes and GDC talk speaker attribution are re-verified before publish.
|
||||
- Third-party code (Fireplace, SabberStone, Argentum) is linked, not copied; Argentum's licence is unclear.
|
||||
|
||||
## 8. Open questions (for the owner)
|
||||
1. **Hosting**: where the website is served and which CI runs the checks (Gitea Actions vs a GitHub mirror with Actions/Pages). Decides the CI and deploy workstream.
|
||||
2. **Licence and IP stance**: licence for prose vs code; free vs possibly monetised; quote real card text verbatim vs paraphrase. Decides LICENSE files, attribution/disclaimer page and how lessons are written.
|
||||
|
||||
## 9. Left to the builder
|
||||
- Exact numeric test values, card stats for non-real cards, puzzle grid layouts (within §5 constraints).
|
||||
- Harness function names and exact event names (consistent across languages).
|
||||
- Test-copy mechanism, CI setup, site theme.
|
||||
Reference in New Issue
Block a user