monoR: a tiny model that plays Mono Red Madness

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

  1. Rules oracle. A hand-written policy (priority lists, a lethal check, a mulligan rule) that only reads what the model sees.
  2. Supervised cloning of the oracle on thousands of Forge games.
  3. Hand-tuned rules, each one tested on the same 50 validation seeds (Forge is deterministic per seed, so comparisons are paired).
  4. 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
-
Safetensors
Model size
121k params
Tensor type
F32
·
Video Preview
loading