126 lines
4.8 KiB
Python
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())
|