"""Level loading, death/respawn, counters, level-clear flow, window sizing.""" import pygame 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 battery_seconds: 30 map: | ##### #P.G# ##### """ def test_level_parse(make_level): lvl = make_level(SIMPLE) assert lvl.name == "My 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 def test_unknown_trap_type_is_skipped(make_level): lvl = make_level(""" name: t tile_size: 32 map: | ### #P# ### traps: - type: not_a_real_trap at: [1, 1] """) assert lvl.traps == [] # skipped, no crash def test_death_pause_then_respawn(make_game): g = make_game(SIMPLE) run(g, 5) d0 = g.deaths g._start_death() assert g.state == "dying" and g.deaths == d0 + 1 frames = 0 while g.state == "dying" and frames < 100: step(g) frames += 1 assert g.state == "playing" assert abs(frames * DT - S.DEATH_PAUSE) < DT * 2 def test_death_counters_per_level_and_total(make_game, tmp_path): # two-level game to test per-level reset vs session total a = tmp_path / "a.yaml" b = tmp_path / "b.yaml" 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(): g._start_death() while g.state == "dying": step(g) 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 die() assert g.level_deaths == 1 and g.deaths == 4 g._replay() 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 for _ in range(70): step(g) assert g.state == "won_all" # single level -> won_all def test_fade_swaps_level_at_black(make_game, tmp_path): a = tmp_path / "a.yaml" b = tmp_path / "b.yaml" 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) out = swapped = done = 0 for i in range(80): prev = g.index step(g) if g.fade_phase == "out" and g.state == "fading": out += 1 if g.index != prev: swapped = i if g.state == "playing": done = i break assert swapped and done and g.index == 1 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 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)) g = Game([str(a), str(b)], str(tmp_path / "noassets")) 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_battery_visual_scales_by_pct(make_level): lvl = make_level(SIMPLE + "battery_pct: 4\n") assert lvl.battery_pct == 4 def test_cli_debug_forces_all_levels(make_game): g = make_game(SIMPLE, debug=True) assert g.level.debug is True g2 = make_game(SIMPLE) assert g2.level.debug is False 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") g._reload_level() 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 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 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 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") a = AssetStore("noassets") def painted(dbg): lvl.debug = dbg 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