DraftZero FDN graph network
A graph neural network that plays Magic: The Gathering Limited with Foundations (FDN) decks, trained by imitating 17lands' top players. It is DraftZero's strongest network on the XMage engine, built on Will Wroble's MageZero graph encoder and network.
- What it does: given the acting player's view of an XMage game as a graph, it scores every legal option (policy) and estimates the chance of winning (value). A tree search can use both, or a bot can play the policy's top choice directly.
- What it saw in training: 10.9 million decisions from 145,898 FDN Premier Draft games by players in 17lands' 60%-and-up win-rate group, replayed inside XMage. The opponent's hand is not part of its input.
- Size: 13.4M parameters (width 256), 1,932 leaf features, 22 edge labels. Float32 weights, 54 MB.
- Source:
gnn/full_r1/best_policy_calibratedin DraftZero's runs (docs/024).
How strong it is
Its bot played MageZero's heuristic bot, which searches 100 simulations a decision. A search at N simulations plays N short look-ahead games before each decision. Both bots used PIMC search: each decision deals the hidden cards once into a world that fits what the bot has seen, guessing the opponent's deck from real 17lands decks, and runs MageZero's tree search on it. Every deck pair was played twice with the seats swapped (paired games), which lowers the variance.
| Network's simulations a decision | Won | Time a decision |
|---|---|---|
| 0: the policy's top choice | 44.7% of 103 games | ~0.2 s |
| 100 | 62.5% of 104 | ~7 s |
| 300 | 69.2% of 104 | ~20 s |
| 1,000 | 69.3% of 101 | 40-65 s |
Times are from those games: up to 30 games at once on one rented RTX 3090 machine, the network served in bfloat16, most of the time spent in XMage rather than in the network. On one CPU thread of a laptop, one network call takes about 20 ms (batch 1, median 226 nodes a state). On held-out test games, at decisions where the human made a play rather than passing, the policy's top choice was a play the human made 85.9% of the time.
Files
| File | What |
|---|---|
model.safetensors |
the weights, float32 |
config.json |
the architecture, the encoder's constants (node types, value buckets, hash) and the source pins |
vocab.json |
leaf_ids and edge_ids: the raw feature ids that have embedding rows, in row order |
graph_net.py |
the network (MageZero's NetGraph), as DraftZero trains and serves it |
gnn_release.py |
a loader with no pickles: weights, vocab, state -> network input, option logits, the golden check |
goldens.jsonl.gz |
300 encoded test states with this network's outputs (below) |
coverage.json |
how much of the test data the vocab covers, a baseline for checking an encoder |
check.json |
gnn_release.py check on this release |
SHA256SUMS |
the files' hashes |
Requirements: Python 3.10+, torch (2.1 or later), numpy, safetensors.
import gnn_release as r
model, leaves, edges, config = r.load(".")
goldens = r.read_goldens("goldens.jsonl.gz")
g = goldens[0]
out = r.evaluate(model, [g["state"]], leaves, edges)[0]
print(r.option_logits(out, g["graph_type"], g["options"]), out["value"])
How it plays: what a port needs
1. The encoder. The input is MageZero's state graph: typed nodes (ROOT, PLAYER, ZONE, STACK_OBJECT, PERMANENT,
CARD, ABILITY) with LEAF children, and edges from child to parent with a label. Leaves are XMage strings: card names,
type and subtype names, Ability.toString() rules text, decision text, step names. Each string becomes a feature id,
StateEncoder.indexFor(StateEncoder.hash64(name)) (graph_net.feature_id in Python). Some leaves also carry a
number (power, toughness, counters, life). The encoder is Java, at
WillWroble/mage@e4afc9c77ba7e4a6dbc24966cbdc6dc0819e0bff (branch graph-encoder,
Mage.Server.Plugins/Mage.Player.AI/src/main/java/mage/player/ai/encoder). DraftZero runs a vendored copy in
java/mzbridge/src/org/draftzero/mzbridge/graph on XMage danieljbrooks/mage@48e49184 (branch
v0.2-generalist). It encodes the acting player's view, with the opponent's hand hidden (perfectInfo false).
A leaf outside the vocab has no embedding: its edges are dropped silently, without an error. So an encoder running on
another XMage version whose strings differ (rules text, decision text) plays worse without failing. To catch this,
count how many leaf occurrences hit vocab.json. On DraftZero's test data the vocab covers more than 99.99% of
them in priority, attack and target decisions, and 99.3% in block decisions (coverage.json).
2. Decisions and options. graph_type is MageZero's ActionType ordinal. Each legal option is a list of graph
nodes, and its logit is the log-sum-exp of its nodes' scores under the head the decision reads:
| Decision | Head | An option's nodes |
|---|---|---|
Priority (graph_type 0) |
priority |
the ability's node, one per copy of a card (two copies of a card are one option); Pass has its own node |
| Target, including blocks (3) | target |
the target's node (CARD, PERMANENT, STACK_OBJECT or PLAYER); Stop Choosing is a CARD node |
| Attack with a creature (3) | target |
no = Stop Choosing's node, yes = the defending player's node |
| Other yes/no (5) | use |
[no, yes] logits, no nodes |
Mulligans, trigger ordering, damage assignment and similar choices have no head. In DraftZero's games mulligans were off (every opening hand was kept), and MageZero's player made the other choices without the network's policy.
3. The value. value = tanh(value_x) is the acting player's expected result in -1..1, and
P(win) = sigmoid(2 * value_x). A temperature fitted on validation games (T = 1.607) is already folded into the value
head's last layer.
4. The search in DraftZero's games. This is MageZero's tree search, run fresh at every decision with no tree reuse, on one sampled world (PIMC):
- selection by PUCT with c = 1;
- priors from the policy: softmax of the option logits at temperature 1.5, then 0.1 added to every option that
isn't Pass or a mana ability (not renormalized: MageZero's
setPriors); - uniform priors at the opponent's decisions;
- leaves scored by the value head, discounted by 0.99 per ply;
- the move with the most visits is played.
The code is java/mzbridge/src/mage/player/ai (BenchPlayer, BenchSearch, GraphNet, GraphMCTSPlayer) at
danieljbrooks/draft-zero@2a461518. Playing the policy's top choice alone needs no search and no hidden-card sampling.
Checking a port with the goldens
goldens.jsonl.gz holds 300 decisions from the test split: 160 priority, 50 attack, 50 target and 40 block
decisions. They come from games the network never trained on, encoded by the Java encoder above. Each line has:
state: the encoder's output in MageZero's graph-server layout:indices(raw feature id per node),values,edge_child,edge_parent,edge_label;graph_type,options(node indices per legal option),option_is_pass,human_choice(the option the 17lands player took, for interest);expected:option_logits,use,valueandvalue_x.
The expected outputs come from DraftZero's games inference server (tools/imitation_scale/graph_server.py, loading
the original checkpoint) on CPU in float32, one state at a time. python gnn_release.py check . loads this release
and compares. A faithful port agrees to about 1e-5 and picks the same top option in all 300 (check.json).
The goldens check the network, its weights and the option mapping. They don't check an encoder, because states can't move between XMage versions as is; for that, use the vocab coverage above.
Limits
- Imitation only. It learned from top players' moves and game results and has not trained by self-play yet.
- Measured against one opponent: MageZero's heuristic bot, with PIMC and guessed decks. There is no Elo or human-play result yet.
- XMage-specific input. The features are XMage's own strings, so other engines need either XMage as the encoder or a re-implementation that reproduces those strings exactly.
- Search is not optional for strength. The policy alone lost to the heuristic bot (44.7%); 100 simulations or more are needed to beat it.
Sources
| What | Where |
|---|---|
| Network | danieljbrooks/draft-zero@2a461518985e3b2bb393bddac4649e217f8fddfe, src/draftzero/gameplay/graph_net.py, a copy of MageZero's NetGraph (WillWroble/MageZero@de225045, branch graph-encoder) |
| Encoder | WillWroble/mage@e4afc9c77ba7e4a6dbc24966cbdc6dc0819e0bff (branch graph-encoder) |
| Engine | danieljbrooks/mage@48e4918413392523a5f9d7915e1f33941ddea21e (branch v0.2-generalist: DraftZero's FDN changes on MageZero v0.2) |
| Training | docs/023 and docs/024 |
| Data | 17lands public game data, FDN Premier Draft, CC BY 4.0 |
The weights are released under CC BY 4.0, like the 17lands data they were trained on. The code (graph_net.py,
gnn_release.py) is MIT, as are DraftZero and MageZero. The network and encoder are Will Wroble's MageZero design;
DraftZero trained these weights.
- Downloads last month
- 10