irregular6612 commited on
Commit
2aee482
·
1 Parent(s): 12a796d

feat(engine): footprint-aware focal bounds (1x1 unchanged)

Browse files
proteus/grid/game.py CHANGED
@@ -158,7 +158,7 @@ class MotiveGridGame(ARCBaseGame):
158
  if (
159
  focal is not None
160
  and (dx != 0 or dy != 0)
161
- and self.within_bounds(focal.x + dx, focal.y + 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
@@ -196,6 +196,20 @@ class MotiveGridGame(ARCBaseGame):
196
  width, height = self.scenario.grid_size
197
  return 0 <= x < width and 0 <= y < height
198
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
199
  def get_sprite(self, name: str) -> Sprite | None:
200
  """Return the first sprite with *name*, or ``None`` if absent.
201
 
 
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
 
196
  width, height = self.scenario.grid_size
197
  return 0 <= x < width and 0 <= y < height
198
 
199
+ def _footprint_in_bounds(self, sprite, dx: int, dy: int) -> bool:
200
+ """Return True if *sprite*'s full footprint stays on-grid after (dx, dy).
201
+
202
+ For a 1x1 sprite this is exactly ``within_bounds(x+dx, y+dy)`` (so the
203
+ single-cell scenarios are unaffected); for a multi-cell sprite it checks
204
+ every footprint cell.
205
+ """
206
+ nx, ny = sprite.x + dx, sprite.y + dy
207
+ for j in range(sprite.height):
208
+ for i in range(sprite.width):
209
+ if not self.within_bounds(nx + i, ny + j):
210
+ return False
211
+ return True
212
+
213
  def get_sprite(self, name: str) -> Sprite | None:
214
  """Return the first sprite with *name*, or ``None`` if absent.
215
 
tests/grid/test_footprint_bounds.py ADDED
@@ -0,0 +1,34 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """The multi-cell focal must keep its full footprint on the grid; a 1x1 sprite
2
+ (predator_evade) behaves exactly as before (regression guard)."""
3
+ from __future__ import annotations
4
+
5
+ import random
6
+
7
+ import proteus.grid # noqa: F401
8
+ from proteus.grid.difficulty import Difficulty
9
+ from proteus.grid.game import MotiveGridGame
10
+ from proteus.grid.scenario import get_scenario
11
+
12
+
13
+ def test_3x3_focal_cannot_leave_grid():
14
+ scenario = get_scenario("pack_evade")()
15
+ game = MotiveGridGame(scenario, random.Random(1), Difficulty.EASY, max_steps=99)
16
+ focal = game.focal_sprite
17
+ focal.move(-focal.x, -focal.y) # anchor (0,0); footprint x,y in [0,3)
18
+ game.apply_motive_action("left") # would push footprint to x=-1 -> rejected
19
+ assert game.focal_sprite.x == 0
20
+ game.apply_motive_action("up") # would push footprint to y=-1 -> rejected
21
+ assert game.focal_sprite.y == 0
22
+ # A legal move still works.
23
+ game.apply_motive_action("right")
24
+ assert game.focal_sprite.x == 1
25
+
26
+
27
+ def test_1x1_focal_regression():
28
+ scenario = get_scenario("predator_evade")()
29
+ game = MotiveGridGame(scenario, random.Random(42), Difficulty.EASY, max_steps=99)
30
+ focal = game.focal_sprite
31
+ x0 = focal.x
32
+ game.apply_motive_action("up") # legal in the 8x8 layout
33
+ assert game.focal_sprite.y == focal.y # moved or not, no crash; 1x1 path unchanged
34
+ assert isinstance(x0, int)