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

126 lines
4.8 KiB
Python

#!/usr/bin/env python3
"""Convert decklist.mgt -> Forge .dck format.
python3 tools/to_dck.py --deck decks/reaper-king-no-more
python3 tools/to_dck.py --deck decks/reaper-king-no-more [decklist.mgt] [-o ~/forge/decks/<slug>.dck]
Forge sections are [Commander], [Main], [Sideboard] (verified in DeckSection.java).
Card names must match Forge's own database, so this validates against
res/cardsfolder and reports anything Forge won't recognise BEFORE you sim.
Output filename and [metadata] Name= are both derived from the deck (slug /
config.yaml `name:`) -- never a hardcoded "reaper.dck" / deck title.
"""
import sys, os, re, zipfile
sys.path.insert(0, os.path.dirname(__file__))
from cards import load_decklist
from deckctx import DeckContext
from collections import Counter
def load_subs(subs_file, enabled=True):
"""Simulation-only card swaps for cards Forge's AI genuinely cannot pilot.
Returns {real_card: stand_in}. See <deck>/sim_substitutions.yaml for the
rationale and the deviations each swap introduces.
"""
if not enabled or not subs_file or not os.path.exists(subs_file):
return {}
try:
import yaml
except ImportError:
print("warning: pyyaml missing, substitutions skipped", file=sys.stderr)
return {}
d = yaml.safe_load(open(subs_file)) or {}
return d.get("substitutions") or {}
FORGE = os.path.expanduser("~/forge")
def forge_card_names():
"""Every card name Forge knows, from its cardsfolder (dir or .zip)."""
names = set()
folder = os.path.join(FORGE, "res", "cardsfolder")
zpath = os.path.join(folder, "cardsfolder.zip")
if os.path.exists(zpath):
with zipfile.ZipFile(zpath) as z:
for n in z.namelist():
if not n.endswith(".txt"):
continue
with z.open(n) as f:
for line in f:
line = line.decode("utf-8", "ignore").strip()
if line.startswith("Name:"):
names.add(line[5:].strip()); break
for root, _, files in os.walk(folder):
for fn in files:
if not fn.endswith(".txt"):
continue
try:
with open(os.path.join(root, fn), encoding="utf-8", errors="ignore") as f:
for line in f:
if line.startswith("Name:"):
names.add(line[5:].strip()); break
except OSError:
pass
return names
def main():
ctx = DeckContext.from_args()
args = [a for a in sys.argv[1:]]
out = args[args.index("-o") + 1] if "-o" in args else \
os.path.join(FORGE, "decks", ctx.dck_filename())
src = next((a for a in args if a.endswith(".mgt")), ctx.decklist_path)
commander, deck = load_decklist(src)
subs_file = ctx.path(ctx.config.get("sim_substitutions_file", "sim_substitutions.yaml"))
subs = load_subs(subs_file, enabled="--no-subs" not in args)
applied = {}
if subs:
present = set(deck)
swapped = [] # NB: not `out` -- that's the output path
for n in deck:
t = subs.get(n)
# never substitute into a duplicate: that would break singleton
if t and t not in present:
applied[n] = t
present.add(t)
swapped.append(t)
else:
if t:
print(f"note: not substituting {n} -> {t} "
f"({t} already in deck)", file=sys.stderr)
swapped.append(n)
deck = swapped
if applied:
print(f"substituted {len(applied)} AI-unpilotable card(s) for simulation:",
file=sys.stderr)
for a, b in applied.items():
print(f" {a} -> {b}", file=sys.stderr)
print(" (per-card stats for these describe the STAND-IN, not the real card)",
file=sys.stderr)
known = forge_card_names()
if known:
missing = sorted({n for n in [commander] + deck if n not in known})
if missing:
print(f"!! {len(missing)} card(s) Forge does not recognise:", file=sys.stderr)
for m in missing:
print(f" {m}", file=sys.stderr)
print(" (fix the name in decklist.mgt, or Forge will drop it)\n", file=sys.stderr)
else:
print("warning: could not read Forge's card list; skipping validation", file=sys.stderr)
lines = ["[metadata]", f"Name={ctx.forge_metadata_name()}", "", "[Commander]", f"1 {commander}", "", "[Main]"]
for name, count in sorted(Counter(deck).items()):
lines.append(f"{count} {name}")
lines += ["", "[Sideboard]", ""]
os.makedirs(os.path.dirname(out), exist_ok=True)
open(out, "w").write("\n".join(lines) + "\n")
print(f"wrote {out} ({len(deck)} + 1 commander)")
return 0
if __name__ == "__main__":
sys.exit(main())