MTG Commander Deck Completion
Given a Commander (and optionally a partial decklist), suggests which other cards belong in the deck. Trained on 122,571 real public Commander decklists from Moxfield (dataset).
Training/evaluation code: github.com/nsaroiu/mtg-deck-completion (the tokenizer + model + training/eval pipeline that produced these weights β not the Moxfield data-collection pipeline or the web UI built on top of this model, which live elsewhere and aren't public).
Architecture
Every card gets a hybrid embedding: a learned per-card vector, summed with a small MLP over structured features (color identity, mana cost, type, keywords) and a semantic sentence embedding of its oracle text β so even a rarely-seen card starts from a meaningful representation instead of a near-random one.
A deck is treated as an unordered set: a permutation-invariant encoder (mean-pool or self-attention) pools the known cards + commander(s) into one vector, scored against the full card vocabulary via a tied dot product (word2vec-style).
Production inference is an ensemble of two specialized checkpoints, not one model β a single model/loss was tried and found to trade off staple recognition against synergy ranking:
- Synergy phase (
set_transformer_softmax_checkpoint.pt, self-attention encoder): generates iteratively, a few picks at a time, re-conditioning on its own prior picks β this is what surfaces specific combo/synergy pieces rather than a generic "good stuff" pile. - Staple backfill (
deepsets_softmax_checkpoint.pt, mean-pool encoder): tops up near-universal staples (Sol Ring, Command Tower, ...) to a realistic target count, since the synergy phase alone under-recommends them. - Optional pruning pass (
prune_checkpoint_inject_lowdensity.pt): a separate discriminator model that re-scores the assembled deck and cuts the weakest fit, the one step no generation-only model can do.
Files
| file | role |
|---|---|
deepsets_softmax_checkpoint.pt |
staple-recognition specialist |
set_transformer_softmax_checkpoint.pt |
synergy-ranking specialist |
prune_checkpoint_inject_lowdensity.pt |
optional deck-pruning pass |
tokenizer.json |
card vocabulary + structured features |
tokenizer_text_embeddings.npy |
cached oracle-text sentence embeddings |
card_tiers.json |
staple / mid-tier / long-tail classification per card |
All three checkpoints share the same card vocabulary and expect
tokenizer.json / tokenizer_text_embeddings.npy alongside them.
Evaluation
Recall@50 / Precision@50 / MRR on held-out real decks, by how much of the deck is already known (mask ratio β low = mostly complete, high = mostly empty), staple-recognition checkpoint, validation split:
| mask ratio | Recall@50 | Precision@50 | MRR |
|---|---|---|---|
| 0.1 (deck mostly complete) | 0.60 | 0.10 | 0.132 |
| 0.5 | 0.51 | 0.44 | 0.068 |
| 0.9 (deck mostly empty) | 0.38 | 0.59 | 0.046 |
Held-out-commander split (commanders never seen in training, testing generalization via card content rather than memorized co-occurrence): Recall@50 0.27β0.41 across the same ratio range β meaningfully above chance, confirming the hybrid content embeddings carry real signal for unfamiliar commanders. Full breakdown by card-popularity tier, plus the synergy/pruning checkpoints' own numbers, in the GitHub repo's eval output.
Usage
There's no standalone pip package yet β loading these checkpoints requires
the tokenizer + model code from the GitHub repo above (tokenizer/ and
training/; no other part of that repo is needed to run inference):
git clone https://github.com/nsaroiu/mtg-deck-completion
cd mtg-deck-completion
pip install -r requirements.txt
from huggingface_hub import hf_hub_download
repo = "nsaroiu/mtg-deck-completion"
for f in ["deepsets_softmax_checkpoint.pt", "set_transformer_softmax_checkpoint.pt",
"prune_checkpoint_inject_lowdensity.pt", "tokenizer.json",
"tokenizer_text_embeddings.npy", "card_tiers.json"]:
hf_hub_download(repo_id=repo, filename=f, local_dir=".")
from training.evaluate import load_checkpoint
from training.ensemble import ensemble_complete_deck, load_tiers
from training.train import pick_device
device = pick_device()
staple_model, tok, _ = load_checkpoint("deepsets_softmax_checkpoint.pt", device)
synergy_model, _, _ = load_checkpoint("set_transformer_softmax_checkpoint.pt", device)
tiers = load_tiers(tok, path="card_tiers.json")
# (name, score, source) triples, source in {"synergy", "staple"}
results = ensemble_complete_deck(
["Atraxa, Praetors' Voice"], [], 20, tok, device,
staple_model, synergy_model, tiers,
)
for name, score, source in results:
print(f"{score:.3f} [{source:7}] {name}")
training/ensemble.py is also a CLI covering the same flow shown above
(--commander β repeatable, for partner commanders β --card,
--chunk-size/--temperature for the iterative synergy phase,
--prune-checkpoint to enable the pruning pass). training/complete_deck.py
exposes just one specialist checkpoint at a time, without the ensemble
fusion, if that's all you need.
Limitations
- Staple-tier ranking is imprecise in a near-empty context. The model confidently recognizes that a staple belongs (~90% precision when it predicts one) well before it reliably ranks which specific staple is the true target β the ensemble's backfill phase exists specifically to work around this.
- Color-identity legality is not learned β it's enforced as a
deterministic post-filter (see
legal_maskintraining/complete_deck.py). Using the raw logits without that filter can suggest illegal cards. - Iterative generation (the synergy phase's default mode) is an experimental setting, tuned on a sample of 11 commanders, not exhaustively validated.
- Trained only on real, currently-popular Commander decks β a Commander format power-level/meta shift (new sets, ban changes) isn't reflected until retrained on fresher data.
License
The weights and code in this repository are released under the MIT license. The training data itself is a scrape of public Moxfield decklists with no asserted license (see the dataset card) β if that matters for your use case, that's Moxfield's terms to check, not a constraint this repo imposes.