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

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