Migrates the reaper-king-no-more proof of concept into decks/reaper-king-no-more/
as the first deck under a shared tools/ engine, so the repo can hold many
Commander decks instead of one implicit one.
- tools/deckctx.py: new shared DeckContext, resolves --deck / $MTG_DECK / cwd,
loads a deck's config.yaml, and derives color identity from the commander's
own card data (never a hardcoded color combination).
- cards.py: drop the JUND=frozenset("BRG") constant; jund_legal() renamed to
identity_legal(card, identity); DB() takes an explicit blacklist_path since
the blacklist is per-deck, not shared.
- combos.py, roles.py, similar.py, similar_text.py, bracket_check.py: take
--deck, use the deck's real identity instead of Jund/BRG.
- power.py, card_stats.py, forge_report.py: OURS deck-name constant now comes
from the deck's config (name: / commander), matched via DeckContext instead
of hardcoded "Reaper King No More".
- to_dck.py, batch_run.py, ab_test.py: .dck filename and [metadata] Name= are
derived from the deck (slug / config name), not a hardcoded "reaper.dck".
- generate.py: RECIPES and make_variants.py's VARIANTS (deck-specific
hypotheses) moved out of Python into decks/reaper-king-no-more/variants.yaml;
protected cards now come from config.yaml's protected: list instead of a
hardcoded set.
- New tools/mtg dispatcher: `tools/mtg decks/<name> <tool> [args...]` runs any
tool against any deck without cd-ing into its directory.
- data/ (Scryfall/MTGJSON/Spellbook bulk, ~1.2GB, gitignored) and embeddings.npz
move to the repo root as shared, commander-format-wide data.
- docs/howto.md: moved from the deck dir (it documents the tool, not the deck)
and updated its example commands to the real generalized CLI.
- README.md: rewritten to describe the multi-deck repo and point at
decks/reaper-king-no-more as the reference example.
Verified after the change: bracket_check, combos, and roles against
reaper-king-no-more reproduce the exact PASS/11-combos/role-profile numbers
from before the move; generate.py --list now loads recipes from YAML;
make_variants.py reproduces all 7 hand-curated variants including the
mikaeus FAIL control case; to_dck.py derives the right Name= and filename;
variant.py and compare.py reproduce decks/README.md's original numbers
exactly. The original POC at ~/mtg/reaper-king-no-more is untouched.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QPKgLYpMR8szWNFXyK1XNu
77 lines
2.8 KiB
Python
77 lines
2.8 KiB
Python
#!/usr/bin/env python3
|
|
"""Download every bulk dataset the toolchain needs. Run once per machine.
|
|
|
|
python3 tools/fetch_data.py # ~1.2GB, skips what already exists
|
|
python3 tools/fetch_data.py --force # re-download everything
|
|
|
|
Scryfall bulk URLs are timestamped and change, so they're resolved live from
|
|
the API rather than hardcoded. Note the field is `jsonl_download_uri` -- Scryfall
|
|
serves JSONL now, not the older JSON array format.
|
|
"""
|
|
import sys, os, json, gzip, shutil, urllib.request
|
|
|
|
ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
|
DATA = os.path.join(ROOT, "data")
|
|
UA = {"User-Agent": "reaper-king-deckbuilder/1.0", "Accept": "*/*"}
|
|
|
|
STATIC = [
|
|
("variants.json",
|
|
"https://json.commanderspellbook.com/variants.json",
|
|
"Commander Spellbook: every combo (~108k)"),
|
|
("AtomicCards.json",
|
|
"https://mtgjson.com/api/v5/AtomicCards.json.gz",
|
|
"MTGJSON: edhrecRank per card"),
|
|
]
|
|
SCRYFALL = [("oracle_cards", "oracle-cards.jsonl", "every unique card"),
|
|
("oracle_tags", "oracle-tags.jsonl", "4,369 community tags"),
|
|
("rulings", "rulings.jsonl", "all rulings")]
|
|
|
|
|
|
def get(url, dest, force=False):
|
|
if os.path.exists(dest) and not force:
|
|
print(f" have {os.path.basename(dest)}")
|
|
return False
|
|
tmp = dest + ".part"
|
|
print(f" get {os.path.basename(dest)} ...", end="", flush=True)
|
|
req = urllib.request.Request(url, headers=UA)
|
|
with urllib.request.urlopen(req) as r, open(tmp, "wb") as f:
|
|
shutil.copyfileobj(r, f)
|
|
if url.endswith(".gz"):
|
|
with gzip.open(tmp) as g, open(dest, "wb") as f:
|
|
shutil.copyfileobj(g, f)
|
|
os.remove(tmp)
|
|
else:
|
|
os.replace(tmp, dest)
|
|
print(f" {os.path.getsize(dest)/1048576:.0f}MB")
|
|
return True
|
|
|
|
|
|
def main():
|
|
force = "--force" in sys.argv
|
|
os.makedirs(DATA, exist_ok=True)
|
|
print("Scryfall bulk (URLs resolved live):")
|
|
meta = json.load(urllib.request.urlopen(
|
|
urllib.request.Request("https://api.scryfall.com/bulk-data", headers=UA)))
|
|
by_type = {b["type"]: b for b in meta["data"]}
|
|
for typ, fname, _ in SCRYFALL:
|
|
b = by_type.get(typ)
|
|
if not b:
|
|
print(f" MISSING bulk type {typ}"); continue
|
|
get(b["jsonl_download_uri"], os.path.join(DATA, fname), force)
|
|
|
|
print("other sources:")
|
|
for fname, url, _ in STATIC:
|
|
get(url, os.path.join(DATA, fname), force)
|
|
|
|
emb = os.path.join(DATA, "embeddings.npz")
|
|
print("embeddings:")
|
|
if os.path.exists(emb) and not force:
|
|
print(" have embeddings.npz")
|
|
else:
|
|
print(" run: python3 tools/embed.py (needs a GPU or ~20min on CPU)")
|
|
|
|
print(f"\ndata/ is {sum(os.path.getsize(os.path.join(DATA,f)) for f in os.listdir(DATA))/1073741824:.1f}GB")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|