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

75 lines
2.9 KiB
Python

#!/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.mgt
"""
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()