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:
@ -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 |
|
||||
|
||||
## 项目结构
|
||||
|
||||
|
||||
56
SKILL.md
56
SKILL.md
@ -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 应持续运行事件守护进程,形成"事件 → 分析 → 操作 → 反馈"的闭环:
|
||||
|
||||
@ -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; } }
|
||||
|
||||
|
||||
@ -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")
|
||||
|
||||
Reference in New Issue
Block a user