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:
2026-05-19 22:18:43 +02:00
parent 486cd6b7c5
commit c7ed24483d
53 changed files with 10169 additions and 3886 deletions
+18 -49
View File
@@ -10,6 +10,7 @@ from engine import maze, sdl2 as engine, controls, graphics, unit_manager, scori
from engine.state_machine import GameState
from engine.collision_system import CollisionSystem
from units import points
from units.unit import UnitType
from engine.user_profile_integration import UserProfileIntegration
from runtime_paths import bundle_path
@@ -394,18 +395,6 @@ class MiceMaze:
# ==================== GAME LOGIC ====================
def refill_ammo(self):
for ammo_type, data in self.ammo.items():
if ammo_type == "bomb":
if random.random() < 0.02:
data["count"] = min(data["count"] + 1, data["max"])
elif ammo_type == "mine":
if random.random() < 0.05:
data["count"] = min(data["count"] + 1, data["max"])
elif ammo_type == "gas":
if random.random() < 0.01:
data["count"] = min(data["count"] + 1, data["max"])
def _can_adjust_audio_menu(self):
if self.game_end[0]:
return False
@@ -493,22 +482,25 @@ class MiceMaze:
self.render_engine.delete_tag("effect")
self.render_engine.delete_tag("cave")
# Clear collision system for new frame
# Clear collision system and legacy dictionaries
self.collision_system.clear()
self.unit_positions.clear()
self.unit_positions_before.clear()
# First pass: Register all units in collision system BEFORE move
# This allows bombs/gas to find victims during their move()
for unit in sorted(self.units.values(), key=lambda u: int(u.id)):
# Calculate bbox if not yet set (first frame)
if not hasattr(unit, 'bbox') or unit.bbox == (0, 0, 0, 0):
# Temporary bbox based on position
# Sort units by ID for deterministic execution
sorted_units = sorted(self.units.values(), key=lambda u: int(u.id))
# Pass 1: MOVE and REGISTER
for unit in sorted_units:
# 1a. Move unit (logic)
unit.move()
# 1b. Register final position in collision system
if unit.bbox == (0.0, 0.0, 0.0, 0.0):
x_pos = unit.position[0] * self.cell_size
y_pos = unit.position[1] * self.cell_size
unit.bbox = (x_pos, y_pos, x_pos + self.cell_size, y_pos + self.cell_size)
unit.bbox = (float(x_pos), float(y_pos), float(x_pos + self.cell_size), float(y_pos + self.cell_size))
# Register unit in optimized collision system
self.collision_system.register_unit(
unit.id,
unit.bbox,
@@ -520,40 +512,17 @@ class MiceMaze:
# Maintain backward compatibility dictionaries
self.unit_positions.setdefault(unit.position, []).append(unit)
self.unit_positions_before.setdefault(unit.position_before, []).append(unit)
# Second pass: move all units (can now access collision system)
for unit in sorted(self.units.values(), key=lambda u: int(u.id)):
unit.move()
# Third pass: Update collision system with new positions after move
self.collision_system.clear()
self.unit_positions.clear()
self.unit_positions_before.clear()
for unit in sorted(self.units.values(), key=lambda u: int(u.id)):
# Register with updated positions/bbox from move()
self.collision_system.register_unit(
unit.id,
unit.bbox,
unit.position,
unit.position_before,
unit.collision_layer
)
self.unit_positions.setdefault(unit.position, []).append(unit)
self.unit_positions_before.setdefault(unit.position_before, []).append(unit)
# Fourth pass: check collisions and draw
for unit in sorted(self.units.values(), key=lambda u: int(u.id)):
# Pass 2: RESOLVE and DRAW
for unit in sorted_units:
unit.collisions()
unit.draw()
self.graphics.draw_cave_foreground()
self.render_engine.draw_pointer(self.pointer[0] * self.cell_size, self.pointer[1] * self.cell_size)
self.render_engine.update_status(f"Mice: {self.unit_manager.count_rats()} - Points: {self.points}")
self.refill_ammo()
self.unit_manager.refill_ammo()
self.render_engine.update_ammo(self.ammo, self.graphics.assets)
self.controls.scroll()
self.render_engine.new_cycle(50, self.update_maze)
@@ -603,7 +572,7 @@ class MiceMaze:
self._record_run_result(completed=False)
return True
if not count_rats and not any(isinstance(unit, points.Point) for unit in self.units.values()):
if not count_rats and not any(unit.type == UnitType.POINT for unit in self.units.values()):
self.render_engine.stop_sound()
self.render_engine.play_sound("VICTORY.WAV")