A 2D top-down prototype built in Unity 2022.3 LTS (C#). One round lasts ~30–90s: grab cores scattered around a small arena, then reach the green extraction zone before the rising pressure (faster enemies, traps, alert stages) catches you. Every extra core is more score — but staying longer is deadlier.
Everything is generated in code: the level, the geometry-shape sprites, and the sine/square/triangle sound effects. There are no binary art or audio assets.
Task spec: CHALLENGE.md · AI assistant rules: CLAUDE.md
./run.sh # build (if needed) and play, preset = normal
./run.sh easy # play the easy preset
./run.sh hard # play the hard preset
./run.sh generated # play the AI-generated preset (after ./run.sh gen)
./run.sh build # build for the CURRENT platform
./run.sh build all # build macOS + Windows + Linux
./run.sh build win # build a specific platform: current | mac | win | linux | all
./run.sh sim # headless bot: 20 runs per preset, prints + saves a summary
./run.sh sim hard 30
./run.sh gen normal 0.5 # AI level-gen: find a layout with ~50% bot win rate
./run.sh gen hard 0.3 7 16 16 # preset target seed candidates runsrun.sh auto-detects the current OS and launches the matching prebuilt player,
building it first if needed. Builds live in per-platform folders so several can
coexist for cross-platform testing:
build/macOS/ExtractionPrototype.app
build/Windows/ExtractionPrototype.exe
build/Linux/ExtractionPrototype.x86_64
If no prebuilt player exists, it finds a Unity editor (via UNITY_PATH or Unity
Hub) and builds in batchmode first.
- Install Unity 2022.3 LTS (Unity Hub) and open the project at
./unity. - Press Play — the game self-bootstraps, so no scene setup is needed.
- (Or) set
UNITY_PATH=/path/to/Unityand re-run./run.sh.
The repo ships project source. Produce the desktop build once (
./run.sh build, or Unity menuExtractionGame ▸ Build Standalone) and the binary lands in./build.
| Action | Keys |
|---|---|
| Move | WASD / Arrow keys |
| Dash (short burst, has cooldown) | Space |
| Start round (menu) | 1/2/3 pick preset, Enter/Space start |
| Restart same preset (result) | R |
| Back to menu (result) | M |
Goal: collect at least one core, then enter the pulsing green ring to extract. Blue circle = you, red triangles = enemies, yellow diamonds = cores, orange diamonds = traps, gray = walls.
The menu also has a 玩家画像 / Player Persona row and (after ./run.sh gen) a
GENERATED (AI) difficulty — both are described below.
All gameplay numbers come from unity/Assets/StreamingAssets/tuning/*.json.
Edit them and replay — no recompile, no code changes. Three presets ship:
easy.json, normal.json, hard.json. Pick a preset in the menu, with
./run.sh <preset>, or via the EXTRACT_PRESET env var / -preset arg.
For non-technical playtesters, the main menu has a 「调参面板 Tuning」 button that opens a Chinese visual panel of sliders for every category. Adjust values and:
- 应用并开始 — start a round immediately with the tweaked values (no file edit).
- 保存到 JSON — write the values back to
StreamingAssets/tuning/<preset>.json. - 恢复预设 / 关闭 — reset to the preset's defaults / close.
This lets a designer feel out a tuning change in seconds, then persist it.
Five tunable categories (spec requires ≥4):
- player — moveSpeed, dashSpeed, dashCooldown, dashDuration, maxHealth, invulnTime
- level — mapWidth, mapHeight, resourceCount, obstacleDensity, seed, extractionRule
- risk — enemyCount, enemyBaseSpeed, enemySpeedGrowthPerSec, alertGrowthPerSec, trapCount, trapDamage, enemyDamage
- scoring — baseScorePerResource, comboBonus, extractionBonus, timeoutPenalty, survivalBonusPerSec
- time — roundDuration, alertStageInterval, resourceRespawn
A persona is a small overlay layered on top of the chosen difficulty, not a
separate difficulty. Difficulty answers "how hard is the level"; persona answers
"what kind of player am I tuning for". Overlays live in
unity/Assets/StreamingAssets/personas/*.json (tunable, no code) and apply
multipliers/deltas to the loaded preset. Pick one in the menu's persona row; it's
recorded per round and shown on the result screen and in the heatmap.
| Persona | What changes | Who it serves |
|---|---|---|
| 新手 Novice | +1 HP, enemies ×0.85, dash cooldown ×0.8, time ×1.15 | A learning player: bigger margins so a mistake isn't fatal and the "grab vs leave" decision can be practised safely. |
| 熟练 Skilled | nothing (neutral) | Plays the preset exactly as authored — the calibrated risk/reward baseline. |
| 冒险型 Adventurer | +2 cores, score ×1.25, enemies ×1.1, time ×0.9 | A score-chaser: more loot on the map and bigger payouts, paid for with faster enemies and a tighter clock. |
The two axes compose: e.g. hard + novice is "a tough map made survivable for
a newcomer," while easy + adventurer is "a gentle map that still rewards
greed." Personas are applied once at round start, so R (replay) reuses them.
Every round samples the player position ~4 Hz into the telemetry path, plus
enough level info (size, seed, density) to rebuild the geometry. On the result
screen, 复盘热力图 Heatmap opens a review overlay with two modes:
- 本局路径 (this run) — the level redrawn with a dwell heatmap (where you lingered, cool→hot), your movement/resource path as fading dots, and event markers (start, pickups, damage, extraction, death). Use ◀/▶ to step through recent sessions.
- 聚合死亡点 (all deaths) — every non-extraction end position across recent sessions of that preset binned onto the map, surfacing kill-zones to rebalance.
This is the designer's "why did they die / where did they get stuck" view.
./run.sh gen <preset> <targetWin> [seed] [candidates] [runs] runs a
generate-then-validate loop driven from a level_seed:
- From the base preset it generates several candidate layouts, varying the level
seed, obstacle density, and resource count (deterministically from
level_seed). - Degenerate candidates are rejected with the existing BFS check (exit must be reachable, enough reachable cores).
- Each surviving layout is validated by the bot: the shared headless
SimCoreplays itrunstimes with a jitteredBotPlayer(different bail boldness per trial, seeded fromlevel_seed), yielding an estimated win rate for that exact layout. - The layout whose estimated win rate is closest to
targetWinis written tounity/Assets/StreamingAssets/tuning/generated.json(playable via./run.sh generatedor the menu's GENERATED button); a rankedtelemetry/level_gen_report.jsonlists every candidate.
Because the bot, layout generator, and scoring are the same code the live game uses, the estimated win rate is meaningful rather than a separate toy model.
This generator was itself built with the AI loop described in AI_WORKFLOW.md. The non-obvious part was the prompt/heuristic iteration: a deterministic bot produces a single win/lose per seed, so the first AI draft (one run per seed) couldn't express a rate. The fix — prompted and reviewed across a few iterations — was to hold the layout fixed and instead jitter the bot's bail thresholds per trial, so a single layout yields a spread of outcomes. Candidates are then filtered by (a) the reachability gate and (b) distance to the target win rate, and ranked in the report so a human can sanity -check the pick rather than trusting it blindly.
Each finished round writes telemetry/session_YYYYMMDD_HHmmss_fff.json with:
preset, persona, win/lose, duration, score, cores collected, combo pickups,
damage taken, fail reason, end position, a timestamped event timeline
(pickup, alert_stage, damage, near_extraction, game_end), a level
block (size/seed/density, used to rebuild geometry for the heatmap) and a sampled
path (the ~4 Hz position trail that drives the dwell heatmap).
An aggregate telemetry/summary.json is regenerated after every round (per-preset
win rate, avg score, avg duration, avg cores, most common fail reason). The same
data is shown on the in-game result/review screen (recent sessions + table).
./run.sh sim writes one telemetry/sim_*.json per bot run plus
telemetry/sim_summary.json, so you can estimate how a preset feels before
playing it yourself.
run.sh one-command launcher (play / build / sim / gen)
telemetry/ generated session + summary files
unity/Assets/Scripts/
Config/ TuningConfig + loader; PersonaConfig + PersonaLoader
Core/ GameBootstrap, GameManager (state machine), Rules
Gameplay/ LevelLayout (deterministic), LevelBuilder, Player, Enemy, ...
Telemetry/ SessionModels (path/level/persona), Recorder, Aggregator
Sim/ SimCore (headless), BotPlayer, BotSimulator, LevelGenerator
Audio/Util/UI/ procedural audio, shape sprites, seeded RNG, IMGUI HUD + HeatmapRenderer
unity/Assets/Editor/ BuildScript (batchmode build + sim + level-gen entry points)
unity/Assets/Tests/EditMode tests (config, scoring, alert, telemetry, layout, sim, persona, heatmap, level-gen)
unity/Assets/StreamingAssets/tuning/ easy.json / normal.json / hard.json (+ generated.json)
unity/Assets/StreamingAssets/personas/ novice.json / skilled.json / adventurer.json
The live game and the bot simulator share the same LevelLayout generator and
Rules math, so the simulator faithfully reflects the real experience.
EditMode tests (Unity Test Framework) cover the core requirements and the bonus
features: config load/override, scoring math, alert progression, telemetry
serialization (incl. path/level/persona), summary aggregation, deterministic +
reachable level layout, a full headless round via SimCore, persona overlay math,
heatmap binning, and the level generator (determinism + closest-to-target pick).
Run from the editor (Window ▸ General ▸ Test Runner) or batchmode:
"$UNITY_PATH" -batchmode -runTests -projectPath unity \
-testPlatform EditMode -testResults unity/test-results.xml以下为上文英文文档的完整中文翻译,便于中文评审阅读。命令、路径与代码标识保持英文原样。
基于 Unity 2022.3 LTS (C#) 开发的 2D 俯视角原型。单局约 30–90 秒:在小型竞技场中 收集散落的核心 (core),然后在压力持续上升(敌人提速、陷阱、警报阶段)之前抵达绿色撤离区。 多拿一个核心就多一份分数——但停留越久越危险。
所有内容均由代码生成:关卡、几何形状精灵 (sprite) 以及正弦/方波/三角波音效。 项目不含任何二进制美术或音频资源。
任务说明:CHALLENGE.md · AI 助手规则:CLAUDE.md
./run.sh # 按需打包并试玩,预设 = normal
./run.sh easy # 试玩 easy 预设
./run.sh hard # 试玩 hard 预设
./run.sh generated # 试玩 AI 生成的预设(需先执行 ./run.sh gen)
./run.sh build # 为当前平台进行打包/构建
./run.sh build all # 构建 macOS + Windows + Linux
./run.sh build win # 构建指定平台:current | mac | win | linux | all
./run.sh sim # 无头模拟器/自动化 Bot:每个预设跑 20 局,输出并保存聚合结果
./run.sh sim hard 30
./run.sh gen normal 0.5 # AI 关卡生成:寻找 Bot 通关率约 50% 的布局
./run.sh gen hard 0.3 7 16 16 # 预设 目标通关率 种子 候选数 每候选试跑次数run.sh 会自动识别当前操作系统,启动对应的预构建播放器 (player);若不存在则先行构建。
构建产物按平台分目录存放,便于多平台并存测试:
build/macOS/ExtractionPrototype.app
build/Windows/ExtractionPrototype.exe
build/Linux/ExtractionPrototype.x86_64
若没有预构建产物,脚本会通过 UNITY_PATH 或 Unity Hub 查找 Unity 编辑器,并先以
batchmode(批处理模式)进行构建。
- 安装 Unity 2022.3 LTS(通过 Unity Hub),并打开位于
./unity的工程。 - 点击 Play —— 游戏会自启动 (self-bootstrap),无需搭建场景。
- (或)设置
UNITY_PATH=/path/to/Unity后重新运行./run.sh。
仓库提交的是工程源码。先执行一次桌面构建(
./run.sh build,或使用 Unity 菜单ExtractionGame ▸ Build Standalone),产物会落到./build。
| 操作 | 按键 |
|---|---|
| 移动 | WASD / 方向键 |
| 冲刺(短促爆发,有冷却) | Space |
| 开始单局(菜单) | 1/2/3 选择预设,Enter/Space 开始 |
| 以相同预设重开(结算页) | R |
| 返回菜单(结算页) | M |
目标: 至少收集一个核心,再进入脉动的绿色光环完成撤离。 蓝色圆=玩家,红色三角=敌人,黄色菱形=核心,橙色菱形=陷阱,灰色=墙体。
菜单中还包含 玩家画像 / Player Persona 一行,以及(执行 ./run.sh gen 后出现的)
GENERATED (AI) 难度——两者均在下文说明。
所有玩法数值均来自 unity/Assets/StreamingAssets/tuning/*.json。
修改后重玩即可生效——无需重新编译、无需改动代码。内置三套预设:
easy.json、normal.json、hard.json。可在菜单中选择,或通过 ./run.sh <preset>,
亦可使用 EXTRACT_PRESET 环境变量 / -preset 启动参数。
面向非技术试玩者,主菜单提供 「调参面板 Tuning」 按钮,打开后是一个中文可视化滑块面板, 覆盖全部类别。调整数值后可:
- 应用并开始 —— 立即以改后的数值开局(不写文件)。
- 保存到 JSON —— 将数值写回
StreamingAssets/tuning/<preset>.json。 - 恢复预设 / 关闭 —— 恢复该预设默认值 / 关闭面板。
这让策划能在数秒内感受一次调参改动,再决定是否持久化。
五个可调类别(任务要求 ≥4 类):
- player(玩家) —— moveSpeed、dashSpeed、dashCooldown、dashDuration、maxHealth、invulnTime
- level(关卡) —— mapWidth、mapHeight、resourceCount、obstacleDensity、seed、extractionRule
- risk(风险) —— enemyCount、enemyBaseSpeed、enemySpeedGrowthPerSec、alertGrowthPerSec、trapCount、trapDamage、enemyDamage
- scoring(评分) —— baseScorePerResource、comboBonus、extractionBonus、timeoutPenalty、survivalBonusPerSec
- time(时间) —— roundDuration、alertStageInterval、resourceRespawn
画像 (persona) 是叠加在所选难度之上的一层薄叠加,并非独立的难度。
难度回答「这张关卡有多难」;画像回答「我在为哪类玩家调参」。叠加层位于
unity/Assets/StreamingAssets/personas/*.json(可调、无需改代码),对已加载的预设施加
乘数/增减量。在菜单画像行中选择即可;它会被逐局埋点记录,并在结算页与热力图中展示。
| 画像 | 改变了什么 | 服务于谁 |
|---|---|---|
| 新手 Novice | +1 生命,敌人 ×0.85,冲刺冷却 ×0.8,时间 ×1.15 | 学习中的玩家:更大的容错空间,让失误不致命,可以安全地练习「多拿还是撤离」的取舍。 |
| 熟练 Skilled | 无变化(中性) | 完全按作者设定来玩——经过校准的风险/收益基线。 |
| 冒险型 Adventurer | +2 核心,分数 ×1.25,敌人 ×1.1,时间 ×0.9 | 刷分型玩家:地图上有更多战利品与更高回报,代价是敌人更快、时钟更紧。 |
两条轴可以组合:例如 hard + novice 即「一张硬核地图,但对新手而言可生还」;
而 easy + adventurer 即「一张温和地图,但仍奖励贪婪」。画像在开局时仅应用一次,
因此 R(重玩)会沿用同一画像。
每局都会以约 4 Hz 把玩家位置采样进遥测 (telemetry) 的 path,并记录足够的 level
信息(尺寸、种子、密度)以重建几何。结算页的 复盘热力图 Heatmap 会打开一个复盘浮层,
含两种模式:
- 本局路径 (this run) —— 重绘关卡,叠加停留热力图(你逗留之处,由冷到热)、 以渐隐圆点表示的移动/取资源路径,以及事件标记(起点、拾取、受伤、撤离、死亡)。 使用 ◀/▶ 翻阅最近的若干局。
- 聚合死亡点 (all deaths) —— 将该预设最近若干局中所有「非撤离」结束位置归并 (bin) 到地图网格上,凸显需要再平衡的死亡热区 (kill-zone)。
这正是策划用来回答「玩家为何死亡 / 在哪里卡住」的视图。
./run.sh gen <preset> <targetWin> [seed] [candidates] [runs] 会以 level_seed
驱动一套**「先生成、再验证」**的流程:
- 从基础预设出发,生成若干候选布局,对关卡种子、障碍密度 (obstacle density) 与
资源数量 (resource count) 进行变化(由
level_seed决定,可复现)。 - 退化的候选会被既有的 BFS 检查剔除(撤离点必须可达、可达核心数量足够)。
- 每个存活布局都由 Bot 进行验证:共享的无头
SimCore用带扰动 (jitter) 的BotPlayer(每次试跑的「撤离果断度」不同,且由level_seed播种)将其试玩runs次, 从而估算该布局的通关率。 - 估算通关率最接近
targetWin的布局会被写入unity/Assets/StreamingAssets/tuning/generated.json(可通过./run.sh generated或菜单 GENERATED 按钮试玩);同时生成一份按优劣排序的telemetry/level_gen_report.json候选清单。
由于 Bot、关卡生成器与评分使用的都是实机同一套代码,因此估算出的通关率是有意义的, 而非另起炉灶的玩具模型。
该生成器本身也是用 AI_WORKFLOW.md 中描述的 AI 流程构建的。 不显然之处在于提示词/启发式的迭代:确定性 Bot 对每个种子只会产出单一的胜/负结果, 因此 AI 的初版(每个种子只跑一次)无法表达「通关率」这一概念。修正方案——经过数轮提示与 人工审阅——是固定布局不变,转而逐次试跑扰动 Bot 的撤离阈值,使同一布局产生一组分布化的 结果。随后再对候选进行筛选:(a) 可达性闸门;(b) 与目标通关率的距离;并在报告中排序, 以便人工对最终选择进行 sanity-check,而非盲目信任。
每局结束都会写入 telemetry/session_YYYYMMDD_HHmmss_fff.json,内容包括:
preset、persona、胜/负、时长、得分、收集核心数、连击拾取数、受伤次数、失败原因、
结束位置、带时间戳的事件时间线(pickup、alert_stage、damage、near_extraction、
game_end)、一段 level 信息块(尺寸/种子/密度,用于为热力图重建几何)以及一段采样
path(约 4 Hz 的位置轨迹,驱动停留热力图)。
每局之后都会重新生成聚合文件 telemetry/summary.json(按预设统计通关率、平均分、平均时长、
平均核心数、最常见失败原因)。相同数据也会展示在游戏内的结算/复盘界面(最近若干局 + 表格)。
./run.sh sim 会为每次 Bot 试跑写入一个 telemetry/sim_*.json,外加
telemetry/sim_summary.json,让你在亲自试玩之前就能估算某个预设的手感。
run.sh 一条命令的启动器(试玩 / 构建 / 模拟 / 生成)
telemetry/ 生成的单局与聚合文件
unity/Assets/Scripts/
Config/ TuningConfig + 加载器;PersonaConfig + PersonaLoader
Core/ GameBootstrap、GameManager(状态机)、Rules
Gameplay/ LevelLayout(确定性)、LevelBuilder、Player、Enemy……
Telemetry/ SessionModels(path/level/persona)、Recorder、Aggregator
Sim/ SimCore(无头)、BotPlayer、BotSimulator、LevelGenerator
Audio/Util/UI/ 程序化音频、形状精灵、播种 RNG、IMGUI HUD + HeatmapRenderer
unity/Assets/Editor/ BuildScript(batchmode 构建 + 模拟 + 关卡生成入口)
unity/Assets/Tests/EditMode 测试(config、scoring、alert、telemetry、layout、sim、persona、heatmap、level-gen)
unity/Assets/StreamingAssets/tuning/ easy.json / normal.json / hard.json(+ generated.json)
unity/Assets/StreamingAssets/personas/ novice.json / skilled.json / adventurer.json
实机游戏与 Bot 模拟器共用同一套 LevelLayout 生成器与 Rules 数学,
因此模拟器能忠实反映真实体验。
EditMode 测试(Unity Test Framework)覆盖了核心要求与加分项功能:
配置加载/覆盖、评分数学、警报推进、遥测序列化(含 path/level/persona)、聚合统计、
关卡布局的确定性 + 可达性、经由 SimCore 的完整无头单局、画像叠加数学、热力图归并,
以及关卡生成器(确定性 + 最接近目标的选择)。
可在编辑器中运行(Window ▸ General ▸ Test Runner)或使用 batchmode:
"$UNITY_PATH" -batchmode -runTests -projectPath unity \
-testPlatform EditMode -testResults unity/test-results.xml