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.
This commit is contained in:
@@ -0,0 +1,148 @@
|
||||
|
||||
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()
|
||||
Reference in New Issue
Block a user