Files
mice/tests/test_final_level_flow.py
T
enne2 c7ed24483d Add comprehensive test suite for game mechanics and level handling
- Introduced `test_final_level_flow.py` to validate final level transitions and game end scenarios.
- Created `test_game_over_flow.py` to ensure game over conditions trigger correctly based on rat counts.
- Implemented `test_keybindings.py` to verify keybinding configurations and their context-specific actions.
- Developed `test_level_editor.py` to assess level editor functionalities and layout computations.
- Added `test_level_io.py` for testing level data serialization and deserialization.
- Established `test_loop_logic_parity.py` to ensure consistent game state across multiple simulation runs.
- Created `test_non_regression.py` to simulate game behavior and capture states for future verification.
- Implemented `test_verify.py` to compare current game states against a golden master for regression detection.
2026-05-19 22:18:43 +02:00

239 lines
8.1 KiB
Python

#!/usr/bin/env python3
import unittest
from pathlib import Path
from types import SimpleNamespace
from engine import maze
from rats import (
GAME_END_LEVEL_CLEAR,
GAME_END_RUN_COMPLETE,
MiceMaze,
RUN_COMPLETE_MUSIC,
)
def build_dummy(level_index):
dummy = MiceMaze.__new__(MiceMaze)
dummy.map = SimpleNamespace(source_path=Path("/tmp/level.dat"))
dummy.current_level = level_index
dummy.total_levels = maze.DEFAULT_LEVELS_PER_DAT_FILE
dummy.points = 1234
dummy.run_recorded = False
dummy.combined_scores = None
dummy.game_status = "game"
dummy.game_end = (False, None)
dummy.menu_screen = None
dummy.units = {}
dummy.assets = {"BMP_WEWIN": object(), "end": object(), "clear": object(), "lose": object()}
dummy.explosions = {"LEFT": object(), "RIGHT": object(), "UP": object(), "DOWN": object()}
dummy.count_rats = lambda: 0
dummy.start_menu_animation_started_at = 0
dummy._difficulty_config = lambda: {"label": "Hard"}
sounds = []
dialogs = []
stats = []
saves = []
load_calls = []
dummy.render_engine = SimpleNamespace(
stop_sound=lambda: sounds.append(("stop",)),
play_sound=lambda *args, **kwargs: sounds.append((args, kwargs)),
dialog=lambda *args, **kwargs: dialogs.append((args, kwargs)),
)
dummy.profile_integration = SimpleNamespace(
update_game_stats=lambda score, completed=True: stats.append((score, completed)) or True,
get_device_leaderboard=lambda limit: [{"user_id": "Player1", "best_score": 1234}][:limit],
get_profile_name=lambda: "Player1",
)
dummy.save_score = lambda: saves.append(dummy.points)
dummy.load_level = lambda *args, **kwargs: load_calls.append((args, kwargs))
return dummy, sounds, dialogs, stats, saves, load_calls
class FinalLevelFlowTests(unittest.TestCase):
def test_debug_flag_helper_opens_final_dialog_without_recording_score(self):
dummy, _, _, stats, saves, _ = build_dummy(3)
dummy.activate_debug_run_complete_dialog(score=777)
self.assertEqual(dummy.current_level, dummy.total_levels - 1)
self.assertEqual(dummy.points, 777)
self.assertEqual(dummy.game_end, (True, GAME_END_RUN_COMPLETE))
self.assertTrue(dummy.run_recorded)
self.assertEqual(dummy.game_status, "paused")
self.assertEqual(stats, [])
self.assertEqual(saves, [])
def test_run_complete_screen_uses_dedicated_music(self):
dummy, _, _, _, _, _ = build_dummy(3)
music_calls = []
dummy.render_engine.play_music = lambda track, loop=True: music_calls.append((track, loop)) or True
dummy.render_engine.pause_music = lambda: music_calls.append(("pause", False))
dummy.game_end = (True, GAME_END_RUN_COMPLETE)
dummy.update_background_music()
self.assertEqual(music_calls, [(RUN_COMPLETE_MUSIC, True)])
def test_advance_level_does_not_wrap_after_last_dat_level(self):
dummy, _, _, stats, saves, load_calls = build_dummy(maze.DEFAULT_LEVELS_PER_DAT_FILE - 1)
dummy.advance_level()
self.assertEqual(dummy.game_end, (True, GAME_END_RUN_COMPLETE))
self.assertTrue(dummy.run_recorded)
self.assertEqual(stats, [(1234, True)])
self.assertEqual(saves, [1234])
self.assertEqual(load_calls, [])
def test_advance_level_starts_next_dat_level_immediately(self):
dummy, _, _, _, _, load_calls = build_dummy(4)
dummy.advance_level()
self.assertEqual(
load_calls,
[((5,), {"preserve_points": True, "show_menu": False})],
)
def test_advance_level_starts_non_dat_level_immediately(self):
dummy, _, _, _, _, load_calls = build_dummy(0)
dummy.map = SimpleNamespace(source_path=Path("/tmp/level.json"))
dummy.advance_level()
self.assertEqual(
load_calls,
[((0,), {"preserve_points": True, "show_menu": False})],
)
def test_regular_level_clear_keeps_run_open(self):
dummy, sounds, dialogs, stats, saves, _ = build_dummy(4)
result = dummy.game_over()
self.assertTrue(result)
self.assertEqual(dummy.game_end, (True, GAME_END_LEVEL_CLEAR))
self.assertFalse(dummy.run_recorded)
self.assertEqual(dummy.combined_scores, [{"user_id": "Player1", "best_score": 1234}])
self.assertEqual(stats, [])
self.assertEqual(saves, [])
self.assertEqual(
sounds,
[
("stop",),
(("VICTORY.WAV",), {}),
],
)
dummy.game_over()
self.assertEqual(dialogs[-1][0][0], "Level 5 Clear! Points: 1234")
self.assertIs(dialogs[-1][1]["image"], dummy.assets["clear"])
def test_defeat_dialog_uses_lose_art(self):
dummy, sounds, dialogs, stats, saves, _ = build_dummy(4)
dummy.count_rats = lambda: 201
result = dummy.game_over()
self.assertTrue(result)
self.assertEqual(dummy.game_end, (True, "defeat"))
self.assertEqual(stats, [(1234, False)])
self.assertEqual(saves, [1234])
self.assertEqual(
sounds,
[
("stop",),
(("WEWIN.WAV",), {}),
],
)
dummy.game_over()
self.assertEqual(dialogs[-1][0][0], "Game Over: Mice are too many!")
self.assertIs(dialogs[-1][1]["image"], dummy.assets["lose"])
self.assertEqual(dialogs[-1][1]["image_scale"], 0.48)
self.assertEqual(dialogs[-1][1]["scores"], dummy.combined_scores)
self.assertEqual(
dialogs[-1][1]["subtitle"],
"Reached level: 5\nPress Return to go back to the start menu",
)
def test_final_dat_level_records_score_and_shows_dedicated_dialog(self):
dummy, sounds, dialogs, stats, saves, _ = build_dummy(maze.DEFAULT_LEVELS_PER_DAT_FILE - 1)
result = dummy.game_over()
self.assertTrue(result)
self.assertEqual(dummy.game_end, (True, GAME_END_RUN_COMPLETE))
self.assertTrue(dummy.run_recorded)
self.assertEqual(stats, [(1234, True)])
self.assertEqual(saves, [1234])
self.assertEqual(dummy.combined_scores, [{"user_id": "Player1", "best_score": 1234}])
self.assertEqual(
sounds,
[
("stop",),
(("VICTORY.WAV",), {}),
(("WELLDONE.WAV",), {"tag": "effects"}),
],
)
dummy.game_over()
self.assertEqual(dialogs[-1][0][0], "THE END")
self.assertEqual(dialogs[-1][1]["style"], "run_complete")
self.assertEqual(dialogs[-1][1]["current_score"], 1234)
self.assertIs(dialogs[-1][1]["image"], dummy.assets["end"])
def test_return_after_final_dialog_goes_back_to_start_menu(self):
dummy, _, _, _, _, load_calls = build_dummy(maze.DEFAULT_LEVELS_PER_DAT_FILE - 1)
dummy.game_end = (True, GAME_END_RUN_COMPLETE)
dummy.game_status = "paused"
dummy.reset_game()
self.assertEqual(
load_calls,
[((0,), {"preserve_points": False, "show_menu": True, "menu_screen": "start"})],
)
def test_return_after_defeat_goes_back_to_start_menu(self):
dummy, _, _, _, _, load_calls = build_dummy(4)
dummy.game_end = (True, "defeat")
dummy.game_status = "paused"
dummy.reset_game()
self.assertEqual(
load_calls,
[((0,), {"preserve_points": False, "show_menu": True, "menu_screen": "start"})],
)
def test_toggle_pause_ignores_game_end_dialogs(self):
dummy, _, _, _, _, _ = build_dummy(2)
dummy.game_status = "paused"
dummy.game_end = (True, GAME_END_LEVEL_CLEAR)
dummy.toggle_pause()
self.assertEqual(dummy.game_status, "paused")
self.assertEqual(dummy.game_end, (True, GAME_END_LEVEL_CLEAR))
def test_toggle_pause_still_toggles_real_pause(self):
dummy, _, _, _, _, _ = build_dummy(2)
dummy.toggle_pause()
self.assertEqual(dummy.game_status, "paused")
dummy.toggle_pause()
self.assertEqual(dummy.game_status, "game")
if __name__ == "__main__":
unittest.main()