#!/usr/bin/env python3 """Run the full analysis suite for one deck variant and write it to /decks//. python3 tools/variant.py --deck decks/reaper-king-no-more no-mikaeus python3 tools/variant.py --deck decks/reaper-king-no-more mikaeus --sim 24 Each variant directory ends up self-contained: /decks// decklist.txt the 100 cards bracket.txt bracket-3 + house-rule check combos.txt combos in the deck, and one card away sim/ Forge runs: .{log,txt,json} README.md what this variant is and what the numbers say """ import sys, os, subprocess, glob from datetime import datetime, timezone sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) from deckctx import DeckContext, ROOT as TOOLS_ROOT def run(cmd, out_path, cwd=None): r = subprocess.run(cmd, capture_output=True, text=True, cwd=cwd) text = r.stdout + (("\n[stderr]\n" + r.stderr) if r.stderr.strip() else "") open(out_path, "w").write(text) return text def main(): ctx = DeckContext.from_args() # pops --deck/-d out of sys.argv first args = sys.argv[1:] if not args: sys.exit(__doc__) name = args[0] games = int(args[args.index("--sim") + 1]) if "--sim" in args else 0 seed = int(args[args.index("--seed") + 1]) if "--seed" in args else 20260907 tools = os.path.join(TOOLS_ROOT, "tools") vdir = ctx.path("decks", name) deck = os.path.join(vdir, "decklist.txt") if not os.path.exists(deck): sys.exit(f"no decklist at {deck}") os.makedirs(os.path.join(vdir, "sim"), exist_ok=True) print(f"[{name}] bracket check ...") bracket = run([sys.executable, os.path.join(tools, "bracket_check.py"), "--deck", ctx.dir, deck], os.path.join(vdir, "bracket.txt")) verdict = "PASS" if "BRACKET 3: PASS" in bracket else "FAIL" print(f"[{name}] combos ...") combos = run([sys.executable, os.path.join(tools, "combos.py"), "--deck", ctx.dir, "--deck-file", deck], os.path.join(vdir, "combos.txt")) if games: print(f"[{name}] forge: {games} games (this is the slow part) ...") subprocess.run(["bash", os.path.join(tools, "run_forge.sh"), ctx.dir, str(games), str(seed), name]) # per-card stats for the newest verbose sim log logs = sorted(glob.glob(os.path.join(vdir, "sim", "*.log"))) if logs: stats_out = os.path.join(vdir, "card_stats.txt") run([sys.executable, os.path.join(tools, "card_stats.py"), "--deck", ctx.dir, logs[-1]], stats_out) # power metrics (needs a verbose log; silently skipped for -q logs) if logs: pw = os.path.join(vdir, "power.txt") out_txt = run([sys.executable, os.path.join(tools, "power.py"), "--deck", ctx.dir, logs[-1]], pw) if "no cast events" in out_txt: os.remove(pw) # index of sim runs sims = sorted(glob.glob(os.path.join(vdir, "sim", "*.txt"))) stamp = datetime.now(timezone.utc).strftime("%Y-%m-%d %H:%MZ") n_deck = sum(1 for l in open(deck) if l.strip() and l.strip()[0].isdigit()) vac = "0" for line in bracket.splitlines(): if "VACUUM" in line: vac = line.split(":")[1].split()[0] readme = [ f"# {name}", "", f"_generated {stamp} by `tools/variant.py`_", "", f"- **bracket 3: {verdict}** (two-card vacuum combos: {vac})", f"- {n_deck} lines in `decklist.txt`", "", "## files", "", "| file | what it is |", "| --- | --- |", "| `decklist.txt` | the deck, `N Card Name` per line |", "| `bracket.txt` | bracket-3 + house-rule check |", "| `combos.txt` | combos in the deck, and one card away |", "| `sim/` | Forge runs — `.log` raw, `.txt` report, `.json` data |", "| `card_stats.txt` | per-card cast rate, avg round, win% (newest sim) |", "| `power.txt` | what the deck DID (engine assembly, pressure) win or lose |", "", ] # surface the actual house-rule violations, not just a count vac_lines = [] grab = False for line in bracket.splitlines(): if "VACUUM" in line: grab = True; continue if grab: if line.strip().startswith("-"): vac_lines.append(line.strip()[2:]) elif line.strip() == "": break if vac_lines: readme += ["## two-card vacuum combos (house-rule violations)", "", "These go infinite with no third card and no board state:", ""] readme += [f"- {v}" for v in vac_lines] + [""] if sims: readme += ["## simulation runs", ""] for s in sims: base = os.path.basename(s) wr = "" for line in open(s): if "our deck" in line: wr = line.strip(); break readme.append(f"- `sim/{base}` — {wr}") readme.append("") else: readme += ["## simulation runs", "", "_none yet — run with `--sim 24`_", ""] open(os.path.join(vdir, "README.md"), "w").write("\n".join(readme) + "\n") print(f"[{name}] -> {ctx.slug}/decks/{name}/ (bracket 3: {verdict})") if __name__ == "__main__": main()