testbed: physics fixes (rest-at-contact), pytest suite (33 tests green)
This commit is contained in:
@@ -0,0 +1,3 @@
|
|||||||
|
[tool.pytest.ini_options]
|
||||||
|
asyncio_mode = "auto"
|
||||||
|
testpaths = ["testbed/tests"]
|
||||||
+34
-25
@@ -183,29 +183,50 @@ class Room:
|
|||||||
fx, fz = self.capsule.forward()
|
fx, fz = self.capsule.forward()
|
||||||
while remaining > 0.0 and hit is None:
|
while remaining > 0.0 and hit is None:
|
||||||
step = min(0.1, remaining)
|
step = min(0.1, remaining)
|
||||||
|
bx, bz = self.capsule.x, self.capsule.z
|
||||||
ok, h = self._try_displace(fx * step, fz * step)
|
ok, h = self._try_displace(fx * step, fz * step)
|
||||||
|
moved += math.hypot(self.capsule.x - bx, self.capsule.z - bz)
|
||||||
if not ok and h is not None:
|
if not ok and h is not None:
|
||||||
h.impulse = remaining
|
h.impulse = max(0.0, distance - moved)
|
||||||
hit = h
|
hit = h
|
||||||
break
|
break
|
||||||
moved += step
|
|
||||||
remaining -= step
|
remaining -= step
|
||||||
self.capsule.speed = moved / 0.1 if moved > 0 else 0.0
|
self.capsule.speed = moved / 0.1 if moved > 0 else 0.0
|
||||||
return moved, hit
|
return moved, hit
|
||||||
|
|
||||||
def _try_displace(self, dx: float, dz: float) -> tuple[bool, Hit | None]:
|
def _try_displace(self, dx: float, dz: float) -> tuple[bool, Hit | None]:
|
||||||
"""Move by (dx, dz) with circle-vs-AABB resolution. Returns (moved, hit)."""
|
"""Move by (dx, dz) with circle-vs-AABB resolution. Returns (moved, hit).
|
||||||
|
|
||||||
|
The capsule always ends at the resolved position: on a blocked step it
|
||||||
|
rests at the contact point (wall or box face).
|
||||||
|
"""
|
||||||
cap = self.capsule
|
cap = self.capsule
|
||||||
|
r = cap.radius
|
||||||
nx = cap.x + dx
|
nx = cap.x + dx
|
||||||
nz = cap.z + dz
|
nz = cap.z + dz
|
||||||
|
|
||||||
# Room walls: clamp to bounds (the capsule cannot leave the room).
|
|
||||||
r = cap.radius
|
|
||||||
if nx < r or nx > ROOM_SIZE - r or nz < r or nz > ROOM_SIZE - r:
|
|
||||||
hit = self._wall_hit(nx, nz)
|
|
||||||
return False, hit
|
|
||||||
|
|
||||||
blocked: Hit | None = None
|
blocked: Hit | None = None
|
||||||
|
if nx < r:
|
||||||
|
nx, blocked = (
|
||||||
|
r,
|
||||||
|
Hit(other="wall_west", normal_x=1.0, normal_z=0.0, impulse=0.0),
|
||||||
|
)
|
||||||
|
elif nx > ROOM_SIZE - r:
|
||||||
|
nx, blocked = (
|
||||||
|
ROOM_SIZE - r,
|
||||||
|
Hit(other="wall_east", normal_x=-1.0, normal_z=0.0, impulse=0.0),
|
||||||
|
)
|
||||||
|
elif nz < r:
|
||||||
|
nz, blocked = (
|
||||||
|
r,
|
||||||
|
Hit(other="wall_north", normal_x=0.0, normal_z=1.0, impulse=0.0),
|
||||||
|
)
|
||||||
|
elif nz > ROOM_SIZE - r:
|
||||||
|
nz, blocked = (
|
||||||
|
ROOM_SIZE - r,
|
||||||
|
Hit(other="wall_south", normal_x=0.0, normal_z=-1.0, impulse=0.0),
|
||||||
|
)
|
||||||
|
|
||||||
for _ in range(8):
|
for _ in range(8):
|
||||||
contact = self._box_contact(nx, nz, r)
|
contact = self._box_contact(nx, nz, r)
|
||||||
if contact is None:
|
if contact is None:
|
||||||
@@ -214,22 +235,10 @@ class Room:
|
|||||||
blocked = Hit(
|
blocked = Hit(
|
||||||
other=other, normal_x=normal_x, normal_z=normal_z, impulse=0.0
|
other=other, normal_x=normal_x, normal_z=normal_z, impulse=0.0
|
||||||
)
|
)
|
||||||
if blocked is not None:
|
|
||||||
return False, blocked
|
|
||||||
|
|
||||||
cap.x = nx
|
cap.x = nx
|
||||||
cap.z = nz
|
cap.z = nz
|
||||||
return True, None
|
return blocked is None, blocked
|
||||||
|
|
||||||
def _wall_hit(self, nx: float, nz: float) -> Hit:
|
|
||||||
r = self.capsule.radius
|
|
||||||
if nx < r:
|
|
||||||
return Hit(other="wall_west", normal_x=1.0, normal_z=0.0, impulse=0.0)
|
|
||||||
if nx > ROOM_SIZE - r:
|
|
||||||
return Hit(other="wall_east", normal_x=-1.0, normal_z=0.0, impulse=0.0)
|
|
||||||
if nz < r:
|
|
||||||
return Hit(other="wall_north", normal_x=0.0, normal_z=1.0, impulse=0.0)
|
|
||||||
return Hit(other="wall_south", normal_x=0.0, normal_z=-1.0, impulse=0.0)
|
|
||||||
|
|
||||||
def _box_contact(
|
def _box_contact(
|
||||||
self, cx: float, cz: float, r: float
|
self, cx: float, cz: float, r: float
|
||||||
@@ -265,10 +274,10 @@ class Room:
|
|||||||
return (cx, nz, 0.0, 1.0 if cz < box.cz else -1.0, box.id)
|
return (cx, nz, 0.0, 1.0 if cz < box.cz else -1.0, box.id)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def turn(self, yaw_delta: float = 0.0, pitch_delta: float = 0.0) -> None:
|
def turn(self, yaw_deg: float = 0.0, pitch_deg: float = 0.0) -> None:
|
||||||
cap = self.capsule
|
cap = self.capsule
|
||||||
cap.yaw_deg = (cap.yaw_deg + yaw_delta) % 360.0
|
cap.yaw_deg = (cap.yaw_deg + yaw_deg) % 360.0
|
||||||
cap.pitch_deg = min(85.0, max(-85.0, cap.pitch_deg + pitch_delta))
|
cap.pitch_deg = min(85.0, max(-85.0, cap.pitch_deg + pitch_deg))
|
||||||
|
|
||||||
def face(self, yaw_deg: float, pitch_deg: float) -> None:
|
def face(self, yaw_deg: float, pitch_deg: float) -> None:
|
||||||
self.capsule.yaw_deg = yaw_deg % 360.0
|
self.capsule.yaw_deg = yaw_deg % 360.0
|
||||||
|
|||||||
@@ -0,0 +1,189 @@
|
|||||||
|
"""Tests for the testbed bridge: tool behavior over the in-process transport."""
|
||||||
|
|
||||||
|
import base64
|
||||||
|
import io
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
from aicc.client import AICCClient
|
||||||
|
from aicc.errors import ToolError
|
||||||
|
from aicc.transport.in_process import InProcessTransport
|
||||||
|
|
||||||
|
from testbed.bridge import (
|
||||||
|
DEPTH_HEIGHT,
|
||||||
|
DEPTH_WIDTH,
|
||||||
|
VISION_HEIGHT,
|
||||||
|
VISION_WIDTH,
|
||||||
|
build_bridge,
|
||||||
|
)
|
||||||
|
from testbed.room.world import Room
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
async def bridge():
|
||||||
|
b = build_bridge()
|
||||||
|
yield b
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
async def client(bridge):
|
||||||
|
t = InProcessTransport.start(bridge)
|
||||||
|
async with t:
|
||||||
|
client = AICCClient(t)
|
||||||
|
async with client:
|
||||||
|
yield client
|
||||||
|
|
||||||
|
|
||||||
|
async def test_manifest_lists_all_tools(client):
|
||||||
|
m = await client.handshake()
|
||||||
|
ids = [t.id for t in m.tools]
|
||||||
|
for expected in (
|
||||||
|
"proprioception",
|
||||||
|
"vision",
|
||||||
|
"depth",
|
||||||
|
"hear",
|
||||||
|
"world_query",
|
||||||
|
"move",
|
||||||
|
"turn",
|
||||||
|
"look_at",
|
||||||
|
"interact",
|
||||||
|
"echo",
|
||||||
|
"boom",
|
||||||
|
"bump",
|
||||||
|
):
|
||||||
|
assert expected in ids
|
||||||
|
assert "testbed_room_01" in m.world.name
|
||||||
|
|
||||||
|
|
||||||
|
async def test_proprioception(client):
|
||||||
|
await client.handshake()
|
||||||
|
out = (await client.call_tool("proprioception", {})).output
|
||||||
|
assert out["position"] == {"x": 1.5, "y": 0.0, "z": 1.5}
|
||||||
|
assert out["rotation"]["yaw_deg"] == 45.0
|
||||||
|
assert out["health"] == 100.0
|
||||||
|
assert out["tick"] == 1
|
||||||
|
|
||||||
|
|
||||||
|
async def test_vision_returns_png(client):
|
||||||
|
await client.handshake()
|
||||||
|
out = (await client.call_tool("vision", {})).output
|
||||||
|
assert out["width"] == VISION_WIDTH
|
||||||
|
assert out["height"] == VISION_HEIGHT
|
||||||
|
raw = base64.b64decode(out["png_b64"])
|
||||||
|
assert raw[:8] == b"\x89PNG\r\n\x1a\n"
|
||||||
|
img = io.BytesIO(raw)
|
||||||
|
from PIL import Image
|
||||||
|
|
||||||
|
assert Image.open(img).size == (VISION_WIDTH, VISION_HEIGHT)
|
||||||
|
|
||||||
|
|
||||||
|
async def test_move_and_collision_event(client):
|
||||||
|
await client.handshake()
|
||||||
|
out = (await client.call_tool("move", {"forward": 2.0})).output
|
||||||
|
assert out["moved"] == pytest.approx(2.0)
|
||||||
|
assert out["collision"] is False
|
||||||
|
out2 = (await client.call_tool("move", {"forward": 5.0})).output
|
||||||
|
assert out2["collision"] is True
|
||||||
|
assert out2["collision_normal"] is not None
|
||||||
|
event = await client.next_event(timeout=1.0)
|
||||||
|
assert event.topic == "collision"
|
||||||
|
assert "other" in event.payload
|
||||||
|
|
||||||
|
|
||||||
|
async def test_look_at_and_interact(client):
|
||||||
|
await client.handshake()
|
||||||
|
la = (await client.call_tool("look_at", {"target": "beacon"})).output
|
||||||
|
assert abs(la["rotation"]["yaw_deg"] - 45.0) < 0.1
|
||||||
|
res = (await client.call_tool("interact", {})).output
|
||||||
|
assert res["success"] is False
|
||||||
|
assert "too far" in res["message"]
|
||||||
|
|
||||||
|
|
||||||
|
async def test_look_at_unknown_target(client):
|
||||||
|
await client.handshake()
|
||||||
|
with pytest.raises(ToolError) as ei:
|
||||||
|
await client.call_tool("look_at", {"target": "moon"})
|
||||||
|
assert ei.value.code == "execution_failed"
|
||||||
|
|
||||||
|
|
||||||
|
async def test_hear_empty_then_hum(client, bridge):
|
||||||
|
await client.handshake()
|
||||||
|
out = (await client.call_tool("hear", {})).output
|
||||||
|
assert out["sounds"] == []
|
||||||
|
bridge.world.capsule.x = bridge.world.beacon.x - 3.0
|
||||||
|
bridge.world.capsule.z = bridge.world.beacon.z
|
||||||
|
out = (await client.call_tool("hear", {})).output
|
||||||
|
assert any(s["kind"] == "beacon_hum" for s in out["sounds"])
|
||||||
|
|
||||||
|
|
||||||
|
async def test_depth_and_world_query(client):
|
||||||
|
await client.handshake()
|
||||||
|
d = (await client.call_tool("depth", {})).output
|
||||||
|
assert d["width"] == DEPTH_WIDTH and d["height"] == DEPTH_HEIGHT
|
||||||
|
assert len(d["depth"]) == DEPTH_HEIGHT and len(d["depth"][0]) == DEPTH_WIDTH
|
||||||
|
wq = (await client.call_tool("world_query", {})).output
|
||||||
|
assert wq["beacon"]["id"] == "beacon"
|
||||||
|
assert len(wq["obstacles"]) == 3
|
||||||
|
|
||||||
|
|
||||||
|
async def test_unknown_tool(client):
|
||||||
|
await client.handshake()
|
||||||
|
with pytest.raises(ToolError) as ei:
|
||||||
|
await client.call_tool("definitely_not_a_tool", {})
|
||||||
|
assert ei.value.code == "tool_unknown"
|
||||||
|
|
||||||
|
|
||||||
|
async def test_boom_execution_failed(client):
|
||||||
|
await client.handshake()
|
||||||
|
with pytest.raises(ToolError) as ei:
|
||||||
|
await client.call_tool("boom", {})
|
||||||
|
assert ei.value.code == "execution_failed"
|
||||||
|
|
||||||
|
|
||||||
|
async def test_bump_emits_event(client):
|
||||||
|
await client.handshake()
|
||||||
|
out = (await client.call_tool("bump", {})).output
|
||||||
|
assert out == {"bumped": True}
|
||||||
|
event = await client.next_event(timeout=1.0)
|
||||||
|
assert event.topic == "collision"
|
||||||
|
|
||||||
|
|
||||||
|
async def test_echo(client):
|
||||||
|
await client.handshake()
|
||||||
|
out = (await client.call_tool("echo", {"value": "hello"})).output
|
||||||
|
assert out == {"value": "hello"}
|
||||||
|
|
||||||
|
|
||||||
|
async def test_interact_success_near_beacon(client, bridge):
|
||||||
|
await client.handshake()
|
||||||
|
bridge.world.capsule.x = bridge.world.beacon.x - 1.0
|
||||||
|
bridge.world.capsule.z = bridge.world.beacon.z
|
||||||
|
res = (await client.call_tool("interact", {})).output
|
||||||
|
assert res["success"] is True
|
||||||
|
assert bridge.world.beacon.active
|
||||||
|
|
||||||
|
|
||||||
|
async def test_move_invalid_input(client):
|
||||||
|
await client.handshake()
|
||||||
|
with pytest.raises(ToolError) as ei:
|
||||||
|
await client.call_tool("move", {"forward": 99.0})
|
||||||
|
assert ei.value.code == "execution_failed"
|
||||||
|
|
||||||
|
|
||||||
|
async def test_world_reset_between_sessions():
|
||||||
|
"""Each fresh bridge owns a fresh Room."""
|
||||||
|
b1, b2 = build_bridge(), build_bridge()
|
||||||
|
assert b1.world is not b2.world
|
||||||
|
|
||||||
|
|
||||||
|
async def test_single_source_of_truth():
|
||||||
|
"""Manifest must carry no world state."""
|
||||||
|
bridge = build_bridge()
|
||||||
|
t = InProcessTransport.start(bridge)
|
||||||
|
async with t:
|
||||||
|
client = AICCClient(t)
|
||||||
|
async with client:
|
||||||
|
m = await client.handshake()
|
||||||
|
dumped = m.model_dump(by_alias=True)
|
||||||
|
world = dumped["world"]
|
||||||
|
assert set(world.keys()) == {"name", "kind"}
|
||||||
|
assert "beacon" not in dumped
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
"""Tests for the headless raycaster: honest frames from real world state."""
|
||||||
|
|
||||||
|
from testbed.room.render import Raycaster
|
||||||
|
from testbed.room.world import Room
|
||||||
|
|
||||||
|
|
||||||
|
def test_spawn_view_shows_crate_and_occludes_beacon():
|
||||||
|
"""From spawn the beacon is behind crate_red, so the frame center is the
|
||||||
|
crate's color, not a cyan glow."""
|
||||||
|
room = Room()
|
||||||
|
img, grid = Raycaster().render(room)
|
||||||
|
px = img.load()
|
||||||
|
r, g, b = px[80, 60]
|
||||||
|
assert r > 120 and g < 100 and b < 90 # reddish crate, distance-fogged
|
||||||
|
assert len(grid) == 30 and len(grid[0]) == 40
|
||||||
|
# The crate is ~5m away: center depth block must be well below 10.
|
||||||
|
assert 2.0 < grid[17][20] < 8.0
|
||||||
|
|
||||||
|
|
||||||
|
def test_wall_nearby_is_bright_gray():
|
||||||
|
room = Room()
|
||||||
|
room.capsule.yaw_deg = 180.0 # face north wall 1.5m away
|
||||||
|
px = Raycaster().render(room)[0].load()
|
||||||
|
r, g, b = px[80, 60]
|
||||||
|
assert abs(r - g) < 8 and r > 80 # gray wall, little fog at 1.5m
|
||||||
|
|
||||||
|
|
||||||
|
def test_beacon_glow_visible_when_facing_it():
|
||||||
|
room = Room()
|
||||||
|
room.capsule.x, room.capsule.z, room.capsule.yaw_deg = 11.0, 11.0, 45.0
|
||||||
|
img, _ = Raycaster().render(room)
|
||||||
|
px = img.load()
|
||||||
|
r, g, b = px[80, 60]
|
||||||
|
assert b > r # inactive beacon glows cyan
|
||||||
|
room.beacon.active = True
|
||||||
|
px = Raycaster().render(room)[0].load()
|
||||||
|
r, g, b = px[80, 60]
|
||||||
|
assert r > b # active beacon glows warm yellow
|
||||||
|
|
||||||
|
|
||||||
|
def test_beacon_occluded_by_crate():
|
||||||
|
room = Room()
|
||||||
|
# Beacon behind crate_red from spawn: center pixel must not be cyan.
|
||||||
|
px = Raycaster().render(room)[0].load()
|
||||||
|
r, g, b = px[80, 60]
|
||||||
|
assert b < 90
|
||||||
|
|
||||||
|
|
||||||
|
def test_depth_grid_aligned_and_bounded():
|
||||||
|
room = Room()
|
||||||
|
_, grid = Raycaster().render(room, max_depth=10.0)
|
||||||
|
assert all(0.0 <= v <= 10.0 for row in grid for v in row)
|
||||||
|
assert any(v > 0 for row in grid for v in row)
|
||||||
@@ -0,0 +1,133 @@
|
|||||||
|
"""Tests for the room world: physics, collision, beacon, audio."""
|
||||||
|
|
||||||
|
import math
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from testbed.room.world import ROOM_SIZE, Beacon, Box, Room
|
||||||
|
|
||||||
|
|
||||||
|
def test_spawn_state():
|
||||||
|
room = Room()
|
||||||
|
assert room.capsule.x == 1.5 and room.capsule.z == 1.5
|
||||||
|
assert room.capsule.yaw_deg == 45.0
|
||||||
|
assert len(room.boxes) == 3
|
||||||
|
|
||||||
|
|
||||||
|
def test_move_forward_free():
|
||||||
|
room = Room()
|
||||||
|
moved, hit = room.move_forward(2.0)
|
||||||
|
assert moved == pytest.approx(2.0)
|
||||||
|
assert hit is None
|
||||||
|
assert room.capsule.x == pytest.approx(1.5 + 2.0 * math.sin(math.radians(45)))
|
||||||
|
assert room.capsule.z == pytest.approx(1.5 + 2.0 * math.cos(math.radians(45)))
|
||||||
|
|
||||||
|
|
||||||
|
def test_move_blocked_by_wall():
|
||||||
|
room = Room()
|
||||||
|
room.capsule.yaw_deg = 180.0 # toward north wall (z=0)
|
||||||
|
moved, hit = room.move_forward(10.0)
|
||||||
|
assert moved == pytest.approx(1.5 - room.capsule.radius, abs=1e-6)
|
||||||
|
assert hit is not None
|
||||||
|
assert hit.other == "wall_north"
|
||||||
|
assert hit.normal_z == pytest.approx(1.0)
|
||||||
|
assert hit.impulse == pytest.approx(10.0 - moved)
|
||||||
|
assert room.capsule.z == pytest.approx(room.capsule.radius)
|
||||||
|
|
||||||
|
|
||||||
|
def test_move_blocked_by_box():
|
||||||
|
room = Room()
|
||||||
|
# Crate red sits at (6,6) with half-extents 1. From spawn heading 45deg.
|
||||||
|
room.capsule.yaw_deg = 45.0
|
||||||
|
moved, hit = room.move_forward(5.0)
|
||||||
|
assert hit is not None
|
||||||
|
assert hit.other == "crate_red"
|
||||||
|
assert moved < 5.0
|
||||||
|
# Capsule must not overlap the box.
|
||||||
|
b = room.boxes[0]
|
||||||
|
near_x = min(max(room.capsule.x, b.x_min()), b.x_max())
|
||||||
|
near_z = min(max(room.capsule.z, b.z_min()), b.z_max())
|
||||||
|
assert (
|
||||||
|
math.hypot(room.capsule.x - near_x, room.capsule.z - near_z)
|
||||||
|
>= room.capsule.radius - 1e-6
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_corner_push_out_of_box():
|
||||||
|
room = Room()
|
||||||
|
room.capsule.x = 6.0
|
||||||
|
room.capsule.z = 6.0 # inside crate_red
|
||||||
|
ok, hit = room._try_displace(0.1, 0.1)
|
||||||
|
assert not ok
|
||||||
|
assert hit is not None and hit.other == "crate_red"
|
||||||
|
near_x = min(max(room.capsule.x, room.boxes[0].x_min()), room.boxes[0].x_max())
|
||||||
|
near_z = min(max(room.capsule.z, room.boxes[0].z_min()), room.boxes[0].z_max())
|
||||||
|
assert (
|
||||||
|
math.hypot(room.capsule.x - near_x, room.capsule.z - near_z)
|
||||||
|
>= room.capsule.radius - 1e-6
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_turn_clamps_pitch():
|
||||||
|
room = Room()
|
||||||
|
room.turn(yaw_deg=30.0, pitch_deg=200.0)
|
||||||
|
assert room.capsule.pitch_deg == 85.0
|
||||||
|
room.turn(pitch_deg=-500.0)
|
||||||
|
assert room.capsule.pitch_deg == -85.0
|
||||||
|
assert room.capsule.yaw_deg == pytest.approx(75.0)
|
||||||
|
|
||||||
|
|
||||||
|
def test_look_at_beacon():
|
||||||
|
room = Room()
|
||||||
|
yaw, pitch = room.look_at_beacon()
|
||||||
|
dx = room.beacon.x - room.capsule.x
|
||||||
|
dz = room.beacon.z - room.capsule.z
|
||||||
|
assert yaw == pytest.approx(math.degrees(math.atan2(dx, dz)))
|
||||||
|
assert room.capsule.yaw_deg == pytest.approx(yaw)
|
||||||
|
|
||||||
|
|
||||||
|
def test_interact_requires_reach():
|
||||||
|
room = Room()
|
||||||
|
ok, msg = room.interact_beacon()
|
||||||
|
assert not ok
|
||||||
|
assert "too far" in msg
|
||||||
|
room.capsule.x = room.beacon.x - 1.0
|
||||||
|
room.capsule.z = room.beacon.z
|
||||||
|
ok, msg = room.interact_beacon()
|
||||||
|
assert ok
|
||||||
|
assert room.beacon.active
|
||||||
|
ok2, _ = room.interact_beacon()
|
||||||
|
assert ok2 # already active is still a success
|
||||||
|
|
||||||
|
|
||||||
|
def test_hear_beacon_hum_only_in_range():
|
||||||
|
room = Room()
|
||||||
|
sounds = room.hear_now()
|
||||||
|
assert sounds == []
|
||||||
|
room.capsule.x = room.beacon.x - 3.0
|
||||||
|
room.capsule.z = room.beacon.z
|
||||||
|
sounds = room.hear_now()
|
||||||
|
assert any(s["kind"] == "beacon_hum" for s in sounds)
|
||||||
|
assert sounds[0]["intensity"] > 0.0
|
||||||
|
|
||||||
|
|
||||||
|
def test_queue_and_drain_audio():
|
||||||
|
room = Room()
|
||||||
|
room.queue_audio("thud", 90.0, 0.5)
|
||||||
|
sounds = room.drain_audio()
|
||||||
|
assert len(sounds) == 1
|
||||||
|
assert room.drain_audio() == []
|
||||||
|
|
||||||
|
|
||||||
|
def test_tick_advances():
|
||||||
|
room = Room()
|
||||||
|
assert room.advance_tick() == 1
|
||||||
|
assert room.advance_tick() == 2
|
||||||
|
|
||||||
|
|
||||||
|
def test_describe_layout():
|
||||||
|
room = Room()
|
||||||
|
d = room.describe()
|
||||||
|
assert d["room"]["width"] == ROOM_SIZE
|
||||||
|
assert len(d["obstacles"]) == 3
|
||||||
|
assert d["beacon"]["id"] == "beacon"
|
||||||
Reference in New Issue
Block a user