commit 98fd6e431b00a3f740dc59c6dcc9d9dc67cee033 Author: Lucas Winther Date: Wed Oct 28 23:44:08 2026 +0100 Add course design draft and sourced research notes Co-Authored-By: Claude Opus 5.5 diff --git a/docs/research/architecture-readings.md b/docs/research/architecture-readings.md new file mode 100644 index 0000000..7e13239 --- /dev/null +++ b/docs/research/architecture-readings.md @@ -0,0 +1,275 @@ +# Architecture readings: verified source assessment + +Research date: 2026-10-28. Every source below was fetched live (WebFetch) or, for repositories, shallow-cloned and read at the commit noted. Nothing was summarized from memory. Quotes are verbatim fragments as returned by the fetch tool, which caps quotes at about 125 characters, so long passages appear as fragments. + +Course patterns referenced: +**[ID]** ID handles instead of references · **[EQ]** event/action queues · **[SBA]** deferred state-based checks (death processing) · **[INT]** interceptor/modifier pipelines (replacement effects, "trigger twice", auras/layers) · **[DATA]** abilities as data/ASTs · **[COMP]** rule compilation (rebuilding derived rules from state) + +--- + +## Summary table + +| # | Source | Loaded? | Verdict | Best for | +|---|--------|---------|---------|----------| +| 1 | Nystrom, *Event Queue* | Yes | **Keep** | EQ | +| 2 | Nystrom, *Bytecode* | Yes | **Keep** | DATA | +| 3 | Liquid Fire, *Make a CCG – Action System* | Yes | **Keep**, but fix the draft's description | EQ, SBA | +| 4 | Liquid Fire, *Make a CCG – Drawing Cards* | Yes | **Drop**, or keep only as optional follow-on | EQ (weak) | +| 5 | Liquid Fire series index | Yes (category pages) | Keep: list the relevant parts | — | +| 6 | Wagar, *Card Game Design as Systems Architecture* | Yes | **Keep** as a conceptual or optional reading | framing | +| 7 | Medium `@casual.evil52` "Events in ECS" | **No** (HTTP 403; archive unreachable; no search hits) | **Drop**: can't be verified | — | +| 8a | MTG Fandom *Layer* | **No** (HTTP 402) | **Replace** with the official CR text | INT | +| 8b | MTG CR 704 (state-based actions) | Yes, from the official Wizards TXT | **Keep** (official URL) | SBA | +| 8c | MTG CR 614/616 (replacement effects), 603.2d | Yes, from the official Wizards TXT | **Keep** (official URL) | INT | +| 9a | phase-rs/phase | Yes | Keep as an "advanced reference", not a reading | INT, SBA, DATA | +| 9b | jleclanche/fireplace | Yes | **Keep**: the best Rivendare example | EQ, SBA, INT (counter-example) | +| 9c | wingedsheep/argentum-engine | Yes | **Keep**: its architecture doc is the best single reading on DATA/INT | DATA, INT, SBA, ID | +| 9d | utilForever/baba-is-auto | Yes | **Keep** | COMP | +| 9e | HearthSim/SabberStone | Yes | Keep as a cautionary example | SBA, INT (counter-example) | +| 10a | Hearthstone Wiki *Advanced rulebook* | Yes | **Add**: the primary spec behind Liquid Fire and fireplace | SBA, EQ, INT | +| 10b | Forge wiki: *Card scripting API* / *Creating a custom Card* | Yes | **Add** | DATA | +| 10c | BasicMod wiki: *Action Queue* (Slay the Spire) | Yes | **Add** as a short practical example | EQ | + +--- + +## 1. Game Programming Patterns: Event Queue + +- **URL:** https://gameprogrammingpatterns.com/event-queue.html +- **Loaded:** yes +- **Author / date:** Robert Nystrom, *Game Programming Patterns*, Decoupling Patterns section (footer © 2009–2021) +- **Content:** The chapter builds the pattern through an audio-engine example. A blocking `playSound()` is replaced by a FIFO queue, implemented as a ring buffer. The chapter covers aggregating duplicate requests, threading, and design choices: what goes in the queue, single-cast vs. broadcast vs. work queue, one writer vs. many, and object ownership. It ends with the hazards: feedback loops and stale world state. +- **Key quotes:** + - "This decouples the sender from the receiver both statically and in time." + - "You only need a queue when you want to decouple something in time." + - "you have to be careful not to assume the current state of the world reflects how the world was" + - "A common rule to avoid this is to avoid sending events from within code that's handling one." + - "With a queue, the asynchrony unwinds the stack, so the game may keep running." +- **Pattern:** EQ. The stale-state warning also motivates **ID** handles and snapshotting event data, which is why a Deathrattle needs "last known information". +- **Verdict: keep.** It's canonical and free online. Note one tension: Nystrom's rule against "sending events from within code that's handling one" is the opposite of what a CCG needs, since there reactions enqueue reactions. Teach it as the general rule and present the card-game queue (Liquid Fire, Hearthstone rulebook) as a controlled exception with explicit ordering and depth. + +## 2. Game Programming Patterns: Bytecode + +- **URL:** https://gameprogrammingpatterns.com/bytecode.html +- **Loaded:** yes +- **Author:** Robert Nystrom +- **Content:** The motivating example is a spell system for a magic fighting game. Behavior is moved into data so it can be edited and reloaded without recompiling. The chapter contrasts the Interpreter pattern (a tree of expression objects, i.e., an AST) with a compact stack-based bytecode VM, and covers instruction design, sandboxing, and the need for authoring tools. +- **Key quotes:** + - "Give behavior the flexibility of data by encoding it as instructions for a virtual machine." + - "We could define these spells in code, but that means an engineer has to be involved every time one is modified." + - "There's a reason most programming languages in wide use aren't based on the Interpreter pattern." + - "The bytecode can't do anything malicious or reach out into weird parts of the game engine because we've only defined a few instructions that touch the rest of the game." +- **Pattern:** DATA. +- **Verdict: keep, with framing.** The course teaches abilities as **ASTs**, which is the Interpreter-pattern side Nystrom treats as the slower baseline before moving to bytecode. For card games, AST interpretation is the right trade-off: the ASTs are small and inspectable (see argentum §1.2). Tell students to read the chapter for the "behavior as data" argument and the sandboxing point, not as advice to build a VM. + +## 3. The Liquid Fire: Make a CCG – Action System + +- **URL:** https://theliquidfire.com/2017/09/11/make-a-ccg-action-system/ +- **Loaded:** yes +- **Author:** the post byline reads "admin". The site's About page (https://theliquidfire.com/about/) names the author as **Jon Parham** ("Jon Parham is of Bottle Rocket's original iOS developers; he's been here since 2010"), and the series repo is `bitbucket.org/thejon/blogccg`. **"Jon Parham" is correct**, though the page itself only shows "admin". +- **Date:** September 11, 2017 +- **Content:** A C#/Unity action system for a Hearthstone-like game. The post says it "was based on the documentation in the advanced rulebook" (the Hearthstone Advanced Rulebook). Its pieces are a `GameAction` base class (fields `id`, `player`, `priority`, `orderOfPlay`, `isCanceled`, plus `prepare` and `perform` `Phase`s), a `Phase` class (`owner`, `handler`, `viewer`), and an `ActionSystem` (`Perform`, `AddReaction`, `Sequence`, `MainPhase`, `ReactPhase`, `EventPhase`, `SortActions`). Reactions are collected in `openReactions`, sorted, and resolved recursively. After the root action finishes, a `deathReaperNotification` loop repeats while new reactions keep appearing. +- **Key quotes:** + - "All actions are performed as a sequence of phases" + - "All reactions occur based on the order of play of the responding cards." + - "All reactions are resolved in a depth-first order (children nodes before sibling nodes)." + - "other loops occur such as the death reaper event to take care of any mortally wounded cards" + - `SortActions`: "if (x.priority != y.priority) {" / "return y.priority.CompareTo(x.priority);" / "} else {" / "return x.orderOfPlay.CompareTo(y.orderOfPlay);" + - Loop: "} while (repeats == true && reactions.Count > 0);" +- **Draft check: mostly accurate, with corrections.** + - "Prepare vs Perform phases": **correct**. Each action has `prepare` and `perform` phases with will/did notifications. There is no separate "validate" phase or notification in this post; cancellation is an `isCanceled` flag checked before a phase runs. + - "sorting triggers by play_order_id": **inaccurate wording.** The field is `orderOfPlay`, and the sort is primarily by **`priority` (descending)**, with `orderOfPlay` (ascending) only as the tie-breaker. The post declares `orderOfPlay` but never shows where it is assigned. + - "depth-first tree resolution": **correct**, and stated verbatim as a design rule. + - **Omitted by the draft:** the post also includes a **death reaper loop**, a deferred death check after the root action that repeats until no new reactions appear. That makes it a reading for SBA too, not just EQ. +- **Pattern:** EQ (primary) and SBA (death reaper loop). +- **Verdict: keep.** It's the most readable worked implementation of a Hearthstone-style reaction tree. Fix the description as above. + +## 4. The Liquid Fire: Make a CCG – Drawing Cards + +- **URL:** http://theliquidfire.com/2017/10/02/make-a-ccg-drawing-cards/ (served over https) +- **Loaded:** yes +- **Author / date:** "admin" (Jon Parham), October 2, 2017 +- **Content:** Implements `DrawCardsAction` as a reaction to the change-turn action, handled in `PlayerSystem`, plus a random-draw list extension, `DrawCardsView`, and `HandView` animation. Fatigue is mentioned as a possible penalty and is actually implemented in the later Damage post. +- **Key quotes:** "All reactions are actually just actions that are played as a triggered event"; "It could be that the player takes some sort of penalty, such as fatigue damage." +- **Pattern:** EQ (an example of an action triggering a reaction). +- **Verdict: drop from the required list.** Most of the post is Unity view and animation code. If you want one follow-on to the Action System post, **Damage** (2017-10-23) is better: its `VictorySystem` runs a deferred game-over check from `OnCompleteAllActions`, quoting "the Victory System was not implemented as an observer". That fits SBA directly. + +## 5. The Liquid Fire "Make a CCG" series index + +- **Index URLs:** https://theliquidfire.com/category/ccg/ and https://theliquidfire.com/category/ccg/page/2/ (the Intro post has no full index; it only links prev/next). Repo: https://bitbucket.org/thejon/blogccg/src/master/ +- All 19 parts in order: Intro (2017-08-21), Aspect Container & Test Runner (08-28), Data Modeling (09-04), **Action System (09-11)**, Changing Turns (09-18), Action Viewer (09-25), Drawing Cards (10-02), Drawing Exceptions (10-16), **Damage (10-23)**, Playing Cards (10-30), Summon Minions (11-06), Mana (11-13), Prepare to Attack (12-04), Attack (12-11), Enemy A.I. (12-26), Taunt (2018-01-08), **Targeting (01-29)**, **Spells & Abilities (02-05)**, **JSON (02-19)**. +- **Relevant parts by topic:** + - *Action system:* **Action System** (09-11). Supporting posts: Changing Turns, Action Viewer. + - *Death / reaper:* the reaper **loop** is in **Action System**. **Damage** (10-23) adds a deferred hero-death / game-over check. **No post implements minion death or graveyard removal**: I checked Damage and Attack, and neither does. Any draft claim of a "death system" post is wrong. + - *Abilities / data-driven cards:* **Spells & Abilities** (2018-02-05) adds `Ability`, `AbilitySystem`, and target selectors (`ManualTarget`, `RandomTarget`, `AllTarget`). Abilities name a `GameAction` class by string (`actionName`, instantiated via `Type.GetType`) and carry `userInfo` config. Cards are still built in code at this stage. **JSON** (2018-02-19) moves card and ability definitions into JSON files (`DemoCards.txt`) loaded via `Load` methods. The author notes "There is still a lot of functionality I didnt implement, including things like secrets and triggered abilities". + - *Targeting:* **Targeting** (2018-01-29) covers `Alliance`/`Zones` bitmasks, `Mark`, a `Target` aspect, a `TargetSystem`, and validation hooked into the play-card validation notification. + - *Auras:* **none.** Spells & Abilities mentions Raid Leader's "Aura" only to say it isn't in the demo deck. The series was paused after JSON. Readers wanting auras need the Hearthstone rulebook (§10a) or an engine (fireplace `aura.py`, argentum layers). + +## 6. CritPoints: Card Game Design as Systems Architecture + +- **URL:** https://critpoints.net/2023/05/26/card-game-design-as-systems-architecture/ +- **Loaded:** yes +- **Author / date:** **Celia Wagar** (page title "… – Celia Wagar's CritPoints"), published May 26, 2023. The page also shows July 13, 2026, likely a modified date. **The draft's author name is correct.** +- **Content:** A designer-side essay mapping card game rules onto software concepts. Triggers are lifecycle hooks or subscriptions (MTG examples Lunarch Veteran and Hallowed Priest), card types are interfaces (adding supertypes can break older cards), restricting reach keeps state manageable (exile likened to a null pointer), "pockets" are arrays (from the author's game *Charmed Chains*), and stacks/chains resolve LIFO. +- **Key quotes:** + - "Card games are surprisingly a lot like enterprise web applications." + - "Trigger conditions are lifecycle hooks for cards." + - "a trading card game needs to consider the side effects of their design decisions into perpetuity." +- **Pattern:** framing and motivation (why "add without rewrites" matters). It touches EQ (stack/chain), but it contains no engine code or implementation patterns. +- **Verdict: keep as an optional or intro reading.** Don't cite it as support for any specific implementation pattern. + +## 7. Medium: "Events in Entity Component Systems" (@casual.evil52) + +- **URL:** https://medium.com/@casual.evil52/events-in-entity-component-systems-22dc5107ed81 +- **Loaded:** **no.** WebFetch returned HTTP 403. web.archive.org is blocked for the fetch tool, and a direct curl to the Wayback availability API was refused by the environment. A web search for the exact title and handle returned no matching result. +- **Author / date / content:** unverifiable. +- **Verdict: drop**, unless someone confirms it exists in a browser. Don't ship a course link that can't be verified. If an ECS-events reading is wanted, argentum's architecture doc §2.2 and §2.5 cover ECS with explicit event emission and are verified (§9c). + +## 8. MTG Comprehensive Rules analogues + +**8a. Fandom "Layer" page:** https://mtg.fandom.com/wiki/Layer returned **HTTP 402 (failed)**, and so did the Fandom pages for State-based action and Replacement effect. mtg.wiki returned 403, and archive.org was unavailable. **Replace all Fandom links** with the official rules text: + +- **Official source used:** https://media.wizards.com/2026/downloads/MagicCompRules%2020260925.txt (linked from https://magic.wizards.com/en/rules; effective 2026-09-25). Rules 613.1 and 613.7/613.8 were read from the yawgatog.com hyperlinked mirror (https://yawgatog.com/resources/magic-rules/) of the same edition. Note that this URL changes with each CR update, so link students to https://magic.wizards.com/en/rules instead. + +**Rule 613 (layers).** Pattern: INT (modifier pipeline with deterministic ordering). +- 613.1: "Then all applicable continuous effects are applied in a series of layers in the following order:" Layer 1 copiable values, 2 control, 3 text, 4 type, 5 color, 6 abilities, 7 power/toughness (with sublayers). +- 613.7: "Within a layer or sublayer, determining which order effects are applied in is usually done using a timestamp system." +- 613.8: "… is sometimes done using a dependency system." +- Course use: an aura or buff is a modifier recomputed from base state, never mutated in. Hearthstone's simpler equivalent is order of play plus the aura-update step (§10a). + +**Rule 704 (state-based actions).** Pattern: SBA. +- 704.1: "State-based actions are game actions that happen automatically whenever certain conditions (listed below) are met." +- 704.3: "…then performs all applicable state-based actions simultaneously as a single event." / "If any state-based actions are performed as a result of a check, the check is repeated" +- 704.5f: "If a creature has toughness 0 or less, it's put into its owner's graveyard." +- 704.5g: "…that creature has been dealt lethal damage and is destroyed." +- Course use: the direct analogue of Hearthstone's Death Creation Step. A "0 health" minion isn't removed when it takes damage. It's removed at a defined checkpoint, simultaneously with others, and the check loops until nothing changes. + +**Rules 614/616 (replacement effects).** Pattern: INT (interceptors). +- 614.1: "Some continuous effects are replacement effects." 614.1a: "Effects that use the word 'instead' are replacement effects." +- 614.5: "A replacement effect doesn't invoke itself repeatedly; it gets only one opportunity to affect an event" (the "each interceptor applies once" rule that prevents infinite rewrites) +- 616.1: when several apply, "the affected object's controller (or its owner if it has no controller) or the affected player chooses one to apply"; 616.1f: "Once the chosen effect has been applied, this process is repeated" + +**Rule 603.2c/d ("triggers an additional time").** This is the **exact MTG analogue of "Deathrattles trigger twice"** and should be added to the course: +- 603.2c: "An ability triggers only once each time its trigger event occurs." +- 603.2d: "An ability may state that a triggered ability triggers additional times." … "then that ability triggers that many times." +- Course use: trigger doubling (Panharmonicon, Drivnod) is a modifier on **trigger collection**, not a replacement effect and not a change to the ability itself. Argentum's Panharmonicon ruling makes the distinction explicit (§9c). + +**Verdict:** replace the Fandom links with the official CR. Keep 613, 704, and 614/616, and **add 603.2d**. + +## 9. Reference engines + +All five repos exist. I shallow-cloned each on 2026-10-28 and read the files listed. + +### 9a. phase-rs/phase + +- **URL:** https://github.com/phase-rs/phase. Rust (plus a WASM/React/TypeScript client), Apache-2.0, 312 stars, created 2026-03-10, last push 2026-10-28 (**very actively maintained**). +- **What it is:** a Magic: The Gathering rules engine and client. The README describes it as implementing "comprehensive MTG rules using functional architecture — pure reducers, discriminated unions, and immutable state", and says the cards (34,300+) are parsed from MTGJSON Oracle text. +- **Relevant files** (commit `429c5ae`): + - `crates/engine/src/types/identifiers.rs`: `pub struct ObjectId(pub u64);` and `CardId`, i.e., newtype ID handles **[ID]**. + - `crates/engine/src/game/sba.rs`: `check_state_based_actions` with `const MAX_SBA_ITERATIONS: u32 = 9;` **[SBA]** + - `crates/engine/src/game/triggers.rs` (around line 12125): "CR 603.2d: Apply trigger doubling from `StaticMode::DoubleTriggers` static abilities. Scans battlefield for permanents with a DoubleTriggers static, then clones matching pending triggers an additional time." `TriggerCause` (in `types/statics.rs`, around line 648) includes `CreatureDying`, which is the Drivnod-class "dies triggers twice" and the closest MTG analogue to Rivendare **[INT]**. + - `crates/engine/src/game/layers.rs` and `replacement.rs` **[INT]**. `crates/engine/src/parser/oracle_*.rs` parses Oracle text into typed ability data **[DATA]**. +- **Caveats:** the files are enormous (`triggers.rs` is about 54.6k lines, `layers.rs` about 35.7k, `replacement.rs` about 27.4k, `types/ability.rs` about 53k). The repo is also heavily agent-developed (it ships CLAUDE.md/AGENTS.md/GEMINI.md). It's unsuitable as student reading, but useful as proof that the patterns scale. Use it only for pointed links to the `DoubleTriggers` doc comment and `TriggerCause` enum. +- **Verdict:** keep as an advanced reference only. + +### 9b. jleclanche/fireplace + +- **URL:** https://github.com/jleclanche/fireplace. Python, AGPL-3.0, 745 stars, last push 2025-12-19 (**maintained, but slowly**). +- **What it is:** "A Hearthstone simulator in Python", with card definitions as a Python DSL. +- **Relevant files** (commit `47a2572`): + - **Baron Rivendare** (`fireplace/cards/naxxramas/collectible.py`, around line 154): `class FP1_031: """Baron Rivendare"""` / `update = Refresh(CONTROLLER, {GameTag.EXTRA_DEATHRATTLES: True})`. Rivendare is an aura that sets a player-level tag. + - **Where the doubling happens** (`fireplace/actions.py`, `class Deathrattle`, around lines 998–1023): + ```python + for deathrattle in entity.deathrattles: + ... + source.game.queue_actions(entity, actions) + if target.controller.extra_deathrattles: + log.info("Triggering deathrattles for %r again", target) + source.game.queue_actions(entity, actions) + ``` + This is a **hard-coded flag check inside the Deathrattle action**, which is exactly the "rewrite Deathrattle to add the feature" shape the course warns against. It's a good counter-example: "trigger twice" works, but the next "trigger N times" or "only for Beasts" variant means editing this function again. The tag is a boolean, so two Rivendares can't stack. Contrast this with phase-rs's `TriggerCause` and argentum's `AdditionalETBOrLTBTriggers` (§9c). + - **Deferred death processing** (`fireplace/game.py`, `action_end`, around lines 132–143): `if not self._action_stack:` / `self.log("Empty stack, refreshing auras and processing deaths")` / `self.refresh_auras()` / `self.process_deaths()`. `process_deaths` (around line 186) runs `Death(self.live_entities)` in a `BlockType.DEATHS` block when `any(card.dead ...)`. This is a clean, short **[SBA]** example: deaths are processed only when the outermost action block closes. + - **Queue** (`game.py` `queue_actions` / `trigger_actions`, around line 234) **[EQ]**. Order-of-play-gated deathrattle queuing appears in `actions.py` `Death._broadcast` (around line 338), which checks `entity.play_counter > target.play_counter`. +- **Verdict: keep.** It's the most readable real engine for the Hearthstone case study. Use `game.py` lines ~127–192 for SBA, and use `actions.py` `Deathrattle` as the "before" picture for the Rivendare refactor. + +### 9c. wingedsheep/argentum-engine + +- **URL:** https://github.com/wingedsheep/argentum-engine. Kotlin (Spring Boot server, React client), license shown as "NOASSERTION" by GitHub (check `LICENSE` before quoting large code), 79 stars, created 2026-01-18, last push 2026-10-28 (**very actively maintained**). It is also agent-assisted (the README invites contributors to "let Claude Code or Codex build it"). +- **What it is:** an MTG rules engine and online platform, with card definitions in a Kotlin DSL (`mtg-sdk`) kept separate from the engine (`rules-engine`). +- **Best file:** `docs/architecture-principles.md` (2,227 lines). Its sections map almost one-to-one onto the course: + - §1.1 "**Principle:** Cards are pure, serializable data structures with zero execution logic." **[DATA]** + - §1.2 "**Principle:** Numbers are represented as inspectable AST nodes, not integers or lambdas." (a `DynamicAmount` sealed hierarchy) **[DATA/AST]** + - §2.2 "every card, token, player, spell, and ability is just an `EntityId`", plus components as pure data **[ID]** + - §2.3 "Rule 613: Base State vs. Projected State" ("Calculated lazily and cached per immutable `GameState` instance"; "the engine doesn't need to 'undo' +3/+3. It simply removes the floating effect.") **[INT]** + - §2.7 "**Principle:** Replacement effects modify game actions *before* they produce events, without using the stack." (replacements as data with an `appliesTo: EventPattern`) **[INT]** + - §2.8 `StateBasedActionChecker.checkAndApply`, a do/while loop "that runs until no more SBAs apply" **[SBA]** +- **"Trigger twice" example:** `mtg-sets/2008-2016/src/main/kotlin/com/wingedsheep/mtg/sets/definitions/kld/cards/Panharmonicon.kt` declares `staticAbility { ability = AdditionalETBOrLTBTriggers(filter = GameObjectFilter.Artifact or GameObjectFilter.Creature, ...) }`, which is pure data. The engine consumes it in `rules-engine/src/main/kotlin/com/wingedsheep/engine/event/TriggerDetector.kt` (`duplicateETBOrLTBTriggers`, around line 4482): "Duplicate ETB and LTB triggers for [AdditionalETBOrLTBTriggers] static abilities … Multiple copies are additive: N copies add N extra copies of each trigger." The card file also carries the ruling "Replacement effects are unaffected by Panharmonicon's ability", which is a useful teaching point on trigger doubling vs. interceptors. +- **Verdict: keep. Make `architecture-principles.md` a required reading** for DATA, INT, and SBA. It's the best single document found. Panharmonicon.kt plus `TriggerDetector.duplicateETBOrLTBTriggers` is the "after" picture for the Rivendare refactor. + +### 9d. utilForever/baba-is-auto + +- **URL:** https://github.com/utilForever/baba-is-auto. C++17 with Python bindings, MIT, 192 stars, last push 2026-10-23 (**maintained**). +- **What it is:** "baba-is-auto simulates a subset of Baba Is You: it loads a text map, parses rules formed by word tiles, applies movement and object interactions, and exposes the resulting state to C++ and Python callers." It also includes RL examples. +- **Relevant files** (commit `2305a3d`): + - `Sources/baba-is-auto/Games/Game.cpp`, `Game::MovePlayer` (around lines 495–520), is the turn pipeline. It calls `ParseRules()` four times between phases (after movement, after `ProcessMoveProperty`, after `ProcessTransformations`, and at the end), then runs `ProcessSink`, `ProcessWeak`, `ProcessHotMelt`, `ProcessDefeat`, `ProcessOpenShut`, and `CheckPlayState`. + - `Game::ParseRules` (around line 611) runs `m_ruleManager.ClearRules();` and then scans every cell horizontally and vertically. Rules are **recompiled from the board** rather than patched incrementally **[COMP]**. + - `Sources/baba-is-auto/Rules/RuleManager.cpp` (93 lines) has `AddRule`, `GetRules`, and `HasProperty`, the compiled query surface the phases read from. +- **Verdict: keep.** It's the clearest small example of "derive rules from world state, then have systems query the derived table". It's a *subset* of the game, so warn students that it isn't a faithful reference for edge-case rule interactions. + +### 9e. HearthSim/SabberStone (supplementary) + +- **URL:** https://github.com/HearthSim/SabberStone. C# / .NET Core, AGPL-3.0, 290 stars. Last push 2022-12-08 and last commit on master 2021-01-15, so it is **effectively unmaintained**. +- **Deferred deaths** (`SabberStoneCore/src/Model/Game.cs`): `DeathProcessingAndAuraUpdate()` (around line 1191) calls `AuraUpdate()`, then loops `do { GraveYard(); // Death Creation Step` / `ProcessTasks(); // Death Resolution Phase } while (DeadMinions.Count != 0);`, then runs `AuraUpdate()` again. `GraveYard()` sorts dead minions with `DeadMinions.InsertionSort(GetOrderOfPlay)`. This is the closest code-level mirror of the Hearthstone Advanced Rulebook's step names **[SBA]**. +- **Baron Rivendare:** `CardSets/NaxxCardsGen.cs` (around line 627) defines `Aura = new Aura(AuraType.CONTROLLER, new Effect(GameTag.EXTRA_MINION_DEATHRATTLES_BASE, EffectOperator.SET, 1))`. However, `Tasks/SimpleTasks/ActivateDeathrattleTask.cs` (around line 44) checks a *different* tag: `if (p.Controller.ControllerAuraEffects[GameTag.EXTRA_DEATHRATTLES_BASE] == 1)`, and then re-runs `p.ActivateTask(PowerActivation.DEATHRATTLE)` inline. The `ControllerAuraEffects` indexer (`Enchants/AuraEffects.cs`) has no case for either deathrattle tag, so its `default` branch returns 0, and the Rivendare unit test is `[Fact(Skip = "ignore")]` with "// TODO BaronRivendare_FP1_031 test". **From static reading, Rivendare doubling appears not to work in current master.** I didn't build or run it to confirm. +- **Verdict: keep as a cautionary example.** Like fireplace, the doubling is a special-case branch inside the deathrattle task. Here a tag mismatch between the producer (aura) and the consumer (task) apparently disabled the feature silently. It's a strong motivating example for making "trigger N extra times" a typed, centrally applied modifier. + +## 10. Additional sources (read and judged valuable) + +### 10a. Hearthstone Wiki: Advanced rulebook + +- **URL:** https://hearthstone.wiki.gg/wiki/Advanced_rulebook (community wiki; no author or edit date shown in the fetched text). Only the first ~100k of ~480k characters were read. +- **Content:** Player actions start a Sequence of Phases. Nested Phases are allowed, but deaths are processed only after the outermost Phase ends: an Aura Update (Health/Attack), then the Death Creation Step, then a Death Phase for Deathrattles, repeated until no new deaths occur. Queues are filled in order of play. Auras update on summon. Rivendare-style multipliers check their condition before an Event starts. +- **Key quotes:** + - "A Sequence consists of one or more Phases, that are resolved in order." + - "Aura Update (Health/Attack), then does _the Death Creation Step_" + - "If one or more Deaths happened after the outermost Phase ended, a new Phase (called a "Death Phase") begins" + - "the order the Entities each Event/trigger is associated with entered play, from oldest to newest" + - On Rivendare and other repeat-trigger cards: "if one Battlecry/Deathrattle/End of turn effect summons them, it will not trigger again." +- **Pattern:** SBA, EQ, INT (auras and Rivendare timing). +- **Verdict: add as a primary source.** Liquid Fire explicitly says its design "was based on the documentation in the advanced rulebook", and SabberStone's comments use its step names. + +### 10b. Forge wiki: Card scripting API and Creating a custom Card + +- **URLs:** https://github.com/Card-Forge/forge/wiki/Card-scripting-API and https://github.com/Card-Forge/forge/wiki/Creating-a-custom-Card (both last edited 2026-10-23 by github-actions[bot], i.e., synced from the repo docs) +- **Content:** Forge (a Java MTG engine) defines cards as plain-text key:value scripts. `A:` lines are abilities, `T:` triggers, `S:` statics, `R:` replacement effects, and `SVar:` lines are named sub-expressions. A trigger names its effect indirectly through `Execute$`, and effects chain through `SubAbility$`. +- **Key quotes:** + - `"T:Mode$ DamageDone | ValidSource$ Card.Self | ValidTarget$ Opponent"` … `"Execute$ TrigDraw"` + - `"SVar:TrigDraw:DB$ Draw | Defined$ You | NumCards$ 1"` + - `"SubAbility$ DBScry"` / `"SVar:DBScry:DB$ Scry | ScryNum$ 1"` +- **Pattern:** DATA. It's a mature, long-lived example of abilities as data, with separate syntax for triggers, statics, and replacements (INT) and an extensible effect vocabulary. +- **Verdict: add** as the "abilities as data at scale" example, alongside argentum's typed DSL. + +### 10c. BasicMod wiki: Action Queue (Slay the Spire modding) + +- **URL:** https://github.com/Alchyr/BasicMod/wiki/Action-Queue (author Alchyr; last edited 2022-04-11) +- **Content:** A short practical explanation of StS's combat action queue, covering `addToBottom` vs. `addToTop` and when to use each. Thorns is the worked example of why top-insertion matters. +- **Key quotes:** + - "Slay the Spire's combat works based on an action queue." + - "In a card's use: add to bottom. In an action's update: add to top. Not sure? add to bottom." + - "each hit of thorns is added to top, which causes it to take place before the next hit of the attack happens." +- **Pattern:** EQ. It shows that "where in the queue does a reaction go" is a design decision, comparable to Liquid Fire's depth-first ordering. +- **Verdict: add** as a short supplementary reading. It doesn't cover the stale-state pitfall; Nystrom does. + +**Searched but not included:** XMage ability architecture (not fetched), the Forge "AbilityFactory/Triggers" subpages (not fetched, since the two pages above were enough), and general ECS-events results (gamedev.net 2013 thread, evenio crate; not read in depth). + +--- + +## Best source per pattern + +| Pattern | Primary | Supporting | +|---------|---------|-----------| +| ID handles | argentum `architecture-principles.md` §2.2 (`EntityId`) | phase-rs `types/identifiers.rs` (`ObjectId`); Nystrom Event Queue (stale-state warning as motivation) | +| Event / action queue | Liquid Fire *Action System* | Nystrom *Event Queue*; fireplace `game.py` `queue_actions`; StS *Action Queue* | +| Deferred state-based checks | Hearthstone *Advanced rulebook* + MTG CR 704 | fireplace `game.py` `action_end`/`process_deaths`; SabberStone `DeathProcessingAndAuraUpdate`; Liquid Fire death reaper loop; argentum §2.8 | +| Interceptor / modifier pipeline | argentum §2.3 + §2.7, MTG CR 613/614/616 | MTG CR 603.2d plus argentum `Panharmonicon.kt` / `TriggerDetector.duplicateETBOrLTBTriggers` (trigger doubling); fireplace and SabberStone Rivendare as "before" counter-examples | +| Abilities as data / ASTs | argentum §1.1–1.2 | Forge scripting wiki; Nystrom *Bytecode* (framing); Liquid Fire *Spells & Abilities* + *JSON* | +| Rule compilation | baba-is-auto `Game::MovePlayer` / `ParseRules` | argentum §2.3 (projected state recomputed from base) | diff --git a/docs/research/baba-is-you.md b/docs/research/baba-is-you.md new file mode 100644 index 0000000..fc2bbea --- /dev/null +++ b/docs/research/baba-is-you.md @@ -0,0 +1,252 @@ +# Baba Is You: source research for the systemic design course + +Researched 2026-10-28. Every behavioral claim below cites a page that was actually retrieved during this session. + +**About quote fidelity.** Fandom wiki quotes are verbatim from the MediaWiki wikitext. That text was pulled with `api.php?action=parse&prop=wikitext`, so template markup such as `{{O|BABA|IS|YOU}}` has been rendered by hand as BABA IS YOU. Esolangs quotes are verbatim from `action=raw`. baba-is-auto quotes are verbatim from raw.githubusercontent.com. Game Developer quotes came through a fetch tool that summarizes pages and caps quotes at about 125 characters. They are excerpts, so check them against the page before printing. + +**Authority caveat.** The Baba Is You Wiki is an unofficial community wiki. Its Order of Operations page names internal Lua functions (`domovement()`, `block()`, ...), which suggests it was written from the game's shipped Lua scripts, but Hempuli did not author it. baba-is-auto is a fan simulator of a *subset* of the game, and its own docs say it diverges from the official game in several places. + +--- + +## Sources + +### 1. baba-is-auto (utilForever): rule-language.md and repository structure +- URL: https://github.com/utilForever/baba-is-auto/blob/main/Documents/rule-language.md +- Raw: https://raw.githubusercontent.com/utilForever/baba-is-auto/main/Documents/rule-language.md (**loaded**, HTTP 200) +- Also loaded: the README (via github.com), `ARCHITECTURE.md` (raw), `Includes/baba-is-auto/Rules/Rule.hpp` (raw), and the repository tree via the GitHub API. +- **Summary.** A C++17 simulator of a subset of Baba Is You, with pybind11 Python bindings, a pygame GUI, an ImGui editor and reinforcement-learning environments. `rule-language.md` is a capability matrix (Implemented / Partial / Unsupported) measured against official version 481d. A rule is a triple of objects plus optional conditions. `RuleManager` stores and queries the active rules, and `Game` parses rules and runs movement and effects. Tests live in `Tests/UnitTests/GameTests.cpp` (C++) and `Tests/PythonTests/test_*.py`, and each matrix row links to the test that proves it. +- Structure: `Includes/baba-is-auto/{Games,Rules,Enums,Agents}`, `Sources/baba-is-auto/...`, `Extensions/{BabaPython,BabaGUI,BabaEditor,BabaRL}`, `Resources/Maps`, `Tests/{UnitTests,PythonTests}`, `Documents/`. +- Verbatim quotes: + - "This page records the rule language implemented by the current simulator. The comparison baseline is Baba Is You version 481d" + - "External references are comparison aids; repository code, focused tests, and maps define the simulator's behavior." + - Rule representation (Rule.hpp): `Rule(Object obj1, Object obj2, Object obj3, std::vector ruleConditions = {});` and `std::tuple objects;` + - `NOT` row: "Partial | `NOT LONELY` and `NOT` before `ON`, `NEAR`, or `FACING` are parsed. Negated subjects, predicates, condition targets, repeated `NOT`, and nested forms are unsupported." + - `AND` row: "Subjects and predicates expand into independent rule combinations. Condition targets and consecutive conditions may also be joined with `AND`. Verb chaining is not supported." + - `TEXT` row: "As a subject it selects text instances, and all text is pushable without an explicit rule." + - `IS` row: "performs snapshot-based noun transformations. Multiple noun predicates may produce multiple objects; an identity result protects the source." + - `FLOAT` row: "Partial | Separates `OPEN`/`SHUT` interactions by float layer. It is not applied to the other overlap/effect properties." (This differs from the official game. See g.) + - ARCHITECTURE.md: "Parse horizontal and vertical `NOUN IS NOUN/PROPERTY` rules from text tiles." / "Text tiles are movable blockers during movement checks, even when they do not have an explicit `PUSH` rule." / "Rules are rebuilt after every move, so moving text can immediately change the active rule set." +- **Course use:** a good example of how to represent rules as data and pin behavior with tests. Do **not** use it as an authority on official game semantics. It does not implement IS NOT for properties or nouns, it hard-codes text as pushable, and its FLOAT is partial. + +### 2. Game Developer: "Designing Baba Is You's delightfully innovative rule-writing system" +- URL: https://www.gamedeveloper.com/design/designing-i-baba-is-you-i-s-delightfully-innovative-rule-writing-system (**loaded** via WebFetch) +- **Summary.** Written by John Harris, published May 8, 2019. Arvi "Hempuli" Teikari describes how rules are rebuilt: the game rebuilds only when text has changed, clears the old rules, detects candidate sentence starts, and resolves conflicts such as NOT at the end. He says stacked text was one of the hardest parts of development and explains why he used Lua on top of Multimedia Fusion 2 (MMF2). +- Quotes (excerpts returned by the fetch tool): + - "The game only updates the rules if it needs to" + - "The previous rules are removed entirely, and words that could potentially start rule sentences are detected on the level." + - "Finally, once everything is (hopefully) set, the game goes through the final set of rules and adjusts them if needed." + - "(For example, if the rule "Baba Is Not You" exists, "Baba Is You" is marked as being not valid.)" + - "the stacking adds the need to go through all the possible permutations a rule could have" + - "Lua allowed me to implement the game logic in ways MMF2's base "event system" couldn't have done without a lot of hassle" + +### 3. Baba Is You Wiki: Order of Operations +- URL: https://babaiswiki.fandom.com/wiki/Order_of_Operations +- Direct HTML fetch **failed**: WebFetch returned HTTP 402, and curl returned HTTP 403 for `Order_of_Operations`, `Order_of_operations` and `Turn_order`. +- WebFetch of web.archive.org is **blocked** for this tool. A snapshot exists (http://web.archive.org/web/20251213151454/https://babaiswiki.fandom.com/wiki/Order_of_Operations) and was downloaded with curl, but it was not used as the primary text. +- **Loaded** via the MediaWiki API: https://babaiswiki.fandom.com/api.php?action=parse&page=Order_of_Operations&prop=wikitext&format=json +- **Summary.** This is the authoritative-looking turn pipeline: + 1. `domovement()`: YOU, then MOVE/AUTO/CHILL, NUDGE, FEAR, SHIFT. Conditions are reparsed between movement stages. Collision reactions are resolved inside movement: OPEN/SHUT, WEAK/EAT, SWAP, PUSH/PULL, then STOP. + 2. Rule parsing. + 3. Transformations (REVERT, IS, WRITE). + 4. Rule parsing. + 5. `moveblock()`: FOLLOW, BACK, TELE, SHIFT. + 6. SELECT, then the stack limit. + 7. `fallblock()`. + 8. Rule parsing. + 9. `statusblock()`: directions, TURN, FLOAT. + 10. `fallblock()` again. + 11. Rule parsing. + 12. `levelblock()`. + 13. `block()`: SINK, BOOM, WEAK, HOT/MELT, DEFEAT, OPEN/SHUT overlap, EAT, MAKE, then BONUS/END/WIN, then out-of-bounds deletion and CRASH. + 14. Rule parsing. +- Verbatim quotes: + - "Within a turn, most properties and verbs take place exactly once. Conditions process when the game checks for what objects have a rule, which is usually right before it uses it." + - "domovement(). ... Note that it completely finishes moving for a particular move stage before checking which units are in the next, so conditions (e.g. IT ON BABA IS YOU AND MOVE) reparse in between." + - "* (rule parsing) + * REVERT and object transformations via IS and WRITE. While they are checked in the same step, REVERT takes priority over the others while '''X IS ALL''' takes the lowest priority. + *(rule parsing)" + - "block(). Deletion occurs after each step that removes objects, but all objects destroyed in the same step are destroyed simultaneously. ** DONE ** MORE ** PLAY ** HOLD ... ** SINK ** BOOM ** WEAK ** HOT MELT ** DEFEAT ** OPEN SHUT (collision check; movement check occurs on movement) ** EAT (collision check; movement check occurs on movement) ** MAKE ... ** BONUS, END, WIN (ran in this order; deletes from bonus don't occur until after) ** Delete out of bounds units ** CRASH * (rule parsing)" + - "Movement check response properties: Before first parsing, the game checks for collisions after every movement step in the following order. * OPEN SHUT * WEAK EAT are checked simultaneously * SWAP * PUSH PULL are checked. * OPEN SHUT for pushed/pulled objects * WEAK EAT for pushed/pulled objects * Passive SWAP * STOP" + - "HAS is checked after every standard destruction, no matter whether the destruction is checked on collision or overlap." + +### 4. Esolangs wiki: Baba Is You +- URL: https://esolangs.org/wiki/Baba_Is_You (**loaded** via WebFetch, then raw via `?action=raw`) +- **Summary.** Treats the game as a cellular automaton or esolang and gives a table of words. It contains no order of operations. The page is marked WIP. +- Verbatim quotes: + - "Text is parsed when nearby other text objects from left to right and top to bottom, and multiple rules are able to cross and overlap if the parsing rules allow for a valid statement." + - "The primary goal of a level is for any controllable object to exist on the same tile as any object that is defined as "WIN", which may also be itself." + - PUSH: "Actors which move into the object will cause it to move in the direction that the actor entered from. If the object is unable to move, the actor will not perform its move." + - STOP: "Like PUSH and PULL, actors that attempt to enter will be blocked." + - OPEN and SHUT: "Any objects which are OPEN will destroy itself and any objects that are SHUT when moving into them, and vice versa." + - SINK: "The object will destroy itself and any actors with it when it shares a space with an actor." + - FLOAT: "Any actors which are BONUS, BOOM, DEFEAT, END, HOT, MELT, OPEN, SHUT, SHIFT, SINK, TELE, WEAK, WIN, or acting upon it through EAT will not affect the object, unless it itself is also FLOAT with it." + - "If an object is to leave the bounds of the level, it is immediately destroyed." (The fandom Order of Operations page says out-of-bounds deletion happens at the end of `block()`, so "immediately" is loose.) + +### 5. Baba Is You Wiki: property and word pages +All pages were loaded as wikitext via `https://babaiswiki.fandom.com/api.php?action=parse&page=&prop=wikitext&format=json&redirects=1`. Direct HTML returns 402/403. + +| Page | Status | Key verbatim quote(s) | +|---|---|---| +| PUSH | loaded | "PUSH is a property that makes an object pushable by any moving object. PUSH also makes the object "solid" for collision checks like STOP does." / "TEXT IS PUSH as a base rule." / "If something tries to move into a PUSH object, it will try to move in the same direction to make room. ** This includes other PUSH objects. ** This ignores float levels. ** This movement is instant" | +| STOP | loaded | "STOP is a property that prevents most forms of movement from moving into the object." / "Some movement checks also override STOP to allow a movement, including EAT OPEN SHUT WEAK." / "FLOAT does not allow bypassing STOP." | +| OPEN | loaded | "If an OPEN object is on, or moved onto, a same-float SHUT object (or vice-versa), both are removed." / "The movement check applies to standard forms of movement, including FEAR AUTO CHILL FALL MOVE NUDGEDOWN PULL PUSH SHIFT YOU YOU2 3D." / "If the movement check succeeds in finding an "unlock", the normal obstruction of movement by STOP PUSH PULL does not apply. *** However, other obstructing objects may still prevent the movement, and this will also prevent the destruction from OPEN SHUT since no movement actually occurs." / "Destruction happens before the moving object actually moves onto the other object, therefore anything it HAS will spawn on the space the object was moving from." / "Only one SHUT object is destroyed per OPEN object (and vice versa), based on the first object in update order." / "If both the OPEN and SHUT objects are SAFE, the check fails. If only one is, the check succeeds and the appropriate object is destroyed." | +| SHUT | loaded | "If a SHUT object is on, or moved onto, a same-float OPEN object (or vice-versa), both are removed. As OPEN and SHUT are a pair, see OPEN for details." | +| HOT | loaded | "HOT is a property that causes the object to destroy any same-float MELT objects it contacts." / "An object that is both HOT and MELT destroys itself." | +| MELT | loaded | "MELT is a property that causes the object to be destroyed if contacting any same-float object that is HOT." / "MELT obeys HAS and SAFE as expected." | +| SINK | loaded | "SINK is a property that causes an object to destroy all overlapping same-float objects along with itself when there is at least one overlapping object." / "A stack of two or more SINK objects will mutually SINK each other." / "SINKing order: Overlapping objects are destroyed (unless SAFE) in priority order, then the SINK object itself." / "SINK has no movement check, only an overlap check." | +| DEFEAT | loaded | "If a DEFEAT object intersects with or is a YOU (or YOU2 or 3D), the YOU object will be destroyed." / "If an object is both WIN and DEFEAT, then DEFEAT takes priority and the YOU object is destroyed." / "if a square contains both a WIN object and a separate DEFEAT object, then DEFEAT takes priority." | +| WIN | loaded | "WIN is a property that causes the level to be won if a same-float YOU YOU2 3D object overlaps the object." / "If an object is both WIN and YOU YOU2 3D, the level is won." / "WIN is checked very late in the process: any other removal of either object involved will occur first and prevent WIN from WINning." | +| FLOAT | loaded | "EAT and properties BONUS BOOM DEFEAT END HOLD HOT MELT OPEN SHUT SHIFT SINK TELE WEAK WIN require that the interacting objects are both FLOAT or both not FLOAT." / "FLOAT has no effect on collision-check properties such as STOP PUSH PULL SWAP, except OPEN SHUT." / "Whether an object is FLOATing or not updates once per turn, or at object creation (including transforms)." | +| Float levels | loaded | "It does not affect ON or collision checks from PULL PUSH STOP SWAP." | +| NOT | loaded | "When added after a verb it makes the corresponding rule not apply, overriding other instances of the rule it disables. ** ROCK IS NOT PUSH makes ROCK NOT PUSHable, even if ROCK IS PUSH exists elsewhere." / "ROCK IS NOT GRASS disables ROCK IS GRASS." / "X IS NOT X makes an object disappear, similarly to X IS EMPTY but with particles. ... As it negates X IS X, it enables other X IS Y rules, which can result in transformation in addition to the disappearance." / "NOT negates itself before negating anything else, so ROCK IS NOT NOT PUSH makes ROCK IS PUSH" | +| IS | loaded | "BABA IS NOT YOU prevents BABA from having the property YOU, overriding any BABA IS YOU rule." / "BABA IS BABA prevents BABA from transforming into any object, including those not in ALL. It does not affect transforming objects into BABA." / "BABA IS NOT KEKE prevents BABA from transforming into KEKE via a BABA IS KEKE rule." / "BABA IS NOT BABA causes BABA to vanish in a puff of logic." / "BABA IS KEKE AND ME turns all BABA into both KEKE and ME" | +| TEXT | loaded | "TEXT IS PUSH as a base rule." / "TEXT is not included in ALL, but is included in NOT ALL." | +| AND | loaded | "AND is an Operator that generally defines two or more rules at the same time; this is mechanically equivalent to each rule being defined individually" / table: "BABA AND KEKE IS PUSH AND STOP = BABA IS PUSH, BABA IS STOP, KEKE IS PUSH, KEKE IS STOP" / "Overlapping rules with AND in the overlapped part does not apply AND to the right/downward rule." / "If a rule is invalid due to circumstances, it does not prevent other chaining of AND rules." | +| Rule (requested as "Rules" and "Base_Rules", redirected) | loaded | "Rules read from left to right in a row, or from top to bottom in a column." / "The following rules are enabled by default, even though there isn't any text saying so. *TEXT IS PUSH *LEVEL IS STOP *CURSOR IS SELECT. These rules can still be detected by FEELING and MIMIC and negated by NOT and BROKEN. These rules are not listed in the pause menu." / "If more than one parsable object is stacked, the game will attempt to produce all possible rules." / "Rules parse when any parsable object changes position or is destroyed, or an object is transformed to text. Between parsings, active rules are simply stored." | +| YOU | loaded | "If a YOU object is on an object that is (or itself is) WIN at the end of the turn, the level will be won." / "Movement from YOU occurs before all other movement." | +| Noun (redirects to Category:Nouns) | loaded | "A noun can be used as a NOUN RULE PROPERTY statement e.g. BABA IS YOU to give it a property or as a NOUN IS NOUN statement eg. WALL IS WATER to turn an object into another object." | +| Advanced rulebook (requested as "Stacking" and "Duplicating") | loaded | "if you write A IS B and A IS C simultaneously in the same turn (or any similar construct like A IS B AND C), then you get both a B and a C for each A" / "Text can be stacked if TEXT IS NOT PUSH can be formed with another way to move text" / "While SINK will consume all objects simultaneously when it triggers, OPEN/SHUT being triggered as a reaction to movement will only consume one object." | +| "X IS X" | no standalone page found. Covered by the IS and NOT pages above. | | +| Transformation, Transformations, Movement, Parsing, Nouns | **do not exist** (API: "The page you specified doesn't exist.") | | + +### 6. GDC talk: "Reading the Rules of Baba Is You" +- URL: https://www.youtube.com/watch?v=Jf5O8S5GiOo +- **Existence confirmed** via YouTube oEmbed (https://www.youtube.com/oembed?url=...Jf5O8S5GiOo): `"title":"Reading the Rules of Baba Is You","author_name":"GDC Festival of Gaming"` (the GDC channel). WebFetch of the watch page also returned the title "Reading the Rules of Baba Is You." +- **Speaker**: WebSearch results (a Game Developer video post and gdconf.com news) describe it as Arvi Teikari's GDC 2020 Design-track talk about turning in-level objects into sentences the game logic can read, and about building with a non-programming tool plus Lua. The gdconf.com news page returned 404 when fetched directly, so the speaker attribution rests on search snippets and was not read on a page. The video itself was not watched, and no claim in this document relies on it. + +--- + +## Answers + +### a. Order of operations within a turn +Source: Order of Operations (wiki, §3). The sequence is as follows. + +1. **Movement** (`domovement()`): YOU first, then MOVE/AUTO/CHILL, NUDGE*, FEAR, then SHIFT. Collision reactions are resolved on every movement step: OPEN/SHUT, WEAK/EAT, SWAP, PUSH/PULL, OPEN/SHUT and WEAK/EAT for pushed objects, passive SWAP, then STOP. Conditions are reparsed between movement stages. +2. **Rule parsing.** +3. **Conversions**: REVERT, X IS Y and WRITE ("REVERT takes priority ... X IS ALL takes the lowest priority"). +4. **Rule parsing.** +5. **Secondary movement** (`moveblock()`): FOLLOW, BACK, TELE, SHIFT direction change. Then SELECT, then the 6-stack limit, then FALL. +6. **Rule parsing.** +7. **Status block** (`statusblock()`): direction properties, TURN, FLOAT. Then FALL again. +8. **Rule parsing.** +9. **Level block** (`levelblock()`). +10. **Destruction and end-of-turn checks** (`block()`): DONE, MORE, PLAY, HOLD, **SINK**, BOOM, WEAK, **HOT/MELT**, **DEFEAT**, **OPEN/SHUT overlap**, EAT, MAKE, HOLD, visuals, then **BONUS, END, WIN**, then out-of-bounds deletion, then CRASH. "Deletion occurs after each step that removes objects". +11. **Rule parsing.** + +**How many parses per turn?** The page lists five "(rule parsing)" points, plus condition reparsing between movement stages. A full reparse only happens when text actually changed. The Rule page says: "Rules parse when any parsable object changes position or is destroyed, or an object is transformed to text. Between parsings, active rules are simply stored." The Game Developer interview agrees: "The game only updates the rules if it needs to." Course-safe wording: "the game reparses rules at up to five checkpoints per turn (whenever text has changed), including once right after movement and once right after conversion." + +### b. BABA IS YOU, BABA IS MELT, LAVA IS HOT: Baba steps onto lava while pushing MELT out of BABA IS MELT +**Baba survives**, provided no other active rule makes Baba MELT and the displaced MELT text does not complete another BABA ... MELT sentence. + +The reasoning, step by step: +- HOT/MELT has no check during movement. Neither the HOT nor the MELT page is in "Checked on Collision", and the movement-check list contains only OPEN/SHUT, WEAK/EAT, SWAP, PUSH/PULL and STOP. +- HOT/MELT is resolved in `block()`, as an overlap check. +- Between movement and `block()`, the rules are reparsed: "(rule parsing)" right after `domovement()`, and again several more times. +- So when HOT/MELT runs, BABA IS MELT no longer exists. MELT is defined as "causes the object to be destroyed if contacting any same-float object that is HOT", which no longer applies to Baba. +- Also check FLOAT: Baba and lava must be on the same float level for HOT/MELT to apply at all. + +Inference status: this follows directly from the documented order. The wiki gives no worked example of this exact scenario. + +### c. NOT and X IS X / X IS NOT X +- **Rule cancellation.** X IS NOT Y overrides X IS Y wherever it appears. + - NOT page: "ROCK IS NOT PUSH makes ROCK NOT PUSHable, even if ROCK IS PUSH exists elsewhere" and "ROCK IS NOT GRASS disables ROCK IS GRASS." + - IS page: "BABA IS NOT YOU ... overriding any BABA IS YOU rule." + - Teikari: "if the rule "Baba Is Not You" exists, "Baba Is You" is marked as being not valid." + - Double NOT cancels itself: "ROCK IS NOT NOT PUSH makes ROCK IS PUSH". +- **X IS X** blocks conversion *out of* X: "BABA IS BABA prevents BABA from transforming into any object... It does not affect transforming objects into BABA." +- **X IS NOT X** does *not* protect X: + - It removes X: "makes an object disappear". + - It negates X IS X: "As it negates X IS X, it enables other X IS Y rules, which can result in transformation in addition to the disappearance." +- **X IS NOT Y** blocks only the specific conversion X IS Y. + +### d. TEXT IS PUSH +- **Yes, it is a built-in base rule.** Rule page: "The following rules are enabled by default, even though there isn't any text saying so. TEXT IS PUSH, LEVEL IS STOP, CURSOR IS SELECT". The PUSH and TEXT pages say the same. +- **Yes, it can be disabled.** Rule page: base rules can be "negated by NOT and BROKEN". The Advanced rulebook relies on this: "Text can be stacked if TEXT IS NOT PUSH can be formed". +- Base rules "are not listed in the pause menu." +- Note: baba-is-auto instead hard-codes "all text is pushable without an explicit rule", so its behavior here is not authoritative. + +### e. Parsing +- **Crossing sentences.** Rules read "from left to right in a row, or from top to bottom in a column" (Rule page). "multiple rules are able to cross and overlap if the parsing rules allow for a valid statement" (Esolangs). So one IS can serve both a horizontal and a vertical sentence. baba-is-auto also parses "horizontal and vertical" rules. +- **AND distributes over subjects and complements.** + - "BABA AND KEKE IS PUSH AND STOP" gives four rules (AND page), "mechanically equivalent to each rule being defined individually". + - Mixed complements work: "BABA IS PUSH AND STOP AND KEKE" gives two properties and one conversion. + - An invalid link does not break the chain. + - Caveat: "Overlapping rules with AND in the overlapped part does not apply AND to the right/downward rule." +- **Stacked text.** + - "If more than one parsable object is stacked, the game will attempt to produce all possible rules" (Rule page). + - Teikari: "the stacking adds the need to go through all the possible permutations a rule could have". + - A stacked noun in A IS B parses multiple times, which duplicates objects (Advanced rulebook). + - Stacking text is hard by default, because TEXT IS PUSH is a base rule. + +### f. Push chains, STOP and OPEN/SHUT +- **Push chain into STOP: nothing moves.** + - "If something tries to move into a PUSH object, it will try to move in the same direction to make room. This includes other PUSH objects" (PUSH page). + - "If the object is unable to move, the actor will not perform its move" (Esolangs). + - So if the front of the chain hits STOP, the whole chain and the YOU object stay put. +- **OPEN/SHUT happens during movement, including for pushed objects.** + - The movement-check list includes "OPEN SHUT for pushed/pulled objects". + - "If the movement check succeeds in finding an "unlock", the normal obstruction of movement by STOP PUSH PULL does not apply." + - "Destruction happens before the moving object actually moves onto the other object". +- **Example: Baba pushes an OPEN+PUSH key into a SHUT+STOP door.** + - Key and door are both removed during movement. + - The key's push counts as unobstructed, so Baba's move succeeds and Baba moves one tile, onto the key's *former* tile. Baba does not reach the door's tile that turn. (This is an inference from the documented rules; the wiki does not spell out the pusher's final position.) + - Anything the key HAS spawns on the key's former tile. + - Only one SHUT is destroyed per OPEN in a movement check. + - The interaction fails if the two are on different float levels, if both are SAFE, or if another obstructing object is on the door tile. + +### g. FLOAT and hazards; what SINK destroys +- **FLOAT gates hazards.** HOT, MELT, SINK, DEFEAT, WIN, OPEN/SHUT, BOOM, WEAK, EAT and others "require that the interacting objects are both FLOAT or both not FLOAT" (FLOAT page). +- **FLOAT does not gate collision.** It does not affect STOP, PUSH, PULL or SWAP ("FLOAT does not allow bypassing STOP"). OPEN/SHUT is the exception. +- FLOAT status updates "once per turn, or at object creation". In the turn order, it updates in `statusblock()`, before `block()`. +- **SINK** destroys "all overlapping same-float objects along with itself when there is at least one overlapping object." + - This means any object, not just YOU: text and keys count too. + - SAFE objects survive. + - Two SINK objects destroy each other. + - It is an overlap check only, with no movement check. + +### h. WIN +- The level is won when a same-float YOU/YOU2/3D overlaps a WIN object, checked "at the end of the turn" (YOU page) as the last step of `block()`. +- An object that is both YOU and WIN wins: "If an object is both WIN and YOU YOU2 3D, the level is won." +- **DEFEAT beats WIN.** It applies whether DEFEAT is on the same object or on a separate object in the same square: "WIN is checked very late in the process: any other removal of either object involved will occur first and prevent WIN". +- Exceptions: + - YOU AND DEFEAT AND WIN AND SAFE wins. + - A HAS-respawned YOU can win in the same turn. + - LEVEL IS WIN AND DEFEAT wins, because level rules are processed in rule order. + +--- + +## Draft claims that are WRONG or unverified + +1. **"Turn = Movement → Reparse → Conversion → Reparse → Hazard & Win."** *Directionally right but incomplete. Reword it.* + - The first four steps match the wiki. + - Between the second reparse and hazards, there is a whole second movement phase (FOLLOW, BACK, TELE, SHIFT), then SELECT, FALL (twice), status updates (TURN, FLOAT, directions) and the level block, with up to three more reparses. + - OPEN/SHUT, WEAK and EAT also fire *during* movement, not only in the hazard phase. + - WIN is not a separate phase. It is the tail of the same `block()` pass, after SINK, BOOM, WEAK, HOT/MELT, DEFEAT, OPEN/SHUT, EAT and MAKE, and before out-of-bounds deletion. + - There is one more reparse at the end. + - It is acceptable as an explicitly labeled simplification for levels that use only basic words. + +2. **"Breaking BABA IS MELT during the move saves Baba standing on lava."** *Supported (true), with caveats.* + - HOT/MELT is an overlap check in `block()`, after the post-movement reparse. + - Caveats: no other rule may make Baba MELT, the displaced text must not form a new MELT sentence, and HOT/MELT needs Baba and lava on the same float level. + - This is inferred from the documented order. No source shows this exact scenario. + +3. **"X IS X or X IS NOT X locks X against conversion."** *Half wrong.* + - X IS X does block X from converting into anything (IS page). + - X IS NOT X does the opposite. It deletes X ("makes an object disappear") and negates X IS X, so it "enables other X IS Y rules". + - To block one specific conversion, use X IS NOT Y. + +4. **"Pushing an OPEN+PUSH key into a SHUT+STOP door destroys both and Baba steps onto the tile."** *First half true; second half wrong or misleading.* + - Both are destroyed during movement. The OPEN/SHUT movement check applies to pushed objects and overrides STOP/PUSH obstruction. + - Baba moves one tile, into the key's former tile, not onto the door's tile. Destruction happens "before the moving object actually moves onto the other object". + - The pusher's exact final position is inferred, not explicitly documented. + - It also requires matching float levels, at most one of the pair being SAFE, and no other obstruction on the door tile. + +5. **"TEXT IS PUSH is an implicit engine rule."** *True.* + - It is listed as a base rule, "enabled by default, even though there isn't any text saying so", alongside LEVEL IS STOP and CURSOR IS SELECT. + - It is a real rule, not hard-wired behavior: it can be negated with TEXT IS NOT PUSH (or BROKEN), detected by FEELING and MIMIC, and is hidden from the pause menu. + - Do not cite baba-is-auto for this. It hard-codes text pushability. + +### Unverified / not sourced +- The GDC talk's speaker attribution (Teikari) comes only from search snippets. The gdconf.com page returned 404 and the video was not watched. +- The fandom wiki is community-written. Its Order of Operations page is detailed and cites internal function names, but it is not an official Hempuli document. +- Game Developer quotes are excerpts returned by a summarizing fetch tool. Check them against the page before publishing. diff --git a/docs/research/hearthstone.md b/docs/research/hearthstone.md new file mode 100644 index 0000000..fbe49fc --- /dev/null +++ b/docs/research/hearthstone.md @@ -0,0 +1,251 @@ +# Hearthstone rules research (for the systemic-design course) + +Researched 2026-10-28. Pages were fetched with WebFetch. WebFetch only returns quotes of 125 characters or fewer, so all "verbatim" quotes below are short excerpts. Longer passages are paraphrased and marked as such. Before publishing, re-check any quote that matters against the live page. + +## Fetch log + +| Source | Result | +|---|---| +| https://hearthstone.wiki.gg/wiki/Advanced_rulebook | Loaded. About 480k characters, read in full in 100k chunks. Last edited 26 June 2026. | +| https://hearthstone.fandom.com/wiki/Advanced_rulebook | FAILED (HTTP 402). | +| curl of wiki.gg `?action=raw` | FAILED. wiki.gg served a "Blocked" page. | +| https://www.reddit.com/r/hearthstone/comments/3be8cr/... (Patashu) | FAILED. The tool cannot fetch from reddit.com. | +| old.reddit.com version of the same | FAILED. The tool cannot fetch from old.reddit.com. | +| web.archive.org copy of the Patashu thread | FAILED. The tool cannot fetch from web.archive.org. | +| https://www.reddit.com/r/hearthstone/comments/1wf8f4k/ and /1wh6bq2/ (Assemble threads) | NOT FETCHED. reddit.com is blocked for the tool. A web search did not surface them either. | +| https://hearthstone.wiki.gg/wiki/Baron_Rivendare | Loaded. Last edited 26 Sept 2023. | +| https://hearthstone.wiki.gg/wiki/Assemble | Loaded. Last edited 22 Oct 2026. | +| https://hearthstone.wiki.gg/wiki/Mismatcher, /Fleshshape, /Rajh | Loaded. | +| https://news.blizzard.com/en-us/article/24304163/ ("Reign of the Black Empire Is Now Live!") | Loaded. Only marketing copy, with no mechanical detail. | +| https://us.forums.blizzard.com/en/hearthstone/t/166766 (37.0 patch notes thread) | Loaded. Only player complaints, with no rules. | +| https://www.playnews.gg/en/guides/hearthstone-assembly-how-does-the-reign-of-the-black-empire-keyword-work | FAILED (HTTP 403). | + +The Patashu "When do minions die?" post could not be read, so nothing in this file is sourced from it. The Advanced rulebook is the community document that grew out of that kind of testing, and it is the source used below. + +--- + +## Source 1: Advanced rulebook (hearthstone.wiki.gg) + +URL: https://hearthstone.wiki.gg/wiki/Advanced_rulebook + +**Summary.** +- A player action starts a **Sequence**. A Sequence is made of **Phases**. +- Each Phase or Event builds a **Queue** of every trigger that can respond, ordered by **order of play**. Order of play means when each trigger's entity entered play, oldest first. +- Once the first entry in a Queue starts resolving, the Queue is locked. +- Resolution is **depth-first**. A trigger's consequences are fully explored before the next sibling in the Queue resolves. +- Phases can nest. Deaths are only processed when the **outermost** Phase ends. At that point the game runs three steps in order: + 1. Aura Update (Health/Attack) + 2. Death Creation Step + 3. Aura Update (Other) +- If anything died, a **Death Phase** resolves the Death Events in order of play. A Death Phase is itself an outermost Phase, so this repeats until no new deaths occur. + +**Quotes (verbatim, short):** +- Sequence: "A Sequence is what begins when a Player Action is taken." +- Phase: "Phases are surrounding blocks created whenever one or more triggers/Events are raised." +- Queue: "A Queue is a sequential container where multiple simultaneous triggers or Events are held." +- Rule 2a: "A Queue is created and filled with all triggers that can respond, in order of play." +- Rule 2c: "A Queue becomes immutable once Hearthstone starts to resolve the first entry in it." +- Order of play: "Order of play means the order the Entities each Event/trigger is associated with entered play, from oldest to newest." +- Exceptions to order of play: + - Redemption "always Queues last". The rulebook's wording is that "all triggers follow order of play exactly" apart from Redemption. + - Poisonous "resolves in a queue with higher priority." +- Depth-first: "The process of resolving an event is similar to a depth-first search" + - Also: "the need to resolve an event before starting the next is a depth-first resolution." + - A newly raised event "immediately begins exploring its consequences, before resolving those remaining for the current trigger/Event." + - The rulebook contrasts this with: "(If multiple events could be partially resolved, that would be breadth-first)" + - Also: "a trigger cannot be nested inside itself" +- Outermost Phase: + - "When the outermost Phase resolves, Hearthstone will run several Steps, including processing Deaths and updating Auras." + - "but only the outermost Phase ending initiates the Death Creation Step." + - "After the outermost Phase ends, Hearthstone does an Aura Update (Health/Attack), then does the Death Creation Step." Aura Update (Other) follows the Death Creation Step (Rule 4a). +- Death Creation Step: + - It "Looks for all mortally wounded (0 or less Health)/pending destroy (hit with a destroy effect) Entities" + - It then "sends them from play to the graveyard and creates Death Events for each." +- Mortally wounded: + - "mortally wounded and pending destroy are ONLY converted into dead once the outermost Phase ends!" + - "negative (damaging/destroying/hindering/misdirecting) triggers ignore mortally wounded and pending destroy characters." + - "However, 'beneficial' (healing/buffing) triggers count mortally wounded, and AoE effects count mortally wounded." +- Death Phase chaining: + - Glossary, Phase: "Then if any Deaths occurred, a Death Phase starts, resolving Death Events in order of play." + - Glossary, Phase: "This continues until there are no new Deaths." + - Glossary, Death: "Because a Death is an Event, a new Phase begins where all simultaneous Deaths are Queued." + - All "Death Event triggers (Deathrattles, on-Death Secrets and on-Death triggered effects) are Queued". + - Combat section: "death processing is not done in the middle of a Phase, but is done between Phases". + - Forced Death Phase: "no follow-up Death Phases can be scheduled until the outer Phase ends". + - Example: "At the end of the Death Phase, Hearthstone checks for additional deaths" +- Auras: + - "whenever a minion is summoned (but not played), Hearthstone updates both kinds of Auras." + - "Auras are not recalculated due to minions leaving play or due to minions being stolen in the middle of a Phase." + - "Enchantments apply and modify Health/Attack/Mana costs the moment they are attached." + - Baron Rivendare is listed among the Aura Update (Other) auras (Rule 4c). +- Trigger-doubling cards (Rule 4d notes): + - "Cards that trigger effects multiple times, like Brann Bronzebeard, Baron Rivendare and Drakkari Enchanter, check before an Event starts." + - "This means if one Battlecry/Deathrattle/End of turn effect summons them, it will not trigger again." + - "But Deathrattles/End of turn effects that follow can benefit, for Aura (Other) is updated when you summon them." +- AoE damage (Rule DH1, paraphrased with an embedded quote): + - For AoE, "all of the Damage/Healing Events are created in play order", and they are then resolved in play order. + - Rule DH3 exceptions: Lightning Storm, Elemental Destruction, Dark Iron Skulker, and Lightbomb resolve each Damage Event as soon as it is created, in play order. They still pick their targets first. +- Flamestrike / Armorsmith example: "the Armorsmiths will be mortally wounded but remain in play, triggering on each Damage Event" +- Frothing Berserker example (Arcane Explosion): "all the damage occurs, then each 'On Damage' Event is resolved based on the order of play." +- Acolyte of Pain example (three Unstable Ghouls dying in one Phase): the Acolyte "is hit, triggered and draws three cards" before it dies. +- Predamage (DH7): "Just before a Damage Event is created, Predamage triggers will queue and resolve in play order." +- Combat damage: + - On damage taken: "the defender ALWAYS triggers first, regardless of order of play." + - On damage dealt: "the attacker ALWAYS triggers first, regardless of order of play." +- Summon position: + - Deathrattle summons appear "in the board position the Death Event remembered from when it was created." + - "Feign Death and similar effects summon minions at the right side of the board, not the minion's position." +- Summon Resolution Step: it "occurs after the first Aura Update (Health/Attack) but before Death Creation Step". + +Topics not found anywhere in the rulebook: the terms "Pre-phase" and "Main phase", Whirlwind specifically, and a Baron-dies-simultaneously example. The Baron case is covered on his card page (Source 4). + +## Source 2: Patashu, "Hearthstone Science: When do minions die?" (reddit) + +URL: https://www.reddit.com/r/hearthstone/comments/3be8cr/hearthstone_science_when_do_minions_die_20_minute/ + +**NOT LOADED.** reddit.com, old.reddit.com and web.archive.org are all blocked for the fetch tool. Nothing in this file comes from this source. Do not cite it in the course until someone reads it manually. + +## Source 3: Assemble (Reign of the Black Empire) + +### 3a. Reddit threads 1wf8f4k and 1wh6bq2 + +**NOT LOADED** (reddit.com is blocked). A web search did not surface them. + +### 3b. Wiki: https://hearthstone.wiki.gg/wiki/Assemble + +Loaded. Last edited 22 Oct 2026. Added in Patch 37.0.0.254477 (2026-10-13). + +**Reminder text (verbatim):** "Get a random minion. Change half its text." + +**How it works:** +- It "adds a random minion to the hand, then presents two other minions." +- The added minion "can have either the top or bottom half of its text replaced with the corresponding text from either presented minion". +- Paraphrased: the minion keeps its own stats, cost, name, art, rarity, and minion type. Only the text changes. + +**Constraints (paraphrased unless quoted):** +- The pool is about 250 curated minions whose effects can be split in half. +- "The Assembled minion must have to have its text altered." The typo is in the source. +- The presented options cost the same as the main minion or up to 2 more. For 9- and 10-cost minions, the options can cost 8, 9, or 10. +- As with Discover, the minions are from your class or neutral unless the card says otherwise. +- When a card names a pool (for example, Fleshshape's "Deathrattle minion"), only the middle (default) minion must come from that pool. + +**Not specified on the page:** what exactly "top half" and "bottom half" mean. The page does not say whether the split is trigger vs effect or first line vs second line, and it gives no worked text example. + +Cards with Assemble (13): Chisel, Darkest Depths, Sandcrafter, Shadowcrafter, Steward of the Vale, Thaw From Ice, Tinker, Attentive Tender, F3TCH3R, Fleshshape, Shadow Sculpt, Mismatcher, Rajh. + +### 3c. Example card pages (wiki.gg) + +- **Mismatcher** (Priest, Epic, 5 mana 4/6 Aberration): "Taunt. Battlecry: Assemble a minion that costs (3) or less with cards that cost (5) or more." +- **Fleshshape** (Death Knight, Rare spell, 3 mana): "Assemble a Deathrattle minion. Give all minions in your hand +2/+2." +- **Rajh** (Paladin, Legendary, 5 mana 4/4 Titan-forged): "Battlecry: Assemble a minion from any class. Transform it into a Passive Hero Power." + - Note: "Rajh Assembles a minion from a smaller pool that includes no Battlecry or Deathrattle effects." + +### 3d. Blizzard news: https://news.blizzard.com/en-us/article/24304163/ + +Loaded ("Reign of the Black Empire Is Now Live!"). It has no mechanics. +- "Create minions unlike any Hearthstone has seen before with the new Assemble keyword." +- "Combine minions from a curated pool to build custom creations." + +## Source 4: Baron Rivendare (wiki.gg) + +URL: https://hearthstone.wiki.gg/wiki/Baron_Rivendare. Last edited 26 Sept 2023, so newer changes may be missing. + +**Card:** +- FP1_031, Curse of Naxxramas. A Core version, CORE_FP1_031, also exists. +- 4 mana 1/7 Legendary, Undead (minion type added in Patch 25.0, 2022-11-29). +- Text: "Your minions trigger their Deathrattles twice." + +**Notes:** +- Simultaneous death: "However, if he dies at the same time as a minion with a Deathrattle, the Deathrattle is not doubled." +- "Baron Rivendare's effect, being an aura, only appears and disappears between Phases when Aura Update (Other) runs". The page says this runs just after the death-processing step. +- Queuing, paraphrased: "The two activations of the Deathrattle do not queue/resolve separately". The Deathrattle queues once, and when it resolves, the aura makes it activate twice. +- "The second activation of the Deathrattle is immediately after the first activation resolves", which is before the next queued Death trigger. +- "Both Deathrattles are treated separately." For example, Webspinner can produce two different cards. +- "Rivendare's ability is not duplicated or multiplied by having more than one Baron Rivendare in play." +- "Baron Rivendare's effect also works on remotely triggered Deathrattles such as from Play Dead." + +--- + +## Answers + +**a. Does a Deathrattle trigger twice if Baron dies in the same AoE?** +No. +- The Baron page says: "if he dies at the same time as a minion with a Deathrattle, the Deathrattle is not doubled." +- Mechanism, from the rulebook: + - Baron's doubling is an Aura (Other). + - At the end of the outermost Phase, the order is Aura Update (Health/Attack), then the Death Creation Step (which removes Baron together with the other minion), then Aura Update (Other). + - Aura Update (Other) runs after Baron has left play, so his aura is gone before the Death Phase resolves the Deathrattles. + - The rulebook also says doubling cards "check before an Event starts." + +**b. In what order do triggers resolve, and is it depth-first?** +- When an Event (or a set of simultaneous Events) occurs, a Phase is opened. A Queue is filled "with all triggers that can respond, in order of play" (oldest entity first). Simultaneous Events are queued in order of play, and so are each Event's triggers (Rule 2b). +- Exceptions: Redemption always queues last, Poisonous has higher priority, and in combat the defender's on-damage trigger goes first. +- The Queue locks once resolution starts. Minions summoned mid-resolution cannot join it. +- Resolution is depth-first: each trigger's resulting Events open nested Phases that resolve completely before the next sibling trigger. +- Deaths are not processed in nested Phases. They wait until the outermost Phase ends. +- The rulebook does not use the terms "Pre-phase" or "Main phase". It does have Predamage triggers, which queue and resolve in play order just before a Damage Event is created. + +**c. When are mortally wounded minions removed, and how do Death Phases chain?** +- A minion at 0 or less Health is "mortally wounded" and stays in play. AoEs and beneficial triggers still count it, while negative triggers ignore it. +- When the outermost Phase ends, the game runs Aura Update (Health/Attack), then the Death Creation Step, then Aura Update (Other). +- The Death Creation Step removes every mortally wounded or pending-destroy entity at once and creates a Death Event for each. +- A Death Phase then queues all on-death triggers (Deathrattles, on-Death Secrets, on-Death triggered effects) in order of play. +- The Death Phase is itself an outermost Phase. If its Deathrattles mortally wound more minions, another Death Creation Step and Death Phase follow, "until there are no new Deaths." + +**d. How does "trigger twice" work (Baron, Brann, Drakkari)?** +- These are auras in the Aura Update (Other) category. They "check before an Event starts." +- If one of these cards is summoned by a Battlecry, Deathrattle, or End of turn effect, that same effect does not trigger twice. Later effects in the same Sequence can benefit, because summoning updates Aura (Other). +- For Baron, the Deathrattle is queued once. When it resolves, the aura makes it activate twice in a row (two separate activations, back to back) before the next queued trigger. +- A Baron who is summoned or stolen mid-Phase only starts or stops doubling at the next Aura Update (Other). +- Rules caveat: the Baron page describes the doubling as applied at resolution of a single queue entry, while the rulebook says "check before an Event starts". Both rule out a doubling source that appears mid-Event. The precise "queue time vs resolve time" distinction is not stated more explicitly in either source. + +**e. How does Assemble work?** +- Reminder text: "Get a random minion. Change half its text." +- A random minion (class or neutral, from a curated pool of about 250 minions with splittable text) is added to your hand. You are then shown two other minions. +- You replace either the top half or the bottom half of the added minion's text with the corresponding half from one of the two presented minions. +- The base minion keeps its own cost, stats, name, art, rarity, and type. Its text must change. +- The presented options cost the same as the base minion or up to 2 more. +- Pool restrictions on the card apply only to the base minion. +- Examples: + - Mismatcher: "Taunt. Battlecry: Assemble a minion that costs (3) or less with cards that cost (5) or more." + - Fleshshape: "Assemble a Deathrattle minion. Give all minions in your hand +2/+2." + - Rajh: "Battlecry: Assemble a minion from any class. Transform it into a Passive Hero Power." +- None of the sources I read defines whether a "half" means trigger vs effect. + +**f. Other teaching-useful details** +- AoE damage: all Damage Events are created first, in play order, and then resolved in play order. So a Flamestrike with two Armorsmiths on board grants armor for every Damage Event before the Armorsmiths die. Frothing Berserker with Arcane Explosion follows the same "all damage, then each on-damage Event in order of play" pattern. +- Acolyte of Pain draws once per Damage Event, even when it is mortally wounded by an earlier one. +- Lightning Storm, Elemental Destruction, Dark Iron Skulker, and Lightbomb are exceptions: they resolve each Damage Event as soon as it is created. +- In combat, the defender's on-damage trigger goes before the attacker's, regardless of order of play. +- Deathrattle summons appear at the dead minion's remembered board position. Feign Death-style effects summon on the right side of the board. + +## Draft claims that are WRONG or unverified + +1. **"If Baron Rivendare and Loot Hoarder die to the same 1-damage AoE, Loot Hoarder's Deathrattle triggers only once because Baron is removed in the Death Creation Step before deathrattles queue."** + - The outcome is CORRECT: the Baron page says "the Deathrattle is not doubled". + - The stated reason is IMPRECISE. The cause is that Baron's doubling is an aura, and Aura Update (Other) runs after the Death Creation Step removes him. His aura is therefore gone when the Death Phase resolves. + - Suggested rewording: "...because both are removed together in the Death Creation Step, and the following Aura Update (Other) removes Baron's doubling aura before the Death Phase resolves Deathrattles." + - Also note: Loot Hoarder is 2/1 and Baron Rivendare is 1/7, not 1 HP. The draft's "1 HP each" is wrong for Baron, so a 1-damage AoE would not kill him. Fix the example, for example by using a 7+ damage AoE, or by saying "both die in the same AoE". + +2. **"Triggers are ordered by play order and resolved depth-first."** + - MOSTLY CORRECT. + - The rulebook term is "order of play" (when each entity entered play, oldest first). + - Add the caveats: Redemption queues last, Poisonous has priority, and the combat defender triggers first. + - Queues lock once resolution starts. + - Deaths are deferred to the end of the outermost Phase, so depth-first does not mean deaths happen mid-chain. + +3. **"Mortally wounded minions stay on board until the outermost phase ends, then Death Creation Step removes all simultaneously in play order and queues Deathrattles."** + - Largely CORRECT, with these corrections: + - (i) An Aura Update (Health/Attack) runs before the Death Creation Step. A minion can be saved or killed by an aura change at that point. + - (ii) Pending-destroy minions are included, not just mortally wounded ones. + - (iii) The Death Creation Step removes them and creates Death Events. A Death Phase then queues and resolves the on-death triggers in order of play. That includes Deathrattles, on-Death Secrets, and on-Death triggers, not only Deathrattles. + - (iv) The process repeats until there are no new deaths. + - "Removes all simultaneously in play order" mixes two things. Removal is simultaneous. The order of play applies to how the Death Events and their triggers are queued. + +4. **"Assemble splices the top half (trigger) of one card with the bottom half (effect) of another."** + - UNVERIFIED / likely MISLEADING. + - The wiki says the base minion (randomly generated) keeps its stats, cost, name, and type. Either its top or its bottom half is replaced with the corresponding half of one of two presented minions. + - So it is not "top from card A plus bottom from card B" in a fixed direction. The player chooses which half to replace, and it is a base card plus one replacement half. + - No source I read defines a half as "trigger" vs "effect". Do not make that claim until it is verified, for example from the reddit threads (which could not be loaded) or from in-game testing. + +The Patashu reddit post and both Assemble reddit threads remain unread. Any course claim attributed to them needs a manual check. diff --git a/docs/superpowers/specs/2026-10-28-systemic-design-course-design.md b/docs/superpowers/specs/2026-10-28-systemic-design-course-design.md new file mode 100644 index 0000000..3bf0b3a --- /dev/null +++ b/docs/superpowers/specs/2026-10-28-systemic-design-course-design.md @@ -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//` 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.