iTheresaApocalypse commited on
Commit
48a9029
·
verified ·
1 Parent(s): b3a43a6

English model card aligned with gamescaling.github.io

Browse files
Files changed (1) hide show
  1. README.md +199 -209
README.md CHANGED
@@ -1,70 +1,104 @@
1
  ---
 
 
2
  tags:
3
  - gamescaling
 
 
 
4
  - qwen3_5
5
  ---
6
 
7
- # GameScaling 推理模板
8
 
9
- 用 GameScaling checkpoint 玩游戏、或把它当 baseline 所需的全部**推理侧**代码:系统提示词
10
- 规范、多轮消息构造、vLLM 请求参数、动作反解,以及论文三个 benchmark(ViZDoom、
11
- OmniGameArena、GameWorld)的接入。不含任何训练代码。
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
12
 
13
  ```
14
- gamescaling/ 核心包,只依赖 requests + Pillow
15
- prompt.py 系统提示词(标准 build_system_prompt + GameWorld 版式)
16
- policy.py 多轮客户端:历史窗口、图像编码、请求体
17
- action.py 动作格式与反解(200 ms chunk -> 6 x 33 ms 键鼠)
18
- keymaps.py 训练词表 <-> 浏览器/Playwright 键名
19
  prompts/
20
- system_prompt_template.txt 标准系统提示词骨架,填三个占位符即可用于任意游戏
21
- example_doom_battle1.txt 填好的示例
22
- models/ 8 个 Qwen3.5 checkpoint,各自带 chat_template.jinja
23
- scripts/serve_vllm.sh 起 OpenAI 兼容服务
24
- tests/test_core.py 离线单测(不需要 GPU)
 
 
 
 
25
  ```
26
 
27
- 默认推理:系统提示词用 `build_system_prompt` 标准格式,每轮 user 是 `current game screen` 加一帧
28
- 游戏画面,assistant 预填 `<think>`。checkpoint 目录:
29
 
30
- | 目录 | 规模 |
31
- |---|---|
32
- | `models/0p8b_s1_1600h_gs6736` | 0.8B,stage 1 |
33
- | `models/0p8b_s3_1600h_gs6760` | 0.8B,stage 3 |
34
- | `models/2b_s1_1600h_gs6736` | 2B,stage 1 |
35
- | `models/2b_s3_1600h_gs6760` | 2B,stage 3 |
36
- | `models/9b_s1_1600h_gs6736` | 9B,stage 1 |
37
- | `models/9b_s3_1600h_gs6760` | 9B,stage 3 |
38
- | `models/27b_s1_1600h_gs6995` | 27B,stage 1 |
39
- | `models/27b_s3_1600h_gs7020` | 27B,stage 3 |
40
 
41
- `27b_s3` 的权重按约 30GB 切成多片,并带 `model.safetensors.index.json`。请用该 checkpoint
42
- 目录里的 `chat_template.jinja`,不要换成原版 Qwen 模板。
 
 
 
 
 
43
 
44
- ## 1. 部署模型
45
 
46
  ```bash
47
- pip install "vllm>=0.17" # checkpoint 是 Qwen3.5 架构;测试版本 0.17.0
48
- bash scripts/serve_vllm.sh /path/to/checkpoint 8000 # 单卡即可跑 0.8B-9B
 
49
  ```
50
 
51
- 脚本以 `--served-model-name gamescaling` 起服务,本仓库所有客户端默认连
52
- `http://127.0.0.1:8000/v1`、模型名 `gamescaling`。请使用 checkpoint 目录里自带的
53
- `chat_template.jinja`(vLLM 默认就会用),**不要**换成原版 Qwen 模板:自带模板会把没有
54
- 思维链的历史轮渲染成 `<think>\n\n</think>\n\n<|action_start|>...`,与训练样本逐 token 一致。
55
 
56
- ## 2. 系统提示词骨架
57
 
58
- `prompts/system_prompt_template.txt` 是标准系统提示词,只有三处要按游戏填写,其余部分**原样保留**
59
- (checkpoint 依赖 Output Format 那一段的精确文本来决定输出标签):
 
60
 
61
- | 占位符 | 填什么 | 示例 |
62
  |---|---|---|
63
- | `{GAME_NAME}` | 游戏名,出现两次(开场白和键位标题) | `Doom Battle-1` |
64
- | `{GAME_RULES}` | 游戏是什么、目标、怎么计分,一段英文 | `Kill as many monsters as possible; collect health and ammo.` |
65
- | `{KEY_BINDINGS}` | 每行一个 `- <键> -> <作用>`,键名必须用训练词表(见 §5) | 见下 |
66
 
67
- `{KEY_BINDINGS}` 的写法:
68
 
69
  ```text
70
  - W -> Move forward
@@ -75,11 +109,12 @@ bash scripts/serve_vllm.sh /path/to/checkpoint 8000 # 单卡即可跑 0.8B-
75
  - Mouse X Y -> aim / rotate the camera (X>0 right, Y>0 down)
76
  ```
77
 
78
- 最后一行只在鼠标控制视角的游戏里加,并且必须原样照抄。键名规则:字母大写(`W`),数字键写
79
- `zero`..`nine`,F1-F12 写 `One`..`Twelve`,鼠标键写 `LB` / `RB` / `MB`,方向键写
80
- `Up` / `Down` / `Left` / `Right`。填好的完整示例见 `prompts/example_doom_battle1.txt`。
 
81
 
82
- 直接用文本文件:
83
 
84
  ```python
85
  tpl = open("prompts/system_prompt_template.txt").read().rstrip("\n")
@@ -89,218 +124,173 @@ system = (tpl.replace("{GAME_NAME}", "Doom Battle-1")
89
  "- Mouse X Y -> aim / rotate the camera (X>0 right, Y>0 down)"))
90
  ```
91
 
92
- 和下面 `build_system_prompt(...)` 生成的结果逐字节相同(`tests/test_core.py` 会检查)。
 
93
 
94
- ## 3. 最小用法
95
 
96
  ```python
97
  from PIL import Image
98
  from gamescaling import GameScalingPolicy, PolicyConfig, build_system_prompt
99
 
100
- # 任意游戏:标准系统提示词(游戏名 + 规则 + 训练词表键位 + think/action 输出格式)
101
  system = build_system_prompt(
102
  "Doom Battle-1",
103
  rules="Kill as many monsters as possible; collect health and ammo.",
104
  key_bindings={"W": "Move forward", "S": "Move backward", "A": "Strafe left",
105
  "D": "Strafe right", "LB": "Fire"},
106
- mouse_aim=True, # 鼠标控视角的游戏
107
- ) # reasoning 默认 True,与默认的 <think> 预填一致
108
- policy = GameScalingPolicy(system, PolicyConfig()) # 预填 <think>,画面按原尺寸送入
109
- policy.reset() # 每局开始调用
110
- decision = policy.act(Image.open("frame.png")) # 一次调用 = 一个 200 ms 决策
111
  a = decision.action
112
- a.mouse # (X, Y, Z):本 chunk 鼠标位移(千分比屏幕)+ 滚轮
113
- a.steps # 6 组,每组是这 33 ms 内按住的键
114
- a.parsed # False = 无法解析,按 no-op 执行
115
  ```
116
 
117
- 之后把 `a` 交给你的执行器:6 个 33 ms 子步,每步按住该组的键(相邻两组都有的键保持按住,
118
- 不重新按下);鼠标位移 `X/1000*屏宽`、`Y/1000*屏高` 在 200 ms 内分摊或一次性下发均可,
119
- 三个集成各自的做法见下文。
 
120
 
121
- 默认口径不绑定某个 benchmark:任意游戏画面 + `build_system_prompt` 标准格式 + 预填 `<think>`。
122
- `TRAINING_SYSTEM_PROMPT` 是预训练的通用提示词,只有 action-only 输出格式,和 `<think>` 预填
123
- 不匹配,只用于消融。复现论文三张表时再改用
124
- `PolicyConfig.preset("omnigamearena" | "vizdoom" | "gameworld")`:前两档把画面缩到 854×480;
125
- `gameworld` 不预填 `<think>`,提示词要用 `build_gameworld_system_prompt(..., reasoning=False)`。
126
 
127
- ## 4. 系统提示词规范
128
 
129
- 所有 benchmark 用同一骨架,顺序固定(`gamescaling/prompt.py`):
130
 
131
- 1. **开场白**:游戏专家 + 游戏名 + "规划接下来 200ms、6 步、每步 33ms" 的契约;
132
- 2. `**Game Rules**`:游戏是什么、怎么计分;
133
- 3. **操作说明**:本游戏用到的键,**必须写成训练词表**(见 §5);
134
- 4. `**Task Instruction**`:仅 GameWorld(一个游戏有多个任务);
135
- 5. `**Output Format**`:模型据此决定输出格式;
136
- 6. `**Explanation**` + `**Key Naming Rules**`。
 
137
 
138
- 两种版式只在第 3 节和分隔符上不同:
139
 
140
- - `build_system_prompt(...)`:键位列表版(`**Common Keys in <game>**` + `- W -> Move
141
- forward`),ViZDoom、OmniGameArena 和**接入新游戏**都用它;鼠标控制视角的游戏加
142
- `mouse_aim=True`。
143
- - `build_gameworld_system_prompt(...)`:自由段落版(`**Role and Controls**`),用于
144
- GameWorld 那种自带文字说明的游戏;浏览器键名用 `translate_controls` 转成训练词表
145
- (`ArrowLeft` -> `Left`,`a/d` -> `A/D`,`1-9:` -> `one-nine:`)。
 
146
 
147
- `Output Format` 有两种,**必须与客户端是否 prefill `<think>` 保持一致**:
148
 
149
- | `reasoning` | 客户端 | 模型输出 |
150
  |---|---|---|
151
- | `True` | `think_prefill=True`,assistant 以 `<think>` 开头续写 | 自己决定想不想、想多久,然后闭合 `</think>` 输出动作 |
152
- | `False` | 不 prefill | 直接输出 `<|action_start|>...<|action_end|>` |
153
-
154
- 提示词请原样拷贝,不要改写:checkpoint 是按这段 Output Format 的精确文本来决定输出标签的。
155
- 每轮 user 消息的���本固定为 `current game screen`,后接当前帧。
156
 
157
- 多轮消息(`policy.py`):
 
158
 
159
  ```
160
- system 系统提示词
161
- user "current game screen" + 第 t-k 帧 ┐ 最近 history_len 轮
162
- assistant 第 t-k 轮执行的动作(默认不带思维链) ┘
163
  ...
164
- user "current game screen" + 当前帧
165
- assistant "<think>"(仅 think_prefill 时,continue_final_message=True)
166
  ```
167
 
168
- 历史轮只放动作(`history_think=0`,论文口径)。解析失败的轮在历史里存成显式 no-op
169
- `<|action_start|>0 0 0 ; ; ; ; ; ; <|action_end|>`,而不是模型的原始坏输出。
 
170
 
171
- ## 5. 动作格式与反解
 
 
 
172
 
173
  ```
174
  <|action_start|>X Y Z ; g1 ; g2 ; g3 ; g4 ; g5 ; g6<|action_end|>
175
  ```
176
 
177
- - `X Y`:整个 200 ms 的鼠标位移,千分比屏幕(`X = Σdx / 屏宽 × 1000`),截断到 ±1000;
178
- X>0 向右,Y>0 向下。
179
- - `Z`:滚轮格数,截断到 ±5,Z>0 向上。
180
- - `g1..g6`:六个 33 ms 子步,每组列出这段时间内**按住**的键,最多 4 个;空组 = 全部松开,
181
- 但 `;` 要保留。
182
- - `<|action_start|>` / `<|action_end|>` 是 special token,请求里必须带
183
- `skip_special_tokens: false`(`build_request` 已设置),否则服务端会把标签吃掉
184
- (`parse_action` 对裸 `X Y Z ; ...` 也能兜底)。
185
- - 动作**只从最后一个 `</think>` 之后**读取:会思考的模型常在思维链里复述格式,从全文搜会执行
186
- 那段引用。`<think>` 没闭合 = 生成被截断,按 no-op 处理(`Action.truncated=True`)。
187
-
188
- 训练词表(74 个,大小写敏感,`gamescaling.action.KEY_VOCAB`):
189
-
190
- | 类别 | token |
 
191
  |---|---|
192
- | 字母 | `A`..`Z` |
193
- | 数字行 | `zero` `one` .. `nine` |
194
- | F1-F12 | `One` `Two` .. `Twelve`(**首字母大写** = F 键) |
195
- | 鼠标键 | `LB` `RB` `MB` |
196
- | 方向 | `Up` `Down` `Left` `Right` |
197
- | 其他 | `Shift` `Ctrl` `Alt` `Tab` `Caps` `Esc` `Space` `Enter` `Back` `Delete` `Insert` `Home` `End` `Pause` `Equal` `Minus` `Period` `Slash` `Quote` |
198
-
199
- 不在词表里的 token 被丢弃(记在 `Action.dropped`)。`keymaps.TO_PLAYWRIGHT` 给出到浏览器
200
- 键名的映射。
201
-
202
- ## 6. 三个 benchmark
203
 
204
- 三者都是**暂停时钟(pdq)**:模型思考时游戏冻结,只在执行 chunk 时前进。论文所有主表都是这个
205
- 口径;不同时钟之间的分数不可比。
206
 
207
- ### 6.1 ViZDoom(自包含)
208
 
209
- ```bash
210
- pip install -r requirements.txt vizdoom==1.2.4
211
- python -m integrations.vizdoom.run --maps battle1 battle2 defend_the_center defend_the_line \
212
- --episodes 10 --out runs/vizdoom
213
- ```
214
-
215
- 四张图、每图 10 局、每局最多 350 个 chunk(= 2100 tic 的地图超时)。一个 33 ms 子步 = 一个
216
- tic(35 tic/s),每个子步单独 `make_action(vec, 1)`。鼠标按 Doom 的投影换算成转向角
217
- (`atan((X/1000) / focal)`,即把屏幕上偏移 X 处的目标转到准星;focal 为实测值),转角在
218
- 6 个 tic 上均分;HUD 开。计分:battle 图数击杀,defend 图击杀减死亡;`overall` 是四图均值的均值。
219
- 场景文件来源见 `integrations/vizdoom/scenarios/PROVENANCE.md`。
220
-
221
- ### 6.2 OmniGameArena
222
 
223
- 在 [OmniGameArena](https://github.com/mxlin043/OmniGameArena)(测于 `f3a4057`)里注册
224
- agent kind `gamescaling`:
225
-
226
- ```bash
227
- bash integrations/omnigamearena/install.sh /path/to/OmniGameArena
228
- export PYTHONPATH=$PWD:$PWD/integrations/omnigamearena:$PYTHONPATH
229
- cd /path/to/OmniGameArena
230
- # 按 OmniGameArena README 启动 UE5(默认端口 12345)
231
- python scripts/run_benchmark.py --config configs/gamescaling/monster_shoot.yaml
232
- ```
233
 
234
- 补丁只做两件事:在 `factory.py` 注册 `gamescaling`;把 `runner.py` 对 `tkinter`
235
- 的导入改成只在 `--live` 时加载(无显示器的服务器上原版会直接 ImportError)。
 
 
 
236
 
237
- 七个 solo 游戏配置在 `configs/gamescaling/`:pdq、每局 300 步(`scene_escape`、
238
- `cue_chase` 为 500)、3 局、温度 1.0、自由思考、历史 5 帧只留动作、画面缩到 854x480。
239
- 论文的计分约定:`solo_craft` 分数为 `min(V/50, 1)`(V 为 UE 返回的原始点数),其余游戏直接用
240
- benchmark 分数;OVERALL 是七个游戏的均值。
241
 
242
- 两个实际部署时的注意点:
 
 
243
 
244
- - **一张 GPU 只跑一个 UE5 实例。** 动作按墙钟 6×33 ms 执行,多个实例抢一张卡的渲染时,按住
245
- 前进 0.2 s 几乎不动,分数会大幅下降且不报错。
246
- - `solo_craft` 的后续 episode 请换一个新的 UE5 进程;复用实例时第 2、3 局会立即结束并复报
247
- 第 1 局分数。
248
 
249
- ### 6.3 GameWorld
 
 
 
 
 
 
 
 
250
 
251
- 在 [GameWorld](https://github.com/gameworld-project/gameworld)(测于 `e236a39`)里注册模型
252
- id `gamescaling`:
253
 
254
  ```bash
255
- bash integrations/gameworld/install.sh /path/to/gameworld
256
- export PYTHONPATH=$PWD:$PYTHONPATH
257
- cd /path/to/gameworld
258
- # 上游仓库只带 2 个游戏;完整游戏库的游戏在该库的 benchmark/ 子目录下
259
- git clone --depth 1 https://github.com/gameworld-dev/gameworld-games.git /tmp/gameworld-games
260
- rm -rf games/benchmark && mv /tmp/gameworld-games/benchmark games/benchmark
261
- pip install -r requirements.txt && python -m playwright install --with-deps chromium
262
- python main.py --config 01_2048+01_01+gamescaling --headless
263
- python run_suite.py --suite <your_suite.yaml> # 批量:suite 里 model 写 gamescaling
264
  ```
265
 
266
- 补丁新增一种 `key_timeline` 动作:六个 33 ms 槽位的按住键(跨窗口保持),加一次鼠标移动和
267
- 滚轮。鼠标是千分比屏幕空间里的虚拟光标,从中心出发、每个 chunk 移动 (X, Y) 并截断到屏幕内。
268
- 系统提示词走 `gamescaling_template.j2`(自由段落版),控制说明经 `translate_controls`
269
- 转成训练词表。
270
 
271
- 口径(`catalog/models/gamescaling.yaml`):历史 5 轮(截图 + 动作)、截图缩放到高 480
272
- (PNG、Lanczos)、不 prefill `<think>`、`max_tokens=4096`、温度 1.0、停在
273
- `<|action_end|>`、宽松解析。`wasd_remap`:只在某游戏 WASD 非法而方向键合法时,把模型输出
274
- 的 WASD 映射成方向键(模型主要在 PC 游戏上训练,习惯用 WASD 移动)。环境变量
275
- `GAMESCALING_ENDPOINT` 可覆盖服务地址,`GAMESCALING_THINK=1` 可打开 think prefill(非论文口径)。
276
 
277
- ## 7. 接入新游戏
278
-
279
- 1. 填 `prompts/system_prompt_template.txt`(§2),或用 `build_system_prompt` 写提示词:游戏名、规则、`key_bindings` 用训练词表;鼠标控视角
280
- 时加 `mouse_aim=True`;`reasoning` 与 `think_prefill` 一致。
281
- 2. 每 200 ms(或暂停时钟下每次决策)截一帧,调用 `policy.act(frame)`。
282
- 3. 按 §5 执行 `Action`:6 个 33 ms 子步的按住键 + 鼠标位移 + 滚轮。
283
- 4. 新游戏沿用默认客户端(原尺寸画面、预填 `<think>`、`build_system_prompt` 标准格式)。
284
- 要对齐论文某一张表时,再改用
285
- `PolicyConfig.preset("omnigamearena" | "vizdoom" | "gameworld")`。
286
-
287
- ## 8. 与论文评测代码的差异
288
-
289
- 发布代码与论文内部评测代码逐项对拍过:三个 benchmark 的全部系统提示词逐字节相同
290
- (ViZDoom 4 图、OmniGameArena 12 游戏 44 个 prompt、GameWorld 170 个任务),历史 trace
291
- 中各 4000 条左右的模型回复反解结果完全相同。以下几处有意改动只在边缘情况下有区别:
292
-
293
- - OmniGameArena 的动作只从最后一个 `</think>` 之后读取(内部代码搜全文,思维链里复述格式时
294
- 会误执行那段引用)。
295
- - GameWorld 中解析失败的轮在历史里存为显式 no-op(内部代码存原始输出)。
296
- - GameWorld 槽位时长用 200/6 = 33.33 ms(内部为 33.0 ms)。
297
- - ViZDoom 历史轮保存动作原文(内部去掉首尾空白),episode 种子用 `crc32`(内部用 Python
298
- `hash`,跨进程不稳定)。
299
- - OmniGameArena 的 `map_wait` 用上游默认 3 s。内部评测等 60 s,但前 3 s 之后世界是冻结的,
300
- 所以两者进入第一步时的游戏时间一致。
301
-
302
- ## 9. 测试
303
-
304
- ```bash
305
- python tests/test_core.py # 或 python -m pytest tests/;离线:提示词、请求体、动作反解,不需要 GPU / 服务
306
  ```
 
1
  ---
2
+ library_name: transformers
3
+ pipeline_tag: image-text-to-text
4
  tags:
5
  - gamescaling
6
+ - game-agent
7
+ - embodied-agent
8
+ - keyboard-mouse
9
  - qwen3_5
10
  ---
11
 
12
+ # Scaling in Games: Continued Pre-Training for Embodied Agents in Diverse Virtual Worlds
13
 
14
+ [Project page](https://gamescaling.github.io) · [Citation](#citation)
15
+
16
+ GameScaling models are Qwen3.5 vision-language models with continued pre-training (CPT) on
17
+ keyboard-and-mouse gameplay. Each call reads the current game frame (plus a short history of past
18
+ frames and actions) and outputs the next **200 ms of keyboard and mouse actions**: 6 steps of
19
+ 33 ms each, plus one mouse movement and scroll for the whole chunk. The model can optionally
20
+ think first.
21
+
22
+ This repository releases eight checkpoints (four model sizes × two data mixtures, all trained on
23
+ 1,600 h of gameplay), the inference client, and the standard system-prompt template.
24
+
25
+ ## Checkpoints
26
+
27
+ | Model | Folder | Mixture |
28
+ |---|---|---|
29
+ | GameScaling-0.8B-S1 | `models/0p8b_s1_1600h_gs6736` | S1 |
30
+ | GameScaling-0.8B-S3 | `models/0p8b_s3_1600h_gs6760` | S3 |
31
+ | GameScaling-2B-S1 | `models/2b_s1_1600h_gs6736` | S1 |
32
+ | GameScaling-2B-S3 | `models/2b_s3_1600h_gs6760` | S3 |
33
+ | GameScaling-9B-S1 | `models/9b_s1_1600h_gs6736` | S1 |
34
+ | GameScaling-9B-S3 | `models/9b_s3_1600h_gs6760` | S3 |
35
+ | GameScaling-27B-S1 | `models/27b_s1_1600h_gs6995` | S1 |
36
+ | GameScaling-27B-S3 | `models/27b_s3_1600h_gs7020` | S3 |
37
+
38
+ - **S1**: trained on Minecraft gameplay only.
39
+ - **S3**: a balanced mixture. Minecraft is cut to 27%, and the rest comes from other games in an
40
+ 80-game, mostly 3D corpus.
41
+ - Folder names: `<size>_<mixture>_<hours of gameplay>_gs<training steps>`.
42
+ - All weights are bfloat16. Every folder has its own `chat_template.jinja`; keep it (see below).
43
+
44
+ In the paper, more data helps most in worlds that are in the training corpus. More diversity (S3)
45
+ helps most in unseen 3D worlds whose actions resemble training. See the
46
+ [project page](https://gamescaling.github.io) for the full results.
47
+
48
+ ## Repository layout
49
 
50
  ```
51
+ models/ eight checkpoints (Qwen3.5 architecture)
 
 
 
 
52
  prompts/
53
+ system_prompt_template.txt standard system prompt; fill in three placeholders for any game
54
+ example_doom_battle1.txt a filled-in example
55
+ gamescaling/ inference client; depends only on requests + Pillow
56
+ prompt.py system prompts
57
+ policy.py multi-turn client: history window, image encoding, request body
58
+ action.py action format and parser (200 ms chunk -> 6 x 33 ms key/mouse)
59
+ keymaps.py training key vocabulary <-> browser / Playwright key names
60
+ scripts/serve_vllm.sh OpenAI-compatible server
61
+ tests/test_core.py offline unit tests (no GPU or server needed)
62
  ```
63
 
64
+ ## 1. Download and serve
 
65
 
66
+ Download only the checkpoint you need:
 
 
 
 
 
 
 
 
 
67
 
68
+ ```bash
69
+ pip install -U huggingface_hub
70
+ huggingface-cli download THUSI-Lab/GameScaling \
71
+ --include "models/9b_s1_1600h_gs6736/*" "gamescaling/*" "prompts/*" "scripts/*" \
72
+ --local-dir ./GameScaling
73
+ cd GameScaling
74
+ ```
75
 
76
+ Serve it with vLLM (tested with 0.17.0):
77
 
78
  ```bash
79
+ pip install "vllm>=0.17"
80
+ bash scripts/serve_vllm.sh models/9b_s1_1600h_gs6736 8000 # 0.8B-9B fit on one GPU
81
+ bash scripts/serve_vllm.sh models/27b_s1_1600h_gs6995 8000 2 # 27B: tensor parallel 2
82
  ```
83
 
84
+ The script serves the model as `gamescaling`. The client connects to `http://127.0.0.1:8000/v1`
85
+ by default. Use the `chat_template.jinja` in the checkpoint folder (vLLM loads it by default). **Do
86
+ not** replace it with the stock Qwen template: this one renders past turns without reasoning as
87
+ `<think>\n\n</think>\n\n<|action_start|>...`, which is token-for-token what training used.
88
 
89
+ ## 2. System prompt template
90
 
91
+ `prompts/system_prompt_template.txt` is the standard system prompt. Fill in three placeholders
92
+ and keep everything else **exactly as written**: the model decides its output tags from the
93
+ exact text of the Output Format section.
94
 
95
+ | Placeholder | What to write | Example |
96
  |---|---|---|
97
+ | `{GAME_NAME}` | The game's name. It appears twice: in the opening and the key-list heading. | `Doom Battle-1` |
98
+ | `{GAME_RULES}` | One English paragraph: what the game is, the goal, and how it is scored. | `Kill as many monsters as possible; collect health and ammo.` |
99
+ | `{KEY_BINDINGS}` | One `- <key> -> <effect>` line per key. Use training key names (§5). | see below |
100
 
101
+ Example `{KEY_BINDINGS}`:
102
 
103
  ```text
104
  - W -> Move forward
 
109
  - Mouse X Y -> aim / rotate the camera (X>0 right, Y>0 down)
110
  ```
111
 
112
+ Add the last line only for games where the mouse controls the camera, and copy it verbatim. Key
113
+ names: uppercase letters (`W`); digits as `zero`..`nine`; F1-F12 as `One`..`Twelve`; mouse
114
+ buttons as `LB` / `RB` / `MB`; arrows as `Up` / `Down` / `Left` / `Right`. A complete filled-in
115
+ prompt is in `prompts/example_doom_battle1.txt`.
116
 
117
+ Using the text file directly:
118
 
119
  ```python
120
  tpl = open("prompts/system_prompt_template.txt").read().rstrip("\n")
 
124
  "- Mouse X Y -> aim / rotate the camera (X>0 right, Y>0 down)"))
125
  ```
126
 
127
+ The result is byte-identical to what `build_system_prompt(...)` below produces;
128
+ `tests/test_core.py` checks this.
129
 
130
+ ## 3. Minimal usage
131
 
132
  ```python
133
  from PIL import Image
134
  from gamescaling import GameScalingPolicy, PolicyConfig, build_system_prompt
135
 
 
136
  system = build_system_prompt(
137
  "Doom Battle-1",
138
  rules="Kill as many monsters as possible; collect health and ammo.",
139
  key_bindings={"W": "Move forward", "S": "Move backward", "A": "Strafe left",
140
  "D": "Strafe right", "LB": "Fire"},
141
+ mouse_aim=True, # the mouse controls the camera
142
+ ) # reasoning=True by default, matching the <think> prefill
143
+ policy = GameScalingPolicy(system, PolicyConfig()) # prefills <think>; frame sent at native size
144
+ policy.reset() # call at the start of every episode
145
+ decision = policy.act(Image.open("frame.png")) # one call = one 200 ms decision
146
  a = decision.action
147
+ a.mouse # (X, Y, Z): mouse movement for this chunk (per-mille of the screen) + scroll
148
+ a.steps # 6 groups: the keys held during each 33 ms step
149
+ a.parsed # False = could not be parsed; execute as a no-op
150
  ```
151
 
152
+ Pass `a` to your executor. Run 6 steps of 33 ms, holding that step's keys during each one. Keys
153
+ that appear in two consecutive groups stay held rather than being pressed again. Mouse movement
154
+ is `X/1000 * screen width` and `Y/1000 * screen height`. You can spread it over the 200 ms or
155
+ send it at once.
156
 
157
+ Defaults are not tied to any benchmark: any game frame, the standard system prompt, and a
158
+ `<think>` prefill. To reproduce a paper table, use
159
+ `PolicyConfig.preset("omnigamearena" | "vizdoom" | "gameworld")` instead (§6).
160
+ `TRAINING_SYSTEM_PROMPT` is the generic pre-training prompt. Its Output Format is action-only and
161
+ does not match a `<think>` prefill, so use it only for ablations.
162
 
163
+ ## 4. Prompt and message structure
164
 
165
+ Every benchmark uses the same skeleton in a fixed order (`gamescaling/prompt.py`):
166
 
167
+ 1. **Opening**: gaming expert + game name + the contract "plan the next 200 ms as 6 steps of
168
+ 33 ms";
169
+ 2. `**Game Rules**`: what the game is and how it is scored;
170
+ 3. **Controls**: the keys the game uses, **written in the training vocabulary** (§5);
171
+ 4. `**Task Instruction**`: GameWorld only (one game can have several tasks);
172
+ 5. `**Output Format**`: how the model picks its output format;
173
+ 6. `**Explanation**` + `**Key Naming Rules**`.
174
 
175
+ There are two layouts. They differ only in section 3 and the separators:
176
 
177
+ - `build_system_prompt(...)`: key list (`**Common Keys in <game>**` + `- W -> Move forward`).
178
+ Used for ViZDoom, OmniGameArena and **any new game**. Add `mouse_aim=True` for games where the
179
+ mouse controls the camera.
180
+ - `build_gameworld_system_prompt(...)`: free-text paragraphs (`**Role and Controls**`), for
181
+ games that come with their own written controls, as in GameWorld. `translate_controls`
182
+ rewrites browser key names into the training vocabulary (`ArrowLeft` -> `Left`, `a/d` ->
183
+ `A/D`, `1-9:` -> `one-nine:`).
184
 
185
+ The Output Format **must match whether the client prefills `<think>`**:
186
 
187
+ | `reasoning` | Client | Model output |
188
  |---|---|---|
189
+ | `True` (default) | `think_prefill=True`: the assistant turn starts with `<think>` | Decides whether and how long to think, closes `</think>`, then outputs the action |
190
+ | `False` | no prefill | Outputs `<|action_start|>...<|action_end|>` directly |
 
 
 
191
 
192
+ Each user turn is the text `current game screen` followed by the frame. Multi-turn messages
193
+ (`policy.py`):
194
 
195
  ```
196
+ system system prompt
197
+ user "current game screen" + frame t-k ┐ last history_len turns (default 5)
198
+ assistant action taken at t-k (no reasoning) ┘
199
  ...
200
+ user "current game screen" + current frame
201
+ assistant "<think>" (prefill; continue_final_message=True)
202
  ```
203
 
204
+ Past turns keep only the action (`history_think=0`, as in the paper). A turn that failed to parse
205
+ is stored as an explicit no-op, `<|action_start|>0 0 0 ; ; ; ; ; ; <|action_end|>`, not as the
206
+ raw bad output.
207
 
208
+ Default request: `temperature=1.0`, `max_tokens=2048`, `stop=["<|action_end|>"]`,
209
+ `skip_special_tokens=false`, JPEG frames (quality 90) at native resolution.
210
+
211
+ ## 5. Action format
212
 
213
  ```
214
  <|action_start|>X Y Z ; g1 ; g2 ; g3 ; g4 ; g5 ; g6<|action_end|>
215
  ```
216
 
217
+ - `X Y`: mouse movement over the whole 200 ms, in per-mille of the screen
218
+ (`X = Σdx / screen width × 1000`), clipped to ±1000. X>0 is right, Y>0 is down.
219
+ - `Z`: scroll notches, clipped to ±5. Z>0 is up.
220
+ - `g1..g6`: six 33 ms steps. Each group lists the keys **held** during that step, at most 4. An
221
+ empty group releases all keys, but the `;` must stay.
222
+ - `<|action_start|>` / `<|action_end|>` are special tokens. Requests must set
223
+ `skip_special_tokens: false` (`build_request` does), or the server strips the tags.
224
+ `parse_action` also accepts a bare `X Y Z ; ...` as a fallback.
225
+ - The action is read **only after the last `</think>`**: a model that thinks often quotes the
226
+ format inside its reasoning. An unclosed `<think>` means the generation was cut off; it is
227
+ executed as a no-op (`Action.truncated=True`).
228
+
229
+ Training key vocabulary (74 tokens, case-sensitive, `gamescaling.action.KEY_VOCAB`):
230
+
231
+ | Group | Tokens |
232
  |---|---|
233
+ | Letters | `A`..`Z` |
234
+ | Number row | `zero` `one` .. `nine` |
235
+ | F1-F12 | `One` `Two` .. `Twelve` (**capitalized** = function key) |
236
+ | Mouse buttons | `LB` `RB` `MB` |
237
+ | Arrows | `Up` `Down` `Left` `Right` |
238
+ | Other | `Shift` `Ctrl` `Alt` `Tab` `Caps` `Esc` `Space` `Enter` `Back` `Delete` `Insert` `Home` `End` `Pause` `Equal` `Minus` `Period` `Slash` `Quote` |
 
 
 
 
 
239
 
240
+ Tokens outside the vocabulary are dropped and recorded in `Action.dropped`.
241
+ `keymaps.TO_PLAYWRIGHT` maps them to browser key names.
242
 
243
+ ## 6. Paper evaluation settings
244
 
245
+ The paper evaluates on MCU (Minecraft), ViZDoom, OmniGameArena and GameWorld. The main tables use
246
+ a **time-paused** clock: the game freezes while the model thinks and advances only while a chunk
247
+ executes. The project page also reports a real-time setting, where the game keeps running during
248
+ inference. Scores from different clocks are not comparable.
 
 
 
 
 
 
 
 
 
249
 
250
+ Client presets for reproducing the paper:
 
 
 
 
 
 
 
 
 
251
 
252
+ | Preset | Frame | `<think>` prefill | Other |
253
+ |---|---|---|---|
254
+ | `omnigamearena` | resized to 854×480, JPEG | yes | 300 steps per episode (500 for `scene_escape` and `cue_chase`), 3 episodes |
255
+ | `vizdoom` | resized to 854×480, JPEG | yes | no stop string; 4 maps × 10 episodes, at most 350 chunks each |
256
+ | `gameworld` | height 480, PNG (Lanczos) | no | `max_tokens=4096`, lenient parsing; use `build_gameworld_system_prompt(..., reasoning=False)` |
257
 
258
+ All presets keep 5 history turns (frame + action) at temperature 1.0. On OmniGameArena,
259
+ `solo_craft` is scored as `min(V/50, 1)` from the raw points `V`, and the overall score is the
260
+ mean over the seven games.
 
261
 
262
+ When running OmniGameArena, use **one UE5 instance per GPU**. Actions are executed in wall-clock
263
+ time (6 × 33 ms), and instances sharing a GPU's renderer barely move. Scores drop sharply with no
264
+ error message.
265
 
266
+ ## 7. Adding a new game
 
 
 
267
 
268
+ 1. Fill in `prompts/system_prompt_template.txt` (§2), or call `build_system_prompt` with the game
269
+ name, rules and `key_bindings` in the training vocabulary. Add `mouse_aim=True` if the mouse
270
+ controls the camera.
271
+ 2. Capture one frame every 200 ms (or at every decision under a paused clock) and call
272
+ `policy.act(frame)`.
273
+ 3. Execute the returned `Action` as in §5: held keys for 6 steps of 33 ms, plus mouse movement and
274
+ scroll.
275
+ 4. Keep the default client (native frame size, `<think>` prefill, standard prompt). Switch to a
276
+ preset (§6) only to match a paper table.
277
 
278
+ ## 8. Tests
 
279
 
280
  ```bash
281
+ python tests/test_core.py # or: python -m pytest tests/
 
 
 
 
 
 
 
 
282
  ```
283
 
284
+ Offline checks of prompts, request bodies and action parsing; no GPU or server needed.
 
 
 
285
 
286
+ ## Citation
 
 
 
 
287
 
288
+ ```bibtex
289
+ @article{zhang2026scaling,
290
+ title = {Scaling in Games: Continued Pre-Training for Embodied Agents in Diverse Virtual Worlds},
291
+ author = {Zhang, Kuan and Chen, Yukun and Yang, Zhihao and Su, Yue and Wu, Xiangnan and
292
+ Chen, Zirong and Luo, Run and Hou, Jinkun and Tan, Tao and Zheng, Yinhe and Li, Yiming},
293
+ journal = {arXiv preprint},
294
+ year = {2026}
295
+ }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
296
  ```