Files
mice/rats.py
T

783 lines
32 KiB
Python

#!/usr/bin/python3
import argparse
import random
import os
import json
import time
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
LEVEL_MUSIC_CONFIG = "level_music"
START_MENU_MUSIC = "High_Score_Garden.mp3"
START_MENU_ANIMATION = "anim/start_mice.gif"
SUPPORTED_MUSIC_EXTENSIONS = {".mp3", ".ogg", ".wav"}
START_MENU_AUDIO_OPTIONS = (
("sound_volume", "Suono"),
("music_volume", "Musica"),
)
VOLUME_STEP = 5
START_MENU_COLORS = {
"panel_fill": (255, 255, 255),
"panel_border": (52, 52, 52),
"header_fill": (255, 255, 255),
"text": (24, 24, 24),
"muted": (82, 82, 82),
"hint_fill": (255, 255, 255),
"track_fill": (212, 215, 216),
"card_fill": (255, 255, 255),
}
START_MENU_AUDIO_STYLES = {
"sound_volume": {
"accent": (214, 146, 62),
"fill": (251, 241, 225),
},
"music_volume": {
"accent": (91, 122, 208),
"fill": (230, 235, 248),
},
}
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)
self.sound_volume = self.profile_integration.get_setting('sound_volume', 50)
self.music_volume = self.profile_integration.get_setting('music_volume', self.sound_volume)
self.cell_size = 40
self.full_screen = False
self.loaded_theme_index = None
self.start_menu_selection = 0
# 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)
self.render_engine.audio = self.audio
# Apply profile settings
if hasattr(self.render_engine, 'set_sound_volume'):
self.render_engine.set_sound_volume(self.sound_volume)
if hasattr(self.render_engine, 'set_music_volume'):
self.render_engine.set_music_volume(self.music_volume)
elif hasattr(self.render_engine, 'set_volume'):
self.render_engine.set_volume(self.music_volume)
self.initialize_keybindings()
self.configs = self.get_config()
self.available_music_tracks = self._load_available_music_tracks()
self.current_level_music = None
self.load_assets()
self.start_menu_animation = self.render_engine.load_animation(START_MENU_ANIMATION)
self.start_menu_animation_started_at = time.monotonic()
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.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 _load_available_music_tracks(self):
music_dir = bundle_path("assets", "music")
tracks = []
for file_name in sorted(os.listdir(music_dir)):
_, extension = os.path.splitext(file_name)
if extension.lower() not in SUPPORTED_MUSIC_EXTENSIONS:
continue
if os.path.isfile(os.path.join(music_dir, file_name)):
tracks.append(file_name)
return tracks
def _resolve_level_music(self, level_index):
level_number = level_index + 1
level_music_config = self.configs.get(LEVEL_MUSIC_CONFIG, {})
configured_tracks = level_music_config.get("levels", {})
configured_track = configured_tracks.get(str(level_number))
if configured_track in self.available_music_tracks:
print(f"[audio] level {level_number} music={configured_track} source=config")
return configured_track
if configured_track:
print(f"[audio] level {level_number} configured music missing: {configured_track}")
if not self.available_music_tracks:
print(f"[audio] no music tracks available for level {level_number}")
return None
fallback_track = random.choice(self.available_music_tracks)
print(f"[audio] level {level_number} music={fallback_track} source=random")
return fallback_track
def start_game(self):
print(f"[flow] start_game: level={self.current_level + 1}")
self.start_menu_animation_started_at = time.monotonic()
self.current_level_music = self._resolve_level_music(self.current_level)
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.current_level_music = self._resolve_level_music(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 _can_adjust_audio_menu(self):
if self.game_end[0]:
return False
return self.game_status == "paused"
def _menu_font(self, size):
clamped = max(10, min(69, int(size)))
return self.render_engine.fonts[clamped]
def _draw_start_menu_slider(self, x, y, width, height, setting_name, label, value, selected):
colors = START_MENU_COLORS
style = START_MENU_AUDIO_STYLES[setting_name]
accent = style["accent"]
fill_color = style["fill"] if selected else colors["card_fill"]
border_color = accent if selected else (156, 156, 156)
text_color = colors["text"]
render_engine = self.render_engine
render_engine.draw_rectangle(x, y, width, height, "start_menu_slider", filling=fill_color)
render_engine.draw_rectangle(x, y, width, height, "start_menu_slider", outline=border_color)
if selected:
render_engine.draw_rectangle(x + 12, y + 10, 8, height - 20, "start_menu_slider", filling=accent)
title_y = y + 10
render_engine.draw_text(label, self._menu_font(render_engine.target_size[1] // 32), (x + 34, title_y), text_color)
render_engine.draw_text(f"{value}%", self._menu_font(render_engine.target_size[1] // 34), (x + width - 84, title_y + 2), text_color)
track_x = x + 34
track_y = y + height - 26
track_width = width - 68
track_height = 14
filled_width = int(track_width * value / 100)
knob_width = 14
knob_x = track_x + int((track_width - knob_width) * value / 100)
render_engine.draw_rectangle(track_x, track_y, track_width, track_height, "start_menu_slider", filling=colors["track_fill"])
render_engine.draw_rectangle(track_x, track_y, track_width, track_height, "start_menu_slider", outline=(128, 128, 128))
if filled_width > 0:
render_engine.draw_rectangle(track_x, track_y, filled_width, track_height, "start_menu_slider", filling=accent)
render_engine.draw_rectangle(knob_x, track_y - 4, knob_width, track_height + 8, "start_menu_slider", filling=(255, 255, 255))
render_engine.draw_rectangle(knob_x, track_y - 4, knob_width, track_height + 8, "start_menu_slider", outline=accent)
def _current_start_menu_animation_frame(self):
animation = getattr(self, "start_menu_animation", None)
if not animation or not animation["frames"]:
return None, (0, 0)
if len(animation["frames"]) == 1 or animation["total_duration"] <= 0:
return animation["frames"][0], animation["size"]
elapsed_ms = int((time.monotonic() - self.start_menu_animation_started_at) * 1000)
current_offset = elapsed_ms % animation["total_duration"]
accumulated = 0
for index, duration in enumerate(animation["durations"]):
accumulated += duration
if current_offset < accumulated:
return animation["frames"][index], animation["size"]
return animation["frames"][-1], animation["size"]
def _render_audio_menu(self, title, subtitle_lines, primary_action_text, hint_text, image_name="BMP_WEWIN"):
colors = START_MENU_COLORS
render_engine = self.render_engine
target_width, target_height = render_engine.target_size
compact_menu = target_height <= 540
panel_x = max(48, target_width // 12)
panel_y = max(34, target_height // 18)
panel_width = target_width - panel_x * 2
panel_height = target_height - panel_y * 2
header_height = max(44, target_height // 13)
render_engine.draw_rectangle(panel_x, panel_y, panel_width, panel_height, "start_menu", filling=colors["panel_fill"])
render_engine.draw_rectangle(panel_x, panel_y, panel_width, panel_height, "start_menu", outline=colors["panel_border"])
render_engine.draw_rectangle(panel_x, panel_y, panel_width, header_height, "start_menu", filling=colors["header_fill"])
render_engine.draw_text(
title,
self._menu_font(target_height // 20),
("center", panel_y + 16),
colors["text"],
)
image = self.assets[image_name]
image_width, image_height = render_engine.get_image_size(image)
image_y = panel_y + header_height + (14 if compact_menu else 18)
render_engine.draw_image(
target_width // 2 - image_width // 2 - render_engine.w_offset,
image_y - render_engine.h_offset,
image,
"start_menu",
)
info_y = image_y + image_height + 18
line_gap = 18 if compact_menu else max(20, target_height // 34)
for index, line in enumerate(subtitle_lines):
render_engine.draw_text(
line,
self._menu_font(target_height // (34 if index == 0 else 36)),
("center", info_y + line_gap * index),
colors["text"] if index == 0 else colors["muted"],
)
cta_y = info_y + line_gap * len(subtitle_lines) + 8
render_engine.draw_text(
primary_action_text,
self._menu_font(target_height // 31),
("center", cta_y),
colors["text"],
)
card_width = max(320, min(panel_width - 160, 720))
card_height = 60 if compact_menu else max(68, target_height // 10)
card_x = target_width // 2 - card_width // 2
cards_y = cta_y + (16 if compact_menu else 26)
card_gap = 12 if compact_menu else 16
for index, (setting_name, label) in enumerate(START_MENU_AUDIO_OPTIONS):
card_y = cards_y + index * (card_height + card_gap)
self._draw_start_menu_slider(
card_x,
card_y,
card_width,
card_height,
setting_name,
label,
getattr(self, setting_name),
index == self.start_menu_selection,
)
cards_bottom = cards_y + len(START_MENU_AUDIO_OPTIONS) * card_height + (len(START_MENU_AUDIO_OPTIONS) - 1) * card_gap
if compact_menu:
hint_y = min(cards_bottom + 8, panel_y + panel_height - 28)
render_engine.draw_text(
hint_text,
self._menu_font(target_height // 42),
("center", hint_y),
colors["muted"],
)
else:
hint_y = cards_bottom + 10
hint_height = max(34, target_height // 18)
hint_width = card_width
hint_x = card_x
render_engine.draw_rectangle(hint_x, hint_y, hint_width, hint_height, "start_menu", filling=colors["hint_fill"])
render_engine.draw_rectangle(hint_x, hint_y, hint_width, hint_height, "start_menu", outline=(170, 170, 170))
render_engine.draw_text(
hint_text,
self._menu_font(target_height // 40),
("center", hint_y + 10),
colors["muted"],
)
def render_start_menu(self):
colors = START_MENU_COLORS
render_engine = self.render_engine
target_width, target_height = render_engine.target_size
compact_menu = target_height <= 540
panel_x = max(48, target_width // 12)
panel_y = max(34, target_height // 18)
panel_width = target_width - panel_x * 2
panel_height = target_height - panel_y * 2
header_height = max(58, target_height // 10)
title_font = self._menu_font(target_height // 15)
body_font = self._menu_font(target_height // 28)
meta_font = self._menu_font(target_height // 30)
cta_font = self._menu_font(target_height // 22)
hint_font = self._menu_font(target_height // 32)
line_gap = 22 if compact_menu else max(26, target_height // 26)
render_engine.draw_rectangle(panel_x, panel_y, panel_width, panel_height, "start_menu", filling=colors["panel_fill"])
render_engine.draw_rectangle(panel_x, panel_y, panel_width, panel_height, "start_menu", outline=colors["panel_border"])
render_engine.draw_rectangle(panel_x, panel_y, panel_width, header_height, "start_menu", filling=colors["header_fill"])
render_engine.draw_text(
f"Welcome to Mice, {self.profile_integration.get_profile_name()}!",
title_font,
("center", panel_y + max(12, header_height // 5)),
colors["text"],
)
animation_frame, animation_size = self._current_start_menu_animation_frame()
animation_bottom = panel_y + header_height + 18
if animation_frame is not None:
max_animation_width = panel_width - (70 if compact_menu else 110)
display_width = min(animation_size[0], max_animation_width)
scale_factor = display_width / animation_size[0]
display_height = max(1, int(animation_size[1] * scale_factor))
animation_x = target_width // 2 - display_width // 2
animation_y = panel_y + header_height + (18 if compact_menu else 26)
render_engine.draw_image(
animation_x - render_engine.w_offset,
animation_y - render_engine.h_offset,
animation_frame,
"start_menu",
source_rect=(0, 0, animation_size[0], animation_size[1]),
dest_size=(display_width, display_height),
)
animation_bottom = animation_y + display_height
player_profile = self.profile_integration.current_profile
device_id = self.profile_integration.get_device_id()
subtitle_lines = ["A game by Matteo, because he was bored."]
if player_profile:
if compact_menu:
subtitle_lines.append(
f"Best: {player_profile['best_score']} | Games: {player_profile['games_played']}"
)
else:
subtitle_lines.append(f"Device: {device_id}")
subtitle_lines.append(
f"Best Score: {player_profile['best_score']} | Games: {player_profile['games_played']}"
)
elif compact_menu:
subtitle_lines.append(f"Guest profile | {device_id}")
else:
subtitle_lines.append(f"Device: {device_id}")
subtitle_lines.append("No profile loaded - playing as guest")
info_y = animation_bottom + (18 if compact_menu else 22)
for index, line in enumerate(subtitle_lines):
render_engine.draw_text(
line,
body_font if index == 0 else meta_font,
("center", info_y + line_gap * index),
colors["text"] if index == 0 else colors["muted"],
)
cta_y = info_y + line_gap * len(subtitle_lines) + 18
render_engine.draw_text(
"Press Return to start",
cta_font,
("center", cta_y),
colors["text"],
)
render_engine.draw_text(
"Esc quits M toggles audio",
hint_font,
("center", cta_y + line_gap + 8),
colors["muted"],
)
def render_pause_menu(self):
subtitle_lines = [
f"Level {self.current_level + 1} | Points: {self.points}",
f"Rats in maze: {self.count_rats()}",
]
self._render_audio_menu(
title="Pause",
subtitle_lines=subtitle_lines,
primary_action_text="Press Return to resume",
hint_text="Up/Down select Left/Right adjust Esc quits",
image_name="BMP_PAUSE" if "BMP_PAUSE" in self.assets else "BMP_WEWIN",
)
def _apply_volume_setting(self, setting_name, value):
clamped = max(0, min(100, int(value)))
setattr(self, setting_name, clamped)
if setting_name == "sound_volume":
self.render_engine.set_sound_volume(clamped)
else:
self.render_engine.set_music_volume(clamped)
self.profile_integration.set_setting(setting_name, clamped)
def menu_up(self):
if not self._can_adjust_audio_menu():
return
self.start_menu_selection = (self.start_menu_selection - 1) % len(START_MENU_AUDIO_OPTIONS)
def menu_down(self):
if not self._can_adjust_audio_menu():
return
self.start_menu_selection = (self.start_menu_selection + 1) % len(START_MENU_AUDIO_OPTIONS)
def _adjust_selected_volume(self, delta):
if not self._can_adjust_audio_menu():
return
setting_name, _ = START_MENU_AUDIO_OPTIONS[self.start_menu_selection]
current_value = getattr(self, setting_name)
self._apply_volume_setting(setting_name, current_value + delta)
def menu_left(self):
self._adjust_selected_volume(-VOLUME_STEP)
def menu_right(self):
self._adjust_selected_volume(VOLUME_STEP)
def update_background_music(self):
if self.game_status == "game" and not self.game_end[0]:
if self.current_level_music:
self.render_engine.play_music(self.current_level_music, loop=True)
return
if self.game_status == "start_menu" and not self.game_end[0]:
if self.menu_screen == "start":
self.render_engine.play_music(START_MENU_MUSIC, loop=True)
return
if self.menu_screen == "level_intro" and self.current_level_music:
self.render_engine.play_music(self.current_level_music, loop=True)
return
self.render_engine.pause_music()
def update_maze(self):
self.update_background_music()
if self.game_over():
return
if self.game_status == "paused":
self.render_pause_menu()
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:
self.render_start_menu()
return
self.render_engine.delete_tag("unit")
self.render_engine.delete_tag("effect")
self.render_engine.delete_tag("cave")
# 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.draw_pointer(self.pointer[0] * self.cell_size, self.pointer[1] * self.cell_size)
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()