McRogueFace/tests/integration/grid_step_test.py
John McCardle 700c21ce96 Phase 3: Behavior system with grid.step() turn manager
- Add EntityBehavior struct with 11 behavior types: IDLE, CUSTOM,
  NOISE4/8, PATH, WAYPOINT, PATROL, LOOP, SLEEP, SEEK, FLEE.
  Each returns BehaviorOutput (MOVED/DONE/BLOCKED/NO_ACTION) without
  modifying entity position directly (closes #300)
- Add grid.step(n=1, turn_order=None) turn manager: groups entities
  by turn_order, executes behaviors, fires triggers (TARGET/DONE/BLOCKED),
  updates cell_position and spatial hash. Snapshot-based iteration for
  callback safety (closes #301)
- Entity properties: behavior_type (read-only), turn_order, move_speed,
  target_label, sight_radius. Method: set_behavior(type, waypoints,
  turns, path)
- Update ColorLayer::updatePerspective to use cell_position

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-15 22:14:02 -04:00

155 lines
5.4 KiB
Python

"""Integration test for #301: grid.step() turn manager."""
import mcrfpy
import sys
def make_grid(w=20, h=20):
"""Create a walkable grid with walls on borders."""
scene = mcrfpy.Scene("test301")
mcrfpy.current_scene = scene
tex = mcrfpy.Texture("assets/kenney_tinydungeon.png", 16, 16)
grid = mcrfpy.Grid(grid_size=(w, h), texture=tex, pos=(0, 0), size=(320, 320))
scene.children.append(grid)
for y in range(h):
for x in range(w):
pt = grid.at(x, y)
if x == 0 or x == w-1 or y == 0 or y == h-1:
pt.walkable = False
pt.transparent = False
else:
pt.walkable = True
pt.transparent = True
return grid
def test_step_basic():
"""grid.step() executes without error."""
grid = make_grid()
e = mcrfpy.Entity((5, 5), grid=grid)
e.set_behavior(int(mcrfpy.Behavior.NOISE4))
grid.step() # Should not crash
print("PASS: grid.step() basic execution")
def test_step_noise_movement():
"""NOISE4 entity moves to adjacent cell after step."""
grid = make_grid()
e = mcrfpy.Entity((10, 10), grid=grid)
e.set_behavior(int(mcrfpy.Behavior.NOISE4))
e.move_speed = 0 # Instant movement
old_x, old_y = e.cell_x, e.cell_y
grid.step()
new_x, new_y = e.cell_x, e.cell_y
# Should have moved to an adjacent cell (or stayed if all blocked, unlikely in open grid)
dx = abs(new_x - old_x)
dy = abs(new_y - old_y)
assert dx + dy <= 1, f"NOISE4 should move at most 1 cell cardinal, moved ({dx}, {dy})"
assert dx + dy == 1, f"NOISE4 should move exactly 1 cell in open grid, moved ({dx}, {dy})"
print("PASS: NOISE4 moves to adjacent cell")
def test_step_idle_no_move():
"""IDLE entity does not move."""
grid = make_grid()
e = mcrfpy.Entity((10, 10), grid=grid)
# Default behavior is IDLE
grid.step()
assert e.cell_x == 10 and e.cell_y == 10, "IDLE entity should not move"
print("PASS: IDLE entity stays put")
def test_step_turn_order():
"""Entities process in turn_order order."""
grid = make_grid()
order_log = []
e1 = mcrfpy.Entity((5, 5), grid=grid)
e1.turn_order = 2
e1.set_behavior(int(mcrfpy.Behavior.CUSTOM))
e1.step = lambda t, d: order_log.append(2)
e2 = mcrfpy.Entity((7, 7), grid=grid)
e2.turn_order = 1
e2.set_behavior(int(mcrfpy.Behavior.CUSTOM))
e2.step = lambda t, d: order_log.append(1)
# CUSTOM behavior fires NO_ACTION, so step callback won't fire via triggers
# But we can verify turn_order sorting via a different approach
# Let's use SLEEP with turns=1 which triggers DONE
e1.set_behavior(int(mcrfpy.Behavior.SLEEP), turns=1)
e1.step = lambda t, d: order_log.append(2)
e2.set_behavior(int(mcrfpy.Behavior.SLEEP), turns=1)
e2.step = lambda t, d: order_log.append(1)
grid.step()
assert order_log == [1, 2], f"Expected [1, 2] turn order, got {order_log}"
print("PASS: turn_order sorting")
def test_step_turn_order_zero_skip():
"""turn_order=0 entities are skipped."""
grid = make_grid()
e = mcrfpy.Entity((10, 10), grid=grid)
e.turn_order = 0
e.set_behavior(int(mcrfpy.Behavior.NOISE4))
e.move_speed = 0
grid.step()
assert e.cell_x == 10 and e.cell_y == 10, "turn_order=0 entity should be skipped"
print("PASS: turn_order=0 skipped")
def test_step_done_trigger():
"""SLEEP behavior fires DONE trigger when turns exhausted."""
grid = make_grid()
triggered = []
e = mcrfpy.Entity((5, 5), grid=grid)
e.set_behavior(int(mcrfpy.Behavior.SLEEP), turns=2)
e.step = lambda t, d: triggered.append(int(t))
grid.step() # Sleep turns: 2 -> 1
assert len(triggered) == 0, "Should not trigger DONE after first step"
grid.step() # Sleep turns: 1 -> 0 -> DONE
assert len(triggered) == 1, f"Should trigger DONE, got {len(triggered)} triggers"
assert triggered[0] == int(mcrfpy.Trigger.DONE), f"Should be DONE trigger, got {triggered[0]}"
print("PASS: SLEEP DONE trigger")
def test_step_n_rounds():
"""grid.step(n=3) executes 3 rounds."""
grid = make_grid()
e = mcrfpy.Entity((10, 10), grid=grid)
e.set_behavior(int(mcrfpy.Behavior.NOISE4))
e.move_speed = 0
grid.step(n=3)
# After 3 steps of NOISE4, entity should have moved
# Can't predict exact position due to randomness
print("PASS: grid.step(n=3) executes without error")
def test_step_turn_order_filter():
"""grid.step(turn_order=1) only processes entities with that turn_order."""
grid = make_grid()
e1 = mcrfpy.Entity((5, 5), grid=grid)
e1.turn_order = 1
e1.set_behavior(int(mcrfpy.Behavior.NOISE4))
e1.move_speed = 0
e2 = mcrfpy.Entity((10, 10), grid=grid)
e2.turn_order = 2
e2.set_behavior(int(mcrfpy.Behavior.NOISE4))
e2.move_speed = 0
grid.step(turn_order=1)
# e1 should have moved, e2 should not
assert not (e1.cell_x == 5 and e1.cell_y == 5), "turn_order=1 entity should have moved"
assert e2.cell_x == 10 and e2.cell_y == 10, "turn_order=2 entity should not have moved"
print("PASS: turn_order filter")
if __name__ == "__main__":
test_step_basic()
test_step_noise_movement()
test_step_idle_no_move()
test_step_turn_order()
test_step_turn_order_zero_skip()
test_step_done_trigger()
test_step_n_rounds()
test_step_turn_order_filter()
print("All #301 tests passed")
sys.exit(0)