2026-07-21 19:36:02 -04:00
|
|
|
"""Crush death, shove/carry interactions, and the collision-resolution
|
|
|
|
|
regressions (corner warp, fits-under, cliff-shove)."""
|
|
|
|
|
|
|
|
|
|
from conftest import step, run, place, hold
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def until_crushed(g, frames=200):
|
|
|
|
|
for _ in range(frames):
|
|
|
|
|
if g.state != "playing":
|
|
|
|
|
break
|
|
|
|
|
step(g)
|
|
|
|
|
if g.player.crushed:
|
|
|
|
|
return True
|
|
|
|
|
return False
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_descend_crush(make_game):
|
|
|
|
|
g = make_game("""
|
|
|
|
|
name: t
|
|
|
|
|
tile_size: 32
|
|
|
|
|
map: |
|
|
|
|
|
#####
|
|
|
|
|
#...#
|
|
|
|
|
#P.G#
|
|
|
|
|
#####
|
|
|
|
|
traps:
|
|
|
|
|
- type: block
|
|
|
|
|
at: [1, 1]
|
|
|
|
|
move: [0, 2]
|
|
|
|
|
speed: 200
|
|
|
|
|
trigger: { within: 20 }
|
|
|
|
|
""")
|
|
|
|
|
# player stands on the floor under the descending block
|
|
|
|
|
assert until_crushed(g, 120)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_fits_under_no_false_crush(make_game):
|
|
|
|
|
# A block that stops with clearance must NOT crush a grounded player.
|
|
|
|
|
g = make_game("""
|
|
|
|
|
name: t
|
|
|
|
|
tile_size: 32
|
|
|
|
|
map: |
|
|
|
|
|
#####
|
|
|
|
|
#...#
|
|
|
|
|
#...#
|
|
|
|
|
#P.G#
|
|
|
|
|
#####
|
|
|
|
|
traps:
|
|
|
|
|
- type: block
|
|
|
|
|
at: [1, 1]
|
|
|
|
|
move: [0, 1]
|
|
|
|
|
speed: 160
|
|
|
|
|
trigger: { within: 20 }
|
|
|
|
|
""")
|
|
|
|
|
place(g, 1, 2)
|
|
|
|
|
for _ in range(120):
|
|
|
|
|
step(g)
|
|
|
|
|
assert not g.player.crushed
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_shove_into_wall_crushes(make_game):
|
|
|
|
|
g = make_game("""
|
|
|
|
|
name: t
|
|
|
|
|
tile_size: 32
|
|
|
|
|
map: |
|
|
|
|
|
#######
|
|
|
|
|
#....G#
|
|
|
|
|
#.....#
|
|
|
|
|
#######
|
|
|
|
|
traps:
|
|
|
|
|
- type: block
|
|
|
|
|
at: [2, 2]
|
|
|
|
|
path: [[2, 2], [6, 2]]
|
|
|
|
|
mode: pingpong
|
|
|
|
|
speed: 200
|
|
|
|
|
""")
|
|
|
|
|
# player pinned against the right wall
|
|
|
|
|
place(g, 5, 2)
|
|
|
|
|
g.player.fx = float(6 * 32 - g.player.w)
|
|
|
|
|
g.player._sync_rect()
|
|
|
|
|
assert until_crushed(g)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_shove_along_not_into_floor(make_game):
|
|
|
|
|
# A block moving horizontally into a grounded player shoves them sideways,
|
|
|
|
|
# never buries them in the floor.
|
|
|
|
|
g = make_game("""
|
|
|
|
|
name: t
|
|
|
|
|
tile_size: 32
|
|
|
|
|
map: |
|
|
|
|
|
############
|
|
|
|
|
#..........#
|
|
|
|
|
#..........#
|
|
|
|
|
#.........G#
|
|
|
|
|
############
|
|
|
|
|
traps:
|
|
|
|
|
- type: block
|
|
|
|
|
at: [8, 3]
|
|
|
|
|
path: [[8, 3], [2, 3]]
|
|
|
|
|
mode: pingpong
|
|
|
|
|
speed: 160
|
|
|
|
|
""")
|
|
|
|
|
place(g, 4, 3) # on the floor, at the block's body level
|
|
|
|
|
floor_top = 4 * 32
|
|
|
|
|
start_x = g.player.rect.x
|
|
|
|
|
max_bottom = g.player.rect.bottom
|
|
|
|
|
for _ in range(120):
|
|
|
|
|
step(g)
|
|
|
|
|
max_bottom = max(max_bottom, g.player.rect.bottom)
|
|
|
|
|
if g.state != "playing":
|
|
|
|
|
break
|
|
|
|
|
assert max_bottom <= floor_top + 1 # never pushed into the floor
|
|
|
|
|
assert g.player.rect.x < start_x # got shoved along
|
|
|
|
|
|
|
|
|
|
|
2026-07-25 13:13:48 -04:00
|
|
|
def test_ride_into_wall_stops_no_crush(make_game):
|
|
|
|
|
# Riding a horizontally-moving platform into a stationary wall must NOT
|
|
|
|
|
# crush the player: they stop at the wall while the block slides underneath.
|
|
|
|
|
# The wall sits one row *above* the platform's track (col 8, row 2), so it
|
|
|
|
|
# catches the player's body while the platform passes below it.
|
|
|
|
|
g = make_game("""
|
|
|
|
|
name: t
|
|
|
|
|
tile_size: 32
|
|
|
|
|
map: |
|
|
|
|
|
###########
|
|
|
|
|
#.........#
|
|
|
|
|
#.......#.#
|
|
|
|
|
#.........#
|
|
|
|
|
#........G#
|
|
|
|
|
###########
|
|
|
|
|
traps:
|
|
|
|
|
- type: block
|
|
|
|
|
at: [1, 3]
|
|
|
|
|
move: [8, 0]
|
|
|
|
|
speed: 200
|
|
|
|
|
trigger: always
|
|
|
|
|
""")
|
|
|
|
|
# stand the player on top of the platform at its left end (body in row 2)
|
|
|
|
|
place(g, 1, 2)
|
|
|
|
|
g.player.fy = float(3 * 32 - g.player.h) # feet on the block's top
|
|
|
|
|
g.player._sync_rect()
|
|
|
|
|
wall_left = 8 * 32
|
|
|
|
|
hit_wall = False
|
|
|
|
|
for _ in range(180):
|
|
|
|
|
step(g)
|
|
|
|
|
if g.state != "playing":
|
|
|
|
|
break
|
|
|
|
|
assert not g.player.crushed # never crushed
|
|
|
|
|
assert g.player.rect.right <= wall_left + 1 # stopped at the wall
|
|
|
|
|
if g.player.rect.right >= wall_left - 1:
|
|
|
|
|
hit_wall = True
|
|
|
|
|
assert g.state == "playing" # survived the whole time
|
|
|
|
|
assert hit_wall # actually reached the wall
|
|
|
|
|
|
|
|
|
|
|
2026-07-21 19:36:02 -04:00
|
|
|
def test_corner_clip_does_not_warp(make_game):
|
|
|
|
|
# Walking into a moving block near its corner must not fling the player
|
|
|
|
|
# across it (the old velocity-sign resolver bug).
|
|
|
|
|
g = make_game("""
|
|
|
|
|
name: t
|
|
|
|
|
tile_size: 32
|
|
|
|
|
map: |
|
|
|
|
|
#########
|
|
|
|
|
#.......#
|
|
|
|
|
#.......#
|
|
|
|
|
#.......#
|
|
|
|
|
#P.....G#
|
|
|
|
|
#########
|
|
|
|
|
traps:
|
|
|
|
|
- type: block
|
|
|
|
|
at: [5, 2]
|
|
|
|
|
path: [[5, 2], [2, 2]]
|
|
|
|
|
mode: pingpong
|
|
|
|
|
speed: 150
|
|
|
|
|
""")
|
|
|
|
|
place(g, 4, 3)
|
|
|
|
|
prev = g.player.rect.x
|
|
|
|
|
for _ in range(120):
|
|
|
|
|
step(g, hold(left=True))
|
|
|
|
|
assert g.player.rect.x - prev <= 28 # no sudden rightward warp
|
|
|
|
|
prev = g.player.rect.x
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_cliff_shove_falls_on_first_pass(make_game):
|
|
|
|
|
# A block sweeping the player toward a ledge should push them off on the
|
|
|
|
|
# first pass (before it turns around).
|
|
|
|
|
g = make_game("""
|
|
|
|
|
name: t
|
|
|
|
|
tile_size: 32
|
|
|
|
|
map: |
|
|
|
|
|
###########
|
|
|
|
|
#.........#
|
|
|
|
|
#.........#
|
|
|
|
|
######....#
|
|
|
|
|
traps:
|
|
|
|
|
- type: block
|
|
|
|
|
at: [1, 2]
|
|
|
|
|
path: [[1, 2], [8, 2]]
|
|
|
|
|
mode: pingpong
|
|
|
|
|
speed: 150
|
|
|
|
|
""")
|
|
|
|
|
b = g.level.traps[0]
|
|
|
|
|
place(g, 4, 2)
|
|
|
|
|
reversalsN, pd, left_at = 0, 1, None
|
|
|
|
|
for i in range(240):
|
|
|
|
|
step(g)
|
|
|
|
|
if g.state != "playing":
|
|
|
|
|
break
|
|
|
|
|
d = 1 if b.x > b.prev[0] else (-1 if b.x < b.prev[0] else pd)
|
|
|
|
|
if d != pd:
|
|
|
|
|
reversalsN += 1
|
|
|
|
|
pd = d
|
|
|
|
|
if not g.player.on_ground and g.player.rect.bottom > 3 * 32:
|
|
|
|
|
left_at = reversalsN
|
|
|
|
|
break
|
|
|
|
|
assert left_at == 0 # fell before any block reversal
|