Spaces:
Runtime error
Runtime error
Commit ·
39c93ec
1
Parent(s): 2e262ce
feat(engine): per-scenario turn_order with predator-first step swap
Browse filesAdd Scenario.turn_order class attribute ("focal_first" default) and honour it
in MotiveGridGame.step(): predator_first calls advance_threat before the focal
moves; focal_first (default) preserves legacy behaviour so predator_evade and
all existing tests are unaffected.
- proteus/game/engine/grid.py +14 -9
- proteus/game/scenarios/base.py +6 -0
- tests/engine/test_turn_order.py +36 -0
proteus/game/engine/grid.py
CHANGED
|
@@ -145,27 +145,32 @@ class MotiveGridGame(ARCBaseGame):
|
|
| 145 |
return self.perform_action(action_input)
|
| 146 |
|
| 147 |
def step(self) -> None:
|
| 148 |
-
"""Resolve a single turn
|
| 149 |
|
| 150 |
-
|
| 151 |
-
|
| 152 |
-
|
|
|
|
| 153 |
"""
|
| 154 |
action = _GAMEACTION_TO_ACTION.get(self._action.id, "stay")
|
| 155 |
dx, dy = _DIRECTION_DELTAS[action]
|
| 156 |
-
|
| 157 |
focal = self.focal_sprite
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 158 |
if (
|
| 159 |
focal is not None
|
| 160 |
and (dx != 0 or dy != 0)
|
| 161 |
and self._footprint_in_bounds(focal, dx, dy)
|
| 162 |
):
|
| 163 |
-
# arc_grid has no implicit world border: try_move_sprite reverts
|
| 164 |
-
# only on sprite collision, so the framework guards the grid edge
|
| 165 |
-
# itself. Internal walls are still handled by try_move_sprite.
|
| 166 |
self.try_move_sprite(focal, dx, dy)
|
| 167 |
|
| 168 |
-
|
|
|
|
| 169 |
|
| 170 |
self.step_count += 1
|
| 171 |
|
|
|
|
| 145 |
return self.perform_action(action_input)
|
| 146 |
|
| 147 |
def step(self) -> None:
|
| 148 |
+
"""Resolve a single turn, honouring ``scenario.turn_order``.
|
| 149 |
|
| 150 |
+
focal_first (default): move focal, then advance threat (chasing the new
|
| 151 |
+
focal cell). predator_first: advance threat first (chasing the current
|
| 152 |
+
focal cell), then move focal. Capture/survival are checked once, after
|
| 153 |
+
both have moved. Always completes the action so the engine loop ends.
|
| 154 |
"""
|
| 155 |
action = _GAMEACTION_TO_ACTION.get(self._action.id, "stay")
|
| 156 |
dx, dy = _DIRECTION_DELTAS[action]
|
|
|
|
| 157 |
focal = self.focal_sprite
|
| 158 |
+
predator_first = (
|
| 159 |
+
getattr(self.scenario, "turn_order", "focal_first") == "predator_first"
|
| 160 |
+
)
|
| 161 |
+
|
| 162 |
+
if predator_first:
|
| 163 |
+
self.scenario.advance_threat(self)
|
| 164 |
+
|
| 165 |
if (
|
| 166 |
focal is not None
|
| 167 |
and (dx != 0 or dy != 0)
|
| 168 |
and self._footprint_in_bounds(focal, dx, dy)
|
| 169 |
):
|
|
|
|
|
|
|
|
|
|
| 170 |
self.try_move_sprite(focal, dx, dy)
|
| 171 |
|
| 172 |
+
if not predator_first:
|
| 173 |
+
self.scenario.advance_threat(self)
|
| 174 |
|
| 175 |
self.step_count += 1
|
| 176 |
|
proteus/game/scenarios/base.py
CHANGED
|
@@ -66,6 +66,12 @@ class Scenario(ABC):
|
|
| 66 |
# Default empty -> callers fall back to rules_text. Scenarios that want a
|
| 67 |
# more transparent practice brief override this with their own text.
|
| 68 |
memory_brief: str = ""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 69 |
|
| 70 |
@abstractmethod
|
| 71 |
def build_level(self, rng: random.Random, difficulty: Difficulty) -> Level:
|
|
|
|
| 66 |
# Default empty -> callers fall back to rules_text. Scenarios that want a
|
| 67 |
# more transparent practice brief override this with their own text.
|
| 68 |
memory_brief: str = ""
|
| 69 |
+
# Turn resolution order. "focal_first" (default): focal moves, then the
|
| 70 |
+
# threat advances (chasing the focal's NEW cell). "predator_first": the
|
| 71 |
+
# threat advances first (chasing the focal's CURRENT cell), then the focal
|
| 72 |
+
# moves. Scenarios override this; predator_evade keeps the default so its
|
| 73 |
+
# dead-end diagnostic is preserved.
|
| 74 |
+
turn_order: str = "focal_first"
|
| 75 |
|
| 76 |
@abstractmethod
|
| 77 |
def build_level(self, rng: random.Random, difficulty: Difficulty) -> Level:
|
tests/engine/test_turn_order.py
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# tests/engine/test_turn_order.py
|
| 2 |
+
"""predator-first reorders the turn: advance_threat sees the focal's PRE-move cell."""
|
| 3 |
+
import random
|
| 4 |
+
|
| 5 |
+
from proteus.game.engine.difficulty import Difficulty
|
| 6 |
+
from proteus.game.engine.grid import MotiveGridGame
|
| 7 |
+
from proteus.game.scenarios.base import get_scenario
|
| 8 |
+
import proteus.game.scenarios # noqa: F401 (register scenarios)
|
| 9 |
+
|
| 10 |
+
|
| 11 |
+
def _focal_cell_seen_by_threat(turn_order: str, action: str = "up"):
|
| 12 |
+
scen = get_scenario("predator_evade")()
|
| 13 |
+
scen.turn_order = turn_order # instance override
|
| 14 |
+
game = MotiveGridGame(scen, random.Random(0), Difficulty.EASY, max_steps=10)
|
| 15 |
+
seen = {}
|
| 16 |
+
real_advance = scen.advance_threat
|
| 17 |
+
|
| 18 |
+
def spy(g):
|
| 19 |
+
f = g.focal_sprite
|
| 20 |
+
seen["focal"] = (f.x, f.y)
|
| 21 |
+
real_advance(g)
|
| 22 |
+
|
| 23 |
+
scen.advance_threat = spy # shadow the bound method on this instance
|
| 24 |
+
before = (game.focal_sprite.x, game.focal_sprite.y)
|
| 25 |
+
game.apply_motive_action(action)
|
| 26 |
+
return before, seen["focal"]
|
| 27 |
+
|
| 28 |
+
|
| 29 |
+
def test_predator_first_threat_sees_premove_focal():
|
| 30 |
+
before, seen = _focal_cell_seen_by_threat("predator_first", "up")
|
| 31 |
+
assert seen == before # predator advanced BEFORE the focal moved
|
| 32 |
+
|
| 33 |
+
|
| 34 |
+
def test_focal_first_threat_sees_postmove_focal():
|
| 35 |
+
before, seen = _focal_cell_seen_by_threat("focal_first", "up")
|
| 36 |
+
assert seen != before # legacy: focal moved first, predator chased the new cell
|