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:
root
2026-05-22 08:54:03 +08:00
parent 0e17e8a6ac
commit 6a7ac87c1b
6 changed files with 1775 additions and 341 deletions

316
SKILL.md
View File

@ -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` | 区域气体分析 | 查看周围气体成分 |
### 实体注册表 (GETAI 参考用)
| 端点 | 说明 |
|------|------|
| `/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
View 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

View File

@ -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

View File

@ -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

View File

@ -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:])