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,119 @@
|
||||
|
||||
import os
|
||||
import sys
|
||||
import unittest
|
||||
import random
|
||||
import json
|
||||
import hashlib
|
||||
from pathlib import Path
|
||||
|
||||
# Add current directory to path
|
||||
sys.path.append(os.getcwd())
|
||||
|
||||
# Set SDL to use dummy video driver
|
||||
os.environ["SDL_VIDEODRIVER"] = "dummy"
|
||||
os.environ["SDL_AUDIODRIVER"] = "dummy"
|
||||
os.environ["MICE_DISABLE_JOYSTICK"] = "1"
|
||||
|
||||
from rats import MiceMaze
|
||||
from engine.state_machine import GameState
|
||||
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
|
||||
|
||||
class LoopParityTester(unittest.TestCase):
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
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):
|
||||
# We need absolute determinism
|
||||
random.seed(12345)
|
||||
self.game = MiceMaze("assets/Rat/level.dat", level_index=0)
|
||||
|
||||
# Reset and restart to clear initialization entropy
|
||||
random.seed(12345)
|
||||
# Monkeypatch UUID to be deterministic
|
||||
import uuid
|
||||
self.uuid_counter = 0
|
||||
def mock_uuid4():
|
||||
self.uuid_counter += 1
|
||||
return self.uuid_counter
|
||||
uuid.uuid4 = mock_uuid4
|
||||
|
||||
self.game.start_game()
|
||||
self.game.state_machine.transition_to(GameState.PLAYING)
|
||||
|
||||
def get_full_snapshot(self):
|
||||
"""Captures extremely detailed state of all units."""
|
||||
snapshot = {
|
||||
"points": self.game.points,
|
||||
"units": []
|
||||
}
|
||||
# Sort by ID for stability
|
||||
sorted_units = sorted(self.game.units.items(), key=lambda x: int(x[0]))
|
||||
|
||||
for uid, u in sorted_units:
|
||||
u_data = {
|
||||
"id": uid,
|
||||
"type": u.__class__.__name__,
|
||||
"pos": list(u.position),
|
||||
"pos_before": list(u.position_before),
|
||||
"partial": float(u.partial_move),
|
||||
"age": int(u.age)
|
||||
}
|
||||
# Optional attributes that affect logic
|
||||
if hasattr(u, "pregnant"): u_data["pregnant"] = int(u.pregnant)
|
||||
if hasattr(u, "babies"): u_data["babies"] = int(u.babies)
|
||||
if hasattr(u, "gassed"): u_data["gassed"] = int(u.gassed)
|
||||
if hasattr(u, "direction"): u_data["dir"] = u.direction
|
||||
|
||||
snapshot["units"].append(u_data)
|
||||
|
||||
# Add a hash of the total unit count and positions for quick check
|
||||
flat_state = str(snapshot).encode('utf-8')
|
||||
snapshot["hash"] = hashlib.md5(flat_state).hexdigest()
|
||||
return snapshot
|
||||
|
||||
def test_record_or_verify(self):
|
||||
steps = 100
|
||||
parity_file = Path("tests/loop_parity_master.json")
|
||||
|
||||
states = []
|
||||
print(f"Running simulation for {steps} steps...")
|
||||
|
||||
for i in range(steps):
|
||||
self.game.update_maze()
|
||||
states.append(self.get_full_snapshot())
|
||||
|
||||
if not parity_file.exists() or os.environ.get("RECORD_PARITY"):
|
||||
with open(parity_file, "w") as f:
|
||||
json.dump(states, f, indent=2)
|
||||
print(f"RECORDED master state to {parity_file}")
|
||||
else:
|
||||
with open(parity_file, "r") as f:
|
||||
master_states = json.load(f)
|
||||
|
||||
self.assertEqual(len(states), len(master_states))
|
||||
|
||||
for i, (curr, master) in enumerate(zip(states, master_states)):
|
||||
if curr["hash"] != master["hash"]:
|
||||
# Detailed comparison on failure
|
||||
self.assertEqual(curr["points"], master["points"], f"Points mismatch at step {i}")
|
||||
self.assertEqual(len(curr["units"]), len(master["units"]), f"Unit count mismatch at step {i}")
|
||||
|
||||
for u_idx, (u_curr, u_master) in enumerate(zip(curr["units"], master["units"])):
|
||||
self.assertEqual(u_curr, u_master, f"Unit {u_idx} mismatch at step {i}")
|
||||
|
||||
print("PARITY VERIFIED: Optimization is functionally identical.")
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Reference in New Issue
Block a user