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