monoR: a 120,514-parameter policy that plays Pauper Mono Red Madness
A tiny transformer that plays the Pauper Mono Red Madness deck in Forge, the open-source Magic: The Gathering rules engine. Its only job is to kill a goldfish (an opponent that never does anything) in as few turns as possible.
On 50 held-out games it wins on turn 5.48 on average, 22% of games by turn 4 and 60% by turn 5. Checkpoint: Simulation run 2, round 5.
How it was built, step by step, with replays of its games: vedalken/monoR_madness.
Inspired by anakin87/tiny-doom-defender.
How it decides
Every decision Forge asks for (mulligan, what to play in a main phase, which card to discard, whether to cast a madness card, which mode) becomes one sequence of tokens:
- one global token: turn, life totals, lands and untapped lands, cards drawn and spells cast this turn, decision kind, phase, and what is left in the library;
- one token per card you can see (hand, battlefield, graveyard, exile), with its zone;
- one token per legal option.
A 3-layer transformer (width 64, 4 heads) reads the sequence and scores the option tokens; the softmax runs over legal options only, so the model can never pick an illegal move. A value head on the global token was used during PPO.
How it was trained
- Rules oracle. A hand-written policy (priority lists, a lethal check, a mulligan rule) that only reads what the model sees.
- Supervised cloning of the oracle on thousands of Forge games.
- Hand-tuned rules, each one tested on the same 50 validation seeds (Forge is deterministic per seed, so comparisons are paired).
- Expert iteration from simulation. Play the model, and at up to 8 non-obvious decisions per game try every legal option: replay to that point, reshuffle the library (the future is unknown, as in real play), finish the game with the model, 4 reshuffles per option. The option with the fastest mean kill becomes the label; fine-tune and repeat. Nobody writes the strategy: if Lava Dart on turn 1 is bad, the rollouts show it.
Results (50 validation games, mean kill turn, lower is better)
| version | mean kill turn | dead by turn 4 | dead by turn 5 |
|---|---|---|---|
| Rules: original oracle | 6.12 | 12% | 46% |
| Rules: best hand-tuned oracle | 5.74 | 20% | 56% |
| SFT clone of the original oracle | 6.12 | 12% | 46% |
| PPO from that clone | 5.98 | 12% | 50% |
| SFT clone of the hand-tuned oracle | 5.76 | 20% | 54% |
| Simulation run 1, round 1 | 6.12 | 14% | 46% |
| Simulation run 1, round 2 | 5.98 | 16% | 52% |
| Simulation run 1, round 3 | 5.92 | 20% | 56% |
| Simulation run 2, round 1 | 5.66 | 20% | 56% |
| Simulation run 2, round 2 | 5.66 | 20% | 60% |
| Simulation run 2, round 3 | 5.62 | 20% | 60% |
| Simulation run 2, round 4 | 5.58 | 20% | 56% |
| Simulation run 2, round 5 | 5.48 | 22% | 60% |
Games not won by turn 12 count as 13.
Use
The model needs Forge and the monoR bridge to produce decision requests; model.py has
the featurizer and the network.
import sys
from huggingface_hub import snapshot_download
from safetensors.torch import load_file
path = snapshot_download("vedalken/monoR_madness")
sys.path.insert(0, path)
from model import Policy, collate, featurize
policy = Policy()
policy.load_state_dict(load_file(f"{path}/model.safetensors"))
policy.eval()
# msg = {"kind": "main", "options": [...], "state": {...}} (a bridge decision request)
# logits, value = policy(collate([featurize(msg)]))
Limitations
- Trained only against a goldfish: it ignores the opponent's creatures, blockers and removal.
- One deck list (
deck.dck) and 12 turns maximum. - Discard choices still copy a weakness of the rules: with fewer than 3 lands in play it avoids discarding Mountains, even when flooded.
Files
model.safetensors,config.json: weights and network size.model.py: featurizer and network.deck.dck: the deck (Forge format).results.json: validation summary of this checkpoint.
- Downloads last month
- -
