irregular6612 Claude Opus 4.8 (1M context) commited on
Commit
cc37031
·
1 Parent(s): 6b5cb90

feat(cp6): Scenario.build_level takes difficulty + record_focal_move/safety_distance defaults

Browse files
proteus/grid/game.py CHANGED
@@ -100,7 +100,7 @@ class MotiveGridGame(ARCBaseGame):
100
  difficulty: Session difficulty (passed through to the scenario).
101
  max_steps: Survival budget; surviving this many played turns wins.
102
  """
103
- level = scenario.build_level(rng)
104
  width, height = scenario.grid_size
105
  camera = Camera(width=width, height=height)
106
 
 
100
  difficulty: Session difficulty (passed through to the scenario).
101
  max_steps: Survival budget; surviving this many played turns wins.
102
  """
103
+ level = scenario.build_level(rng, difficulty)
104
  width, height = scenario.grid_size
105
  camera = Camera(width=width, height=height)
106
 
proteus/grid/scenario.py CHANGED
@@ -39,6 +39,7 @@ if TYPE_CHECKING:
39
  # is injected into MotiveGridGame, and the game reads it back here only
40
  # for annotations.
41
  from .game import MotiveGridGame
 
42
 
43
 
44
  class Scenario(ABC):
@@ -63,19 +64,18 @@ class Scenario(ABC):
63
  rules_text: str
64
 
65
  @abstractmethod
66
- def build_level(self, rng: random.Random) -> Level:
67
- """Build the arc_grid level: walls plus initial sprite positions.
68
 
69
- All randomness (wall placement, initial positions) must flow from
70
- *rng* so that a given ``(scenario, seed)`` is fully deterministic.
71
 
72
  Args:
73
  rng: Seeded RNG owned by the game/module.
 
74
 
75
  Returns:
76
- A freshly built :class:`~proteus.arc_grid.Level` containing the
77
- focal sprite (``name="focal"``), the predator sprite
78
- (``name="predator"``), and any walls / NPCs.
79
  """
80
  raise NotImplementedError
81
 
@@ -175,6 +175,23 @@ class Scenario(ABC):
175
  """
176
  raise NotImplementedError
177
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
178
 
179
  # --------------------------------------------------------------------------- #
180
  # Scenario registry
 
39
  # is injected into MotiveGridGame, and the game reads it back here only
40
  # for annotations.
41
  from .game import MotiveGridGame
42
+ from .difficulty import Difficulty
43
 
44
 
45
  class Scenario(ABC):
 
64
  rules_text: str
65
 
66
  @abstractmethod
67
+ def build_level(self, rng: random.Random, difficulty: Difficulty) -> Level:
68
+ """Build the arc_grid level for *difficulty*: walls + initial sprites.
69
 
70
+ All randomness must flow from *rng* so that a given
71
+ ``(scenario, seed, difficulty)`` is fully deterministic.
72
 
73
  Args:
74
  rng: Seeded RNG owned by the game/module.
75
+ difficulty: The session difficulty band (a ``Difficulty``).
76
 
77
  Returns:
78
+ A freshly built :class:`~proteus.arc_grid.Level`.
 
 
79
  """
80
  raise NotImplementedError
81
 
 
175
  """
176
  raise NotImplementedError
177
 
178
+ def record_focal_move(self, action: str) -> None:
179
+ """Record the focal's last committed move (no-op by default).
180
+
181
+ Scenarios whose ``habit_action`` tracks the live trajectory override
182
+ this; others (and future scenarios) inherit a safe no-op so callers
183
+ like ``SessionRunner`` and ``viz.reconstruct`` never hit AttributeError.
184
+ """
185
+
186
+ def safety_distance(self, game: MotiveGridGame) -> int | None:
187
+ """Return the focal's 'safety' distance for this motive (None by default).
188
+
189
+ Survival-style scenarios return the BFS distance from the focal to the
190
+ threat (larger = safer). Non-survival scenarios leave it ``None`` so the
191
+ trajectory final-distance metric is simply skipped for them.
192
+ """
193
+ return None
194
+
195
 
196
  # --------------------------------------------------------------------------- #
197
  # Scenario registry
proteus/grid/scenarios/predator_evade.py CHANGED
@@ -158,23 +158,27 @@ class PredatorEvade(Scenario):
158
  # ------------------------------------------------------------------ #
159
  # World construction
160
  # ------------------------------------------------------------------ #
161
- def build_level(self, rng: random.Random) -> Level:
162
- """Build the EASY level: focal mid-west, predator east, dead-end wall.
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
 
169
  Args:
170
  rng: Seeded RNG owned by the game/module (unused at EASY).
 
171
 
172
  Returns:
173
- A :class:`~squid_game.arc_grid.Level` with the focal
174
  (``name="focal"``), predator (``name="predator"``), and the wall
175
  sprites (tagged ``sys_static`` so the engine merges them).
176
  """
177
- del rng # deterministic EASY layout; reserved for harder difficulties
 
178
 
179
  self._wall_cells = frozenset(_WALL_CELLS)
180
 
 
158
  # ------------------------------------------------------------------ #
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
 
tests/grid/test_difficulty_layouts.py ADDED
@@ -0,0 +1,26 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import random
2
+
3
+ from proteus.grid.difficulty import Difficulty
4
+ from proteus.grid.scenario import Scenario, get_scenario
5
+
6
+
7
+ def _scenario():
8
+ return get_scenario("predator_evade")()
9
+
10
+
11
+ def test_build_level_accepts_difficulty():
12
+ s = _scenario()
13
+ level = s.build_level(random.Random(42), Difficulty.EASY)
14
+ names = {sp.name for sp in level.get_sprites()}
15
+ assert "focal" in names and "predator" in names
16
+
17
+
18
+ def test_record_focal_move_default_is_noop_on_base():
19
+ # hasattr proves the no-op default is now inherited from the Scenario ABC
20
+ # (so non-tracking scenarios never AttributeError). PredatorEvade overrides
21
+ # it to track the live trajectory, which habit_action reads back.
22
+ assert hasattr(Scenario, "record_focal_move")
23
+ # PredatorEvade still tracks it (used by habit_action).
24
+ s = _scenario()
25
+ s.record_focal_move("up")
26
+ assert s.habit_action(None) == "up"