191 lines
5.1 KiB
Python
191 lines
5.1 KiB
Python
"""The unified `block` trap: modes, deadly/fake/crumble, jitter, sensing."""
|
|
|
|
import pygame
|
|
from conftest import FakeGame, FakeLevel, reversals, step, run, place, hold
|
|
from game.traps import Block, expand_spec
|
|
|
|
|
|
def block(**spec):
|
|
spec.setdefault("type", "block")
|
|
spec.setdefault("at", [16, 6])
|
|
return Block(spec, FakeLevel())
|
|
|
|
|
|
def test_stationary_is_solid():
|
|
b = block(at=[3, 3])
|
|
assert b.solid_rects() == [b._rect()]
|
|
assert b.hazard_rects() == []
|
|
|
|
|
|
def test_deadly_is_hazard_not_solid():
|
|
b = block(at=[3, 3], deadly=True)
|
|
assert b.solid_rects() == []
|
|
assert b.hazard_rects() # non-empty
|
|
|
|
|
|
def test_fake_is_drawn_but_not_solid():
|
|
b = block(at=[3, 3], fake=True)
|
|
assert b.solid_rects() == [] # you fall through
|
|
assert b.sprite == "fake_block" # looks like a real block
|
|
|
|
|
|
def test_crumble_cycle_and_emerge_kill(make_game):
|
|
g = make_game("""
|
|
name: t
|
|
tile_size: 32
|
|
map: |
|
|
######
|
|
#.P..#
|
|
#...G#
|
|
######
|
|
traps:
|
|
- type: block
|
|
at: [2, 2]
|
|
crumble: true
|
|
crumble_delay: 0.3
|
|
respawn: 0.8
|
|
""")
|
|
cb = g.level.traps[0]
|
|
states, killed, d0 = set(), False, g.deaths
|
|
for _ in range(300):
|
|
step(g)
|
|
states.add(cb.cstate)
|
|
if g.deaths > d0:
|
|
killed = True
|
|
break
|
|
assert "gone" in states # it crumbled away
|
|
assert killed # re-formed onto the standing player
|
|
|
|
|
|
def test_moving_block_that_crumbles(make_game):
|
|
g = make_game("""
|
|
name: t
|
|
tile_size: 32
|
|
map: |
|
|
##########
|
|
#........#
|
|
#........#
|
|
#.......G#
|
|
##########
|
|
traps:
|
|
- type: block
|
|
at: [2, 2]
|
|
path: [[2, 2], [7, 2]]
|
|
mode: pingpong
|
|
speed: 90
|
|
crumble: true
|
|
crumble_delay: 0.3
|
|
respawn: 1.0
|
|
""")
|
|
b = g.level.traps[0]
|
|
place(g, 2, 1) # ride the platform
|
|
moved = crumbled = False
|
|
start = b.x
|
|
for _ in range(200):
|
|
step(g)
|
|
moved |= abs(b.x - start) > 20
|
|
crumbled |= b.cstate != "solid"
|
|
if g.state != "playing":
|
|
break
|
|
assert moved and crumbled
|
|
|
|
|
|
def test_patrol_loop_visits_all_waypoints():
|
|
b = block(at=[0, 0], path=[[0, 0], [4, 0], [4, 3]], mode="loop", speed=400)
|
|
g = FakeGame(pygame.Rect(0, 0, 4, 4))
|
|
seen = set()
|
|
for _ in range(600):
|
|
b.update(1 / 60, g)
|
|
seen.add((round(b.x / 32), round(b.y / 32)))
|
|
assert {(0, 0), (4, 0), (4, 3)} <= seen
|
|
|
|
|
|
def test_slider_once_no_jitter_and_retracts():
|
|
# sense=home (default): a slider moving away can't toggle its own trigger.
|
|
b = block(at=[16, 6], move=[-3, 0], speed=260, trigger={"within": 3})
|
|
g = FakeGame(pygame.Rect(17 * 32, 6 * 32, 23, 29)) # player parked to the right
|
|
xs = [(b.update(1 / 60, g), b.x)[1] for _ in range(120)]
|
|
assert reversals(xs) == 0 and abs(xs[-1] - 13 * 32) < 2 # committed to displaced
|
|
g.player.rect.x = 30 * 32 # leave
|
|
xs = [(b.update(1 / 60, g), b.x)[1] for _ in range(120)]
|
|
assert reversals(xs) == 0 and abs(xs[-1] - 16 * 32) < 2 # retracted to base
|
|
|
|
|
|
def test_sense_current_rides_down():
|
|
# A dropper (sense=current) commits to the bottom and holds while ridden.
|
|
b = block(
|
|
at=[21, 9],
|
|
move=[0, 4],
|
|
speed=220,
|
|
sense="current",
|
|
trigger={"all": [{"within": 5}, {"dir": "above", "aligned": True}]},
|
|
)
|
|
# player standing on top of it
|
|
p = pygame.Rect(21 * 32 + 4, 9 * 32 - 29, 23, 29)
|
|
g = FakeGame(p)
|
|
ys = []
|
|
for _ in range(90):
|
|
b.update(1 / 60, g)
|
|
# keep the player riding the block top
|
|
p.bottom = b._rect().top
|
|
ys.append(b.y)
|
|
assert reversals(ys) == 0 and abs(ys[-1] - 13 * 32) < 2 # dropped fully, no bob
|
|
|
|
|
|
def test_carriers_reports_motion():
|
|
b = block(at=[0, 5], path=[[0, 5], [5, 5]], mode="pingpong", speed=120)
|
|
g = FakeGame(pygame.Rect(0, 0, 4, 4))
|
|
b.update(1 / 60, g)
|
|
((rect, dx, dy),) = b.carriers()
|
|
assert dx != 0 and dy == 0 # moving horizontally
|
|
|
|
|
|
# --- array expansion (count / spacing) --------------------------------------
|
|
def test_expand_no_count_yields_one():
|
|
specs = list(expand_spec({"type": "block", "at": [2, 3]}))
|
|
assert specs == [{"type": "block", "at": [2, 3]}]
|
|
|
|
|
|
def test_expand_line():
|
|
ats = [s["at"] for s in expand_spec({"type": "spike", "at": [1, 1], "count": 4})]
|
|
assert ats == [[1, 1], [2, 1], [3, 1], [4, 1]]
|
|
|
|
|
|
def test_expand_grid_with_spacing():
|
|
ats = [
|
|
s["at"]
|
|
for s in expand_spec(
|
|
{"type": "block", "at": [0, 0], "count": [3, 2], "spacing": [2, 3]}
|
|
)
|
|
]
|
|
assert ats == [[0, 0], [2, 0], [4, 0], [0, 3], [2, 3], [4, 3]]
|
|
# count/spacing are stripped from each expanded spec
|
|
for s in expand_spec({"type": "block", "at": [0, 0], "count": [2, 1]}):
|
|
assert "count" not in s and "spacing" not in s
|
|
|
|
|
|
def test_array_expands_in_level(make_level):
|
|
lvl = make_level("""
|
|
name: t
|
|
tile_size: 32
|
|
map: |
|
|
########
|
|
#......#
|
|
#P....G#
|
|
########
|
|
traps:
|
|
- type: block
|
|
at: [2, 1]
|
|
deadly: true
|
|
count: [4, 1]
|
|
""")
|
|
blocks = [t for t in lvl.traps if isinstance(t, Block)]
|
|
assert len(blocks) == 4
|
|
assert all(b.deadly for b in blocks)
|
|
assert sorted(b.current_rect().x for b in blocks) == [
|
|
2 * 32,
|
|
3 * 32,
|
|
4 * 32,
|
|
5 * 32,
|
|
]
|