feat: comprehensive AI-oriented data model with cell-level map API and knowledge base
- Add cell/tile map APIs: /api/state/cell, /api/state/cells, /api/state/cells/slice, /api/state/gas - Add entity registry APIs: /api/registry/buildings, /api/registry/elements, /api/registry/techs - Add plants, rooms, mop, harvest endpoints - Rich semantic metadata: element state/category, building category/power, duplicant chore/cell - AI-friendly coordinate system with (x,y) + cell index in all responses - Build AI_KNOWLEDGE_BASE.md with building IDs, element IDs, tech trees, game mechanics - Rewrite SKILL.md with data model explanation, coordinate guide, operation patterns - Update Python tools: explore, cell, cells, slice, gas, registry subcommands - Update MOD_DEV_GUIDE.md with AI data design principles
This commit is contained in:
316
SKILL.md
316
SKILL.md
@ -3,75 +3,283 @@
|
||||
## 职责
|
||||
协助玩家操作和管理游戏"缺氧"(Oxygen Not Included),提供游戏知识、策略建议,并通过 Mod API 直接操控游戏。
|
||||
|
||||
## 工程结构
|
||||
|
||||
```
|
||||
oni-agent/
|
||||
├── config.json # Mod 连接配置
|
||||
├── mod/
|
||||
│ ├── mod_info.yaml # Mod 元信息
|
||||
│ └── ONIAgentBridge.cs # Mod HTTP API 服务 (端口 23876)
|
||||
├── tools/
|
||||
│ ├── oni_api.py # Mod API 客户端
|
||||
│ ├── oni_analyzer.py # 游戏状态分析
|
||||
│ └── oni_builder.py # 蓝图建造规划
|
||||
├── scripts/
|
||||
│ ├── auto_repair.sh # 连接诊断
|
||||
│ ├── auto_analyze.sh # 一键分析
|
||||
│ ├── watch.sh # 持续监控
|
||||
│ └── setup.sh # 环境初始化
|
||||
├── docs/
|
||||
│ ├── MOD_DEV_GUIDE.md # Mod 开发指南
|
||||
│ └── AI_KNOWLEDGE_BASE.md # AI 知识库 (ID注册表/语义标签)
|
||||
├── skills/
|
||||
│ └── oni_agent.md # Agent skill 定义
|
||||
└── SKILL.md # 本文件
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 重要概念:理解 ONI 的数据模型
|
||||
|
||||
### 1. 坐标系
|
||||
|
||||
ONI 使用二维方格(tile)系统。AI 必须理解坐标系才能正确操作:
|
||||
|
||||
```
|
||||
y ▲
|
||||
│ ┌────┬────┬────┐
|
||||
│ │(5,5)│(6,5)│(7,5)│
|
||||
│ ├────┼────┼────┤
|
||||
│ │(5,4)│(6,4)│(7,4)│ ← 这个格子 (6,4) 包含一个电解器
|
||||
│ ├────┼────┼────┤
|
||||
│ │(5,3)│(6,3)│(7,3)│
|
||||
│ └────┴────┴────┘
|
||||
└──────────────────────────► x
|
||||
(0,0)
|
||||
```
|
||||
|
||||
- **原点 (0,0)** 在地图**左下角**
|
||||
- **x 轴**向右增加,**y 轴**向上增加
|
||||
- 每个格子 (cell) 有唯一的 (x, y) 坐标
|
||||
- 建筑占用 w×h 个格子,其坐标是**左下角锚点**
|
||||
- 世界大小通过 `/api/state/game` 查询(`gridWidth` x `gridHeight`)
|
||||
- 典型地图: ~256 x 384 格
|
||||
|
||||
### 2. 理解格子状态
|
||||
|
||||
每格的数据结构如下(通过 `/api/state/cell?x=&y=` 查询):
|
||||
|
||||
```json
|
||||
{
|
||||
"x": 10, "y": 5,
|
||||
"element": "Oxygen", // 该格包含的元素名称
|
||||
"elementState": "gas", // solid/liquid/gas/vacuum
|
||||
"massKg": 1.8, // 该格中元素的质量
|
||||
"temperatureC": 23.5, // 温度(摄氏度)
|
||||
"hasBuilding": true, // 是否有建筑
|
||||
"buildingName": "Electrolyzer",// 建筑名称(如有)
|
||||
"hasDuplicant": false, // 是否有复制人
|
||||
"isVacuum": false, // 是否为真空
|
||||
"isSolid": false, // 是否为固体
|
||||
"isLiquid": false,
|
||||
"isGas": true,
|
||||
"isVisible": true // 是否已探索
|
||||
}
|
||||
```
|
||||
|
||||
### 3. 理解地图区域
|
||||
|
||||
通过 `/api/state/cells?x=&y=&width=&height=` 获取矩形区域的格子数组。
|
||||
|
||||
通过 `explore <x> <y> <w> <h>` 命令获取 AI 友好的结构化摘要:
|
||||
- 该区域的建筑列表(带是否可运行)
|
||||
- 该区域的复制人列表(带压力/当前任务)
|
||||
- 元素分布统计
|
||||
- 感兴趣的关键格子
|
||||
|
||||
---
|
||||
|
||||
## 通信方式
|
||||
|
||||
- Mod 在游戏内启动 HTTP 服务,暴露 RESTful API
|
||||
- 通过 `http://127.0.0.1:PORT` 与游戏通信
|
||||
- 端口在 `oni-agent/config.json` 中配置(默认 23876)
|
||||
|
||||
---
|
||||
|
||||
## 可用 API 端点
|
||||
|
||||
### 状态查询 (GET)
|
||||
| 端点 | 返回内容 |
|
||||
|------|---------|
|
||||
| `/api/state/game` | 周期、复制人数量、全局资源 |
|
||||
| `/api/state/resources` | 所有主要资源存量 |
|
||||
| `/api/state/duplicants` | 所有复制人状态 |
|
||||
| `/api/state/buildings` | 所有建筑列表 |
|
||||
| `/api/state/research` | 科技树进度 |
|
||||
| `/api/state/geysers` | 喷泉状态 |
|
||||
| `/api/state/alert` | 当前警报 |
|
||||
| `/api/state/critters` | 小动物状态 |
|
||||
|
||||
| 端点 | 说明 | 用途 |
|
||||
|------|------|------|
|
||||
| `/health` | Mod 存活检测 | 连接检查 |
|
||||
| `/api/state/game` | 全局状态(周期/人数/世界尺寸) | 总览 |
|
||||
| `/api/state/resources` | 资源列表(含分类/物态) | 物资盘点 |
|
||||
| `/api/state/duplicants` | 复制人详情(位置/压力/食物/当前任务) | 人员管理 |
|
||||
| `/api/state/buildings` | 建筑列表(位置/是否运行/功耗/分类) | 基建评估 |
|
||||
| `/api/state/research` | 科技树(进度/解锁的建筑) | 科研规划 |
|
||||
| `/api/state/geysers` | 喷泉(位置/状态/排放率) | 资源规划 |
|
||||
| `/api/state/alert` | 警报列表 | 紧急处理 |
|
||||
| `/api/state/critters` | 小动物(位置/种类/幸福度/年龄) | 养殖管理 |
|
||||
| `/api/state/plants` | 植物(位置/生长进度/是否枯萎) | 农业管理 |
|
||||
| `/api/state/rooms` | 房间(类型/格数/建筑数) | 房间判定 |
|
||||
|
||||
### 地图/格子数据 (GET)
|
||||
|
||||
| 端点 | 说明 | 示例 |
|
||||
|------|------|------|
|
||||
| `/api/state/cell?x=10&y=5` | 单格详情 | 查看某个格子是气体/液体/建筑 |
|
||||
| `/api/state/cells?x=0&y=0&width=10&height=10` | 矩形区域 | 查看 10x10 区域 |
|
||||
| `/api/state/cells/slice?axis=y&index=20&start=0&end=50` | 行/列扫描 | 查看第 20 行 |
|
||||
| `/api/state/gas?x=10&y=10&radius=20` | 区域气体分析 | 查看周围气体成分 |
|
||||
|
||||
### 实体注册表 (GET,AI 参考用)
|
||||
|
||||
| 端点 | 说明 |
|
||||
|------|------|
|
||||
| `/api/registry/buildings` | 所有建筑 ID 及尺寸/功耗/发热 |
|
||||
| `/api/registry/elements` | 所有元素 ID 及比热容/导热/熔沸点 |
|
||||
| `/api/registry/techs` | 所有科技 ID 及前置/解锁内容 |
|
||||
|
||||
### 操作 (POST)
|
||||
| 端点 | 请求体 |
|
||||
|------|--------|
|
||||
| `/api/action/dig` | `{x, y, width, height}` |
|
||||
| `/api/action/build` | `{buildingId, x, y, rotation?}` |
|
||||
| `/api/action/deconstruct` | `{buildingId, x, y}` |
|
||||
| `/api/action/prioritize` | `{x, y, priority}` |
|
||||
| `/api/action/research` | `{techId}` |
|
||||
| `/api/action/schedule` | `{duplicantId, schedule}` |
|
||||
| `/api/action/wardrobe` | `{duplicantId, equipment}` |
|
||||
|
||||
| 端点 | 请求体 | 用途 |
|
||||
|------|--------|------|
|
||||
| `/api/action/dig` | `{x, y, width, height}` | 挖掘区域 |
|
||||
| `/api/action/build` | `{buildingId, x, y}` | 建造建筑 |
|
||||
| `/api/action/deconstruct` | `{buildingId, x, y}` | 拆除建筑 |
|
||||
| `/api/action/prioritize` | `{x, y, priority}` | 设优先级 |
|
||||
| `/api/action/research` | `{techId}` | 选研究项目 |
|
||||
| `/api/action/mop` | `{x, y}` | 清理液体 |
|
||||
| `/api/action/harvest` | `{x, y}` | 收获植物 |
|
||||
|
||||
---
|
||||
|
||||
## AI 如何进行推理和操作
|
||||
|
||||
### 第一步:获取全局上下文
|
||||
|
||||
```bash
|
||||
python3 tools/oni_api.py status
|
||||
python3 tools/oni_api.py buildings
|
||||
python3 tools/oni_analyzer.py
|
||||
```
|
||||
|
||||
### 第二步:理解地图
|
||||
|
||||
```bash
|
||||
# 探索基地中心区域(假设基地在 50,50)
|
||||
python3 tools/oni_api.py explore 40 40 40 30
|
||||
|
||||
# 检查某个格子的详细信息
|
||||
python3 tools/oni_api.py cell 45 48
|
||||
|
||||
# 查看气体分布
|
||||
python3 tools/oni_api.py gas 50 50 30
|
||||
```
|
||||
|
||||
### 第三步:参考知识库
|
||||
|
||||
```bash
|
||||
# 查找某个建筑的 ID
|
||||
python3 tools/oni_api.py registry buildings Electrolyzer
|
||||
|
||||
# 查看元素属性
|
||||
python3 tools/oni_api.py registry elements Water
|
||||
|
||||
# 查看科技树
|
||||
python3 tools/oni_api.py registry techs
|
||||
```
|
||||
|
||||
### 第四步:执行操作
|
||||
|
||||
```bash
|
||||
# 建造电解器
|
||||
python3 tools/oni_api.py build Electrolyzer 45 48
|
||||
|
||||
# 挖掘空间
|
||||
python3 tools/oni_api.py dig 40 45 8 6
|
||||
|
||||
# 选择科研方向
|
||||
python3 tools/oni_api.py research_select ImprovedOxygen
|
||||
|
||||
# 使用蓝图
|
||||
python3 tools/oni_builder.py build spom 42 42
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## AI 如何理解常见游戏场景
|
||||
|
||||
### 场景 1:氧气不足
|
||||
|
||||
**AI 推理过程:**
|
||||
1. 检查 `/api/state/resources` 中的 O2 和 Algae 存量
|
||||
2. 检查 `/api/state/buildings` 是否有电解器或氧气扩散器
|
||||
3. 检查 `/api/state/cell?x=&y=` 查询基地气体分布
|
||||
4. 如果 Algae < 1t 且无电解器 → 建议建造 SPOM
|
||||
5. SPOM 需要:水源 + 电解器 + 气体泵 + 氢气发电机 + 气体过滤器
|
||||
6. 通过 `explore` 找到一个 8x6 的空地
|
||||
7. 执行 `build Electrolyzer x y` + `build GasPump ...` + `build HydrogenGenerator ...`
|
||||
|
||||
### 场景 2:食物短缺
|
||||
|
||||
**AI 推理过程:**
|
||||
1. 检查 Calories < 500,000 kcal → 食物预警
|
||||
2. 检查是否有 PlanterBox/FarmTile 和 ElectricGrill
|
||||
3. 如果没有农场 → 建议建造 5 个 PlanterBox 种 Mealwood
|
||||
4. Mealwood 不需要灌溉或施肥,只需 Dirt
|
||||
5. 检查 Dirt 存量,如果足够 → 执行建造
|
||||
6. 如果有污水 → 建议建造 Water Sieve + 厕所水循环
|
||||
|
||||
### 场景 3:温度过高
|
||||
|
||||
**AI 推理过程:**
|
||||
1. 检查温度数据(通过资源中的 Temperature 或格子数据)
|
||||
2. 查看热源(煤发电机、精炼厂等靠近基地的位置)
|
||||
3. 建议:用隔热门包围热源 + 建造液冷模块
|
||||
4. 液冷模块需要:Aquatuner + SteamTurbine + 导热液体管道
|
||||
|
||||
---
|
||||
|
||||
## AI 如何表达"在哪个格子做什么"
|
||||
|
||||
### 定位语法
|
||||
|
||||
AI 在描述操作时应使用以下格式:
|
||||
|
||||
```
|
||||
在坐标 (x, y) 建造 <buildingId>
|
||||
在区域 (x, y, width, height) 进行挖掘
|
||||
从 (x1,y1) 到 (x2,y2) 铺设管道/电线
|
||||
在格子 (x, y) 设置优先级为 <priority>
|
||||
```
|
||||
|
||||
### 坐标查找策略
|
||||
|
||||
当 AI 不确定在哪里建造时:
|
||||
1. 先用 `explore` 找一个空闲区域(没有建筑和固体阻挡)
|
||||
2. 检查空闲区域的元素和温度是否适合
|
||||
3. 用 `cell` 命令确认目标格子状态
|
||||
4. 然后用 `dig` 清理空间
|
||||
5. 最后用 `build` 建造
|
||||
|
||||
### 建筑放置规则
|
||||
|
||||
- 建筑坐标是其**左下角**的位置
|
||||
- 建筑占用的 w×h 区域必须全部是空地
|
||||
- 需要确认目标区域无建筑、无固体自然方块
|
||||
- 气体/液体不会阻挡建筑
|
||||
- 如果建筑需要特定环境(如电解器需要水),AI 需要先检查环境
|
||||
|
||||
---
|
||||
|
||||
## 工具列表
|
||||
|
||||
| 工具 | 用途 |
|
||||
|------|------|
|
||||
| `tools/oni_api.py` | 与 Mod HTTP API 通信 |
|
||||
| `tools/oni_analyzer.py` | 分析游戏状态、生成建议 |
|
||||
| `tools/oni_builder.py` | 蓝图/建造规划 |
|
||||
| `scripts/auto_repair.sh` | 诊断连接问题 |
|
||||
| `scripts/auto_analyze.sh` | 一键拉取状态+分析 |
|
||||
| `scripts/watch.sh` | 持续监控模式 |
|
||||
| `scripts/setup.sh` | 环境初始化 |
|
||||
| `tools/oni_api.py` | 与 Mod HTTP API 通信(所有查询/操作) |
|
||||
| `tools/oni_analyzer.py` | 自动分析游戏状态、生成预警和建议 |
|
||||
| `tools/oni_builder.py` | 预置蓝图建造(SPOM/农场/养殖等) |
|
||||
| `scripts/auto_repair.sh` | 诊断 Mod 连接问题 |
|
||||
| `scripts/auto_analyze.sh` | 一键健康检查+状态+分析 |
|
||||
| `scripts/watch.sh [秒]` | 循环监控模式 |
|
||||
| `scripts/setup.sh` | 环境初始化与检查 |
|
||||
| `docs/AI_KNOWLEDGE_BASE.md` | 建筑/元素/科技 ID 注册表和游戏机制参考 |
|
||||
|
||||
## 使用方式
|
||||
|
||||
```bash
|
||||
# 检查游戏连接
|
||||
python3 tools/oni_api.py health
|
||||
|
||||
# 获取游戏状态概览
|
||||
python3 tools/oni_api.py status
|
||||
|
||||
# 列出所有资源
|
||||
python3 tools/oni_api.py resources
|
||||
|
||||
# 分析并生成建议
|
||||
python3 tools/oni_analyzer.py
|
||||
|
||||
# 查看可用蓝图
|
||||
python3 tools/oni_builder.py list
|
||||
|
||||
# 执行建造计划
|
||||
python3 tools/oni_builder.py build spom 15 10
|
||||
|
||||
# 持续监控(每 60 秒)
|
||||
bash scripts/watch.sh 60
|
||||
|
||||
# 诊断连接
|
||||
bash scripts/auto_repair.sh
|
||||
```
|
||||
---
|
||||
|
||||
## 核心游戏知识
|
||||
|
||||
@ -83,7 +291,7 @@ bash scripts/auto_repair.sh
|
||||
5. **水资源管理** — 净水器、污水过滤
|
||||
|
||||
### 常用布局
|
||||
- SPOM (Self-Powered Oxygen Module): 电解制氧 + 氢气发电闭环
|
||||
- SPOM: 电解制氧 + 氢气发电闭环
|
||||
- 卫生间水循环: 卫生间 → 净水器 → 卫生间
|
||||
- 冷却系统: 液冷 + 蒸汽机 + 导热管
|
||||
- Ranch 模块: 养殖哈奇/滑鳞/飞鱼
|
||||
|
||||
268
docs/AI_KNOWLEDGE_BASE.md
Normal file
268
docs/AI_KNOWLEDGE_BASE.md
Normal file
@ -0,0 +1,268 @@
|
||||
# ONI Knowledge Base — AI Reference
|
||||
|
||||
This file aggregates all building IDs, element IDs, tech IDs, and their semantic metadata
|
||||
in one structured document. The AI uses this to understand what each entity means and
|
||||
how to reason about the game.
|
||||
|
||||
## Coordinate System
|
||||
|
||||
ONI uses a 2D grid. The origin (0,0) is at the **bottom-left** of the map.
|
||||
- x increases to the right
|
||||
- y increases upward
|
||||
- Each cell is 1x1 tile
|
||||
- Buildings may occupy multiple cells (width x height); the (x,y) is the **bottom-left anchor**
|
||||
- World size varies by asteroid (default ~256x384 cells)
|
||||
|
||||
When the AI wants to reference a location:
|
||||
- Use absolute (x,y) coordinates
|
||||
- For regions: (x, y, width, height)
|
||||
- For buildings: reference the bottom-left cell of the building
|
||||
- For movement: "at (x,y)" or "from (x1,y1) to (x2,y2)"
|
||||
|
||||
## Building Categories
|
||||
|
||||
| Category | Purpose | Examples |
|
||||
|------------|----------------------------------------|---------------------------------------------------|
|
||||
| Base | Structure, storage, doors, ladders | Tile, Ladder, StorageBin, InsulatedTile, Door |
|
||||
| Oxygen | Oxygen production, gas cleaning | Electrolyzer, OxygenDiffuser, Deodorizer |
|
||||
| Power | Power generation, storage, wiring | CoalGenerator, HydrogenGenerator, Battery |
|
||||
| Food | Food production, cooking, ranching | PlanterBox, ElectricGrill, MicrobeMusher, Ranch |
|
||||
| Plumbing | Liquid handling, hygiene | LiquidPump, Lavatory, WaterSiever, Shower |
|
||||
| Ventilation| Gas handling | GasPump, GasFilter, GasVent |
|
||||
| Refinement | Material processing | MetalRefinery, RockCrusher, Kiln, Compost |
|
||||
| Medicine | Health, disease treatment | Apothecary, MassageTable, SickBay |
|
||||
| Furniture | Decor, morale, stress relief | Cot, MessTable, ArcadeCabinet, FlowerPot |
|
||||
| Stations | Research, suit docks, crafting | ResearchStation, SuperComputer, AtmoSuitDock |
|
||||
| Utilities | Temperature management | ThermoAquatuner, SpaceHeater, TempshiftPlate |
|
||||
| Automation | Logic circuits, sensors | AND Gate, AtmoSensor, AutomationWire |
|
||||
| Shipping | Conveyor systems | ConveyorLoader, ConveyorRail, SolidFilter |
|
||||
|
||||
## Key Building IDs (frequently used by AI)
|
||||
|
||||
```
|
||||
# Oxygen production
|
||||
Electrolyzer -> Produces O2 + H2 from water (needs 1kg/s water)
|
||||
OxygenDiffuser -> Produces O2 from algae (early game)
|
||||
Deodorizer -> Converts PollutedOxygen to Oxygen (needs filtration medium)
|
||||
|
||||
# Power
|
||||
ManualGenerator -> 400W, duplicant powered
|
||||
CoalGenerator -> 600W, consumes Coal
|
||||
HydrogenGenerator -> 800W, consumes H2
|
||||
NaturalGasGenerator-> 800W, consumes NaturalGas
|
||||
PetroleumGenerator -> 2000W, consumes Petroleum
|
||||
SteamTurbine -> Extracts heat from steam, produces power
|
||||
SolarPanel -> 380W max, needs light
|
||||
WoodBurner -> 300W, consumes Lumber
|
||||
|
||||
# Power storage
|
||||
Battery -> 10kJ storage, small
|
||||
JumboBattery -> 40kJ storage
|
||||
SmartBattery -> 20kJ storage, automation output
|
||||
|
||||
# Food
|
||||
PlanterBox -> Grows plants, needs dupe delivery
|
||||
FarmTile -> Grows plants with irrigation
|
||||
HydroponicFarm -> Grows plants with automatic irrigation
|
||||
ElectricGrill -> Cooks food (better quality)
|
||||
MicrobeMusher -> Makes basic mush bars from water + dirt
|
||||
GasRange -> Advanced cooking with gas
|
||||
Refrigerator -> Stores food, slows decay
|
||||
|
||||
# Plumbing
|
||||
LiquidPump -> Pumps liquids (240kg/s)
|
||||
Lavatory -> Produces PollutedWater from dupe use
|
||||
WaterSiever -> Filters PollutedWater -> Water
|
||||
Desalinator -> Removes salt from SaltWater/Brine
|
||||
LiquidVent -> Outputs liquid into world
|
||||
|
||||
# Ventilation
|
||||
GasPump -> Pumps gases (500g/s)
|
||||
GasFilter -> Filters specific gas from mixed pipes
|
||||
GasVent -> Outputs gas into world
|
||||
HighPressureGasVent-> 20kg/tile max pressure
|
||||
|
||||
# Refinement
|
||||
MetalRefinery -> Refines metal ores into refined metals
|
||||
RockCrusher -> Crushes rock into sand/power
|
||||
Kiln -> Burns clay -> ceramic
|
||||
GlassForge -> Makes glass from sand
|
||||
Compost -> Converts polluted dirt into dirt
|
||||
|
||||
# Base
|
||||
Tile -> Standard tile, 2 tiles high
|
||||
Ladder -> Allows vertical movement
|
||||
InsulatedTile -> Reduces heat transfer (best insulation)
|
||||
ManualAirlock -> Manual door
|
||||
PneumaticDoor -> Auto door, lets gas pass
|
||||
MechanizedAirlock -> Auto door, seals gas/liquid
|
||||
StorageBin -> Stores solid resources
|
||||
LiquidReservoir -> Stores 5t of liquid
|
||||
GasReservoir -> Stores 150kg of gas
|
||||
|
||||
# Temperature
|
||||
ThermoAquatuner -> Cools liquid piped through it (needs steam room)
|
||||
ThermoRegulator -> Cools gas piped through it
|
||||
SpaceHeater -> Heats area (inefficient)
|
||||
LiquidTepidizer -> Heats liquid (up to 85C)
|
||||
TempshiftPlate -> Distributes heat evenly
|
||||
|
||||
# Stations
|
||||
ResearchStation -> Basic research (needs dirt)
|
||||
SuperComputer -> Advanced research (needs plastic)
|
||||
AtmoSuitDock -> Stores atmo suit
|
||||
GroomingStation -> Grooms critters for happiness/eggs
|
||||
RanchStation -> Ranching skill station
|
||||
FarmStation -> Improves farm yield (needs fertilizer)
|
||||
PowerControlStation-> Improves generator efficiency
|
||||
|
||||
# Medicine
|
||||
MassageTable -> Reduces stress
|
||||
SickBay -> Cures diseases
|
||||
TriageCot -> Heals physical damage
|
||||
Apothecary -> Produces medicine
|
||||
|
||||
# Automation (common items)
|
||||
AtmoSensor -> Senses gas pressure
|
||||
ThermoSensor -> Senses temperature
|
||||
HydroSensor -> Senses liquid pressure
|
||||
GasElementSensor -> Senses specific gas type
|
||||
AND Gate, NOT Gate, FILTER Gate, BUFFER Gate
|
||||
```
|
||||
|
||||
## Key Element IDs (resources)
|
||||
|
||||
```
|
||||
# Critical survival
|
||||
Oxygen -> Breathable gas (needed by dupes)
|
||||
Water -> Essential for farming, electrolysis, life support
|
||||
Dirt -> Used in research (early) and farming
|
||||
Algae -> Consumed by OxygenDiffuser
|
||||
|
||||
# Food chain
|
||||
Calories -> Aggregate food energy for colony
|
||||
MealLice -> Basic food from mealwood plants
|
||||
BristleBerry -> Mid-tier food from bristle blossom
|
||||
Mushroom -> Food from dusk cap (needs CO2 + slime)
|
||||
RawEgg -> Egg for cooking
|
||||
Meat -> From ranching
|
||||
|
||||
# Power chain
|
||||
Coal -> Burned in CoalGenerator
|
||||
Hydrogen -> Burned in HydrogenGenerator
|
||||
NaturalGas -> Burned in NaturalGasGenerator
|
||||
Petroleum -> Burned in PetroleumGenerator
|
||||
CrudeOil -> Can be refined to Petroleum
|
||||
|
||||
# Construction metals
|
||||
CopperOre -> Early building material
|
||||
IronOre -> Mid building material
|
||||
GoldAmalgam -> High corrosion resistance
|
||||
Wolframite -> Very high melting point (tungsten source)
|
||||
Steel -> Strong, high melting point (refined)
|
||||
RefinedIron -> Mid refined metal
|
||||
Plastic -> Advanced material (from polymer press or dreckos)
|
||||
Ceramic -> Best insulation material
|
||||
|
||||
# Water / liquids
|
||||
SaltWater -> Can be desalinated to Water
|
||||
Brine -> Can be desalinated (more salt per water)
|
||||
PollutedWater -> Can be filtered to Water (also used by reed fiber)
|
||||
CrudeOil -> Pumped from oil biome
|
||||
Petroleum -> Refined from oil (cooking or refinery)
|
||||
Lumber -> From arbor trees, burned in wood burner
|
||||
Ethanol -> From lumber, burned in petroleum generator
|
||||
|
||||
# Gas handling
|
||||
PollutedOxygen -> Can be deodorized to Oxygen
|
||||
CarbonDioxide -> Sinks to bottom, used by mushrooms/soda fountain
|
||||
ChlorineGas -> Disinfects, used by balm lily
|
||||
SourGas -> Can be cooled to methane + sulfur
|
||||
|
||||
# Rare / advanced
|
||||
Niobium -> End-game space material
|
||||
Thermium -> Best heat conductor (space material)
|
||||
Isoresin -> Used for insulation (space material)
|
||||
ViscoGel -> Non-mixing liquid (space material)
|
||||
Radium -> Radioactive, spaced out DLC
|
||||
```
|
||||
|
||||
## Research Tech Tree (simplified path)
|
||||
|
||||
```
|
||||
Tier 0 (start): FarmingTech, PowerRegulation, Plumbing, Ventilation
|
||||
Tier 1: FoodPreparation (-> ElectricGrill, MicrobeMusher)
|
||||
InteriorDecor (-> Cot, FlowerPot, various decor)
|
||||
AdvancedPowerRegulation (-> SmartBattery, PowerTransformer)
|
||||
LiquidPiping (-> LiquidPump, WaterSiever, Desalinator)
|
||||
GasPiping (-> GasPump, GasFilter)
|
||||
FineDining (-> GasRange, Refrigerator)
|
||||
Tier 2: ImprovedOxygen (-> Electrolyzer)
|
||||
RefinedObjects (-> MetalRefinery, RockCrusher)
|
||||
TemperatureModulation (-> ThermoAquatuner, ThermoRegulator)
|
||||
Ranching (-> GroomingStation, Incubator)
|
||||
SmartStorage (-> LiquidReservoir, GasReservoir)
|
||||
Automation (-> first automation buildings)
|
||||
Tier 3: HighTemperatureForging (-> Steel)
|
||||
SpaceProgram (-> telescope, rocket platform)
|
||||
CryoFuelPropulsion (-> hydrogen rocket engine)
|
||||
Tier 4: MaterialsScience (-> SuperCoolant, ViscoGel)
|
||||
```
|
||||
|
||||
## Building Size Reference (width x height)
|
||||
|
||||
```
|
||||
1x1: Wire, GasPipe, LiquidPipe, AutomationWire, Switch, Sensor
|
||||
1x2: ManualGenerator, Battery, PlanterBox, FarmTile, RationBox
|
||||
2x1: GasFilter, LiquidFilter, Incubator, Jukebot
|
||||
2x2: CoalGenerator, HydrogenGenerator, OxygenDiffuser, Electrolyzer,
|
||||
GasPump, LiquidPump, AlgaeTerrarium, Deodorizer, WaterSiever,
|
||||
ElectricGrill, MicrobeMusher, RockCrusher, Kiln, Compost,
|
||||
MassageTable, Apothecary, ResearchStation, TextileLoom
|
||||
2x3: MetalRefinery, PolymerPress, OilRefinery, AtmoSuitDock
|
||||
2x4: Telescope, MolecularForge
|
||||
3x1: LadderBed, DisplayShelf
|
||||
3x2: ManualAirlock, PneumaticDoor, MechanizedAirlock, ConveyorLoader,
|
||||
SmartStorageBin, LiquidReservoir, GasReservoir
|
||||
3x3: Shower, Sauna, HotTub
|
||||
3x4: SteamTurbine
|
||||
4x2: SuperComputer, JumboBattery, StorageBin
|
||||
4x4: SolarPanel
|
||||
```
|
||||
|
||||
## Common Game Mechanics (AI reference)
|
||||
|
||||
### Gas/Liquid Physics
|
||||
- Gases **layer by density**: CO2 (sinks) < O2 < PollutedO2 < NaturalGas < H2 (rises)
|
||||
- Liquids **layer by density**: Petroleum < Water/SaltWater/Brine < CrudeOil < Mercury
|
||||
- Pressure limit for gas vents: GasVent = 2kg/tile, HighPressureGasVent = 20kg/tile
|
||||
- Buildings overheat above their overheat temperature (default 75C, some higher)
|
||||
- Insulated Tile reduces heat transfer by ~100x
|
||||
|
||||
### Duplicant Needs
|
||||
- Oxygen: need ~100g/s per dupe
|
||||
- Food: need ~1000kcal/cycle per dupe
|
||||
- Stress: > 50% starts causing problems, > 80% mental breaks
|
||||
- Temperature: comfortable at 18-35C, hypothermia below 10C, hyperthermia above 40C
|
||||
- Morale: affected by decor, food quality, room bonuses
|
||||
|
||||
### Room Types
|
||||
- Latrine: Outhouse + WashBasin = +1 morale
|
||||
- Washroom: Lavatory + Sink = +2 morale
|
||||
- Barracks: Cot/LadderBed + 1 decor = +1 morale
|
||||
- Bedroom: ComfyBed + 1 decor = +2 morale
|
||||
- Mess Hall: MessTable + 1 decor = +3 morale
|
||||
- Great Hall: MessTable + 2+ decor and recreation = +6 morale
|
||||
- Park: 4+ wild/planted plants = +1 morale
|
||||
- Nature Reserve: 12+ wild plants = +6 morale
|
||||
- Stable: GroomingStation + CritterDropOff = ranching room
|
||||
- Ranch: Incubator + feeder (spaced out)
|
||||
|
||||
### SPOM (Self-Powered Oxygen Module)
|
||||
The classic Rodriguez SPOM:
|
||||
- 1 Electrolyzer (consumes 1kg/s water -> 888g/s O2 + 112g/s H2)
|
||||
- 2 GasPumps for O2 extraction
|
||||
- 1 HydrogenGenerator (burns 100g/s H2, produces 800W)
|
||||
- 1 GasFilter to separate H2 from O2
|
||||
- Net power positive (runs on its own hydrogen)
|
||||
- Standard size: 8x6 tiles
|
||||
@ -52,16 +52,98 @@ GET /health
|
||||
|
||||
所有状态查询为 `GET` 请求,路径前缀 `/api/state/`。
|
||||
|
||||
| 端点 | 返回内容 | 必需 |
|
||||
|------|---------|------|
|
||||
| `/api/state/game` | 周期、复制人数量、世界名称 | **是** |
|
||||
| `/api/state/resources` | 所有主要资源存量列表 | **是** |
|
||||
| `/api/state/duplicants` | 每个复制人的压力/食物/体力/氧气 | **是** |
|
||||
| `/api/state/buildings` | 已建造的建筑列表 | 推荐 |
|
||||
| `/api/state/research` | 科技树进度 | 推荐 |
|
||||
| `/api/state/geysers` | 喷泉位置和状态 | 可选 |
|
||||
| `/api/state/alert` | 当前游戏警报 | 推荐 |
|
||||
| `/api/state/critters` | 小动物状态 | 可选 |
|
||||
| 端点 | 返回内容 | 必需 | 新增字段 |
|
||||
|------|---------|------|---------|
|
||||
| `/api/state/game` | 周期、复制人、世界尺寸 | **是** | `gridWidth`, `gridHeight` |
|
||||
| `/api/state/resources` | 资源列表(含分类/物态) | **是** | `id`, `state`, `category` |
|
||||
| `/api/state/duplicants` | 复制人详情 | **是** | `x`, `y`, `cell`, `currentChore` |
|
||||
| `/api/state/buildings` | 建筑列表 | 推荐 | `cell`, `category`, `powerWatt` |
|
||||
| `/api/state/research` | 科技树(含解锁列表) | 推荐 | `requiredTechs`, `unlockedBuildings` |
|
||||
| `/api/state/geysers` | 喷泉详情 | 可选 | `cell`, `isActive`, `isDormant` |
|
||||
| `/api/state/alert` | 当前警报 | 推荐 | `clickable` |
|
||||
| `/api/state/critters` | 小动物详情 | 可选 | `cell`, `calories` |
|
||||
| `/api/state/plants` | 植物列表 | 可选 | `isGrown`, `progress`, `isWilting` |
|
||||
| `/api/state/rooms` | 房间列表 | 可选 | 类型/格数/建筑/生物/植物 |
|
||||
|
||||
### 2.3 地图/格子数据端点
|
||||
|
||||
这是 AI 理解游戏世界最重要的端点。格子级数据让 AI 知道每个具体位置的状态。
|
||||
|
||||
| 端点 | 说明 | 参数 |
|
||||
|------|------|------|
|
||||
| `GET /api/state/cell` | 单格详情 | `?x=&y=` |
|
||||
| `GET /api/state/cells` | 矩形区域(批量) | `?x=&y=&width=&height=` |
|
||||
| `GET /api/state/cells/slice` | 行或列扫描 | `?axis=x&index=&start=&end=` |
|
||||
| `GET /api/state/gas` | 气体分布分析 | `?x=&y=&radius=` |
|
||||
|
||||
#### `GET /api/state/cell?x=10&y=5`
|
||||
|
||||
```json
|
||||
{
|
||||
"x": 10, "y": 5, "cell": 4523,
|
||||
"element": "Oxygen",
|
||||
"elementId": "Oxygen",
|
||||
"elementState": "gas",
|
||||
"massKg": 1.8,
|
||||
"temperatureC": 23.5,
|
||||
"isSolid": false, "isLiquid": false, "isGas": true,
|
||||
"hasBuilding": true, "buildingName": "GasPump",
|
||||
"hasDuplicant": false, "duplicantName": null,
|
||||
"isVacuum": false, "isVisible": true
|
||||
}
|
||||
```
|
||||
|
||||
#### `GET /api/state/cells?x=0&y=0&width=5&height=5`
|
||||
|
||||
```json
|
||||
{
|
||||
"region": { "x": 0, "y": 0, "width": 5, "height": 5 },
|
||||
"cells": [ /* array of cell objects */ ]
|
||||
}
|
||||
```
|
||||
|
||||
### 2.4 实体注册表端点 (AI 参考)
|
||||
|
||||
用于 AI 在运行时查询游戏实体的元数据。所有为 `GET` 请求。
|
||||
|
||||
| 端点 | 返回内容 |
|
||||
|------|---------|
|
||||
| `/api/registry/buildings` | 全部建筑定义(尺寸/功耗/发热/材料) |
|
||||
| `/api/registry/elements` | 全部元素定义(比热容/导热/熔沸点) |
|
||||
| `/api/registry/techs` | 全部科技定义(前置/解锁建筑) |
|
||||
|
||||
#### `GET /api/registry/buildings`
|
||||
|
||||
```json
|
||||
[
|
||||
{
|
||||
"id": "Electrolyzer",
|
||||
"name": "Electrolyzer",
|
||||
"category": "Oxygen",
|
||||
"width": 2, "height": 2,
|
||||
"powerCost": 120,
|
||||
"heatGeneration": 1.25,
|
||||
"constructionMass": ["IronOre", "IronOre"]
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
### 2.5 操作端点
|
||||
|
||||
所有操作为 `POST` 请求,路径前缀 `/api/action/`。
|
||||
|
||||
| 端点 | 作用 | 必需 |
|
||||
|------|------|------|
|
||||
| `/api/action/dig` | 挖掘指定区域 | **是** |
|
||||
| `/api/action/build` | 建造建筑 | **是** |
|
||||
| `/api/action/deconstruct` | 拆除建筑 | 推荐 |
|
||||
| `/api/action/prioritize` | 设置优先级 | 可选 |
|
||||
| `/api/action/research` | 选择研究方向 | 推荐 |
|
||||
| `/api/action/schedule` | 修改复制人日程 | 可选 |
|
||||
| `/api/action/wardrobe` | 修改复制人装备 | 可选 |
|
||||
| `/api/action/mop` | 清理液体 | 推荐 |
|
||||
| `/api/action/harvest` | 收获植物 | 推荐 |
|
||||
| `/api/action/cancel` | 取消操作 | 可选 |
|
||||
|
||||
#### `GET /api/state/game`
|
||||
|
||||
@ -205,7 +287,39 @@ Agent 侧通过 `config.json` 定位 Mod:
|
||||
|
||||
---
|
||||
|
||||
## 5. 扩展建议
|
||||
## 5. AI-Friendly 数据设计原则
|
||||
|
||||
### 5.1 数据可读性
|
||||
所有暴露的数据应满足 AI 可直接理解的三个条件:
|
||||
1. **命名语义化** — 使用自然语言字段名(如 `temperatureC` 而非 `tempK`)
|
||||
2. **数据类型合理** — 使用数字而非枚举字符串(温度用 `float` 而非 `string`)
|
||||
3. **上下文完整** — 每个实体包含足够的位置和状态信息(坐标、类别、是否可运行)
|
||||
|
||||
### 5.2 坐标系统
|
||||
- 统一使用 `(x, y)` 整数坐标,对应游戏网格
|
||||
- 原点在左下角:`(0, 0)`
|
||||
- 建筑使用其左下角锚点坐标
|
||||
- 返回数据中同时提供 `cell` 索引(Grid 内部使用)和 `(x, y)` 坐标
|
||||
|
||||
### 5.3 实体分类
|
||||
每个实体(建筑、元素、科技)必须包含分类标签:
|
||||
- 建筑: `category`(Base/Oxygen/Power/Food/...)
|
||||
- 元素: `state`(solid/liquid/gas)+ `category`(metal/water/fuel/...)
|
||||
- 资源: 带 `state` 和 `category` 帮助 AI 推理用途
|
||||
|
||||
### 5.4 AI 推理辅助
|
||||
- `/api/registry/*` 端点提供完整的实体元数据查询
|
||||
- `GET /api/state/gas` 提供区域气体分布统计(AI 无法逐格遍历)
|
||||
- `/api/state/rooms` 提供房间判定结果(AI 无法自行判断房间类型)
|
||||
|
||||
### 5.5 操作设计
|
||||
操作设计遵循以下原则:
|
||||
- **幂等性** — 同个操作重复执行不产生副作用
|
||||
- **非阻塞** — 操作立即返回 `queued`,异步执行
|
||||
- **确定性** — 使用绝对坐标 `(x, y)`,不支持"在某个建筑旁边"这类模糊表述
|
||||
- **输入校验** — 拒绝未知的 `buildingId` 或越界坐标
|
||||
|
||||
## 6. 扩展建议
|
||||
|
||||
### 5.1 添加新端点
|
||||
1. 在 `ProcessRequest` 中添加路由匹配
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@ -1,6 +1,6 @@
|
||||
import json
|
||||
import sys
|
||||
from oni_api import api_get, api_post
|
||||
from oni_api import api_get
|
||||
|
||||
|
||||
def get_game_state():
|
||||
@ -13,6 +13,8 @@ def get_game_state():
|
||||
'geysers': api_get('/api/state/geysers'),
|
||||
'alerts': api_get('/api/state/alert'),
|
||||
'critters': api_get('/api/state/critters'),
|
||||
'plants': api_get('/api/state/plants'),
|
||||
'rooms': api_get('/api/state/rooms'),
|
||||
}
|
||||
|
||||
|
||||
@ -22,22 +24,36 @@ def as_dict(resources):
|
||||
return {r.get('name'): r for r in resources}
|
||||
|
||||
|
||||
def analyze_o2(resources):
|
||||
def buildings_by_cat(buildings):
|
||||
cats = {}
|
||||
for b in (buildings or []):
|
||||
cat = b.get('category', 'Other')
|
||||
cats.setdefault(cat, []).append(b)
|
||||
return cats
|
||||
|
||||
|
||||
def analyze_o2(resources, buildings):
|
||||
r = as_dict(resources)
|
||||
o2 = r.get('Oxygen', {}).get('amount', 0)
|
||||
algae = r.get('Algae', {}).get('amount', 0)
|
||||
pw = r.get('PollutedWater', {}).get('amount', 0)
|
||||
|
||||
has_electrolyzer = any(b.get('id') == 'Electrolyzer' for b in (buildings or []))
|
||||
has_diffuser = any(b.get('id') == 'OxygenDiffuser' for b in (buildings or []))
|
||||
|
||||
warnings = []
|
||||
if o2 < 100:
|
||||
warnings.append(("CRITICAL", f"Oxygen critically low ({o2:.0f} kg)"))
|
||||
warnings.append(("CRITICAL", f"Oxygen critically low ({o2:.0f} kg)!"))
|
||||
elif o2 < 500:
|
||||
warnings.append(("WARN", f"Oxygen declining ({o2:.0f} kg)"))
|
||||
|
||||
if o2 < 1000 and not has_electrolyzer and not has_diffuser:
|
||||
warnings.append(("CRITICAL", "No oxygen production buildings found! Build OxygenDiffuser or Electrolyzer"))
|
||||
|
||||
if algae < 1000:
|
||||
warnings.append(("WARN", f"Algae running out ({algae:.0f} kg) — build electrolyzer"))
|
||||
elif algae < 5000:
|
||||
warnings.append(("INFO", f"Algae moderate ({algae:.0f} kg) — plan SPOM"))
|
||||
elif algae < 5000 and not has_electrolyzer:
|
||||
warnings.append(("INFO", f"Algae moderate ({algae:.0f} kg) — plan SPOM transition"))
|
||||
|
||||
if pw > 50000:
|
||||
warnings.append(("INFO", f"Polluted water abundant ({pw:.0f} kg) — use for reed fiber / pincha pepper"))
|
||||
@ -45,33 +61,54 @@ def analyze_o2(resources):
|
||||
return warnings
|
||||
|
||||
|
||||
def analyze_food(resources):
|
||||
def analyze_food(resources, buildings):
|
||||
r = as_dict(resources)
|
||||
cal = r.get('Calories', {}).get('amount', 0)
|
||||
|
||||
has_farm = any(b.get('id') in ('PlanterBox', 'FarmTile') for b in (buildings or []))
|
||||
has_grill = any(b.get('id') == 'ElectricGrill' for b in (buildings or []))
|
||||
|
||||
warnings = []
|
||||
if cal < 100000:
|
||||
warnings.append(("CRITICAL", f"Food shortage ({cal:.0f} kcal)"))
|
||||
warnings.append(("CRITICAL", f"Food shortage ({cal:.0f} kcal)!"))
|
||||
elif cal < 500000:
|
||||
warnings.append(("WARN", f"Food declining ({cal:.0f} kcal)"))
|
||||
elif cal > 2000000:
|
||||
warnings.append(("INFO", f"Food surplus ({cal:.0f} kcal) — consider more dupes"))
|
||||
warnings.append(("INFO", f"Food surplus ({cal:.0f} kcal)"))
|
||||
|
||||
if cal < 500000 and not has_farm:
|
||||
warnings.append(("WARN", "No farm plots found. Build PlanterBox and plant Mealwood"))
|
||||
if cal < 500000 and not has_grill:
|
||||
warnings.append(("INFO", "Build ElectricGrill to improve food quality"))
|
||||
return warnings
|
||||
|
||||
|
||||
def analyze_power(resources):
|
||||
def analyze_power(resources, buildings):
|
||||
r = as_dict(resources)
|
||||
coal = r.get('Coal', {}).get('amount', 0)
|
||||
hydrogen = r.get('Hydrogen', {}).get('amount', 0)
|
||||
natgas = r.get('NaturalGas', {}).get('amount', 0)
|
||||
|
||||
generators = [b for b in (buildings or []) if b.get('id') in (
|
||||
'CoalGenerator', 'HydrogenGenerator', 'NaturalGasGenerator',
|
||||
'ManualGenerator', 'PetroleumGenerator', 'WoodBurner'
|
||||
)]
|
||||
batteries = [b for b in (buildings or []) if b.get('id') in ('Battery', 'JumboBattery', 'SmartBattery')]
|
||||
|
||||
warnings = []
|
||||
if coal < 5000:
|
||||
if not generators:
|
||||
warnings.append(("CRITICAL", "No power generators found! Build ManualGenerator or CoalGenerator"))
|
||||
else:
|
||||
powered_on = sum(1 for g in generators if g.get('isOperational'))
|
||||
warnings.append(("INFO", f"Power: {len(generators)} generators ({powered_on} operational), {len(batteries)} batteries"))
|
||||
|
||||
if coal < 5000 and any(g.get('id') == 'CoalGenerator' for g in generators):
|
||||
warnings.append(("WARN", f"Coal low ({coal:.0f} kg) — diversify power"))
|
||||
if hydrogen > 20000:
|
||||
warnings.append(("INFO", f"Hydrogen stockpiled ({hydrogen:.0f} kg) — add generators"))
|
||||
if natgas > 10000:
|
||||
warnings.append(("INFO", f"Natural gas abundant ({natgas:.0f} kg) — tap for power"))
|
||||
warnings.append(("INFO", f"Natural gas abundant ({natgas:.0f} kg)"))
|
||||
|
||||
return warnings
|
||||
|
||||
|
||||
@ -94,52 +131,65 @@ def analyze_water(resources):
|
||||
r = as_dict(resources)
|
||||
water = r.get('Water', {}).get('amount', 0)
|
||||
pw = r.get('PollutedWater', {}).get('amount', 0)
|
||||
sw = r.get('SaltWater', {}).get('amount', 0)
|
||||
|
||||
total_water = water + pw + sw
|
||||
warnings = []
|
||||
if water < 10000:
|
||||
warnings.append(("WARN", f"Clean water low ({water:.0f} kg) — conserve / filter PW"))
|
||||
if pw > water * 2 and water > 0:
|
||||
warnings.append(("INFO", f"More polluted water than clean — build water purifier"))
|
||||
if total_water < 10000:
|
||||
warnings.append(("WARN", f"Total water low ({total_water:.0f} kg across all sources)"))
|
||||
elif total_water < 50000:
|
||||
warnings.append(("INFO", f"Water reserves moderate ({total_water:.0f} kg)"))
|
||||
if water < 10000 and pw > 10000:
|
||||
warnings.append(("INFO", f"Filter polluted water ({pw:.0f} kg available)"))
|
||||
|
||||
return warnings
|
||||
|
||||
|
||||
def suggest_actions(warnings, alerts):
|
||||
def analyze_research(research):
|
||||
if not isinstance(research, list):
|
||||
return []
|
||||
warnings = []
|
||||
done = sum(1 for t in research if t.get('isComplete'))
|
||||
total = len(research)
|
||||
if done == 0 and total > 0:
|
||||
warnings.append(("WARN", "No research completed! Start with Research Station"))
|
||||
elif done < total * 0.3:
|
||||
warnings.append(("INFO", f"Research progress: {done}/{total} ({done*100//total}%)"))
|
||||
return warnings
|
||||
|
||||
|
||||
def suggest_actions(warnings, alerts, buildings):
|
||||
suggestions = []
|
||||
|
||||
for sev, msg in warnings:
|
||||
if 'Oxygen' in msg or 'oxygen' in msg:
|
||||
if 'CRITICAL' in sev:
|
||||
suggestions.append("URGENT: Build algae deoxidizer or electrolyzer immediately")
|
||||
else:
|
||||
suggestions.append("Build or expand SPOM (Self-Powered Oxygen Module)")
|
||||
elif 'Food' in msg or 'food' in msg:
|
||||
if 'CRITICAL' in sev:
|
||||
suggestions.append("URGENT: Harvest wild plants or cook mush fry")
|
||||
else:
|
||||
suggestions.append("Expand mealwood farm or start hatch ranching")
|
||||
elif 'Coal' in msg:
|
||||
suggestions.append("Diversify power: hydrogen generator, natural gas, or solar")
|
||||
elif 'Hydrogen' in msg:
|
||||
suggestions.append("Build more hydrogen generators and battery bank")
|
||||
elif 'NaturalGas' in msg:
|
||||
suggestions.append("Build natural gas generator + gas pipe system")
|
||||
elif 'temperature' in msg.lower() or 'overheat' in msg.lower():
|
||||
suggestions.append("Check cooling loop; add liquid pipe thermo sensor")
|
||||
elif 'water' in msg.lower() and 'low' in msg.lower():
|
||||
suggestions.append("Dig more water sources or filter polluted water")
|
||||
has_electrolyzer = any(b.get('id') == 'Electrolyzer' for b in (buildings or []))
|
||||
has_lavatory = any(b.get('id') == 'Lavatory' for b in (buildings or []))
|
||||
has_sieve = any(b.get('id') == 'WaterSiever' for b in (buildings or []))
|
||||
|
||||
if isinstance(alerts, list):
|
||||
for a in alerts:
|
||||
msg = a.get('message', '') or a.get('title', '')
|
||||
ml = msg.lower()
|
||||
if 'oxygen' in ml or 'breathable' in ml:
|
||||
suggestions.append("Build or expand electrolyzer setup (SPOM)")
|
||||
elif 'food' in ml or 'starving' in ml:
|
||||
suggestions.append("Expand mealwood farm or start ranching")
|
||||
elif 'heat' in ml or 'temperature' in ml:
|
||||
suggestions.append("Check cooling system, expand steam turbine setup")
|
||||
elif 'power' in ml or 'wattage' in ml:
|
||||
suggestions.append("Add power generation (hydrogen/natural gas)")
|
||||
for sev, msg in warnings:
|
||||
ml = msg.lower()
|
||||
if 'oxygen' in ml:
|
||||
suggestions.append("Build SPOM: Electrolyzer + Hydrogen Generator at a water source")
|
||||
elif 'food' in ml and 'shortage' in ml:
|
||||
suggestions.append("Build PlanterBoxes x5, plant Mealwood (no irrigation needed)")
|
||||
elif 'food' in ml and 'declining' in ml:
|
||||
suggestions.append("Expand farm or start hatch ranching (Hatch eats Sedimentary Rock)")
|
||||
elif 'power' in ml and 'generator' in ml:
|
||||
suggestions.append("Build ManualGenerator (early) or CoalGenerator (durable)")
|
||||
elif 'coal' in ml:
|
||||
suggestions.append("Diversify power: build HydrogenGenerator + SmartBattery")
|
||||
elif 'hydrogen' in ml:
|
||||
suggestions.append("Connect HydrogenGenerator to your hydrogen vent/SPOM")
|
||||
elif 'water' in ml and 'low' in ml:
|
||||
suggestions.append("Dig to find water geyser or filter polluted water")
|
||||
elif 'heat' in ml or 'overheat' in ml:
|
||||
suggestions.append("Build insulated tiles around heat sources; add cooling loop")
|
||||
|
||||
if not has_lavatory:
|
||||
suggestions.append("Build Lavatory + Water Sieve for renewable water loop")
|
||||
if not has_electrolyzer:
|
||||
suggestions.append("Plan SPOM once Algae < 5t or you have renewable water")
|
||||
if not has_sieve and has_lavatory:
|
||||
suggestions.append("Build Water Sieve to close the bathroom loop")
|
||||
|
||||
return list(dict.fromkeys(suggestions))
|
||||
|
||||
@ -151,35 +201,40 @@ def print_report(state):
|
||||
return False
|
||||
|
||||
resources = state.get('resources', [])
|
||||
buildings = state.get('buildings', [])
|
||||
alerts = state.get('alerts', [])
|
||||
|
||||
all_warnings = []
|
||||
all_warnings += analyze_o2(resources)
|
||||
all_warnings += analyze_food(resources)
|
||||
all_warnings += analyze_power(resources)
|
||||
all_warnings += analyze_o2(resources, buildings)
|
||||
all_warnings += analyze_food(resources, buildings)
|
||||
all_warnings += analyze_power(resources, buildings)
|
||||
all_warnings += analyze_temp(resources)
|
||||
all_warnings += analyze_water(resources)
|
||||
all_warnings += analyze_research(state.get('research', []))
|
||||
|
||||
critical = [w for w in all_warnings if w[0] == 'CRITICAL']
|
||||
warns = [w for w in all_warnings if w[0] == 'WARN']
|
||||
infos = [w for w in all_warnings if w[0] == 'INFO']
|
||||
|
||||
suggestions = suggest_actions(all_warnings, alerts)
|
||||
suggestions = suggest_actions(all_warnings, alerts, buildings)
|
||||
|
||||
print("=" * 52)
|
||||
print("=" * 56)
|
||||
print(" ONI Analysis Report")
|
||||
print("=" * 52)
|
||||
print("=" * 56)
|
||||
print(f" Cycle: {g.get('cycle', '?')}")
|
||||
print(f" Duplicants: {g.get('duplicantCount', '?')}")
|
||||
print(f" World: {g.get('worldName', '?')}")
|
||||
print(f" Buildings: {len(state.get('buildings', []) or [])}")
|
||||
print(f" Grid: {g.get('gridWidth', '?')} x {g.get('gridHeight', '?')}")
|
||||
print(f" Buildings: {len(buildings or [])}")
|
||||
print(f" Critters: {len(state.get('critters', []) or [])}")
|
||||
print(f" Geysers: {len(state.get('geysers', []) or [])}")
|
||||
print(f" Research done: {sum(1 for t in (state.get('research') or []) if t.get('isComplete'))}")
|
||||
print(f" Plants: {len(state.get('plants', []) or [])}")
|
||||
print(f" Rooms: {len(state.get('rooms', []) or [])}")
|
||||
print(f" Research done: {sum(1 for t in (state.get('research') or []) if t.get('isComplete'))}/{len(state.get('research', []) or [])}")
|
||||
print()
|
||||
|
||||
if critical:
|
||||
print(f" [CRITICAL] {len(critical)} issues")
|
||||
print(f" [CRITICAL] {len(critical)} issues — act immediately!")
|
||||
for _, msg in critical:
|
||||
print(f" ! {msg}")
|
||||
print()
|
||||
@ -206,11 +261,11 @@ def print_report(state):
|
||||
print(f" -> {s}")
|
||||
print()
|
||||
|
||||
print(f" Alerts in-game: {len(alerts) if isinstance(alerts, list) else 0}")
|
||||
print(f" In-game alerts: {len(alerts) if isinstance(alerts, list) else 0}")
|
||||
if isinstance(alerts, list):
|
||||
for a in alerts:
|
||||
print(f" [{a.get('severity', '?')}] {a.get('title', '?')}: {a.get('message', '')}")
|
||||
print("=" * 52)
|
||||
print(f" [{a.get('severity', '?')}] {a.get('title', '?')}")
|
||||
print("=" * 56)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
348
tools/oni_api.py
348
tools/oni_api.py
@ -38,8 +38,12 @@ def api_post(endpoint, data):
|
||||
except urllib.error.URLError as e:
|
||||
return {"error": str(e)}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Command implementations
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def cmd_health():
|
||||
print(api_get('/health'))
|
||||
print(json.dumps(api_get('/health'), indent=2, ensure_ascii=False))
|
||||
|
||||
def cmd_status():
|
||||
game = api_get('/api/state/game')
|
||||
@ -52,43 +56,74 @@ def cmd_status():
|
||||
return
|
||||
|
||||
print("=== Game ===")
|
||||
print(f" Cycle: {game.get('cycle', '?')}")
|
||||
print(f" Duplicants: {game.get('duplicantCount', '?')}")
|
||||
print(f" World: {game.get('worldName', '?')}")
|
||||
print(f" Cycle: {game.get('cycle', '?')}")
|
||||
print(f" Duplicants: {game.get('duplicantCount', '?')}")
|
||||
print(f" World: {game.get('worldName', '?')}")
|
||||
print(f" Grid: {game.get('gridWidth', '?')} x {game.get('gridHeight', '?')}")
|
||||
|
||||
print("\n=== Resources (top 15) ===")
|
||||
if isinstance(resources, list):
|
||||
for r in sorted(resources, key=lambda x: x.get('amount', 0), reverse=True)[:15]:
|
||||
print(f" {r.get('name', '?'):20s} {r.get('amount', 0):>10.1f} {r.get('unit', '')}")
|
||||
state_mark = {'solid': '■', 'liquid': '≈', 'gas': '◌', 'vacuum': ' '}.get(r.get('state', ''), '?')
|
||||
cat = r.get('category', '')
|
||||
print(f" {state_mark} {r.get('name', '?'):20s} {r.get('amount', 0):>10.1f} kg [{cat}]")
|
||||
|
||||
print("\n=== Duplicants ===")
|
||||
if isinstance(dups, list):
|
||||
for d in dups:
|
||||
print(f" {d.get('name'):12s} stress={d.get('stress', '?'):>5} food={d.get('calories', 0)/1000:>6.0f} kcal"
|
||||
f" stamina={d.get('stamina', '?'):>5} o2={d.get('oxygen', '?'):>5}")
|
||||
chore = d.get('currentChore', '?').replace('Chore', '')
|
||||
print(f" {d.get('name'):12s} at ({d.get('x', '?'):3d},{d.get('y', '?'):3d}) "
|
||||
f"stress={d.get('stress', 0):.0f}% food={d.get('calories', 0)/1000:.0f} kcal "
|
||||
f"doing={chore}")
|
||||
|
||||
print("\n=== Alerts ===")
|
||||
if isinstance(alerts, list):
|
||||
for a in alerts:
|
||||
print(f" [{a.get('severity', '?')}] {a.get('title', '?')}: {a.get('message', '')}" if alerts else " (none)")
|
||||
if alerts:
|
||||
for a in alerts:
|
||||
print(f" [{a.get('severity', '?')}] {a.get('title', '?')}: {a.get('message', '')}")
|
||||
else:
|
||||
print(" (none)")
|
||||
|
||||
def cmd_resources():
|
||||
data = api_get('/api/state/resources')
|
||||
if isinstance(data, list):
|
||||
for r in sorted(data, key=lambda x: x.get('amount', 0), reverse=True):
|
||||
print(f"{r.get('name', '?'):30s} {r.get('amount', 0):>12.1f} {r.get('unit', '')}")
|
||||
state_mark = {'solid': '■', 'liquid': '≈', 'gas': '◌'}.get(r.get('state', ''), ' ')
|
||||
print(f"{state_mark} {r.get('name', '?'):25s} {r.get('amount', 0):>12.1f} kg {r.get('state', '?'):8s} [{r.get('category', '?')}]")
|
||||
else:
|
||||
print(json.dumps(data, indent=2, ensure_ascii=False))
|
||||
|
||||
def cmd_duplicants():
|
||||
print(json.dumps(api_get('/api/state/duplicants'), indent=2, ensure_ascii=False))
|
||||
data = api_get('/api/state/duplicants')
|
||||
if isinstance(data, list):
|
||||
for d in data:
|
||||
print(f" Name: {d.get('name', '?')}")
|
||||
print(f" Cell: ({d.get('x', '?')}, {d.get('y', '?')})")
|
||||
print(f" Stress: {d.get('stress', 0):.1f}%")
|
||||
print(f" Food: {d.get('calories', 0)/1000:.0f} kcal")
|
||||
print(f" Stamina: {d.get('stamina', 0):.0f}%")
|
||||
print(f" Oxygen: {d.get('oxygen', 0):.0f}%")
|
||||
print(f" Chore: {d.get('currentChore', '?')}")
|
||||
print()
|
||||
else:
|
||||
print(json.dumps(data, indent=2, ensure_ascii=False))
|
||||
|
||||
def cmd_buildings():
|
||||
data = api_get('/api/state/buildings')
|
||||
if isinstance(data, list):
|
||||
cats = {}
|
||||
for b in data:
|
||||
print(f" {b.get('name', '?'):25s} at ({b.get('x', '?')}, {b.get('y', '?')}) "
|
||||
f"{'ON' if b.get('isOperational') else 'OFF'}")
|
||||
cat = b.get('category', 'Other')
|
||||
if cat not in cats:
|
||||
cats[cat] = []
|
||||
cats[cat].append(b)
|
||||
for cat, blist in sorted(cats.items()):
|
||||
print(f"[{cat}] ({len(blist)})")
|
||||
for b in blist:
|
||||
op = 'ON' if b.get('isOperational') else 'OFF'
|
||||
pw = f" {b.get('powerWatt', 0)}W" if b.get('powerWatt', 0) > 0 else ''
|
||||
print(f" {b.get('name', '?'):25s} at ({b.get('x', '?')},{b.get('y', '?')}) [{op}]{pw}")
|
||||
print()
|
||||
else:
|
||||
print(json.dumps(data, indent=2, ensure_ascii=False))
|
||||
|
||||
@ -97,7 +132,7 @@ def cmd_research():
|
||||
if isinstance(data, list):
|
||||
for t in data:
|
||||
status = "DONE" if t.get('isComplete') else f"{t.get('progress', 0)*100:.0f}%"
|
||||
print(f" {t.get('name', '?'):25s} [{status}] ({t.get('category', '?')})")
|
||||
print(f" {t.get('name', '?'):25s} [{status:5s}] ({t.get('category', '?')})")
|
||||
else:
|
||||
print(json.dumps(data, indent=2, ensure_ascii=False))
|
||||
|
||||
@ -106,7 +141,7 @@ def cmd_geysers():
|
||||
if isinstance(data, list):
|
||||
for g in data:
|
||||
print(f" {g.get('name', '?'):25s} at ({g.get('x', '?')}, {g.get('y', '?')}) "
|
||||
f"state={g.get('state', '?')} rate={g.get('emitRate', '?')}g/s")
|
||||
f"state={g.get('state', '?'):8s} rate={g.get('emitRate', 0):.1f} g/s")
|
||||
else:
|
||||
print(json.dumps(data, indent=2, ensure_ascii=False))
|
||||
|
||||
@ -114,8 +149,148 @@ def cmd_critters():
|
||||
data = api_get('/api/state/critters')
|
||||
if isinstance(data, list):
|
||||
for c in data:
|
||||
print(f" {c.get('name', '?'):20s} ({c.get('species', '?')}) at ({c.get('x', '?')}, {c.get('y', '?')}) "
|
||||
f"age={c.get('age', '?'):.1f} happy={c.get('happiness', '?')}")
|
||||
print(f" {c.get('name', '?'):20s} ({c.get('species', '?')}) at ({c.get('x', '?')},{c.get('y', '?')}) "
|
||||
f"age={c.get('age', 0):.1f} happy={c.get('happiness', 0)}")
|
||||
else:
|
||||
print(json.dumps(data, indent=2, ensure_ascii=False))
|
||||
|
||||
def cmd_plants():
|
||||
data = api_get('/api/state/plants')
|
||||
if isinstance(data, list):
|
||||
for p in data:
|
||||
grown = "GROWN" if p.get('isGrown') else f"{p.get('progress', 0)*100:.0f}%"
|
||||
wilt = " WILT" if p.get('isWilting') else ""
|
||||
print(f" {p.get('name', '?'):25s} at ({p.get('x', '?')},{p.get('y', '?')}) [{grown}]{wilt}")
|
||||
else:
|
||||
print(json.dumps(data, indent=2, ensure_ascii=False))
|
||||
|
||||
def cmd_rooms():
|
||||
data = api_get('/api/state/rooms')
|
||||
if isinstance(data, list):
|
||||
for r in data:
|
||||
print(f" {r.get('name', '?'):25s} cells={r.get('cellCount', 0):4d} "
|
||||
f"buildings={r.get('buildings', 0)} creatures={r.get('creatures', 0)} plants={r.get('plants', 0)}")
|
||||
else:
|
||||
print(json.dumps(data, indent=2, ensure_ascii=False))
|
||||
|
||||
def cmd_cell(args):
|
||||
if len(args) < 2:
|
||||
print("Usage: cell <x> <y>")
|
||||
return
|
||||
data = api_get(f"/api/state/cell?x={args[0]}&y={args[1]}")
|
||||
if 'error' in data:
|
||||
print(f"Error: {data['error']}")
|
||||
return
|
||||
print(f"Cell ({data.get('x', '?')}, {data.get('y', '?')}) [{data.get('cell', '?')}]")
|
||||
print(f" Element: {data.get('element', '?')} ({data.get('elementState', '?')})")
|
||||
print(f" Mass: {data.get('massKg', 0):.1f} kg")
|
||||
print(f" Temp: {data.get('temperatureC', 0):.1f} °C")
|
||||
print(f" Building: {data.get('buildingName', 'none')}")
|
||||
print(f" Duplicant: {data.get('duplicantName', 'none')}")
|
||||
print(f" Pressure: {data.get('pressure', 0):.3f} kg")
|
||||
print(f" Visible: {data.get('isVisible', False)}")
|
||||
|
||||
def cmd_cells(args):
|
||||
if len(args) < 4:
|
||||
print("Usage: cells <x> <y> <width> <height>")
|
||||
return
|
||||
data = api_get(f"/api/state/cells?x={args[0]}&y={args[1]}&width={args[2]}&height={args[3]}")
|
||||
if 'error' in data:
|
||||
print(f"Error: {data['error']}")
|
||||
return
|
||||
region = data.get('region', {})
|
||||
cells = data.get('cells', [])
|
||||
print(f"Region ({region.get('x', '?')},{region.get('y', '?')}) {region.get('width', '?')}x{region.get('height', '?')} ({len(cells)} cells)")
|
||||
print()
|
||||
# Print as a grid
|
||||
grid = {}
|
||||
for c in cells:
|
||||
key = (c.get('x'), c.get('y'))
|
||||
grid[key] = c
|
||||
rx, ry = region.get('x', 0), region.get('y', 0)
|
||||
rw, rh = region.get('width', 0), region.get('height', 0)
|
||||
# Header row
|
||||
header = " "
|
||||
for cx in range(rx, rx + rw):
|
||||
header += f"{cx % 10} "
|
||||
print(header)
|
||||
for cy in range(ry + rh - 1, ry - 1, -1):
|
||||
row = f"{cy:3d} "
|
||||
for cx in range(rx, rx + rw):
|
||||
c = grid.get((cx, cy))
|
||||
if c is None:
|
||||
row += " "
|
||||
elif c.get('isVacuum'):
|
||||
row += " "
|
||||
elif c.get('hasDuplicant'):
|
||||
row += "D "
|
||||
elif c.get('hasBuilding'):
|
||||
row += "B "
|
||||
elif c.get('isLiquid'):
|
||||
row += "~ "
|
||||
elif c.get('isGas'):
|
||||
row += ". "
|
||||
elif c.get('isSolid'):
|
||||
row += "# "
|
||||
else:
|
||||
row += " "
|
||||
print(row)
|
||||
|
||||
def cmd_cell_slice(args):
|
||||
if len(args) < 2:
|
||||
print("Usage: slice <x|y> <index> [start] [end]")
|
||||
return
|
||||
axis = args[0]
|
||||
index = args[1]
|
||||
start = args[2] if len(args) > 2 else "0"
|
||||
end = args[3] if len(args) > 3 else "100"
|
||||
data = api_get(f"/api/state/cells/slice?axis={axis}&index={index}&start={start}&end={end}")
|
||||
if 'error' in data:
|
||||
print(f"Error: {data['error']}")
|
||||
return
|
||||
for c in data.get('cells', []):
|
||||
state = "VAC" if c.get('isVacuum') else c.get('element', '?')
|
||||
building = f" [{c.get('buildingName', '')}]" if c.get('hasBuilding') else ""
|
||||
print(f" ({c.get('x', '?')},{c.get('y', '?')}) {state:15s} {c.get('temperatureC', 0):6.1f}°C {c.get('massKg', 0):8.1f}kg{building}")
|
||||
|
||||
def cmd_gas(args):
|
||||
if len(args) < 2:
|
||||
print("Usage: gas <x> <y> [radius]")
|
||||
return
|
||||
x, y = args[0], args[1]
|
||||
radius = args[2] if len(args) > 2 else "20"
|
||||
data = api_get(f"/api/state/gas?x={x}&y={y}&radius={radius}")
|
||||
if 'error' in data:
|
||||
print(f"Error: {data['error']}")
|
||||
return
|
||||
print(f"Gas analysis at ({x},{y}) radius={radius}")
|
||||
for g in data.get('gases', []):
|
||||
print(f" {g.get('gas', '?'):25s} {g.get('mass', 0):10.1f} kg ({g.get('count', 0)} cells)")
|
||||
|
||||
def cmd_registry(args):
|
||||
if len(args) < 1:
|
||||
print("Usage: registry <buildings|elements|techs> [filter]")
|
||||
return
|
||||
kind = args[0]
|
||||
filt = args[1].lower() if len(args) > 1 else ""
|
||||
data = api_get(f"/api/registry/{kind}")
|
||||
if isinstance(data, list):
|
||||
count = 0
|
||||
for item in data:
|
||||
name = item.get('name', item.get('id', '?'))
|
||||
item_id = item.get('id', '')
|
||||
if filt and filt not in name.lower() and filt not in item_id.lower():
|
||||
continue
|
||||
count += 1
|
||||
if kind == 'buildings':
|
||||
print(f" {item_id:35s} {item.get('name', '?'):30s} {item.get('width', 1)}x{item.get('height', 1)} {item.get('powerCost', 0)}W")
|
||||
elif kind == 'elements':
|
||||
print(f" {item_id:25s} {item.get('name', '?'):20s} {item.get('state', '?'):7s} [{item.get('category', '?')}]")
|
||||
elif kind == 'techs':
|
||||
status = "DONE" if item.get('isComplete') else "PENDING"
|
||||
unlocks = ', '.join(item.get('unlockedBuildings', [])[:5])
|
||||
print(f" {item_id:30s} {item.get('name', '?'):25s} [{status}] -> {unlocks}")
|
||||
print(f"\n Total: {count} matches")
|
||||
else:
|
||||
print(json.dumps(data, indent=2, ensure_ascii=False))
|
||||
|
||||
@ -164,6 +339,88 @@ def cmd_research_select(args):
|
||||
result = api_post('/api/action/research', {"techId": args[0]})
|
||||
print(json.dumps(result, indent=2, ensure_ascii=False))
|
||||
|
||||
def cmd_mop(args):
|
||||
if len(args) < 2:
|
||||
print("Usage: mop <x> <y>")
|
||||
return
|
||||
result = api_post('/api/action/mop', {"x": int(args[0]), "y": int(args[1])})
|
||||
print(json.dumps(result, indent=2, ensure_ascii=False))
|
||||
|
||||
def cmd_harvest(args):
|
||||
if len(args) < 2:
|
||||
print("Usage: harvest <x> <y>")
|
||||
return
|
||||
result = api_post('/api/action/harvest', {"x": int(args[0]), "y": int(args[1])})
|
||||
print(json.dumps(result, indent=2, ensure_ascii=False))
|
||||
|
||||
def cmd_explore(args):
|
||||
"""AI-friendly exploration: reads a region and returns structured text summary."""
|
||||
x = int(args[0]) if len(args) > 0 else 0
|
||||
y = int(args[1]) if len(args) > 1 else 0
|
||||
w = int(args[2]) if len(args) > 2 else 20
|
||||
h = int(args[3]) if len(args) > 3 else 20
|
||||
|
||||
cells_data = api_get(f"/api/state/cells?x={x}&y={y}&width={w}&height={h}")
|
||||
buildings_data = api_get("/api/state/buildings")
|
||||
dups_data = api_get("/api/state/duplicants")
|
||||
|
||||
summary = {
|
||||
"region": f"({x},{y}) to ({x+w-1},{y+h-1})",
|
||||
"cell_count": len(cells_data.get('cells', [])),
|
||||
"buildings_in_region": [],
|
||||
"duplicants_in_region": [],
|
||||
"elements_summary": {},
|
||||
"interesting_cells": []
|
||||
}
|
||||
|
||||
region_cells = cells_data.get('cells', [])
|
||||
|
||||
# Buildings overlapping region
|
||||
if isinstance(buildings_data, list):
|
||||
for b in buildings_data:
|
||||
bx, by = b.get('x', -1), b.get('y', -1)
|
||||
if x <= bx < x + w and y <= by < y + h:
|
||||
summary["buildings_in_region"].append({
|
||||
"name": b.get('name', '?'),
|
||||
"id": b.get('id', '?'),
|
||||
"position": (bx, by),
|
||||
"operational": b.get('isOperational', False)
|
||||
})
|
||||
|
||||
# Dupes in region
|
||||
if isinstance(dups_data, list):
|
||||
for d in dups_data:
|
||||
dx, dy = d.get('x', -1), d.get('y', -1)
|
||||
if x <= dx < x + w and y <= dy < y + h:
|
||||
summary["duplicants_in_region"].append({
|
||||
"name": d.get('name', '?'),
|
||||
"position": (dx, dy),
|
||||
"stress": d.get('stress', 0),
|
||||
"chore": d.get('currentChore', '?')
|
||||
})
|
||||
|
||||
# Element summary
|
||||
elem_counts = {}
|
||||
for c in region_cells:
|
||||
ename = c.get('element', 'Vacuum')
|
||||
elem_counts[ename] = elem_counts.get(ename, 0) + 1
|
||||
summary["elements_summary"] = elem_counts
|
||||
|
||||
# Interesting cells (buildings, dupes, liquids, hot)
|
||||
for c in region_cells:
|
||||
if c.get('hasDuplicant') or c.get('hasBuilding') or (c.get('isLiquid') and c.get('massKg', 0) > 100):
|
||||
summary["interesting_cells"].append({
|
||||
"pos": (c.get('x'), c.get('y')),
|
||||
"element": c.get('element'),
|
||||
"mass_kg": c.get('massKg', 0),
|
||||
"temp_c": c.get('temperatureC', 0),
|
||||
"building": c.get('buildingName'),
|
||||
"dupe": c.get('duplicantName')
|
||||
})
|
||||
|
||||
print(json.dumps(summary, indent=2, ensure_ascii=False))
|
||||
|
||||
|
||||
COMMANDS = {
|
||||
'health': cmd_health,
|
||||
'status': cmd_status,
|
||||
@ -173,11 +430,21 @@ COMMANDS = {
|
||||
'research': cmd_research,
|
||||
'geysers': cmd_geysers,
|
||||
'critters': cmd_critters,
|
||||
'plants': cmd_plants,
|
||||
'rooms': cmd_rooms,
|
||||
'cell': cmd_cell,
|
||||
'cells': cmd_cells,
|
||||
'slice': cmd_cell_slice,
|
||||
'gas': cmd_gas,
|
||||
'registry': cmd_registry,
|
||||
'dig': cmd_dig,
|
||||
'build': cmd_build,
|
||||
'deconstruct': cmd_deconstruct,
|
||||
'prioritize': cmd_prioritize,
|
||||
'research_select': cmd_research_select,
|
||||
'mop': cmd_mop,
|
||||
'harvest': cmd_harvest,
|
||||
'explore': cmd_explore,
|
||||
}
|
||||
|
||||
if __name__ == '__main__':
|
||||
@ -185,20 +452,39 @@ if __name__ == '__main__':
|
||||
|
||||
if cmd == 'help' or cmd not in COMMANDS:
|
||||
print("ONI Agent API Client")
|
||||
print("=" * 60)
|
||||
print("")
|
||||
print("Commands:")
|
||||
print(" health Check Mod connection")
|
||||
print(" status Game overview (cycle, resources, dups, alerts)")
|
||||
print(" resources List all resources with amounts")
|
||||
print(" duplicants Show duplicant details")
|
||||
print(" buildings List all buildings")
|
||||
print(" research Show research tree progress")
|
||||
print(" geysers Show geyser states")
|
||||
print(" critters Show critter list")
|
||||
print(" dig <x> <y> <w> <h> Dig area")
|
||||
print(" build <id> <x> <y> Place building")
|
||||
print(" deconstruct <id> <x> <y> Remove building")
|
||||
print(" prioritize <x> <y> <p> Set priority")
|
||||
print(" research_select <id> Select tech to research")
|
||||
print("=== State Queries ===")
|
||||
print(" health Check Mod connection")
|
||||
print(" status Game overview (cycle, resources, dups, alerts)")
|
||||
print(" resources [filter] List all resources with amounts")
|
||||
print(" duplicants Show duplicant details")
|
||||
print(" buildings List all buildings (grouped by category)")
|
||||
print(" research Show research tree progress")
|
||||
print(" geysers Show geyser states")
|
||||
print(" critters Show critter list")
|
||||
print(" plants Show plant list")
|
||||
print(" rooms Show rooms")
|
||||
print("")
|
||||
print("=== Map / Cell Data ===")
|
||||
print(" cell <x> <y> Get single cell details")
|
||||
print(" cells <x> <y> <w> <h> Get region as grid")
|
||||
print(" slice <x|y> <idx> <s> <e> Row/column scan")
|
||||
print(" gas <x> <y> [r] Gas analysis in radius r")
|
||||
print(" explore <x> <y> <w> <h> AI-friendly region summary")
|
||||
print("")
|
||||
print("=== Registries (AI Reference) ===")
|
||||
print(" registry buildings [f] List all building IDs with metadata")
|
||||
print(" registry elements [f] List all element IDs with properties")
|
||||
print(" registry techs [f] List all tech IDs with unlocks")
|
||||
print("")
|
||||
print("=== Actions ===")
|
||||
print(" dig <x> <y> <w> <h> Dig area")
|
||||
print(" build <id> <x> <y> Place building")
|
||||
print(" deconstruct <id> <x> <y> Remove building")
|
||||
print(" prioritize <x> <y> <p> Set priority")
|
||||
print(" research_select <id> Select tech to research")
|
||||
print(" mop <x> <y> Mop liquid")
|
||||
print(" harvest <x> <y> Harvest plant")
|
||||
else:
|
||||
COMMANDS[cmd](sys.argv[2:])
|
||||
|
||||
Reference in New Issue
Block a user