Alex-GSL commited on
Commit
5afa8d5
·
verified ·
1 Parent(s): 31225de

Upload README.md with huggingface_hub

Browse files
Files changed (1) hide show
  1. README.md +586 -145
README.md CHANGED
@@ -8,200 +8,641 @@ tags:
8
  - self-play
9
  - jax
10
  - flax
11
- - simba
12
  license: apache-2.0
13
- datasets:
14
- - self-play
15
  pipeline_tag: reinforcement-learning
16
  ---
17
 
18
- # Gin Rummy MDP -- R42 RL Agent
 
 
 
 
19
 
20
- A JAX-native reinforcement learning agent for Gin Rummy, trained via PPO self-play. **R42** is the latest and strongest model, using a SimBa (Simple Block Architecture) backbone with auxiliary phase-decomposed value heads.
21
 
22
- Play against it in your browser or load it in code -- no GPU required.
23
 
24
- ## Quickstart: Play Against R42 in Your Browser
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
25
 
26
  ```bash
27
- git clone https://github.com/GoodStartLabs/GinRummyMdp.git
28
- cd GinRummyMdp
29
- pip install uv
30
- uv sync
31
-
32
- # Download the R42 checkpoint
33
- pip install huggingface_hub
34
- python -c "
35
  from huggingface_hub import hf_hub_download
36
- hf_hub_download(
37
- repo_id='GoodStartLabs/gin-rummy-mdp',
38
- filename='checkpoints/r42/stage1_final.pkl',
39
- local_dir='.',
40
- repo_type='model',
 
 
41
  )
42
- "
43
 
44
- # Launch the web UI
45
- uv run python scripts/play_web.py --checkpoint checkpoints/r42/stage1_final.pkl --port 8080
 
 
 
 
 
 
46
  ```
47
 
48
- Then open [http://localhost:8080](http://localhost:8080) in your browser.
49
 
50
- ## Model Details
51
 
52
- | Property | Value |
53
- |---|---|
54
- | **Architecture** | `ActorCriticSimBaAux` -- SimBa with residual blocks + auxiliary heads |
55
- | **Parameters** | ~4.5M |
56
- | **Checkpoint size** | ~18 MB (`.pkl`, pickled JAX arrays) |
57
- | **Hidden dim** | 1024 |
58
- | **Residual blocks** | 2 |
59
- | **Normalization** | LayerNorm |
60
- | **Training steps** | 1.4 billion environment steps (PPO self-play) |
61
- | **Framework** | JAX + Flax Linen |
62
- | **Observation dim** | 342 |
63
- | **Action dim** | 16 |
64
 
65
- ### Architecture
 
 
 
66
 
67
- `ActorCriticSimBaAux` uses Simple Block Architecture (SimBa) as the backbone -- a stack of residual blocks with LayerNorm and linear projections. On top of the shared trunk, it has:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
68
 
69
- - **Policy head** -- 16-dim action logits
70
- - **3 phase-specific value heads** -- separate critics for draw, discard, and knock phases
71
- - **Auxiliary head** -- opponent deadwood prediction (helps the agent reason about opponent hand strength)
72
 
73
- ### Observation Space (342 dimensions)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
74
 
75
- The observation vector encodes the full visible game state:
76
- - Cards in hand, discard pile top, known/unknown cards
77
- - Meld structure and deadwood count
78
- - Turn number, stock size, phase indicator
79
- - Gin rating, opponent deadwood estimate, unseen card pool statistics
80
 
81
- ### Action Space (16 discrete actions)
82
 
83
- | Action | Meaning |
84
- |---|---|
85
- | 0 | Draw from stock pile |
86
- | 1 | Draw from discard pile |
87
- | 2--12 | Discard card at hand index 0--10 |
88
- | 13 | Continue (don't knock) |
89
- | 14 | Knock |
90
- | 15 | Gin |
 
 
 
 
 
 
 
 
 
 
 
 
 
 
91
 
92
- ### Card Encoding
93
 
94
- Cards are integers 0--51:
95
- - **Suit**: `card // 13` (0=Clubs, 1=Diamonds, 2=Hearts, 3=Spades)
96
- - **Rank**: `card % 13` (0=Ace, 1=Two, ..., 9=Ten, 10=Jack, 11=Queen, 12=King)
97
- - **Deadwood values**: Ace=1, 2--9=face value, 10/J/Q/K=10
98
 
99
- ## Loading the Model in Code
100
 
101
- ```python
102
- import pickle
103
- import jax.numpy as jnp
104
- from training.networks import ActorCriticSimBaAux
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
105
 
106
- # Load checkpoint
107
- with open("checkpoints/r42/stage1_final.pkl", "rb") as f:
108
- params = pickle.load(f)
109
 
110
- # Initialize network
111
- network = ActorCriticSimBaAux(action_dim=16, hidden_dim=1024, num_blocks=2)
112
 
113
- # Run inference
114
- dummy_obs = jnp.zeros((342,), dtype=jnp.float32)
115
- logits, v_draw, v_discard, v_knock, opp_dw_pred = network.apply(params, dummy_obs)
116
 
117
- # Sample an action
118
- import jax
119
- key = jax.random.PRNGKey(0)
120
- action = jax.random.categorical(key, logits)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
121
  ```
122
 
123
- ## Requirements
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
124
 
125
- - Python 3.10+
126
- - JAX (CPU is fine -- model is tiny, runs in milliseconds)
127
- - Flax
128
- - The game engine from [GoodStartLabs/GinRummyMdp](https://github.com/GoodStartLabs/GinRummyMdp)
129
 
130
- Install everything:
131
- ```bash
132
- git clone https://github.com/GoodStartLabs/GinRummyMdp.git
133
- cd GinRummyMdp
134
- pip install uv && uv sync
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
135
  ```
136
 
137
- ## Available Checkpoints
138
 
139
- ### R42 (latest, recommended)
140
 
141
- SimBaAux architecture, 1.4B steps, phase-decomposed value heads.
142
-
143
- | File | Steps | Size |
144
- |---|---|---|
145
- | `checkpoints/r42/stage1_final.pkl` | 1.4B (final) | 18 MB |
146
- | `checkpoints/r42/stage1_100M.pkl` | 100M | 18 MB |
147
- | `checkpoints/r42/stage1_200M.pkl` | 200M | 18 MB |
148
- | `checkpoints/r42/stage1_300M.pkl` | 300M | 18 MB |
149
- | `checkpoints/r42/stage1_400M.pkl` | 400M | 18 MB |
150
- | `checkpoints/r42/stage1_500M.pkl` | 500M | 18 MB |
151
- | `checkpoints/r42/stage1_600M.pkl` | 600M | 18 MB |
152
- | `checkpoints/r42/stage1_700M.pkl` | 700M | 18 MB |
153
- | `checkpoints/r42/stage1_800M.pkl` | 800M | 18 MB |
154
- | `checkpoints/r42/stage1_900M.pkl` | 900M | 18 MB |
155
- | `checkpoints/r42/stage1_1000M.pkl` | 1.0B | 18 MB |
156
- | `checkpoints/r42/stage1_1100M.pkl` | 1.1B | 18 MB |
157
- | `checkpoints/r42/stage1_1200M.pkl` | 1.2B | 18 MB |
158
- | `checkpoints/r42/stage1_1300M.pkl` | 1.3B | 18 MB |
159
- | `checkpoints/r42/stage1_1400M.pkl` | 1.4B | 18 MB |
160
 
161
- ### Older Runs
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
162
 
163
- | Directory | Architecture | Notes |
164
- |---|---|---|
165
- | `checkpoints/r39/` | SimBa | Earlier SimBa experiment |
166
- | `checkpoints/r40/` | SimBa | SimBa with tuning |
167
- | `checkpoints/r35/`, `r36/` | MLP | Categorical reward variants |
168
- | `checkpoints/r33/`, `r34/` | MLP | Prior MLP runs |
169
- | `checkpoints/r25/` | MLP | First observation-upgraded model |
 
170
 
171
- ### Other Data
172
 
173
- - `human_games/` -- JSON logs of human vs. model games (useful for analysis)
174
- - `configs/` -- Training configuration files
175
 
176
- ## Training Details
177
 
178
- R42 was trained with:
179
- - **Algorithm**: PPO (Proximal Policy Optimization) with GAE
180
- - **Self-play**: Agent plays against a mixed pool of opponents
181
- - **Opponent mix**: 30% heuristic, 15% aggressive knock, 10% meld builder, 10% early knock, 10% defensive, 10% superhuman (lv5), 5% superhuman (lv4), 5% superhuman (lv7), 5% frozen checkpoint
182
- - **Hyperparameters**: lr=2.5e-4 (annealed), 4096 parallel envs, 128 steps/rollout, 4 minibatches, 4 update epochs, gamma=1.0, GAE lambda=0.98, clip=0.2, entropy=0.025
183
- - **Auxiliary loss**: opponent deadwood prediction (coef=0.1)
184
- - **Infrastructure**: NVIDIA A40 GPU, ~18 hours wall time
185
 
186
- W&B project: [good-start-labs/gsl-gin-rummy-mdp](https://wandb.ai/good-start-labs/gsl-gin-rummy-mdp)
187
 
188
- ## About the Game Engine
189
 
190
- The Gin Rummy environment is a fully JAX-native implementation:
191
- - Pure functional game logic (no Python control flow on traced values)
192
- - Fully JIT-compatible -- `jax.jit`, `jax.vmap`, `jax.lax.scan` all work
193
- - Supports thousands of parallel games for fast self-play training
194
- - Web UI for human vs. model play
195
 
196
- Source: [github.com/GoodStartLabs/GinRummyMdp](https://github.com/GoodStartLabs/GinRummyMdp)
197
 
198
- ## Citation
199
 
200
- ```bibtex
201
- @misc{goodstartlabs2026ginrummy,
202
- title={Gin Rummy MDP: A JAX-Native RL Environment and Agent},
203
- author={Good Start Labs},
204
- year={2026},
205
- url={https://huggingface.co/GoodStartLabs/gin-rummy-mdp}
206
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
207
  ```
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
8
  - self-play
9
  - jax
10
  - flax
 
11
  license: apache-2.0
 
 
12
  pipeline_tag: reinforcement-learning
13
  ---
14
 
15
+ # Gin Rummy MDP
16
+
17
+ A reinforcement learning model for [Gin Rummy](https://en.wikipedia.org/wiki/Gin_rummy), trained via PPO self-play in pure JAX.
18
+
19
+ **R42** is the latest checkpoint: a 4.58M parameter SimBa residual network trained for 1.4 billion environment steps against a mixed curriculum of opponents. The checkpoint is an 18 MB pickle file that runs on CPU with no GPU required.
20
 
21
+ **Play it live:** [https://sleeping-frames-coalition-justin.trycloudflare.com](https://sleeping-frames-coalition-justin.trycloudflare.com)
22
 
23
+ **Source code:** [github.com/GoodStartLabs/gin-rummy-mdp](https://github.com/GoodStartLabs/gin-rummy-mdp) (not required to use the model)
24
 
25
+ ---
26
+
27
+ ## Table of Contents
28
+
29
+ 1. [Quick Start (Python)](#1-quick-start-python)
30
+ 2. [Card Encoding](#2-card-encoding)
31
+ 3. [Action Space](#3-action-space)
32
+ 4. [Game Flow](#4-game-flow)
33
+ 5. [Observation Vector (342 dimensions)](#5-observation-vector-342-dimensions)
34
+ 6. [Network Architecture](#6-network-architecture)
35
+ 7. [Checkpoint Format](#7-checkpoint-format)
36
+ 8. [Inference Step by Step](#8-inference-step-by-step)
37
+ 9. [Legal Action Rules](#9-legal-action-rules)
38
+ 10. [Scoring Rules](#10-scoring-rules)
39
+ 11. [PyTorch Reference Implementation](#11-pytorch-reference-implementation)
40
+ 12. [Available Checkpoints](#12-available-checkpoints)
41
+ 13. [Training Details](#13-training-details)
42
+
43
+ ---
44
+
45
+ ## 1. Quick Start (Python)
46
 
47
  ```bash
48
+ pip install jax flax huggingface_hub
49
+ ```
50
+
51
+ ```python
 
 
 
 
52
  from huggingface_hub import hf_hub_download
53
+ import pickle, jax.numpy as jnp
54
+
55
+ # Download the R42 final checkpoint (1.4B steps)
56
+ path = hf_hub_download(
57
+ repo_id="GoodStartLabs/gin-rummy-mdp",
58
+ filename="checkpoints/r42/stage1_final.pkl",
59
+ repo_type="model",
60
  )
 
61
 
62
+ # Load parameters
63
+ with open(path, "rb") as f:
64
+ params = pickle.load(f)
65
+ p = params.get("params", params)
66
+
67
+ # p is a dict of weight arrays — see "Checkpoint Format" below
68
+ print(sorted(p.keys()))
69
+ # ['Dense_0', 'Dense_1', ..., 'LayerNorm_0', ..., 'opp_dw_pred', 'value_discard', 'value_draw', 'value_knock']
70
  ```
71
 
72
+ ---
73
 
74
+ ## 2. Card Encoding
75
 
76
+ Cards are integers **0 through 51**.
 
 
 
 
 
 
 
 
 
 
 
77
 
78
+ ```
79
+ suit = card // 13 (0=Spades, 1=Hearts, 2=Diamonds, 3=Clubs)
80
+ rank = card % 13 (0=Ace, 1=Two, 2=Three, ..., 9=Ten, 10=Jack, 11=Queen, 12=King)
81
+ ```
82
 
83
+ ### Full Mapping
84
+
85
+ | Card | Suit | Rank | Name |
86
+ |------|------|------|------|
87
+ | 0 | Spades | 0 | Ace of Spades |
88
+ | 1 | Spades | 1 | Two of Spades |
89
+ | 2 | Spades | 2 | Three of Spades |
90
+ | ... | ... | ... | ... |
91
+ | 9 | Spades | 9 | Ten of Spades |
92
+ | 10 | Spades | 10 | Jack of Spades |
93
+ | 11 | Spades | 11 | Queen of Spades |
94
+ | 12 | Spades | 12 | King of Spades |
95
+ | 13 | Hearts | 0 | Ace of Hearts |
96
+ | 14 | Hearts | 1 | Two of Hearts |
97
+ | ... | ... | ... | ... |
98
+ | 25 | Hearts | 12 | King of Hearts |
99
+ | 26 | Diamonds | 0 | Ace of Diamonds |
100
+ | ... | ... | ... | ... |
101
+ | 38 | Diamonds | 12 | King of Diamonds |
102
+ | 39 | Clubs | 0 | Ace of Clubs |
103
+ | ... | ... | ... | ... |
104
+ | 51 | Clubs | 12 | King of Clubs |
105
+
106
+ ### Deadwood Values
107
+
108
+ | Rank | Card | Deadwood Points |
109
+ |------|------|-----------------|
110
+ | 0 | Ace | 1 |
111
+ | 1 | Two | 2 |
112
+ | 2 | Three | 3 |
113
+ | 3 | Four | 4 |
114
+ | 4 | Five | 5 |
115
+ | 5 | Six | 6 |
116
+ | 6 | Seven | 7 |
117
+ | 7 | Eight | 8 |
118
+ | 8 | Nine | 9 |
119
+ | 9 | Ten | 10 |
120
+ | 10 | Jack | 10 |
121
+ | 11 | Queen | 10 |
122
+ | 12 | King | 10 |
123
+
124
+ ### Melds
125
+
126
+ - **Set (group):** 3 or 4 cards of the same rank, any suits. Example: 5 of Spades (4), 5 of Hearts (17), 5 of Clubs (43).
127
+ - **Run (sequence):** 3+ consecutive ranks in the same suit. Example: 3/4/5 of Diamonds (28, 29, 30). Aces are low only (A-2-3 is valid, Q-K-A is not).
128
 
129
+ ---
 
 
130
 
131
+ ## 3. Action Space
132
+
133
+ The model uses a **unified 16-action space** across all phases. The game phase determines which actions are legal.
134
+
135
+ | Action | Phase | Meaning |
136
+ |--------|-------|---------|
137
+ | 0 | Draw | Draw from stock pile (face-down) |
138
+ | 1 | Draw | Draw from discard pile (face-up top card) |
139
+ | 2 | Discard | Discard card at hand index 0 |
140
+ | 3 | Discard | Discard card at hand index 1 |
141
+ | 4 | Discard | Discard card at hand index 2 |
142
+ | 5 | Discard | Discard card at hand index 3 |
143
+ | 6 | Discard | Discard card at hand index 4 |
144
+ | 7 | Discard | Discard card at hand index 5 |
145
+ | 8 | Discard | Discard card at hand index 6 |
146
+ | 9 | Discard | Discard card at hand index 7 |
147
+ | 10 | Discard | Discard card at hand index 8 |
148
+ | 11 | Discard | Discard card at hand index 9 |
149
+ | 12 | Discard | Discard card at hand index 10 |
150
+ | 13 | Knock Decision | Continue playing (don't knock) |
151
+ | 14 | Knock Decision | Knock (requires deadwood <= 10) |
152
+ | 15 | Knock Decision | Gin (requires deadwood = 0) |
153
 
154
+ ---
 
 
 
 
155
 
156
+ ## 4. Game Flow
157
 
158
+ ```
159
+ Deal: 10 cards each, 1 upcard placed on discard pile
160
+ |
161
+ v
162
+ +-> [DRAW PHASE] -- Player draws 1 card (from stock or discard)
163
+ | |
164
+ | v
165
+ | [DISCARD PHASE] -- Player discards 1 card (now has 10 again)
166
+ | |
167
+ | v
168
+ | Deadwood <= 10? --No--> Switch to other player, go to DRAW
169
+ | |
170
+ | Yes
171
+ | v
172
+ | [KNOCK DECISION]
173
+ | |
174
+ | Continue? --Yes--> Switch to other player, go to DRAW
175
+ | |
176
+ | No (Knock or Gin)
177
+ | v
178
+ + [GAME OVER] -- Score the hand
179
+ ```
180
 
181
+ ### Terminal Conditions
182
 
183
+ - **Knock:** Player declares knock (deadwood 1-10). Score is computed with layoffs.
184
+ - **Gin:** Player declares gin (deadwood 0). Bonus awarded, no layoffs for defender.
185
+ - **Stock exhausted:** When 2 or fewer cards remain in the stock pile, the hand is a draw (no points awarded).
 
186
 
187
+ ---
188
 
189
+ ## 5. Observation Vector (342 dimensions)
190
+
191
+ The model receives a flat `float32[342]` vector. Every feature is documented below with its exact index range.
192
+
193
+ | Index Range | Dims | Feature | Value Range |
194
+ |-------------|------|---------|-------------|
195
+ | `0:52` | 52 | **Hand mask** — 1.0 if card is in the player's hand | binary {0, 1} |
196
+ | `52:104` | 52 | **Discard pile visible** — 1.0 if card has been discarded | binary {0, 1} |
197
+ | `104:156` | 52 | **Discard top card** — one-hot encoding of the top discard card | one-hot |
198
+ | `156` | 1 | **Deadwood** — player's current deadwood / 100 | [0, 1] |
199
+ | `157:161` | 4 | **Phase** — one-hot (draw / discard / knock_decision / game_over) | one-hot |
200
+ | `161` | 1 | **Hand size** — number of cards in hand / 11 | [0, 1] |
201
+ | `162` | 1 | **Discard pile size** — cards in discard / 52 | [0, 1] |
202
+ | `163` | 1 | **Stock remaining** — cards left in stock / 31 | [0, 1] |
203
+ | `164` | 1 | **Turn count** — turns elapsed / 35 | [0, 1] |
204
+ | `165` | 1 | **Can knock** — 1.0 if deadwood <= 10 | binary {0, 1} |
205
+ | `166:177` | 11 | **Discard deadwood** — deadwood after discarding each hand slot / 100 | [0, 1] |
206
+ | `177` | 1 | **Draw-from-discard deadwood** — best deadwood if drawing top discard / 100 | [0, 1] |
207
+ | `178:230` | 52 | **Opponent drew-from-discard** — 1.0 for each card opponent picked from discard | binary {0, 1} |
208
+ | `230:282` | 52 | **Opponent declined-discard** — 1.0 for each card opponent chose not to pick | binary {0, 1} |
209
+ | `282` | 1 | **Opponent estimated deadwood** — heuristic estimate from card counting | [0, 1] |
210
+ | `283:294` | 11 | **Discard safety** — safety score per hand slot (high = opponent unlikely to want it) | [0, 1] |
211
+ | `294` | 1 | **Undercut risk** — risk of being undercut if knocking now | [0, 1] |
212
+ | `295:306` | 11 | **Meld membership** — 1.0 if discarding that slot increases deadwood | binary {0, 1} |
213
+ | `306:317` | 11 | **Connector scores** — how many unseen cards could complete melds with each hand card | [0, 1] |
214
+ | `317` | 1 | **Fraction of hand in melds** — sum(meld_membership) / 10 | [0, 1] |
215
+ | `318` | 1 | **Cards from gin** — unmelded cards / 10 | [0, 1] |
216
+ | `319` | 1 | **Game urgency** — 1.0 - stock_remaining (increases as deck runs out) | [0, 1] |
217
+ | `320` | 1 | **Knock margin estimate** — (est_opp_dw - our_dw) / 50 | [-1, 1] |
218
+ | `321` | 1 | **Opponent draw activity** — opponent discard draws / 5 | [0, 1] |
219
+ | `322:342` | 20 | **Opponent type** — one-hot encoding of opponent type ID | one-hot |
220
+
221
+ **Notes:**
222
+ - Index `163`: stock is normalized by 31 (52 total - 21 dealt cards = 31 initial stock).
223
+ - Index `166:177`: invalid hand slots (index >= hand_size) are padded with 1.0.
224
+ - Index `177`: set to 1.0 if discard pile is empty.
225
+ - Index `322:342`: set to all zeros for unknown opponents or during human play.
226
 
227
+ ---
 
 
228
 
229
+ ## 6. Network Architecture
 
230
 
231
+ The R42 model uses a **SimBa (Simplified Balanced) residual architecture** with phase-decomposed value heads and an auxiliary opponent deadwood prediction head.
 
 
232
 
233
+ ### Layer-by-Layer Specification
234
+
235
+ ```
236
+ Input: float32[342]
237
+ |
238
+ v
239
+ Dense_0: Linear(342 -> 1024) + bias
240
+ | activation: ReLU
241
+ v
242
+ === Residual Block 1 ===
243
+ |-- save as `residual`
244
+ | LayerNorm_0: scale[1024], bias[1024], eps=1e-5
245
+ | Dense_1: Linear(1024 -> 1024) + bias
246
+ | activation: ReLU
247
+ | Dense_2: Linear(1024 -> 1024) + bias (NO activation)
248
+ |-- x = residual + x
249
+ v
250
+ === Residual Block 2 ===
251
+ |-- save as `residual`
252
+ | LayerNorm_1: scale[1024], bias[1024], eps=1e-5
253
+ | Dense_3: Linear(1024 -> 1024) + bias
254
+ | activation: ReLU
255
+ | Dense_4: Linear(1024 -> 1024) + bias (NO activation)
256
+ |-- x = residual + x
257
+ v
258
+ LayerNorm_2: scale[1024], bias[1024], eps=1e-5
259
+ |
260
+ v (shared features, used by all heads below)
261
+ |
262
+ +-- Dense_5: Linear(1024 -> 16) --> action logits
263
+ +-- value_draw: Linear(1024 -> 1) --> value estimate (draw phase)
264
+ +-- value_discard: Linear(1024 -> 1) --> value estimate (discard phase)
265
+ +-- value_knock: Linear(1024 -> 1) --> value estimate (knock decision phase)
266
+ +-- opp_dw_pred: Linear(1024 -> 1) --> auxiliary opponent deadwood prediction
267
  ```
268
 
269
+ ### Parameter Count
270
+
271
+ | Layer | Parameters |
272
+ |-------|-----------|
273
+ | Dense_0 (342x1024 + 1024) | 351,232 |
274
+ | Dense_1 (1024x1024 + 1024) | 1,049,600 |
275
+ | Dense_2 (1024x1024 + 1024) | 1,049,600 |
276
+ | Dense_3 (1024x1024 + 1024) | 1,049,600 |
277
+ | Dense_4 (1024x1024 + 1024) | 1,049,600 |
278
+ | Dense_5 (1024x16 + 16) | 16,400 |
279
+ | LayerNorm_0 (1024 + 1024) | 2,048 |
280
+ | LayerNorm_1 (1024 + 1024) | 2,048 |
281
+ | LayerNorm_2 (1024 + 1024) | 2,048 |
282
+ | value_draw (1024x1 + 1) | 1,025 |
283
+ | value_discard (1024x1 + 1) | 1,025 |
284
+ | value_knock (1024x1 + 1) | 1,025 |
285
+ | opp_dw_pred (1024x1 + 1) | 1,025 |
286
+ | **Total** | **4,576,276** |
287
 
288
+ ---
 
 
 
289
 
290
+ ## 7. Checkpoint Format
291
+
292
+ The `.pkl` file is a Python pickle containing a dict:
293
+
294
+ ```python
295
+ {
296
+ "params": {
297
+ "Dense_0": {"kernel": float32[342, 1024], "bias": float32[1024]},
298
+ "Dense_1": {"kernel": float32[1024, 1024], "bias": float32[1024]},
299
+ "Dense_2": {"kernel": float32[1024, 1024], "bias": float32[1024]},
300
+ "Dense_3": {"kernel": float32[1024, 1024], "bias": float32[1024]},
301
+ "Dense_4": {"kernel": float32[1024, 1024], "bias": float32[1024]},
302
+ "Dense_5": {"kernel": float32[1024, 16], "bias": float32[16]},
303
+ "LayerNorm_0": {"scale": float32[1024], "bias": float32[1024]},
304
+ "LayerNorm_1": {"scale": float32[1024], "bias": float32[1024]},
305
+ "LayerNorm_2": {"scale": float32[1024], "bias": float32[1024]},
306
+ "value_draw": {"kernel": float32[1024, 1], "bias": float32[1]},
307
+ "value_discard": {"kernel": float32[1024, 1], "bias": float32[1]},
308
+ "value_knock": {"kernel": float32[1024, 1], "bias": float32[1]},
309
+ "opp_dw_pred": {"kernel": float32[1024, 1], "bias": float32[1]},
310
+ }
311
+ }
312
  ```
313
 
314
+ **Important:** Flax Dense layers store the kernel as `[input_dim, output_dim]` (NOT transposed). The forward pass computes `output = input @ kernel + bias`.
315
 
316
+ ---
317
 
318
+ ## 8. Inference Step by Step
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
319
 
320
+ ```
321
+ 1. Construct the 342-dimensional observation vector from game state
322
+ (see Section 5 for the complete index map)
323
+
324
+ 2. Forward pass through the network:
325
+ x = relu(obs @ Dense_0.kernel + Dense_0.bias)
326
+ # Residual block 1
327
+ r = x
328
+ x = layer_norm(x, LN0.scale, LN0.bias)
329
+ x = relu(x @ Dense_1.kernel + Dense_1.bias)
330
+ x = x @ Dense_2.kernel + Dense_2.bias
331
+ x = r + x
332
+ # Residual block 2
333
+ r = x
334
+ x = layer_norm(x, LN1.scale, LN1.bias)
335
+ x = relu(x @ Dense_3.kernel + Dense_3.bias)
336
+ x = x @ Dense_4.kernel + Dense_4.bias
337
+ x = r + x
338
+ # Final norm
339
+ x = layer_norm(x, LN2.scale, LN2.bias)
340
+ # Actor head
341
+ logits = x @ Dense_5.kernel + Dense_5.bias # float32[16]
342
+
343
+ 3. Compute legal action mask based on current game phase (see Section 9)
344
+
345
+ 4. Mask illegal actions:
346
+ for i in range(16):
347
+ if not legal[i]:
348
+ logits[i] = -infinity
349
+
350
+ 5. Select action:
351
+ - Greedy: action = argmax(logits)
352
+ - Stochastic: action = sample from softmax(logits)
353
+
354
+ 6. Execute the action in your game engine
355
+ ```
356
 
357
+ **LayerNorm formula:**
358
+ ```
359
+ layer_norm(x, scale, bias, eps=1e-5):
360
+ mean = mean(x)
361
+ var = var(x)
362
+ x_norm = (x - mean) / sqrt(var + eps)
363
+ return x_norm * scale + bias
364
+ ```
365
 
366
+ ---
367
 
368
+ ## 9. Legal Action Rules
 
369
 
370
+ ### Draw Phase (player has 10 cards)
371
 
372
+ | Action | Legal When |
373
+ |--------|-----------|
374
+ | 0 (draw stock) | Stock has > 2 cards remaining |
375
+ | 1 (draw discard) | Discard pile is not empty |
 
 
 
376
 
377
+ Both are typically legal. If stock is nearly exhausted, only discard draw may be available.
378
 
379
+ ### Discard Phase (player has 11 cards)
380
 
381
+ | Action | Legal When |
382
+ |--------|-----------|
383
+ | 2 through 12 | Hand index (action - 2) < hand_size **AND** the card at that index is not the card just drawn from the discard pile |
 
 
384
 
385
+ The "re-discard ban" prevents immediately returning a card picked up from the discard pile.
386
 
387
+ ### Knock Decision Phase (player has 10 cards, deadwood <= 10)
388
 
389
+ | Action | Legal When |
390
+ |--------|-----------|
391
+ | 13 (continue) | Always legal |
392
+ | 14 (knock) | Deadwood is 1 through 10 |
393
+ | 15 (gin) | Deadwood is exactly 0 |
394
+
395
+ This phase only occurs when deadwood <= 10 after discarding. If deadwood > 10, the game skips directly to the other player's draw phase.
396
+
397
+ ---
398
+
399
+ ## 10. Scoring Rules
400
+
401
+ ### Normal Knock (deadwood 1-10)
402
+
403
+ 1. Knocker reveals hand and forms melds.
404
+ 2. Defender reveals hand, forms their own melds, then **lays off** unmelded cards onto the knocker's melds (extending runs or completing sets).
405
+ 3. Compare deadwood:
406
+ - **Knocker wins:** knocker_deadwood < defender_deadwood_after_layoffs. Knocker scores the difference.
407
+ - **Undercut:** defender_deadwood_after_layoffs <= knocker_deadwood. Defender scores the difference **+ 25 bonus**.
408
+
409
+ ### Gin (deadwood = 0)
410
+
411
+ - Knocker scores defender's deadwood **+ 25 bonus**.
412
+ - Defender gets **no layoffs** against a gin hand.
413
+
414
+ ### Stock Exhausted
415
+
416
+ - When 2 or fewer cards remain in the stock pile, the hand ends in a **draw**.
417
+ - Neither player scores any points.
418
+
419
+ ### Deadwood Calculation
420
+
421
+ Sum the deadwood values (see Section 2) of all cards NOT part of any meld. The optimal meld arrangement is used (minimizing deadwood).
422
+
423
+ ---
424
+
425
+ ## 11. PyTorch Reference Implementation
426
+
427
+ A complete, self-contained PyTorch implementation for loading and running the model. No JAX or Flax dependency required.
428
+
429
+ ```python
430
+ import pickle
431
+ import numpy as np
432
+ import torch
433
+ import torch.nn as nn
434
+
435
+
436
+ class GinRummyModel(nn.Module):
437
+ """R42 SimBa architecture with phase-decomposed value heads."""
438
+
439
+ def __init__(self):
440
+ super().__init__()
441
+ # Input projection
442
+ self.dense_0 = nn.Linear(342, 1024)
443
+ # Residual block 1
444
+ self.ln_0 = nn.LayerNorm(1024, eps=1e-5)
445
+ self.dense_1 = nn.Linear(1024, 1024)
446
+ self.dense_2 = nn.Linear(1024, 1024)
447
+ # Residual block 2
448
+ self.ln_1 = nn.LayerNorm(1024, eps=1e-5)
449
+ self.dense_3 = nn.Linear(1024, 1024)
450
+ self.dense_4 = nn.Linear(1024, 1024)
451
+ # Final norm
452
+ self.ln_2 = nn.LayerNorm(1024, eps=1e-5)
453
+ # Output heads
454
+ self.actor = nn.Linear(1024, 16)
455
+ self.value_draw = nn.Linear(1024, 1)
456
+ self.value_discard = nn.Linear(1024, 1)
457
+ self.value_knock = nn.Linear(1024, 1)
458
+ self.opp_dw_pred = nn.Linear(1024, 1)
459
+
460
+ def forward(self, obs):
461
+ """
462
+ Args:
463
+ obs: float32 tensor of shape (..., 342)
464
+ Returns:
465
+ logits: float32 (..., 16) — raw action logits (mask before use)
466
+ value_draw: float32 (..., 1) — value estimate for draw phase
467
+ value_discard: float32 (..., 1) — value estimate for discard phase
468
+ value_knock: float32 (..., 1) — value estimate for knock phase
469
+ opp_dw_pred: float32 (..., 1) — predicted opponent deadwood
470
+ """
471
+ # Input projection
472
+ x = torch.relu(self.dense_0(obs))
473
+
474
+ # Residual block 1
475
+ r = x
476
+ x = self.ln_0(x)
477
+ x = torch.relu(self.dense_1(x))
478
+ x = self.dense_2(x)
479
+ x = r + x
480
+
481
+ # Residual block 2
482
+ r = x
483
+ x = self.ln_1(x)
484
+ x = torch.relu(self.dense_3(x))
485
+ x = self.dense_4(x)
486
+ x = r + x
487
+
488
+ # Final norm + heads
489
+ x = self.ln_2(x)
490
+ logits = self.actor(x)
491
+ return (
492
+ logits,
493
+ self.value_draw(x),
494
+ self.value_discard(x),
495
+ self.value_knock(x),
496
+ self.opp_dw_pred(x),
497
+ )
498
+
499
+
500
+ def load_from_pkl(pkl_path: str) -> GinRummyModel:
501
+ """Load Flax weights from a .pkl checkpoint into PyTorch.
502
+
503
+ Handles the Flax [in, out] -> PyTorch [out, in] kernel transposition.
504
+ """
505
+ with open(pkl_path, "rb") as f:
506
+ params = pickle.load(f)
507
+ p = params.get("params", params)
508
+
509
+ model = GinRummyModel()
510
+
511
+ def set_linear(module, name):
512
+ # Flax kernel is [in_features, out_features]
513
+ # PyTorch weight is [out_features, in_features]
514
+ module.weight.data = torch.from_numpy(np.array(p[name]["kernel"]).T)
515
+ module.bias.data = torch.from_numpy(np.array(p[name]["bias"]).ravel())
516
+
517
+ def set_ln(module, name):
518
+ module.weight.data = torch.from_numpy(np.array(p[name]["scale"]))
519
+ module.bias.data = torch.from_numpy(np.array(p[name]["bias"]))
520
+
521
+ set_linear(model.dense_0, "Dense_0")
522
+ set_linear(model.dense_1, "Dense_1")
523
+ set_linear(model.dense_2, "Dense_2")
524
+ set_linear(model.dense_3, "Dense_3")
525
+ set_linear(model.dense_4, "Dense_4")
526
+ set_linear(model.actor, "Dense_5")
527
+ set_linear(model.value_draw, "value_draw")
528
+ set_linear(model.value_discard, "value_discard")
529
+ set_linear(model.value_knock, "value_knock")
530
+ set_linear(model.opp_dw_pred, "opp_dw_pred")
531
+ set_ln(model.ln_0, "LayerNorm_0")
532
+ set_ln(model.ln_1, "LayerNorm_1")
533
+ set_ln(model.ln_2, "LayerNorm_2")
534
+
535
+ model.eval()
536
+ return model
537
+
538
+
539
+ # --- Usage example ---
540
+ if __name__ == "__main__":
541
+ from huggingface_hub import hf_hub_download
542
+
543
+ path = hf_hub_download(
544
+ repo_id="GoodStartLabs/gin-rummy-mdp",
545
+ filename="checkpoints/r42/stage1_final.pkl",
546
+ repo_type="model",
547
+ )
548
+ model = load_from_pkl(path)
549
+
550
+ # Create a dummy observation (all zeros)
551
+ obs = torch.zeros(342)
552
+ with torch.no_grad():
553
+ logits, v_draw, v_discard, v_knock, opp_dw = model(obs)
554
+ print(f"Logits shape: {logits.shape}") # torch.Size([16])
555
+ print(f"Top action: {logits.argmax().item()}")
556
  ```
557
+
558
+ ---
559
+
560
+ ## 12. Available Checkpoints
561
+
562
+ ### R42 (latest, recommended)
563
+
564
+ SimBa + auxiliary heads, trained with mixed opponent curriculum. **Use `stage1_final.pkl` unless you need an intermediate snapshot.**
565
+
566
+ | File | Steps | Notes |
567
+ |------|-------|-------|
568
+ | `checkpoints/r42/stage1_final.pkl` | 1.4B | Final model (recommended) |
569
+ | `checkpoints/r42/stage1_1400M.pkl` | 1.4B | Same as final |
570
+ | `checkpoints/r42/stage1_1300M.pkl` | 1.3B | |
571
+ | `checkpoints/r42/stage1_1200M.pkl` | 1.2B | |
572
+ | `checkpoints/r42/stage1_1100M.pkl` | 1.1B | |
573
+ | `checkpoints/r42/stage1_1000M.pkl` | 1.0B | |
574
+ | `checkpoints/r42/stage1_900M.pkl` | 900M | |
575
+ | `checkpoints/r42/stage1_800M.pkl` | 800M | |
576
+ | `checkpoints/r42/stage1_700M.pkl` | 700M | |
577
+ | `checkpoints/r42/stage1_600M.pkl` | 600M | |
578
+ | `checkpoints/r42/stage1_500M.pkl` | 500M | |
579
+ | `checkpoints/r42/stage1_400M.pkl` | 400M | |
580
+ | `checkpoints/r42/stage1_300M.pkl` | 300M | |
581
+ | `checkpoints/r42/stage1_200M.pkl` | 200M | |
582
+ | `checkpoints/r42/stage1_100M.pkl` | 100M | |
583
+ | `checkpoints/r42/run42_config.toml` | — | Training configuration |
584
+
585
+ ### Older Runs
586
+
587
+ | Path | Architecture | Notes |
588
+ |------|-------------|-------|
589
+ | `checkpoints/r40/` | SimBa | 700M steps, predecessor to R42 |
590
+ | `checkpoints/r39/` | SimBa | 900M steps |
591
+ | `checkpoints/r37_*.pkl`, `r38_*.pkl` | SimBa | Earlier experiments |
592
+ | `checkpoints/r33/`, `r34/`, `r35/`, `r36/` | SimBa | Older runs with different obs dims |
593
+ | `checkpoints/r24_*`, `r25_*`, `r26_*` | MLP (no residual) | Early MLP architecture |
594
+
595
+ ### Other Files
596
+
597
+ | Path | Description |
598
+ |------|-------------|
599
+ | `human_games/*.json` | Recorded games from human play sessions |
600
+ | `configs/run26_config.toml` | Example training config |
601
+
602
+ > **Compatibility note:** Checkpoints from R39 and later use the same 342-dim observation and SimBa architecture as R42. Earlier runs (R24-R38) use different observation sizes or architectures and are not interchangeable.
603
+
604
+ ---
605
+
606
+ ## 13. Training Details
607
+
608
+ | Parameter | Value |
609
+ |-----------|-------|
610
+ | Algorithm | PPO (Proximal Policy Optimization) |
611
+ | Total environment steps | 1.4 billion |
612
+ | Architecture | SimBa (residual + LayerNorm), 4.58M params |
613
+ | Learning rate | 2.5e-4 (annealed to 0) |
614
+ | Environments (parallel) | 4,096 |
615
+ | Steps per rollout | 128 |
616
+ | Minibatches | 4 |
617
+ | Update epochs | 4 |
618
+ | Discount (gamma) | 1.0 |
619
+ | GAE lambda | 0.98 |
620
+ | Clip epsilon | 0.2 |
621
+ | Entropy coefficient | 0.025 |
622
+ | Value function coefficient | 0.75 |
623
+ | Max gradient norm | 0.5 |
624
+ | Reward | Categorical terminal (+1 gin, +0.25..+0.70 knock win, -0.15..-0.85 losses) |
625
+ | Auxiliary loss | Opponent deadwood prediction (coefficient 0.1) |
626
+ | P0/P1 alternation | Agent plays as both first and second player |
627
+
628
+ ### Opponent Curriculum
629
+
630
+ The agent trains against a mix of opponents, sampled per-episode:
631
+
632
+ | Opponent | Probability | Description |
633
+ |----------|------------|-------------|
634
+ | Heuristic | 30% | Rule-based player with meld tracking |
635
+ | Aggressive Knock | 15% | Knocks as soon as legally possible |
636
+ | Meld Builder | 10% | Prioritizes forming melds over low deadwood |
637
+ | Early Knock | 10% | Targets fast knocks with moderate deadwood |
638
+ | Defensive | 10% | Conservative, safety-focused play |
639
+ | Superhuman Lv5 | 10% | Strong opponent with deep card tracking |
640
+ | Frozen Self | 5% | Past checkpoint of the learning agent |
641
+ | Superhuman Lv4 | 5% | Moderate-strength superhuman |
642
+ | Superhuman Lv7 | 5% | Advanced opponent with aggressive timing |
643
+
644
+ ---
645
+
646
+ ## License
647
+
648
+ Apache 2.0