27 KiB
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:
- 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.
- "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).
- 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:
bracket_check.pyreports 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.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:
- Don't put the card's name in the embedded text. With
nameincluded, 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.pydeliberately omits the name field. - 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
-ddecks (quote names with spaces) ·-f Commanderrequired, else constructed-ngames ·-mbest-of-M match ·-sRNG seed (use it — reproducible runs)-aAI profile per player ·-cseconds before a draw (default 120) ·-qquiet
Two gotchas that cost time, both now handled by the tooling:
- Commander decks must live in
~/.forge/decks/commander/. The-Dflag did not work for Commander format — Forge looks in its own profile directory and otherwise reportsCould not load deck.run_forge.shwrites there directly. - 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.mgt ─────────────────────────► │ 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.mgt, the log, the JSON, and itself, so any number is traceable.
4. Repo layout
reaper-king-no-more/
decklist.mgt # 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.mgt, 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
— done, six files infetch_data.pydata/(1.1GB).— done. oracle + 4,369 tags + edhrecRank + combos + blacklist.cards.py— done and passing.bracket_check.py— done (Layer B).embed.py/similar.py/similar_text.pyDraft the 100— done,decklist.mgt.Forge installed +— done. 4-player pod vs precons, reproducible by seed.to_dck.py+run_forge.sh+forge_report.py- 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.mgt, 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:
- 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.
- Loss data from previous sims. 15 of 17 losses were life hitting 0 against
aggro → that's
heavy-removalandlow-curve. - Tension in the deck's own design. The commander only steals from opponents'
creatures while our engine counters our own →
commander-focus. - Control cases.
mikaeusexists 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:
- Sample size. A card cast in 5 games has a +/-40% win-rate swing from noise.
- "Never cast" is three different failures wearing a trenchcoat — bad card, card the AI can't pilot, card never drawn. They need opposite fixes.
- 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.
- 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-focusright 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.