#!/usr/bin/env python3 """Compare every variant side by side. Writes /decks/README.md. python3 tools/compare.py --deck decks/reaper-king-no-more """ import sys, os, re, glob, json sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) from cards import DB, load_decklist from deckctx import DeckContext from collections import Counter def variant_order(ctx): """Hand-curated variants (from variants.yaml `handwritten:`) first, in the order they're declared there, then any generated/other variant dirs.""" hand = [] vfile = ctx.path(ctx.config.get("variants_file", "variants.yaml")) if os.path.exists(vfile): try: import yaml spec = yaml.safe_load(open(vfile)) or {} hand = list((spec.get("handwritten") or {}).keys()) except ImportError: pass rest = sorted(os.path.basename(os.path.dirname(p)) for p in glob.glob(ctx.path("decks", "*", "decklist.mtg")) if os.path.basename(os.path.dirname(p)) not in hand) return hand + rest def hypothesis(path, hand_names): for line in open(path): if line.startswith("# ") and "Commander" not in line and "Main" not in line: t = line[2:].strip() if t and not t.endswith(tuple(hand_names)): return t return "" def main(): ctx = DeckContext.from_args() db = DB(blacklist_path=ctx.blacklist_path) order = variant_order(ctx) DECKS = ctx.path("decks") baseline = order[0] if order else None rows = [] for name in order: vdir = os.path.join(DECKS, name) deck_f = os.path.join(vdir, "decklist.mtg") if not os.path.exists(deck_f): continue cmd, deck = load_decklist(deck_f) cards = [db[n] for n in deck if db.get(n)] nl = [c for c in cards if not c.is_land] lands = len(cards) - len(nl) mv = sum(c.mana_value for c in nl) / len(nl) if nl else 0 bracket = open(os.path.join(vdir, "bracket.txt")).read() if os.path.exists( os.path.join(vdir, "bracket.txt")) else "" verdict = "PASS" if "BRACKET 3: PASS" in bracket else "FAIL" m = re.search(r"VACUUM: (\d+)", bracket) vac = m.group(1) if m else "?" combos_f = os.path.join(vdir, "combos.txt") n_combos = "?" if os.path.exists(combos_f): m = re.search(r"COMBOS ALREADY IN THE DECK: (\d+)", open(combos_f).read()) n_combos = m.group(1) if m else "?" # latest sim result wr = "—" sims = sorted(f for f in glob.glob(os.path.join(vdir, "sim", "*.json")) if not f.endswith("-cards.json")) if sims: d = json.load(open(sims[-1])) r = d.get("our_win_rate") if r is not None: wr = f"{100*r:.0f}% ({d['meta']['games_completed']}g)" rows.append({ "name": name, "verdict": verdict, "vac": vac, "combos": n_combos, "lands": lands, "mv": mv, "wr": wr, "outlets": sum(1 for c in nl if "free-sacrifice-outlet" in c.tags), "synergy": sum(1 for c in nl if "death-trigger-opponent" in c.tags), "why": hypothesis(deck_f, order), }) L = ["# decks/", "", "Forge simulation results and deck analysis, one directory per **archetype " "variant**.", "", "Each variant is a hypothesis: change a few cards, then measure what it does " "to legality, combo density, and win rate. Every file here is generated — see " "*regenerating* below.", "", "## variants", "", "| variant | bracket 3 | vacuum combos | combos | sac outlets | cmdr synergy | lands | avg MV | Forge win rate |", "| --- | --- | --- | --- | --- | --- | --- | --- | --- |"] for r in rows: star = " ⭐" if r["name"] == baseline else "" v = f"**{r['verdict']}**" if r["verdict"] == "PASS" else f"**{r['verdict']}**" L.append(f"| [`{r['name']}/`]({r['name']}/){star} | {v} | {r['vac']} | " f"{r['combos']} | {r['outlets']} | {r['synergy']} | {r['lands']} | " f"{r['mv']:.2f} | {r['wr']} |") L += ["", "⭐ = current baseline", "", "## the hypotheses", ""] for r in rows: L.append(f"**`{r['name']}`** — {r['why']}") L.append("") slug = ctx.slug L += ["## layout", "", "```", "decks//", " decklist.mtg the 100 cards, \"N Card Name\" per line", " bracket.txt bracket-3 + house-rule check", " combos.txt combos already in the deck, and one card away", " sim/ Forge runs, one triple per batch:", " .log raw Forge output, every game", " .txt human-readable report", " .json machine-readable results", " README.md generated summary", "```", "", "## regenerating", "", "```bash", f"python3 tools/make_variants.py --deck decks/{slug} # rebuild every decklist from the spec", f"python3 tools/variant.py --deck decks/{slug} # analysis for one variant (~10s)", f"python3 tools/variant.py --deck decks/{slug} --sim 24 # + 24 Forge games (~25 min)", f"python3 tools/compare.py --deck decks/{slug} # rebuild this file", "```", "", "The decklists are generated from `tools/make_variants.py`, which holds each " "variant as a cut/add list against the baseline. Change the baseline and every " "variant follows. Analysis files are rewritten each run; **sim runs are " "additive**, so batches accumulate and stay comparable.", "", "## reading the win rates", "", "Forge's AI is heuristic and **weak at combo decks** by its own documentation. " "Ours is a combo deck, so the AI under-plays it. Treat win rates as a **floor** " "and as evidence the deck functions and its cards interact — not as a power " "rating. Random baseline in a 4-player pod is 25%. Opponents are stock " "Commander precons (roughly bracket 2).", ""] open(os.path.join(DECKS, "README.md"), "w").write("\n".join(L) + "\n") print("\n".join(L[:20])) if __name__ == "__main__": main()