Use new extension

This commit is contained in:
mark rosenberg 2026-09-12 00:41:44 -04:00
parent dde4bd71ea
commit 1e177058e0
44 changed files with 65 additions and 65 deletions

View file

@ -56,7 +56,7 @@ Each variant is a hypothesis: change a few cards, then measure what it does to l
```
decks/<variant>/
decklist.txt the 100 cards, "N Card Name" per line
decklist.mgt the 100 cards, "N Card Name" per line
bracket.txt bracket-3 + house-rule check
combos.txt combos already in the deck, and one card away
sim/ Forge runs, one triple per batch:

View file

@ -3,13 +3,13 @@
_generated 2026-09-07 23:37Z by `tools/variant.py`_
- **bracket 3: PASS** (two-card vacuum combos: 0)
- 86 lines in `decklist.txt`
- 86 lines in `decklist.mgt`
## files
| file | what it is |
| --- | --- |
| `decklist.txt` | the deck, `N Card Name` per line |
| `decklist.mgt` | the deck, `N Card Name` per line |
| `bracket.txt` | bracket-3 + house-rule check |
| `combos.txt` | combos in the deck, and one card away |
| `sim/` | Forge runs — `.log` raw, `.txt` report, `.json` data |

View file

@ -3,13 +3,13 @@
_generated 2026-09-07 23:37Z by `tools/variant.py`_
- **bracket 3: PASS** (two-card vacuum combos: 0)
- 86 lines in `decklist.txt`
- 86 lines in `decklist.mgt`
## files
| file | what it is |
| --- | --- |
| `decklist.txt` | the deck, `N Card Name` per line |
| `decklist.mgt` | the deck, `N Card Name` per line |
| `bracket.txt` | bracket-3 + house-rule check |
| `combos.txt` | combos in the deck, and one card away |
| `sim/` | Forge runs — `.log` raw, `.txt` report, `.json` data |

View file

@ -3,13 +3,13 @@
_generated 2026-09-07 15:51Z by `tools/variant.py`_
- **bracket 3: PASS** (two-card vacuum combos: 0)
- 86 lines in `decklist.txt`
- 86 lines in `decklist.mgt`
## files
| file | what it is |
| --- | --- |
| `decklist.txt` | the deck, `N Card Name` per line |
| `decklist.mgt` | the deck, `N Card Name` per line |
| `bracket.txt` | bracket-3 + house-rule check |
| `combos.txt` | combos in the deck, and one card away |
| `sim/` | Forge runs — `.log` raw, `.txt` report, `.json` data |

View file

@ -3,13 +3,13 @@
_generated 2026-09-08 02:07Z by `tools/variant.py`_
- **bracket 3: PASS** (two-card vacuum combos: 0)
- 86 lines in `decklist.txt`
- 86 lines in `decklist.mgt`
## files
| file | what it is |
| --- | --- |
| `decklist.txt` | the deck, `N Card Name` per line |
| `decklist.mgt` | the deck, `N Card Name` per line |
| `bracket.txt` | bracket-3 + house-rule check |
| `combos.txt` | combos in the deck, and one card away |
| `sim/` | Forge runs — `.log` raw, `.txt` report, `.json` data |

View file

@ -3,13 +3,13 @@
_generated 2026-09-07 23:37Z by `tools/variant.py`_
- **bracket 3: PASS** (two-card vacuum combos: 0)
- 86 lines in `decklist.txt`
- 86 lines in `decklist.mgt`
## files
| file | what it is |
| --- | --- |
| `decklist.txt` | the deck, `N Card Name` per line |
| `decklist.mgt` | the deck, `N Card Name` per line |
| `bracket.txt` | bracket-3 + house-rule check |
| `combos.txt` | combos in the deck, and one card away |
| `sim/` | Forge runs — `.log` raw, `.txt` report, `.json` data |

View file

@ -3,13 +3,13 @@
_generated 2026-09-07 23:38Z by `tools/variant.py`_
- **bracket 3: PASS** (two-card vacuum combos: 0)
- 86 lines in `decklist.txt`
- 86 lines in `decklist.mgt`
## files
| file | what it is |
| --- | --- |
| `decklist.txt` | the deck, `N Card Name` per line |
| `decklist.mgt` | the deck, `N Card Name` per line |
| `bracket.txt` | bracket-3 + house-rule check |
| `combos.txt` | combos in the deck, and one card away |
| `sim/` | Forge runs — `.log` raw, `.txt` report, `.json` data |

View file

@ -3,13 +3,13 @@
_generated 2026-09-07 15:51Z by `tools/variant.py`_
- **bracket 3: PASS** (two-card vacuum combos: 0)
- 86 lines in `decklist.txt`
- 86 lines in `decklist.mgt`
## files
| file | what it is |
| --- | --- |
| `decklist.txt` | the deck, `N Card Name` per line |
| `decklist.mgt` | the deck, `N Card Name` per line |
| `bracket.txt` | bracket-3 + house-rule check |
| `combos.txt` | combos in the deck, and one card away |
| `sim/` | Forge runs — `.log` raw, `.txt` report, `.json` data |

View file

@ -3,13 +3,13 @@
_generated 2026-09-07 15:51Z by `tools/variant.py`_
- **bracket 3: PASS** (two-card vacuum combos: 0)
- 86 lines in `decklist.txt`
- 86 lines in `decklist.mgt`
## files
| file | what it is |
| --- | --- |
| `decklist.txt` | the deck, `N Card Name` per line |
| `decklist.mgt` | the deck, `N Card Name` per line |
| `bracket.txt` | bracket-3 + house-rule check |
| `combos.txt` | combos in the deck, and one card away |
| `sim/` | Forge runs — `.log` raw, `.txt` report, `.json` data |

View file

@ -3,13 +3,13 @@
_generated 2026-09-07 15:52Z by `tools/variant.py`_
- **bracket 3: PASS** (two-card vacuum combos: 0)
- 86 lines in `decklist.txt`
- 86 lines in `decklist.mgt`
## files
| file | what it is |
| --- | --- |
| `decklist.txt` | the deck, `N Card Name` per line |
| `decklist.mgt` | the deck, `N Card Name` per line |
| `bracket.txt` | bracket-3 + house-rule check |
| `combos.txt` | combos in the deck, and one card away |
| `sim/` | Forge runs — `.log` raw, `.txt` report, `.json` data |

View file

@ -3,13 +3,13 @@
_generated 2026-09-07 15:51Z by `tools/variant.py`_
- **bracket 3: FAIL** (two-card vacuum combos: 7)
- 86 lines in `decklist.txt`
- 86 lines in `decklist.mgt`
## files
| file | what it is |
| --- | --- |
| `decklist.txt` | the deck, `N Card Name` per line |
| `decklist.mgt` | the deck, `N Card Name` per line |
| `bracket.txt` | bracket-3 + house-rule check |
| `combos.txt` | combos in the deck, and one card away |
| `sim/` | Forge runs — `.log` raw, `.txt` report, `.json` data |

View file

@ -3,13 +3,13 @@
_generated 2026-09-07 15:56Z by `tools/variant.py`_
- **bracket 3: PASS** (two-card vacuum combos: 0)
- 86 lines in `decklist.txt`
- 86 lines in `decklist.mgt`
## files
| file | what it is |
| --- | --- |
| `decklist.txt` | the deck, `N Card Name` per line |
| `decklist.mgt` | the deck, `N Card Name` per line |
| `bracket.txt` | bracket-3 + house-rule check |
| `combos.txt` | combos in the deck, and one card away |
| `sim/` | Forge runs — `.log` raw, `.txt` report, `.json` data |

View file

@ -3,13 +3,13 @@
_generated 2026-09-07 23:37Z by `tools/variant.py`_
- **bracket 3: PASS** (two-card vacuum combos: 0)
- 86 lines in `decklist.txt`
- 86 lines in `decklist.mgt`
## files
| file | what it is |
| --- | --- |
| `decklist.txt` | the deck, `N Card Name` per line |
| `decklist.mgt` | the deck, `N Card Name` per line |
| `bracket.txt` | bracket-3 + house-rule check |
| `combos.txt` | combos in the deck, and one card away |
| `sim/` | Forge runs — `.log` raw, `.txt` report, `.json` data |

View file

@ -3,13 +3,13 @@
_generated 2026-09-07 23:38Z by `tools/variant.py`_
- **bracket 3: PASS** (two-card vacuum combos: 0)
- 86 lines in `decklist.txt`
- 86 lines in `decklist.mgt`
## files
| file | what it is |
| --- | --- |
| `decklist.txt` | the deck, `N Card Name` per line |
| `decklist.mgt` | the deck, `N Card Name` per line |
| `bracket.txt` | bracket-3 + house-rule check |
| `combos.txt` | combos in the deck, and one card away |
| `sim/` | Forge runs — `.log` raw, `.txt` report, `.json` data |

View file

@ -300,7 +300,7 @@ Card selection is offline and instant. Simulation is Forge, and it's the slow st
```
make data (once/per set) ──► data/*.jsonl, variants.json, AtomicCards.json
│
decklist.txt ─────────────────────────► │ blacklist.txt ──┐
decklist.mgt ─────────────────────────► │ blacklist.txt ──┐
│ │ │
├─► bracket_check.py ── game changers, MLD, vacuum combos, blacklist ──┐
├─► combos.py ── included / almostIncluded (offline) ──────────┤
@ -313,13 +313,13 @@ Card selection is offline and instant. Simulation is Forge, and it's the slow st
Every simulation run writes a timestamped triple into `out/`: the full Forge `.log`,
a human `.txt` report, and a `.json` artifact — and the report footer names
`decklist.txt`, the log, the JSON, and itself, so any number is traceable.
`decklist.mgt`, the log, the JSON, and itself, so any number is traceable.
## 4. Repo layout
```
reaper-king-no-more/
decklist.txt # working deck, "1 Card Name" per line
decklist.mgt # working deck, "1 Card Name" per line
blacklist.txt # cards I won't play, "Card Name | reason"
plan.md
data/ # ~1.2GB, all gitignored
@ -327,7 +327,7 @@ reaper-king-no-more/
variants.json AtomicCards.json embeddings.npz (generated)
decks/ # RESULTS, one dir per archetype variant
README.md
no-mikaeus/ # decklist.txt, bracket.txt, combos.txt,
no-mikaeus/ # decklist.mgt, bracket.txt, combos.txt,
mikaeus/ # README.md, sim/<stamp>.{log,txt,json}
tools/
cards.py # shared index: oracle+tags+edhrec+combos+blacklist
@ -363,7 +363,7 @@ insists Commander decks live).
2. ~~`cards.py`~~ — **done.** oracle + 4,369 tags + edhrecRank + combos + blacklist.
3. ~~`bracket_check.py`~~ — **done and passing.**
4. ~~`embed.py` / `similar.py` / `similar_text.py`~~ — **done** (Layer B).
5. ~~Draft the 100~~ — **done**, `decklist.txt`.
5. ~~Draft the 100~~ — **done**, `decklist.mgt`.
6. ~~Forge installed + `to_dck.py` + `run_forge.sh` + `forge_report.py`~~ — **done.**
4-player pod vs precons, reproducible by seed.
7. Iterate on the list using Forge results + the offline combo/tag tools.
@ -375,7 +375,7 @@ with the real one is worse than having one. Forge is the simulator. (`goldfish.p
and its `out/goldfish-*` runs are removed.)
**Report convention** (every tool): a timestamped `.txt` human report plus a `.json`
artifact in `out/`, with the report footer naming `decklist.txt`, the raw log, the
artifact in `out/`, with the report footer naming `decklist.mgt`, the raw log, the
JSON, and itself.
## 6. Deck skeleton to draft against

View file

@ -7,7 +7,7 @@
# correctly (the removal slot still does removal) even though the specific card
# differs.
#
# decklist.txt remains the source of truth. This map applies ONLY when building
# decklist.mgt remains the source of truth. This map applies ONLY when building
# the .dck for simulation, and every substitution is recorded in the run's JSON
# metadata and printed in reports.
#

View file

@ -53,7 +53,7 @@ def run_arm(ctx, label, variant, games, seed, profile, overlay, clock, workdir):
tools_dir = os.path.dirname(os.path.abspath(__file__))
subprocess.run([sys.executable, os.path.join(tools_dir, "to_dck.py"),
"--deck", ctx.dir,
os.path.join(ctx.dir, "decks", variant, "decklist.txt"),
os.path.join(ctx.dir, "decks", variant, "decklist.mgt"),
"-o", os.path.join(dst, dck_name)], capture_output=True)
import shutil as sh
for p in POD:

View file

@ -59,7 +59,7 @@ def variants(deck_root, only=None, resume=False):
out = []
for name in sorted(os.listdir(os.path.join(deck_root, "decks"))):
d = os.path.join(deck_root, "decks", name)
if not os.path.isdir(d) or not os.path.exists(os.path.join(d, "decklist.txt")):
if not os.path.isdir(d) or not os.path.exists(os.path.join(d, "decklist.mgt")):
continue
if only and name not in only:
continue
@ -122,7 +122,7 @@ def run_variant(ctx, name, games, seed, wid, pod_idx):
# build this variant's .dck into the worker's own profile
r = subprocess.run([sys.executable, os.path.join(TOOLS_ROOT, "tools", "to_dck.py"),
"--deck", deck_root,
os.path.join(vdir, "decklist.txt"),
os.path.join(vdir, "decklist.mgt"),
"-o", os.path.join(dst, dck_name)],
capture_output=True, text=True)
if r.returncode != 0:
@ -160,7 +160,7 @@ def run_variant(ctx, name, games, seed, wid, pod_idx):
subprocess.run([sys.executable, os.path.join(TOOLS_ROOT, "tools", "forge_report.py"),
out + ".log", out + ".json", out + ".txt", str(games), str(seed),
os.path.join(vdir, "decklist.txt"), ctx.forge_metadata_name()],
os.path.join(vdir, "decklist.mgt"), ctx.forge_metadata_name()],
capture_output=True)
# Stamp the run config so tuned and stock arms are never silently compared.

View file

@ -2,7 +2,7 @@
"""Check a decklist against Commander Bracket 3 + our house rules.
python3 tools/bracket_check.py --deck decks/reaper-king-no-more
python3 tools/bracket_check.py --deck decks/reaper-king-no-more decks/reaper-king-no-more/decks/no-mikaeus/decklist.txt
python3 tools/bracket_check.py --deck decks/reaper-king-no-more decks/reaper-king-no-more/decks/no-mikaeus/decklist.mgt
Enforces:
* <= 3 Game Changers (official)

View file

@ -103,7 +103,7 @@ def main():
# only cards actually in our decklist
db = DB(blacklist_path=ctx.blacklist_path)
variant_dir = os.path.dirname(os.path.dirname(log))
deck_file = os.path.join(variant_dir, "decklist.txt")
deck_file = os.path.join(variant_dir, "decklist.mgt")
commander, deck = load_decklist(deck_file)
deck_set = set(deck) | {commander}

View file

@ -4,7 +4,7 @@ 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.txt
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__))

View file

@ -23,7 +23,7 @@ def variant_order(ctx):
except ImportError:
pass
rest = sorted(os.path.basename(os.path.dirname(p))
for p in glob.glob(ctx.path("decks", "*", "decklist.txt"))
for p in glob.glob(ctx.path("decks", "*", "decklist.mgt"))
if os.path.basename(os.path.dirname(p)) not in hand)
return hand + rest
@ -46,7 +46,7 @@ def main():
rows = []
for name in order:
vdir = os.path.join(DECKS, name)
deck_f = os.path.join(vdir, "decklist.txt")
deck_f = os.path.join(vdir, "decklist.mgt")
if not os.path.exists(deck_f):
continue
cmd, deck = load_decklist(deck_f)
@ -107,7 +107,7 @@ def main():
slug = ctx.slug
L += ["## layout", "", "```", "decks/<variant>/",
" decklist.txt the 100 cards, \"N Card Name\" per line",
" decklist.mgt the 100 cards, \"N Card Name\" per line",
" bracket.txt bracket-3 + house-rule check",
" combos.txt combos already in the deck, and one card away",
" sim/ Forge runs, one triple per batch:",

View file

@ -17,7 +17,7 @@ Resolution order for the deck directory:
from deckctx import DeckContext
ctx = DeckContext.from_args() # pops --deck out of sys.argv
ctx.path("decklist.txt") # -> decks/<name>/decklist.txt
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"
@ -30,7 +30,7 @@ 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.txt"))
os.path.exists(os.path.join(d, "decklist.mgt"))
def resolve_deck_dir(argv=None, pop=True):
@ -103,7 +103,7 @@ class DeckContext:
@property
def decklist_path(self):
return self.config.get("decklist") and self.path(self.config["decklist"]) \
or self.path("decklist.txt")
or self.path("decklist.mgt")
@property
def blacklist_path(self):

View file

@ -104,7 +104,7 @@ def parse(log_path):
return games
def report(games, n_req, seed, log, jsonp, txtp, deck_path="decklist.txt",
def report(games, n_req, seed, log, jsonp, txtp, deck_path="decklist.mgt",
tally=None, tally_total=0, timeouts=0):
L, add = [], lambda s: L.append(s)
# prefer the authoritative Match Result tally over per-game lines
@ -209,7 +209,7 @@ def main():
log, jsonp, txtp = sys.argv[1], sys.argv[2], sys.argv[3]
n_req = sys.argv[4] if len(sys.argv) > 4 else "?"
seed = sys.argv[5] if len(sys.argv) > 5 else "?"
deck_path = sys.argv[6] if len(sys.argv) > 6 else "decklist.txt"
deck_path = sys.argv[6] if len(sys.argv) > 6 else "decklist.mgt"
ours_name = sys.argv[7] if len(sys.argv) > 7 else deck_name_for(deck_path)
if ours_name:
OURS = ours_name

View file

@ -159,7 +159,7 @@ def main():
only = args[args.index("--only") + 1] if "--only" in args else None
stats = args[args.index("--from-stats") + 1] if "--from-stats" in args else None
base_path = args[args.index("--base") + 1] if "--base" in args else \
ctx.path(ctx.config.get("generate_base", "decks/no-mikaeus/decklist.txt"))
ctx.path(ctx.config.get("generate_base", "decks/no-mikaeus/decklist.mgt"))
recipes = load_recipes(ctx)
if "--list" in args:
@ -203,7 +203,7 @@ def main():
body = [f"1 {n}" for n in sorted(set(res["deck"]) - {"Swamp", "Forest", "Mountain"})]
basics = [f"{Counter(res['deck'])[b]} {b}" for b in ("Swamp", "Forest", "Mountain")
if Counter(res["deck"])[b]]
open(os.path.join(vdir, "decklist.txt"), "w").write(
open(os.path.join(vdir, "decklist.mgt"), "w").write(
"\n".join(header + body + basics) + "\n")
print(f"{name:18} ok -{len(res['cuts'])} +{len(res['adds'])}")
print(f"{'':18} cut: {', '.join(res['cuts'][:4])}{'...' if len(res['cuts'])>4 else ''}")

View file

@ -53,7 +53,7 @@ def main():
from cards import DB
db = DB(blacklist_path=ctx.blacklist_path)
commander = ctx.commander
base_path = ctx.path("decks", "no-mikaeus", "decklist.txt")
base_path = ctx.path("decks", "no-mikaeus", "decklist.mgt")
if not os.path.exists(base_path):
base_path = ctx.decklist_path
base = load(base_path)
@ -87,7 +87,7 @@ def main():
os.makedirs(vdir, exist_ok=True)
header = [f"# {name}", f"# {why}", "", "# Commander",
f"1 {commander}", "", "# Main"]
open(os.path.join(vdir, "decklist.txt"), "w").write(
open(os.path.join(vdir, "decklist.mgt"), "w").write(
"\n".join(header + lines) + "\n")
flags = []

View file

@ -6,7 +6,7 @@ allocation. This module defines the roles (as verified Scryfall Tagger slugs plu
oracle-text fallbacks) and can classify any card or score a whole decklist.
python3 tools/roles.py --deck decks/reaper-king-no-more
python3 tools/roles.py --deck decks/reaper-king-no-more decks/reaper-king-no-more/decks/fast-ramp/decklist.txt
python3 tools/roles.py --deck decks/reaper-king-no-more decks/reaper-king-no-more/decks/fast-ramp/decklist.mgt
"""
import sys, os
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))

View file

@ -22,7 +22,7 @@ STAMP="$(date -u +%Y%m%dT%H%M%SZ)"
VDIR="$DECK_DIR/decks/$VARIANT"
OUT="$VDIR/sim/$STAMP"
[ -f "$VDIR/decklist.txt" ] || { echo "no decklist: $VDIR/decklist.txt" >&2; exit 1; }
[ -f "$VDIR/decklist.mgt" ] || { echo "no decklist: $VDIR/decklist.mgt" >&2; exit 1; }
mkdir -p "$VDIR/sim"
OPPONENTS=(
@ -33,7 +33,7 @@ OPPONENTS=(
# .dck filename and [metadata] Name= are derived from the deck (slug /
# config.yaml `name:`), never hardcoded -- ask to_dck.py what it wrote.
DCK_NAME="$(python3 "$TOOLS/to_dck.py" --deck "$DECK_DIR" "$VDIR/decklist.txt" \
DCK_NAME="$(python3 "$TOOLS/to_dck.py" --deck "$DECK_DIR" "$VDIR/decklist.mgt" \
-o "$FORGE_DECKS/$(basename "$DECK_DIR").dck" | sed -n 's/^wrote \(.*\) (.*/\1/p')"
DCK_NAME="$(basename "$DCK_NAME")"
@ -48,4 +48,4 @@ echo "[$VARIANT] $GAMES games, 4-player Commander pod, seed $SEED"
| stdbuf -oL grep -E "^Game Result:" || true
python3 "$TOOLS/forge_report.py" "$OUT.log" "$OUT.json" "$OUT.txt" \
"$GAMES" "$SEED" "$VDIR/decklist.txt"
"$GAMES" "$SEED" "$VDIR/decklist.mgt"

View file

@ -1,8 +1,8 @@
#!/usr/bin/env python3
"""Convert decklist.txt -> Forge .dck format.
"""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.txt] [-o ~/forge/decks/<slug>.dck]
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
@ -70,7 +70,7 @@ def main():
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(".txt")), ctx.decklist_path)
src = next((a for a in args if a.endswith(".mgt")), ctx.decklist_path)
commander, deck = load_decklist(src)
@ -107,7 +107,7 @@ def main():
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.txt, or Forge will drop it)\n", 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)

View file

@ -7,7 +7,7 @@
Each variant directory ends up self-contained:
<deck>/decks/<name>/
decklist.txt the 100 cards
decklist.mgt the 100 cards
bracket.txt bracket-3 + house-rule check
combos.txt combos in the deck, and one card away
sim/ Forge runs: <stamp>.{log,txt,json}
@ -38,7 +38,7 @@ def main():
tools = os.path.join(TOOLS_ROOT, "tools")
vdir = ctx.path("decks", name)
deck = os.path.join(vdir, "decklist.txt")
deck = os.path.join(vdir, "decklist.mgt")
if not os.path.exists(deck):
sys.exit(f"no decklist at {deck}")
os.makedirs(os.path.join(vdir, "sim"), exist_ok=True)
@ -90,13 +90,13 @@ def main():
f"_generated {stamp} by `tools/variant.py`_",
"",
f"- **bracket 3: {verdict}** (two-card vacuum combos: {vac})",
f"- {n_deck} lines in `decklist.txt`",
f"- {n_deck} lines in `decklist.mgt`",
"",
"## files",
"",
"| file | what it is |",
"| --- | --- |",
"| `decklist.txt` | the deck, `N Card Name` per line |",
"| `decklist.mgt` | the deck, `N Card Name` per line |",
"| `bracket.txt` | bracket-3 + house-rule check |",
"| `combos.txt` | combos in the deck, and one card away |",
"| `sim/` | Forge runs — `.log` raw, `.txt` report, `.json` data |",