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

137 lines
4.7 KiB
Python

#!/usr/bin/env python3
"""Resolve "which deck am I operating on" the same way in every tool.
Every tool used to assume it was run with cwd == that one deck's directory.
Now every tool accepts an explicit deck path, so the repo can hold many decks:
python3 tools/bracket_check.py --deck decks/reaper-king-no-more
MTG_DECK=decks/reaper-king-no-more python3 tools/bracket_check.py
Resolution order for the deck directory:
1. --deck/-d PATH on the command line (removed from argv before the caller
does its own parsing)
2. $MTG_DECK environment variable
3. cwd, if it looks like a deck dir (has config.yaml) -- backwards compat
with the old "cd into the deck dir and run tools/x.py" workflow
4. error
from deckctx import DeckContext
ctx = DeckContext.from_args() # pops --deck out of sys.argv
ctx.path("decklist.mgt") # -> decks/<name>/decklist.mgt
ctx.commander # -> "The Reaper, King No More"
ctx.identity # -> frozenset("BRG"), from the DB
ctx.slug # -> "reaper-king-no-more"
"""
import os
import sys
ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
def _looks_like_deck_dir(d):
return os.path.exists(os.path.join(d, "config.yaml")) or \
os.path.exists(os.path.join(d, "decklist.mgt"))
def resolve_deck_dir(argv=None, pop=True):
"""Find the deck directory, optionally stripping --deck/-d out of argv."""
argv = sys.argv if argv is None else argv
for flag in ("--deck", "-d"):
if flag in argv:
i = argv.index(flag)
path = argv[i + 1]
if pop:
del argv[i:i + 2]
return os.path.abspath(path)
env = os.environ.get("MTG_DECK")
if env:
return os.path.abspath(env)
if _looks_like_deck_dir("."):
return os.path.abspath(".")
sys.exit("no deck specified -- pass --deck decks/<name>, set $MTG_DECK, "
"or run from inside a deck directory")
class DeckContext:
"""Everything a tool needs to know about the deck it's operating on."""
def __init__(self, deck_dir):
self.dir = os.path.abspath(deck_dir)
if not os.path.isdir(self.dir):
sys.exit(f"no such deck directory: {self.dir}")
self.config = self._load_config()
self._commander = self.config.get("commander")
@classmethod
def from_args(cls, argv=None, pop=True):
return cls(resolve_deck_dir(argv, pop))
def _load_config(self):
cfg_path = os.path.join(self.dir, "config.yaml")
if not os.path.exists(cfg_path):
return {}
try:
import yaml
except ImportError:
print("warning: pyyaml missing, config.yaml ignored", file=sys.stderr)
return {}
return yaml.safe_load(open(cfg_path)) or {}
def path(self, *parts):
"""A path inside this deck's directory."""
return os.path.join(self.dir, *parts)
@property
def slug(self):
"""Directory basename, used as a filesystem-safe deck identifier."""
return os.path.basename(self.dir.rstrip("/"))
@property
def name(self):
"""Human-readable deck name. Falls back to the commander, then the slug."""
return self.config.get("name") or self._commander or self.slug
@property
def commander(self):
if not self._commander:
sys.exit(f"{self.path('config.yaml')}: no 'commander:' set")
return self._commander
@property
def decklist_path(self):
return self.config.get("decklist") and self.path(self.config["decklist"]) \
or self.path("decklist.mgt")
@property
def blacklist_path(self):
return self.path(self.config.get("blacklist_file", "blacklist.txt"))
@property
def protected(self):
return set(self.config.get("protected", []))
def identity(self, db=None):
"""Color identity, derived from the commander's own card data.
Never hardcode a color combination -- look up the commander in the
oracle database and use its color_identity, full stop.
"""
if db is None:
from cards import DB
db = DB()
card = db.get(self.commander)
if card is None:
sys.exit(f"commander '{self.commander}' not found in card database "
f"-- check config.yaml / run tools/fetch_data.py")
return card.color_identity
def dck_filename(self):
"""Forge .dck filename derived from this deck's slug."""
return f"{self.slug}.dck"
def forge_metadata_name(self):
"""[metadata] Name= value for the .dck file."""
return self.name