feat: game pause/speed control API with AI pause protocol

- POST /api/action/pause - pause game (AI must call before operations)
- POST /api/action/unpause - resume game at desired speed
- POST /api/action/speed - set game speed (1x/2x/3x)
- Game state now includes isPaused and gameSpeed
- AI pause protocol documented in SKILL.md: pause before every operation
- Introduces cell.isDiggable flag that filters neutronium properly
- pause/unpause/speed CLI commands added to oni_api.py
This commit is contained in:
root
2026-05-22 09:04:04 +08:00
parent a4f2062c1a
commit 48a70e4928
4 changed files with 183 additions and 1 deletions

View File

@ -100,6 +100,11 @@ python3 tools/oni_api.py research_select ImprovedOxygen # 科研
python3 tools/oni_api.py mop 10 10 # 清理液体
python3 tools/oni_api.py harvest 20 15 # 收获植物
# 游戏速度控制AI 操作前必须先暂停)
python3 tools/oni_api.py pause "Reason here" # 暂停游戏
python3 tools/oni_api.py unpause 1 # 恢复1x 速度)
python3 tools/oni_api.py speed 3 # 直接设速度(不暂停)
# 批量任务
python3 tools/oni_api.py batch docs/batch_example.json # 执行批量建造计划
@ -231,6 +236,9 @@ python3 scripts/event_daemon.py # 事件守护进程AI 输入源)
| `/api/action/batch` | `{actions: [{type, ...}]}` — 批量执行 |
| `/api/action/priority_global` | `{target, priority}` — 全局默认优先级 |
| `/api/action/priority_type` | `{buildingType, priority}` — 按建筑类型设优先级 |
| `/api/action/pause` | `{reason?}` — 暂停游戏AI 操作前必须调用) |
| `/api/action/unpause` | `{speed?}` — 恢复游戏 |
| `/api/action/speed` | `{speed}` — 设置速度 1x/2x/3x |
## 项目结构

View File

@ -234,6 +234,62 @@ python3 tools/oni_builder.py build spom 42 42
---
## AI 暂停协议(关键规则)
AI **必须**在每次操作序列前暂停游戏操作完成后恢复这防止推理延迟期间游戏状态变化导致操作失效
### 规则
```
1. 收到事件 / 决定操作 → 立即 PAUSE
2. 查询状态、分析、决策(可耗时,游戏已暂停)
3. 执行操作dig / build / batch
4. 检查操作反馈
5. 所有操作完成后 → UNPAUSE
```
### 示例
```bash
# 1. 暂停
python3 tools/oni_api.py pause "Building SPOM"
# 2. 查询状态(游戏已冻结,不会变化)
python3 tools/oni_api.py explore 40 40 30 20
python3 tools/oni_api.py cell 45 42
# 3. 执行操作
python3 tools/oni_api.py dig 40 40 8 6
python3 tools/oni_api.py build Electrolyzer 45 42
# 4. 恢复
python3 tools/oni_api.py unpause 1
```
### 当前游戏速度
通过 `status` 命令查看
```
isPaused: true ← 游戏是否暂停
gameSpeed: 0 ← 0=暂停, 1=1x, 2=2x, 3=3x
```
### 总是暂停的场景
| 场景 | 原因 |
|------|------|
| 收到 critical 事件 | 可能需要紧急操作 |
| 执行 dig / build / deconstruct | 坐标必须精确不能偏移 |
| 执行 batch | 多个相关操作需原子性 |
| 查询状态后做决策 | 避免推理时状态变化 |
### 不需要暂停的场景
| 场景 | 原因 |
|------|------|
| 仅查看 status / resources | 只读操作无副作用 |
| 轮询 events | 事件本身是增量变化的 |
## AI 事件驱动工作流
AI 应持续运行事件守护进程形成"事件 分析 操作 反馈"的闭环

View File

@ -181,6 +181,15 @@ namespace ONIAgentBridge
case ("/api/action/priority_type", "POST"):
responseJson = ExecutePriorityType(ctx);
break;
case ("/api/action/pause", "POST"):
responseJson = ExecutePause(ctx);
break;
case ("/api/action/unpause", "POST"):
responseJson = ExecuteUnpause(ctx);
break;
case ("/api/action/speed", "POST"):
responseJson = ExecuteSpeed(ctx);
break;
default:
ctx.Response.StatusCode = 404;
@ -293,6 +302,9 @@ namespace ONIAgentBridge
PushEvent("state_poll", "info", "Game state polled", $"Cycle {GameClock.Instance?.GetCycle() ?? 0}");
bool isPaused = SpeedControlScreen.Instance?.IsPaused ?? false;
int gameSpeed = isPaused ? 0 : (SpeedControlScreen.Instance?.GetSpeed() ?? 1);
return JsonSerializer.Serialize(new
{
cycle = GameClock.Instance?.GetCycle() ?? 0,
@ -304,6 +316,8 @@ namespace ONIAgentBridge
worldSize = cellCount,
gridWidth = Grid.WidthInCells,
gridHeight = Grid.HeightInCells,
isPaused,
gameSpeed,
eventCount = _eventSeq
});
}
@ -847,7 +861,8 @@ namespace ONIAgentBridge
bool isDiggable = Grid.Solid[cell] && elem != null
&& elem.id != SimHashes.Unobtanium
&& elem.id != SimHashes.Katairite
&& elem.id != SimHashes.Void;
&& elem.id != SimHashes.Void
&& !elem.name.Contains("Neutronium");
// Check dupe safety
bool isSafeForDupe = elem != null
@ -1246,6 +1261,74 @@ namespace ONIAgentBridge
catch (Exception e) { return JsonSerializer.Serialize(FailException(e)); }
}
// ===================================================================
// Pause / Speed Control
// ===================================================================
private string ExecutePause(HttpListenerContext ctx)
{
try
{
var data = ReadBody<PauseRequest>(ctx);
// No body needed, but accept optional { "reason": "..." }
string reason = data?.reason ?? "AI operation in progress";
SpeedControlScreen.Instance?.Pause(false, true);
PushEvent("pause", "info", "Game paused",
$"Game paused by AI: {reason}", "system");
return JsonSerializer.Serialize(ActionOk("game_paused",
new { reason, isPaused = true }));
}
catch (Exception e) { return JsonSerializer.Serialize(FailException(e)); }
}
private string ExecuteUnpause(HttpListenerContext ctx)
{
try
{
var data = ReadBody<UnpauseRequest>(ctx);
int speed = data?.speed ?? 1;
if (speed < 1) speed = 1;
if (speed > 3) speed = 3;
SpeedControlScreen.Instance?.Unpause(true);
SpeedControlScreen.Instance?.SetSpeed(speed);
PushEvent("unpause", "info", "Game resumed",
$"Game resumed by AI at {speed}x speed", "system");
return JsonSerializer.Serialize(ActionOk("game_unpaused",
new { speed, isPaused = false }));
}
catch (Exception e) { return JsonSerializer.Serialize(FailException(e)); }
}
private string ExecuteSpeed(HttpListenerContext ctx)
{
try
{
var data = ReadBody<SpeedRequest>(ctx);
if (data == null)
return JsonSerializer.Serialize(FailInvalid("invalid_request"));
int speed = data.speed;
if (speed < 1) speed = 1;
if (speed > 3) speed = 3;
bool isPaused = SpeedControlScreen.Instance?.IsPaused ?? false;
if (!isPaused)
{
SpeedControlScreen.Instance?.SetSpeed(speed);
}
PushEvent("speed", "info", $"Game speed set to {speed}x",
$"Speed changed to {speed}x", "system");
return JsonSerializer.Serialize(ActionOk("speed_set",
new { speed, isPaused }));
}
catch (Exception e) { return JsonSerializer.Serialize(FailException(e)); }
}
// ===================================================================
// Batch System
// ===================================================================
@ -1578,6 +1661,9 @@ namespace ONIAgentBridge
internal class MopRequest { public int x { get; set; } public int y { get; set; } }
internal class HarvestRequest { public int x { get; set; } public int y { get; set; } }
internal class CancelRequest { public int x { get; set; } public int y { get; set; } }
internal class PauseRequest { public string reason { get; set; } }
internal class UnpauseRequest { public int speed { get; set; } }
internal class SpeedRequest { public int speed { get; set; } }
internal class PriorityGlobalRequest { public string target { get; set; } public int priority { get; set; } }
internal class PriorityTypeRequest { public string buildingType { get; set; } public int priority { get; set; } }

View File

@ -474,6 +474,30 @@ def cmd_priority_type(args):
})
_print_feedback(result)
def cmd_pause(args):
"""Pause the game. Usage: pause [reason]"""
reason = ' '.join(args) if args else 'AI operation in progress'
result = api_post('/api/action/pause', {} if not args else {"reason": reason})
_print_feedback(result)
def cmd_unpause(args):
"""Unpause the game. Usage: unpause [speed]"""
speed = int(args[0]) if args else 1
result = api_post('/api/action/unpause', {"speed": speed})
_print_feedback(result)
def cmd_speed(args):
"""Set game speed. Usage: speed <1|2|3>"""
if not args:
print("Usage: speed <1|2|3>")
return
speed = int(args[0])
if speed < 1 or speed > 3:
print("Speed must be 1, 2, or 3")
return
result = api_post('/api/action/speed', {"speed": speed})
_print_feedback(result)
def _print_feedback(result):
"""Pretty-print action feedback."""
if result.get('success'):
@ -584,6 +608,9 @@ COMMANDS = {
'batch': cmd_batch,
'priority_global': cmd_priority_global,
'priority_type': cmd_priority_type,
'pause': cmd_pause,
'unpause': cmd_unpause,
'speed': cmd_speed,
}
if __name__ == '__main__':
@ -631,6 +658,11 @@ if __name__ == '__main__':
print(" mop <x> <y> Mop liquid")
print(" harvest <x> <y> Harvest plant")
print("")
print("=== Game Speed Control ===")
print(" pause [reason] Pause the game (AI should always pause before ops)")
print(" unpause [speed] Unpause the game at 1x/2x/3x speed")
print(" speed <1|2|3> Set game speed while running")
print("")
print("=== Batch / Priority (Advanced) ===")
print(" batch <json_file> Execute batch plan")
print(" priority_global <t> <p> Set global default priority")