- Added BMP_1_EXPLOSION_DOWN.png to original and preview directories. - Added BMP_1_EXPLOSION_LEFT.png to original and preview directories. - Added BMP_1_EXPLOSION_RIGHT.png to original and preview directories. - Added BMP_1_EXPLOSION_UP.png to original and preview directories. These assets are part of the explosion animation for the game, enhancing visual effects during gameplay.
420 lines
16 KiB
Python
420 lines
16 KiB
Python
#!/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")
|
|
|
|
# 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)
|
|
|
|
self.draw_cave_foreground()
|
|
self.render_engine.draw_pointer(self.pointer[0] * self.cell_size, self.pointer[1] * self.cell_size)
|
|
|
|
# Fourth pass: check collisions and draw
|
|
for unit in self.units.copy().values():
|
|
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 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()
|
|
|