emergence-mini-dilles/engine/turn.py
Jeuners ddf9598518 Emergence-Mini: minimaler Klon von Emergence-World
4 Agenten, 14 Landmarks, 15 Tools, 240x240 Grid, SQLite-Persistenz.
Round-Robin Turn-Manager mit Reactive Triggern, Town-Hall-Voting
(70%-Threshold) mit Live-Constitution-Amendment.

- engine/: db, world, agents, needs, tools, reasoning, governance, turn
- web/: Canvas-basierte Live-View mit WebSocket-Stream
- server.py: FastAPI + WebSocket auf 127.0.0.1:8080
- tests/: 70 Unit + Integration Tests (pytest), alle gruen
- smoke_test.py: 50+ End-to-End-Checks
- README: Quickstart, Architektur, Security, Tests, Lizenz
- .gitignore: DB, Cache, Logs

Basiert auf https://github.com/EmergenceAI/Emergence-World
(Lizenz: CC-BY-NC-4.0, Research-only)
2026-06-15 01:07:38 +02:00

157 lines
5.4 KiB
Python

"""Turn manager: round-robin + reactive triggers."""
import json
import time
import threading
import queue
from . import agents as agents_mod
from . import needs
from . import tools
from . import world
from . import reasoning
from . import governance
from . import db
class Engine:
"""Holds the simulation loop and a state-change broadcast queue."""
def __init__(self):
self.tick = 0
self.broadcasts: "queue.Queue[dict]" = queue.Queue()
self._stop = threading.Event()
self._thread: threading.Thread | None = None
self._speak_events: list[dict] = []
# -------- Loop control --------
def start(self):
if self._thread and self._thread.is_alive():
return
self._stop.clear()
self._thread = threading.Thread(target=self._run, daemon=True)
self._thread.start()
def stop(self):
self._stop.set()
# -------- Main loop --------
def _run(self):
tools.bootstrap()
while not self._stop.is_set():
self._one_round()
time.sleep(2.0) # 2s per tick
def _one_round(self):
self.tick += 1
db.set_world_state("tick", self.tick)
needs.tick_all_needs()
# round-robin over live agents
for a in agents_mod.all_agents():
self._agent_turn(a)
governance.apply_accepted_proposals_to_constitution()
self._broadcast({"type": "tick", "tick": self.tick})
def _agent_turn(self, agent):
ctx = {"speak_events": self._speak_events}
tool_name, args, rationale = reasoning.decide(agent)
tool = tools.get(tool_name)
if not tool:
self._record_turn(agent["id"], tool_name, args,
{"ok": False, "error": "tool not found"})
return
at_lm = world.landmark_at(agent["x"], agent["y"])
if not tool.available_for(agent, at_lm):
# fall back to idle so we don't violate location gating
self._record_turn(agent["id"], "idle", {}, {"ok": True, "fallback": True})
return
result = tool.handler(agent, args, ctx) if tool.handler else {"ok": False, "error": "no handler"}
self._record_turn(agent["id"], tool_name, args, result)
# refresh agent after possible state change
a2 = agents_mod.get(agent["id"])
if a2:
self._broadcast({
"type": "action",
"agent": a2["id"],
"name": a2["name"],
"tool": tool_name,
"args": args,
"result": result,
"rationale": rationale,
"x": a2["x"], "y": a2["y"],
"energy": a2["energy"], "knowledge": a2["knowledge"],
"influence": a2["influence"], "credits": a2["credits"],
"mood": a2["mood"],
})
# reactive triggers
self._handle_reactive(a2 or agent)
def _handle_reactive(self, speaker):
events = list(self._speak_events)
self._speak_events.clear()
if not events:
return
for ev in events:
if not ev.get("public") and ev.get("to") is None:
continue
nearby = world.nearby_agents(speaker["id"], ev["x"], ev["y"])
for listener in nearby[:4]:
self._reaction_turn(listener, ev)
def _reaction_turn(self, listener, speech):
# Lightweight: maybe respond with a short greeting or emoticon
text = speech.get("text", "")
if not text:
return
if any(t in listener["personality"] for t in ["warm", "expressive", "cooperative"]):
reply = f"Acknowledged: {text[:24]}"
ctx = {"speak_events": []}
tools.get("say_to_agent").handler(
listener,
{"target": speech["from"], "text": reply},
ctx,
)
def _record_turn(self, agent_id, tool, args, result):
import sqlite3
c = sqlite3.connect(db.DB_PATH, check_same_thread=False)
try:
c.execute(
"INSERT INTO turn_log(agent_id,tool,args,result,ts) VALUES(?,?,?,?,?)",
(agent_id, tool, json.dumps(args), json.dumps(result), time.time()),
)
c.commit()
finally:
c.close()
def _broadcast(self, message: dict):
self.broadcasts.put(message)
db.log_event("engine", message.get("type", "info"), message)
# -------- Manual trigger (for tests / forced turns) --------
def force_turn(self, agent_id: str, tool_name: str, args: dict):
agent = agents_mod.get(agent_id)
if not agent:
return {"ok": False, "error": "no such agent"}
tool = tools.get(tool_name)
if not tool:
return {"ok": False, "error": "no such tool"}
ctx = {"speak_events": self._speak_events}
result = tool.handler(agent, args, ctx)
self._record_turn(agent_id, tool_name, args, result)
a2 = agents_mod.get(agent_id)
self._broadcast({
"type": "action", "agent": a2["id"], "name": a2["name"],
"tool": tool_name, "args": args, "result": result,
"rationale": "forced",
"x": a2["x"], "y": a2["y"],
"energy": a2["energy"], "knowledge": a2["knowledge"],
"influence": a2["influence"], "credits": a2["credits"],
"mood": a2["mood"],
})
return result
engine = Engine()