Spaces:
Runtime error
Runtime error
Commit ·
14842f9
1
Parent(s): 53ff5ab
feat(cp6): runtime.rollout — deterministic optimal-trajectory simulator
Browse files- proteus/runtime/rollout.py +73 -0
- tests/runtime/test_rollout.py +23 -0
proteus/runtime/rollout.py
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Deterministic optimal-trajectory rollout for the trajectory metric.
|
| 2 |
+
|
| 3 |
+
Mirrors viz.reconstruct's engine-replay pattern: rebuild the game from
|
| 4 |
+
(scenario, seed, difficulty), replay the scripted Cut pre-roll, then play
|
| 5 |
+
OPTIMALLY (scenario.optimal_action each turn) for up to n_turns, capturing the
|
| 6 |
+
pre-move focal position per turn and the final safety distance. This is the
|
| 7 |
+
"ideal motive-reader" reference path the realized trajectory is scored against.
|
| 8 |
+
"""
|
| 9 |
+
|
| 10 |
+
from __future__ import annotations
|
| 11 |
+
|
| 12 |
+
import random
|
| 13 |
+
from dataclasses import dataclass
|
| 14 |
+
|
| 15 |
+
from proteus.grid.difficulty import Difficulty
|
| 16 |
+
from proteus.grid.game import MotiveGridGame
|
| 17 |
+
from proteus.grid.scenario import get_scenario
|
| 18 |
+
|
| 19 |
+
|
| 20 |
+
@dataclass(frozen=True)
|
| 21 |
+
class RolloutResult:
|
| 22 |
+
"""The optimal player's trajectory for one session."""
|
| 23 |
+
|
| 24 |
+
focal_positions: list[tuple[int, int]] # pre-move focal cell per optimal turn
|
| 25 |
+
final_safety_distance: int | None # BFS focal->predator at rollout end
|
| 26 |
+
|
| 27 |
+
|
| 28 |
+
def optimal_rollout(
|
| 29 |
+
scenario_name: str,
|
| 30 |
+
seed: int | None,
|
| 31 |
+
difficulty: Difficulty,
|
| 32 |
+
n_turns: int,
|
| 33 |
+
) -> RolloutResult:
|
| 34 |
+
"""Simulate the optimal trajectory and return its positions + final safety.
|
| 35 |
+
|
| 36 |
+
Args:
|
| 37 |
+
scenario_name: Registered scenario key.
|
| 38 |
+
seed: Session seed (same world as the real run).
|
| 39 |
+
difficulty: Session difficulty band.
|
| 40 |
+
n_turns: Cap on optimal play turns (use the realized play length so the
|
| 41 |
+
two trajectories align index-for-index).
|
| 42 |
+
|
| 43 |
+
Returns:
|
| 44 |
+
A :class:`RolloutResult` with the per-turn pre-move focal positions of
|
| 45 |
+
the optimal player and the final BFS safety distance at rollout end.
|
| 46 |
+
"""
|
| 47 |
+
scenario = get_scenario(scenario_name)()
|
| 48 |
+
rng = random.Random(seed)
|
| 49 |
+
cut_length = scenario.cut_length(difficulty)
|
| 50 |
+
game = MotiveGridGame(scenario, rng, difficulty, max_steps=cut_length + n_turns)
|
| 51 |
+
|
| 52 |
+
# Cut pre-roll (scripted policy), identical to SessionRunner.
|
| 53 |
+
for _ in range(cut_length):
|
| 54 |
+
action = scenario.cut_focal_policy(game)
|
| 55 |
+
game.apply_motive_action(action)
|
| 56 |
+
scenario.record_focal_move(action)
|
| 57 |
+
|
| 58 |
+
focal_positions: list[tuple[int, int]] = []
|
| 59 |
+
for _ in range(n_turns):
|
| 60 |
+
focal = game.focal_sprite
|
| 61 |
+
if focal is None:
|
| 62 |
+
break
|
| 63 |
+
focal_positions.append((focal.x, focal.y)) # pre-move
|
| 64 |
+
action = scenario.optimal_action(game)
|
| 65 |
+
game.apply_motive_action(action)
|
| 66 |
+
scenario.record_focal_move(action)
|
| 67 |
+
if game.eliminated or game.survived:
|
| 68 |
+
break
|
| 69 |
+
|
| 70 |
+
return RolloutResult(
|
| 71 |
+
focal_positions=focal_positions,
|
| 72 |
+
final_safety_distance=scenario.safety_distance(game),
|
| 73 |
+
)
|
tests/runtime/test_rollout.py
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from proteus.grid.difficulty import Difficulty
|
| 2 |
+
from proteus.runtime.rollout import RolloutResult, optimal_rollout
|
| 3 |
+
|
| 4 |
+
|
| 5 |
+
def test_optimal_rollout_is_deterministic():
|
| 6 |
+
a = optimal_rollout("predator_evade", seed=42, difficulty=Difficulty.EASY, n_turns=5)
|
| 7 |
+
b = optimal_rollout("predator_evade", seed=42, difficulty=Difficulty.EASY, n_turns=5)
|
| 8 |
+
assert isinstance(a, RolloutResult)
|
| 9 |
+
assert a.focal_positions == b.focal_positions
|
| 10 |
+
assert a.final_safety_distance == b.final_safety_distance
|
| 11 |
+
|
| 12 |
+
|
| 13 |
+
def test_optimal_rollout_length_capped_by_n_turns():
|
| 14 |
+
r = optimal_rollout("predator_evade", seed=42, difficulty=Difficulty.EASY, n_turns=3)
|
| 15 |
+
assert len(r.focal_positions) <= 3
|
| 16 |
+
# Each recorded position is the PRE-move focal cell for that optimal turn.
|
| 17 |
+
assert all(isinstance(p, tuple) and len(p) == 2 for p in r.focal_positions)
|
| 18 |
+
|
| 19 |
+
|
| 20 |
+
def test_optimal_rollout_first_position_is_handover():
|
| 21 |
+
# The first optimal pre-move position equals the focal's handover cell.
|
| 22 |
+
r = optimal_rollout("predator_evade", seed=42, difficulty=Difficulty.EASY, n_turns=5)
|
| 23 |
+
assert r.focal_positions[0] == (3, 3) # EASY handover
|