search challenge: triangle marker on a crate's back face (rendered in vision, in digest), report tool with distance verification, smooth turn(duration), --free/--search autonomous modes, look_at crates; verified with gpt-5.6-luna
This commit is contained in:
@@ -114,6 +114,7 @@ class Raycaster:
|
||||
self._draw_beacon(room, pixels, depth, fx, fz, rx, rz, eye, tan_pitch)
|
||||
|
||||
img = Image.frombuffer("RGB", (w, h), bytes(pixels), "raw", "RGB", 0, 1)
|
||||
self._draw_marker(room, img, depth, fx, fz, rx, rz, eye, tan_pitch)
|
||||
grid = self._depth_grid(depth, max_depth)
|
||||
return img, grid
|
||||
|
||||
@@ -260,6 +261,65 @@ class Raycaster:
|
||||
col = FLOOR_LINE
|
||||
return _fog(col, t)
|
||||
|
||||
def _draw_marker(
|
||||
self,
|
||||
room: Room,
|
||||
img: Image.Image,
|
||||
depth: list[list[float]],
|
||||
fx: float,
|
||||
fz: float,
|
||||
rx: float,
|
||||
rz: float,
|
||||
eye: float,
|
||||
tan_pitch: float,
|
||||
) -> None:
|
||||
"""Draw the search-challenge triangle (a sprite on the crate's back
|
||||
face), only when the anchor pixel is actually that face."""
|
||||
m = room.marker
|
||||
if m is None:
|
||||
return
|
||||
pos = room.marker_world_pos()
|
||||
if pos is None:
|
||||
return
|
||||
w, h = self.width, self.height
|
||||
cam = room.capsule
|
||||
rel_x = pos[0] - cam.x
|
||||
rel_z = pos[2] - cam.z
|
||||
along = rel_x * fx + rel_z * fz
|
||||
if along < 0.3:
|
||||
return
|
||||
right = rel_x * rx + rel_z * rz
|
||||
col_c = (right / along / self.tan_fx + 1.0) / 2.0 * w
|
||||
vv = (pos[1] - eye) / along - tan_pitch
|
||||
row_c = (1.0 - vv / self.tan_fy) / 2.0 * h
|
||||
r0, c0 = round(row_c), round(col_c)
|
||||
if not (0 <= r0 < h and 0 <= c0 < w):
|
||||
return
|
||||
if abs(depth[r0][c0] - along) > 0.25:
|
||||
return # the anchor pixel is not the marker's face (occluded)
|
||||
half = max(2.5, m["size"] / along / self.tan_fx * w / 2.0)
|
||||
draw = ImageDraw.Draw(img)
|
||||
color = tuple(m["color"])
|
||||
glow = tuple(min(255, c + 90) for c in color)
|
||||
draw.ellipse(
|
||||
[
|
||||
col_c - half * 2.2,
|
||||
row_c - half * 2.2,
|
||||
col_c + half * 2.2,
|
||||
row_c + half * 2.2,
|
||||
],
|
||||
fill=glow,
|
||||
)
|
||||
draw.polygon(
|
||||
[
|
||||
(col_c, row_c - half),
|
||||
(col_c - half * 0.9, row_c + half * 0.7),
|
||||
(col_c + half * 0.9, row_c + half * 0.7),
|
||||
],
|
||||
fill=color,
|
||||
outline=(255, 255, 255),
|
||||
)
|
||||
|
||||
def _draw_beacon(
|
||||
self,
|
||||
room: Room,
|
||||
@@ -405,6 +465,22 @@ def render_topdown(room: Room, size: int = 400) -> Image.Image:
|
||||
fx, fz = c.forward()
|
||||
tip = xy(c.x + fx * 0.7, c.z + fz * 0.7)
|
||||
draw.line([(cx, cz), tip], fill=(30, 30, 40), width=3)
|
||||
|
||||
m = room.marker
|
||||
if m is not None:
|
||||
pos = room.marker_world_pos()
|
||||
if pos is not None:
|
||||
mx, mz = xy(pos[0], pos[2])
|
||||
r = m["size"] * scale
|
||||
draw.polygon(
|
||||
[
|
||||
(mx, mz - r * 1.4),
|
||||
(mx - r, mz + r),
|
||||
(mx + r, mz + r),
|
||||
],
|
||||
fill=tuple(m["color"]),
|
||||
outline=(20, 20, 26),
|
||||
)
|
||||
return img
|
||||
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@ from __future__ import annotations
|
||||
|
||||
import math
|
||||
from dataclasses import dataclass
|
||||
from typing import Any
|
||||
|
||||
ROOM_SIZE = 16.0
|
||||
ROOM_HEIGHT = 3.0
|
||||
@@ -124,6 +125,62 @@ class Room:
|
||||
]
|
||||
# Pending audio events, drained by the hear sensor.
|
||||
self._audio: list[dict] = []
|
||||
# Search challenge: a small triangle painted on the BACK side of one
|
||||
# of the crates (the face pointing away from the spawn corner). The
|
||||
# agent must find it by exploring and looking with vision.
|
||||
self.marker: dict[str, Any] = self._place_marker()
|
||||
|
||||
# ---------- search marker ----------
|
||||
|
||||
def _place_marker(self) -> dict[str, Any]:
|
||||
"""Put the triangle on a random crate, on the face most hidden from
|
||||
the spawn corner (1.5, 1.5)."""
|
||||
import random
|
||||
|
||||
box = random.choice(self.boxes)
|
||||
to_spawn = (1.5 - box.cx, 1.5 - box.cz)
|
||||
normals = {
|
||||
"x0": (-1.0, 0.0),
|
||||
"x1": (1.0, 0.0),
|
||||
"z0": (0.0, -1.0),
|
||||
"z1": (0.0, 1.0),
|
||||
}
|
||||
faces = sorted(
|
||||
normals,
|
||||
key=lambda f: normals[f][0] * to_spawn[0] + normals[f][1] * to_spawn[1],
|
||||
)[:2] # the two faces pointing away from spawn
|
||||
return {
|
||||
"box": box.id,
|
||||
"face": random.choice(faces),
|
||||
"u": 0.5,
|
||||
"v": 0.55,
|
||||
"size": 0.42,
|
||||
"color": (255, 150, 40),
|
||||
}
|
||||
|
||||
def marker_world_pos(self) -> tuple[float, float, float] | None:
|
||||
"""World coordinates (x, y, z) of the marker center, or None."""
|
||||
m = self.marker
|
||||
if m is None:
|
||||
return None
|
||||
box = next((b for b in self.boxes if b.id == m["box"]), None)
|
||||
if box is None:
|
||||
return None
|
||||
if m["face"] == "x0":
|
||||
x, z = box.x_min(), box.cz - box.half_d + m["u"] * 2 * box.half_d
|
||||
elif m["face"] == "x1":
|
||||
x, z = box.x_max(), box.cz - box.half_d + m["u"] * 2 * box.half_d
|
||||
elif m["face"] == "z0":
|
||||
x, z = box.cx - box.half_w + m["u"] * 2 * box.half_w, box.z_min()
|
||||
else:
|
||||
x, z = box.cx - box.half_w + m["u"] * 2 * box.half_w, box.z_max()
|
||||
return (x, m["v"] * box.height, z)
|
||||
|
||||
def distance_to_marker(self) -> float:
|
||||
pos = self.marker_world_pos()
|
||||
if pos is None:
|
||||
return float("inf")
|
||||
return math.hypot(pos[0] - self.capsule.x, pos[2] - self.capsule.z)
|
||||
|
||||
# ---------- ticks ----------
|
||||
|
||||
|
||||
Reference in New Issue
Block a user