import os import sys import random import unittest import json from pathlib import Path from PIL import Image import numpy as np # Add current directory to path sys.path.append(os.getcwd()) # Set SDL to use dummy video driver for headless environments os.environ["SDL_VIDEODRIVER"] = "dummy" os.environ["SDL_AUDIODRIVER"] = "dummy" os.environ["MICE_DISABLE_JOYSTICK"] = "1" from rats import MiceMaze from engine.sdl2 import GameWindow def mock_init_audio(self): self.music_enabled = False self.audio_devs = {"base": 0, "effects": 0, "music": 0} self.sound_volume = 0 self.music_volume = 0 import uuid # Global counter for deterministic UUIDs _uuid_counter = 0 def mock_uuid4(): global _uuid_counter val = _uuid_counter _uuid_counter += 1 return val class NonRegressionVerification(unittest.TestCase): @classmethod def setUpClass(cls): # Deterministic UUIDs uuid.uuid4 = mock_uuid4 GameWindow.show_intro = lambda *args, **kwargs: None GameWindow.show_loading_screen = lambda *args, **kwargs: None GameWindow._init_audio_system = mock_init_audio GameWindow.play_sound = lambda *args, **kwargs: None GameWindow.stop_sound = lambda *args, **kwargs: None def setUp(self): global _uuid_counter _uuid_counter = 0 random.seed(42) self.game = MiceMaze("assets/Rat/level.dat", level_index=0) # Re-seed again to clear entropy consumed by asset loading random.seed(42) _uuid_counter = 0 self.game.start_game() # Trigger background generation once to consume those random calls # before the simulation starts, ensuring stability. self.game.graphics.draw_maze() self.game.game_status = "game" self.game.menu_screen = None def test_verify_against_golden_master(self): golden_master_dir = Path("tests/golden_master") if not golden_master_dir.exists(): self.skipTest("Golden master not found. Run recording first.") with open(golden_master_dir / "states.json", "r") as f: golden_states = json.load(f) steps = 200 golden_idx = 0 print(f"Verifying against golden master for {steps} steps...") for i in range(steps): self.game.update_maze() if i % 50 == 0 or i == steps - 1: current_state = self.dump_game_state() golden_state = golden_states[golden_idx] # Compare unit count self.assertEqual(current_state["unit_count"], golden_state["unit_count"], f"Unit count mismatch at step {i}") # Compare units for u_idx, (curr_u, gold_u) in enumerate(zip(current_state["units"], golden_state["units"])): self.assertEqual(curr_u["id"], gold_u["id"], f"Unit ID mismatch at step {i}, index {u_idx}") self.assertEqual(curr_u["type"], gold_u["type"], f"Unit type mismatch at step {i}, unit {curr_u['id']}") self.assertEqual(curr_u["pos"], gold_u["pos"], f"Unit pos mismatch at step {i}, unit {curr_u['id']}") self.assertAlmostEqual(curr_u["partial_move"], gold_u["partial_move"], places=5, msg=f"Unit partial_move mismatch at step {i}, unit {curr_u['id']}") golden_idx += 1 print("Verification successful! No regressions detected.") def dump_game_state(self): state = { "points": self.game.points, "unit_count": len(self.game.units), "units": [] } # Sort units by ID to be deterministic sorted_units = sorted(self.game.units.items(), key=lambda x: int(x[0])) for uid, unit in sorted_units: unit_state = { "id": str(uid), "type": unit.__class__.__name__, "pos": list(unit.position), "partial_move": float(unit.partial_move), } state["units"].append(unit_state) return state if __name__ == "__main__": unittest.main()