irregular6612 Claude Sonnet 4.6 commited on
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 an 8x8 grid. A predator 'B' hunts "
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*: focal, predator, walls.
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 owned by the game/module (unused at EASY).
173
- difficulty: The session difficulty band (a ``Difficulty``).
174
 
175
  Returns:
176
- A :class:`~proteus.arc_grid.Level` with the focal
177
- (``name="focal"``), predator (``name="predator"``), and the wall
178
- sprites (tagged ``sys_static`` so the engine merges them).
179
  """
180
- del rng # deterministic layout; rng reserved for tie-breaks
181
- del difficulty # per-difficulty dispatch added in Task 2
182
-
183
- self._wall_cells = frozenset(_WALL_CELLS)
 
 
184
 
185
  focal = Sprite(
186
  pixels=[[FOCAL_IDX]],
187
  name="focal",
188
- x=_FOCAL_START[0],
189
- y=_FOCAL_START[1],
190
  blocking=BlockingMode.PIXEL_PERFECT,
191
  )
192
  predator = Sprite(
193
  pixels=[[PREDATOR_IDX]],
194
  name="predator",
195
- x=_PREDATOR_START[0],
196
- y=_PREDATOR_START[1],
197
  blocking=BlockingMode.PIXEL_PERFECT,
198
  )
199
  sprites: list[Sprite] = [focal, predator]
200
- for (wx, wy) in _WALL_CELLS:
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())