#!/usr/bin/python3 import random import os import json from engine import maze, controls, graphics, pygame_layer as engine, unit_manager, scoring from units import points from engine.user_profile_integration import UserProfileIntegration class MiceMaze( controls.KeyBindings, unit_manager.UnitManager, graphics.Graphics, scoring.Scoring ): # ==================== INITIALIZATION ==================== def __init__(self, maze_file): # Initialize user profile integration print("[DEBUG] Initializing user profile integration...") self.profile_integration = UserProfileIntegration() print(f"[DEBUG] Profile integration initialized. Has current_profile: {self.profile_integration.current_profile is not None}") if self.profile_integration.current_profile: print(f"[DEBUG] Current profile: {self.profile_integration.get_profile_name()}") else: print("[DEBUG] No profile loaded, will use default settings") #self.profile_integration = None self.map = maze.Map(maze_file) # Load profile'-specific settings if self.profile_integration is None: self.audio = True sound_volume = 50 else: 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 # Initialize render engine with profile-aware title if self.profile_integration is None: player_name = "Guest" else: player_name = self.profile_integration.get_profile_name() window_title = f"Mice! - {player_name}" # If running under Pyodide in the browser, ensure the JS canvas is bound # to Pyodide's pygame integration _before_ creating the display. This # avoids cases where pygame.display.set_mode is called before the HTML # canvas is attached, which would result in a blank canvas in the page. try: # 'js' is available under Pyodide as a proxy to the global window import js try: canvas_el = js.document.getElementById('canvas') if hasattr(js.pyodide, 'canvas') and hasattr(js.pyodide.canvas, 'setCanvas2D'): js.pyodide.canvas.setCanvas2D(canvas_el) except Exception: # Non-fatal: continue with engine initialization pass except Exception: # Not running under Pyodide (native run), ignore pass 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() # Show window (for pygame, this is implicit; for SDL2 it's explicit) if hasattr(self.render_engine.window, 'show'): self.render_engine.window.show() self.pointer = (random.randint(1, self.map.width-2), random.randint(1, self.map.height-2)) self.scroll_cursor() self.points = 0 self.units = {} self.unit_positions = {} self.unit_positions_before = {} self.scrolling_direction = None self.game_status = "start_menu" self.game_end = (False, None) 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 = {} for file in os.listdir("conf"): if file.endswith(".json"): with open(os.path.join("conf", file)) as f: configs[file[:-5]] = json.load(f) return configs def start_game(self): self.combined_scores = False self.ammo = { "bomb": { "count": 2, "max": 8 }, "nuclear": { "count": 11, "max": 1 }, "mine": { "count": 2, "max": 4 }, "gas": { "count": 2, "max": 4 } } self.blood_stains = {} self.background_texture = None for _ in range(5): self.spawn_rat() def reset_game(self): self.pause = False self.game_status = "game" self.game_end = (False, None) self.units.clear() self.points = 0 self.start_game() # ==================== 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": # Create personalized greeting if self.profile_integration: player_name = self.profile_integration.get_profile_name() else: player_name = "Guest" if self.profile_integration: device_id = self.profile_integration.get_device_id() else: device_id = "Unknown Device" greeting_title = f"Welcome to Mice, {player_name}!" # Build subtitle with proper formatting subtitle = "A game by Matteo, because he was bored." device_line = f"Device: {device_id}" # Show profile stats if available if self.profile_integration and 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}" else: full_subtitle = f"{device_line}\nNo profile loaded - playing as guest" self.render_engine.dialog(greeting_title, subtitle=full_subtitle, image=self.assets["da"]) return self.render_engine.delete_tag("unit") self.render_engine.delete_tag("effect") self.render_engine.draw_pointer(self.pointer[0] * self.cell_size, self.pointer[1] * self.cell_size) self.unit_positions.clear() self.unit_positions_before.clear() for unit in self.units.values(): self.unit_positions.setdefault(unit.position, []).append(unit) self.unit_positions_before.setdefault(unit.position_before, []).append(unit) for unit in self.units.copy().values(): unit.move() unit.collisions() unit.draw() 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 tick(self): """Run a single frame tick: update logic + background update (non-blocking). Intended to be called repeatedly from the browser via requestAnimationFrame. """ try: # Use render_engine.step() to run one frame iteration (draw + flip) if hasattr(self.render_engine, 'step'): self.render_engine.step(update=self.update_maze, bg_update=self.draw_maze) else: # Fallback: call update_maze directly self.update_maze() except Exception: pass 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 not self.combined_scores: self.combined_scores = self.profile_integration.get_global_leaderboard(4) global_scores = [] for entry in self.combined_scores: # Convert to format expected by dialog: [date, score, name, device] global_scores.append([ entry.get('last_play', ''), entry.get('best_score', 0), entry.get('user_id', 'Unknown'), ]) if not self.game_end[1]: self.render_engine.dialog( "Game Over: Mice are too many!", image=self.assets["BMP_WEWIN"], scores=global_scores ) else: self.render_engine.dialog( f"You Win! Points: {self.points}", image=self.assets["BMP_WEWIN"], scores=global_scores ) return True count_rats = self.count_rats() if count_rats > 200: print("[DEBUG GAME_OVER] Loss condition: rats > 200") print(f"[DEBUG GAME_OVER] Rat count: {count_rats}, Points: {self.points}") self.render_engine.stop_sound() self.render_engine.play_sound("WEWIN.WAV") self.game_end = (True, False) self.game_status = "paused" # Track incomplete game in profile print(f"[DEBUG GAME_OVER] Calling update_game_stats(completed=False)") self.profile_integration.update_game_stats(self.points, completed=False) return True if not count_rats and not any(isinstance(unit, points.Point) for unit in self.units.values()): print("[DEBUG GAME_OVER] Win condition: all rats and points cleared") print(f"[DEBUG GAME_OVER] Points earned: {self.points}") 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" # Save score to both traditional file and user profile print(f"[DEBUG GAME_OVER] Calling save_score()") self.save_score() print(f"[DEBUG GAME_OVER] Calling update_game_stats(completed=True)") self.profile_integration.update_game_stats(self.points, completed=True) return True if __name__ == "__main__": print("Game starting...") solver = MiceMaze('maze.json') solver.run()