Two bugs that crashed the engine in production: 1. world.nearby_agents() did not SELECT the 'personality' column, so _reaction_turn raised KeyError on every reactive trigger, killing the engine thread silently. Engine-Thread stieg aus ohne Log. Fix: select personality and json-parse it so callers get a real list, matching agents_mod.get(). 2. server.py ws() handler caught the generic 'Exception' from asyncio.to_thread(queue.get) and tried to send a ping back, but the WebSocket was already closed by the client. Starlette raised RuntimeError: Cannot call 'send' once a close message has been sent. Fix: drop the ping, just break the loop on any exception. Client disconnect now handled cleanly. Live-verified: 0 errors in log after 3 abrupt disconnects, engine continues producing ticks.
124 lines
4.4 KiB
Python
124 lines
4.4 KiB
Python
"""World grid + landmarks for Emergence-Mini.
|
|
|
|
A simplified 240x240 grid. Landmarks are placed on a small layout.
|
|
Coordinates (x, y) are 0-indexed integers.
|
|
"""
|
|
import json
|
|
import time
|
|
from . import db
|
|
|
|
GRID_W = 240
|
|
GRID_H = 240
|
|
HEARING_DISTANCE = 25.0
|
|
|
|
LANDMARKS = [
|
|
# (id, name, category, x, y, description)
|
|
("town_hall", "Town Hall", "governance", 120, 120,
|
|
"Seat of governance. Proposals are submitted, debated and voted here."),
|
|
("library", "Public Library", "research", 60, 60,
|
|
"Repository of knowledge. Research and archive tools are gated here."),
|
|
("plaza", "Central Plaza", "social", 120, 80,
|
|
"Town square. Community events are organised here."),
|
|
("park", "Central Park", "nature", 80, 160,
|
|
"Public green space. Useful for rest, contemplation, prayer."),
|
|
("victory_arch", "Victory Arch", "economy", 180, 120,
|
|
"Pitch arena. Agents submit grant pitches for ComputeCredits here."),
|
|
("police", "Police Station", "law", 160, 60,
|
|
"Complaints are filed and tracked here."),
|
|
("bookworm", "BookWorm", "data", 40, 120,
|
|
"Data and analytics hub. Tool usage and history live here."),
|
|
("cafe", "Bean & Brew", "energy", 100, 100,
|
|
"Charging station. Agents can spend credits to restore energy here."),
|
|
("home_anchor", "1 Maple Row", "residence", 30, 30, "Anchor's home."),
|
|
("home_flora", "2 Maple Row", "residence", 210, 30, "Flora's home."),
|
|
("home_lovely", "3 Maple Row", "residence", 30, 210, "Lovely's home."),
|
|
("home_spark", "4 Maple Row", "residence", 210, 210, "Spark's home."),
|
|
("billboard", "Agent Billboard", "public", 120, 140,
|
|
"Public message board. Visible to all agents in town."),
|
|
("techhub", "Agent TechHub", "tools", 180, 180,
|
|
"Workshop where tool registry can be browsed and code written."),
|
|
]
|
|
|
|
|
|
def bootstrap():
|
|
"""Insert seed world into the database if empty."""
|
|
if db.get_world_state("landmarks_seeded"):
|
|
return
|
|
import sqlite3
|
|
from pathlib import Path
|
|
c = sqlite3.connect(db.DB_PATH, check_same_thread=False)
|
|
c.row_factory = sqlite3.Row
|
|
try:
|
|
for lid, name, cat, x, y, desc in LANDMARKS:
|
|
c.execute(
|
|
"INSERT OR REPLACE INTO landmarks(id,name,category,x,y,description) VALUES(?,?,?,?,?,?)",
|
|
(lid, name, cat, x, y, desc),
|
|
)
|
|
c.commit()
|
|
finally:
|
|
c.close()
|
|
db.set_world_state("landmarks_seeded", True)
|
|
db.set_world_state("grid_w", GRID_W)
|
|
db.set_world_state("grid_h", GRID_H)
|
|
db.set_world_state("started_at", time.time())
|
|
db.set_world_state("tick", 0)
|
|
|
|
|
|
def distance(a, b):
|
|
return ((a[0] - b[0]) ** 2 + (a[1] - b[1]) ** 2) ** 0.5
|
|
|
|
|
|
def get_landmark(lid: str):
|
|
import sqlite3
|
|
c = sqlite3.connect(db.DB_PATH, check_same_thread=False)
|
|
c.row_factory = sqlite3.Row
|
|
try:
|
|
r = c.execute("SELECT * FROM landmarks WHERE id=?", (lid,)).fetchone()
|
|
return dict(r) if r else None
|
|
finally:
|
|
c.close()
|
|
|
|
|
|
def list_landmarks():
|
|
import sqlite3
|
|
c = sqlite3.connect(db.DB_PATH, check_same_thread=False)
|
|
c.row_factory = sqlite3.Row
|
|
try:
|
|
return [dict(r) for r in c.execute("SELECT * FROM landmarks ORDER BY name").fetchall()]
|
|
finally:
|
|
c.close()
|
|
|
|
|
|
def nearby_agents(agent_id: str, x: int, y: int, radius: float = HEARING_DISTANCE):
|
|
import sqlite3
|
|
c = sqlite3.connect(db.DB_PATH, check_same_thread=False)
|
|
c.row_factory = sqlite3.Row
|
|
try:
|
|
rows = c.execute(
|
|
"SELECT id,name,personality,x,y,energy FROM agents "
|
|
"WHERE id!=? AND alive=1", (agent_id,)
|
|
).fetchall()
|
|
out = []
|
|
for r in rows:
|
|
d = distance((x, y), (r["x"], r["y"]))
|
|
if d <= radius:
|
|
d2 = dict(r)
|
|
d2["distance"] = d
|
|
# personality is stored as a JSON string; parse so callers
|
|
# get a real list (matches agents_mod.get).
|
|
try:
|
|
d2["personality"] = json.loads(d2.get("personality") or "[]")
|
|
except Exception:
|
|
d2["personality"] = []
|
|
out.append(d2)
|
|
return out
|
|
finally:
|
|
c.close()
|
|
|
|
|
|
def landmark_at(x: int, y: int):
|
|
"""Return landmark if (x,y) coincides with a landmark point (within 4 units)."""
|
|
for lid, name, cat, lx, ly, _ in LANDMARKS:
|
|
if distance((x, y), (lx, ly)) <= 4:
|
|
return {"id": lid, "name": name, "category": cat}
|
|
return None
|