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

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