2026-07-21 19:36:02 -04:00
|
|
|
"""Trigger conditions and the arm-delay hysteresis."""
|
|
|
|
|
|
|
|
|
|
import pygame
|
|
|
|
|
from conftest import FakeGame, FakeLevel
|
|
|
|
|
from game.traps import make_condition
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def ev(spec, px, py, w=20, h=28, dt=0.0):
|
2026-08-01 12:19:46 -04:00
|
|
|
trap = type(
|
|
|
|
|
"T", (), {"tile": 32, "sensor_rect": lambda self: pygame.Rect(100, 100, 32, 32)}
|
|
|
|
|
)()
|
2026-07-21 19:36:02 -04:00
|
|
|
return make_condition(spec).evaluate(trap, FakeGame(pygame.Rect(px, py, w, h)), dt)
|
2026-08-01 12:19:46 -04:00
|
|
|
|
|
|
|
|
|
2026-07-21 19:36:02 -04:00
|
|
|
# trap centre = (116, 116); left100 right132 top100 bottom132
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_always():
|
|
|
|
|
assert ev("always", 999, 999) is True
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_within_radius():
|
2026-08-01 12:19:46 -04:00
|
|
|
assert ev({"within": 2}, 106, 104) is True # ~10px away
|
|
|
|
|
assert ev({"within": 2}, 400, 116) is False # far
|
2026-07-21 19:36:02 -04:00
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_dir_left_right():
|
2026-08-01 12:19:46 -04:00
|
|
|
assert ev({"dir": "left"}, 40, 105) is True # to the left
|
|
|
|
|
assert ev({"dir": "left"}, 200, 105) is False # to the right
|
2026-07-21 19:36:02 -04:00
|
|
|
assert ev({"dir": "right"}, 200, 105) is True
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_dir_range():
|
2026-08-01 12:19:46 -04:00
|
|
|
assert ev({"dir": "left", "range": 2}, 60, 105) is True # within 2 tiles left
|
|
|
|
|
assert ev({"dir": "left", "range": 2}, 10, 105) is False # too far left
|
2026-07-21 19:36:02 -04:00
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_dir_aligned():
|
|
|
|
|
# above + aligned requires horizontal overlap with the trap column
|
|
|
|
|
assert ev({"dir": "above", "aligned": True}, 108, 60) is True
|
|
|
|
|
assert ev({"dir": "above", "aligned": True}, 108, 110) is False # not above
|
2026-08-01 12:19:46 -04:00
|
|
|
assert ev({"dir": "above", "aligned": True}, 400, 60) is False # not aligned
|
2026-07-21 19:36:02 -04:00
|
|
|
# without aligned, any column counts
|
|
|
|
|
assert ev({"dir": "above"}, 400, 60) is True
|
|
|
|
|
|
|
|
|
|
|
2026-07-25 13:13:48 -04:00
|
|
|
def test_dir_inclusive():
|
|
|
|
|
# trap column spans x 100..132 (centre 116). A player standing *inside* the
|
|
|
|
|
# column (centre 116) is neither strictly left nor strictly right of it.
|
2026-08-01 12:19:46 -04:00
|
|
|
assert ev({"dir": "left"}, 106, 105) is False # centre 116, inside -> not left
|
|
|
|
|
assert ev({"dir": "right"}, 106, 105) is False # inside -> not right
|
2026-07-25 13:13:48 -04:00
|
|
|
# inclusive counts the trap's own tile as being on that side
|
|
|
|
|
assert ev({"dir": "left", "inclusive": True}, 106, 105) is True
|
|
|
|
|
assert ev({"dir": "right", "inclusive": True}, 106, 105) is True
|
|
|
|
|
# and vertically, too (column spans y 100..132)
|
|
|
|
|
assert ev({"dir": "above"}, 108, 106) is False
|
|
|
|
|
assert ev({"dir": "above", "inclusive": True}, 108, 106) is True
|
|
|
|
|
assert ev({"dir": "below"}, 108, 106) is False
|
|
|
|
|
assert ev({"dir": "below", "inclusive": True}, 108, 106) is True
|
|
|
|
|
|
|
|
|
|
|
2026-07-21 19:36:02 -04:00
|
|
|
def test_all_and_any():
|
|
|
|
|
c = {"all": [{"within": 3}, {"dir": "above", "aligned": True}]}
|
|
|
|
|
assert ev(c, 108, 80) is True
|
2026-08-01 12:19:46 -04:00
|
|
|
assert ev(c, 108, 110) is False # close but not above
|
2026-07-21 19:36:02 -04:00
|
|
|
c2 = {"any": [{"dir": "left"}, {"dir": "right"}]}
|
|
|
|
|
assert ev(c2, 40, 105) is True
|
2026-08-01 12:19:46 -04:00
|
|
|
assert ev(c2, 110, 40) is False # above-only satisfies neither
|
2026-07-21 19:36:02 -04:00
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_timer_cycles():
|
|
|
|
|
c = make_condition({"timer": {"interval": 0.3, "up_time": 0.2}})
|
2026-08-01 12:19:46 -04:00
|
|
|
trap = type(
|
|
|
|
|
"T", (), {"tile": 32, "sensor_rect": lambda self: pygame.Rect(0, 0, 32, 32)}
|
|
|
|
|
)()
|
2026-07-21 19:36:02 -04:00
|
|
|
g = FakeGame(pygame.Rect(0, 0, 4, 4))
|
|
|
|
|
states = [c.evaluate(trap, g, 1 / 60) for _ in range(30)]
|
2026-08-01 12:19:46 -04:00
|
|
|
assert states[0] is False # starts in the "off" interval
|
|
|
|
|
assert any(states) and not all(states) # cycles on and off
|
2026-07-21 19:36:02 -04:00
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_bad_condition_raises():
|
|
|
|
|
import pytest
|
2026-08-01 12:19:46 -04:00
|
|
|
|
2026-07-21 19:36:02 -04:00
|
|
|
with pytest.raises(ValueError):
|
|
|
|
|
make_condition({"nope": 1})
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_delay_hysteresis(make_level):
|
|
|
|
|
# A spike within-1.6 with delay 0.25 arms only after staying in range.
|
|
|
|
|
lvl = make_level("""
|
|
|
|
|
name: t
|
|
|
|
|
tile_size: 32
|
|
|
|
|
map: |
|
|
|
|
|
#####
|
|
|
|
|
#P.G#
|
|
|
|
|
#####
|
|
|
|
|
traps:
|
|
|
|
|
- type: spike
|
|
|
|
|
at: [1, 1]
|
|
|
|
|
trigger: { within: 5 }
|
|
|
|
|
delay: 0.25
|
|
|
|
|
""")
|
|
|
|
|
sp = lvl.traps[0]
|
2026-08-01 12:19:46 -04:00
|
|
|
g = FakeGame(sp.base_rect.copy()) # player right on it -> in range
|
2026-07-21 19:36:02 -04:00
|
|
|
armed = None
|
|
|
|
|
for i in range(30):
|
|
|
|
|
if sp.triggered(g, 1 / 60):
|
|
|
|
|
armed = i
|
|
|
|
|
break
|
2026-08-01 12:19:46 -04:00
|
|
|
assert armed is not None and 13 <= armed <= 17 # ~0.25s = 15 frames
|