"""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, ]