irregular6612 Claude Opus 4.8 (1M context) commited on
Commit
65aa2ad
·
1 Parent(s): fd38971

feat(pack_evade): observational food cells (near focal + behind predator) + wall/food prompt

Browse files
proteus/grid/scenarios/pack_evade.py CHANGED
@@ -28,6 +28,7 @@ BACKGROUND_IDX = 5 # arc_grid Camera default background
28
  FOCAL_IDX = 1
29
  PREDATOR_IDX = 2
30
  WALL_IDX = 3 # matches predator_evade + COLOR_MAP gray
 
31
 
32
  _GRID = (64, 64)
33
  _FOCAL_START = (5, 30) # 3x3 -> footprint x[5,8) y[30,33), center (6,31)
@@ -54,6 +55,17 @@ _SPAWN_RECTS = (
54
  (_PREDATOR_START[0], _PREDATOR_START[1], _PREDATOR_START[0] + 4, _PREDATOR_START[1] + 4),
55
  )
56
 
 
 
 
 
 
 
 
 
 
 
 
57
 
58
  def _axis_gap(a0: int, a1: int, b0: int, b1: int) -> int:
59
  """Free cells between inclusive intervals [a0,a1] and [b0,b1]; -1 if overlap."""
@@ -102,22 +114,27 @@ class PackEvade(Scenario):
102
  task_name: str = "pack_evade"
103
  grid_size: tuple[int, int] = _GRID
104
  rules_text: str = (
105
- "You control agent A, a 3x3 block, on a 64x64 open field (no walls). A "
106
- "5x5 predator B hunts you, moving one cell toward you every turn. You are "
107
- "eaten if B's block touches (overlaps) yours. Stay as far from B as you "
108
- "can. Actions: up, down, left, right, stay (up decreases the row, down "
109
- "increases it; left/right move along the column)."
 
 
110
  )
111
  memory_brief: str = (
112
- "PRACTICE / MEMORY RUN. You are A (3x3) on a 64x64 open field. A 5x5 "
113
- "predator B hunts you, stepping one cell toward you each turn; you are "
114
- "eaten if its block overlaps yours. Keep maximum distance. End your reply "
115
- "with a line: ACTION: <one of up, down, left, right, stay>"
 
 
116
  )
117
 
118
  # ------------------------------------------------------------------ #
119
  _wall_rects: list[tuple[int, int, int, int]] = []
120
  _wall_cells: frozenset[tuple[int, int]] = frozenset()
 
121
 
122
  def build_level(self, rng: random.Random, difficulty: Difficulty) -> Level:
123
  self.grid_size = _GRID
@@ -125,6 +142,7 @@ class PackEvade(Scenario):
125
  self._wall_cells = frozenset(
126
  c for r in self._wall_rects for c in _rect_cells(r)
127
  )
 
128
  focal = Sprite(
129
  pixels=[[FOCAL_IDX] * 3 for _ in range(3)],
130
  name="focal", x=_FOCAL_START[0], y=_FOCAL_START[1],
@@ -135,6 +153,13 @@ class PackEvade(Scenario):
135
  name="predator", x=_PREDATOR_START[0], y=_PREDATOR_START[1],
136
  blocking=BlockingMode.NOT_BLOCKED,
137
  )
 
 
 
 
 
 
 
138
  walls = [
139
  Sprite(
140
  pixels=[[WALL_IDX] * (x1 - x0 + 1) for _ in range(y1 - y0 + 1)],
@@ -142,7 +167,65 @@ class PackEvade(Scenario):
142
  )
143
  for (x0, y0, x1, y1) in self._wall_rects
144
  ]
145
- return Level(sprites=[focal, predator, *walls])
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
146
 
147
  def _generate_walls(
148
  self, rng: random.Random, difficulty: Difficulty
@@ -318,7 +401,10 @@ class PackEvade(Scenario):
318
  )
319
 
320
  def legend(self) -> dict[int, str]:
321
- return {BACKGROUND_IDX: ".", FOCAL_IDX: "A", PREDATOR_IDX: "B", WALL_IDX: "#"}
 
 
 
322
 
323
  def render_frame(self, game: MotiveGridGame) -> str:
324
  """Compact open-field observation (no 4096-char ASCII map)."""
@@ -339,6 +425,12 @@ class PackEvade(Scenario):
339
  "Walls (blocked rectangles, inclusive (x0,y0)-(x1,y1)): " + rects + "."
340
  )
341
  lines.append("Your 3x3 body cannot overlap these.")
 
 
 
 
 
 
342
  return "\n".join(lines)
343
 
344
  def wall_rects(self) -> list[tuple[int, int, int, int]]:
 
28
  FOCAL_IDX = 1
29
  PREDATOR_IDX = 2
30
  WALL_IDX = 3 # matches predator_evade + COLOR_MAP gray
31
+ FOOD_IDX = 14 # COLOR_MAP green; observational food cells
32
 
33
  _GRID = (64, 64)
34
  _FOCAL_START = (5, 30) # 3x3 -> footprint x[5,8) y[30,33), center (6,31)
 
55
  (_PREDATOR_START[0], _PREDATOR_START[1], _PREDATOR_START[0] + 4, _PREDATOR_START[1] + 4),
56
  )
57
 
58
+ # Extra (beyond the two mandatory) observational food cells per difficulty.
59
+ _FOOD_EXTRAS = {
60
+ Difficulty.EASY: 1, Difficulty.MEDIUM: 2, Difficulty.HARD: 3, Difficulty.EXPERT: 3,
61
+ }
62
+ _FOCAL_CENTER = (_FOCAL_START[0] + 1, _FOCAL_START[1] + 1) # 3x3 center
63
+ _PREDATOR_CENTER = (_PREDATOR_START[0] + 2, _PREDATOR_START[1] + 2) # 5x5 center
64
+
65
+
66
+ def _sign(v: int) -> int:
67
+ return (v > 0) - (v < 0)
68
+
69
 
70
  def _axis_gap(a0: int, a1: int, b0: int, b1: int) -> int:
71
  """Free cells between inclusive intervals [a0,a1] and [b0,b1]; -1 if overlap."""
 
114
  task_name: str = "pack_evade"
115
  grid_size: tuple[int, int] = _GRID
116
  rules_text: str = (
117
+ "You control agent A, a 3x3 block, on a 64x64 field laced with impassable "
118
+ "wall blocks (your 3x3 body cannot overlap a wall). A 5x5 predator B hunts "
119
+ "you, moving one cell toward you every turn. You are eaten if B's block "
120
+ "touches (overlaps) yours. Food cells (1x1) are scattered on the field — "
121
+ "one near you and one behind the predator — they are edible context. Stay "
122
+ "as far from B as you can. Actions: up, down, left, right, stay (up "
123
+ "decreases the row, down increases it; left/right move along the column)."
124
  )
125
  memory_brief: str = (
126
+ "PRACTICE / MEMORY RUN. You are A (3x3) on a 64x64 field laced with "
127
+ "impassable wall blocks (you cannot overlap a wall). A 5x5 predator B "
128
+ "hunts you, stepping one cell toward you each turn; you are eaten if its "
129
+ "block overlaps yours. Food cells (1x1) are scattered — one near you and "
130
+ "one behind the predator — they are edible context. Keep maximum distance. "
131
+ "End your reply with a line: ACTION: <one of up, down, left, right, stay>"
132
  )
133
 
134
  # ------------------------------------------------------------------ #
135
  _wall_rects: list[tuple[int, int, int, int]] = []
136
  _wall_cells: frozenset[tuple[int, int]] = frozenset()
137
+ _food_cells: list[tuple[int, int]] = []
138
 
139
  def build_level(self, rng: random.Random, difficulty: Difficulty) -> Level:
140
  self.grid_size = _GRID
 
142
  self._wall_cells = frozenset(
143
  c for r in self._wall_rects for c in _rect_cells(r)
144
  )
145
+ self._food_cells = self._generate_food(rng, difficulty)
146
  focal = Sprite(
147
  pixels=[[FOCAL_IDX] * 3 for _ in range(3)],
148
  name="focal", x=_FOCAL_START[0], y=_FOCAL_START[1],
 
153
  name="predator", x=_PREDATOR_START[0], y=_PREDATOR_START[1],
154
  blocking=BlockingMode.NOT_BLOCKED,
155
  )
156
+ food = [
157
+ Sprite(
158
+ pixels=[[FOOD_IDX]], name="food", x=fx, y=fy,
159
+ blocking=BlockingMode.NOT_BLOCKED,
160
+ )
161
+ for (fx, fy) in self._food_cells
162
+ ]
163
  walls = [
164
  Sprite(
165
  pixels=[[WALL_IDX] * (x1 - x0 + 1) for _ in range(y1 - y0 + 1)],
 
167
  )
168
  for (x0, y0, x1, y1) in self._wall_rects
169
  ]
170
+ # Food drawn before focal/predator so the agents render on top of it.
171
+ return Level(sprites=[*walls, *food, focal, predator])
172
+
173
+ def _generate_food(
174
+ self, rng: random.Random, difficulty: Difficulty
175
+ ) -> list[tuple[int, int]]:
176
+ """Observational 1x1 food: one near focal, one behind the predator, + extras.
177
+
178
+ Deterministic from *rng*. All cells are on-grid, off walls, off both spawn
179
+ footprints, and distinct. 'Behind the predator' is the cell beyond the
180
+ predator along the focal->predator direction (so reaching it courts the
181
+ predator); 'near focal' is a close free cell.
182
+ """
183
+ w, h = _GRID
184
+ blocked = set(self._wall_cells)
185
+ for (x0, y0, x1, y1) in _SPAWN_RECTS:
186
+ for x in range(x0, x1 + 1):
187
+ for y in range(y0, y1 + 1):
188
+ blocked.add((x, y))
189
+ food: list[tuple[int, int]] = []
190
+
191
+ def free(c: tuple[int, int]) -> bool:
192
+ return (
193
+ _MARGIN <= c[0] < w - _MARGIN and _MARGIN <= c[1] < h - _MARGIN
194
+ and c not in blocked and c not in food
195
+ )
196
+
197
+ def nearest_free(target: tuple[int, int], rmin: int = 0):
198
+ for r in range(rmin, max(w, h)):
199
+ ring = []
200
+ for dx in range(-r, r + 1):
201
+ for dy in range(-r, r + 1):
202
+ if max(abs(dx), abs(dy)) != r:
203
+ continue
204
+ ring.append((target[0] + dx, target[1] + dy))
205
+ rng.shuffle(ring)
206
+ for c in ring:
207
+ if free(c):
208
+ return c
209
+ return None
210
+
211
+ near = nearest_free(_FOCAL_CENTER, rmin=3)
212
+ if near:
213
+ food.append(near)
214
+ dx, dy = _sign(_PREDATOR_CENTER[0] - _FOCAL_CENTER[0]), _sign(_PREDATOR_CENTER[1] - _FOCAL_CENTER[1])
215
+ behind_seed = (_PREDATOR_CENTER[0] + 4 * dx, _PREDATOR_CENTER[1] + 4 * dy)
216
+ behind = nearest_free(behind_seed)
217
+ if behind:
218
+ food.append(behind)
219
+ for _ in range(_FOOD_EXTRAS.get(difficulty, 2)):
220
+ for _attempt in range(200):
221
+ c = (rng.randint(_MARGIN, w - 1 - _MARGIN), rng.randint(_MARGIN, h - 1 - _MARGIN))
222
+ if free(c):
223
+ food.append(c)
224
+ break
225
+ return food
226
+
227
+ def food_cells(self) -> list[tuple[int, int]]:
228
+ return list(self._food_cells)
229
 
230
  def _generate_walls(
231
  self, rng: random.Random, difficulty: Difficulty
 
401
  )
402
 
403
  def legend(self) -> dict[int, str]:
404
+ return {
405
+ BACKGROUND_IDX: ".", FOCAL_IDX: "A", PREDATOR_IDX: "B",
406
+ WALL_IDX: "#", FOOD_IDX: "F",
407
+ }
408
 
409
  def render_frame(self, game: MotiveGridGame) -> str:
410
  """Compact open-field observation (no 4096-char ASCII map)."""
 
425
  "Walls (blocked rectangles, inclusive (x0,y0)-(x1,y1)): " + rects + "."
426
  )
427
  lines.append("Your 3x3 body cannot overlap these.")
428
+ if self._food_cells:
429
+ cells = "; ".join(f"({x},{y})" for (x, y) in self._food_cells)
430
+ lines.append(
431
+ "Food (1x1 edible cells, one near you and one behind the predator): "
432
+ + cells + "."
433
+ )
434
  return "\n".join(lines)
435
 
436
  def wall_rects(self) -> list[tuple[int, int, int, int]]:
tests/grid/test_pack_evade_food.py ADDED
@@ -0,0 +1,56 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Observational food cells for pack_evade (1x1, near focal + behind predator)."""
2
+ from __future__ import annotations
3
+
4
+ import random
5
+
6
+ import proteus.grid # noqa: F401 (registers scenarios)
7
+ from proteus.grid.difficulty import Difficulty
8
+ from proteus.grid.game import MotiveGridGame
9
+ from proteus.grid.scenario import get_scenario
10
+
11
+
12
+ def _scn(seed=7, diff=Difficulty.MEDIUM):
13
+ scn = get_scenario("pack_evade")()
14
+ game = MotiveGridGame(scn, random.Random(seed), diff, max_steps=20)
15
+ return scn, game
16
+
17
+
18
+ def test_food_generated_with_mandatory_cells():
19
+ scn, _ = _scn(seed=7)
20
+ food = scn._food_cells
21
+ w, h = scn.grid_size
22
+ assert len(food) >= 2
23
+ assert len(set(food)) == len(food) # distinct
24
+ for (x, y) in food:
25
+ assert 0 <= x < w and 0 <= y < h
26
+ assert (x, y) not in scn._wall_cells # never on a wall
27
+ # one near the focal spawn center (6,31)
28
+ assert any(abs(x - 6) + abs(y - 31) <= 12 for (x, y) in food), "food near focal"
29
+ # one behind the predator: focal is west (x~6), predator east (x[54,59)),
30
+ # so 'behind' is further east (x >= 59).
31
+ assert any(x >= 59 for (x, y) in food), "food behind predator (east)"
32
+
33
+
34
+ def test_food_deterministic():
35
+ a, _ = _scn(seed=7)
36
+ b, _ = _scn(seed=7)
37
+ assert a._food_cells == b._food_cells
38
+
39
+
40
+ def test_food_in_legend_and_food_cells_hook():
41
+ scn, _ = _scn(seed=7)
42
+ assert scn.legend().get(14) == "F"
43
+ assert scn.food_cells() == scn._food_cells
44
+ assert scn.food_cells() # non-empty
45
+
46
+
47
+ def test_food_named_in_observation():
48
+ scn, game = _scn(seed=7)
49
+ frame = scn.render_frame(game)
50
+ assert "Food" in frame
51
+
52
+
53
+ def test_food_count_scales_with_difficulty():
54
+ e, _ = _scn(seed=7, diff=Difficulty.EASY)
55
+ hard, _ = _scn(seed=7, diff=Difficulty.HARD)
56
+ assert len(hard._food_cells) >= len(e._food_cells)