irregular6612 Claude Sonnet 4.6 commited on
Commit
14842f9
·
1 Parent(s): 53ff5ab

feat(cp6): runtime.rollout — deterministic optimal-trajectory simulator

Browse files
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