Spaces:
Runtime error
Runtime error
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
|
| 106 |
-
"
|
| 107 |
-
"
|
| 108 |
-
"
|
| 109 |
-
"
|
|
|
|
|
|
|
| 110 |
)
|
| 111 |
memory_brief: str = (
|
| 112 |
-
"PRACTICE / MEMORY RUN. You are A (3x3) on a 64x64
|
| 113 |
-
"
|
| 114 |
-
"
|
| 115 |
-
"
|
|
|
|
|
|
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 {
|
|
|
|
|
|
|
|
|
|
| 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)
|