Image-Text-to-Text
Transformers
Safetensors
English
gamescaling
game-agent
embodied-agent
keyboard-mouse
continued-pretraining
qwen3_5
Instructions to use THUSI-Lab/GameScaling with libraries, inference providers, notebooks, and local apps. Follow these links to get started.
- Libraries
- Transformers
How to use THUSI-Lab/GameScaling with Transformers:
# Use a pipeline as a high-level helper from transformers import pipeline pipe = pipeline("image-text-to-text", model="THUSI-Lab/GameScaling")# pip install -U transformers accelerate # Load model directly from transformers import AutoModel model = AutoModel.from_pretrained("THUSI-Lab/GameScaling", device_map="auto") - Notebooks
- Google Colab
- Kaggle
- Local Apps Settings
- vLLM
How to use THUSI-Lab/GameScaling with vLLM:
Install from pip and serve model
# Install vLLM from pip: pip install vllm # Start the vLLM server: vllm serve "THUSI-Lab/GameScaling" # Call the server using curl (OpenAI-compatible API): curl -X POST "http://localhost:8000/v1/completions" \ -H "Content-Type: application/json" \ --data '{ "model": "THUSI-Lab/GameScaling", "prompt": "Once upon a time,", "max_tokens": 512, "temperature": 0.5 }'Use Docker
docker model run hf.co/THUSI-Lab/GameScaling
- SGLang
How to use THUSI-Lab/GameScaling with SGLang:
Install from pip and serve model
# Install SGLang from pip: pip install sglang # Start the SGLang server: python3 -m sglang.launch_server \ --model-path "THUSI-Lab/GameScaling" \ --host 0.0.0.0 \ --port 30000 # Call the server using curl (OpenAI-compatible API): curl -X POST "http://localhost:30000/v1/completions" \ -H "Content-Type: application/json" \ --data '{ "model": "THUSI-Lab/GameScaling", "prompt": "Once upon a time,", "max_tokens": 512, "temperature": 0.5 }'Use Docker images
docker run --gpus all \ --shm-size 32g \ -p 30000:30000 \ -v ~/.cache/huggingface:/root/.cache/huggingface \ --env "HF_TOKEN=<secret>" \ --ipc=host \ lmsysorg/sglang:latest \ python3 -m sglang.launch_server \ --model-path "THUSI-Lab/GameScaling" \ --host 0.0.0.0 \ --port 30000 # Call the server using curl (OpenAI-compatible API): curl -X POST "http://localhost:30000/v1/completions" \ -H "Content-Type: application/json" \ --data '{ "model": "THUSI-Lab/GameScaling", "prompt": "Once upon a time,", "max_tokens": 512, "temperature": 0.5 }' - Docker Model Runner
How to use THUSI-Lab/GameScaling with Docker Model Runner:
docker model run hf.co/THUSI-Lab/GameScaling
English model card aligned with gamescaling.github.io
Browse files
README.md
CHANGED
|
@@ -1,70 +1,104 @@
|
|
| 1 |
---
|
|
|
|
|
|
|
| 2 |
tags:
|
| 3 |
- gamescaling
|
|
|
|
|
|
|
|
|
|
| 4 |
- qwen3_5
|
| 5 |
---
|
| 6 |
|
| 7 |
-
#
|
| 8 |
|
| 9 |
-
|
| 10 |
-
|
| 11 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 12 |
|
| 13 |
```
|
| 14 |
-
|
| 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 |
-
|
| 23 |
-
|
| 24 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 25 |
```
|
| 26 |
|
| 27 |
-
|
| 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 |
-
`
|
| 42 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 43 |
|
| 44 |
-
|
| 45 |
|
| 46 |
```bash
|
| 47 |
-
pip install "vllm>=0.17"
|
| 48 |
-
bash scripts/serve_vllm.sh /
|
|
|
|
| 49 |
```
|
| 50 |
|
| 51 |
-
|
| 52 |
-
|
| 53 |
-
|
| 54 |
-
|
| 55 |
|
| 56 |
-
## 2.
|
| 57 |
|
| 58 |
-
`prompts/system_prompt_template.txt`
|
| 59 |
-
|
|
|
|
| 60 |
|
| 61 |
-
|
|
| 62 |
|---|---|---|
|
| 63 |
-
| `{GAME_NAME}` |
|
| 64 |
-
| `{GAME_RULES}` |
|
| 65 |
-
| `{KEY_BINDINGS}` |
|
| 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 |
-
|
| 79 |
-
`zero`..`nine`
|
| 80 |
-
`Up` / `Down` / `Left` / `Right`
|
|
|
|
| 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 |
-
|
|
|
|
| 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
|
| 108 |
-
policy = GameScalingPolicy(system, PolicyConfig()) #
|
| 109 |
-
policy.reset() #
|
| 110 |
-
decision = policy.act(Image.open("frame.png")) #
|
| 111 |
a = decision.action
|
| 112 |
-
a.mouse # (X, Y, Z)
|
| 113 |
-
a.steps # 6
|
| 114 |
-
a.parsed # False =
|
| 115 |
```
|
| 116 |
|
| 117 |
-
|
| 118 |
-
|
| 119 |
-
|
|
|
|
| 120 |
|
| 121 |
-
|
| 122 |
-
`
|
| 123 |
-
|
| 124 |
-
`
|
| 125 |
-
|
| 126 |
|
| 127 |
-
## 4.
|
| 128 |
|
| 129 |
-
|
| 130 |
|
| 131 |
-
1. **
|
| 132 |
-
|
| 133 |
-
|
| 134 |
-
|
| 135 |
-
|
| 136 |
-
|
|
|
|
| 137 |
|
| 138 |
-
|
| 139 |
|
| 140 |
-
- `build_system_prompt(...)`
|
| 141 |
-
|
| 142 |
-
|
| 143 |
-
- `build_gameworld_system_prompt(...)`
|
| 144 |
-
|
| 145 |
-
|
|
|
|
| 146 |
|
| 147 |
-
|
| 148 |
|
| 149 |
-
| `reasoning` |
|
| 150 |
|---|---|---|
|
| 151 |
-
| `True` | `think_prefill=True`
|
| 152 |
-
| `False` |
|
| 153 |
-
|
| 154 |
-
提示词请原样拷贝,不要改写:checkpoint 是按这段 Output Format 的精确文本来决定输出标签的。
|
| 155 |
-
每轮 user 消息的���本固定为 `current game screen`,后接当前帧。
|
| 156 |
|
| 157 |
-
|
|
|
|
| 158 |
|
| 159 |
```
|
| 160 |
-
system
|
| 161 |
-
user "current game screen" +
|
| 162 |
-
assistant
|
| 163 |
...
|
| 164 |
-
user "current game screen" +
|
| 165 |
-
assistant "<think>"
|
| 166 |
```
|
| 167 |
|
| 168 |
-
|
| 169 |
-
`<|action_start|>0 0 0 ; ; ; ; ; ; <|action_end|>`
|
|
|
|
| 170 |
|
| 171 |
-
|
|
|
|
|
|
|
|
|
|
| 172 |
|
| 173 |
```
|
| 174 |
<|action_start|>X Y Z ; g1 ; g2 ; g3 ; g4 ; g5 ; g6<|action_end|>
|
| 175 |
```
|
| 176 |
|
| 177 |
-
- `X Y`
|
| 178 |
-
X>0
|
| 179 |
-
- `Z`
|
| 180 |
-
- `g1..g6`
|
| 181 |
-
|
| 182 |
-
- `<|action_start|>` / `<|action_end|>`
|
| 183 |
-
`skip_special_tokens: false`
|
| 184 |
-
|
| 185 |
-
-
|
| 186 |
-
|
| 187 |
-
|
| 188 |
-
|
| 189 |
-
|
| 190 |
-
|
|
|
|
| 191 |
|---|---|
|
| 192 |
-
|
|
| 193 |
-
|
|
| 194 |
-
| F1-F12 | `One` `Two` .. `Twelve`
|
| 195 |
-
|
|
| 196 |
-
|
|
| 197 |
-
|
|
| 198 |
-
|
| 199 |
-
不在词表里的 token 被丢弃(记在 `Action.dropped`)。`keymaps.TO_PLAYWRIGHT` 给出到浏览器
|
| 200 |
-
键名的映射。
|
| 201 |
-
|
| 202 |
-
## 6. 三个 benchmark
|
| 203 |
|
| 204 |
-
|
| 205 |
-
|
| 206 |
|
| 207 |
-
##
|
| 208 |
|
| 209 |
-
|
| 210 |
-
|
| 211 |
-
|
| 212 |
-
|
| 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 |
-
|
| 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 |
-
|
| 235 |
-
|
|
|
|
|
|
|
|
|
|
| 236 |
|
| 237 |
-
|
| 238 |
-
`
|
| 239 |
-
|
| 240 |
-
benchmark 分数;OVERALL 是七个游戏的均值。
|
| 241 |
|
| 242 |
-
|
|
|
|
|
|
|
| 243 |
|
| 244 |
-
|
| 245 |
-
前进 0.2 s 几乎不动,分数会大幅下降且不报错。
|
| 246 |
-
- `solo_craft` 的后续 episode 请换一个新的 UE5 进程;复用实例时第 2、3 局会立即结束并复报
|
| 247 |
-
第 1 局分数。
|
| 248 |
|
| 249 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 250 |
|
| 251 |
-
|
| 252 |
-
id `gamescaling`:
|
| 253 |
|
| 254 |
```bash
|
| 255 |
-
|
| 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 |
-
|
| 267 |
-
滚轮。鼠标是千分比屏幕空间里的虚拟光标,从中心出发、每个 chunk 移动 (X, Y) 并截断到屏幕内。
|
| 268 |
-
系统提示词走 `gamescaling_template.j2`(自由段落版),控制说明经 `translate_controls`
|
| 269 |
-
转成训练词表。
|
| 270 |
|
| 271 |
-
|
| 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 |
-
|
| 278 |
-
|
| 279 |
-
|
| 280 |
-
|
| 281 |
-
|
| 282 |
-
|
| 283 |
-
|
| 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 |
```
|