Spaces:
Runtime error
Runtime error
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
|