You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
419 lines
16 KiB
419 lines
16 KiB
#!/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() |
|
|
|
|