irregular6612 Claude Sonnet 4.6 commited on
Commit
a629d19
·
1 Parent(s): 7e5166c

feat(memory): per-turn cells overlay channel + npc_down/npc_active colours in memory_frames

Browse files
proteus/game/runtime/memory.py CHANGED
@@ -59,6 +59,13 @@ class MemoryTurn(BaseModel):
59
  """Per-sprite render states; non-empty ⇒ the multi-agent render path."""
60
  resources: list[tuple[int, int]] = Field(default_factory=list)
61
  """Collectible resource cells still present this turn."""
 
 
 
 
 
 
 
62
  events: list[str] = Field(default_factory=list)
63
  """Narration for this turn, e.g. ``"a1 eaten"`` / ``"a0 got resource"``."""
64
 
@@ -203,6 +210,10 @@ def _predator_solid_offsets(facing: str) -> list[tuple[int, int]]:
203
  return [(c, r) for r in range(3) for c in range(3) if (c, r) not in mouth]
204
 
205
 
 
 
 
 
206
  def memory_frames(
207
  checkpoint: MemoryCheckpoint, *, legend: dict[int, str], grid_size: tuple[int, int]
208
  ) -> list[dict]:
@@ -239,6 +250,9 @@ def memory_frames(
239
  for y in range(max(0, ry0), min(h, ry1 + 1)):
240
  for x in range(max(0, rx0), min(w, rx1 + 1)):
241
  grid[y][x] = wall_idx
 
 
 
242
  for (fx, fy) in mt.resources:
243
  if 0 <= fx < w and 0 <= fy < h:
244
  grid[fy][fx] = food_idx
@@ -250,6 +264,8 @@ def memory_frames(
250
  x, y = ag.pos[0] + c, ag.pos[1] + r
251
  if 0 <= x < w and 0 <= y < h:
252
  grid[y][x] = predator_idx
 
 
253
  else:
254
  color = focal_idx if ag.is_chosen else distractor_idx
255
  for r in range(ag.size):
 
59
  """Per-sprite render states; non-empty ⇒ the multi-agent render path."""
60
  resources: list[tuple[int, int]] = Field(default_factory=list)
61
  """Collectible resource cells still present this turn."""
62
+ cells: list[tuple[int, int, int]] = Field(default_factory=list)
63
+ """Generic coloured overlay cells ``(x, y, palette_idx)`` for this turn.
64
+
65
+ Painted after walls and before agents in :func:`memory_frames`. Lets the
66
+ errand_runner director draw per-tick world objects (the recolouring
67
+ traffic-light bar, roads, construction, wallet, home) without a bespoke
68
+ field per object type. Empty for all legacy single-/multi-agent turns."""
69
  events: list[str] = Field(default_factory=list)
70
  """Narration for this turn, e.g. ``"a1 eaten"`` / ``"a0 got resource"``."""
71
 
 
210
  return [(c, r) for r in range(3) for c in range(3) if (c, r) not in mouth]
211
 
212
 
213
+ # NPC agent kinds -> palette colour (errand_runner fallen-pedestrian rescue).
214
+ _NPC_COLOR: dict[str, int] = {"npc_down": 11, "npc_active": 14} # yellow -> green
215
+
216
+
217
  def memory_frames(
218
  checkpoint: MemoryCheckpoint, *, legend: dict[int, str], grid_size: tuple[int, int]
219
  ) -> list[dict]:
 
250
  for y in range(max(0, ry0), min(h, ry1 + 1)):
251
  for x in range(max(0, rx0), min(w, rx1 + 1)):
252
  grid[y][x] = wall_idx
253
+ for (cx, cy, cidx) in mt.cells:
254
+ if 0 <= cx < w and 0 <= cy < h:
255
+ grid[cy][cx] = cidx
256
  for (fx, fy) in mt.resources:
257
  if 0 <= fx < w and 0 <= fy < h:
258
  grid[fy][fx] = food_idx
 
264
  x, y = ag.pos[0] + c, ag.pos[1] + r
265
  if 0 <= x < w and 0 <= y < h:
266
  grid[y][x] = predator_idx
267
+ elif ag.kind in _NPC_COLOR:
268
+ paint(grid, ag.pos[0], ag.pos[1], ag.size, _NPC_COLOR[ag.kind])
269
  else:
270
  color = focal_idx if ag.is_chosen else distractor_idx
271
  for r in range(ag.size):
tests/runtime/test_memory_frames_overlay.py ADDED
@@ -0,0 +1,60 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # tests/runtime/test_memory_frames_overlay.py
2
+ """errand_runner memory render: generic `cells` overlay + npc agent colours.
3
+
4
+ Legacy turns (no cells, kind in {agent,predator}) stay byte-identical.
5
+ """
6
+ from proteus.game.runtime.memory import (
7
+ AgentFrame, MemoryCheckpoint, MemoryTurn, memory_frames,
8
+ )
9
+
10
+ _LEGEND = {5: ".", 1: "A", 2: "B", 3: "#", 14: "F"}
11
+ _GRID = (12, 12)
12
+
13
+
14
+ def _ck(turn):
15
+ return MemoryCheckpoint(
16
+ model="m", scenario="errand_runner", difficulty="easy", created_at="x",
17
+ outcome="survived", transparent_prompt="p", memory_turns=[turn],
18
+ )
19
+
20
+
21
+ def test_cells_overlay_painted_over_background():
22
+ turn = MemoryTurn(
23
+ turn_idx=1, frame_ascii="", action="up", focal_pos=(0, 0), predator_pos=(0, 0),
24
+ agents=[AgentFrame(id="a0", kind="agent", pos=(0, 0), size=2)],
25
+ cells=[(7, 3, 8), (7, 4, 8), (7, 5, 8)], # red (8) traffic-light bar
26
+ )
27
+ grid = memory_frames(_ck(turn), legend=_LEGEND, grid_size=_GRID)[0]["grid"]
28
+ assert grid[3][7] == 8 and grid[4][7] == 8 and grid[5][7] == 8
29
+
30
+
31
+ def test_agents_paint_over_cells():
32
+ # A candidate body on a road cell must render as the body, not the road.
33
+ turn = MemoryTurn(
34
+ turn_idx=1, frame_ascii="", action="up", focal_pos=(0, 0), predator_pos=(0, 0),
35
+ agents=[AgentFrame(id="a0", kind="agent", pos=(2, 2), size=2, is_chosen=True)],
36
+ cells=[(2, 2, 3)], # neutral road under the agent's top-left cell
37
+ )
38
+ grid = memory_frames(_ck(turn), legend=_LEGEND, grid_size=_GRID)[0]["grid"]
39
+ assert grid[2][2] == 1 # focal colour wins over the overlay
40
+
41
+
42
+ def test_npc_down_is_yellow_and_active_is_green():
43
+ turn = MemoryTurn(
44
+ turn_idx=1, frame_ascii="", action="up", focal_pos=(0, 0), predator_pos=(0, 0),
45
+ agents=[
46
+ AgentFrame(id="ped", kind="npc_down", pos=(2, 2), size=2),
47
+ AgentFrame(id="ped2", kind="npc_active", pos=(8, 8), size=2),
48
+ ],
49
+ )
50
+ grid = memory_frames(_ck(turn), legend=_LEGEND, grid_size=_GRID)[0]["grid"]
51
+ assert grid[2][2] == 11 # npc_down -> yellow
52
+ assert grid[8][8] == 14 # npc_active -> green
53
+
54
+
55
+ def test_legacy_turn_without_cells_unaffected():
56
+ turn = MemoryTurn(turn_idx=1, frame_ascii="", action="up", focal_pos=(0, 0),
57
+ predator_pos=(0, 0),
58
+ agents=[AgentFrame(id="a0", kind="agent", pos=(0, 0), size=2)])
59
+ out = memory_frames(_ck(turn), legend=_LEGEND, grid_size=_GRID)[0]
60
+ assert "grid" in out and out["events"] == []