|
Download README.md from danbrooks/draftzero-fdn-gnn: direct link, hf CLI and curl.
- Browser
- Download file 9.7 kB
-
https://huggingface.co/danbrooks/draftzero-fdn-gnn/resolve/main/README.md
- Command line
-
hf download hf://danbrooks/draftzero-fdn-gnn/README.md
-
curl -L -o README.md https://huggingface.co/danbrooks/draftzero-fdn-gnn/resolve/main/README.md
9.7 kB
| 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. | |