Spaces:
Runtime error
Runtime error
Commit ·
0464e32
1
Parent(s): 39c93ec
feat(pack_evade): 2x2 focal, 3x3 open-mouth predator, gap-2, predator-first
Browse files
proteus/game/scenarios/pack_evade.py
CHANGED
|
@@ -1,6 +1,6 @@
|
|
| 1 |
"""pack_evade — a 64x64 open-field, multi-cell predator-evasion scenario.
|
| 2 |
|
| 3 |
-
A single
|
| 4 |
three; the two that were caught live only in the hand-authored handover memory,
|
| 5 |
see ``proteus.game.runtime.pack_evade_memory``). The field is open (no walls), so all
|
| 6 |
geometry is **center-to-center Manhattan** — no BFS. There is no habit/diagnostic
|
|
@@ -31,8 +31,19 @@ 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 |
-
|
| 35 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 36 |
|
| 37 |
_DELTAS: dict[str, tuple[int, int]] = {
|
| 38 |
"up": (0, -1), "down": (0, 1), "left": (-1, 0), "right": (1, 0), "stay": (0, 0),
|
|
@@ -40,8 +51,8 @@ _DELTAS: dict[str, tuple[int, int]] = {
|
|
| 40 |
_TIE_BREAK_ORDER: tuple[str, ...] = ("up", "down", "left", "right", "stay")
|
| 41 |
|
| 42 |
# --- wall terrain config + geometry helpers -------------------------------- #
|
| 43 |
-
_NARROW_GAP =
|
| 44 |
-
_CLEARANCE =
|
| 45 |
_MARGIN = 2 # keep walls off the very border
|
| 46 |
# (solid blocks, focal-only channels) per difficulty
|
| 47 |
_WALL_COUNTS = {
|
|
@@ -51,16 +62,21 @@ _WALL_COUNTS = {
|
|
| 51 |
Difficulty.EXPERT: (5, 3),
|
| 52 |
}
|
| 53 |
_SPAWN_RECTS = (
|
| 54 |
-
(_FOCAL_START[0], _FOCAL_START[1],
|
| 55 |
-
|
|
|
|
|
|
|
| 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] +
|
| 63 |
-
_PREDATOR_CENTER = (
|
|
|
|
|
|
|
|
|
|
| 64 |
|
| 65 |
|
| 66 |
def _sign(v: int) -> int:
|
|
@@ -113,9 +129,10 @@ class PackEvade(Scenario):
|
|
| 113 |
|
| 114 |
task_name: str = "pack_evade"
|
| 115 |
grid_size: tuple[int, int] = _GRID
|
|
|
|
| 116 |
rules_text: str = (
|
| 117 |
-
"You control agent A, a
|
| 118 |
-
"wall blocks (your
|
| 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 "
|
|
@@ -123,8 +140,8 @@ class PackEvade(Scenario):
|
|
| 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 (
|
| 127 |
-
"impassable wall blocks (you cannot overlap a wall). A
|
| 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. "
|
|
@@ -144,12 +161,12 @@ class PackEvade(Scenario):
|
|
| 144 |
)
|
| 145 |
self._food_cells = self._generate_food(rng, difficulty)
|
| 146 |
focal = Sprite(
|
| 147 |
-
pixels=[[FOCAL_IDX] *
|
| 148 |
name="focal", x=_FOCAL_START[0], y=_FOCAL_START[1],
|
| 149 |
blocking=BlockingMode.BOUNDING_BOX,
|
| 150 |
)
|
| 151 |
predator = Sprite(
|
| 152 |
-
pixels=[[
|
| 153 |
name="predator", x=_PREDATOR_START[0], y=_PREDATOR_START[1],
|
| 154 |
blocking=BlockingMode.NOT_BLOCKED,
|
| 155 |
)
|
|
@@ -230,11 +247,11 @@ class PackEvade(Scenario):
|
|
| 230 |
def _generate_walls(
|
| 231 |
self, rng: random.Random, difficulty: Difficulty
|
| 232 |
) -> list[tuple[int, int, int, int]]:
|
| 233 |
-
"""Seeded rectangular blocks + focal-only (width-
|
| 234 |
|
| 235 |
Blocks/channels keep a predator-wide ``_CLEARANCE`` from each other, the
|
| 236 |
border, and both spawns, so the predator-navigable space stays connected;
|
| 237 |
-
each channel's
|
| 238 |
"""
|
| 239 |
w, h = _GRID
|
| 240 |
n_blocks, n_channels = _WALL_COUNTS.get(difficulty, (3, 2))
|
|
@@ -334,6 +351,8 @@ class PackEvade(Scenario):
|
|
| 334 |
best_dist, best_action = dist, action
|
| 335 |
dx, dy = _DELTAS[best_action]
|
| 336 |
predator.move(dx, dy)
|
|
|
|
|
|
|
| 337 |
|
| 338 |
def optimal_action(self, game: MotiveGridGame) -> str:
|
| 339 |
focal, predator = game.focal_sprite, game.predator_sprite
|
|
@@ -368,7 +387,10 @@ class PackEvade(Scenario):
|
|
| 368 |
if focal is None:
|
| 369 |
return 0.0
|
| 370 |
fw, fh = focal.width, focal.height
|
| 371 |
-
pred_center_before = (
|
|
|
|
|
|
|
|
|
|
| 372 |
center_after = (focal.x + fw // 2, focal.y + fh // 2)
|
| 373 |
center_before = (focal_before[0] + fw // 2, focal_before[1] + fh // 2)
|
| 374 |
return float(
|
|
@@ -392,7 +414,10 @@ class PackEvade(Scenario):
|
|
| 392 |
if focal is None:
|
| 393 |
return None
|
| 394 |
fw, fh = focal.width, focal.height
|
| 395 |
-
pred_center_before = (
|
|
|
|
|
|
|
|
|
|
| 396 |
center_after = (focal.x + fw // 2, focal.y + fh // 2)
|
| 397 |
center_before = (focal_before[0] + fw // 2, focal_before[1] + fh // 2)
|
| 398 |
return float(
|
|
@@ -414,8 +439,8 @@ class PackEvade(Scenario):
|
|
| 414 |
fc, pc = _center(focal), _center(predator)
|
| 415 |
w, h = self.grid_size
|
| 416 |
lines = [
|
| 417 |
-
f"Open field {w}x{h}. You are A (
|
| 418 |
-
f"Predator B (
|
| 419 |
]
|
| 420 |
if self._wall_rects:
|
| 421 |
rects = "; ".join(
|
|
@@ -424,7 +449,7 @@ class PackEvade(Scenario):
|
|
| 424 |
lines.append(
|
| 425 |
"Walls (blocked rectangles, inclusive (x0,y0)-(x1,y1)): " + rects + "."
|
| 426 |
)
|
| 427 |
-
lines.append("Your
|
| 428 |
if self._food_cells:
|
| 429 |
cells = "; ".join(f"({x},{y})" for (x, y) in self._food_cells)
|
| 430 |
lines.append(
|
|
@@ -438,7 +463,7 @@ class PackEvade(Scenario):
|
|
| 438 |
|
| 439 |
# --- persona-policy compatibility shims (wall-aware focal navigation) --- #
|
| 440 |
def _is_free(self, game: MotiveGridGame, cell: tuple[int, int]) -> bool:
|
| 441 |
-
"""True iff the
|
| 442 |
|
| 443 |
Used only by the shared hidden-persona reference policy
|
| 444 |
(``proteus.game.metrics.persona``), which calls it on the focal's top-left
|
|
|
|
| 1 |
"""pack_evade — a 64x64 open-field, multi-cell predator-evasion scenario.
|
| 2 |
|
| 3 |
+
A single 3x3 predator hunts a 2x2 focal agent (the lone survivor of a pack of
|
| 4 |
three; the two that were caught live only in the hand-authored handover memory,
|
| 5 |
see ``proteus.game.runtime.pack_evade_memory``). The field is open (no walls), so all
|
| 6 |
geometry is **center-to-center Manhattan** — no BFS. There is no habit/diagnostic
|
|
|
|
| 31 |
FOOD_IDX = 14 # COLOR_MAP green; observational food cells
|
| 32 |
|
| 33 |
_GRID = (64, 64)
|
| 34 |
+
_FOCAL_SIZE = 2
|
| 35 |
+
_PREDATOR_SIZE = 3
|
| 36 |
+
_FOCAL_START = (5, 30) # 2x2 -> footprint x[5,7) y[30,32), center (6,31)
|
| 37 |
+
_PREDATOR_START = (54, 30) # 3x3 -> footprint x[54,57) y[30,33), center (55,31)
|
| 38 |
+
# ㄷ (open-mouth) predator pixels; mouth opens EAST in the base orientation.
|
| 39 |
+
# Transparent (-1) cells render as background and never collide.
|
| 40 |
+
_PREDATOR_PIXELS = [
|
| 41 |
+
[PREDATOR_IDX, PREDATOR_IDX, PREDATOR_IDX],
|
| 42 |
+
[PREDATOR_IDX, -1, -1],
|
| 43 |
+
[PREDATOR_IDX, PREDATOR_IDX, PREDATOR_IDX],
|
| 44 |
+
]
|
| 45 |
+
# action -> clockwise rotation so the mouth faces the movement direction.
|
| 46 |
+
_FACING = {"right": 0, "down": 90, "left": 180, "up": 270}
|
| 47 |
|
| 48 |
_DELTAS: dict[str, tuple[int, int]] = {
|
| 49 |
"up": (0, -1), "down": (0, 1), "left": (-1, 0), "right": (1, 0), "stay": (0, 0),
|
|
|
|
| 51 |
_TIE_BREAK_ORDER: tuple[str, ...] = ("up", "down", "left", "right", "stay")
|
| 52 |
|
| 53 |
# --- wall terrain config + geometry helpers -------------------------------- #
|
| 54 |
+
_NARROW_GAP = 2 # focal (2-wide) fits; predator (3-wide) cannot
|
| 55 |
+
_CLEARANCE = 4 # min free corridor around blocks/spawns (> predator-wide)
|
| 56 |
_MARGIN = 2 # keep walls off the very border
|
| 57 |
# (solid blocks, focal-only channels) per difficulty
|
| 58 |
_WALL_COUNTS = {
|
|
|
|
| 62 |
Difficulty.EXPERT: (5, 3),
|
| 63 |
}
|
| 64 |
_SPAWN_RECTS = (
|
| 65 |
+
(_FOCAL_START[0], _FOCAL_START[1],
|
| 66 |
+
_FOCAL_START[0] + _FOCAL_SIZE - 1, _FOCAL_START[1] + _FOCAL_SIZE - 1),
|
| 67 |
+
(_PREDATOR_START[0], _PREDATOR_START[1],
|
| 68 |
+
_PREDATOR_START[0] + _PREDATOR_SIZE - 1, _PREDATOR_START[1] + _PREDATOR_SIZE - 1),
|
| 69 |
)
|
| 70 |
|
| 71 |
# Extra (beyond the two mandatory) observational food cells per difficulty.
|
| 72 |
_FOOD_EXTRAS = {
|
| 73 |
Difficulty.EASY: 1, Difficulty.MEDIUM: 2, Difficulty.HARD: 3, Difficulty.EXPERT: 3,
|
| 74 |
}
|
| 75 |
+
_FOCAL_CENTER = (_FOCAL_START[0] + _FOCAL_SIZE // 2, _FOCAL_START[1] + _FOCAL_SIZE // 2)
|
| 76 |
+
_PREDATOR_CENTER = (
|
| 77 |
+
_PREDATOR_START[0] + _PREDATOR_SIZE // 2,
|
| 78 |
+
_PREDATOR_START[1] + _PREDATOR_SIZE // 2,
|
| 79 |
+
)
|
| 80 |
|
| 81 |
|
| 82 |
def _sign(v: int) -> int:
|
|
|
|
| 129 |
|
| 130 |
task_name: str = "pack_evade"
|
| 131 |
grid_size: tuple[int, int] = _GRID
|
| 132 |
+
turn_order: str = "predator_first"
|
| 133 |
rules_text: str = (
|
| 134 |
+
"You control agent A, a 2x2 block, on a 64x64 field laced with impassable "
|
| 135 |
+
"wall blocks (your 2x2 body cannot overlap a wall). A 3x3 predator B hunts "
|
| 136 |
"you, moving one cell toward you every turn. You are eaten if B's block "
|
| 137 |
"touches (overlaps) yours. Food cells (1x1) are scattered on the field — "
|
| 138 |
"one near you and one behind the predator — they are edible context. Stay "
|
|
|
|
| 140 |
"decreases the row, down increases it; left/right move along the column)."
|
| 141 |
)
|
| 142 |
memory_brief: str = (
|
| 143 |
+
"PRACTICE / MEMORY RUN. You are A (2x2) on a 64x64 field laced with "
|
| 144 |
+
"impassable wall blocks (you cannot overlap a wall). A 3x3 predator B "
|
| 145 |
"hunts you, stepping one cell toward you each turn; you are eaten if its "
|
| 146 |
"block overlaps yours. Food cells (1x1) are scattered — one near you and "
|
| 147 |
"one behind the predator — they are edible context. Keep maximum distance. "
|
|
|
|
| 161 |
)
|
| 162 |
self._food_cells = self._generate_food(rng, difficulty)
|
| 163 |
focal = Sprite(
|
| 164 |
+
pixels=[[FOCAL_IDX] * _FOCAL_SIZE for _ in range(_FOCAL_SIZE)],
|
| 165 |
name="focal", x=_FOCAL_START[0], y=_FOCAL_START[1],
|
| 166 |
blocking=BlockingMode.BOUNDING_BOX,
|
| 167 |
)
|
| 168 |
predator = Sprite(
|
| 169 |
+
pixels=[row[:] for row in _PREDATOR_PIXELS],
|
| 170 |
name="predator", x=_PREDATOR_START[0], y=_PREDATOR_START[1],
|
| 171 |
blocking=BlockingMode.NOT_BLOCKED,
|
| 172 |
)
|
|
|
|
| 247 |
def _generate_walls(
|
| 248 |
self, rng: random.Random, difficulty: Difficulty
|
| 249 |
) -> list[tuple[int, int, int, int]]:
|
| 250 |
+
"""Seeded rectangular blocks + focal-only (width-2) channels.
|
| 251 |
|
| 252 |
Blocks/channels keep a predator-wide ``_CLEARANCE`` from each other, the
|
| 253 |
border, and both spawns, so the predator-navigable space stays connected;
|
| 254 |
+
each channel's 2-wide slot admits the 2x2 focal but not the 3x3 predator.
|
| 255 |
"""
|
| 256 |
w, h = _GRID
|
| 257 |
n_blocks, n_channels = _WALL_COUNTS.get(difficulty, (3, 2))
|
|
|
|
| 351 |
best_dist, best_action = dist, action
|
| 352 |
dx, dy = _DELTAS[best_action]
|
| 353 |
predator.move(dx, dy)
|
| 354 |
+
if best_action in _FACING:
|
| 355 |
+
predator.set_rotation(_FACING[best_action])
|
| 356 |
|
| 357 |
def optimal_action(self, game: MotiveGridGame) -> str:
|
| 358 |
focal, predator = game.focal_sprite, game.predator_sprite
|
|
|
|
| 387 |
if focal is None:
|
| 388 |
return 0.0
|
| 389 |
fw, fh = focal.width, focal.height
|
| 390 |
+
pred_center_before = (
|
| 391 |
+
predator_before[0] + _PREDATOR_SIZE // 2,
|
| 392 |
+
predator_before[1] + _PREDATOR_SIZE // 2,
|
| 393 |
+
)
|
| 394 |
center_after = (focal.x + fw // 2, focal.y + fh // 2)
|
| 395 |
center_before = (focal_before[0] + fw // 2, focal_before[1] + fh // 2)
|
| 396 |
return float(
|
|
|
|
| 414 |
if focal is None:
|
| 415 |
return None
|
| 416 |
fw, fh = focal.width, focal.height
|
| 417 |
+
pred_center_before = (
|
| 418 |
+
predator_before[0] + _PREDATOR_SIZE // 2,
|
| 419 |
+
predator_before[1] + _PREDATOR_SIZE // 2,
|
| 420 |
+
)
|
| 421 |
center_after = (focal.x + fw // 2, focal.y + fh // 2)
|
| 422 |
center_before = (focal_before[0] + fw // 2, focal_before[1] + fh // 2)
|
| 423 |
return float(
|
|
|
|
| 439 |
fc, pc = _center(focal), _center(predator)
|
| 440 |
w, h = self.grid_size
|
| 441 |
lines = [
|
| 442 |
+
f"Open field {w}x{h}. You are A (2x2) centered at {fc}. "
|
| 443 |
+
f"Predator B (3x3) centered at {pc}. Manhattan distance {_manhattan(fc, pc)}."
|
| 444 |
]
|
| 445 |
if self._wall_rects:
|
| 446 |
rects = "; ".join(
|
|
|
|
| 449 |
lines.append(
|
| 450 |
"Walls (blocked rectangles, inclusive (x0,y0)-(x1,y1)): " + rects + "."
|
| 451 |
)
|
| 452 |
+
lines.append("Your 2x2 body cannot overlap these.")
|
| 453 |
if self._food_cells:
|
| 454 |
cells = "; ".join(f"({x},{y})" for (x, y) in self._food_cells)
|
| 455 |
lines.append(
|
|
|
|
| 463 |
|
| 464 |
# --- persona-policy compatibility shims (wall-aware focal navigation) --- #
|
| 465 |
def _is_free(self, game: MotiveGridGame, cell: tuple[int, int]) -> bool:
|
| 466 |
+
"""True iff the 2x2 focal footprint anchored at *cell* is on-grid + wall-free.
|
| 467 |
|
| 468 |
Used only by the shared hidden-persona reference policy
|
| 469 |
(``proteus.game.metrics.persona``), which calls it on the focal's top-left
|
tests/grid/test_pack_evade.py
CHANGED
|
@@ -19,8 +19,8 @@ def test_build_is_64x64_with_multicell_sprites():
|
|
| 19 |
scenario, game = _game()
|
| 20 |
assert scenario.grid_size == (64, 64)
|
| 21 |
focal, predator = game.focal_sprite, game.predator_sprite
|
| 22 |
-
assert (focal.width, focal.height) == (
|
| 23 |
-
assert (predator.width, predator.height) == (
|
| 24 |
# Disjoint at start (not already eaten).
|
| 25 |
assert not scenario.check_elimination(game)
|
| 26 |
|
|
@@ -29,20 +29,20 @@ def test_eat_fires_on_footprint_overlap_not_adjacency():
|
|
| 29 |
scenario, game = _game()
|
| 30 |
focal, predator = game.focal_sprite, game.predator_sprite
|
| 31 |
# Place predator footprint just touching but NOT overlapping the focal:
|
| 32 |
-
focal.move(-focal.x, -focal.y) # focal anchor -> (0,0), occupies x,y in [0,
|
| 33 |
-
predator.move(
|
| 34 |
assert not scenario.check_elimination(game)
|
| 35 |
-
predator.move(-1, 0) # anchor -> (
|
| 36 |
assert scenario.check_elimination(game)
|
| 37 |
|
| 38 |
|
| 39 |
def test_center_manhattan_safety_distance():
|
| 40 |
scenario, game = _game()
|
| 41 |
focal, predator = game.focal_sprite, game.predator_sprite
|
| 42 |
-
focal.move(-focal.x, -focal.y) # center (1,1)
|
| 43 |
-
predator.move(10 - predator.x, -predator.y) # anchor (10,0), center (
|
| 44 |
-
# |
|
| 45 |
-
assert scenario.safety_distance(game) ==
|
| 46 |
|
| 47 |
|
| 48 |
def test_optimal_moves_away_and_no_diagnostic():
|
|
|
|
| 19 |
scenario, game = _game()
|
| 20 |
assert scenario.grid_size == (64, 64)
|
| 21 |
focal, predator = game.focal_sprite, game.predator_sprite
|
| 22 |
+
assert (focal.width, focal.height) == (2, 2)
|
| 23 |
+
assert (predator.width, predator.height) == (3, 3)
|
| 24 |
# Disjoint at start (not already eaten).
|
| 25 |
assert not scenario.check_elimination(game)
|
| 26 |
|
|
|
|
| 29 |
scenario, game = _game()
|
| 30 |
focal, predator = game.focal_sprite, game.predator_sprite
|
| 31 |
# Place predator footprint just touching but NOT overlapping the focal:
|
| 32 |
+
focal.move(-focal.x, -focal.y) # focal anchor -> (0,0), occupies x,y in [0,2)
|
| 33 |
+
predator.move(2 - predator.x, -predator.y) # predator anchor -> (2,0): x in [2,5), no overlap
|
| 34 |
assert not scenario.check_elimination(game)
|
| 35 |
+
predator.move(-1, 0) # anchor -> (1,0): x in [1,4) overlaps focal x in [0,2)
|
| 36 |
assert scenario.check_elimination(game)
|
| 37 |
|
| 38 |
|
| 39 |
def test_center_manhattan_safety_distance():
|
| 40 |
scenario, game = _game()
|
| 41 |
focal, predator = game.focal_sprite, game.predator_sprite
|
| 42 |
+
focal.move(-focal.x, -focal.y) # focal 2x2: center (1,1)
|
| 43 |
+
predator.move(10 - predator.x, -predator.y) # anchor (10,0), predator 3x3: center (11,1)
|
| 44 |
+
# |11-1| + |1-1| = 10
|
| 45 |
+
assert scenario.safety_distance(game) == 10
|
| 46 |
|
| 47 |
|
| 48 |
def test_optimal_moves_away_and_no_diagnostic():
|
tests/grid/test_pack_evade_walls.py
CHANGED
|
@@ -26,8 +26,8 @@ def test_walls_are_generated_and_deterministic():
|
|
| 26 |
def test_walls_clear_spawns_and_border():
|
| 27 |
scn, _ = _scn(seed=7)
|
| 28 |
w, h = scn.grid_size
|
| 29 |
-
# spawn footprints must be wall-free
|
| 30 |
-
for (sx, sy, sw, sh) in [(5, 30,
|
| 31 |
for j in range(sh):
|
| 32 |
for i in range(sw):
|
| 33 |
assert (sx + i, sy + j) not in scn._wall_cells
|
|
@@ -37,7 +37,7 @@ def test_walls_clear_spawns_and_border():
|
|
| 37 |
|
| 38 |
|
| 39 |
def test_has_a_focal_only_channel():
|
| 40 |
-
"""Some
|
| 41 |
scn, _ = _scn(seed=7)
|
| 42 |
w, h = scn.grid_size
|
| 43 |
wc = scn._wall_cells
|
|
@@ -51,35 +51,35 @@ def test_has_a_focal_only_channel():
|
|
| 51 |
return True
|
| 52 |
|
| 53 |
found = any(
|
| 54 |
-
fp_free(x, y,
|
| 55 |
for x in range(w) for y in range(h)
|
| 56 |
)
|
| 57 |
-
assert found, "expected at least one focal-only (
|
| 58 |
|
| 59 |
|
| 60 |
def test_predator_can_reach_focal_region():
|
| 61 |
-
"""Predator (
|
| 62 |
scn, _ = _scn(seed=7)
|
| 63 |
w, h = scn.grid_size
|
| 64 |
wc = scn._wall_cells
|
| 65 |
|
| 66 |
-
def
|
| 67 |
return all(
|
| 68 |
0 <= x + i < w and 0 <= y + j < h and (x + i, y + j) not in wc
|
| 69 |
-
for j in range(
|
| 70 |
)
|
| 71 |
|
| 72 |
from collections import deque
|
| 73 |
-
start = (54,
|
| 74 |
seen = {start}
|
| 75 |
q = deque([start])
|
| 76 |
while q:
|
| 77 |
x, y = q.popleft()
|
| 78 |
for dx, dy in ((0, -1), (0, 1), (-1, 0), (1, 0)):
|
| 79 |
nx, ny = x + dx, y + dy
|
| 80 |
-
if (nx, ny) not in seen and
|
| 81 |
seen.add((nx, ny)); q.append((nx, ny))
|
| 82 |
-
# an anchor whose
|
| 83 |
assert any(abs(ax - 5) <= 6 and abs(ay - 30) <= 6 for (ax, ay) in seen)
|
| 84 |
|
| 85 |
|
|
@@ -87,11 +87,11 @@ def test_focal_cannot_move_into_a_wall():
|
|
| 87 |
scn, game = _scn(seed=7)
|
| 88 |
focal = game.focal_sprite
|
| 89 |
wx, wy = sorted(scn._wall_cells)[len(scn._wall_cells) // 2]
|
| 90 |
-
# place focal so moving +1 x makes its
|
| 91 |
-
focal.set_position(wx -
|
| 92 |
-
assert (focal.x, focal.y) == (wx -
|
| 93 |
game.apply_motive_action("right") # would put footprint over (wx,wy)
|
| 94 |
-
assert focal.x == wx -
|
| 95 |
|
| 96 |
|
| 97 |
def test_predator_never_lands_on_a_wall():
|
|
@@ -101,7 +101,7 @@ def test_predator_never_lands_on_a_wall():
|
|
| 101 |
break
|
| 102 |
game.apply_motive_action("stay")
|
| 103 |
pred = game.predator_sprite
|
| 104 |
-
cells = {(pred.x + i, pred.y + j) for j in range(
|
| 105 |
assert not (cells & scn._wall_cells), "predator footprint entered a wall"
|
| 106 |
|
| 107 |
|
|
|
|
| 26 |
def test_walls_clear_spawns_and_border():
|
| 27 |
scn, _ = _scn(seed=7)
|
| 28 |
w, h = scn.grid_size
|
| 29 |
+
# spawn footprints must be wall-free (focal 2x2 at (5,30), predator 3x3 at (54,30))
|
| 30 |
+
for (sx, sy, sw, sh) in [(5, 30, 2, 2), (54, 30, 3, 3)]:
|
| 31 |
for j in range(sh):
|
| 32 |
for i in range(sw):
|
| 33 |
assert (sx + i, sy + j) not in scn._wall_cells
|
|
|
|
| 37 |
|
| 38 |
|
| 39 |
def test_has_a_focal_only_channel():
|
| 40 |
+
"""Some 2x2-passable cell is NOT 3x3-passable: a focal-only gap exists."""
|
| 41 |
scn, _ = _scn(seed=7)
|
| 42 |
w, h = scn.grid_size
|
| 43 |
wc = scn._wall_cells
|
|
|
|
| 51 |
return True
|
| 52 |
|
| 53 |
found = any(
|
| 54 |
+
fp_free(x, y, 2) and not fp_free(x, y, 3)
|
| 55 |
for x in range(w) for y in range(h)
|
| 56 |
)
|
| 57 |
+
assert found, "expected at least one focal-only (2x2 yes, 3x3 no) anchor"
|
| 58 |
|
| 59 |
|
| 60 |
def test_predator_can_reach_focal_region():
|
| 61 |
+
"""Predator (3x3) BFS from its spawn reaches adjacency of the focal spawn."""
|
| 62 |
scn, _ = _scn(seed=7)
|
| 63 |
w, h = scn.grid_size
|
| 64 |
wc = scn._wall_cells
|
| 65 |
|
| 66 |
+
def fp_free3(x, y):
|
| 67 |
return all(
|
| 68 |
0 <= x + i < w and 0 <= y + j < h and (x + i, y + j) not in wc
|
| 69 |
+
for j in range(3) for i in range(3)
|
| 70 |
)
|
| 71 |
|
| 72 |
from collections import deque
|
| 73 |
+
start = (54, 30)
|
| 74 |
seen = {start}
|
| 75 |
q = deque([start])
|
| 76 |
while q:
|
| 77 |
x, y = q.popleft()
|
| 78 |
for dx, dy in ((0, -1), (0, 1), (-1, 0), (1, 0)):
|
| 79 |
nx, ny = x + dx, y + dy
|
| 80 |
+
if (nx, ny) not in seen and fp_free3(nx, ny):
|
| 81 |
seen.add((nx, ny)); q.append((nx, ny))
|
| 82 |
+
# an anchor whose 3x3 footprint can sit adjacent to the focal spawn (5,30)
|
| 83 |
assert any(abs(ax - 5) <= 6 and abs(ay - 30) <= 6 for (ax, ay) in seen)
|
| 84 |
|
| 85 |
|
|
|
|
| 87 |
scn, game = _scn(seed=7)
|
| 88 |
focal = game.focal_sprite
|
| 89 |
wx, wy = sorted(scn._wall_cells)[len(scn._wall_cells) // 2]
|
| 90 |
+
# place focal so moving +1 x makes its 2x2 footprint cover (wx,wy)
|
| 91 |
+
focal.set_position(wx - 2, wy) # footprint x[wx-2, wx), not yet on wall
|
| 92 |
+
assert (focal.x, focal.y) == (wx - 2, wy)
|
| 93 |
game.apply_motive_action("right") # would put footprint over (wx,wy)
|
| 94 |
+
assert focal.x == wx - 2, "move into wall must be reverted by the engine"
|
| 95 |
|
| 96 |
|
| 97 |
def test_predator_never_lands_on_a_wall():
|
|
|
|
| 101 |
break
|
| 102 |
game.apply_motive_action("stay")
|
| 103 |
pred = game.predator_sprite
|
| 104 |
+
cells = {(pred.x + i, pred.y + j) for j in range(pred.height) for i in range(pred.width)}
|
| 105 |
assert not (cells & scn._wall_cells), "predator footprint entered a wall"
|
| 106 |
|
| 107 |
|
tests/scenarios/test_pack_evade_resize.py
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# tests/scenarios/test_pack_evade_resize.py
|
| 2 |
+
"""pack_evade after resize: 2x2 focal, 3x3 open-mouth predator, gap-2 channels."""
|
| 3 |
+
import random
|
| 4 |
+
|
| 5 |
+
from proteus.game.engine.difficulty import Difficulty
|
| 6 |
+
from proteus.game.engine.grid import MotiveGridGame
|
| 7 |
+
from proteus.game.scenarios.base import get_scenario
|
| 8 |
+
import proteus.game.scenarios # noqa: F401
|
| 9 |
+
|
| 10 |
+
|
| 11 |
+
def _game(diff=Difficulty.EASY, seed=42):
|
| 12 |
+
scen = get_scenario("pack_evade")()
|
| 13 |
+
return scen, MotiveGridGame(scen, random.Random(seed), diff, max_steps=50)
|
| 14 |
+
|
| 15 |
+
|
| 16 |
+
def test_sprite_sizes():
|
| 17 |
+
scen, game = _game()
|
| 18 |
+
assert (game.focal_sprite.width, game.focal_sprite.height) == (2, 2)
|
| 19 |
+
assert (game.predator_sprite.width, game.predator_sprite.height) == (3, 3)
|
| 20 |
+
|
| 21 |
+
|
| 22 |
+
def test_predator_has_open_mouth():
|
| 23 |
+
scen, game = _game()
|
| 24 |
+
# ㄷ shape: 9-cell bounding box, exactly 7 solid (2 transparent mouth cells).
|
| 25 |
+
pix = game.predator_sprite.render()
|
| 26 |
+
assert (pix != -1).sum() == 7
|
| 27 |
+
|
| 28 |
+
|
| 29 |
+
def test_turn_order_is_predator_first():
|
| 30 |
+
scen, _ = _game()
|
| 31 |
+
assert scen.turn_order == "predator_first"
|
| 32 |
+
|
| 33 |
+
|
| 34 |
+
def test_mouth_faces_movement_direction():
|
| 35 |
+
scen, game = _game()
|
| 36 |
+
# Force the predator to take a known step and assert its rotation updates.
|
| 37 |
+
pred = game.predator_sprite
|
| 38 |
+
pred.set_position(30, 30)
|
| 39 |
+
game.focal_sprite.set_position(30, 10) # straight up from predator
|
| 40 |
+
scen.advance_threat(game)
|
| 41 |
+
# Moving up => rotation 270 (see _FACING in pack_evade).
|
| 42 |
+
assert game.predator_sprite.rotation == 270
|
| 43 |
+
|
| 44 |
+
|
| 45 |
+
def test_narrow_gap_blocks_predator_admits_focal():
|
| 46 |
+
scen, game = _game()
|
| 47 |
+
scen.build_level(random.Random(0), Difficulty.EASY)
|
| 48 |
+
# A width-2 free corridor: 2-wide focal footprint fits, 3-wide predator does not.
|
| 49 |
+
# Use the scenario primitive on a synthetic 2-wide gap between two walls.
|
| 50 |
+
walls = {(10, y) for y in range(0, 20)} | {(13, y) for y in range(0, 20)}
|
| 51 |
+
scen._wall_cells = frozenset(walls)
|
| 52 |
+
# focal anchor (11,5): footprint x in {11,12} — clear of both wall columns.
|
| 53 |
+
assert scen._footprint_free(game, game.focal_sprite, 11, 5) is True
|
| 54 |
+
# predator anchor (11,5): footprint x in {11,12,13} — hits wall column 13.
|
| 55 |
+
assert scen._footprint_free(game, game.predator_sprite, 11, 5) is False
|