mtg/decks/reaper-king-no-more/plan.md
2026-09-12 00:46:38 -04:00

520 lines
27 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Plan: The Reaper, King No More — Bracket 3
How to use AI + local tooling to find cards, test them, and simulate the deck
entirely on this machine.
## 0. Read this first: the commander's actual text
Scryfall oracle text (verified 2026-09-07):
> **The Reaper, King No More** — {2/B}{2/R}{2/G}
> Legendary Artifact Creature — Scarecrow
> When The Reaper enters, put a -1/-1 counter on each of up to two target creatures.
> Whenever a creature an opponent controls with a -1/-1 counter on it dies, you may
> put that card onto the battlefield under your control. **Do this only once each turn.**
Three consequences that shape the whole build, and one correction to the README:
1. **Color identity is Jund (B/R/G)**, and the hybrid {2/X} cost means it can be
cast off any 6 lands — it is castable in any deck, but our identity is BRG.
2. **"Do this only once each turn" hard-caps the commander as a combo engine.**
An infinite -1/-1 loop does *not* produce infinite steals off The Reaper. The
loop's payoff has to come from somewhere else (a sac outlet + drain, Nest of
Scarabs tokens, Blood Artist-style drains).
3. **The steal trigger only reads creatures an _opponent_ controls.** So the deck
wants ways to spread -1/-1 counters onto *their* board, not just ours. This is a
real tension with the Hapatra/Blowfly engine, which mostly counters our own stuff.
The README's framing — "infinite combo with aristocrats ETB + -1/-1 synergy" — is
sound, but the commander is a *value/removal* engine, not the combo's payoff.
Build it as: aristocrats combo deck that happens to be in Jund, with The Reaper as a
repeatable one-per-turn removal-plus-theft engine on top.
## 1. Bracket 3 constraints (the guardrails every tool must enforce)
From the official brackets article:
| Rule | Bracket 3 |
| --- | --- |
| Game Changers | **Up to 3** |
| Mass land denial | **None** |
| Extra turns | Sparse, no chaining/looping |
| Two-card infinite combos | **None that go off cheaply within ~the first 6 turns** |
| Tutors | Not explicitly limited (but keep them honest) |
The Game Changers list is 53 cards as of 2026-09-06 and is queryable on Scryfall as
`is:gamechanger` — so bracket legality is fully scriptable, no manual list to maintain.
**Our house rule, stricter and simpler than the official wording: no two-card
infinite combos that work in a vacuum.** If two cards go infinite with no third
enabler and no board state required, they're out. Two-card lines that need an
existing creature, a sac outlet, or a specific board are fine, as are 3+ card combos.
This is checkable directly against the offline combo DB by filtering `len(uses) == 2`.
**Good news for the README's plan: the Blowfly package passes this rule.** Checked
against the offline DB — both `Blowfly Infestation + Hapatra` and
`Blowfly Infestation + Nest of Scarabs` list identical `notablePrerequisites`:
> You control at least two other creatures with toughness equal to 1.
> You have a way to put a -1/-1 counter on a creature.
So they are two *named* cards but four moving parts, needing a real board and a third
enabler. That is a build-around engine, not a vacuum combo — exactly the kind of thing
bracket 3 is meant to allow. Keep the package. The rule to enforce instead: **don't
add tutors that assemble it**, and don't add a genuine 2-card-in-a-vacuum line
alongside it.
## 1b. The blacklist — cards I won't play
Separate from legality and separate from bracket rules: `blacklist.txt` holds cards
excluded **by preference**, each with a reason. Format is `Card Name | reason`.
```
Sol Ring | boycott, not a power-level call
Talisman of Indulgence | prefer land ramp
Arcane Signet | prefer land ramp
```
Two categories so far:
- **Boycott.** Sol Ring is out for personal reasons. No game-logic justification is
needed or implied, and the tooling should never argue about it.
- **Playstyle: ramp with lands, not rocks.** We're in green, so use it. All the
Talismans, Signet, Mind Stone, Fellwar Stone, Chromatic Lantern etc. are out. This
one *does* have a cost — see below.
This is wired into both halves of the toolchain, which is the point of keeping it in
one file:
1. `bracket_check.py` reports blacklisted cards **in their own section**, clearly
marked "personal preference, not a bracket rule", so they never get confused with
an actual bracket-3 violation.
2. `db.tagged()` filters them out of every search, so they don't get re-suggested
later. Verified: `db.tagged("mana-rock")` no longer returns Sol Ring.
**The honest tradeoff on cutting rocks:** land ramp is a turn slower and Sol Ring is
the single best accelerant in the format. The deck swapped 5 rocks for Rampant Growth,
Harrow, Wood Elves, Springbloom Druid, and Binding the Old Gods. Average MV went
2.92 → 3.02 and the deck is measurably slower out of the gate. Two things soften it:
Wood Elves and Springbloom Druid are *creatures*, so they feed the sacrifice theme
that rocks never would, and Binding the Old Gods is removal stapled to a fetch.
**Measured, so it's settled.** 20,000 games each, identical seed, the only change
being 5 rocks (incl. Sol Ring) swapped for the 5 land-ramp spells:
| build | commander by T6 | engine assembled | -1/-1 online | 3+ lands on T4 |
| --- | --- | --- | --- | --- |
| land ramp (current) | **46.9%** | **35.7%** | median T6 | 95.5% |
| rocks (blacklisted) | 54.8% | 34.6% | median T6 | 95.2% |
The cost is real but small: **~8 percentage points on casting the commander by T6**,
and essentially nothing everywhere else — engine assembly is actually a hair *better*
with land ramp, and the -1/-1 engine comes online on the same turn. Sol Ring costs
you a bit of explosiveness, not the deck's function. Blacklist stands, no regrets.
## 2. Tooling: what to install and what each thing is for
Everything runs locally. Local runtimes present: Python 3.13, Node 25, Java 26
(Forge targets 17 — a newer JRE runs it fine), cargo.
### Layer A — Bulk data (download once, query offline forever)
**No live API calls in the build loop.** Every dataset below is a static file. Total
~1.2GB on disk; refresh with a single `make data` when a set drops. All sizes and
schemas below were verified by actually downloading them on 2026-09-07.
| File | Size | Source | What it gives us |
| --- | --- | --- | --- |
| `oracle-cards.jsonl.gz` | 23MB | Scryfall bulk | One object per unique card: oracle text, mana cost, CI, `legalities`, `is:gamechanger` |
| `oracle-tags.jsonl.gz` | 5.6MB | Scryfall bulk | **4,524 community tags** + full hierarchy |
| `rulings.jsonl.gz` | 5.1MB | Scryfall bulk | Every ruling, joined by `oracle_id` |
| `variants.json` | 646MB | Commander Spellbook | **108,535 combos**, each with `bracketTag` |
| `AtomicCards.json.gz` | 51MB | MTGJSON | `edhrecRank` (popularity sanity check only) |
| `embeddings.npz` | 64MB | **generated locally** | 35,312 × 768-dim mechanics vectors (see Layer B) |
Endpoints (note: Scryfall bulk is **JSONL now**, the field is `jsonl_download_uri`,
not the older `download_uri` — `GET api.scryfall.com/bulk-data` for current URLs):
```
https://data.scryfall.io/oracle-cards/oracle-cards-<ts>.jsonl.gz
https://data.scryfall.io/oracle-tags/oracle-tags-<ts>.jsonl.gz
https://json.commanderspellbook.com/variants.json # whole combo DB, no API
https://mtgjson.com/api/v5/AtomicCards.json.gz
```
**The Scryfall Tagger data is the best part of this**, and it's the thing you can't
reconstruct from oracle text with regex. 4,524 hand-curated tags, with `parent_ids` /
`child_ids` forming a hierarchy. Tags that are directly load-bearing for this deck
(counts are real, from the downloaded file):
| Tag | Cards | Why it matters here |
| --- | --- | --- |
| `death-trigger-opponent` | 73 | **Triggers off an _opponent's_ creature dying** — maps exactly onto the commander's steal clause |
| `free-sacrifice-outlet` | 192 | Sac outlets with no mana cost — the aristocrats core |
| `sacrifice-outlet-universal` | 53 | Sac any permanent type |
| `your-sacrifice-matters` | 122 | Payoffs for your own sacrifices |
| `death-trigger-self` | 709 | Dies-triggers |
| `repeatable-proliferate` | 43 | Repeatable -1/-1 counter multiplication |
| `pseudo-proliferate` | 44 | Near-proliferate effects |
| `shrink` | 107 | Toughness reduction |
| `persist` / `gives-persist` | 26 / 7 | Persist + -1/-1 is the classic engine |
`death-trigger-opponent` at 73 cards is the highest-signal query in the whole project.
Grepping oracle text for that concept is miserable; the tag is exact. Read it as
"the shortlist of cards that do what my commander does."
Joining all three files (tags ∩ Jund identity ∩ commander-legal, sorted by
`edhrecRank`) — run locally, output verbatim — yields **55 cards**, top of list:
```
#511 The Meathook Massacre #1031 Yahenni, Undying Partisan
#512 Massacre Wurm #1695 Massacre Girl, Known Killer
#753 Bloodchief Ascension #2536 Mari, the Killing Quill
#2944 Necroskitter #3511 Overseer of the Damned
```
That's a real shortlist for "cards that care when an opponent's creature dies,"
produced in about a second with no network. This is the whole argument for Layer A.
**Combos, fully offline.** The entire Spellbook DB is one JSON download — no
`find-my-combos` POSTs. Verified locally: 108,535 combos, **24,699 within Jund
identity**, and **30 that use The Reaper itself**. Each combo carries `uses`,
`produces`, `identity`, `legalities`, `popularity`, and a `bracketTag`
(E/S/P/O/R/B/C) we can filter on directly for bracket-3 policing.
A real result from the offline file — the commander's own combos are mostly
Mikaeus, the Unhallowed shells:
```
O [Mikaeus, the Unhallowed, The Reaper, Bloodthrone Vampire] => Infinite -1/-1 counters, Infinite creature ETB
O [Mikaeus, the Unhallowed, Spawning Pit, The Reaper] => Infinite -1/-1 counters
```
Reimplementing `find-my-combos` offline is ~15 lines: index combos by card name, then
`included` = combos whose `uses` ⊆ deck, `almostIncluded` = combos missing exactly one.
### Layer B — Semantic search ("try other cards")
**Short answer to "how do I get all the cards": you generate them yourself, in 82
seconds, and you already have.** Don't download `minimaxir/mtg-embeddings` — it's
stale (through Aetherdrift, Feb 2024) and, verified, **does not contain The Reaper**.
Instead run the same model over our own current `oracle-cards.jsonl`:
```
python3 tools/embed.py # -> data/embeddings.npz
```
That encodes every card with `Alibaba-NLP/gte-modernbert-base` (the same model the HF
dataset used) and writes a 64MB `.npz`. On this box (RTX 3060, 12GB) it took **1m22s
for 35,312 cards**. It is a `pip install sentence-transformers` and one script — no
account, no download, and it re-runs whenever a set drops. This is strictly better
than the HF file in every dimension, which is why Layer B is now "build", not "fetch".
**Two things I got wrong on the first pass, both worth knowing:**
1. **Don't put the card's name in the embedded text.** With `name` included, querying
The Reaper returned Midnight Reaper, Reaper of the Wilds, Grim Reaper, Slum Reaper,
Cemetery Reaper — it matched the *word* "Reaper", not the mechanics. `embed.py`
deliberately omits the name field.
2. **Card-to-card similarity matches a card's overall _shape_, not its key ability.**
With names removed, The Reaper's neighbors became Scarecrone, Grim Poppet,
Blazethorn Scarecrow, Jousting Dummy — the model correctly saw "artifact creature
Scarecrow" and matched creature type. Useless for deckbuilding.
**So query by ability text, not by card.** That's what actually works:
```
python3 tools/similar_text.py "whenever a creature an opponent controls with a -1/-1 counter dies, put that card onto the battlefield under your control"
```
```
0.897 Instill Infection 0.891 Make Obsolete 0.890 Cower in Fear
0.882 Shrivel 0.880 Persist 0.876 Hazardous Conditions
0.874 Gnarled Effigy 0.872 Consumptive Goo 0.871 Quillspike
```
Genuine -1/-1 cards. Both scripts filter to Jund identity + commander-legal.
**It's still fuzzy** — it ranks by "reads like", so it surfaces candidates, not
recommendations. Use embeddings to *generate* a candidate list, then filter it through
the Tagger tags and legality from Layer A. Tags are precise but only cover concepts
someone thought to tag; embeddings cover anything but imprecisely. They fail in
opposite directions, which is why the plan uses both.
### Layer C — Popularity / prior art
MTGJSON `AtomicCards.json` (verified current: 2026-09-06, 35,022 cards) carries
`edhrecRank` per card — no EDHREC scraping needed. Use rank only as a loose "how
normal is this inclusion" sanity check. We are **not** tracking saltiness and not
scoring the deck on it.
The one power-level rule we *do* enforce mechanically: **no two-card-in-a-vacuum
infinite combos.** Two cards that go infinite by themselves, with no third
enabler and no board state required, are out — that's the bracket-3 line and it's
checkable directly against the offline combo DB (`len(uses) == 2`). Longer combos,
or two-card lines that need an existing creature / sac outlet / specific board, are
fine. See §1 for how this lands on the Blowfly package.
### Layer D — Simulation (Forge)
**Forge is the simulator. We are not writing our own.** A hand-rolled goldfish sim
models mana and curve but not the game — no blockers, no interaction, no stack, no
opponents. For a deck whose whole point is death triggers and sacrifice loops, that
misses everything that matters. Forge implements the actual rules.
**Installed and working:** Forge 2.0.14, `~/forge`, from the prebuilt
`forge-installer-2.0.14.tar.bz2` (no Maven needed — the release ships a fat jar).
Java 26 runs it fine despite the project targeting 17. It loads 33,617 cards.
Exact syntax, from `SimulateMatch.java` and confirmed against the running binary:
```
sh forge.sh sim -d <deck1.dck> ... <deckX.dck> -f Commander -n <games> -s <seed> -q
```
- `-d` decks (quote names with spaces) · `-f Commander` **required**, else constructed
- `-n` games · `-m` best-of-M match · `-s` RNG seed (use it — reproducible runs)
- `-a` AI profile per player · `-c` seconds before a draw (default 120) · `-q` quiet
**Two gotchas that cost time, both now handled by the tooling:**
1. **Commander decks must live in `~/.forge/decks/commander/`.** The `-D` flag did
*not* work for Commander format — Forge looks in its own profile directory and
otherwise reports `Could not load deck`. `run_forge.sh` writes there directly.
2. **With one deck, Forge "wins" on turn 0 in 300ms** — no opponents, no game. You
need real opponents. Forge ships **173 Commander precons** in
`res/quest/commanderprecons/`; we use three (Faceless Menace C19, Merciless Rage
C19, Arcane Maelstrom C20) for a 4-player pod. They're roughly bracket 2.
Real 4-player games run ~31 turns in ~60 seconds each, so a 24-game batch is ~25 min.
**The AI caveat is not a footnote.** Per Forge's own wiki the AI is heuristic, not
trained: decent at aggro/midrange, moderate at control, **weak at combo decks**. Ours
is a combo deck, so the AI systematically under-plays it and will miss sac-loop lines
a human would take. Every report prints this. Read Forge's win rate as a **floor** and
as proof the deck functions and its cards interact — never as a power rating.
## 3. The build loop
Card selection is offline and instant. Simulation is Forge, and it's the slow step —
~60s per 4-player game — so it runs in batches, not on every edit.
```
make data (once/per set) ──► data/*.jsonl, variants.json, AtomicCards.json
│
decklist.mtg ─────────────────────────► │ blacklist.txt ──┐
│ │ │
├─► bracket_check.py ── game changers, MLD, vacuum combos, blacklist ──┐
├─► combos.py ── included / almostIncluded (offline) ──────────┤
├─► tags.py ── death-trigger-opponent, sac outlets ──────────┼─► out/
├─► similar_text.py ── "cards whose text reads like ..." ──────────┤
│ │
└─► to_dck.py ──► run_forge.sh ──► forge_report.py ─────────────────────┘
(validates names) (24 games, 4-player pod) (.txt + .json + .log)
```
Every simulation run writes a timestamped triple into `out/`: the full Forge `.log`,
a human `.txt` report, and a `.json` artifact — and the report footer names
`decklist.mtg`, the log, the JSON, and itself, so any number is traceable.
## 4. Repo layout
```
reaper-king-no-more/
decklist.mtg # working deck, "1 Card Name" per line
blacklist.txt # cards I won't play, "Card Name | reason"
plan.md
data/ # ~1.2GB, all gitignored
oracle-cards.jsonl oracle-tags.jsonl rulings.jsonl
variants.json AtomicCards.json embeddings.npz (generated)
decks/ # RESULTS, one dir per archetype variant
README.md
no-mikaeus/ # decklist.mtg, bracket.txt, combos.txt,
mikaeus/ # README.md, sim/<stamp>.{log,txt,json}
tools/
cards.py # shared index: oracle+tags+edhrec+combos+blacklist
bracket_check.py # game changers, MLD, vacuum combos, blacklist
combos.py # offline included / almostIncluded (no API)
embed.py # generate embeddings.npz on GPU (~82s)
similar.py # "cards like <card>"
similar_text.py # "cards whose text reads like <query>"
to_dck.py # decklist -> forge .dck, validates names vs Forge
run_forge.sh # batch 4-player sim into decks/<variant>/sim/
forge_report.py # Forge log -> .txt report + .json artifact
variant.py # run the whole suite for one variant
```
**Variants are the unit of work.** A variant is one answer to "what if the deck were
built this way" — its own 100 cards, its own legality verdict, its own combo profile,
its own simulation history. Regenerate one with:
```
python3 tools/variant.py no-mikaeus # analysis only, ~10s
python3 tools/variant.py mikaeus --sim 24 # + 24 Forge games, ~25 min
```
Analysis files are rewritten each run; sim runs are additive, so every batch is kept
and runs stay comparable.
External: `~/forge` (Forge 2.0.14) and `~/.forge/decks/commander/` (where Forge
insists Commander decks live).
## 5. Build order
1. ~~`fetch_data.py`~~ — **done**, six files in `data/` (1.1GB).
2. ~~`cards.py`~~ — **done.** oracle + 4,369 tags + edhrecRank + combos + blacklist.
3. ~~`bracket_check.py`~~ — **done and passing.**
4. ~~`embed.py` / `similar.py` / `similar_text.py`~~ — **done** (Layer B).
5. ~~Draft the 100~~ — **done**, `decklist.mtg`.
6. ~~Forge installed + `to_dck.py` + `run_forge.sh` + `forge_report.py`~~ — **done.**
4-player pod vs precons, reproducible by seed.
7. Iterate on the list using Forge results + the offline combo/tag tools.
**Dropped: the hand-rolled goldfish sim.** It was written and worked, but it models
mana and curve only — no interaction, no blockers, no stack. For an aristocrats combo
deck that's the wrong abstraction, and maintaining a second simulator that disagrees
with the real one is worse than having one. Forge is the simulator. (`goldfish.py`
and its `out/goldfish-*` runs are removed.)
**Report convention** (every tool): a timestamped `.txt` human report plus a `.json`
artifact in `out/`, with the report footer naming `decklist.mtg`, the raw log, the
JSON, and itself.
## 6. Deck skeleton to draft against
Rough shape, ~99 + commander:
- **Lands 36–37** (hybrid commander is easy to cast; the rest of the deck is greedy)
- **Ramp 10** — Jund has good rocks + Cultivate/Farseek effects
- **-1/-1 engines** — Hapatra, Blowfly Infestation, Nest of Scarabs, Yawgmoth,
Necroskitter (steals like the commander!), Contagion Clasp/Engine, Archfiend of Ifnir
- **Sac outlets** — free ones matter: Viscera Seer, Carrion Feeder, Ashnod's Altar
- **Drain payoffs** — Blood Artist, Zulaport Cutthroat, Bastion of Remembrance
- **Removal/interaction 8–10** — bracket 3 pods interact
- **One-offs that love the commander** — Humble Defector (per README), and note
**Necroskitter** is close to a second copy of the commander's steal ability
- **Game Changers: pick ≤3, deliberately.** Check with `bracket_check.py`.
`Necroskitter` came straight out of the Scryfall query in §2 and is arguably the
best non-commander card for this exact deck — good sign the search approach works.
## 6b. Variants — the unit of work
Six variants, each a falsifiable hypothesis rather than a taste preference. All are
generated from one spec (`tools/make_variants.py`) as cut/add lists against the
baseline, so changing the baseline propagates everywhere.
| variant | bracket 3 | vacuum | combos | outlets | cmdr synergy | avg MV |
| --- | --- | --- | --- | --- | --- | --- |
| **no-mikaeus** (baseline) | PASS | 0 | 11 | 8 | 6 | 3.02 |
| lean-outlets | PASS | 0 | 10 | 5 | 7 | 2.97 |
| commander-focus | PASS | 0 | 7 | 7 | 10 | 3.03 |
| heavy-removal | PASS | 0 | 7 | 8 | 8 | 2.95 |
| low-curve | PASS | 0 | 11 | 8 | 9 | 2.67 |
| mikaeus | **FAIL** | **7** | 39 | 8 | 6 | 3.08 |
The hypotheses:
- **lean-outlets** — 8 sac outlets is more than the deck needs. Cut 3 for interaction
and card advantage; does consistency actually drop?
- **commander-focus** — the commander only steals from *opponents'* creatures, but our
-1/-1 engine mostly counters our own. Push counters onto their board instead.
(Raises synergy cards 6 → 10, at the cost of 4 combos.)
- **heavy-removal** — bracket 3 pods interact. Trade combo redundancy for removal:
does surviving longer beat assembling faster?
- **low-curve** — the engine lands ~T6. Cut the top of the curve entirely (MV 3.02 →
**2.67**) and see if coming online sooner wins more.
- **mikaeus** — the control case. Confirms the house rule catches what it should.
**Mikaeus is settled — the answer is no.** Adding it creates **7 two-card vacuum
combos**, because it goes infinite with *every sac outlet in the deck*:
```
Mikaeus + Ashnod's Altar Mikaeus + Carrion Feeder
Mikaeus + Phyrexian Altar Mikaeus + Viscera Seer
Mikaeus + Goblin Bombardment Mikaeus + Woe Strider
Mikaeus + Yahenni, Undying Partisan
```
Two cards, no third enabler, no board state — exactly what the house rule targets.
Its combo count jumps 11 → 39, which is the same problem counted a different way.
Kept as a variant so the comparison stays visible rather than buried in a chat log.
**What the baseline's own numbers say:** `combos.py` finds 11 combos in the deck, all
requiring board state — redundant three-card engines, no vacuum lines. That is the
deck working as designed.
## 6c. How to design variants, and what to do with the results
### Designing variants
A variant is only worth running if it can be **wrong**. The rule I'm using: each one
states a hypothesis, changes 3-5 cards, and holds everything else constant. Anything
bigger and you can't attribute the result; anything vaguer and there's no result to
attribute.
Where the hypotheses come from, in rough order of value:
1. **Structural facts about the list.** Count things and look for outliers. "8 sac
outlets when 5 would do" and "22 three-drops, 5 of them ramp" both came from
counting, not intuition. This is the best source.
2. **Loss data from previous sims.** 15 of 17 losses were life hitting 0 against
aggro → that's `heavy-removal` and `low-curve`.
3. **Tension in the deck's own design.** The commander only steals from *opponents'*
creatures while our engine counters our own → `commander-focus`.
4. **Control cases.** `mikaeus` exists to prove the house rule catches what it should.
A test suite needs a case that fails on purpose.
All of it lives in `tools/make_variants.py` as cut/add lists against the baseline, so
changing the baseline propagates everywhere and no two lists silently drift apart.
The generator refuses to add a card already in the deck (it caught three of my own
mistakes) and validates every name against Forge's database before simming.
### Card-level weights from simulation data
Run Forge **without `-q`** and it logs the whole game, attributed by player:
```
Add To Stack: Ai(1)-Reaper King No More cast Terminate targeting [Face-down card (H4)]
Zone Change: Zulaport Cutthroat (33) was put into Graveyard from Battlefield.
Life: Ai(2)-Faceless Menace 40 > 39
```
`tools/card_stats.py` turns that into per-card numbers: how often a card was cast,
the average turn it landed, and the deck's win rate in games where it was cast. That
last column is the "weight".
**What it's legitimately good for:**
- **Cards never cast in any game.** The single most actionable output. A card that
never gets played across dozens of games is dead weight, uncastable by the AI, or
too expensive — all worth investigating.
- **Average turn played** — measures whether the curve behaves as intended.
- **Cast rate** — is this card doing anything, or is it a 100th-card filler?
**What it is NOT:** a fitness function you can optimize against. Four reasons, all
real:
1. **Sample size.** A card cast in 5 games has a +/-40% win-rate swing from noise.
2. **"Never cast" is three different failures wearing a trenchcoat** — bad card, card
the AI can't pilot, card never drawn. They need opposite fixes.
3. **Correlation, not causation.** Cards cast in long games look good because long
games are ones we *survived*. The card didn't cause the win; surviving did.
4. **The AI is weak at combo**, so it systematically under-casts our combo pieces —
exactly the cards we care most about.
So: use card stats to generate *hypotheses* (this card never gets cast — why?), then
test the hypothesis with a variant. Don't let a win-rate column pick the deck. The
loop is **stats -> hypothesis -> variant -> sim -> stats**, with a human deciding at
each arrow.
## 7. Open questions
- **Which variant is actually best?** Five pass bracket 3 and differ meaningfully on
paper. Forge decides this, not argument — each needs a 24-game batch.
- What's the primary wincon — a drain loop off infinite death triggers, or grinding
value off stolen creatures? The commander's once-per-turn clause means it can't be
the payoff, so something else has to close.
- Do we want any Game Changers at all? Bracket 3 allows 3; running 0 is also fine and
makes the deck's bracket unambiguous.
- Is `commander-focus` right that the deck under-uses its own commander? It nearly
doubles opponent-facing -1/-1 effects (6 → 10) but loses 4 combos. That trade is
the single most interesting question in the list.