Format code with black
This commit is contained in:
@@ -22,7 +22,7 @@ DT = 1 / 60.0
|
||||
@pytest.fixture(scope="session", autouse=True)
|
||||
def _pygame():
|
||||
pygame.init()
|
||||
pygame.display.set_mode((64, 64)) # a display so convert_alpha() works
|
||||
pygame.display.set_mode((64, 64)) # a display so convert_alpha() works
|
||||
yield
|
||||
pygame.quit()
|
||||
|
||||
|
||||
@@ -20,12 +20,12 @@ def test_stationary_is_solid():
|
||||
def test_deadly_is_hazard_not_solid():
|
||||
b = block(at=[3, 3], deadly=True)
|
||||
assert b.solid_rects() == []
|
||||
assert b.hazard_rects() # non-empty
|
||||
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.solid_rects() == [] # you fall through
|
||||
assert b.sprite == "fake_block" # looks like a real block
|
||||
|
||||
|
||||
@@ -53,8 +53,8 @@ traps:
|
||||
if g.deaths > d0:
|
||||
killed = True
|
||||
break
|
||||
assert "gone" in states # it crumbled away
|
||||
assert killed # re-formed onto the standing player
|
||||
assert "gone" in states # it crumbled away
|
||||
assert killed # re-formed onto the standing player
|
||||
|
||||
|
||||
def test_moving_block_that_crumbles(make_game):
|
||||
@@ -78,7 +78,7 @@ traps:
|
||||
respawn: 1.0
|
||||
""")
|
||||
b = g.level.traps[0]
|
||||
place(g, 2, 1) # ride the platform
|
||||
place(g, 2, 1) # ride the platform
|
||||
moved = crumbled = False
|
||||
start = b.x
|
||||
for _ in range(200):
|
||||
@@ -103,18 +103,23 @@ def test_patrol_loop_visits_all_waypoints():
|
||||
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
|
||||
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
|
||||
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
|
||||
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}]})
|
||||
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)
|
||||
@@ -124,15 +129,15 @@ def test_sense_current_rides_down():
|
||||
# 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
|
||||
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
|
||||
((rect, dx, dy),) = b.carriers()
|
||||
assert dx != 0 and dy == 0 # moving horizontally
|
||||
|
||||
|
||||
# --- array expansion (count / spacing) --------------------------------------
|
||||
@@ -147,8 +152,12 @@ def test_expand_line():
|
||||
|
||||
|
||||
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]})]
|
||||
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]}):
|
||||
@@ -173,4 +182,9 @@ traps:
|
||||
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]
|
||||
assert sorted(b.current_rect().x for b in blocks) == [
|
||||
2 * 32,
|
||||
3 * 32,
|
||||
4 * 32,
|
||||
5 * 32,
|
||||
]
|
||||
|
||||
@@ -100,7 +100,7 @@ traps:
|
||||
mode: pingpong
|
||||
speed: 160
|
||||
""")
|
||||
place(g, 4, 3) # on the floor, at the block's body level
|
||||
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
|
||||
@@ -109,8 +109,8 @@ traps:
|
||||
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
|
||||
assert max_bottom <= floor_top + 1 # never pushed into the floor
|
||||
assert g.player.rect.x < start_x # got shoved along
|
||||
|
||||
|
||||
def test_ride_into_wall_stops_no_crush(make_game):
|
||||
@@ -137,7 +137,7 @@ traps:
|
||||
""")
|
||||
# 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.fy = float(3 * 32 - g.player.h) # feet on the block's top
|
||||
g.player._sync_rect()
|
||||
wall_left = 8 * 32
|
||||
hit_wall = False
|
||||
@@ -145,12 +145,12 @@ traps:
|
||||
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
|
||||
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
|
||||
assert g.state == "playing" # survived the whole time
|
||||
assert hit_wall # actually reached the wall
|
||||
|
||||
|
||||
def test_corner_clip_does_not_warp(make_game):
|
||||
@@ -177,7 +177,7 @@ traps:
|
||||
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
|
||||
assert g.player.rect.x - prev <= 28 # no sudden rightward warp
|
||||
prev = g.player.rect.x
|
||||
|
||||
|
||||
@@ -213,4 +213,4 @@ traps:
|
||||
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
|
||||
assert left_at == 0 # fell before any block reversal
|
||||
|
||||
@@ -5,7 +5,6 @@ from conftest import step, run, place, hold, DT
|
||||
from game import settings as S
|
||||
from game.player import InputState
|
||||
|
||||
|
||||
SIMPLE = """
|
||||
name: My Level
|
||||
tile_size: 32
|
||||
@@ -23,7 +22,7 @@ def test_level_parse(make_level):
|
||||
assert lvl.width == 5 * 32 and lvl.height == 3 * 32
|
||||
assert lvl.spawn == (1 * 32, 1 * 32)
|
||||
assert lvl.goal_rect.topleft == (3 * 32, 1 * 32)
|
||||
assert len(lvl.solids) == 5 + 5 + 2 # top row + bottom row + side walls
|
||||
assert len(lvl.solids) == 5 + 5 + 2 # top row + bottom row + side walls
|
||||
|
||||
|
||||
def test_unknown_trap_type_is_skipped(make_level):
|
||||
@@ -38,7 +37,7 @@ traps:
|
||||
- type: not_a_real_trap
|
||||
at: [1, 1]
|
||||
""")
|
||||
assert lvl.traps == [] # skipped, no crash
|
||||
assert lvl.traps == [] # skipped, no crash
|
||||
|
||||
|
||||
def test_death_pause_then_respawn(make_game):
|
||||
@@ -62,6 +61,7 @@ def test_death_counters_per_level_and_total(make_game, tmp_path):
|
||||
a.write_text(SIMPLE)
|
||||
b.write_text(SIMPLE.replace("My Level", "Two"))
|
||||
from game.game import Game
|
||||
|
||||
g = Game([str(a), str(b)], str(tmp_path / "noassets"))
|
||||
|
||||
def die():
|
||||
@@ -69,24 +69,26 @@ def test_death_counters_per_level_and_total(make_game, tmp_path):
|
||||
while g.state == "dying":
|
||||
step(g)
|
||||
|
||||
die(); die(); die()
|
||||
die()
|
||||
die()
|
||||
die()
|
||||
assert g.level_deaths == 3 and g.deaths == 3
|
||||
g._advance()
|
||||
assert g.level_deaths == 0 and g.deaths == 3 # per-level resets, total persists
|
||||
assert g.level_deaths == 0 and g.deaths == 3 # per-level resets, total persists
|
||||
die()
|
||||
assert g.level_deaths == 1 and g.deaths == 4
|
||||
g._replay()
|
||||
assert g.index == 0 and g.deaths == 0 # replay resets the total
|
||||
assert g.index == 0 and g.deaths == 0 # replay resets the total
|
||||
|
||||
|
||||
def test_reach_goal_charges_then_wins(make_game):
|
||||
g = make_game(SIMPLE)
|
||||
g._reach_goal()
|
||||
assert g.state == "charging"
|
||||
assert 0 <= g.charge_from < 0.2 # near-empty start
|
||||
assert 0 <= g.charge_from < 0.2 # near-empty start
|
||||
for _ in range(70):
|
||||
step(g)
|
||||
assert g.state == "won_all" # single level -> won_all
|
||||
assert g.state == "won_all" # single level -> won_all
|
||||
|
||||
|
||||
def test_fade_swaps_level_at_black(make_game, tmp_path):
|
||||
@@ -95,6 +97,7 @@ def test_fade_swaps_level_at_black(make_game, tmp_path):
|
||||
a.write_text(SIMPLE)
|
||||
b.write_text(SIMPLE.replace("My Level", "Two"))
|
||||
from game.game import Game
|
||||
|
||||
g = Game([str(a), str(b)], str(tmp_path / "noassets"))
|
||||
g.state = "won_level"
|
||||
g._start_fade(g._advance)
|
||||
@@ -110,32 +113,45 @@ def test_fade_swaps_level_at_black(make_game, tmp_path):
|
||||
done = i
|
||||
break
|
||||
assert swapped and done and g.index == 1
|
||||
assert done - swapped >= 15 # fade-in ~0.3s not skipped
|
||||
assert done - swapped >= 15 # fade-in ~0.3s not skipped
|
||||
|
||||
|
||||
def test_window_sized_once_for_tallest_level(make_game, tmp_path, monkeypatch):
|
||||
a = tmp_path / "a.yaml"
|
||||
b = tmp_path / "b.yaml"
|
||||
a.write_text("name: A\ntile_size: 32\nmap: |\n #####\n #P.G#\n #####\n") # 3 rows
|
||||
b.write_text("name: B\ntile_size: 32\nmap: |\n #####\n #...#\n #P.G#\n #####\n") # 4 rows
|
||||
a.write_text(
|
||||
"name: A\ntile_size: 32\nmap: |\n #####\n #P.G#\n #####\n"
|
||||
) # 3 rows
|
||||
b.write_text(
|
||||
"name: B\ntile_size: 32\nmap: |\n #####\n #...#\n #P.G#\n #####\n"
|
||||
) # 4 rows
|
||||
from game.game import Game
|
||||
|
||||
calls = []
|
||||
orig = pygame.display.set_mode
|
||||
monkeypatch.setattr(pygame.display, "set_mode",
|
||||
lambda size, *a, **k: calls.append(size) or orig(size, *a, **k))
|
||||
monkeypatch.setattr(
|
||||
pygame.display,
|
||||
"set_mode",
|
||||
lambda size, *a, **k: calls.append(size) or orig(size, *a, **k),
|
||||
)
|
||||
g = Game([str(a), str(b)], str(tmp_path / "noassets"))
|
||||
g._advance(); g._replay()
|
||||
assert len(calls) == 1 # never recreated the window
|
||||
g._advance()
|
||||
g._replay()
|
||||
assert len(calls) == 1 # never recreated the window
|
||||
# sized to the tallest level (4 rows + HUD)
|
||||
from game.game import HUD_H
|
||||
|
||||
assert g.win_h == 4 * 32 + HUD_H
|
||||
|
||||
|
||||
def test_debug_view_adds_overscan_margin(tmp_path):
|
||||
from game.game import Game, HUD_H
|
||||
from game import settings as S
|
||||
|
||||
p = tmp_path / "d.yaml"
|
||||
p.write_text("name: t\ntile_size: 32\ndebug: true\nmap: |\n #####\n #P.G#\n #####\n")
|
||||
p.write_text(
|
||||
"name: t\ntile_size: 32\ndebug: true\nmap: |\n #####\n #P.G#\n #####\n"
|
||||
)
|
||||
g = Game([str(p)], str(tmp_path / "noassets"))
|
||||
m = S.DEBUG_VIEW_MARGIN * 32
|
||||
assert m > 0
|
||||
@@ -148,6 +164,7 @@ def test_debug_view_adds_overscan_margin(tmp_path):
|
||||
|
||||
def test_no_overscan_without_debug(tmp_path):
|
||||
from game.game import Game
|
||||
|
||||
p = tmp_path / "n.yaml"
|
||||
p.write_text("name: t\ntile_size: 32\nmap: |\n #####\n #P.G#\n #####\n")
|
||||
g = Game([str(p)], str(tmp_path / "noassets"))
|
||||
@@ -169,39 +186,46 @@ def test_cli_debug_forces_all_levels(make_game):
|
||||
|
||||
def test_f5_hot_reload_picks_up_edits_and_counts_death(tmp_path):
|
||||
from game.game import Game
|
||||
|
||||
p = tmp_path / "hot.yaml"
|
||||
p.write_text("name: A\ntile_size: 32\nmap: |\n #####\n #P.G#\n #####\n")
|
||||
g = Game([str(p)], str(tmp_path / "noassets"))
|
||||
assert g.level.name == "A" and len(g.level.traps) == 0
|
||||
d0 = g.deaths
|
||||
# edit the file on disk, then F5
|
||||
p.write_text("name: B\ntile_size: 32\nmap: |\n #####\n #P.G#\n #####\n"
|
||||
"traps:\n - type: block\n at: [2, 1]\n deadly: true\n")
|
||||
p.write_text(
|
||||
"name: B\ntile_size: 32\nmap: |\n #####\n #P.G#\n #####\n"
|
||||
"traps:\n - type: block\n at: [2, 1]\n deadly: true\n"
|
||||
)
|
||||
g._reload_level()
|
||||
assert g.state == "dying" and g.deaths == d0 + 1 # death beat + counter
|
||||
assert g.state == "dying" and g.deaths == d0 + 1 # death beat + counter
|
||||
while g.state == "dying":
|
||||
step(g)
|
||||
assert g.level.name == "B" and len(g.level.traps) == 1 # picked up the edit
|
||||
assert g.level_deaths == 1 # counted, not reset to 0
|
||||
assert g.level.name == "B" and len(g.level.traps) == 1 # picked up the edit
|
||||
assert g.level_deaths == 1 # counted, not reset to 0
|
||||
|
||||
|
||||
def test_f5_bad_yaml_does_not_crash(tmp_path, capsys):
|
||||
from game.game import Game
|
||||
|
||||
p = tmp_path / "bad.yaml"
|
||||
p.write_text("name: A\ntile_size: 32\nmap: |\n #####\n #P.G#\n #####\n")
|
||||
g = Game([str(p)], str(tmp_path / "noassets"))
|
||||
p.write_text("name: A\n bad: [unclosed\n") # invalid YAML
|
||||
p.write_text("name: A\n bad: [unclosed\n") # invalid YAML
|
||||
g._reload_level()
|
||||
while g.state == "dying":
|
||||
step(g)
|
||||
assert g.state == "playing" # fell back, no crash
|
||||
assert g.level.name == "A" # kept the old level
|
||||
assert g.state == "playing" # fell back, no crash
|
||||
assert g.level.name == "A" # kept the old level
|
||||
|
||||
|
||||
def test_debug_grid_adds_pixels(make_level):
|
||||
import pygame
|
||||
from game.assets import AssetStore
|
||||
lvl = make_level("name: t\ntile_size: 32\nmap: |\n #####\n #...#\n #...#\n #####\n")
|
||||
|
||||
lvl = make_level(
|
||||
"name: t\ntile_size: 32\nmap: |\n #####\n #...#\n #...#\n #####\n"
|
||||
)
|
||||
a = AssetStore("noassets")
|
||||
|
||||
def painted(dbg):
|
||||
@@ -209,6 +233,11 @@ def test_debug_grid_adds_pixels(make_level):
|
||||
w = pygame.Surface((lvl.width, lvl.height))
|
||||
w.fill((0, 0, 0))
|
||||
lvl.draw(w, a)
|
||||
return sum(1 for y in range(lvl.height) for x in range(lvl.width)
|
||||
if w.get_at((x, y))[:3] != (0, 0, 0))
|
||||
assert painted(True) > painted(False) # grid + labels add ink
|
||||
return sum(
|
||||
1
|
||||
for y in range(lvl.height)
|
||||
for x in range(lvl.width)
|
||||
if w.get_at((x, y))[:3] != (0, 0, 0)
|
||||
)
|
||||
|
||||
assert painted(True) > painted(False) # grid + labels add ink
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
from conftest import step, DT
|
||||
from game.traps import Block, Spike
|
||||
|
||||
|
||||
PLATFORM_WITH_SPIKE = """
|
||||
name: t
|
||||
tile_size: 32
|
||||
@@ -74,8 +73,10 @@ traps:
|
||||
for m in plat.mounts:
|
||||
assert m._mounted and m.deadly
|
||||
# each deadly mount tracks the platform at its offset and is lethal
|
||||
exp = (plat.current_rect().x + m._mount_off[0],
|
||||
plat.current_rect().y + m._mount_off[1])
|
||||
exp = (
|
||||
plat.current_rect().x + m._mount_off[0],
|
||||
plat.current_rect().y + m._mount_off[1],
|
||||
)
|
||||
assert (m.current_rect().x, m.current_rect().y) == exp
|
||||
assert m.hazard_rects()
|
||||
|
||||
|
||||
@@ -18,10 +18,10 @@ map: |
|
||||
|
||||
def test_falls_and_lands_on_floor(make_game):
|
||||
g = make_game(FLAT)
|
||||
place(g, 4, 1) # up in the air
|
||||
place(g, 4, 1) # up in the air
|
||||
run(g, 60)
|
||||
assert g.player.on_ground
|
||||
assert g.player.rect.bottom == 5 * 32 # floor is row 5's top (y=160)
|
||||
assert g.player.rect.bottom == 5 * 32 # floor is row 5's top (y=160)
|
||||
|
||||
|
||||
def test_terminal_velocity(make_game):
|
||||
@@ -34,13 +34,13 @@ def test_terminal_velocity(make_game):
|
||||
|
||||
def test_jump_gains_height_then_returns(make_game):
|
||||
g = make_game(FLAT)
|
||||
run(g, 30) # settle on floor
|
||||
run(g, 30) # settle on floor
|
||||
ground = g.player.rect.bottom
|
||||
peak = ground
|
||||
for i in range(60):
|
||||
step(g, hold(jump_pressed=(i == 0), jump_held=True))
|
||||
peak = min(peak, g.player.rect.bottom)
|
||||
assert peak < ground - 32 # rose at least a tile
|
||||
assert peak < ground - 32 # rose at least a tile
|
||||
|
||||
|
||||
def test_variable_jump_height(make_game):
|
||||
@@ -55,6 +55,7 @@ def test_variable_jump_height(make_game):
|
||||
step(g, hold(jump_pressed=(i == 0), jump_held=held))
|
||||
hi = min(hi, g.player.rect.bottom)
|
||||
return ground - hi
|
||||
|
||||
assert peak(60) > peak(1) + 8
|
||||
|
||||
|
||||
@@ -71,22 +72,22 @@ map: |
|
||||
#P#...#
|
||||
#######
|
||||
""")
|
||||
place(g, 1, 3) # standing on the little step at [1,3] top
|
||||
place(g, 1, 3) # standing on the little step at [1,3] top
|
||||
# walk right off the step for a couple frames, then jump
|
||||
run(g, 4, lambda i: hold(right=True))
|
||||
y_before = g.player.rect.bottom
|
||||
step(g, hold(right=True, jump_pressed=True, jump_held=True))
|
||||
step(g, hold(right=True, jump_held=True))
|
||||
assert g.player.vy < 0 # a jump actually started
|
||||
assert g.player.vy < 0 # a jump actually started
|
||||
|
||||
|
||||
def test_walls_stop_horizontal_movement(make_game):
|
||||
g = make_game(FLAT)
|
||||
place(g, 1, 4)
|
||||
run(g, 120, lambda i: hold(right=True))
|
||||
assert g.player.rect.right <= 9 * 32 # right wall inner edge (x=288)
|
||||
assert g.player.rect.right <= 9 * 32 # right wall inner edge (x=288)
|
||||
run(g, 120, lambda i: hold(left=True))
|
||||
assert g.player.rect.left >= 1 * 32 # left wall inner edge (x=32)
|
||||
assert g.player.rect.left >= 1 * 32 # left wall inner edge (x=32)
|
||||
|
||||
|
||||
def test_oneway_platform_land_from_above_pass_from_below(make_game):
|
||||
@@ -103,7 +104,7 @@ map: |
|
||||
#######
|
||||
""")
|
||||
# Fall onto the one-way from above -> lands on it.
|
||||
place(g, 1, 1) # open air above the one-way (row 3)
|
||||
place(g, 1, 1) # open air above the one-way (row 3)
|
||||
run(g, 60)
|
||||
assert g.player.on_ground and g.player.rect.bottom == 3 * 32
|
||||
|
||||
@@ -119,12 +120,12 @@ map: |
|
||||
#P...G#
|
||||
#######
|
||||
""")
|
||||
place(g2, 1, 4) # on the floor, below the one-way (row 2)
|
||||
run(g2, 5) # settle so on_ground is set before jumping
|
||||
place(g2, 1, 4) # on the floor, below the one-way (row 2)
|
||||
run(g2, 5) # settle so on_ground is set before jumping
|
||||
passed = False
|
||||
for i in range(40):
|
||||
step(g2, hold(jump_pressed=(i == 0), jump_held=(i < 12)))
|
||||
if g2.player.rect.top < 2 * 32: # rose above the one-way row
|
||||
if g2.player.rect.top < 2 * 32: # rose above the one-way row
|
||||
passed = True
|
||||
assert passed
|
||||
|
||||
@@ -142,10 +143,10 @@ map: |
|
||||
#....G#
|
||||
#######
|
||||
""")
|
||||
place(g, 1, 2) # standing on the one-way at row 3
|
||||
place(g, 1, 2) # standing on the one-way at row 3
|
||||
run(g, 20)
|
||||
assert g.player.on_ground
|
||||
# press down + jump to drop through
|
||||
for i in range(30):
|
||||
step(g, hold(down=True, jump_pressed=(i == 0)))
|
||||
assert g.player.rect.top > 3 * 32 # fell below the one-way
|
||||
assert g.player.rect.top > 3 * 32 # fell below the one-way
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
import hashlib
|
||||
import pygame
|
||||
from conftest import (FakeGame, step, run, place, hold, DT)
|
||||
from conftest import FakeGame, step, run, place, hold, DT
|
||||
from game.assets import AssetStore
|
||||
from game.traps import Spike, ArrowShooter, Warp, PhaseBlock, Block
|
||||
|
||||
@@ -51,7 +51,7 @@ traps:
|
||||
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
|
||||
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"))
|
||||
@@ -62,7 +62,8 @@ def test_spike_sprite_rotates_per_direction(tmp_path):
|
||||
|
||||
def h(s):
|
||||
return hashlib.md5(pygame.image.tostring(s, "RGBA")).hexdigest()
|
||||
assert h(up) != h(down) # rotation actually happened
|
||||
|
||||
assert h(up) != h(down) # rotation actually happened
|
||||
assert left.get_size() == (16, 32) and up.get_size() == (32, 16)
|
||||
|
||||
|
||||
@@ -85,10 +86,10 @@ traps:
|
||||
""")
|
||||
sh = lvl.traps[0]
|
||||
g = FakeGame(pygame.Rect(0, 0, 4, 4))
|
||||
for _ in range(40): # ~0.66s -> at least one shot
|
||||
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
|
||||
assert sh.arrows # spawned arrows
|
||||
assert sh.hazard_rects() # arrows are hazards
|
||||
|
||||
|
||||
def test_arrow_shooter_trigger_gates_firing(make_level):
|
||||
@@ -108,10 +109,10 @@ traps:
|
||||
trigger: { within: 1 }
|
||||
""")
|
||||
sh = lvl.traps[0]
|
||||
far = FakeGame(pygame.Rect(0, 0, 4, 4)) # nowhere near
|
||||
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
|
||||
assert not sh.arrows # never fired while player out of range
|
||||
|
||||
|
||||
# --- invisible wall, now an array of invisible blocks ------------------------
|
||||
@@ -132,10 +133,10 @@ traps:
|
||||
count: [1, 2]
|
||||
""")
|
||||
blocks = [t for t in lvl.traps if isinstance(t, Block)]
|
||||
assert len(blocks) == 2 # one per cell
|
||||
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
|
||||
assert ys == [1 * 32, 2 * 32] # stacked vertically
|
||||
|
||||
|
||||
# --- warp --------------------------------------------------------------------
|
||||
@@ -178,9 +179,9 @@ traps:
|
||||
to: [7, 1]
|
||||
""")
|
||||
warp = g.level.traps[0]
|
||||
assert warp._pulse == 0.0 # dormant: no aura
|
||||
place(g, 4, 2) # stand on the warp tile
|
||||
step(g) # -> teleports, aura flashes on
|
||||
assert warp._pulse == 0.0 # dormant: no aura
|
||||
place(g, 4, 2) # stand on the warp tile
|
||||
step(g) # -> teleports, aura flashes on
|
||||
assert warp._pulse > 0.9
|
||||
|
||||
# The aura paints at BOTH the source and the destination — even though the
|
||||
@@ -218,9 +219,9 @@ traps:
|
||||
pb = g.level.traps[0]
|
||||
place(g, 1, 3)
|
||||
step(g)
|
||||
assert not pb.solid and pb.alpha == 0 # dormant far away
|
||||
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
|
||||
assert pb.solid and pb.alpha > 0 # phased in solid on approach
|
||||
|
||||
|
||||
def test_phase_block_kills_if_inside_when_it_forms(make_game):
|
||||
@@ -239,7 +240,7 @@ traps:
|
||||
trigger: { within: 3 }
|
||||
""")
|
||||
pb = g.level.traps[0]
|
||||
place(g, 3, 2) # standing right where it will form
|
||||
place(g, 3, 2) # standing right where it will form
|
||||
killed, d0 = False, g.deaths
|
||||
for _ in range(10):
|
||||
step(g)
|
||||
@@ -268,7 +269,7 @@ traps:
|
||||
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.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()
|
||||
@@ -331,21 +332,23 @@ traps:
|
||||
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
|
||||
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
|
||||
g._start_death() # die from something
|
||||
assert pb.alpha == 1.0 and pb.solid # snapped fully visible for the freeze
|
||||
|
||||
|
||||
def test_debug_overscan_reveals_offmap_trap(make_level):
|
||||
# In debug the play surface is enlarged and shifted (render_offset) so a trap
|
||||
# placed just off the map is drawn into the overscan instead of being clipped.
|
||||
from game import settings as S
|
||||
|
||||
lvl = make_level(
|
||||
"name: t\ntile_size: 32\ndebug: true\nmap: |\n #####\n #...#\n #####\n"
|
||||
"traps:\n - type: block\n at: [5, 1]\n deadly: true\n") # col 5 = off-map
|
||||
"traps:\n - type: block\n at: [5, 1]\n deadly: true\n"
|
||||
) # col 5 = off-map
|
||||
a = AssetStore("noassets")
|
||||
m = S.DEBUG_VIEW_MARGIN * lvl.tile
|
||||
lvl.render_offset = (m, m)
|
||||
@@ -354,7 +357,7 @@ def test_debug_overscan_reveals_offmap_trap(make_level):
|
||||
lvl.draw(surf, a)
|
||||
cx = 5 * lvl.tile + m + lvl.tile // 2
|
||||
cy = 1 * lvl.tile + m + lvl.tile // 2
|
||||
assert surf.get_at((cx, cy))[:3] != (0, 0, 0) # off-map block painted in overscan
|
||||
assert surf.get_at((cx, cy))[:3] != (0, 0, 0) # off-map block painted in overscan
|
||||
|
||||
|
||||
# --- generic invisible flag --------------------------------------------------
|
||||
@@ -384,9 +387,12 @@ traps:
|
||||
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))
|
||||
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
|
||||
assert painted(False) is False # invisible in play
|
||||
assert painted(True) is True # revealed in debug
|
||||
assert sp.hazard_rects() # still deadly either way
|
||||
|
||||
@@ -6,9 +6,12 @@ from game.traps import make_condition
|
||||
|
||||
|
||||
def ev(spec, px, py, w=20, h=28, dt=0.0):
|
||||
trap = type("T", (), {"tile": 32,
|
||||
"sensor_rect": lambda self: pygame.Rect(100, 100, 32, 32)})()
|
||||
trap = type(
|
||||
"T", (), {"tile": 32, "sensor_rect": lambda self: pygame.Rect(100, 100, 32, 32)}
|
||||
)()
|
||||
return make_condition(spec).evaluate(trap, FakeGame(pygame.Rect(px, py, w, h)), dt)
|
||||
|
||||
|
||||
# trap centre = (116, 116); left100 right132 top100 bottom132
|
||||
|
||||
|
||||
@@ -17,26 +20,26 @@ def test_always():
|
||||
|
||||
|
||||
def test_within_radius():
|
||||
assert ev({"within": 2}, 106, 104) is True # ~10px away
|
||||
assert ev({"within": 2}, 400, 116) is False # far
|
||||
assert ev({"within": 2}, 106, 104) is True # ~10px away
|
||||
assert ev({"within": 2}, 400, 116) is False # far
|
||||
|
||||
|
||||
def test_dir_left_right():
|
||||
assert ev({"dir": "left"}, 40, 105) is True # to the left
|
||||
assert ev({"dir": "left"}, 200, 105) is False # to the right
|
||||
assert ev({"dir": "left"}, 40, 105) is True # to the left
|
||||
assert ev({"dir": "left"}, 200, 105) is False # to the right
|
||||
assert ev({"dir": "right"}, 200, 105) is True
|
||||
|
||||
|
||||
def test_dir_range():
|
||||
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
|
||||
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
|
||||
|
||||
|
||||
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
|
||||
assert ev({"dir": "above", "aligned": True}, 400, 60) is False # not aligned
|
||||
assert ev({"dir": "above", "aligned": True}, 400, 60) is False # not aligned
|
||||
# without aligned, any column counts
|
||||
assert ev({"dir": "above"}, 400, 60) is True
|
||||
|
||||
@@ -44,8 +47,8 @@ def test_dir_aligned():
|
||||
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.
|
||||
assert ev({"dir": "left"}, 106, 105) is False # centre 116, inside -> not left
|
||||
assert ev({"dir": "right"}, 106, 105) is False # inside -> not right
|
||||
assert ev({"dir": "left"}, 106, 105) is False # centre 116, inside -> not left
|
||||
assert ev({"dir": "right"}, 106, 105) is False # inside -> not right
|
||||
# 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
|
||||
@@ -59,24 +62,26 @@ def test_dir_inclusive():
|
||||
def test_all_and_any():
|
||||
c = {"all": [{"within": 3}, {"dir": "above", "aligned": True}]}
|
||||
assert ev(c, 108, 80) is True
|
||||
assert ev(c, 108, 110) is False # close but not above
|
||||
assert ev(c, 108, 110) is False # close but not above
|
||||
c2 = {"any": [{"dir": "left"}, {"dir": "right"}]}
|
||||
assert ev(c2, 40, 105) is True
|
||||
assert ev(c2, 110, 40) is False # above-only satisfies neither
|
||||
assert ev(c2, 110, 40) is False # above-only satisfies neither
|
||||
|
||||
|
||||
def test_timer_cycles():
|
||||
c = make_condition({"timer": {"interval": 0.3, "up_time": 0.2}})
|
||||
trap = type("T", (), {"tile": 32,
|
||||
"sensor_rect": lambda self: pygame.Rect(0, 0, 32, 32)})()
|
||||
trap = type(
|
||||
"T", (), {"tile": 32, "sensor_rect": lambda self: pygame.Rect(0, 0, 32, 32)}
|
||||
)()
|
||||
g = FakeGame(pygame.Rect(0, 0, 4, 4))
|
||||
states = [c.evaluate(trap, g, 1 / 60) for _ in range(30)]
|
||||
assert states[0] is False # starts in the "off" interval
|
||||
assert any(states) and not all(states) # cycles on and off
|
||||
assert states[0] is False # starts in the "off" interval
|
||||
assert any(states) and not all(states) # cycles on and off
|
||||
|
||||
|
||||
def test_bad_condition_raises():
|
||||
import pytest
|
||||
|
||||
with pytest.raises(ValueError):
|
||||
make_condition({"nope": 1})
|
||||
|
||||
@@ -97,10 +102,10 @@ traps:
|
||||
delay: 0.25
|
||||
""")
|
||||
sp = lvl.traps[0]
|
||||
g = FakeGame(sp.base_rect.copy()) # player right on it -> in range
|
||||
g = FakeGame(sp.base_rect.copy()) # player right on it -> in range
|
||||
armed = None
|
||||
for i in range(30):
|
||||
if sp.triggered(g, 1 / 60):
|
||||
armed = i
|
||||
break
|
||||
assert armed is not None and 13 <= armed <= 17 # ~0.25s = 15 frames
|
||||
assert armed is not None and 13 <= armed <= 17 # ~0.25s = 15 frames
|
||||
|
||||
Reference in New Issue
Block a user