wtf
This commit is contained in:
327
ai_god.py
Normal file
327
ai_god.py
Normal file
@@ -0,0 +1,327 @@
|
||||
"""
|
||||
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":"<name>","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":"<name>","position":[x,y,z]}
|
||||
{"action":"rotate", "id":"<name>","rotation":[x,y,z]} // radians
|
||||
{"action":"scale", "id":"<name>","scale":[x,y,z]}
|
||||
{"action":"delete", "id":"<name>"}
|
||||
{"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":"<clip>","volume":1.0}
|
||||
{"action":"music", "name":"<clip>","volume":0.7}
|
||||
{"action":"stop_music"}
|
||||
{"action":"set", "property":"fov|draw_distance|clear_color|time_scale",
|
||||
"value":<number or [r,g,b]>}
|
||||
{"action":"log", "message":"<text shown on screen>"}
|
||||
|
||||
{"action":"save_world", "name":"<name>"} // writes worlds/<name>.json
|
||||
{"action":"load_world", "name":"<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":"<existing-object>",
|
||||
"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":"<name>", "impulse":[x,y,z], "point":[x,y,z]?}
|
||||
{"action":"velocity", "id":"<name>", "linear":[x,y,z], "angular":[x,y,z]}
|
||||
{"action":"remove_body", "id":"<name>"}
|
||||
|
||||
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>", "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":"<name>", "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())
|
||||
Reference in New Issue
Block a user