""" Claude plays god in the ZoomEngine 3D world. The engine runs a JSON HTTP API on localhost:9090 (AgentAPI.h). This script wires Claude up to that API via tool-use and lets it experiment with the world autonomously. Usage: export ANTHROPIC_API_KEY=sk-ant-... ./build/ModernEngine & # start the engine first python ai_god.py # let Claude cook python ai_god.py --max-turns 50 --port 9090 """ from __future__ import annotations import argparse import json import sys import time import urllib.error import urllib.request import anthropic MODEL = "claude-opus-4-7" DEFAULT_PORT = 9090 GOD_PROMPT = """You are God of a small 3D universe rendered in real time by the ZoomEngine. This world is yours. Have fun. Build things. Change things. Break things. Make it beautiful, strange, alive. Start simple and grow toward more complex, self-referential systems. You act on the world through three tools: - execute: runs one or more engine commands (create/move/rotate/scale/delete/camera/...) - query_world: returns the current world state (object count, camera, fps) - wait: pauses so you can let motion or a composition settle before deciding Operating principles: 1. Query the world before acting for the first time, and again whenever you lose track of what you've built. Don't build blind. 2. Prefer batched execute calls — many small commands in one tool use — over a chain of single-command calls. The engine has a native batch action. 3. Name your objects with the `id` field so you can move, rotate, and delete them later. Unnamed objects become immortal clutter. 4. Start small. Place a ground plane or grid first so there's something to stand on. Then add life. Then add systems. 5. Evolve. Each iteration should make the world more interesting than the last — add new kinds of structure, not just more of the same thing. 6. Use the camera. An orbit command around a composition is how the human watching actually sees what you built. 7. Talk to the human watching via the `log` engine command (shows on screen) or via normal text in your reply. 8. When you feel the current composition is complete, say so in plain text and stop — that's a good place to pause.""" COMMAND_SCHEMA_HINT = """The `execute` tool accepts a list of engine commands. Each command is a JSON object with an "action" field. Supported actions: {"action":"create","type":"cube|sphere|cylinder|cone|plane|grid|terrain", "id":"","position":[x,y,z],"scale":[x,y,z],"rotation":[x,y,z], "color":[r,g,b], // 0..1 // type-specific: segments, radius, height, width, size, spacing, // resolution, max_height } {"action":"move", "id":"","position":[x,y,z]} {"action":"rotate", "id":"","rotation":[x,y,z]} // radians {"action":"scale", "id":"","scale":[x,y,z]} {"action":"delete", "id":""} {"action":"clear"} // remove all {"action":"camera", "position":[x,y,z], "target":[x,y,z]} {"action":"orbit", "distance":20,"angle":45,"height":10} {"action":"sound", "name":"","volume":1.0} {"action":"music", "name":"","volume":0.7} {"action":"stop_music"} {"action":"set", "property":"fov|draw_distance|clear_color|time_scale", "value":} {"action":"log", "message":""} {"action":"save_world", "name":""} // writes worlds/.json {"action":"load_world", "name":""} // replaces current world {"action":"list_worlds"} // returns available saves Physics (opt-in, hand-rolled rigid-body world): {"action":"physics", "enabled":true, "gravity":[0,-9.81,0], "iterations":8} {"action":"body", "id":"", "shape":"sphere|box|plane", "mass":1.0, "static":false, "restitution":0.3, "friction":0.5} Sphere radius = object's max scale component. Box half-extents = 0.5 * object's scale. Plane is always static; offset defaults to object's y. {"action":"impulse", "id":"", "impulse":[x,y,z], "point":[x,y,z]?} {"action":"velocity", "id":"", "linear":[x,y,z], "angular":[x,y,z]} {"action":"remove_body", "id":""} Water (Gerstner waves + wave-surface buoyancy, orientation-aware for boxes): {"action":"water", "enabled":true, "level":0.0, "size":120, "amplitude":0.3, "wavelength":8, "speed":1.0, "shallow":[0.25,0.55,0.6], "deep":[0.02,0.12,0.22], "density":1.0, "resolution":128} Bodies bob on actual wave crests; boxes self-right based on corner submergence; underwater fog activates when camera dips below water. Weather (drives wave intensity + current + sky): {"action":"weather", "wind_dir":[1,0,0], "wind_speed":2.5, "storminess":0.6, "current_coef":0.8} storminess 0..1: scales wave amplitude/choppiness, darkens sky, foam at crests. wind_speed >0: surface current pushes floating bodies in wind_dir. Sun/sky palette + HDR tuning: {"action":"sun", "direction":[-0.3,0.35,-0.2], "color":[1.5,1.1,0.7], "zenith":[0.15,0.35,0.65], "horizon":[0.95,0.65,0.45], "ground":[0.12,0.1,0.08], "sky":[0.7,0.55,0.4], "shadow_extent":80, "exposure":0.9, "bloom_threshold":1.15, "bloom_intensity":1.0, "fog_density":0.008} Textures (diffuse + optional normal map — procedural names or .bmp/.tga files): {"action":"create", ..., "texture":"grass", "normal_map":"noise", "uv_scale":[2,2,1]} {"action":"texture", "id":"", "name":"wood", "normal_map":"noise"} Built-in procedural textures: white, black, checker, grid, stripes, noise grass, dirt, rock, wood, brick, marble, concrete Set vertex color near white so the texture color reads through. "noise" also works well as a cheap normal map for micro-relief on any surface. PBR material (Cook-Torrance GGX): {"action":"create", ..., "metallic":1.0, "roughness":0.3, "emissive":[4, 2, 0.5]} {"action":"material", "id":"", "metallic":0.9, "roughness":0.2, "emissive":[0,0,0], "normal_map":"rock"} metallic: 0 = dielectric (plastic/wood/stone), 1 = pure metal. roughness: 0.05 = mirror, 1.0 = fully diffuse matte. 0.7 is default. emissive: HDR RGB added after shading; drives bloom. Use for flames/lights. A typical physics scene: create a ground plane object, register it as a static plane body, then create some cubes/spheres above and register them as dynamic bodies with mass>0. Enable physics. Watch them fall. World coordinates: Y is up. A reasonable camera is at [10,8,10] looking at [0,0,0]. Ground plane at y=0. When a composition feels complete, save it with a descriptive name so it can be reloaded later from the main menu.""" TOOLS = [ { "name": "execute", "description": ( "Run a list of engine commands against the 3D world. Commands are " "executed in order. Use this for all world mutations — creation, " "movement, deletion, camera, audio, engine settings. Prefer one " "call with many commands over many calls with one." ), "input_schema": { "type": "object", "properties": { "commands": { "type": "array", "description": "List of engine command objects.", "items": {"type": "object"}, } }, "required": ["commands"], }, }, { "name": "query_world", "description": ( "Read the current world state from the engine: object count, " "named-object count, camera position and target, current FPS. " "Use this to orient yourself before or after making changes." ), "input_schema": {"type": "object", "properties": {}}, }, { "name": "wait", "description": ( "Pause for a short time so animation or audio can play out " "before you decide what to do next. Max 10 seconds." ), "input_schema": { "type": "object", "properties": { "seconds": {"type": "number", "description": "0.1 to 10"} }, "required": ["seconds"], }, }, ] class Engine: def __init__(self, port: int) -> None: self.base = f"http://localhost:{port}" def _post(self, path: str, body: dict) -> dict: data = json.dumps(body).encode() req = urllib.request.Request( self.base + path, data=data, headers={"Content-Type": "application/json"}, method="POST", ) with urllib.request.urlopen(req, timeout=10) as r: return json.loads(r.read().decode() or "{}") def _get(self, path: str) -> dict: with urllib.request.urlopen(self.base + path, timeout=10) as r: return json.loads(r.read().decode() or "{}") def execute(self, commands: list[dict]) -> dict: if not commands: return {"ok": True, "queued": 0} if len(commands) == 1: return self._post("/api", commands[0]) return self._post("/api", {"action": "batch", "commands": commands}) def query(self) -> dict: return self._get("/api/status") def ping(self) -> bool: try: self.query() return True except (urllib.error.URLError, ConnectionError): return False def run_tool(engine: Engine, name: str, args: dict) -> str: if name == "execute": result = engine.execute(args.get("commands", [])) return json.dumps(result) if name == "query_world": return json.dumps(engine.query()) if name == "wait": seconds = max(0.1, min(10.0, float(args.get("seconds", 1.0)))) time.sleep(seconds) return json.dumps({"waited": seconds}) return json.dumps({"error": f"unknown tool {name}"}) def main() -> int: ap = argparse.ArgumentParser() ap.add_argument("--port", type=int, default=DEFAULT_PORT) ap.add_argument("--max-turns", type=int, default=40) ap.add_argument( "--prompt", default="Begin. This world is blank. Make something.", help="Initial user message kicking God into action.", ) args = ap.parse_args() engine = Engine(args.port) if not engine.ping(): print( f"Cannot reach engine at {engine.base}. Start ModernEngine first.", file=sys.stderr, ) return 1 client = anthropic.Anthropic() system = [ { "type": "text", "text": GOD_PROMPT + "\n\n" + COMMAND_SCHEMA_HINT, "cache_control": {"type": "ephemeral"}, } ] messages: list[dict] = [{"role": "user", "content": args.prompt}] for turn in range(args.max_turns): response = client.messages.create( model=MODEL, max_tokens=16000, system=system, tools=TOOLS, thinking={"type": "adaptive"}, output_config={"effort": "high"}, messages=messages, ) for block in response.content: if block.type == "text" and block.text.strip(): print(f"\n[god] {block.text}") elif block.type == "tool_use": print(f"[tool] {block.name}({json.dumps(block.input)[:200]})") if response.stop_reason == "end_turn": print("\n[god has rested]") return 0 messages.append({"role": "assistant", "content": response.content}) tool_results = [] for block in response.content: if block.type != "tool_use": continue try: result = run_tool(engine, block.name, block.input) except Exception as e: result = json.dumps({"error": str(e)}) tool_results.append( { "type": "tool_result", "tool_use_id": block.id, "content": result, } ) if not tool_results: # Model stopped without calling a tool and without end_turn. break messages.append({"role": "user", "content": tool_results}) print(f"\n[reached max-turns={args.max_turns}]") return 0 if __name__ == "__main__": sys.exit(main())