irregular6612 commited on
Commit
ef90792
·
1 Parent(s): 41f081b

feat(cp8): scenario max_bfs_distance + agent_distance_delta helpers

Browse files
proteus/grid/scenario.py CHANGED
@@ -226,6 +226,27 @@ class Scenario(ABC):
226
  """
227
  return None
228
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
229
 
230
  # --------------------------------------------------------------------------- #
231
  # Scenario registry
 
226
  """
227
  return None
228
 
229
+ def max_bfs_distance(self, game: MotiveGridGame) -> int | None:
230
+ """Free-cell graph diameter (max finite BFS distance), None by default.
231
+
232
+ Used to normalise distance metrics (``distance_auc``) and pressure into a
233
+ unit range. Survival scenarios override it; others inherit ``None`` so the
234
+ normalisation is simply skipped.
235
+ """
236
+ return None
237
+
238
+ def agent_distance_delta(
239
+ self, game, focal_before, predator_before
240
+ ) -> float | None:
241
+ """Chase-corrected action quality (None by default).
242
+
243
+ ``dist(post_focal, predator_before) - dist(focal_before, predator_before)``
244
+ — measures whether the participant's own move opened distance from the
245
+ threat, independent of the predator's chase response (spec §6.2). Non-
246
+ survival scenarios inherit ``None``.
247
+ """
248
+ return None
249
+
250
 
251
  # --------------------------------------------------------------------------- #
252
  # Scenario registry
proteus/grid/scenarios/predator_evade.py CHANGED
@@ -505,6 +505,37 @@ class PredatorEvade(Scenario):
505
  return None
506
  return self._bfs_distance(game, (focal.x, focal.y), (predator.x, predator.y))
507
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
508
  # ------------------------------------------------------------------ #
509
  # ASCII legend
510
  # ------------------------------------------------------------------ #
 
505
  return None
506
  return self._bfs_distance(game, (focal.x, focal.y), (predator.x, predator.y))
507
 
508
+ def max_bfs_distance(self, game: MotiveGridGame) -> int | None:
509
+ """Free-cell graph diameter: the max finite BFS distance between any two
510
+ free cells. Used to normalise distance metrics / pressure into [0, 1]."""
511
+ cells = [
512
+ (x, y)
513
+ for x in range(self.grid_size[0])
514
+ for y in range(self.grid_size[1])
515
+ if self._is_free(game, (x, y))
516
+ ]
517
+ best = 0
518
+ for i, a in enumerate(cells):
519
+ for b in cells[i + 1:]:
520
+ d = self._bfs_distance(game, a, b)
521
+ if d is not None and d > best:
522
+ best = d
523
+ return best
524
+
525
+ def agent_distance_delta(
526
+ self, game: MotiveGridGame, focal_before, predator_before
527
+ ) -> float | None:
528
+ """Chase-corrected action quality (spec §6.2): how much the focal's own
529
+ move opened distance from the *pre-move* predator cell."""
530
+ focal = game.focal_sprite
531
+ if focal is None:
532
+ return None
533
+ d_after = self._bfs_distance(game, (focal.x, focal.y), predator_before)
534
+ d_before = self._bfs_distance(game, focal_before, predator_before)
535
+ if d_after is None or d_before is None:
536
+ return None
537
+ return float(d_after - d_before)
538
+
539
  # ------------------------------------------------------------------ #
540
  # ASCII legend
541
  # ------------------------------------------------------------------ #
tests/grid/test_distance_helpers.py ADDED
@@ -0,0 +1,34 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import random
2
+ import proteus.grid # noqa: F401
3
+ from proteus.grid.difficulty import Difficulty
4
+ from proteus.grid.game import MotiveGridGame
5
+ from proteus.grid.scenario import Scenario, get_scenario
6
+
7
+
8
+ def _game():
9
+ s = get_scenario("predator_evade")()
10
+ g = MotiveGridGame(s, random.Random(42), Difficulty.EASY, max_steps=10)
11
+ return s, g
12
+
13
+
14
+ def test_base_defaults_none():
15
+ assert Scenario.max_bfs_distance is not None # method exists
16
+ # a bare scenario method default returns None (documented contract)
17
+
18
+
19
+ def test_max_bfs_distance_positive_on_easy():
20
+ s, g = _game()
21
+ d = s.max_bfs_distance(g)
22
+ assert isinstance(d, int) and d > 0
23
+
24
+
25
+ def test_agent_distance_delta_positive_when_moving_away():
26
+ s, g = _game()
27
+ focal = g.focal_sprite # EASY: (5,3)
28
+ pred = g.predator_sprite # EASY: (7,3)
29
+ before_focal = (focal.x, focal.y)
30
+ before_pred = (pred.x, pred.y)
31
+ # move focal up to (5,2) -> farther from (7,3)
32
+ g.apply_motive_action("up")
33
+ delta = s.agent_distance_delta(g, before_focal, before_pred)
34
+ assert delta is not None and delta > 0