#!/usr/bin/env python3 """Offline combo finder — reimplements Commander Spellbook's find-my-combos against the local variants.json. No API calls. python3 tools/combos.py --deck decks/reaper-king-no-more python3 tools/combos.py --deck decks/reaper-king-no-more --almost 30 python3 tools/combos.py --deck decks/reaper-king-no-more --deck-file some/decklist.txt """ import sys, os, json sys.path.insert(0, os.path.dirname(__file__)) from cards import DB, load_decklist from deckctx import DeckContext def main(): args = sys.argv[1:] limit = int(args[args.index("--almost") + 1]) if "--almost" in args else 15 # --deck-file lets a caller (e.g. variant.py) point at a specific decklist # inside the deck dir while --deck/-d (consumed by DeckContext) still # selects which deck's config/identity to use. explicit_deck_file = args[args.index("--deck-file") + 1] if "--deck-file" in args else None ctx = DeckContext.from_args(args, pop=False) deck_path = explicit_deck_file or ctx.decklist_path db = DB(blacklist_path=ctx.blacklist_path) identity = ctx.identity(db) commander, deck = load_decklist(deck_path) pool = set(deck) | {commander} variants, _ = db.combos included, almost = [], [] for c in variants: if not set(c["identity"]) <= identity: continue uses = [u["card"]["name"] for u in c["uses"]] have = pool & set(uses) missing = set(uses) - pool if not have: continue if not missing: included.append((uses, c)) elif len(missing) == 1: m = next(iter(missing)) if m in db.blacklist: # never suggest a blacklisted card continue almost.append((m, uses, c)) print("=" * 66) print(f"COMBOS ALREADY IN THE DECK: {len(included)}") print("=" * 66) for uses, c in sorted(included, key=lambda x: len(x[0])): print(f" [{c['bracketTag']}] {' + '.join(uses)}") for p in c["produces"][:3]: print(f" -> {p['feature']['name']}") if c.get("notablePrerequisites"): for line in c["notablePrerequisites"].splitlines(): print(f" requires: {line}") print() # rank "one card away" by how many combos that single card unlocks from collections import Counter unlocks = Counter(m for m, _, _ in almost) print("=" * 66) print(f"ONE CARD AWAY: {len(almost)} combos, {len(unlocks)} distinct cards") print("=" * 66) for name, n in unlocks.most_common(limit): card = db.get(name) rank = f"#{card.edhrec_rank}" if card and card.edhrec_rank else "-" cost = card.mana_cost if card else "?" print(f" {n:>3} combos {name:<34} {cost:<12} {rank}") print() print(f"decklist: {deck_path} | source: data/variants.json (108,535 combos)") if __name__ == "__main__": main()