File size: 9,699 Bytes
a8351e8
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
---
license: cc-by-4.0
tags:
- magic-the-gathering
- game-playing
- graph-neural-network
- xmage
---

# 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](https://github.com/danieljbrooks/draft-zero)'s strongest network on the
XMage engine, built on Will Wroble's [MageZero](https://github.com/WillWroble/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_calibrated` in DraftZero's runs
  ([docs/024](https://github.com/danieljbrooks/draft-zero/blob/main/docs/024-gnn-local-layers-and-games.md)).

## 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`.

```python
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`, `value` and `value_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](https://github.com/danieljbrooks/draft-zero/blob/main/docs/023-gnn-run-log.md) and [docs/024](https://github.com/danieljbrooks/draft-zero/blob/main/docs/024-gnn-local-layers-and-games.md) |
| Data | [17lands public game data](https://www.17lands.com/public_datasets), 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.