+Microduck World is starting
+Reconnecting shortly. You can leave this page open.
+Try now
+"""
+
+
+def unavailable(request: Request) -> Response:
+ headers = {"cache-control": "no-store", "retry-after": "3"}
+ if request.url.path == "/" and "text/html" in request.headers.get("accept", ""):
+ return HTMLResponse(STARTING_PAGE, status_code=503, headers=headers)
+ return JSONResponse({"status": "starting"}, status_code=503, headers=headers)
+
+
+class GatewayConfig(BaseModel):
+ model_config = ConfigDict(extra="forbid")
+
+ bind_host: IPv4Address
+ port: int = Field(ge=1024, le=65535)
+ public_origin: str
+ upstream_url: str
+ cert_file: Path
+ key_file: Path
+ max_peers: int = Field(default=64, ge=1, le=256)
+ frontend_dir: Path | None = None
+ world_assets_dir: Path | None = None
+
+ @field_validator("bind_host")
+ @classmethod
+ def private_interface(cls, host: IPv4Address) -> IPv4Address:
+ if not host.is_loopback and host not in ip_network("100.64.0.0/10"):
+ raise ValueError("Bind only to a Tailscale IPv4 address or loopback")
+ return host
+
+ @field_validator("public_origin")
+ @classmethod
+ def https_origin(cls, value: str) -> str:
+ parsed = urlsplit(value)
+ if (
+ parsed.scheme != "https"
+ or not parsed.hostname
+ or parsed.username
+ or parsed.path not in ("", "/")
+ or parsed.query
+ or parsed.fragment
+ ):
+ raise ValueError("public_origin must be an HTTPS origin without a path")
+ return value.rstrip("/")
+
+ @field_validator("upstream_url")
+ @classmethod
+ def local_upstream(cls, value: str) -> str:
+ parsed = urlsplit(value)
+ if (
+ parsed.scheme != "http"
+ or parsed.hostname != "127.0.0.1"
+ or parsed.username
+ or parsed.path not in ("", "/")
+ or parsed.query
+ or parsed.fragment
+ ):
+ raise ValueError("upstream_url must be a loopback HTTP origin")
+ return value.rstrip("/")
+
+
+def rewrite_info(
+ info: dict[str, Any], config: GatewayConfig, forwarder: UdpForwarder
+) -> dict[str, Any]:
+ """Keep the end-to-end certificate pin while publishing the private UDP endpoint."""
+ upstream = urlsplit(info["wtUrl"])
+ if upstream.scheme != "https" or upstream.hostname != "127.0.0.1" or not upstream.port:
+ raise ValueError("Relay advertised a non-loopback QUIC endpoint")
+ forwarder.set_target((upstream.hostname, upstream.port))
+ public = urlsplit(config.public_origin)
+ return {
+ **info,
+ "wtUrl": urlunsplit(("https", public.netloc, upstream.path, upstream.query, "")),
+ }
+
+
+def create_app(config: GatewayConfig) -> Starlette:
+ forwarder = UdpForwarder(max_peers=config.max_peers)
+ frontend = StaticFiles(directory=config.frontend_dir) if config.frontend_dir else None
+
+ @asynccontextmanager
+ async def lifespan(app: Starlette) -> AsyncIterator[None]:
+ async with httpx.AsyncClient(timeout=10.0, trust_env=False) as client:
+ app.state.client = client
+ await forwarder.start(str(config.bind_host), config.port)
+ try:
+ yield
+ finally:
+ await forwarder.stop()
+
+ async def serve(request: Request) -> Response:
+ # Tailnet membership is the access boundary; a foreign website must
+ # not read bootstrap data or use this as its own cross-origin relay.
+ if request.headers.get("host") != urlsplit(config.public_origin).netloc:
+ return Response("Unexpected host", status_code=421)
+ origin = request.headers.get("origin")
+ if origin is not None and origin != config.public_origin:
+ return Response("Origin not allowed", status_code=403)
+ if request.headers.get("sec-fetch-site") == "cross-site":
+ return Response("Cross-site access not allowed", status_code=403)
+ path = request.url.path
+ if frontend is not None and (path == "/" or path.startswith("/client/")):
+ file = "index.html" if path == "/" else path.removeprefix("/client/")
+ result = await frontend.get_response(file, request.scope)
+ result.headers["x-content-type-options"] = "nosniff"
+ result.headers["cache-control"] = "no-cache"
+ return result
+ if path.startswith("/world-assets/") and config.world_assets_dir is not None:
+ name = path.removeprefix("/world-assets/")
+ if re.fullmatch(r"scene-[a-f0-9]{20}\.json", name) is None:
+ return Response("Unknown world asset", status_code=404)
+ file_path = config.world_assets_dir / name
+ headers = {
+ "cache-control": "public, max-age=31536000, immutable",
+ "vary": "accept-encoding",
+ }
+ if "gzip" in request.headers.get("accept-encoding", ""):
+ file_path = file_path.with_suffix(".json.gz")
+ headers["content-encoding"] = "gzip"
+ if not file_path.is_file():
+ return Response("World asset not ready", status_code=404)
+ headers["x-content-type-options"] = "nosniff"
+ return FileResponse(file_path, media_type="application/json", headers=headers)
+ session_info = re.fullmatch(r"/sessions/[a-f0-9-]{72}/api/info", path) is not None
+ if (
+ path not in ("/", "/healthz", "/api/stats", "/api/info", "/api/lobby", "/sdk.js")
+ and not session_info
+ ):
+ return Response("Not found", status_code=404)
+ if request.method == "POST" and path != "/api/lobby":
+ return Response("Method not allowed", status_code=405)
+ body = bytearray()
+ if request.method == "POST":
+ async for chunk in request.stream():
+ body.extend(chunk)
+ if len(body) > 1024:
+ return Response("Request too large", status_code=413)
+ health = path == "/healthz"
+ target = httpx.URL(config.upstream_url).copy_with(
+ path="/api/stats" if health else request.url.path,
+ query=request.url.query.encode(),
+ )
+ try:
+ response = await request.app.state.client.request(
+ request.method, target, content=bytes(body)
+ )
+ except httpx.RequestError:
+ return unavailable(request)
+ if health:
+ try:
+ stats = response.json()
+ robots = stats.get("robots") if isinstance(stats, dict) else None
+ ready = response.is_success and isinstance(robots, list) and len(robots) > 0
+ except ValueError:
+ ready = False
+ return JSONResponse(
+ {"status": "ready" if ready else "starting"},
+ status_code=200 if ready else 503,
+ headers={"cache-control": "no-store"},
+ )
+ if (
+ (path == "/api/info" or session_info)
+ and request.method == "GET"
+ and response.is_success
+ ):
+ try:
+ info = rewrite_info(response.json(), config, forwarder)
+ except (ValueError, KeyError, TypeError):
+ return Response("Invalid relay discovery response", status_code=502)
+ return JSONResponse(info, headers={"cache-control": "no-store"})
+ # Deliberately omit upstream wildcard CORS and content-encoding:
+ # httpx has already decoded the response body.
+ headers = {
+ key: response.headers[key]
+ for key in ("content-type", "cache-control")
+ if key in response.headers
+ }
+ headers["x-content-type-options"] = "nosniff"
+ return Response(response.content, status_code=response.status_code, headers=headers)
+
+ return Starlette(
+ routes=[Route("/{path:path}", serve, methods=["GET", "HEAD", "POST"])], lifespan=lifespan
+ )
+
+
+def main() -> None:
+ parser = argparse.ArgumentParser(description=__doc__)
+ parser.add_argument("--config", type=Path, required=True)
+ args = parser.parse_args()
+ config = GatewayConfig.model_validate_json(args.config.read_text())
+ uvicorn.run(
+ create_app(config),
+ host=str(config.bind_host),
+ port=config.port,
+ ssl_certfile=str(config.cert_file),
+ ssl_keyfile=str(config.key_file),
+ access_log=False,
+ )
+
+
+if __name__ == "__main__":
+ main()
diff --git a/examples/microduck-world/app/microduck_world/knowledge.py b/examples/microduck-world/app/microduck_world/knowledge.py
new file mode 100644
index 0000000000..a044ca394d
--- /dev/null
+++ b/examples/microduck-world/app/microduck_world/knowledge.py
@@ -0,0 +1,221 @@
+# Copyright 2026 Dimensional Inc.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+"""Private, observation-backed place and object knowledge for one robot runtime."""
+
+import json
+import math
+import threading
+import time
+from collections import OrderedDict
+from dataclasses import dataclass
+from pathlib import Path
+from typing import Any
+
+import cv2
+import numpy as np
+from dimos.agents.annotation import skill
+from dimos.agents.skill_result import SkillResult
+from dimos.core.core import rpc
+from dimos.core.stream import In
+from dimos.msgs.nav_msgs.OccupancyGrid import OccupancyGrid
+from dimos.robot.pollen.microduck.skills import (
+ MicroduckSkillContainer,
+ MicroduckSkillContainerConfig,
+)
+from microduck_world.robot_io import Observation
+from reactivex.disposable import Disposable
+
+
+@dataclass(frozen=True)
+class CameraView:
+ id: str
+ observation: Observation
+
+ def agent_encode(self) -> list[dict[str, Any]]:
+ image = self.observation.image
+ return [
+ {
+ "type": "text",
+ "text": json.dumps(
+ {
+ "observation_id": self.id,
+ "width": self.observation.camera_info.width,
+ "height": self.observation.camera_info.height,
+ "coordinates": "Pixels: x from left, y from top.",
+ }
+ ),
+ },
+ *image.agent_encode(),
+ ]
+
+
+class KnowledgeConfig(MicroduckSkillContainerConfig):
+ knowledge_dir: str
+
+
+def locate_pixel(observation: Observation, x: int, y: int) -> tuple[float, float, float]:
+ info = observation.camera_info
+ if not 0 <= x < info.width or not 0 <= y < info.height:
+ raise ValueError("Pixel is outside this camera image")
+ depth = float(observation.depth.data[y, x])
+ if not math.isfinite(depth) or not 0.05 <= depth <= 6:
+ raise ValueError("This pixel has no usable measured depth")
+ point = np.array(
+ [(x - info.K[2]) * depth / info.K[0], (y - info.K[5]) * depth / info.K[4], depth]
+ )
+ pose = observation.camera_pose
+ world = pose.orientation.to_rotation_matrix() @ point + pose.position.to_numpy()
+ return float(world[0]), float(world[1]), float(world[2])
+
+
+class DuckKnowledge(MicroduckSkillContainer):
+ config: KnowledgeConfig
+ observation: In[Observation]
+ global_costmap: In[OccupancyGrid]
+
+ def __init__(self, **kwargs: Any) -> None:
+ super().__init__(**kwargs)
+ self._observations_lock = threading.Lock()
+ self._latest: Observation | None = None
+ self._seen: OrderedDict[str, Observation] = OrderedDict()
+ self._known_cells = 0
+ self._known_area = 0.0
+ self._journal: list[dict[str, Any]] = []
+
+ @rpc
+ def start(self) -> None:
+ super().start()
+ directory = Path(self.config.knowledge_dir)
+ directory.mkdir(parents=True, exist_ok=True)
+ path = directory / "observations.json"
+ if path.exists():
+ self._journal = json.loads(path.read_text())[-100:]
+ self.register_disposable(Disposable(self.observation.subscribe(self._on_observation)))
+ self.register_disposable(Disposable(self.global_costmap.subscribe(self._on_map)))
+
+ def _on_observation(self, observation: Observation) -> None:
+ with self._observations_lock:
+ self._latest = observation
+
+ def _on_map(self, grid: OccupancyGrid) -> None:
+ with self._observations_lock:
+ self._known_cells = int(np.count_nonzero(grid.grid >= 0))
+ self._known_area = self._known_cells * grid.resolution**2
+
+ @skill
+ def observe(self) -> CameraView | SkillResult:
+ """Look through your own camera.
+
+ Returns an image and observation_id for recording objects or observations.
+ """
+ with self._observations_lock:
+ observation = self._latest
+ if observation is None or time.time() - observation.image.ts > 5:
+ return SkillResult.fail("EXECUTION_TIMEOUT", "No recent camera observation")
+ id = f"{observation.image.ts:.6f}"
+ self._seen[id] = observation
+ while len(self._seen) > 8:
+ self._seen.popitem(last=False)
+ return CameraView(id, observation)
+
+ def _seen_observation(self, id: str) -> Observation:
+ with self._observations_lock:
+ observation = self._seen.get(id)
+ if observation is None:
+ raise ValueError("Unknown observation. Call observe and use its observation_id.")
+ return observation
+
+ def _record(self, observation: Observation, description: str) -> str:
+ description = description.strip()
+ if not description or len(description) > 1000:
+ raise ValueError("Description must contain 1–1000 characters")
+ directory = Path(self.config.knowledge_dir)
+ image_name = f"seen-{observation.image.ts:.6f}.jpg"
+ if not cv2.imwrite(str(directory / image_name), observation.image.data[:, :, ::-1]):
+ raise ValueError("Could not save camera evidence")
+ entry = {
+ "description": description,
+ "observed_at": observation.image.ts,
+ "image": image_name,
+ "camera_position": observation.camera_pose.position.to_numpy().tolist(),
+ }
+ with self._observations_lock:
+ self._journal = [*self._journal, entry][-100:]
+ (directory / "observations.json").write_text(json.dumps(self._journal, indent=2))
+ return image_name
+
+ @skill
+ def remember_object(
+ self, name: str, observation_id: str, pixel_x: int, pixel_y: int, description: str = ""
+ ) -> str:
+ """Annotate a visible object using your camera and measured depth.
+
+ Args:
+ name: Your name for the visible object.
+ observation_id: ID returned by observe.
+ pixel_x: Horizontal pixel on the object, measured from the image left edge.
+ pixel_y: Vertical pixel on the object, measured from the image top edge.
+ description: What you observed; mention uncertainty when appropriate.
+ """
+ try:
+ name = name.strip()
+ if not name or len(name) > 100:
+ raise ValueError("Object name must contain 1–100 characters")
+ observation = self._seen_observation(observation_id)
+ x, y, z = locate_pixel(observation, pixel_x, pixel_y)
+ image = self._record(observation, description or name)
+ self._places_memory().add(
+ name,
+ x,
+ y,
+ kind="object",
+ metadata={
+ "source": "camera-depth",
+ "observation_id": observation_id,
+ "description": description,
+ "image": image,
+ "z": z,
+ "pixel": [pixel_x, pixel_y],
+ },
+ )
+ self._publish_places()
+ return f"Remembered {name} at ({x:.2f}, {y:.2f}) from my camera observation."
+ except ValueError as exc:
+ return f"Could not annotate object: {exc}"
+
+ @skill
+ def record_observation(self, observation_id: str, description: str) -> str:
+ """Remember what you learned from one of your own camera observations.
+
+ Args:
+ observation_id: ID returned by observe.
+ description: What you saw or inferred, explicitly noting uncertainty.
+ """
+ try:
+ self._record(self._seen_observation(observation_id), description)
+ return "Saved this observation in my private memory."
+ except ValueError as exc:
+ return f"Could not remember observation: {exc}"
+
+ @skill
+ def understanding(self) -> str:
+ """Summarize your measured map coverage, known places and recorded observations."""
+ with self._observations_lock:
+ coverage = {
+ "observed_cells": self._known_cells,
+ "observed_area_m2": round(self._known_area, 2),
+ }
+ journal = list(self._journal[-20:])
+ return json.dumps({"map": coverage, "places": self.list_places(), "observations": journal})
diff --git a/examples/microduck-world/app/microduck_world/physics_robots.py b/examples/microduck-world/app/microduck_world/physics_robots.py
new file mode 100644
index 0000000000..1e79cfe3f2
--- /dev/null
+++ b/examples/microduck-world/app/microduck_world/physics_robots.py
@@ -0,0 +1,305 @@
+# Copyright 2026 Dimensional Inc.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+"""Robot bodies and shared gait execution, owned by the physics thread."""
+
+import json
+import math
+import threading
+import time
+from dataclasses import dataclass
+from typing import Any
+
+import mujoco
+import numpy as np
+from dimos.msgs.geometry_msgs.PoseStamped import PoseStamped
+from dimos.msgs.sensor_msgs.JointState import JointState
+from dimos.robot.pollen.microduck.gait import CONTROL_DT
+from dimos.robot.pollen.microduck.policies import FALL_GRAVITY_Z, PolicyBank, PolicyScheduler
+from dimos.robot.pollen.microduck.sim_module import (
+ POLICY_DECIMATION,
+ MicroduckSimModuleConfig,
+ shape_twist,
+)
+from microduck_world.robot_io import (
+ ROBOT_IDS,
+ VISITOR_IDS,
+ RobotCommand,
+ RobotState,
+)
+from microduck_world.roster import ROSTER
+from numpy.typing import NDArray
+
+
+@dataclass
+class RobotBody:
+ id: str
+ bank: PolicyBank
+ scheduler: PolicyScheduler
+ actuators: list[int]
+ joint_qpos: list[int]
+ joint_qvel: list[int]
+ geoms: list[int]
+ collision: NDArray[np.int32]
+ origin: tuple[float, float] = (0.0, 0.0)
+ active: bool = False
+ generation: str = ""
+ fallen_since: float | None = None
+ respawns: int = 0
+
+
+class WorldRobots:
+ """Only step() mutates physics; subscribers update bounded mailboxes."""
+
+ def __init__(
+ self,
+ model: mujoco.MjModel,
+ bank: PolicyBank,
+ settings: dict[str, Any],
+ config: MicroduckSimModuleConfig,
+ ) -> None:
+ self.model, self.config = model, config
+ self.roster = settings.get("robots", ROSTER)
+ self.clearance = float(settings["clearance"])
+ self.robots: dict[str, RobotBody] = {}
+ self._lock = threading.Lock()
+ self._wanted: dict[str, str] = {}
+ self._commands: dict[str, tuple[RobotCommand, float]] = {}
+ self._policies: dict[str, RobotCommand] = {}
+ self._respawns: dict[str, RobotCommand] = {}
+ self._lease_at = 0.0
+ self._step = 0
+ for id in ROBOT_IDS:
+ prefix = "" if id == "duck1" else id + "_"
+ scoped = bank.for_robot(model, prefix)
+ joints = [model.joint(prefix + name) for name in bank.joint_names]
+ actuators = [int(np.flatnonzero(model.actuator_trnid[:, 0] == j.id)[0]) for j in joints]
+ root = model.body(prefix + "trunk_base").id
+ geoms = []
+ for i in range(model.ngeom):
+ ancestor = int(model.geom_bodyid[i])
+ while ancestor and ancestor != root:
+ ancestor = int(model.body_parentid[ancestor])
+ if ancestor == root:
+ geoms.append(i)
+ self.robots[id] = RobotBody(
+ id,
+ scoped,
+ PolicyScheduler(scoped.availability, scoped.variant),
+ actuators,
+ [int(j.qposadr[0]) for j in joints],
+ [int(j.dofadr[0]) for j in joints],
+ geoms,
+ np.array([model.geom_contype[geoms], model.geom_conaffinity[geoms]]),
+ )
+
+ def leases(self, occupants: dict[str, str]) -> None:
+ with self._lock:
+ self._wanted = {id: gen for id, gen in occupants.items() if id in VISITOR_IDS}
+ self._lease_at = time.monotonic()
+
+ def command(self, id: str, command: RobotCommand) -> None:
+ if id not in ROBOT_IDS:
+ return
+ with self._lock:
+ if command.kind == "twist":
+ if not isinstance(command.value, tuple) or len(command.value) != 3:
+ return
+ if not all(math.isfinite(v) for v in command.value):
+ return
+ self._commands[id] = (command, time.monotonic())
+ elif command.kind == "policy":
+ self._policies[id] = command
+ elif command.kind == "respawn":
+ self._respawns[id] = command
+
+ def _place(self, robot: RobotBody, data: mujoco.MjData, xy: tuple[float, float]) -> None:
+ adr = robot.bank.root_qpos_adr
+ data.qpos[adr : adr + 2] = xy
+ robot.bank.initial_qpos(data)
+ robot.bank.reset()
+ data.ctrl[robot.actuators] = robot.bank.default_pose
+
+ def _spawn(self, robot: RobotBody, data: mujoco.MjData, *, reset_origin: bool = True) -> bool:
+ player = self.roster[robot.id]
+ candidates = [
+ player,
+ *(p for id, p in self.roster.items() if id != robot.id and p["team"] == player["team"]),
+ ]
+ for candidate in candidates:
+ x, y = candidate["spawn"]
+ if all(
+ other is robot
+ or not other.active
+ or math.hypot(
+ data.qpos[other.bank.root_qpos_adr] - x,
+ data.qpos[other.bank.root_qpos_adr + 1] - y,
+ )
+ >= self.clearance
+ for other in self.robots.values()
+ ) and self._clear_spawn(data, x, y):
+ self._place(robot, data, (x, y))
+ yaw = float(candidate["yaw"])
+ adr = robot.bank.root_qpos_adr
+ data.qpos[adr + 3 : adr + 7] = (math.cos(yaw / 2), 0, 0, math.sin(yaw / 2))
+ if reset_origin:
+ # All supplied places use this frame. Knowledge is still private.
+ robot.origin = (0.0, 0.0)
+ self.model.geom_contype[robot.geoms] = robot.collision[0]
+ self.model.geom_conaffinity[robot.geoms] = robot.collision[1]
+ robot.active = True
+ return True
+ return False
+
+ def _clear_spawn(self, data: mujoco.MjData, x: float, y: float) -> bool:
+ """Reject fixed obstacles and free balls in the footprint above the floor."""
+ radius = 0.18
+ robot_geoms = {g for r in self.robots.values() for g in r.geoms}
+ for i in range(self.model.ngeom):
+ if i in robot_geoms or not (
+ self.model.geom_contype[i] or self.model.geom_conaffinity[i]
+ ):
+ continue
+ position = data.geom_xpos[i]
+ if self.model.geom_type[i] == mujoco.mjtGeom.mjGEOM_BOX:
+ rotation = data.geom_xmat[i].reshape(3, 3)
+ extent = np.abs(rotation) @ self.model.geom_size[i]
+ if position[2] + extent[2] <= 0.035 or position[2] - extent[2] > 0.35:
+ continue
+ dx = max(abs(x - position[0]) - extent[0], 0)
+ dy = max(abs(y - position[1]) - extent[1], 0)
+ if math.hypot(dx, dy) < radius:
+ return False
+ elif self.model.geom_type[i] == mujoco.mjtGeom.mjGEOM_SPHERE:
+ size = float(self.model.geom_size[i, 0])
+ if position[2] + size > 0.035 and position[2] - size < 0.35:
+ if math.hypot(x - position[0], y - position[1]) < radius + size:
+ return False
+ return True
+
+ def step(self, data: mujoco.MjData) -> None:
+ now = time.monotonic()
+ with self._lock:
+ wanted = dict(self._wanted) if now - self._lease_at < 3 else {}
+ commands = dict(self._commands)
+ policies, self._policies = self._policies, {}
+ respawns, self._respawns = self._respawns, {}
+ for index, robot in enumerate(self.robots.values()):
+ generation = wanted.get(robot.id, "")
+ if generation != robot.generation or self._step == 0:
+ robot.active = False
+ robot.generation = generation
+ robot.fallen_since = None
+ robot.respawns = 0
+ robot.scheduler = PolicyScheduler(robot.bank.availability, robot.bank.variant)
+ robot.bank.reset()
+ self.model.geom_contype[robot.geoms] = 0
+ self.model.geom_conaffinity[robot.geoms] = 0
+ if generation and not robot.active:
+ self._spawn(robot, data)
+ if not robot.active:
+ self._place(robot, data, (10 + index, 10))
+ data.qpos[robot.bank.root_qpos_adr + 2] = -5
+ continue
+ respawn = respawns.get(robot.id)
+ if respawn and respawn.generation == generation:
+ if self._spawn(robot, data, reset_origin=False):
+ robot.scheduler = PolicyScheduler(robot.bank.availability, robot.bank.variant)
+ robot.fallen_since = None
+ robot.respawns += 1
+ # Old queued commands must not restart motion after a reset.
+ commands.pop(robot.id, None)
+ policies.pop(robot.id, None)
+ with self._lock:
+ self._commands.pop(robot.id, None)
+ self._policies.pop(robot.id, None)
+ else:
+ # Keep an explicit reset pending until its own team has space.
+ with self._lock:
+ self._respawns[robot.id] = respawn
+ self._commands.pop(robot.id, None)
+ commands.pop(robot.id, None)
+ request = policies.get(robot.id)
+ if request and request.generation == generation and isinstance(request.value, str):
+ try:
+ payload = json.loads(request.value)
+ if isinstance(payload, dict):
+ robot.scheduler.request(payload.get("policy"), payload.get("action", ""))
+ except (ValueError, TypeError):
+ pass
+ if self._step % POLICY_DECIMATION:
+ continue
+ twist = (0.0, 0.0, 0.0)
+ current = commands.get(robot.id)
+ if (
+ current
+ and current[0].generation == generation
+ and now - current[1] < self.config.cmd_timeout
+ ):
+ value = current[0].value
+ if isinstance(value, tuple):
+ twist = value
+ robot.scheduler.set_twist(*shape_twist(self.config, *twist))
+ if not robot.scheduler.suspend_fall_detector:
+ gravity = float(robot.bank.projected_gravity(data)[2])
+ if gravity > FALL_GRAVITY_Z:
+ if robot.fallen_since is None:
+ robot.fallen_since = now
+ robot.scheduler.notify_fall(
+ now - robot.fallen_since > self.config.auto_stand_after
+ )
+ else:
+ robot.fallen_since = None
+ robot.scheduler.notify_fall(False)
+ name, command = robot.scheduler.tick(CONTROL_DT)
+ data.ctrl[robot.actuators] = robot.bank.step(name, command, data)
+ self._step += 1
+
+ def states(self, data: mujoco.MjData) -> dict[str, RobotState]:
+ result = {}
+ now = time.time()
+ for robot in self.robots.values():
+ if not robot.active:
+ continue
+ adr = robot.bank.root_qpos_adr
+ x, y, z, w, qx, qy, qz = data.qpos[adr : adr + 7]
+ result[robot.id] = RobotState(
+ robot.generation,
+ PoseStamped(
+ position=(x - robot.origin[0], y - robot.origin[1], z),
+ orientation=(qx, qy, qz, w),
+ frame_id="world",
+ ts=now,
+ ),
+ JointState(
+ name=list(robot.bank.joint_names),
+ position=data.qpos[robot.joint_qpos].tolist(),
+ velocity=data.qvel[robot.joint_qvel].tolist(),
+ ts=now,
+ ),
+ json.dumps(
+ {**robot.scheduler.snapshot(), "respawns": robot.respawns},
+ separators=(",", ":"),
+ ),
+ )
+ return result
+
+ def snapshot(self) -> list[dict[str, Any]]:
+ return [
+ {"id": r.id, "active": r.active, "generation": r.generation}
+ for r in self.robots.values()
+ ]
+
+ def sensor_assignments(self) -> dict[str, tuple[str, tuple[float, float]]]:
+ return {r.id: (r.generation, r.origin) for r in self.robots.values() if r.active}
diff --git a/examples/microduck-world/app/microduck_world/relay.py b/examples/microduck-world/app/microduck_world/relay.py
new file mode 100644
index 0000000000..381d08749b
--- /dev/null
+++ b/examples/microduck-world/app/microduck_world/relay.py
@@ -0,0 +1,107 @@
+# Copyright 2026 Dimensional Inc.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+"""Project relay policy and discovery, using the existing bridge and transport."""
+
+import asyncio
+from dataclasses import replace
+from pathlib import Path
+from typing import Any, Literal
+
+import requests
+from dimos.core.stream import In
+from dimos.msgs.nav_msgs.Path import Path as NavPath
+from dimos.msgs.sensor_msgs.Image import Image
+from dimos.web.relay_bridge.protocol import Tx
+from dimos.web.relay_bridge.relay_bridge_module import RelayBridgeModule, _Session
+from dimos.web.relay_bridge.relay_process import RelayProcess
+from langchain_core.messages.base import BaseMessage
+from microduck_world.gateway import GatewayConfig
+from microduck_world.scene import PROJECT_ROOT
+from pydantic import BaseModel, ConfigDict, Field
+
+
+def relay_settings() -> GatewayConfig:
+ return GatewayConfig.model_validate_json((PROJECT_ROOT / "config/tailnet.json").read_text())
+
+
+def private_relay_get(path: str) -> dict[str, Any]:
+ with requests.Session() as client:
+ client.trust_env = False
+ response = client.get(relay_settings().upstream_url + path, timeout=1)
+ response.raise_for_status()
+ value = response.json()
+ if not isinstance(value, dict):
+ raise ValueError("Invalid private relay response")
+ return value
+
+
+class WorldCommand(BaseModel):
+ model_config = ConfigDict(strict=True, allow_inf_nan=False)
+ name: Literal["set_mode", "policy", "cancel_nav", "respawn", "drop_ball"]
+ args: dict[str, Any] = Field(default_factory=dict)
+
+
+class WorldBridge(RelayBridgeModule):
+ world_state: In[str]
+ duck1_ball_camera: In[str]
+ duck2_ball_camera: In[str]
+ duck3_ball_camera: In[str]
+ duck4_ball_camera: In[str]
+ duck5_ball_camera: In[str]
+ duck6_ball_camera: In[str]
+ world_compare_image: In[Image]
+ agent: In[BaseMessage]
+ agent_idle: In[bool]
+ mode: In[str]
+ policy_state: In[str]
+ path: In[NavPath]
+ nav_state: In[str]
+ places: In[str]
+
+ def _on_wire_tx(self, msg: Tx) -> None:
+ # Extend this project's command vocabulary while retaining the stock
+ # manifest, sequence, rate-limit, validation and publishing path.
+ handler = self._tx_defs.get(msg.ch)
+ if msg.ch == "ui_command" and handler is not None and handler.model is not WorldCommand:
+ self._tx_defs[msg.ch] = replace(handler, model=WorldCommand)
+ super()._on_wire_tx(msg)
+
+ def _spawn_relay(self, open_browser: bool, serve_dir: Path | None) -> str:
+ self._relay = RelayProcess(
+ port=self.config.local_port,
+ web_dir=PROJECT_ROOT / "relay",
+ cockpit_dir=PROJECT_ROOT / "vendor/dimos/web/cockpit/dist",
+ sdk_dir=PROJECT_ROOT / "vendor/dimos/web/sdk/dist",
+ entrypoint=PROJECT_ROOT / "relay/main.ts",
+ )
+ return self._relay.start().wt_url
+
+ async def _connect_and_hello(self) -> _Session:
+ # Refresh discovery on every reconnect: the supervised relay's QUIC
+ # port and private registration credential rotate after a restart.
+ for attempt in range(60):
+ try:
+ info = await asyncio.to_thread(private_relay_get, "/internal/robot-info")
+ break
+ except requests.RequestException:
+ if attempt == 59:
+ raise
+ await asyncio.sleep(0.5)
+ previous = self._url
+ try:
+ self._url = info["wtUrl"]
+ return await super()._connect_and_hello()
+ finally:
+ self._url = previous
diff --git a/examples/microduck-world/app/microduck_world/robot_blueprint.py b/examples/microduck-world/app/microduck_world/robot_blueprint.py
new file mode 100644
index 0000000000..b822537861
--- /dev/null
+++ b/examples/microduck-world/app/microduck_world/robot_blueprint.py
@@ -0,0 +1,131 @@
+# Copyright 2026 Dimensional Inc.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+"""The same independently runnable DimOS robot composition for all six ducks."""
+
+import os
+from uuid import UUID
+
+from dimos.agents.mcp.mcp_client import McpClient
+from dimos.core.coordination.blueprints import Blueprint, autoconnect
+from dimos.mapping.costmapper import CostMapper
+from dimos.mapping.pointclouds.occupancy import HeightCostConfig
+from dimos.mapping.voxels.module import VoxelGridMapper
+from dimos.navigation.replanning_a_star.module import ReplanningAStarPlanner
+from dimos.robot.pollen.microduck.config import MICRODUCK
+from microduck_world.cockpit import world_cockpit
+from microduck_world.connection import SimRobotConnection
+from microduck_world.control import WorldControl
+from microduck_world.exploration import DuckExplorer
+from microduck_world.knowledge import DuckKnowledge
+from microduck_world.robot_io import ROBOT_IDS
+from microduck_world.robot_mcp import RobotMcpServer
+from microduck_world.roster import ROSTER
+from microduck_world.scene import PROJECT_ROOT, load_world
+
+
+def robot_blueprint(robot: str, generation: str) -> Blueprint:
+ if robot not in ROBOT_IDS:
+ raise ValueError("Unknown robot")
+ UUID(generation)
+ player = ROSTER[robot]
+ port = int(player["mcp_port"]) + int(os.environ.get("MICRODUCK_MCP_PORT_OFFSET", "0"))
+ if not 1024 <= port <= 65535:
+ raise ValueError("MCP port offset is outside the allowed port range")
+ scene = load_world()[1]
+ directory = PROJECT_ROOT / "state/robots" / scene.id / robot / generation
+ prompt = f"""You operate {player["name"]}, a small biped on the {player["team"]} football team.
+You have your own camera, range sensors, map, places and conversation.
+Only use knowledge from your own tools, observations and the user's instructions.
+The full-world browser view and other ducks' data are unavailable to you.
+Start idle. Do not explore or move until instructed. Agent mode enables navigation.
+Use observe for visual questions. Record useful findings with record_observation;
+use remember_object with the observation ID and a pixel on the visible object.
+Positions for object annotations come from your measured depth, not guesses.
+Use remember_place to name your current location. Query understanding and list_places
+to recall what you have learned. Rooms and object names appearing in tool examples
+are examples, not evidence that those places exist here.
+Use begin_exploration and end_exploration to discover your own map's frontiers.
+Kicks only perform the physical motion: approach and align with a ball first.
+A kick does not place or attract a ball. Never claim a goal without observing it.
+Move slowly. Use movement tools one at a time; stop exploration before navigating
+to a named place or performing a trick. Use list_policies for available actions.
+Never pretend to see around an obstacle or infer exact unobserved room boundaries.
+Distinguish observations from hypotheses. Keep replies brief.
+"""
+ prompt += (
+ "You know the pitch and both locker-room locations as supplied prior knowledge. "
+ "Your occupancy map and other discoveries come from your own sensors. "
+ "Both teams play free football, with no kickoff, timer or automatic resets. "
+ "Blue defends the west goal; red defends the east goal. "
+ "Teammates do not share their private observations or tools.\n"
+ )
+ modules = [
+ SimRobotConnection.blueprint(generation=generation),
+ VoxelGridMapper.blueprint(emit_every=1, voxel_size=0.03, device="CPU:0"),
+ CostMapper.blueprint(
+ config=HeightCostConfig(
+ resolution=0.03, can_pass_under=MICRODUCK.height_clearance + 0.05, can_climb=0.03
+ ),
+ initial_safe_radius_meters=MICRODUCK.width_clearance + 0.15,
+ ),
+ ReplanningAStarPlanner.blueprint(
+ robot_width=MICRODUCK.width_clearance,
+ robot_rotation_diameter=MICRODUCK.rotation_diameter,
+ stuck_time_window=10.0,
+ stuck_threshold=0.15,
+ ),
+ WorldControl.blueprint(),
+ DuckExplorer.blueprint(
+ min_frontier_perimeter=0.15,
+ safe_distance=0.6,
+ lookahead_distance=2.0,
+ max_explored_distance=6.0,
+ goal_timeout=45.0,
+ ),
+ DuckKnowledge.blueprint(
+ rooms={name: scene.rooms[name] for name in ("football", "red_lockers", "blue_lockers")},
+ objects={},
+ scene=scene.id,
+ places_db=str(directory / "places.db"),
+ knowledge_dir=str(directory / "knowledge"),
+ ),
+ RobotMcpServer.blueprint(robot=robot, port=port),
+ ]
+ if os.environ.get("OPENAI_API_KEY"):
+ modules.append(
+ McpClient.blueprint(
+ system_prompt=prompt,
+ mcp_server_url=f"http://127.0.0.1:{port}/mcp",
+ trace_dir=directory / "agent-traces",
+ )
+ )
+ stack = (
+ autoconnect(*modules)
+ .remappings(
+ [
+ (VoxelGridMapper, "lidar", "pointcloud"),
+ ]
+ )
+ .namespace(robot)
+ )
+ return autoconnect(stack, world_cockpit(robot, generation)).global_config(
+ robot_model="microduck",
+ nerf_speed=0.5,
+ viewer="none",
+ n_workers=4,
+ mcp_port=port,
+ tool_stream_topic=f"/{robot}/{generation}/tool_streams",
+ listen_host="127.0.0.1",
+ )
diff --git a/examples/microduck-world/app/microduck_world/robot_io.py b/examples/microduck-world/app/microduck_world/robot_io.py
new file mode 100644
index 0000000000..ff7871c7be
--- /dev/null
+++ b/examples/microduck-world/app/microduck_world/robot_io.py
@@ -0,0 +1,69 @@
+# Copyright 2026 Dimensional Inc.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+"""Typed virtual-hardware boundary; no scene or other-robot state crosses it."""
+
+from dataclasses import dataclass
+from typing import Literal
+
+import numpy as np
+from dimos.msgs.geometry_msgs.PoseStamped import PoseStamped
+from dimos.msgs.sensor_msgs.CameraInfo import CameraInfo
+from dimos.msgs.sensor_msgs.Image import Image
+from dimos.msgs.sensor_msgs.JointState import JointState
+from dimos.msgs.tf2_msgs.TFMessage import TFMessage
+from microduck_world.roster import ROBOT_IDS
+from numpy.typing import NDArray
+
+VISITOR_IDS = ROBOT_IDS
+
+
+@dataclass(frozen=True)
+class RobotCommand:
+ generation: str
+ kind: Literal["twist", "policy", "respawn", "drop_ball"]
+ value: tuple[float, float, float] | str
+
+
+@dataclass(frozen=True)
+class RobotState:
+ generation: str
+ odom: PoseStamped
+ joints: JointState
+ policy: str
+
+
+@dataclass(frozen=True)
+class RobotVision:
+ generation: str
+ image: Image
+ depth: Image
+ camera_info: CameraInfo
+ camera_pose: PoseStamped
+ tf: TFMessage
+ points: NDArray[np.float32]
+
+
+@dataclass(frozen=True)
+class Observation:
+ """A synchronized RGB-D observation in this robot's own map frame."""
+
+ image: Image
+ depth: Image
+ camera_info: CameraInfo
+ camera_pose: PoseStamped
+
+
+def allowed_generation(actual: str, expected: str) -> bool:
+ return bool(expected) and actual == expected
diff --git a/examples/microduck-world/app/microduck_world/robot_mcp.py b/examples/microduck-world/app/microduck_world/robot_mcp.py
new file mode 100644
index 0000000000..a7d69d52bd
--- /dev/null
+++ b/examples/microduck-world/app/microduck_world/robot_mcp.py
@@ -0,0 +1,43 @@
+# Copyright 2026 Dimensional Inc.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+"""One stock MCP server per robot process, with an explicit local tool boundary."""
+
+from dimos.agents.mcp.mcp_server import McpServer
+from dimos.core.core import rpc
+from dimos.core.module import ModuleConfig
+from dimos.core.rpc_client import RPCClient
+
+
+class RobotMcpConfig(ModuleConfig):
+ robot: str
+ port: int
+
+
+class RobotMcpServer(McpServer):
+ config: RobotMcpConfig
+ dedicated_worker = True
+
+ def _start_server(self, port: int | None = None) -> None:
+ super()._start_server(self.config.port)
+
+ @rpc
+ def on_system_modules(self, modules: list[RPCClient]) -> None:
+ prefix = self.config.robot + "/"
+ own = [module for module in modules if module.remote_name.startswith(prefix)]
+ # The framework's generic agent_send skill targets a global human_input
+ # topic. Each browser already has its own scoped input, so do not expose
+ # the server's operator/introspection skills to robot agents.
+ own = [module for module in own if not issubclass(module.actor_class, McpServer)]
+ super().on_system_modules(own)
diff --git a/examples/microduck-world/app/microduck_world/roster.py b/examples/microduck-world/app/microduck_world/roster.py
new file mode 100644
index 0000000000..69d59969b9
--- /dev/null
+++ b/examples/microduck-world/app/microduck_world/roster.py
@@ -0,0 +1,33 @@
+# Copyright 2026 Dimensional Inc.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+"""Shared player roster, also consumed by the lobby and browser."""
+
+import json
+
+from microduck_world.scene import PROJECT_ROOT
+
+SETTINGS = json.loads((PROJECT_ROOT / "assets/scenes/apartment/multiplayer.json").read_text())
+ROSTER = SETTINGS["robots"]
+ROBOT_IDS = tuple(ROSTER)
+
+if len(ROBOT_IDS) != 6 or any(
+ sum(player["team"] == team for player in ROSTER.values()) != 3 for team in ("red", "blue")
+):
+ raise ValueError("The football roster requires three red and three blue players")
+
+
+def prefix_for(robot: str) -> str:
+ """The engine's first body remains unprefixed; player permissions are equal."""
+ return "" if robot == ROBOT_IDS[0] else robot + "_"
diff --git a/examples/microduck-world/app/microduck_world/run_robot.py b/examples/microduck-world/app/microduck_world/run_robot.py
new file mode 100644
index 0000000000..3e7f0e62fd
--- /dev/null
+++ b/examples/microduck-world/app/microduck_world/run_robot.py
@@ -0,0 +1,52 @@
+# Copyright 2026 Dimensional Inc.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+"""Run one robot blueprint against the persistent world."""
+
+import argparse
+import faulthandler
+import signal
+import threading
+
+from dimos.core.coordination.module_coordinator import ModuleCoordinator
+from microduck_world.robot_blueprint import robot_blueprint
+from microduck_world.robot_io import ROBOT_IDS
+
+
+def main() -> None:
+ parser = argparse.ArgumentParser(description=__doc__)
+ parser.add_argument("--robot", choices=ROBOT_IDS, required=True)
+ parser.add_argument("--generation", required=True)
+ args = parser.parse_args()
+ stopped = threading.Event()
+ signal.signal(signal.SIGTERM, lambda *_: stopped.set())
+ signal.signal(signal.SIGINT, lambda *_: stopped.set())
+ # A missing RPC discovery reply can stall a startup. Save the blocked stack
+ # and exit so the supervisor can replace this process and its worker group.
+ # Hosted assets are already installed; a normal build takes a few seconds.
+ faulthandler.dump_traceback_later(60, exit=True)
+ try:
+ coordinator = ModuleCoordinator.build(robot_blueprint(args.robot, args.generation))
+ finally:
+ faulthandler.cancel_dump_traceback_later()
+ try:
+ # The world owns the operator coordinator endpoint. Robot modules keep
+ # their own namespaced RPC endpoints and private MCP server.
+ stopped.wait()
+ finally:
+ coordinator.stop()
+
+
+if __name__ == "__main__":
+ main()
diff --git a/examples/microduck-world/app/microduck_world/scene.py b/examples/microduck-world/app/microduck_world/scene.py
new file mode 100644
index 0000000000..4acdf915ed
--- /dev/null
+++ b/examples/microduck-world/app/microduck_world/scene.py
@@ -0,0 +1,65 @@
+# Copyright 2026 Dimensional Inc.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+"""Load project scene artifacts and named navigation locations."""
+
+from pathlib import Path
+from typing import Self
+
+from dimos.robot.pollen.microduck.places import RoomSpec
+from dimos.simulation.scene_assets.spec import ScenePackage, load_scene_package
+from pydantic import BaseModel, ConfigDict, Field, model_validator
+
+PROJECT_ROOT = Path(__file__).resolve().parents[2]
+
+
+class WorldScene(BaseModel):
+ model_config = ConfigDict(extra="forbid", allow_inf_nan=False, str_strip_whitespace=True)
+
+ id: str = Field(min_length=1)
+ spawn_xy: tuple[float, float]
+ rooms: dict[str, RoomSpec]
+ objects: dict[str, tuple[float, float]]
+
+ @model_validator(mode="after")
+ def validate_places(self) -> Self:
+ names: set[str] = set()
+ for key, room in self.rooms.items():
+ if key != room.name:
+ raise ValueError(f"Room key {key!r} must match its name {room.name!r}")
+ xmin, xmax, ymin, ymax = room.bounds
+ if xmin >= xmax or ymin >= ymax:
+ raise ValueError(f"Room {key!r} must have increasing x/y bounds")
+ if not room.contains(room.target[0], room.target[1]):
+ raise ValueError(f"Room {key!r} navigation target must be inside its bounds")
+ for name in (room.name, *room.aliases):
+ normalized = name.strip().casefold()
+ if not normalized or normalized in names:
+ raise ValueError(f"Place name or alias is empty or ambiguous: {name!r}")
+ names.add(normalized)
+ for name in self.objects:
+ normalized = name.strip().casefold()
+ if not normalized or normalized in names:
+ raise ValueError(f"Place name is empty or ambiguous: {name!r}")
+ names.add(normalized)
+ return self
+
+
+def load_world() -> tuple[ScenePackage, WorldScene]:
+ package = load_scene_package(PROJECT_ROOT / "assets/scenes/apartment/scene.meta.json")
+ if package.mujoco_scene_path is None or package.objects_path is None:
+ raise ValueError("World scene requires MuJoCo geometry and place metadata")
+ if not package.mujoco_scene_path.is_file():
+ raise FileNotFoundError(package.mujoco_scene_path)
+ return package, WorldScene.model_validate_json(package.objects_path.read_text())
diff --git a/examples/microduck-world/app/microduck_world/scorers.py b/examples/microduck-world/app/microduck_world/scorers.py
new file mode 100644
index 0000000000..6875bc28f8
--- /dev/null
+++ b/examples/microduck-world/app/microduck_world/scorers.py
@@ -0,0 +1,63 @@
+# Copyright 2026 Dimensional Inc.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+"""Persistent scorer totals. SQLite writes run on the lobby thread, not physics."""
+
+import queue
+import sqlite3
+from pathlib import Path
+from uuid import uuid4
+
+
+class ScorerLedger:
+ def __init__(self, path: Path):
+ self.path = path
+ path.parent.mkdir(parents=True, exist_ok=True)
+ self.pending = queue.SimpleQueue()
+ with sqlite3.connect(path) as db:
+ db.execute(
+ "CREATE TABLE IF NOT EXISTS goals "
+ "(id TEXT PRIMARY KEY, user_id TEXT, handle TEXT, scored_at REAL)"
+ )
+ self.rows = self._read(db)
+
+ @staticmethod
+ def _read(db):
+ return [
+ dict(handle=handle, goals=count)
+ for handle, count in db.execute(
+ "SELECT (SELECT handle FROM goals newer WHERE newer.user_id=g.user_id "
+ "ORDER BY scored_at DESC, rowid DESC LIMIT 1), COUNT(*) "
+ "FROM goals g GROUP BY user_id ORDER BY COUNT(*) DESC, g.user_id LIMIT 20"
+ )
+ ]
+
+ def record(self, identity, scored_at):
+ self.pending.put((str(uuid4()), identity["userId"], identity["handle"], scored_at))
+
+ def flush(self):
+ batch = []
+ while not self.pending.empty():
+ batch.append(self.pending.get_nowait())
+ if not batch:
+ return
+ try:
+ with sqlite3.connect(self.path) as db:
+ db.executemany("INSERT OR IGNORE INTO goals VALUES (?,?,?,?)", batch)
+ rows = self._read(db)
+ self.rows = rows
+ except Exception:
+ for item in batch:
+ self.pending.put(item)
+ raise
diff --git a/examples/microduck-world/app/microduck_world/sensors.py b/examples/microduck-world/app/microduck_world/sensors.py
new file mode 100644
index 0000000000..673fd83fa0
--- /dev/null
+++ b/examples/microduck-world/app/microduck_world/sensors.py
@@ -0,0 +1,211 @@
+# Copyright 2026 Dimensional Inc.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+"""Render synchronized, occluded measurements for each robot on a private scene copy."""
+
+import math
+import threading
+import time
+from collections.abc import Callable
+from copy import copy
+
+import mujoco
+import numpy as np
+from dimos.msgs.geometry_msgs.PoseStamped import PoseStamped
+from dimos.msgs.geometry_msgs.Quaternion import Quaternion
+from dimos.msgs.geometry_msgs.Transform import Transform
+from dimos.msgs.geometry_msgs.Vector3 import Vector3
+from dimos.msgs.sensor_msgs.CameraInfo import CameraInfo
+from dimos.msgs.sensor_msgs.Image import Image, ImageFormat
+from dimos.msgs.tf2_msgs.TFMessage import TFMessage
+from dimos.robot.pollen.microduck.sim_module import LIDAR_CAMERA_SPECS
+from dimos.simulation.engines.mujoco_engine import camera_ray_directions
+from dimos.utils.logging_config import setup_logger
+from microduck_world.camera import HEAD_CAMERA
+from microduck_world.football import Scoreboard
+from microduck_world.robot_io import RobotVision
+from numpy.typing import NDArray
+
+logger = setup_logger()
+SENSOR_SIZE = (640, 360)
+SENSOR_FPS = 3.0
+LIDAR_SIZE = (96, 48)
+
+
+class RobotSensors:
+ def __init__(self, model: mujoco.MjModel, publish: Callable[[str, RobotVision], None]) -> None:
+ self._source = model
+ self._publish = publish
+ self._lock = threading.Lock()
+ self._stop = threading.Event()
+ self._snapshot: (
+ tuple[NDArray[np.float64], dict[str, tuple[str, tuple[float, float]]], list[int]] | None
+ ) = None
+ self._thread = threading.Thread(target=self._run, name="robot-sensors", daemon=True)
+ self._error: str | None = None
+
+ @property
+ def error(self) -> str | None:
+ with self._lock:
+ return self._error
+
+ def start(self) -> None:
+ self._thread.start()
+
+ def snapshot(
+ self,
+ qpos: NDArray[np.float64],
+ assignments: dict[str, tuple[str, tuple[float, float]]],
+ lit: list[int] | None = None,
+ ) -> None:
+ with self._lock:
+ self._snapshot = (qpos.copy(), dict(assignments), list(lit or []))
+
+ def close(self) -> None:
+ self._stop.set()
+ self._thread.join(timeout=10)
+ if self._thread.is_alive():
+ raise RuntimeError("Robot sensor renderer did not stop")
+
+ def _run(self) -> None:
+ renderer: mujoco.Renderer | None = None
+ try:
+ model = copy(self._source)
+ scoreboard = Scoreboard(model)
+ data = mujoco.MjData(model)
+ renderer = mujoco.Renderer(model, height=SENSOR_SIZE[1], width=SENSOR_SIZE[0])
+ option = mujoco.MjvOption()
+ option.geomgroup[:] = [1, 1, 1, 0, 0, 0]
+ directions = camera_ray_directions(*LIDAR_SIZE, 140.0)
+ while not self._stop.is_set():
+ started = time.monotonic()
+ with self._lock:
+ current = self._snapshot
+ if current is None:
+ self._stop.wait(0.05)
+ continue
+ qpos, assignments, lit = current
+ scoreboard.apply(model, lit)
+ data.qpos[:] = qpos
+ mujoco.mj_forward(model, data)
+ for id, (generation, origin) in assignments.items():
+ if self._stop.is_set():
+ break
+ prefix = "" if id == "duck1" else id + "_"
+ camera = model.camera(prefix + HEAD_CAMERA).id
+ ts = time.time()
+ offset = np.array([*origin, 0.0])
+ position = data.cam_xpos[camera].copy() - offset
+ optical = data.cam_xmat[camera].reshape(3, 3) @ np.diag([1, -1, -1])
+ orientation = Quaternion.from_rotation_matrix(optical)
+ pose = PoseStamped(
+ position=position.tolist(), orientation=orientation, frame_id="world", ts=ts
+ )
+ width, height = SENSOR_SIZE
+ focal = height / (2 * math.tan(math.radians(float(model.cam_fovy[camera])) / 2))
+ info = CameraInfo.from_intrinsics(
+ focal,
+ focal,
+ width / 2,
+ height / 2,
+ width,
+ height,
+ frame_id="camera_optical",
+ )
+ info.ts = ts
+ renderer.update_scene(data, camera=camera, scene_option=option)
+ image = renderer.render().copy()
+ renderer.enable_depth_rendering()
+ depth = renderer.render().copy()
+ renderer.disable_depth_rendering()
+ points = self._range_points(model, data, prefix, directions) - offset
+ tf = TFMessage(
+ Transform(
+ translation=Vector3(*position),
+ rotation=orientation,
+ frame_id="world",
+ child_frame_id="camera_optical",
+ ts=ts,
+ )
+ )
+ self._publish(
+ id,
+ RobotVision(
+ generation,
+ Image(
+ data=image, format=ImageFormat.RGB, frame_id="camera_optical", ts=ts
+ ),
+ Image(
+ data=depth,
+ format=ImageFormat.DEPTH,
+ frame_id="camera_optical",
+ ts=ts,
+ ),
+ info,
+ pose,
+ tf,
+ points.astype(np.float32),
+ ),
+ )
+ self._stop.wait(max(0.001, 1 / SENSOR_FPS - (time.monotonic() - started)))
+ except Exception as exc:
+ with self._lock:
+ self._error = str(exc)
+ logger.exception("Robot sensor pipeline failed")
+ finally:
+ if renderer is not None:
+ renderer.close()
+
+ @staticmethod
+ def _range_points(
+ model: mujoco.MjModel, data: mujoco.MjData, prefix: str, directions: NDArray[np.float64]
+ ) -> NDArray[np.float64]:
+ root = model.body(prefix + "trunk_base").id
+ own = []
+ for geom in range(model.ngeom):
+ ancestor = int(model.geom_bodyid[geom])
+ while ancestor and ancestor != root:
+ ancestor = int(model.body_parentid[ancestor])
+ if ancestor == root:
+ own.append(geom)
+ groups = model.geom_group[own].copy()
+ model.geom_group[own] = 5
+ hits = []
+ try:
+ for name, _ in LIDAR_CAMERA_SPECS:
+ camera = model.camera(prefix + name).id
+ origin = data.cam_xpos[camera].copy()
+ rays = directions @ data.cam_xmat[camera].reshape(3, 3).T
+ geom_ids = np.full(len(rays), -1, dtype=np.int32)
+ distance = np.full(len(rays), -1.0, dtype=np.float64)
+ mujoco.mj_multiRay(
+ model,
+ data,
+ origin,
+ rays.ravel(),
+ np.array([1, 0, 0, 1, 0, 0], dtype=np.uint8),
+ 1,
+ -1,
+ geom_ids,
+ distance,
+ None,
+ len(rays),
+ 6.0,
+ )
+ valid = (distance >= 0.05) & (distance <= 6.0)
+ valid &= np.abs(directions[:, 1] * distance) <= 0.6
+ hits.append(origin + rays[valid] * distance[valid, None])
+ finally:
+ model.geom_group[own] = groups
+ return np.vstack(hits)
diff --git a/examples/microduck-world/app/microduck_world/supervisor.py b/examples/microduck-world/app/microduck_world/supervisor.py
new file mode 100644
index 0000000000..2ff9e1c8f0
--- /dev/null
+++ b/examples/microduck-world/app/microduck_world/supervisor.py
@@ -0,0 +1,147 @@
+# Copyright 2026 Dimensional Inc.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+"""One independently restartable DimOS runtime per robot visitor generation."""
+
+import json
+import os
+import signal
+import subprocess
+import sys
+import threading
+import time
+from contextlib import suppress
+from dataclasses import dataclass
+from typing import IO, Any
+
+import requests
+from dimos.core.core import rpc
+from dimos.core.module import Module
+from dimos.utils.logging_config import setup_logger
+from microduck_world.relay import relay_settings
+from microduck_world.robot_io import VISITOR_IDS
+from microduck_world.scene import PROJECT_ROOT
+
+logger = setup_logger()
+
+
+@dataclass
+class RobotProcess:
+ generation: str
+ process: subprocess.Popen[bytes]
+ log: IO[bytes]
+
+ def close(self) -> None:
+ try:
+ self.process.send_signal(signal.SIGTERM)
+ except ProcessLookupError:
+ pass
+ try:
+ self.process.wait(timeout=15)
+ except subprocess.TimeoutExpired:
+ os.killpg(self.process.pid, signal.SIGKILL)
+ self.process.wait(timeout=5)
+ finally:
+ # A crashed/timed-out parent can leave live workers behind even when
+ # wait() returns immediately. Every runtime owns its process group.
+ with suppress(ProcessLookupError):
+ os.killpg(self.process.pid, signal.SIGKILL)
+ self.log.close()
+
+
+class RobotSupervisor(Module):
+ def __init__(self, **kwargs: Any) -> None:
+ super().__init__(**kwargs)
+ self._stop = threading.Event()
+ self._thread: threading.Thread | None = None
+ self._children: dict[str, RobotProcess] = {}
+
+ @rpc
+ def start(self) -> None:
+ super().start()
+ self._thread = threading.Thread(target=self._run, name="robot-supervisor", daemon=True)
+ self._thread.start()
+
+ @rpc
+ def stop(self) -> None:
+ self._stop.set()
+ if self._thread is not None:
+ self._thread.join(timeout=65)
+ if self._thread.is_alive():
+ raise RuntimeError("Robot runtimes did not stop")
+ super().stop()
+
+ def _run(self) -> None:
+ settings = relay_settings()
+ wanted: dict[str, str] = {}
+ last_ok = 0.0
+ retry: dict[str, float] = {}
+ try:
+ with requests.Session() as http:
+ http.trust_env = False
+ while not self._stop.is_set():
+ try:
+ response = http.get(
+ settings.upstream_url + "/internal/assignments", timeout=1
+ )
+ response.raise_for_status()
+ value = response.json()
+ if not isinstance(value, dict) or not all(
+ k in VISITOR_IDS and isinstance(v, str) and v for k, v in value.items()
+ ):
+ raise ValueError("Invalid robot assignments")
+ wanted = value
+ last_ok = time.monotonic()
+ except (requests.RequestException, ValueError):
+ if time.monotonic() - last_ok > 3:
+ wanted = {}
+ for id, child in list(self._children.items()):
+ if child.generation != wanted.get(id) or child.process.poll() is not None:
+ child.close()
+ del self._children[id]
+ for id, generation in wanted.items():
+ if id in self._children or time.monotonic() < retry.get(id, 0):
+ continue
+ log = (PROJECT_ROOT / f"logs/{id}.log").open("ab")
+ process = subprocess.Popen(
+ [
+ sys.executable,
+ "-m",
+ "microduck_world.run_robot",
+ "--robot",
+ id,
+ "--generation",
+ generation,
+ ],
+ cwd=PROJECT_ROOT,
+ stdout=log,
+ stderr=subprocess.STDOUT,
+ start_new_session=True,
+ )
+ self._children[id] = RobotProcess(generation, process, log)
+ retry[id] = time.monotonic() + 10
+ logger.info("Started robot runtime", robot=id, pid=process.pid)
+ (PROJECT_ROOT / "state/robot-runtimes.json").write_text(
+ json.dumps(
+ {
+ id: {"generation": c.generation, "pid": c.process.pid}
+ for id, c in self._children.items()
+ }
+ )
+ )
+ self._stop.wait(0.5)
+ finally:
+ for child in self._children.values():
+ child.close()
+ self._children.clear()
diff --git a/examples/microduck-world/app/microduck_world/test_ball_detection.py b/examples/microduck-world/app/microduck_world/test_ball_detection.py
new file mode 100644
index 0000000000..fa716c55ce
--- /dev/null
+++ b/examples/microduck-world/app/microduck_world/test_ball_detection.py
@@ -0,0 +1,79 @@
+# Copyright 2026 Dimensional Inc.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import base64
+import time
+from types import SimpleNamespace
+from unittest.mock import Mock
+
+import cv2
+import numpy as np
+from dimos.msgs.sensor_msgs.Image import Image, ImageFormat
+from microduck_world.ball_detection import BallPerception
+
+
+def vision(generation="first"):
+ pixels = np.zeros((36, 64, 3), dtype=np.uint8)
+ pixels[:, :, 0] = 220
+ return SimpleNamespace(
+ generation=generation, image=Image(data=pixels, format=ImageFormat.RGB, ts=time.time())
+ )
+
+
+def test_shared_queue_is_bounded_latest_per_duck_and_fair(module_factory):
+ module = module_factory(BallPerception)
+ for i in range(1, 7):
+ module.receive(f"duck{i}", vision())
+ latest = vision("second")
+ module.receive("duck1", latest)
+ assert len(module._pending) == 6
+ assert list(module._pending)[0] == "duck1"
+ assert module._pending["duck1"] is latest
+ assert module._generations["duck1"] == "second"
+ module.receive("unknown", vision())
+ assert len(module._pending) == 6
+
+
+def test_frame_and_boxes_share_camera_identity_and_pixels(module_factory):
+ module = module_factory(BallPerception)
+ module._detector = Mock(device="cpu")
+ boxes = Mock()
+ boxes.xyxy.cpu().tolist.return_value = [[2, 3, 22, 23], [1, 1, 8, 8]]
+ boxes.conf.cpu().tolist.return_value = [0.9, 0.8]
+ boxes.cls.cpu().tolist.return_value = [32, 0]
+ module._detector.model.predict.return_value = [SimpleNamespace(boxes=boxes)]
+ camera = vision()
+ payload, observation = module.process("duck3", camera)
+ assert payload["robot"] == "duck3"
+ assert payload["generation"] == "first"
+ assert payload["ts"] == camera.image.ts
+ assert payload["boxes"] == [{"xyxy": (2.0, 3.0, 22.0, 23.0), "confidence": 0.9}]
+ assert observation.robot == "duck3"
+ assert observation.timestamp == camera.image.ts
+ module._detector.model.track.assert_not_called()
+ assert module._detector.model.predict.call_args.kwargs["classes"] == [32]
+ pixels = cv2.imdecode(
+ np.frombuffer(base64.b64decode(payload["image"].split(",")[1]), dtype=np.uint8),
+ cv2.IMREAD_COLOR,
+ )
+ assert pixels.shape == (36, 64, 3)
+ assert pixels[0, 0, 2] > 200 and pixels[0, 0, 0] < 10
+
+
+def test_detector_failure_is_distinct_from_a_frame_with_no_ball(module_factory):
+ module = module_factory(BallPerception)
+ payload, observation = module.process("duck1", vision())
+ assert payload["status"] == "unavailable"
+ assert payload["image"].startswith("data:image/jpeg;base64,")
+ assert observation is None
diff --git a/examples/microduck-world/app/microduck_world/test_ball_physics.py b/examples/microduck-world/app/microduck_world/test_ball_physics.py
new file mode 100644
index 0000000000..e11600fb6b
--- /dev/null
+++ b/examples/microduck-world/app/microduck_world/test_ball_physics.py
@@ -0,0 +1,127 @@
+# Copyright 2026 Dimensional Inc.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+"""Check compiled contacts in the six-robot scene, including the benchmark ball."""
+
+import mujoco
+import numpy as np
+import pytest
+from dimos.robot.pollen.microduck.assets_fetch import ensure_assets
+from microduck_world.ball_physics import FLOOR_NAMES
+from microduck_world.football import BALL_NAMES, FootballMatch
+from microduck_world.scene import load_world
+from microduck_world.visual_scene import export_scene
+from microduck_world.world_sim import WorldSimModule
+
+
+@pytest.fixture(scope="module")
+def world():
+ assets = ensure_assets()
+ module = WorldSimModule(
+ scene_xml=load_world()[0].mujoco_scene_path,
+ robot_mjcf=str(assets.robot_mjcf("default")),
+ headless=True,
+ auto_stand=False,
+ )
+ spec = module._compose_spec()
+ return spec, spec.compile()
+
+
+def test_threejs_receives_the_compiled_ball_radius(world):
+ _, model = world
+ scene = export_scene(model, {})
+ geoms = {g["name"]: g for g in scene["geoms"]}
+ for name in BALL_NAMES:
+ assert geoms[name + "_geom"]["kind"] == "sphere"
+ assert geoms[name + "_geom"]["size"] == [0.05, 0, 0]
+
+
+@pytest.mark.parametrize("name", BALL_NAMES)
+def test_every_ball_has_upstream_mass_inertia_contacts_and_clear_reset_height(world, name):
+ spec, model = world
+ geom, body = model.geom(name + "_geom"), model.body(name)
+ np.testing.assert_allclose(geom.size, [0.05, 0, 0])
+ np.testing.assert_allclose(body.mass, [0.03])
+ np.testing.assert_allclose(body.inertia, [0.00003] * 3)
+ np.testing.assert_allclose(geom.friction, [0.4, 0.01, 0.003])
+ np.testing.assert_allclose(geom.solref, [0.03, 0.4])
+ np.testing.assert_allclose(geom.solimp, [0.9, 0.95, 0.001, 0.5, 2])
+ assert geom.condim == 6
+ adr = int(model.joint(name + "_freejoint").qposadr[0])
+ assert model.qpos0[adr + 2] == pytest.approx(0.051)
+ data = mujoco.MjData(model)
+ mujoco.mj_forward(model, data)
+ assert all(geom.id not in c.geom or c.dist >= 0 for c in data.contact)
+ data.qpos[adr + 2] = 1
+ mujoco.mj_resetData(model, data)
+ assert data.qpos[adr + 2] == pytest.approx(0.051)
+ for patch in spec.geoms:
+ if patch.name.startswith(name + "_patch_"):
+ assert patch.mass == 0 and patch.contype == 0 and patch.conaffinity == 0
+ # Scaling the paint must leave it at the enlarged ball's surface.
+ assert 0.05 < np.linalg.norm(patch.pos) + patch.size[2] < 0.051
+
+
+@pytest.mark.parametrize("ball", BALL_NAMES)
+@pytest.mark.parametrize("floor", FLOOR_NAMES)
+def test_compiled_ball_floor_contact_mixing(world, ball, floor):
+ _, model = world
+ data = mujoco.MjData(model)
+ mujoco.mj_forward(model, data)
+ ground = model.geom(floor)
+ sphere = model.geom(ball + "_geom")
+ np.testing.assert_allclose(ground.friction, [1, 0.005, 0.0001])
+ np.testing.assert_allclose(ground.solref, [0.02, 1])
+ np.testing.assert_allclose(ground.solimp, [0.9, 0.95, 0.001, 0.5, 2])
+ assert ground.condim == 3
+ for geom in (ground, sphere):
+ assert geom.priority == 0 and geom.solmix == 1
+ assert geom.margin == 0 and geom.gap == 0
+ adr = int(model.joint(ball + "_freejoint").qposadr[0])
+ # A slight overlap only in this fixture exposes the compiled contact pair.
+ data.qpos[adr : adr + 3] = data.geom_xpos[ground.id] + [0, 0, ground.size[2] + 0.0499]
+ mujoco.mj_forward(model, data)
+ contacts = [c for c in data.contact if set(c.geom) == {ground.id, sphere.id}]
+ assert len(contacts) == 1
+ contact = contacts[0]
+ assert contact.dim == 6
+ np.testing.assert_allclose(contact.friction, [1, 1, 0.01, 0.003, 0.003])
+ np.testing.assert_allclose(contact.solref, [0.025, 0.7])
+ np.testing.assert_allclose(contact.solimp, [0.9, 0.95, 0.001, 0.5, 2])
+ assert model.opt.timestep == 0.005
+ np.testing.assert_allclose(model.opt.gravity, [0, 0, -9.81])
+
+
+@pytest.mark.parametrize("ball", BALL_NAMES)
+@pytest.mark.parametrize("sign, expected", [(1, [1, 0]), (-1, [0, 1])])
+def test_each_actual_scene_ball_physically_scores_without_reset_or_impulse(
+ world, ball, sign, expected
+):
+ _, model = world
+ data = mujoco.MjData(model)
+ match = FootballMatch(model)
+ joint = model.joint(ball + "_freejoint")
+ adr, dof = int(joint.qposadr[0]), int(joint.dofadr[0])
+ # Initial rolling shot is a test fixture; match.update cannot alter physics.
+ data.qpos[adr : adr + 3] = [sign * 0.85, 4.3, 0.05]
+ data.qvel[dof] = sign
+ data.qvel[dof + 4] = sign / 0.05
+ for _ in range(400):
+ mujoco.mj_step(model, data)
+ before = data.qpos.copy(), data.qvel.copy()
+ match.update(data)
+ np.testing.assert_array_equal(data.qpos, before[0])
+ np.testing.assert_array_equal(data.qvel, before[1])
+ assert match.scores == expected
+ assert 1.356 < sign * data.qpos[adr] < 1.74
diff --git a/examples/microduck-world/app/microduck_world/test_camera.py b/examples/microduck-world/app/microduck_world/test_camera.py
new file mode 100644
index 0000000000..47cd2e1a34
--- /dev/null
+++ b/examples/microduck-world/app/microduck_world/test_camera.py
@@ -0,0 +1,55 @@
+# Copyright 2026 Dimensional Inc.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import mujoco
+import numpy as np
+import pytest
+from microduck_world.camera import configure_clipping, configure_head_camera
+
+
+def test_camera_uses_published_mount_and_follows_head_motion():
+ spec = mujoco.MjSpec.from_string("""