520 lines
27 KiB
Markdown
520 lines
27 KiB
Markdown
# 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.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
|
||
|
||
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.mgt`.
|
||
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.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:
|
||
|
||
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.
|