diff --git a/src/infrastructure/world/adapters/threejs.py b/src/infrastructure/world/adapters/threejs.py new file mode 100644 index 00000000..aaee4d0b --- /dev/null +++ b/src/infrastructure/world/adapters/threejs.py @@ -0,0 +1,149 @@ +"""Three.js world adapter — bridges Kimi's AI World Builder to WorldInterface. + +Studied from Kimisworld.zip (issue #870). Kimi's world is a React + +Three.js app ("AI World Builder v1.0") that exposes a JSON state API and +accepts ``addObject`` / ``updateObject`` / ``removeObject`` commands. + +This adapter is a stub: ``connect()`` and the core methods outline the +HTTP / WebSocket wiring that would be needed to talk to a running instance. +The ``observe()`` response maps Kimi's ``WorldObject`` schema to +``PerceptionOutput`` entities so that any WorldInterface consumer can +treat the Three.js canvas like any other game world. + +Usage:: + + registry.register("threejs", ThreeJSWorldAdapter) + adapter = registry.get("threejs", base_url="http://localhost:5173") + adapter.connect() + perception = adapter.observe() + adapter.act(CommandInput(action="add_object", parameters={"geometry": "sphere", ...})) + adapter.speak("Hello from Timmy", target="broadcast") +""" + +from __future__ import annotations + +import logging + +from infrastructure.world.interface import WorldInterface +from infrastructure.world.types import ActionResult, ActionStatus, CommandInput, PerceptionOutput + +logger = logging.getLogger(__name__) + +# --------------------------------------------------------------------------- +# Kimi's WorldObject geometry / material vocabulary (from WorldObjects.tsx) +# --------------------------------------------------------------------------- + +_VALID_GEOMETRIES = {"box", "sphere", "cylinder", "torus", "cone", "dodecahedron"} +_VALID_MATERIALS = {"standard", "wireframe", "glass", "glow"} +_VALID_TYPES = {"mesh", "light", "particle", "custom"} + + +def _object_to_entity_description(obj: dict) -> str: + """Render a Kimi WorldObject dict as a human-readable entity string. + + Example output: ``sphere/glow #ff006e at (2.1, 3.0, -1.5)`` + """ + geometry = obj.get("geometry", "unknown") + material = obj.get("material", "unknown") + color = obj.get("color", "#ffffff") + pos = obj.get("position", [0, 0, 0]) + obj_type = obj.get("type", "mesh") + pos_str = "({:.1f}, {:.1f}, {:.1f})".format(*pos) + return f"{obj_type}/{geometry}/{material} {color} at {pos_str}" + + +class ThreeJSWorldAdapter(WorldInterface): + """Adapter for Kimi's Three.js AI World Builder. + + Connects to a running Three.js world that exposes: + - ``GET /api/world/state`` — returns current WorldObject list + - ``POST /api/world/execute`` — accepts addObject / updateObject code + - WebSocket ``/ws/world`` — streams state change events + + All core methods raise ``NotImplementedError`` until HTTP wiring is + added. Implement ``connect()`` first — it should verify that the + Three.js app is running and optionally open a WebSocket for live events. + + Key insight from studying Kimi's world (issue #870): + - Objects carry a geometry, material, color, position, rotation, scale, + and an optional *animation* string executed via ``new Function()`` + each animation frame. + - The AI agent (``AIAgent.tsx``) moves through the world with lerp() + targeting, cycles through moods, and pulses its core during "thinking" + states — a model for how Timmy could manifest presence in a 3D world. + - World complexity is tracked as a simple counter (one unit per object) + which the AI uses to decide whether to create, modify, or upgrade. + """ + + def __init__(self, *, base_url: str = "http://localhost:5173") -> None: + self._base_url = base_url.rstrip("/") + self._connected = False + + # -- lifecycle --------------------------------------------------------- + + def connect(self) -> None: + raise NotImplementedError( + "ThreeJSWorldAdapter.connect() — verify Three.js app is running at " + f"{self._base_url} and optionally open a WebSocket to /ws/world" + ) + + def disconnect(self) -> None: + self._connected = False + logger.info("ThreeJSWorldAdapter disconnected") + + @property + def is_connected(self) -> bool: + return self._connected + + # -- core contract (stubs) --------------------------------------------- + + def observe(self) -> PerceptionOutput: + """Return current Three.js world state as structured perception. + + Expected HTTP call:: + + GET {base_url}/api/world/state + → {"objects": [...WorldObject], "worldComplexity": int, ...} + + Each WorldObject becomes an entity description string. + """ + raise NotImplementedError( + "ThreeJSWorldAdapter.observe() — GET /api/world/state, " + "map each WorldObject via _object_to_entity_description()" + ) + + def act(self, command: CommandInput) -> ActionResult: + """Dispatch a command to the Three.js world. + + Supported actions (mirrors Kimi's CodeExecutor API): + - ``add_object`` — parameters: WorldObject fields (geometry, material, …) + - ``update_object`` — parameters: id + partial WorldObject fields + - ``remove_object`` — parameters: id + - ``clear_world`` — parameters: (none) + + Expected HTTP call:: + + POST {base_url}/api/world/execute + Content-Type: application/json + {"action": "add_object", "parameters": {...}} + """ + raise NotImplementedError( + f"ThreeJSWorldAdapter.act({command.action!r}) — " + "POST /api/world/execute with serialised CommandInput" + ) + + def speak(self, message: str, target: str | None = None) -> None: + """Inject a text message into the Three.js world. + + Kimi's world does not have a native chat layer, so the recommended + implementation is to create a short-lived ``Text`` entity at a + visible position (or broadcast via the world WebSocket). + + Expected WebSocket frame:: + + {"type": "timmy_speech", "text": message, "target": target} + """ + raise NotImplementedError( + "ThreeJSWorldAdapter.speak() — send timmy_speech frame over " + "/ws/world WebSocket, or POST a temporary Text entity" + )