2eccf504f5
- Add sdl2.create_overlay_texture() and draw_overlay_texture(alpha) for transparent full-map overlays built from sub-tile surface blits. - Add Graphics.regenerate_tunnel_cover() which builds an overlay of 4 random grass sub-tiles (20x20) for every internal tunnel cell, defined as a tunnel cell surrounded by occupied cells (wall or tunnel) on all four sides. Cells with at least one open side are handled by cave_foreground and skipped here. - Draw the tunnel cover in the game loop after top-layer units/effects but before cave_foreground and points, at 95% opacity (alpha=242) so the unit and effect passing underneath is just barely visible.
650 lines
27 KiB
Python
650 lines
27 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, state_machine, config
|
|
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
|
|
|
|
|
|
class MiceMaze:
|
|
|
|
# ==================== INITIALIZATION ====================
|
|
|
|
def _update_startup_loading(self, message, detail=None, progress=None):
|
|
if getattr(self, "startup_loading_active", False):
|
|
self.render_engine.show_loading_screen(message, detail=detail, progress=progress)
|
|
|
|
def __init__(self, maze_file, level_index=0):
|
|
self._setup_initial_state(maze_file, level_index)
|
|
self._setup_engine()
|
|
self._setup_components()
|
|
self._setup_game_assets()
|
|
self._setup_initial_units()
|
|
|
|
def _setup_initial_state(self, maze_file, level_index):
|
|
# Initialize user profile integration
|
|
self.profile_integration = UserProfileIntegration()
|
|
self.map_source = maze_file
|
|
self.map = maze.Map(maze_file, level_index=level_index)
|
|
self.current_level = self.map.level_index
|
|
self.total_levels = self.map.level_count
|
|
|
|
# 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.difficulty = config.DEFAULT_DIFFICULTY
|
|
self.initial_rat_count = config.BASE_INITIAL_RATS
|
|
self.rat_speed_multiplier = 1.0
|
|
self._apply_difficulty(self.profile_integration.get_setting('difficulty', config.DEFAULT_DIFFICULTY), persist=False)
|
|
|
|
self.cell_size = 40
|
|
self.full_screen = False
|
|
self.start_menu_selection = 0
|
|
self.points = 0
|
|
self.units = {}
|
|
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.background_texture = None
|
|
self.tunnel_cover_texture = None
|
|
self.combined_scores = None
|
|
|
|
def _setup_engine(self):
|
|
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=lambda action: self.controls.trigger(action))
|
|
self.render_engine.audio = self.audio
|
|
self.startup_loading_active = True
|
|
|
|
# Apply profile volumes
|
|
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)
|
|
|
|
def _setup_components(self):
|
|
self.scoring = scoring.Scoring(self)
|
|
self.unit_manager = unit_manager.UnitManager(self)
|
|
self.graphics = graphics.Graphics(self)
|
|
self.controls = controls.KeyBindings(self)
|
|
self.state_machine = state_machine.StateMachine(self)
|
|
self.collision_system = CollisionSystem(self.cell_size, self.map.width, self.map.height)
|
|
|
|
def _setup_game_assets(self):
|
|
self._update_startup_loading("Loading Mice!", detail="Applying player settings", progress=0.08)
|
|
self.controls.initialize_keybindings()
|
|
|
|
self._update_startup_loading("Loading configuration", detail="Reading bundled settings", progress=0.14)
|
|
self.configs = self.get_config()
|
|
|
|
self._update_startup_loading("Scanning music", detail="Building soundtrack list", progress=0.2)
|
|
self.available_music_tracks = self._load_available_music_tracks()
|
|
self.current_level_music = None
|
|
|
|
self._update_startup_loading("Loading graphics", detail="Preparing common assets", progress=0.26)
|
|
self.graphics.load_assets()
|
|
|
|
self._update_startup_loading("Loading start menu", detail="Preparing menu animation", progress=0.9)
|
|
self.start_menu_animation = self.render_engine.load_animation(config.START_MENU_ANIMATION)
|
|
self.start_menu_animation_started_at = time.monotonic()
|
|
|
|
def _setup_initial_units(self):
|
|
self._update_startup_loading("Starting", detail="Opening intro screen", progress=0.98)
|
|
self.render_engine.show_intro(bundle_path("assets", "Rat", "intro.png"))
|
|
self.startup_loading_active = False
|
|
|
|
self.pointer = (random.randint(1, self.map.width-2), random.randint(1, self.map.height-2))
|
|
self.graphics.scroll_cursor()
|
|
|
|
self.start_game()
|
|
|
|
def get_config(self):
|
|
configs = {}
|
|
conf_dir = bundle_path("conf")
|
|
for file in sorted(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 config.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(config.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 _normalize_difficulty(self, difficulty_key):
|
|
difficulty_key = config.DIFFICULTY_ALIASES.get(difficulty_key, difficulty_key)
|
|
if difficulty_key in config.DIFFICULTY_OPTIONS_BY_KEY:
|
|
return difficulty_key
|
|
return config.DEFAULT_DIFFICULTY
|
|
|
|
def _difficulty_config(self):
|
|
return config.DIFFICULTY_OPTIONS_BY_KEY[self.difficulty]
|
|
|
|
def _apply_difficulty(self, difficulty_key, persist=True):
|
|
normalized_difficulty = self._normalize_difficulty(difficulty_key)
|
|
difficulty_config = config.DIFFICULTY_OPTIONS_BY_KEY[normalized_difficulty]
|
|
self.difficulty = normalized_difficulty
|
|
self.initial_rat_count = max(
|
|
1,
|
|
int(round(config.BASE_INITIAL_RATS * difficulty_config["starting_rats_multiplier"])),
|
|
)
|
|
self.rat_speed_multiplier = difficulty_config["speed_multiplier"]
|
|
if persist:
|
|
self.profile_integration.set_setting("difficulty", normalized_difficulty)
|
|
|
|
def _can_adjust_start_difficulty(self):
|
|
if self.game_end[0]:
|
|
return False
|
|
return self.state_machine.current_state == GameState.START_MENU
|
|
|
|
def _cycle_difficulty(self, delta):
|
|
if not self._can_adjust_start_difficulty():
|
|
return
|
|
|
|
current_index = 0
|
|
for index, option in enumerate(config.DIFFICULTY_OPTIONS):
|
|
if option["key"] == self.difficulty:
|
|
current_index = index
|
|
break
|
|
|
|
next_index = (current_index + delta) % len(config.DIFFICULTY_OPTIONS)
|
|
self._apply_difficulty(config.DIFFICULTY_OPTIONS[next_index]["key"])
|
|
|
|
def _is_dat_campaign(self):
|
|
return self.map.source_path.suffix.lower() == ".dat"
|
|
|
|
def _is_last_dat_level(self):
|
|
return self._is_dat_campaign() and self.current_level >= self.total_levels - 1
|
|
|
|
def _normalize_target_level(self, level_index):
|
|
if self._is_dat_campaign():
|
|
return maze.normalize_level_index(level_index, self.total_levels)
|
|
return 0
|
|
|
|
def _record_run_result(self, completed):
|
|
if not self.run_recorded:
|
|
self.scoring.save_score()
|
|
self.profile_integration.update_game_stats(self.points, completed=completed)
|
|
self.run_recorded = True
|
|
self.combined_scores = self.profile_integration.get_device_leaderboard(5)
|
|
|
|
def activate_debug_run_complete_dialog(self, score=None):
|
|
if score is not None:
|
|
self.points = max(0, int(score))
|
|
self.current_level = max(0, self.total_levels - 1)
|
|
self.state_machine.transition_to(GameState.PAUSED)
|
|
self.game_end = (True, config.GAME_END_RUN_COMPLETE)
|
|
self.run_recorded = True
|
|
self.combined_scores = self.profile_integration.get_device_leaderboard(5)
|
|
print(
|
|
f"[debug] showing run-complete dialog: level={self.current_level + 1}/{self.total_levels} "
|
|
f"points={self.points}"
|
|
)
|
|
|
|
def start_game(self):
|
|
print(
|
|
f"[flow] start_game: level={self.current_level + 1} "
|
|
f"difficulty={self.difficulty} starting_rats={self.initial_rat_count} "
|
|
f"rat_speed={int(self.rat_speed_multiplier * 100)}%"
|
|
)
|
|
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.graphics.blood_layer_sprites.clear()
|
|
self.graphics.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.graphics.scroll_cursor()
|
|
|
|
for _ in range(self.initial_rat_count):
|
|
self.unit_manager.spawn_rat()
|
|
|
|
def load_level(self, level_index, preserve_points=True, show_menu=False, menu_screen=None):
|
|
target_level_index = self._normalize_target_level(level_index)
|
|
next_theme_index = target_level_index // 8 + 1
|
|
print(
|
|
f"[flow] load_level requested: target_level={target_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.map = maze.Map(self.map_source, level_index=target_level_index)
|
|
self.current_level = self.map.level_index
|
|
self.total_levels = self.map.level_count
|
|
self.current_level_music = self._resolve_level_music(self.current_level)
|
|
self._valid_positions = None
|
|
self.collision_system = CollisionSystem(
|
|
self.cell_size,
|
|
self.map.width,
|
|
self.map.height
|
|
)
|
|
|
|
if self.graphics.loaded_theme_index != next_theme_index:
|
|
print(
|
|
f"[flow] theme switch needed: loaded_theme={self.graphics.loaded_theme_index} "
|
|
f"-> next_theme={next_theme_index}"
|
|
)
|
|
self.graphics.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.graphics.blood_layer_sprites.clear()
|
|
self.graphics.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.graphics.scroll_cursor()
|
|
if not preserve_points:
|
|
self.points = 0
|
|
self.run_recorded = False
|
|
print("[flow] points reset for new run")
|
|
|
|
print(
|
|
f"[flow] spawning {self.initial_rat_count} rats for level={self.current_level + 1} "
|
|
f"difficulty={self.difficulty}",
|
|
flush=True,
|
|
)
|
|
for _ in range(self.initial_rat_count):
|
|
self.unit_manager.spawn_rat()
|
|
|
|
if show_menu:
|
|
if menu_screen == "start":
|
|
self.state_machine.transition_to(GameState.START_MENU)
|
|
else:
|
|
self.state_machine.transition_to(GameState.PLAYING)
|
|
print(f"[flow] level loaded into state: {self.state_machine.current_state} points={self.points}")
|
|
else:
|
|
self.state_machine.transition_to(GameState.PLAYING)
|
|
print(f"[flow] level loaded directly into PLAYING: points={self.points}")
|
|
|
|
|
|
def advance_level(self):
|
|
print(f"[flow] advance_level called from level={self.current_level + 1} points={self.points}")
|
|
if not self._is_dat_campaign():
|
|
self.load_level(self.current_level, preserve_points=True, show_menu=False)
|
|
return
|
|
|
|
if self._is_last_dat_level():
|
|
print("[flow] advance_level -> reached end of DAT campaign")
|
|
self.game_end = (True, config.GAME_END_RUN_COMPLETE)
|
|
self.state_machine.transition_to(GameState.VICTORY)
|
|
self._record_run_result(completed=True)
|
|
return
|
|
|
|
next_level = self.current_level + 1
|
|
print(f"[flow] advancing to level={next_level + 1}")
|
|
self.load_level(next_level, preserve_points=True, show_menu=False)
|
|
|
|
def reset_game(self):
|
|
print(
|
|
f"[flow] reset_game called: game_end={self.game_end} state={self.state_machine.current_state} "
|
|
f"menu_screen={self.menu_screen} points={self.points} level={self.current_level + 1}"
|
|
)
|
|
if self.game_end[0]:
|
|
if self.game_end[1] == config.GAME_END_LEVEL_CLEAR:
|
|
print("[flow] reset_game -> post-victory path")
|
|
self.advance_level()
|
|
elif self.game_end[1] == config.GAME_END_RUN_COMPLETE:
|
|
print("[flow] reset_game -> DAT run complete, returning to start menu")
|
|
self.start_menu_animation_started_at = time.monotonic()
|
|
self.load_level(0, preserve_points=False, show_menu=True, menu_screen="start")
|
|
else:
|
|
print("[flow] reset_game -> defeat, returning to start menu")
|
|
self.start_menu_animation_started_at = time.monotonic()
|
|
self.load_level(0, preserve_points=False, show_menu=True, menu_screen="start")
|
|
return
|
|
|
|
if self.state_machine.current_state == GameState.PAUSED:
|
|
print("[flow] reset_game -> unpausing current level")
|
|
self.state_machine.transition_to(GameState.PLAYING)
|
|
return
|
|
|
|
if self.state_machine.current_state == GameState.START_MENU:
|
|
print(f"[flow] reset_game -> entering gameplay")
|
|
self.state_machine.transition_to(GameState.PLAYING)
|
|
return
|
|
|
|
print("[flow] reset_game -> hard reload current level")
|
|
self.load_level(self.current_level, preserve_points=False, show_menu=False)
|
|
|
|
|
|
# ==================== GAME LOGIC ====================
|
|
|
|
def _can_adjust_audio_menu(self):
|
|
if self.game_end[0]:
|
|
return False
|
|
return self.state_machine.current_state == GameState.PAUSED
|
|
|
|
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 self._can_adjust_start_difficulty():
|
|
self._cycle_difficulty(-1)
|
|
return
|
|
if not self._can_adjust_audio_menu():
|
|
return
|
|
self.start_menu_selection = (self.start_menu_selection - 1) % len(config.START_MENU_AUDIO_OPTIONS)
|
|
|
|
def menu_down(self):
|
|
if self._can_adjust_start_difficulty():
|
|
self._cycle_difficulty(1)
|
|
return
|
|
if not self._can_adjust_audio_menu():
|
|
return
|
|
self.start_menu_selection = (self.start_menu_selection + 1) % len(config.START_MENU_AUDIO_OPTIONS)
|
|
|
|
def _adjust_selected_volume(self, delta):
|
|
if not self._can_adjust_audio_menu():
|
|
return
|
|
setting_name, _ = config.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):
|
|
if self._can_adjust_start_difficulty():
|
|
self._cycle_difficulty(-1)
|
|
return
|
|
self._adjust_selected_volume(-config.VOLUME_STEP)
|
|
|
|
def menu_right(self):
|
|
if self._can_adjust_start_difficulty():
|
|
self._cycle_difficulty(1)
|
|
return
|
|
self._adjust_selected_volume(config.VOLUME_STEP)
|
|
|
|
def update_background_music(self):
|
|
if self.game_end[0] and self.game_end[1] == config.GAME_END_RUN_COMPLETE:
|
|
self.render_engine.play_music(config.RUN_COMPLETE_MUSIC, loop=True)
|
|
return
|
|
if self.state_machine.current_state == GameState.PLAYING 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.state_machine.current_state == GameState.START_MENU and not self.game_end[0]:
|
|
self.render_engine.play_music(config.START_MENU_MUSIC, loop=True)
|
|
return
|
|
self.render_engine.pause_music()
|
|
|
|
def update_maze(self):
|
|
self.update_background_music()
|
|
|
|
# Handle non-playing states via state machine
|
|
if self.state_machine.current_state != GameState.PLAYING:
|
|
self.state_machine.update()
|
|
return
|
|
|
|
# Actual active gameplay logic
|
|
if self.game_end[0]:
|
|
if self.game_end[1] == config.GAME_END_DEFEAT:
|
|
self.state_machine.transition_to(GameState.GAME_OVER)
|
|
else:
|
|
self.state_machine.transition_to(GameState.VICTORY)
|
|
return
|
|
|
|
if self.game_over():
|
|
return
|
|
|
|
self.render_engine.delete_tag("unit")
|
|
self.render_engine.delete_tag("effect")
|
|
self.render_engine.delete_tag("cave")
|
|
|
|
# Clear collision system and legacy dictionaries
|
|
self.collision_system.clear()
|
|
self.unit_positions.clear()
|
|
self.unit_positions_before.clear()
|
|
|
|
# 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 = (float(x_pos), float(y_pos), float(x_pos + self.cell_size), float(y_pos + self.cell_size))
|
|
|
|
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)
|
|
|
|
# Pass 2: RESOLVE and DRAW
|
|
for unit in sorted_units:
|
|
unit.collisions()
|
|
|
|
# Draw mobile units first so ground effects appear on top
|
|
for unit in sorted_units:
|
|
if not getattr(unit, "draw_on_top", False):
|
|
unit.draw()
|
|
|
|
# Draw top-layer effects on top of mobile units
|
|
for unit in sorted_units:
|
|
if getattr(unit, "draw_on_top", False) and not getattr(unit, "draw_last", False):
|
|
unit.draw()
|
|
|
|
# Draw tunnel cover overlay above units/effects but below cave foreground and points
|
|
self.render_engine.draw_overlay_texture(self.graphics.tunnel_cover_texture, alpha=191)
|
|
|
|
# Draw cave foreground (tunnel entrances) above the tunnel cover
|
|
self.graphics.draw_cave_foreground()
|
|
|
|
# Draw foreground/last-layer units (points) on top of everything
|
|
for unit in sorted_units:
|
|
if getattr(unit, "draw_last", False):
|
|
unit.draw()
|
|
|
|
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.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)
|
|
|
|
def run(self):
|
|
self.render_engine.mainloop(update=self.update_maze, bg_update=self.graphics.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 self.game_end[1] == config.GAME_END_DEFEAT:
|
|
self.render_engine.dialog(
|
|
"Game Over: Mice are too many!",
|
|
image=self.graphics.assets.get("lose", self.graphics.assets["BMP_WEWIN"]),
|
|
subtitle=f"Reached level: {self.current_level + 1}\nPress Return to go back to the start menu",
|
|
scores=self.combined_scores,
|
|
)
|
|
elif self.game_end[1] == config.GAME_END_RUN_COMPLETE:
|
|
self.render_engine.dialog(
|
|
"THE END",
|
|
image=self.graphics.assets.get("end", self.graphics.assets["BMP_WEWIN"]),
|
|
current_score=self.points,
|
|
style="run_complete",
|
|
)
|
|
else:
|
|
self.render_engine.dialog(
|
|
f"Level {self.current_level + 1} Clear! Points: {self.points}",
|
|
image=self.graphics.assets.get("clear", self.graphics.assets["BMP_WEWIN"]),
|
|
subtitle="Press Return for the next level",
|
|
scores=self.combined_scores
|
|
)
|
|
|
|
|
|
return True
|
|
count_rats = self.unit_manager.count_rats()
|
|
if count_rats > 200:
|
|
self.render_engine.stop_sound()
|
|
self.render_engine.play_sound("WEWIN.WAV")
|
|
self.game_end = (True, config.GAME_END_DEFEAT)
|
|
self.state_machine.transition_to(GameState.GAME_OVER)
|
|
print(f"[flow] defeat reached: rats={count_rats} points={self.points} level={self.current_level + 1}")
|
|
|
|
self._record_run_result(completed=False)
|
|
|
|
return True
|
|
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")
|
|
|
|
if self._is_last_dat_level():
|
|
self.render_engine.play_sound("WELLDONE.WAV", tag="effects")
|
|
self.game_end = (True, config.GAME_END_RUN_COMPLETE)
|
|
self.state_machine.transition_to(GameState.VICTORY)
|
|
self._record_run_result(completed=True)
|
|
print(f"[flow] final DAT victory reached: points={self.points} level={self.current_level + 1}")
|
|
else:
|
|
self.game_end = (True, config.GAME_END_LEVEL_CLEAR)
|
|
self.state_machine.transition_to(GameState.VICTORY)
|
|
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():
|
|
def parse_debug_score(value):
|
|
try:
|
|
parsed = float(str(value).strip())
|
|
except ValueError as exc:
|
|
raise argparse.ArgumentTypeError(f"invalid int value: {value!r}") from exc
|
|
if not parsed.is_integer():
|
|
raise argparse.ArgumentTypeError(f"invalid int value: {value!r}")
|
|
return int(parsed)
|
|
|
|
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)")
|
|
parser.add_argument(
|
|
"--debug-run-complete-dialog",
|
|
action="store_true",
|
|
help="Start directly on the final run-complete dialog without saving a fake high score",
|
|
)
|
|
parser.add_argument(
|
|
"--debug-final-score",
|
|
type=parse_debug_score,
|
|
default=1125,
|
|
help="Score shown by --debug-run-complete-dialog (default: 1125)",
|
|
)
|
|
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)
|
|
if args.debug_run_complete_dialog:
|
|
solver.activate_debug_run_complete_dialog(score=args.debug_final_score)
|
|
solver.run()
|