Files
systemic-game-design-course/docs/research/architecture-readings.md
T
2026-10-28 23:44:08 +01:00

32 KiB
Raw Blame History

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 Phases), 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:

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):
      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)