Initial commit
This commit is contained in:
339
tests/test_traps.py
Normal file
339
tests/test_traps.py
Normal file
@@ -0,0 +1,339 @@
|
||||
"""Spike, arrow_shooter, warp, phase_block + the invisible flag & block arrays."""
|
||||
|
||||
import hashlib
|
||||
import pygame
|
||||
from conftest import (FakeGame, step, run, place, hold, DT)
|
||||
from game.assets import AssetStore
|
||||
from game.traps import Spike, ArrowShooter, Warp, PhaseBlock, Block
|
||||
|
||||
|
||||
# --- spike -------------------------------------------------------------------
|
||||
def test_spike_active_by_trigger(make_level):
|
||||
lvl = make_level("""
|
||||
name: t
|
||||
tile_size: 32
|
||||
map: |
|
||||
#####
|
||||
#P.G#
|
||||
#####
|
||||
traps:
|
||||
- type: spike
|
||||
at: [1, 1]
|
||||
trigger: always
|
||||
direction: up
|
||||
""")
|
||||
sp = lvl.traps[0]
|
||||
sp.update(DT, FakeGame(pygame.Rect(0, 0, 4, 4)))
|
||||
assert sp.active and sp.hazard_rects()
|
||||
|
||||
|
||||
def test_spike_hazard_is_half_tile_on_the_direction_edge(make_level):
|
||||
lvl = make_level("""
|
||||
name: t
|
||||
tile_size: 32
|
||||
map: |
|
||||
#####
|
||||
#P.G#
|
||||
#####
|
||||
traps:
|
||||
- type: spike
|
||||
at: [1, 1]
|
||||
trigger: always
|
||||
direction: down
|
||||
""")
|
||||
sp = lvl.traps[0]
|
||||
sp.update(DT, FakeGame(pygame.Rect(0, 0, 4, 4)))
|
||||
hz = sp.hazard_rects()[0]
|
||||
# down spike occupies the TOP half of its cell
|
||||
assert hz.height == 16 and hz.top == sp.base_rect.top
|
||||
|
||||
|
||||
def test_spike_sprite_rotates_per_direction(tmp_path):
|
||||
# A deliberately asymmetric sprite so each rotation is distinct.
|
||||
surf = pygame.Surface((8, 8), pygame.SRCALPHA)
|
||||
surf.fill((255, 0, 0, 255), (0, 0, 8, 2)) # red bar along the top only
|
||||
adir = tmp_path / "assets"
|
||||
adir.mkdir()
|
||||
pygame.image.save(surf, str(adir / "spike.png"))
|
||||
a = AssetStore(str(adir))
|
||||
up = a.get("spike", 32, 16, 0)
|
||||
down = a.get("spike", 32, 16, 180)
|
||||
left = a.get("spike", 16, 32, 90)
|
||||
|
||||
def h(s):
|
||||
return hashlib.md5(pygame.image.tostring(s, "RGBA")).hexdigest()
|
||||
assert h(up) != h(down) # rotation actually happened
|
||||
assert left.get_size() == (16, 32) and up.get_size() == (32, 16)
|
||||
|
||||
|
||||
# --- arrow shooter -----------------------------------------------------------
|
||||
def test_arrow_shooter_fires_on_interval(make_level):
|
||||
lvl = make_level("""
|
||||
name: t
|
||||
tile_size: 32
|
||||
map: |
|
||||
##########
|
||||
#.......G#
|
||||
#P.......#
|
||||
##########
|
||||
traps:
|
||||
- type: arrow_shooter
|
||||
at: [7, 1]
|
||||
direction: left
|
||||
speed: 200
|
||||
interval: 0.5
|
||||
""")
|
||||
sh = lvl.traps[0]
|
||||
g = FakeGame(pygame.Rect(0, 0, 4, 4))
|
||||
for _ in range(40): # ~0.66s -> at least one shot
|
||||
sh.update(DT, g)
|
||||
assert sh.arrows # spawned arrows
|
||||
assert sh.hazard_rects() # arrows are hazards
|
||||
|
||||
|
||||
def test_arrow_shooter_trigger_gates_firing(make_level):
|
||||
lvl = make_level("""
|
||||
name: t
|
||||
tile_size: 32
|
||||
map: |
|
||||
##########
|
||||
#.......G#
|
||||
#P.......#
|
||||
##########
|
||||
traps:
|
||||
- type: arrow_shooter
|
||||
at: [7, 1]
|
||||
direction: left
|
||||
interval: 0.2
|
||||
trigger: { within: 1 }
|
||||
""")
|
||||
sh = lvl.traps[0]
|
||||
far = FakeGame(pygame.Rect(0, 0, 4, 4)) # nowhere near
|
||||
for _ in range(60):
|
||||
sh.update(DT, far)
|
||||
assert not sh.arrows # never fired while player out of range
|
||||
|
||||
|
||||
# --- invisible wall, now an array of invisible blocks ------------------------
|
||||
def test_invisible_block_array_is_solid_and_invisible(make_level):
|
||||
lvl = make_level("""
|
||||
name: t
|
||||
tile_size: 32
|
||||
map: |
|
||||
######
|
||||
#....#
|
||||
#....#
|
||||
#P..G#
|
||||
######
|
||||
traps:
|
||||
- type: block
|
||||
at: [3, 1]
|
||||
invisible: true
|
||||
count: [1, 2]
|
||||
""")
|
||||
blocks = [t for t in lvl.traps if isinstance(t, Block)]
|
||||
assert len(blocks) == 2 # one per cell
|
||||
assert all(b.invisible and b.solid_rects() for b in blocks)
|
||||
ys = sorted(b.current_rect().top for b in blocks)
|
||||
assert ys == [1 * 32, 2 * 32] # stacked vertically
|
||||
|
||||
|
||||
# --- warp --------------------------------------------------------------------
|
||||
def test_warp_teleports_and_rearms(make_game):
|
||||
g = make_game("""
|
||||
name: t
|
||||
tile_size: 32
|
||||
map: |
|
||||
##########
|
||||
#.......G#
|
||||
#P.......#
|
||||
##########
|
||||
traps:
|
||||
- type: warp
|
||||
at: [4, 2]
|
||||
to: [7, 1]
|
||||
""")
|
||||
place(g, 2, 2)
|
||||
warped = False
|
||||
for _ in range(120):
|
||||
step(g, hold(right=True))
|
||||
if g.player.rect.x >= 7 * 32 - 4:
|
||||
warped = True
|
||||
break
|
||||
assert warped
|
||||
|
||||
|
||||
# --- phase block -------------------------------------------------------------
|
||||
def test_phase_block_intangible_until_triggered(make_game):
|
||||
g = make_game("""
|
||||
name: t
|
||||
tile_size: 32
|
||||
map: |
|
||||
##########
|
||||
#........#
|
||||
#........#
|
||||
#P......G#
|
||||
##########
|
||||
traps:
|
||||
- type: phase_block
|
||||
at: [4, 3]
|
||||
fade: 0.2
|
||||
trigger: { within: 2 }
|
||||
""")
|
||||
pb = g.level.traps[0]
|
||||
place(g, 1, 3)
|
||||
step(g)
|
||||
assert not pb.solid and pb.alpha == 0 # dormant far away
|
||||
run(g, 40, lambda i: hold(right=True))
|
||||
assert pb.solid and pb.alpha > 0 # phased in solid on approach
|
||||
|
||||
|
||||
def test_phase_block_kills_if_inside_when_it_forms(make_game):
|
||||
g = make_game("""
|
||||
name: t
|
||||
tile_size: 32
|
||||
map: |
|
||||
########
|
||||
#......#
|
||||
#.....G#
|
||||
########
|
||||
traps:
|
||||
- type: phase_block
|
||||
at: [3, 2]
|
||||
fade: 0.2
|
||||
trigger: { within: 3 }
|
||||
""")
|
||||
pb = g.level.traps[0]
|
||||
place(g, 3, 2) # standing right where it will form
|
||||
killed, d0 = False, g.deaths
|
||||
for _ in range(10):
|
||||
step(g)
|
||||
if g.deaths > d0:
|
||||
killed = True
|
||||
break
|
||||
assert killed
|
||||
|
||||
|
||||
def test_phase_block_nudges_player_clipping_edge(make_game):
|
||||
# Only clipping the edge of a forming phase block -> shoved clear, not killed.
|
||||
g = make_game("""
|
||||
name: t
|
||||
tile_size: 32
|
||||
map: |
|
||||
#########
|
||||
#.......#
|
||||
#......G#
|
||||
#########
|
||||
traps:
|
||||
- type: phase_block
|
||||
at: [4, 1]
|
||||
fade: 0.2
|
||||
trigger: { within: 5 }
|
||||
""")
|
||||
pb = g.level.traps[0]
|
||||
b = pb.base_rect
|
||||
# Straddle the block's left edge: mostly outside, just clipping into it.
|
||||
g.player.fx = float(b.left - g.player.w + 5) # 5px of overlap
|
||||
g.player.fy = float(b.top + 2)
|
||||
g.player.vx = g.player.vy = 0.0
|
||||
g.player._sync_rect()
|
||||
d0 = g.deaths
|
||||
step(g)
|
||||
# survived, and pushed out to the left so it no longer overlaps the cell
|
||||
assert g.deaths == d0
|
||||
assert g.player.rect.right <= b.left
|
||||
assert pb.solid and not pb.emerge_kill
|
||||
|
||||
|
||||
def test_phase_block_kills_when_shove_would_squish(make_game):
|
||||
# Clipping the edge but backed by a wall on the escape side -> lethal.
|
||||
g = make_game("""
|
||||
name: t
|
||||
tile_size: 32
|
||||
map: |
|
||||
########
|
||||
#......#
|
||||
#.....G#
|
||||
########
|
||||
traps:
|
||||
- type: phase_block
|
||||
at: [1, 1]
|
||||
fade: 0.2
|
||||
trigger: { within: 5 }
|
||||
""")
|
||||
pb = g.level.traps[0]
|
||||
b = pb.base_rect
|
||||
# Clip the block's left edge, but the level wall (col 0) is right there, so
|
||||
# the leftward shove has nowhere to go — squished.
|
||||
g.player.fx = float(b.left - g.player.w + 5)
|
||||
g.player.fy = float(b.top + 2)
|
||||
g.player.vx = g.player.vy = 0.0
|
||||
g.player._sync_rect()
|
||||
killed, d0 = False, g.deaths
|
||||
for _ in range(6):
|
||||
step(g)
|
||||
if g.deaths > d0:
|
||||
killed = True
|
||||
break
|
||||
assert killed
|
||||
|
||||
|
||||
def test_phase_block_snaps_visible_on_death(make_game):
|
||||
# Dying while a phase block is forming should snap it fully visible for the
|
||||
# frozen death tableau.
|
||||
g = make_game("""
|
||||
name: t
|
||||
tile_size: 32
|
||||
map: |
|
||||
########
|
||||
#......#
|
||||
#.....G#
|
||||
########
|
||||
traps:
|
||||
- type: phase_block
|
||||
at: [3, 2]
|
||||
fade: 0.5
|
||||
trigger: { within: 3 }
|
||||
""")
|
||||
pb = g.level.traps[0]
|
||||
place(g, 5, 2) # near enough to trigger, not on the cell
|
||||
for _ in range(6): # let it partially fade in
|
||||
step(g)
|
||||
assert 0 < pb.alpha < 1
|
||||
g._start_death() # die from something
|
||||
assert pb.alpha == 1.0 and pb.solid # snapped fully visible for the freeze
|
||||
|
||||
|
||||
# --- generic invisible flag --------------------------------------------------
|
||||
def test_invisible_flag_on_any_trap(make_level):
|
||||
lvl = make_level("""
|
||||
name: t
|
||||
tile_size: 32
|
||||
map: |
|
||||
#####
|
||||
#...#
|
||||
#P.G#
|
||||
#####
|
||||
traps:
|
||||
- type: spike
|
||||
at: [2, 1]
|
||||
trigger: always
|
||||
direction: up
|
||||
invisible: true
|
||||
""")
|
||||
a = AssetStore("no_assets_dir")
|
||||
sp = lvl.traps[0]
|
||||
g = FakeGame(pygame.Rect(999, 999, 4, 4))
|
||||
|
||||
def painted(debug):
|
||||
lvl.debug = debug
|
||||
sp.tick(DT, g)
|
||||
w = pygame.Surface((lvl.width, lvl.height))
|
||||
w.fill((0, 0, 0))
|
||||
sp.render(w, a)
|
||||
return any(w.get_at((x, y))[:3] != (0, 0, 0)
|
||||
for y in range(32, 96) for x in range(32, 96))
|
||||
|
||||
assert painted(False) is False # invisible in play
|
||||
assert painted(True) is True # revealed in debug
|
||||
assert sp.hazard_rects() # still deadly either way
|
||||
Reference in New Issue
Block a user