mtg/tools/variant.py
2026-09-12 00:41:47 -04:00

141 lines
5.3 KiB
Python

#!/usr/bin/env python3
"""Run the full analysis suite for one deck variant and write it to <deck>/decks/<name>/.
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:
<deck>/decks/<name>/
decklist.mgt the 100 cards
bracket.txt bracket-3 + house-rule check
combos.txt combos in the deck, and one card away
sim/ Forge runs: <stamp>.{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.mgt")
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.mgt`",
"",
"## files",
"",
"| file | what it is |",
"| --- | --- |",
"| `decklist.mgt` | 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()