Spaces:
Runtime error
Runtime error
Commit ·
f9924aa
1
Parent(s): cc37031
feat(cp6): per-difficulty hand-authored predator_evade layouts (EASY unchanged)
Browse files
proteus/grid/scenarios/predator_evade.py
CHANGED
|
@@ -59,6 +59,7 @@ from __future__ import annotations
|
|
| 59 |
|
| 60 |
import random
|
| 61 |
from collections import deque
|
|
|
|
| 62 |
from typing import TYPE_CHECKING
|
| 63 |
|
| 64 |
from proteus.arc_grid import BlockingMode, Level, Sprite
|
|
@@ -91,6 +92,57 @@ _WALL_CELLS: tuple[tuple[int, int], ...] = (
|
|
| 91 |
(2, 5),
|
| 92 |
)
|
| 93 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 94 |
# Action string -> (dx, dy). Mirror of game._DIRECTION_DELTAS; duplicated here
|
| 95 |
# so the scenario's answer-key reasoning is self-contained and does not import
|
| 96 |
# private game internals.
|
|
@@ -132,9 +184,12 @@ class PredatorEvade(Scenario):
|
|
| 132 |
"""
|
| 133 |
|
| 134 |
task_name: str = "predator_evade"
|
|
|
|
|
|
|
|
|
|
| 135 |
grid_size: tuple[int, int] = _GRID_SIZE
|
| 136 |
rules_text: str = (
|
| 137 |
-
"You control the focal agent 'A' on
|
| 138 |
"you, moving one cell along the shortest path toward you every turn. "
|
| 139 |
"Walls '#' block movement; '.' is open ground. You are eliminated if "
|
| 140 |
"the predator reaches your cell. Survive as long as you can. "
|
|
@@ -159,45 +214,38 @@ class PredatorEvade(Scenario):
|
|
| 159 |
# World construction
|
| 160 |
# ------------------------------------------------------------------ #
|
| 161 |
def build_level(self, rng: random.Random, difficulty: Difficulty) -> Level:
|
| 162 |
-
"""Build the level for *difficulty*
|
| 163 |
-
|
| 164 |
-
The layout is deterministic (fixed coordinates), so *rng* is accepted
|
| 165 |
-
for interface conformance and forward compatibility (harder difficulties
|
| 166 |
-
may randomise wall placement) but is not consumed here. ``_wall_cells``
|
| 167 |
-
is refreshed so the BFS helpers stay in sync with the built level.
|
| 168 |
-
Per-difficulty dispatch is added in Task 2; currently all difficulties
|
| 169 |
-
produce the EASY layout unchanged.
|
| 170 |
|
| 171 |
Args:
|
| 172 |
-
rng: Seeded RNG
|
| 173 |
-
difficulty: The session difficulty band
|
| 174 |
|
| 175 |
Returns:
|
| 176 |
-
A :class:`~proteus.arc_grid.Level` with
|
| 177 |
-
(``name="focal"``), predator (``name="predator"``), and the wall
|
| 178 |
-
sprites (tagged ``sys_static`` so the engine merges them).
|
| 179 |
"""
|
| 180 |
-
del rng # deterministic
|
| 181 |
-
|
| 182 |
-
|
| 183 |
-
|
|
|
|
|
|
|
| 184 |
|
| 185 |
focal = Sprite(
|
| 186 |
pixels=[[FOCAL_IDX]],
|
| 187 |
name="focal",
|
| 188 |
-
x=
|
| 189 |
-
y=
|
| 190 |
blocking=BlockingMode.PIXEL_PERFECT,
|
| 191 |
)
|
| 192 |
predator = Sprite(
|
| 193 |
pixels=[[PREDATOR_IDX]],
|
| 194 |
name="predator",
|
| 195 |
-
x=
|
| 196 |
-
y=
|
| 197 |
blocking=BlockingMode.PIXEL_PERFECT,
|
| 198 |
)
|
| 199 |
sprites: list[Sprite] = [focal, predator]
|
| 200 |
-
for (wx, wy) in
|
| 201 |
sprites.append(
|
| 202 |
Sprite(
|
| 203 |
pixels=[[WALL_IDX]],
|
|
|
|
| 59 |
|
| 60 |
import random
|
| 61 |
from collections import deque
|
| 62 |
+
from dataclasses import dataclass
|
| 63 |
from typing import TYPE_CHECKING
|
| 64 |
|
| 65 |
from proteus.arc_grid import BlockingMode, Level, Sprite
|
|
|
|
| 92 |
(2, 5),
|
| 93 |
)
|
| 94 |
|
| 95 |
+
# --------------------------------------------------------------------------- #
|
| 96 |
+
# Per-difficulty hand-authored layouts.
|
| 97 |
+
# --------------------------------------------------------------------------- #
|
| 98 |
+
|
| 99 |
+
@dataclass(frozen=True)
|
| 100 |
+
class _Layout:
|
| 101 |
+
"""A hand-authored deterministic layout for one difficulty band."""
|
| 102 |
+
|
| 103 |
+
grid_size: tuple[int, int]
|
| 104 |
+
focal_start: tuple[int, int]
|
| 105 |
+
predator_start: tuple[int, int]
|
| 106 |
+
wall_cells: tuple[tuple[int, int], ...]
|
| 107 |
+
|
| 108 |
+
|
| 109 |
+
# Candidate hand-authored layouts. Tuned under the golden invariant test
|
| 110 |
+
# (Task 3): the pre-roll direction ("left") must dead-end into a wall at the
|
| 111 |
+
# handover while a perpendicular move increases BFS distance, so
|
| 112 |
+
# optimal_action != habit_action. Adjust coordinates if Task 3 fails.
|
| 113 |
+
_LAYOUTS: dict[Difficulty, _Layout] = {
|
| 114 |
+
Difficulty.EASY: _Layout(
|
| 115 |
+
grid_size=(8, 8),
|
| 116 |
+
focal_start=(5, 3),
|
| 117 |
+
predator_start=(7, 3),
|
| 118 |
+
wall_cells=((2, 2), (2, 3), (2, 4), (2, 5)),
|
| 119 |
+
),
|
| 120 |
+
Difficulty.MEDIUM: _Layout(
|
| 121 |
+
grid_size=(10, 10),
|
| 122 |
+
focal_start=(6, 4),
|
| 123 |
+
predator_start=(8, 4),
|
| 124 |
+
wall_cells=tuple((3, y) for y in range(2, 8)),
|
| 125 |
+
),
|
| 126 |
+
Difficulty.HARD: _Layout(
|
| 127 |
+
grid_size=(12, 12),
|
| 128 |
+
focal_start=(7, 5),
|
| 129 |
+
predator_start=(9, 5),
|
| 130 |
+
wall_cells=(
|
| 131 |
+
tuple((3, y) for y in range(3, 9))
|
| 132 |
+
+ tuple((x, 8) for x in range(4, 7))
|
| 133 |
+
),
|
| 134 |
+
),
|
| 135 |
+
Difficulty.EXPERT: _Layout(
|
| 136 |
+
grid_size=(12, 12),
|
| 137 |
+
focal_start=(7, 5),
|
| 138 |
+
predator_start=(9, 4),
|
| 139 |
+
wall_cells=(
|
| 140 |
+
tuple((3, y) for y in range(2, 10))
|
| 141 |
+
+ ((4, 3), (5, 3))
|
| 142 |
+
),
|
| 143 |
+
),
|
| 144 |
+
}
|
| 145 |
+
|
| 146 |
# Action string -> (dx, dy). Mirror of game._DIRECTION_DELTAS; duplicated here
|
| 147 |
# so the scenario's answer-key reasoning is self-contained and does not import
|
| 148 |
# private game internals.
|
|
|
|
| 184 |
"""
|
| 185 |
|
| 186 |
task_name: str = "predator_evade"
|
| 187 |
+
# Default hint only; build_level() overwrites this per instance with the
|
| 188 |
+
# difficulty-specific size. Read scenario.grid_size only AFTER build_level
|
| 189 |
+
# has been called (the game sizes its camera from it right after building).
|
| 190 |
grid_size: tuple[int, int] = _GRID_SIZE
|
| 191 |
rules_text: str = (
|
| 192 |
+
"You control the focal agent 'A' on a grid. A predator 'B' hunts "
|
| 193 |
"you, moving one cell along the shortest path toward you every turn. "
|
| 194 |
"Walls '#' block movement; '.' is open ground. You are eliminated if "
|
| 195 |
"the predator reaches your cell. Survive as long as you can. "
|
|
|
|
| 214 |
# World construction
|
| 215 |
# ------------------------------------------------------------------ #
|
| 216 |
def build_level(self, rng: random.Random, difficulty: Difficulty) -> Level:
|
| 217 |
+
"""Build the hand-authored level for *difficulty* (deterministic).
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 218 |
|
| 219 |
Args:
|
| 220 |
+
rng: Seeded RNG (reserved for tie-breaks; layouts are fixed).
|
| 221 |
+
difficulty: The session difficulty band.
|
| 222 |
|
| 223 |
Returns:
|
| 224 |
+
A :class:`~proteus.arc_grid.Level` with focal, predator, and walls.
|
|
|
|
|
|
|
| 225 |
"""
|
| 226 |
+
del rng # deterministic hand-authored layouts
|
| 227 |
+
layout = _LAYOUTS.get(difficulty, _LAYOUTS[Difficulty.EASY])
|
| 228 |
+
self._wall_cells = frozenset(layout.wall_cells)
|
| 229 |
+
# Instance attribute shadows the class default so the camera and
|
| 230 |
+
# within_bounds both see THIS band's size (they read scenario.grid_size).
|
| 231 |
+
self.grid_size = layout.grid_size
|
| 232 |
|
| 233 |
focal = Sprite(
|
| 234 |
pixels=[[FOCAL_IDX]],
|
| 235 |
name="focal",
|
| 236 |
+
x=layout.focal_start[0],
|
| 237 |
+
y=layout.focal_start[1],
|
| 238 |
blocking=BlockingMode.PIXEL_PERFECT,
|
| 239 |
)
|
| 240 |
predator = Sprite(
|
| 241 |
pixels=[[PREDATOR_IDX]],
|
| 242 |
name="predator",
|
| 243 |
+
x=layout.predator_start[0],
|
| 244 |
+
y=layout.predator_start[1],
|
| 245 |
blocking=BlockingMode.PIXEL_PERFECT,
|
| 246 |
)
|
| 247 |
sprites: list[Sprite] = [focal, predator]
|
| 248 |
+
for (wx, wy) in layout.wall_cells:
|
| 249 |
sprites.append(
|
| 250 |
Sprite(
|
| 251 |
pixels=[[WALL_IDX]],
|
tests/grid/test_difficulty_layouts.py
CHANGED
|
@@ -1,6 +1,9 @@
|
|
| 1 |
import random
|
| 2 |
|
|
|
|
|
|
|
| 3 |
from proteus.grid.difficulty import Difficulty
|
|
|
|
| 4 |
from proteus.grid.scenario import Scenario, get_scenario
|
| 5 |
|
| 6 |
|
|
@@ -24,3 +27,38 @@ def test_record_focal_move_default_is_noop_on_base():
|
|
| 24 |
s = _scenario()
|
| 25 |
s.record_focal_move("up")
|
| 26 |
assert s.habit_action(None) == "up"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
import random
|
| 2 |
|
| 3 |
+
import numpy as np
|
| 4 |
+
|
| 5 |
from proteus.grid.difficulty import Difficulty
|
| 6 |
+
from proteus.grid.game import MotiveGridGame
|
| 7 |
from proteus.grid.scenario import Scenario, get_scenario
|
| 8 |
|
| 9 |
|
|
|
|
| 27 |
s = _scenario()
|
| 28 |
s.record_focal_move("up")
|
| 29 |
assert s.habit_action(None) == "up"
|
| 30 |
+
|
| 31 |
+
|
| 32 |
+
_BANDS = [Difficulty.EASY, Difficulty.MEDIUM, Difficulty.HARD, Difficulty.EXPERT]
|
| 33 |
+
|
| 34 |
+
|
| 35 |
+
def _game(difficulty, seed=42):
|
| 36 |
+
s = _scenario()
|
| 37 |
+
return MotiveGridGame(s, random.Random(seed), difficulty, max_steps=20), s
|
| 38 |
+
|
| 39 |
+
|
| 40 |
+
def test_easy_layout_unchanged():
|
| 41 |
+
# EASY remains the 8x8 single-wall world: focal (5,3), predator (7,3).
|
| 42 |
+
game, _ = _game(Difficulty.EASY)
|
| 43 |
+
assert game.current_grid().shape == (8, 8)
|
| 44 |
+
assert (game.focal_sprite.x, game.focal_sprite.y) == (5, 3)
|
| 45 |
+
assert (game.predator_sprite.x, game.predator_sprite.y) == (7, 3)
|
| 46 |
+
|
| 47 |
+
|
| 48 |
+
def test_bands_have_distinct_grid_sizes_or_starts():
|
| 49 |
+
seen = set()
|
| 50 |
+
for d in _BANDS:
|
| 51 |
+
game, _ = _game(d)
|
| 52 |
+
h, w = game.current_grid().shape
|
| 53 |
+
fx, fy = game.focal_sprite.x, game.focal_sprite.y
|
| 54 |
+
px, py = game.predator_sprite.x, game.predator_sprite.y
|
| 55 |
+
seen.add((h, w, fx, fy, px, py))
|
| 56 |
+
# All four bands must be pairwise distinct: each band's (size, focal, predator) tuple unique.
|
| 57 |
+
assert len(seen) == 4
|
| 58 |
+
|
| 59 |
+
|
| 60 |
+
def test_each_band_is_deterministic():
|
| 61 |
+
for d in _BANDS:
|
| 62 |
+
g1, _ = _game(d, seed=7)
|
| 63 |
+
g2, _ = _game(d, seed=7)
|
| 64 |
+
assert np.array_equal(g1.current_grid(), g2.current_grid())
|