Ironsmith is a Magic: The Gathering rules engine, oracle-text compiler, and browser-playable game runtime built primarily in Rust. You can try it out (and even play with your friends!) at https://chiplis.com/ironsmith
The project is organized around one central idea: card behavior should come from parsed rules text whenever possible, not from hand-written one-off logic. That design shows up everywhere in the codebase:
- card definitions are funneled through a parser and lowering pipeline
- effects are represented as structured runtime executors
- game actions emit typed events
- replacement, prevention, triggers, and state-based checks consume those events
- rule evaluation is split into reusable subsystems instead of being hidden inside a single game loop
The result is a codebase that is useful both as a playable engine and as a tooling platform for parser iteration, semantic audits, and registry generation.
What This Repository Contains
At a high level, this repo contains:
- a Rust engine crate in
src/ - a CLI package that exposes the interactive
ironsmithbinary incrates/ironsmith-cli/ - a tooling package with parser/report/audit binaries in
crates/ironsmith-tools/ - a wasm bridge and browser UI in
src/wasm_api.rsandweb/ui/ - Python and shell scripts that generate registries, stream Scryfall data, and rebuild frontend artifacts in
scripts/andrebuild-wasm.sh
Architecture Overview
Ironsmith is easiest to understand as four cooperating layers:
- Card text ingestion and compilation
- Runtime execution of effects and abilities
- Event-driven replacement, prevention, and trigger handling
- Rule enforcement through combat, damage, state-based actions, and the game loop
The sections below focus on the subsystems that matter most when working on the engine.
Parser and Card Compilation Pipeline
The parser is the front door for most of the engine.
The entry point most tooling uses is CardDefinitionBuilder::parse_text(...) in src/cards/builders.rs. From there, the pipeline looks roughly like this:
- Raw card text is tokenized and split into lines, clauses, and sentence fragments.
- The parser builds a card AST describing static abilities, triggered abilities, activated abilities, modal text, additional costs, and statement effects.
- The AST is normalized into a form that is easier to lower consistently.
- Reference analysis resolves words like “it”, “that creature”, “that player”, and tagged objects produced by earlier effects.
- Effects and triggers are lowered into runtime engine structures such as
Effect,Trigger,StaticAbility,OptionalCost, andAlternativeCastingMethod. - The compiled result is finalized into a
CardDefinition.
The main hubs for that pipeline are:
src/cards/builders.rs: public builder surface and parser entrypointssrc/cards/builders/parse_rewrite/effect_pipeline.rs: rewrite parser/lowering orchestration used by the public builder APIssrc/cards/builders/parse_rewrite/parse.rs: preprocessing, CST construction, semantic IR construction, and unsupported classificationsrc/cards/builders/parse_rewrite/lower.rs: normalization and lowering from rewrite semantic items into runtime abilities/effectssrc/cards/builders/parse_rewrite/lexer.rs:logoslexer and token/cursor typessrc/cards/builders/parse_rewrite/leaf.rs:winnowleaf parsers for costs, mana groups, counts, and type-line structuresrc/cards/builders/parse_rewrite/effect_sentences/: rewrite-owned effect sentence parsingsrc/cards/builders/parse_rewrite/activation_and_restrictions.rs: rewrite-owned trigger, activation, and restriction parsingsrc/cards/builders/parse_rewrite/keyword_static/: rewrite-owned keyword and static-line parsingsrc/cards/builders/parse_rewrite/object_filters.rs: rewrite-owned object/filter parsingsrc/cards/builders/parse_rewrite/reference_model.rs: import/export model for pronoun and tag resolution across effect sequences
Parser Design Notes
Several project choices are worth calling out because they strongly shape how parser work gets added:
- The parser is rule-index driven, not a giant chain of ad hoc
ifstatements.src/cards/builders/parse_rewrite/rule_engine.rsdefines reusable keyed rule tables with priorities and diagnostics for unsupported patterns. - Reference tracking is explicit. The
ReferenceEnv,ReferenceImports, andReferenceExportsmodel lets a sequence like “destroy target creature. Its controller loses 2 life.” carry meaning across clauses without fragile string hacks. - The pipeline distinguishes parsing from lowering, and rewrite semantic items already carry parsed runtime payloads for the main line families. Lowering consumes those payloads directly instead of reparsing semantic line text.
- Unsupported content can be preserved intentionally.
parse_text_allow_unsupported(...)and parser annotations are there so tooling can keep moving while coverage improves.
Hand-Written Definitions Still Go Through The Parser
Even the hand-maintained cards in src/cards/definitions/ are meant to stay on the parser path.
build.rs enforces a boundary that prevents definitions from bypassing the text compiler and directly hardcoding most effect/ability wiring. In practice that means handwritten definition files are primarily for metadata plus oracle text, while runtime behavior should still come from the same parse/normalize/lower flow used for generated cards.
That boundary is important because it keeps the engine honest: parser coverage improves only if real cards actually depend on it.
Generated Registry and SQLite
The repo expects a local SQLite registry DB for wasm builds and semantic audits. A gitignored cards.json Scryfall-style dump is still used to ingest canonical card data into that DB.
scripts/download_scryfall_cards.pydownloads the official Scryfalldefault-cardsbulk export, keeps paper cards that are legal in Commander, Standard, Modern, Legacy, or Vintage, and collapses printings down to one canonical paper record per card.scripts/stream_scryfall_blocks.pystreams playable cards as parser input blocks.scripts/generate_baked_registry.pyconverts registry DB rows into generated Rust source used by the registry build.build.rsgenerates a stub registry unless thegenerated-registryfeature is enabled.
This keeps normal engine iteration fast while still supporting “compile the world” style workflows when needed.
Events System
Ironsmith’s event system is the bridge between action execution and rules processing.
The central pieces are:
src/events/mod.rs: typed event modules and theEventwrappersrc/events/traits.rs:GameEventType,ReplacementMatcher, andEventKindsrc/events/raw_event.rs: shared event envelope used by replacement and trigger pipelinessrc/event_processor.rs: replacement processing and event application
How Events Work
Each event type implements GameEventType. Examples include:
- damage events
- zone changes and enters-the-battlefield events
- card draw and discard events
- life gain and loss events
- counter movement events
- spell cast / spell copied / ability activated events
- combat and phase-step events
Events are wrapped in RawEvent, which adds provenance metadata and gives both the trigger system and replacement system a shared envelope to work with.
That shared envelope matters because the engine often needs to answer questions like:
- what happened?
- who was affected?
- what object or player was involved?
- what last-known-information snapshot should matching use?
- which source action caused this event?
Why The Event Model Matters
The event layer is doing more than logging. It is the coordination point for:
- replacement effects
- prevention effects
- trigger matching
- UI/provenance tracing
- “what happened this turn” bookkeeping
Because events are strongly typed and categorized with EventKind, the engine can avoid a lot of brittle special-case coupling between effect execution and downstream rules logic.
Effects System
The effects system is the executable vocabulary of the engine.
Core files:
src/effect.rs: theEffectwrapper, IDs, values, and outcomessrc/effects/mod.rs: modular effect organization and exportssrc/effects/executor_trait.rs: theEffectExecutortraitsrc/executor.rs: runtime execution context and effect dispatch helpers
Effect Model
An effect in Ironsmith is not just “do a thing.” It carries enough structure to support:
- target requirements
- dynamic values like
X - conditional “if you do” follow-ups
- tagging objects and players for later clauses
- choice prompts and modal branching
- cost execution
- emitted game events
- structured outcomes that later effects can inspect
Execution returns an EffectOutcome, which combines:
- control-flow status
- structured payloads
- emitted triggerable events
- non-triggerable execution facts
That outcome model is what allows the engine to express patterns like:
- “destroy target creature. If you do, draw a card.”
- “you may”
- “choose one or more”
- “for each”
- “unless”
- “repeat this process”
without collapsing everything into handwritten one-off spell logic.
Modular Effect Families
Effect implementations are grouped by domain in src/effects/:
cards/: draw, mill, discard, reveal, search, surveil, scrycombat/: PT changes, fight, damage prevention, goad, enter attackingcomposition/: sequencing, conditionals, loops, tags, votes, choice orchestrationcounters/: add/remove/move/proliferate countersdamage/: direct damage, redirection, prevention shieldslife/: gain, lose, exchange, set life totalsmana/: add mana, pay mana, choose colors, commander color identity supportpermanents/: tap/untap, transform, regenerate, ninjutsu, soulbond, renown, saddleplayer/: extra turns, monarch, energy, poison, “you win/lose the game”, casting permissionsreplacement/,delayed/,continuous/,stack/,tokens/,zones/
The composition family is especially important. A lot of “real card text” complexity is not in primitive verbs like damage or draw, but in how smaller actions get stitched together. That is where effects like SequenceEffect, ConditionalEffect, MayEffect, ForEachTaggedEffect, UnlessPaysEffect, and ReflexiveTriggerEffect become the real grammar of the engine.
Replacement and Trigger Flow
Relevant files:
Ironsmith uses typed matchers for both triggers and replacement effects:
- replacement matchers decide whether an event is modified, prevented, redirected, or replaced
- trigger matchers decide whether an event should enqueue triggered abilities
event_processor.rs implements a rules-aware replacement loop modeled around MTG rules 614-616:
- find applicable replacement effects
- sort by priority
- let the affected player choose when multiple effects are tied
- apply one effect at a time
- prevent one-shot replacements from reapplying indefinitely
This is the part of the engine that turns a simple event like “object would enter the battlefield” into more realistic outcomes such as:
- enters tapped
- enters with counters
- enters as a copy
- discard/pay-life or redirect interactions
- “instead” effects that replace the event with a new effect sequence
Rules System
The rules layer is where executable effects meet broader game legality and state maintenance.
Core files:
src/rules/mod.rssrc/rules/combat.rssrc/rules/damage.rssrc/rules/state_based.rssrc/game_loop/mod.rs
Combat Rules
The combat rules module handles legality and combat-specific heuristics such as:
- attack/block restrictions
- flying, reach, shadow, horsemanship, fear, intimidate, skulk
- menace and minimum blockers
- protection-based blocking failures
- “can’t block”, “can’t be blocked”, and related restrictions
The module works with calculated characteristics, not just printed card state, so static abilities and continuous effects can change combat outcomes correctly.
Damage Rules
The damage subsystem handles keyword-sensitive damage processing:
- deathtouch
- lifelink
- infect
- wither
- trample excess calculations
This layer is intentionally separated from raw effect execution so the rest of the engine can ask consistent questions like “what does 3 damage from this source actually mean?”
State-Based Actions
src/rules/state_based.rs checks and applies state-based actions such as:
- lethal damage and zero toughness deaths
- planeswalkers with zero loyalty
- players losing for life, poison, or commander damage
- legend rule enforcement
- Auras or Equipment falling off
- token/copy cleanup
- counter annihilation
- saga sacrifice
- commander command-zone handling
This is a major part of what makes the engine feel like MTG instead of just a spell resolver.
The Game Loop
The game loop in src/game_loop/ integrates:
- priority passing
- casting and activation decisions
- stack resolution
- target selection
- state-based action checks
- triggered ability queuing
- combat steps and combat damage
- turn advancement
The rules modules are deliberately separate, but this is where they get composed into an actual playable game.
Project Structure
Here is the most important repository structure at a glance:
Engine Core
src/lib.rs: public engine surface and re-exportssrc/game_state.rs: authoritative mutable game statesrc/game_loop/: stack, priority, combat, targeting, and turn executionsrc/turn.rs: phase/step sequencing and priority helperssrc/decision.rsandsrc/decisions/: decision interfaces and decision-context payloads
Parser and Card Definition Stack
src/cards/builders.rs: builder API plus parser/compiler pipeline entrypointssrc/cards/builders/: parser, normalization, lowering, reference analysissrc/cards/definitions/: handwritten metadata/oracle sourcessrc/cards/tokens/: built-in token definitionssrc/compiled_text/: oracle-like rendering of compiled output for audit and comparison tooling
Runtime Semantics
src/effects/: modular effect executorssrc/events/: typed event definitions and matcherssrc/triggers/: modular trigger matcherssrc/rules/: combat, damage, and SBAssrc/static_abilities/: static ability representation and helperssrc/targeting/: targeting validation, ward, and legal-target computations
Frontend and Wasm
src/wasm_api.rs: wasm-facing game wrapperweb/ui/: React/Vite UI for browser play and inspectionweb/wasm_demo/: lightweight wasm demo outputpkg/: generated wasm package artifacts
Tooling and Reports
crates/ironsmith-cli/: package that exposes the interactive CLI binarycrates/ironsmith-tools/: package containing parser and audit binariessrc/bin/: source for the binaries listed belowscripts/: Python helpers for Scryfall streaming and registry generationreports/: generated parser/error/cluster reportsreports/engine-status.sqlite3: default SQLite index for canonical compilation history and imported card tags
Available Binary Utilities
The repo ships several useful binaries. Most of them live in the ironsmith-tools package; the interactive game CLI lives in ironsmith-cli.
Interactive CLI
ironsmith- Package:
ironsmith-cli - Purpose: launch an interactive two-player game in the terminal, with optional custom hands/decks/battlefield setup
- Package:
Parser Inspection and Conversion
-
compile_oracle_text- Package:
ironsmith-tools - Purpose: parse card text and print compiled/oracle-like output, optionally with traces or raw debug output
- Package:
-
parse_card_text- Package:
ironsmith-tools - Purpose: batch-parse
Name: ...card blocks from stdin and summarize failures, error buckets, and pattern matches
- Package:
-
export_compiled_oracle_csv- Package:
ironsmith-tools - Purpose: export CSVs comparing source oracle text against compiled oracle-like output
- Package:
Audit and Coverage Utilities
-
audit_compiled_cards- Package:
ironsmith-tools - Purpose: inspect compiled output for parse failures, unimplemented markers, and object-filter usage
- Package:
-
audit_oracle_clusters- Package:
ironsmith-tools - Purpose: cluster oracle text, compare parser output semantically, and produce JSON/CSV audits for large card sets
- Package:
-
audit_parsed_mechanics- Package:
ironsmith-tools - Purpose: tally which mechanics and fallback reasons appear across parsed cards
- Package:
-
audit_unimplemented_partition- Package:
ironsmith-tools - Purpose: analyze a subset/partition of cards that still contain unimplemented or fallback content
- Package:
-
report_replacement_effect_parse_status- Package:
ironsmith-tools - Purpose: produce a focused parse-status report for replacement-effect-heavy cards
- Package:
-
dump_false_positive_texts- Package:
ironsmith-tools - Purpose: dump oracle text and compiled output for a list of suspected semantic false positives
- Package:
Report Rebuild and Data Export
-
rebuild_reports- Package:
ironsmith-tools - Purpose: orchestrate parser report regeneration, including semantic audit artifacts and cluster/error CSVs
- Package:
-
sync_card_status_db- Package:
ironsmith-tools - Purpose: compile canonical
registry_cardentries from SQLite and append only changed rows into the compilation history tables
- Package:
-
cleanup_compilation_history- Package:
ironsmith-tools - Purpose: delete historical
card_compilationrows while keeping the latest snapshot for each card
- Package:
-
sync_registry_db- Package:
ironsmith-tools - Purpose: ingest canonical cards from
cards.jsoninto the SQLiteregistry_cardtable and prune removed cards
- Package:
-
import_card_tags- Package:
ironsmith-tools - Purpose: import tag research CSVs into the SQLite status DB as current
(card_name, tag)rows
- Package:
-
export_cedh_support_report- Package: root
ironsmithcrate - Purpose: fetch cEDH event/deck data and generate support coverage reports for popular cards
- Notes: requires the
toolingfeature
- Package: root
Helper Scripts
Not everything in the repo is a Rust binary. A few non-binary helpers are part of the normal workflow:
rebuild-wasm.sh: rebuild wasm artifacts, refresh semantic score caches, and sync frontend assetsscripts/stream_scryfall_blocks.py: stream playable Scryfall entries into parser-friendly blocksscripts/generate_baked_registry.py: generate the parser-backed registry used whengenerated-registryis enabledweb/ui/scripts/peer-server.mjs: local PeerJS signaling server for multiplayer UI development
Development Workflow
Requirements
You will typically want:
- Rust/Cargo
- Python 3
wasm-packfor wasm buildspnpmfor the React UI- a local SQLite registry DB for wasm builds and semantic audits
- a local
cards.jsondump when you need to refresh canonical registry data from Scryfall
cards.json is intentionally gitignored, along with most generated reports, CSV/JSON outputs, and the SQLite engine status DB.
SQLite Queries
Once the DB has been populated, a few useful queries are:
SELECT card_name, similarity_score, semantic_mismatch FROM latest_card_compilation ORDER BY similarity_score ASC LIMIT 20;
SELECT lcc.card_name, lcc.similarity_score, ct.tag FROM latest_card_compilation AS lcc JOIN card_tagging AS ct ON ct.card_name = lcc.card_name WHERE ct.tag = 'consult' ORDER BY lcc.similarity_score ASC, lcc.card_name ASC;
SELECT ot.tag FROM oracle_tag AS ot LEFT JOIN card_tagging AS ct ON ct.tag = ot.tag WHERE ct.tag IS NULL ORDER BY ot.tag ASC;
Common Commands
Refresh the local filtered Scryfall dump:
python3 scripts/download_scryfall_cards.py
Run the interactive CLI:
cargo run -p ironsmith-cli --bin ironsmith --
Probe the parser for a single card:
cargo run -p ironsmith-tools --bin compile_oracle_text -- \ --name "Lightning Bolt" \ --text $'Mana cost: {R}\nType: Instant\nLightning Bolt deals 3 damage to any target.'
Ingest canonical cards into the SQLite registry:
cargo run -p ironsmith-tools --bin sync_registry_db -- \ --cards cards.json \ --db-path reports/engine-status.sqlite3
Compile/update the SQLite engine status index from canonical registry rows:
cargo run -p ironsmith-tools --bin sync_card_status_db -- \ --db-path reports/engine-status.sqlite3
Prune historical compilation rows and keep only the latest snapshot per card:
cargo run -p ironsmith-tools --bin cleanup_compilation_history -- \ --db-path reports/engine-status.sqlite3
Import tag research CSVs into the SQLite index:
cargo run -p ironsmith-tools --bin import_card_tags -- \ --csv reports/cards/oracle-tag-research-consult-20260323T141838Z.csv \ --db-path reports/engine-status.sqlite3
Sync the canonical functional oracle tag catalog from Scryfall:
cargo run -p ironsmith-tools --bin sync_oracle_tags -- \ --db-path reports/engine-status.sqlite3
Populate card_tagging from Tagger’s oracle-card memberships:
cargo run -p ironsmith-tools --bin sync_card_tagging -- \ --cards cards.json \ --db-path reports/engine-status.sqlite3
Resume from the 100th oracle tag and process the next 250 tags:
cargo run -p ironsmith-tools --bin sync_card_tagging -- \ --cards cards.json \ --db-path reports/engine-status.sqlite3 \ --start 100 \ --limit 250
Batch-parse card blocks from cards.json:
python3 scripts/stream_scryfall_blocks.py --cards cards.json \
| cargo run -p ironsmith-tools --bin parse_card_text --Regenerate wasm artifacts and semantic caches from SQLite:
./rebuild-wasm.sh --threshold 0.99
Run the web UI:
cd web/ui
pnpm install
pnpm devRun the local multiplayer signal server:
Run tests:
Features
Important Cargo features from Cargo.toml:
serialization: enables serde-based serialization supporttooling: enables tooling-oriented binaries and support pathsgenerated-registry: generates and bakes the parser-backed registry fromcards.jsonwasm: enables the wasm API and generated-registry-backed browser buildengine-integration-tests: enables larger engine integration test coverageparser-tests/parser-tests-full: parser-focused test gates
Frontend Notes
The browser UI is a React/Vite app that talks to the engine through wasm exports. The wasm-facing bridge lives in src/wasm_api.rs, where game state is converted into UI-friendly snapshots and grouped battlefield representations.
The frontend package is in web/ui/package.json. It includes:
pnpm devpnpm buildpnpm previewpnpm lintpnpm signal
The existing web/ui/README.md also has notes on PeerJS signaling configuration for multiplayer development.
Why This Project Is Structured This Way
Ironsmith is doing two jobs at once:
- It is a game engine that must execute complicated MTG interactions correctly.
- It is a parser and tooling platform that needs feedback loops for coverage, diagnostics, and semantic comparison.
That is why the repo contains both:
- “play the game” code such as
game_loop,rules,events, andeffects - “improve the compiler” code such as
compiled_text, parser audits, report rebuilders, cluster analysis, and generated-registry tooling
If you are new to the codebase, the most productive reading order is usually: