#!/usr/bin/env python3 """Check a decklist against Commander Bracket 3 + our house rules. python3 tools/bracket_check.py --deck decks/reaper-king-no-more python3 tools/bracket_check.py --deck decks/reaper-king-no-more decks/reaper-king-no-more/decks/no-mikaeus/decklist.mgt Enforces: * <= 3 Game Changers (official) * no mass land denial (official) * extra turns sparse, never looping (official) * NO two-card-in-a-vacuum infinite combos (our house rule, stricter) * singleton + 100 cards (format) """ import os, sys, json from collections import Counter sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) from cards import DB, load_decklist from deckctx import DeckContext MLD_TAGS = {"mass-land-denial"} # verified: the real Tagger slug, 114 cards EXTRA_TURN = "extra-turn" LIMIT_GC = 3 def vacuum_combos(db, deck_names, commander): """Two-card infinite combos among our cards that need NO extra board state. Spellbook gives prerequisites per combo; a combo requiring creatures on the battlefield or a third enabler is a build-around, not a vacuum combo. """ _, idx = db.combos pool = set(deck_names) | {commander} seen, flagged, buildarounds = set(), [], [] for name in pool: for combo in idx.get(name, []): if combo["id"] in seen: continue uses = [u["card"]["name"] for u in combo["uses"]] if len(uses) != 2 or not set(uses) <= pool: continue seen.add(combo["id"]) prereq = (combo.get("notablePrerequisites") or "").strip() infinite = any("Infinite" in p["feature"]["name"] or "wins the game" in p["feature"]["name"] or "loses the game" in p["feature"]["name"] for p in combo["produces"]) if not infinite: continue (flagged if not prereq else buildarounds).append((uses, prereq, combo)) return flagged, buildarounds def main(argv=None): argv = sys.argv[1:] if argv is None else argv ctx = DeckContext.from_args(argv) # any remaining positional arg overrides which decklist file to check, # while --deck still selects the deck (config, identity, blacklist) path = next((a for a in argv if not a.startswith("-")), None) or ctx.decklist_path db = DB(blacklist_path=ctx.blacklist_path) commander, main_deck = load_decklist(path) if not commander: sys.exit("no commander found -- decklist needs a '# Commander' section") print(f"COMMANDER: {commander}") print(f"DECK SIZE: {len(main_deck)} + 1 commander = {len(main_deck)+1}\n") ok = True # --- unknown cards --- unknown = [n for n in [commander] + main_deck if not db.get(n)] if unknown: ok = False print("!! UNKNOWN CARDS (typo or not in bulk data):") for n in unknown: print(f" {n}") print() known = [db[n] for n in main_deck if db.get(n)] cmd = db.get(commander) # --- size + singleton --- if len(main_deck) + 1 != 100: ok = False print(f"!! SIZE: {len(main_deck)+1} cards, need exactly 100\n") dupes = {n: c for n, c in Counter(main_deck).items() if c > 1 and db.get(n) and "Basic" not in db[n].type_line} if dupes: ok = False print("!! SINGLETON violated:", dupes, "\n") # --- color identity --- # Fall back to the configured deck's own commander identity (never a # hardcoded color combination) if this decklist's own commander card # wasn't found in the database. ci = cmd.color_identity if cmd else ctx.identity(db) off = [c.name for c in known if not c.color_identity <= ci] if off: ok = False print(f"!! OFF-IDENTITY (commander is {''.join(sorted(ci))}):") for n in off: print(f" {n}") print() # --- blacklist (preference, not a bracket rule) --- bl = [c for c in known if c.name in db.blacklist] + \ ([cmd] if cmd and cmd.name in db.blacklist else []) if bl: ok = False print(f"!! BLACKLISTED CARDS IN DECK: {len(bl)} (personal preference, not a bracket rule)") for c in bl: print(f" - {c.name} -- {db.blacklist[c.name]}") print() # --- game changers --- gc = [c for c in known if c.game_changer] + ([cmd] if cmd and cmd.game_changer else []) status = "OK" if len(gc) <= LIMIT_GC else "!! OVER LIMIT" print(f"GAME CHANGERS: {len(gc)}/{LIMIT_GC} {status}") for c in gc: print(f" - {c.name}") if len(gc) > LIMIT_GC: ok = False print() # --- mass land denial --- mld = [c for c in known if c.tags & MLD_TAGS] print(f"MASS LAND DENIAL: {len(mld)} {'OK' if not mld else '!! NOT ALLOWED IN B3'}") for c in mld: print(f" - {c.name}") if mld: ok = False print() # --- extra turns --- et = [c for c in known if EXTRA_TURN in c.tags] print(f"EXTRA TURNS: {len(et)} {'OK' if len(et) <= 1 else '?? keep sparse, never loop'}") for c in et: print(f" - {c.name}") print() # --- combos --- vac, build = vacuum_combos(db, main_deck, commander) print(f"TWO-CARD INFINITE COMBOS IN A VACUUM: {len(vac)} {'OK' if not vac else '!! HOUSE RULE VIOLATION'}") for uses, _, _ in vac: print(f" - {' + '.join(uses)}") if vac: ok = False print() if build: print(f"two-card combos needing board state (ALLOWED, {len(build)}):") for uses, prereq, _ in build: print(f" - {' + '.join(uses)}") for line in prereq.splitlines(): print(f" requires: {line}") print() print("=" * 58) print("BRACKET 3: PASS" if ok else "BRACKET 3: FAIL -- see !! above") return 0 if ok else 1 if __name__ == "__main__": sys.exit(main())