#!/usr/bin/python3 import argparse import random import os import json from engine import maze, sdl2 as engine, controls, graphics, unit_manager, scoring from engine.collision_system import CollisionSystem from units import points from engine.user_profile_integration import UserProfileIntegration from runtime_paths import bundle_path class MiceMaze( controls.KeyBindings, unit_manager.UnitManager, graphics.Graphics, scoring.Scoring ): # ==================== INITIALIZATION ==================== def __init__(self, maze_file, level_index=0): # Initialize user profile integration self.profile_integration = UserProfileIntegration() self.map_source = maze_file self.current_level = level_index self.map = maze.Map(maze_file, level_index=level_index) # Load profile-specific settings self.audio = self.profile_integration.get_setting('sound_enabled', True) sound_volume = self.profile_integration.get_setting('sound_volume', 50) self.cell_size = 40 self.full_screen = False self.loaded_theme_index = None # Initialize render engine with profile-aware title player_name = self.profile_integration.get_profile_name() window_title = f"Mice! - {player_name}" self.render_engine = engine.GameWindow(self.map.width, self.map.height, self.cell_size, window_title, key_callback=self.trigger) # Apply profile settings if hasattr(self.render_engine, 'set_volume'): self.render_engine.set_volume(sound_volume) self.load_assets() self.render_engine.window.show() self.render_engine.show_intro(bundle_path("assets", "Rat", "intro.png")) self.pointer = (random.randint(1, self.map.width-2), random.randint(1, self.map.height-2)) self.scroll_cursor() self.points = 0 self.units = {} # Initialize optimized collision system with NumPy self.collision_system = CollisionSystem( self.cell_size, self.map.width, self.map.height ) # Keep old dictionaries for backward compatibility (can be removed later) self.unit_positions = {} self.unit_positions_before = {} self.scrolling_direction = None self.game_status = "start_menu" self.menu_screen = "start" self.game_end = (False, None) self.run_recorded = False self.scrolling = False self.sounds = {} self.start_game() self.background_texture = None self.configs = self.get_config() self.combined_scores = None def get_config(self): configs = {} conf_dir = bundle_path("conf") for file in os.listdir(conf_dir): if file.endswith(".json"): with open(os.path.join(conf_dir, file), encoding="utf-8") as f: configs[file[:-5]] = json.load(f) return configs def start_game(self): print(f"[flow] start_game: level={self.current_level + 1}") self._valid_positions = None self.combined_scores = None self.run_recorded = False self.ammo = { "bomb": { "count": 2, "max": 8 }, "nuclear": { "count": 1, "max": 1 }, "mine": { "count": 2, "max": 4 }, "gas": { "count": 2, "max": 4 } } self.blood_stains = {} self.background_texture = None # Clear blood layer on game start/restart self.blood_layer_sprites.clear() self.cave_foreground_tiles.clear() self.game_end = (False, None) self.game_status = "start_menu" self.menu_screen = "start" self.units.clear() self.unit_positions.clear() self.unit_positions_before.clear() self.points = 0 self.pointer = (random.randint(1, self.map.width-2), random.randint(1, self.map.height-2)) self.scroll_cursor() for _ in range(5): self.spawn_rat() def load_level(self, level_index, preserve_points=True, show_menu=False, menu_screen=None): next_theme_index = level_index % maze.LEVELS_PER_DAT_FILE // 8 + 1 print( f"[flow] load_level requested: target_level={level_index + 1} " f"preserve_points={preserve_points} show_menu={show_menu} menu_screen={menu_screen} " f"current_points={self.points} next_theme={next_theme_index}" ) self.current_level = level_index self.map = maze.Map(self.map_source, level_index=level_index) self._valid_positions = None self.collision_system = CollisionSystem( self.cell_size, self.map.width, self.map.height ) if getattr(self, "loaded_theme_index", None) != next_theme_index: print( f"[flow] theme switch needed: loaded_theme={getattr(self, 'loaded_theme_index', None)} " f"-> next_theme={next_theme_index}" ) self.load_assets() else: print(f"[flow] theme unchanged: reusing theme {next_theme_index}") self.units.clear() self.unit_positions.clear() self.unit_positions_before.clear() self.blood_stains = {} self.blood_layer_sprites.clear() self.cave_foreground_tiles.clear() self.background_texture = None self.ammo = { "bomb": {"count": 2, "max": 8}, "nuclear": {"count": 1, "max": 1}, "mine": {"count": 2, "max": 4}, "gas": {"count": 2, "max": 4}, } self.combined_scores = None self.game_end = (False, None) self.pointer = (random.randint(1, self.map.width-2), random.randint(1, self.map.height-2)) self.scroll_cursor() if not preserve_points: self.points = 0 self.run_recorded = False print("[flow] points reset for new run") for spawn_index in range(5): print(f"[flow] spawning rat {spawn_index + 1}/5 for level={self.current_level + 1}", flush=True) self.spawn_rat() if show_menu: self.game_status = "start_menu" self.menu_screen = menu_screen or "level_intro" print(f"[flow] level loaded into menu state: menu_screen={self.menu_screen} points={self.points}") else: self.game_status = "game" self.menu_screen = None print(f"[flow] level loaded directly into gameplay: level={self.current_level + 1} points={self.points}") def advance_level(self): print(f"[flow] advance_level called from level={self.current_level + 1} points={self.points}") if self.map.source_path.suffix.lower() != ".dat": self.load_level(self.current_level, preserve_points=True, show_menu=True, menu_screen="level_intro") return next_level = (self.current_level + 1) % maze.LEVELS_PER_DAT_FILE print(f"[flow] advancing to level={next_level + 1}") self.load_level(next_level, preserve_points=True, show_menu=True, menu_screen="level_intro") def reset_game(self): print( f"[flow] reset_game called: game_end={self.game_end} game_status={self.game_status} " f"menu_screen={self.menu_screen} points={self.points} level={self.current_level + 1}" ) if self.game_end[0]: if self.game_end[1]: print("[flow] reset_game -> post-victory path") self.advance_level() else: print("[flow] reset_game -> restart from level 1 after defeat") self.load_level(0, preserve_points=False, show_menu=False) return if self.game_status == "paused": print("[flow] reset_game -> unpausing current level") self.game_status = "game" return if self.game_status == "start_menu": print(f"[flow] reset_game -> leaving menu_screen={self.menu_screen} and entering gameplay") self.game_status = "game" self.menu_screen = None return print("[flow] reset_game -> hard reload current level") self.load_level(self.current_level, preserve_points=False, show_menu=False) # ==================== 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 update_maze(self): if self.game_over(): return if self.game_status == "paused": self.render_engine.dialog("Pause") return if self.game_status == "start_menu": if self.menu_screen == "level_intro": self.render_engine.dialog( f"Level {self.current_level + 1}", subtitle=f"Points: {self.points}\nPress Return to begin", image=self.assets["BMP_WEWIN"], ) else: player_name = self.profile_integration.get_profile_name() device_id = self.profile_integration.get_device_id() greeting_title = f"Welcome to Mice, {player_name}!" subtitle = "A game by Matteo, because he was bored." device_line = f"Device: {device_id}" if self.profile_integration.current_profile: profile = self.profile_integration.current_profile stats_line = f"Best Score: {profile['best_score']} | Games: {profile['games_played']}" full_subtitle = f"{subtitle}\n{device_line}\n{stats_line}\nPress Return to start" else: full_subtitle = f"{device_line}\nNo profile loaded - playing as guest\nPress Return to start" self.render_engine.dialog( greeting_title, subtitle=full_subtitle, image=self.assets["BMP_WEWIN"], ) return self.render_engine.delete_tag("unit") self.render_engine.delete_tag("effect") self.render_engine.delete_tag("cave") self.render_engine.draw_pointer(self.pointer[0] * self.cell_size, self.pointer[1] * self.cell_size) # Clear collision system for new frame 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 self.units.values(): # 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 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) # Register unit in optimized collision system self.collision_system.register_unit( unit.id, unit.bbox, unit.position, unit.position_before, unit.collision_layer ) # 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 self.units.copy().values(): 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 self.units.values(): # 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 self.units.copy().values(): unit.collisions() unit.draw() self.draw_cave_foreground() self.render_engine.update_status(f"Mice: {self.count_rats()} - Points: {self.points}") self.refill_ammo() self.render_engine.update_ammo(self.ammo, self.assets) self.scroll() self.render_engine.new_cycle(50, self.update_maze) def run(self): self.render_engine.mainloop(update=self.update_maze, bg_update=self.draw_maze) # ==================== GAME OVER LOGIC ==================== def game_over(self): if self.game_end[0]: if self.combined_scores is None: self.combined_scores = self.profile_integration.get_device_leaderboard(5) if not self.game_end[1]: self.render_engine.dialog( "Game Over: Mice are too many!", image=self.assets["BMP_WEWIN"], subtitle=f"Reached level: {self.current_level + 1}\nPress Return to restart from level 1", scores=self.combined_scores ) else: self.render_engine.dialog( f"Level {self.current_level + 1} Clear! Points: {self.points}", image=self.assets["BMP_WEWIN"], subtitle="Press Return for the next level", scores=self.combined_scores ) return True count_rats = self.count_rats() if count_rats > 200: self.render_engine.stop_sound() self.render_engine.play_sound("WEWIN.WAV") self.game_end = (True, False) self.game_status = "paused" print(f"[flow] defeat reached: rats={count_rats} points={self.points} level={self.current_level + 1}") if not self.run_recorded: self.save_score() self.profile_integration.update_game_stats(self.points, completed=False) self.run_recorded = True return True if not count_rats and not any(isinstance(unit, points.Point) for unit in self.units.values()): self.render_engine.stop_sound() self.render_engine.play_sound("VICTORY.WAV") self.render_engine.play_sound("WELLDONE.WAV", tag="effects") self.game_end = (True, True) self.game_status = "paused" self.combined_scores = self.profile_integration.get_device_leaderboard(5) print(f"[flow] victory reached: points={self.points} level={self.current_level + 1}") return True def parse_args(): parser = argparse.ArgumentParser(description="Run Mice! with DAT or JSON map loading") parser.add_argument("--level", type=int, default=0, help="Level index to load from level.dat (default: 0)") parser.add_argument("--map", dest="map_path", default=None, help="Optional map path override (.dat or .json)") return parser.parse_args() if __name__ == "__main__": args = parse_args() print("Game starting...") map_source = args.map_path or maze.get_default_map_source() print(f"Loading map from {map_source} (level {args.level})") solver = MiceMaze(map_source, level_index=args.level) solver.run()