#!/usr/bin/env python3 """Generate every deck variant from one declarative spec. Each variant is a hypothesis: change a few cards, see what it does to legality, combo density, and Forge win rate. Keeping the spec here (rather than as hand-edited decklists) means a change to the baseline propagates to every variant. python3 tools/make_variants.py --deck decks/reaper-king-no-more python3 tools/make_variants.py --deck decks/reaper-king-no-more --list The hand-written variant spec (VARIANTS below) is this deck's own hand-curated cut/add lists -- distinct from tools/generate.py's role-budget recipes. """ import sys, os sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) from deckctx import DeckContext def load_variants(ctx): """{name: (why, cuts, adds)} from this deck's variants.yaml `handwritten:` section.""" vfile = ctx.path(ctx.config.get("variants_file", "variants.yaml")) if not os.path.exists(vfile): return {} try: import yaml except ImportError: sys.exit("pyyaml required to read variants.yaml") spec = yaml.safe_load(open(vfile)) or {} out = {} for name, v in (spec.get("handwritten") or {}).items(): out[name] = (v.get("why", "").strip(), list(v.get("cut") or []), list(v.get("add") or [])) return out def load(path): """Main-deck lines only -- the '# Commander' section is re-added per variant.""" out, section = [], "main" for line in open(path): s = line.strip() if s.startswith("#"): if not s.startswith("##"): low = s.lower() if "commander" in low: section = "commander" elif "main" in low or "deck" in low: section = "main" continue if s and s[0].isdigit() and section == "main": out.append(s) return out def main(): ctx = DeckContext.from_args() # pops --deck/-d out of sys.argv first from cards import DB db = DB(blacklist_path=ctx.blacklist_path) commander = ctx.commander base_path = ctx.path("decks", "no-mikaeus", "decklist.txt") if not os.path.exists(base_path): base_path = ctx.decklist_path base = load(base_path) variants = load_variants(ctx) for name, (why, cuts, adds) in variants.items(): if "--list" in sys.argv: print(f"{name:18} -{len(cuts):<2} +{len(adds):<2} {why[:60]}") continue lines = base[:] # cut missing_cuts = [] for c in cuts: before = len(lines) lines = [l for l in lines if l.split(None, 1)[1] != c] if len(lines) == before: missing_cuts.append(c) # add (before the lands block so the file stays readable) idx = next((i for i, l in enumerate(lines) if l.split(None, 1)[1] == "Command Tower"), len(lines)) lines[idx:idx] = [f"1 {a}" for a in adds] # validate bad = [a for a in adds if not db.get(a)] present = {l.split(None, 1)[1] for l in lines} dupes = [a for a in adds if sum(1 for l in lines if l.split(None, 1)[1] == a) > 1] total = sum(int(l.split(None, 1)[0]) for l in lines) vdir = ctx.path("decks", name) os.makedirs(vdir, exist_ok=True) header = [f"# {name}", f"# {why}", "", "# Commander", f"1 {commander}", "", "# Main"] open(os.path.join(vdir, "decklist.txt"), "w").write( "\n".join(header + lines) + "\n") flags = [] if missing_cuts: flags.append(f"cut-not-found: {missing_cuts}") if bad: flags.append(f"UNKNOWN CARDS: {bad}") if dupes: flags.append(f"!! ALREADY IN DECK: {dupes}") if total != 99: flags.append(f"!! {total} cards, need 99") print(f"{name:18} {total:>3} cards {' | '.join(flags) if flags else 'ok'}") if __name__ == "__main__": main()