import os import sys import random import unittest import json import time from pathlib import Path from PIL import Image # 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 NonRegressionTest(unittest.TestCase): @classmethod def setUpClass(cls): # Deterministic UUIDs uuid.uuid4 = mock_uuid4 # Monkeypatch SDL2 methods that block or show windows GameWindow.show_intro = lambda *args, **kwargs: None GameWindow.show_loading_screen = lambda *args, **kwargs: None # Disable audio and its effects 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 # Initial seed for constructor random.seed(42) # Initialize game self.game = MiceMaze("assets/Rat/level.dat", level_index=0) # Re-seed again to clear entropy consumed by asset loading (blood stains etc) # This ensures the game logic starts from a consistent random state random.seed(42) _uuid_counter = 0 # Reset UUIDs too for initial spawns self.game.start_game() # Trigger background generation once to consume those random calls # before the simulation starts, ensuring stability. self.game.graphics.draw_maze() # Override dynamic attributes self.game.start_menu_animation_started_at = 0 # Skip menu and start gameplay self.game.game_status = "game" self.game.menu_screen = None def test_simulation_run(self): steps = 200 states = [] # Output directory output_dir = Path("tests/non_regression_output") output_dir.mkdir(parents=True, exist_ok=True) print(f"Starting simulation for {steps} steps...") for i in range(steps): # Advance game state self.game.update_maze() # Every 50 steps, record state and screenshot if i % 50 == 0 or i == steps - 1: state = self.dump_game_state() state["frame"] = i states.append(state) # Visual snapshot # Note: In dummy driver, RenderReadPixels might return empty/black # but we'll try anyway. If it fails, we rely on the state JSON. try: self.game.graphics.draw_maze() # Manually draw units because we are not in mainloop for unit in list(self.game.units.values()): unit.draw() img = self.game.render_engine.capture_frame() img_path = output_dir / f"frame_{i:04d}.png" img.save(img_path) except Exception as e: print(f"Warning: Could not capture frame at step {i}: {e}") # Save states to JSON states_path = output_dir / "states.json" with open(states_path, "w") as f: json.dump(states, f, indent=2) print(f"Simulation complete. Outputs saved to {output_dir}") 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), "pos_before": list(unit.position_before), "partial_move": float(unit.partial_move), "age": int(unit.age), } if hasattr(unit, "sex"): unit_state["sex"] = unit.sex if hasattr(unit, "direction"): unit_state["direction"] = unit.direction state["units"].append(unit_state) return state if __name__ == "__main__": unittest.main()