Add non-regression test states and Bluetooth diagnostic script
- Introduced a new JSON file containing non-regression test states with detailed unit information, including positions, ages, and movement directions across multiple frames. - Added a shell script for Bluetooth diagnostics that checks system information, Bluetooth binaries, running processes, D-Bus status, Bluetooth controller details, and audio stack status, providing a comprehensive overview for troubleshooting.
|
Before Width: | Height: | Size: 614 KiB After Width: | Height: | Size: 129 KiB |
@@ -0,0 +1,73 @@
|
|||||||
|
# Game constants and configuration
|
||||||
|
|
||||||
|
LEVEL_MUSIC_CONFIG = "level_music"
|
||||||
|
START_MENU_MUSIC = "High_Score_Garden.mp3"
|
||||||
|
RUN_COMPLETE_MUSIC = "Sunset_At_Pixel_Gardens.mp3"
|
||||||
|
START_MENU_ANIMATION = "anim/start_mice.gif"
|
||||||
|
SUPPORTED_MUSIC_EXTENSIONS = {".mp3", ".ogg", ".wav"}
|
||||||
|
BASE_INITIAL_RATS = 5
|
||||||
|
DEFAULT_DIFFICULTY = "easy"
|
||||||
|
DIFFICULTY_ALIASES = {
|
||||||
|
"medium": "normal",
|
||||||
|
"normale": "normal",
|
||||||
|
}
|
||||||
|
START_MENU_AUDIO_OPTIONS = (
|
||||||
|
("sound_volume", "Suono"),
|
||||||
|
("music_volume", "Musica"),
|
||||||
|
)
|
||||||
|
VOLUME_STEP = 5
|
||||||
|
|
||||||
|
GAME_END_LEVEL_CLEAR = "level_clear"
|
||||||
|
GAME_END_DEFEAT = "defeat"
|
||||||
|
GAME_END_RUN_COMPLETE = "run_complete"
|
||||||
|
|
||||||
|
DIFFICULTY_OPTIONS = (
|
||||||
|
{
|
||||||
|
"key": "easy",
|
||||||
|
"label": "Easy",
|
||||||
|
"starting_rats_multiplier": 1,
|
||||||
|
"speed_multiplier": 1.0,
|
||||||
|
"fill": (235, 246, 234),
|
||||||
|
"accent": (88, 148, 82),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "normal",
|
||||||
|
"label": "Normal",
|
||||||
|
"starting_rats_multiplier": 2,
|
||||||
|
"speed_multiplier": 1.5,
|
||||||
|
"fill": (252, 242, 223),
|
||||||
|
"accent": (204, 146, 44),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "hard",
|
||||||
|
"label": "Hard",
|
||||||
|
"starting_rats_multiplier": 3,
|
||||||
|
"speed_multiplier": 2.0,
|
||||||
|
"fill": (251, 229, 229),
|
||||||
|
"accent": (188, 68, 68),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
DIFFICULTY_OPTIONS_BY_KEY = {option["key"]: option for option in DIFFICULTY_OPTIONS}
|
||||||
|
|
||||||
|
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),
|
||||||
|
},
|
||||||
|
}
|
||||||
@@ -196,8 +196,12 @@ def resolve_keybindings(preferred_profile=None, preferred_file=None, render_engi
|
|||||||
|
|
||||||
|
|
||||||
class KeyBindings:
|
class KeyBindings:
|
||||||
|
def __init__(self, game):
|
||||||
|
self.game = game
|
||||||
|
self.bindings = {}
|
||||||
|
|
||||||
def _binding_sections_for_action(self):
|
def _binding_sections_for_action(self):
|
||||||
game_end_active, game_end_reason = getattr(self, "game_end", (False, None))
|
game_end_active, game_end_reason = getattr(self.game, "game_end", (False, None))
|
||||||
if game_end_active:
|
if game_end_active:
|
||||||
if game_end_reason == "level_clear":
|
if game_end_reason == "level_clear":
|
||||||
return ["keybinding_level_clear", "keybinding_paused"]
|
return ["keybinding_level_clear", "keybinding_paused"]
|
||||||
@@ -207,12 +211,12 @@ class KeyBindings:
|
|||||||
return ["keybinding_run_complete", "keybinding_paused"]
|
return ["keybinding_run_complete", "keybinding_paused"]
|
||||||
return ["keybinding_paused"]
|
return ["keybinding_paused"]
|
||||||
|
|
||||||
if getattr(self, "game_status", None) == "start_menu":
|
if getattr(self.game, "game_status", None) == "start_menu":
|
||||||
if getattr(self, "menu_screen", None) == "level_intro":
|
if getattr(self.game, "menu_screen", None) == "level_intro":
|
||||||
return ["keybinding_level_intro", "keybinding_start_menu"]
|
return ["keybinding_level_intro", "keybinding_start_menu"]
|
||||||
return ["keybinding_start_menu"]
|
return ["keybinding_start_menu"]
|
||||||
|
|
||||||
status = getattr(self, "game_status", None)
|
status = getattr(self.game, "game_status", None)
|
||||||
if status:
|
if status:
|
||||||
return [f"keybinding_{status}"]
|
return [f"keybinding_{status}"]
|
||||||
|
|
||||||
@@ -222,14 +226,14 @@ class KeyBindings:
|
|||||||
preferred_profile = None
|
preferred_profile = None
|
||||||
preferred_file = None
|
preferred_file = None
|
||||||
|
|
||||||
if hasattr(self, "profile_integration") and self.profile_integration:
|
if hasattr(self.game, "profile_integration") and self.game.profile_integration:
|
||||||
preferred_profile = self.profile_integration.get_setting("keybindings_profile")
|
preferred_profile = self.game.profile_integration.get_setting("keybindings_profile")
|
||||||
preferred_file = self.profile_integration.get_setting("keybindings_file")
|
preferred_file = self.game.profile_integration.get_setting("keybindings_file")
|
||||||
|
|
||||||
bindings, source_path, profile_name, reason = resolve_keybindings(
|
bindings, source_path, profile_name, reason = resolve_keybindings(
|
||||||
preferred_profile=preferred_profile,
|
preferred_profile=preferred_profile,
|
||||||
preferred_file=preferred_file,
|
preferred_file=preferred_file,
|
||||||
render_engine=getattr(self, "render_engine", None),
|
render_engine=getattr(self.game, "render_engine", None),
|
||||||
)
|
)
|
||||||
|
|
||||||
self.bindings = self._validate_bindings(bindings, source_path)
|
self.bindings = self._validate_bindings(bindings, source_path)
|
||||||
@@ -255,7 +259,8 @@ class KeyBindings:
|
|||||||
continue
|
continue
|
||||||
|
|
||||||
method_name = value.split("|", 1)[0]
|
method_name = value.split("|", 1)[0]
|
||||||
method = getattr(self, method_name, None)
|
# Check both self (KeyBindings) and self.game (MiceMaze)
|
||||||
|
method = getattr(self, method_name, getattr(self.game, method_name, None))
|
||||||
if callable(method):
|
if callable(method):
|
||||||
validated[section_name][action] = value
|
validated[section_name][action] = value
|
||||||
continue
|
continue
|
||||||
@@ -272,7 +277,7 @@ class KeyBindings:
|
|||||||
return validated
|
return validated
|
||||||
|
|
||||||
def trigger(self, action):
|
def trigger(self, action):
|
||||||
if not hasattr(self, "bindings"):
|
if not self.bindings:
|
||||||
self.initialize_keybindings()
|
self.initialize_keybindings()
|
||||||
|
|
||||||
value = None
|
value = None
|
||||||
@@ -286,77 +291,81 @@ class KeyBindings:
|
|||||||
|
|
||||||
if "|" in value:
|
if "|" in value:
|
||||||
method_name, *args = value.split("|")
|
method_name, *args = value.split("|")
|
||||||
method = getattr(self, method_name, None)
|
method = getattr(self, method_name, getattr(self.game, method_name, None))
|
||||||
if callable(method):
|
if callable(method):
|
||||||
method(*args)
|
method(*args)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
method = getattr(self, value, None)
|
method = getattr(self, value, getattr(self.game, value, None))
|
||||||
if callable(method):
|
if callable(method):
|
||||||
method()
|
method()
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
def spawn_rat(self):
|
||||||
|
self.game.unit_manager.spawn_rat()
|
||||||
|
|
||||||
def spawn_new_bomb(self):
|
def spawn_new_bomb(self):
|
||||||
self.spawn_bomb(self.pointer)
|
self.game.unit_manager.spawn_bomb(self.game.pointer)
|
||||||
|
|
||||||
def spawn_new_mine(self):
|
def spawn_new_mine(self):
|
||||||
self.spawn_mine(self.pointer)
|
self.game.unit_manager.spawn_mine(self.game.pointer)
|
||||||
|
|
||||||
def spawn_new_nuclear_bomb(self):
|
def spawn_new_nuclear_bomb(self):
|
||||||
self.spawn_nuclear_bomb(self.pointer)
|
self.game.unit_manager.spawn_nuclear_bomb(self.game.pointer)
|
||||||
|
|
||||||
def spawn_new_gas(self):
|
def spawn_new_gas(self):
|
||||||
self.spawn_gas()
|
self.game.unit_manager.spawn_gas()
|
||||||
|
|
||||||
def toggle_audio(self):
|
def toggle_audio(self):
|
||||||
self.render_engine.audio = not self.render_engine.audio
|
self.game.render_engine.audio = not self.game.render_engine.audio
|
||||||
self.audio = self.render_engine.audio
|
self.game.audio = self.game.render_engine.audio
|
||||||
if hasattr(self, "profile_integration") and self.profile_integration:
|
if hasattr(self.game, "profile_integration") and self.game.profile_integration:
|
||||||
self.profile_integration.set_setting("sound_enabled", self.audio)
|
self.game.profile_integration.set_setting("sound_enabled", self.game.audio)
|
||||||
if not self.render_engine.audio:
|
if not self.game.render_engine.audio:
|
||||||
self.render_engine.stop_sound()
|
self.game.render_engine.stop_sound()
|
||||||
|
|
||||||
def toggle_pause(self):
|
def toggle_pause(self):
|
||||||
if getattr(self, "game_end", (False, None))[0]:
|
if getattr(self.game, "game_end", (False, None))[0]:
|
||||||
return
|
return
|
||||||
|
|
||||||
if self.game_status == "game":
|
if self.game.state_machine.current_state == state_machine.GameState.PLAYING:
|
||||||
self.game_status = "paused"
|
self.game.state_machine.transition_to(state_machine.GameState.PAUSED)
|
||||||
return
|
return
|
||||||
|
|
||||||
if self.game_status == "paused":
|
if self.game.state_machine.current_state == state_machine.GameState.PAUSED:
|
||||||
self.game_status = "game"
|
self.game.state_machine.transition_to(state_machine.GameState.PLAYING)
|
||||||
return
|
return
|
||||||
|
|
||||||
if self.game_status == "start_menu" and getattr(self, "menu_screen", None) == "start":
|
if self.game.state_machine.current_state == state_machine.GameState.START_MENU:
|
||||||
self.reset_game()
|
self.game.reset_game()
|
||||||
|
|
||||||
def toggle_full_screen(self):
|
def toggle_full_screen(self):
|
||||||
self.full_screen = not self.full_screen
|
self.game.full_screen = not self.game.full_screen
|
||||||
self.render_engine.full_screen(self.full_screen)
|
self.game.render_engine.full_screen(self.game.full_screen)
|
||||||
|
|
||||||
def quit_game(self):
|
def quit_game(self):
|
||||||
self.render_engine.close()
|
self.game.render_engine.close()
|
||||||
|
|
||||||
def start_scrolling(self, direction):
|
def start_scrolling(self, direction):
|
||||||
self.scrolling_direction = direction
|
self.game.scrolling_direction = direction
|
||||||
if not self.scrolling:
|
if not self.game.scrolling:
|
||||||
self.scrolling = 1
|
self.game.scrolling = 1
|
||||||
|
|
||||||
def stop_scrolling(self):
|
def stop_scrolling(self):
|
||||||
self.scrolling = 0
|
self.game.scrolling = 0
|
||||||
|
|
||||||
def scroll(self):
|
def scroll(self):
|
||||||
if self.scrolling:
|
if self.game.scrolling:
|
||||||
if not self.scrolling % 5:
|
if not self.game.scrolling % 5:
|
||||||
if self.scrolling_direction == "Up":
|
if self.game.scrolling_direction == "Up":
|
||||||
self.scroll_cursor(y=-1)
|
self.game.graphics.scroll_cursor(y=-1)
|
||||||
elif self.scrolling_direction == "Down":
|
elif self.game.scrolling_direction == "Down":
|
||||||
self.scroll_cursor(y=1)
|
self.game.graphics.scroll_cursor(y=1)
|
||||||
elif self.scrolling_direction == "Left":
|
elif self.game.scrolling_direction == "Left":
|
||||||
self.scroll_cursor(x=-1)
|
self.game.graphics.scroll_cursor(x=-1)
|
||||||
elif self.scrolling_direction == "Right":
|
elif self.game.scrolling_direction == "Right":
|
||||||
self.scroll_cursor(x=1)
|
self.game.graphics.scroll_cursor(x=1)
|
||||||
self.scrolling += 1
|
self.game.scrolling += 1
|
||||||
|
|
||||||
def axis_scroll(self, x, y):
|
def axis_scroll(self, x, y):
|
||||||
self.scroll_cursor(1 if x > 0 else -1, 1 if y > 0 else -1)
|
self.game.graphics.scroll_cursor(1 if x > 0 else -1, 1 if y > 0 else -1)
|
||||||
@@ -1,16 +1,21 @@
|
|||||||
import os
|
import os
|
||||||
import random
|
import random
|
||||||
|
import time
|
||||||
|
|
||||||
from engine import maze
|
from engine import maze, config
|
||||||
from engine.collision_system import CollisionLayer
|
from engine.collision_system import CollisionLayer
|
||||||
from runtime_paths import bundle_path
|
from runtime_paths import bundle_path
|
||||||
|
|
||||||
class Graphics():
|
class Graphics:
|
||||||
|
def __init__(self, game):
|
||||||
|
self.game = game
|
||||||
|
self.loaded_theme_index = None
|
||||||
|
|
||||||
def load_assets(self):
|
def load_assets(self):
|
||||||
theme_index = self.get_theme_index()
|
theme_index = self.get_theme_index()
|
||||||
print(f"[gfx] load_assets requested: level={self.current_level + 1} theme={theme_index}")
|
print(f"[gfx] load_assets requested: level={self.game.current_level + 1} theme={theme_index}")
|
||||||
if getattr(self, "startup_loading_active", False):
|
if getattr(self.game, "startup_loading_active", False):
|
||||||
self._update_startup_loading(
|
self.game._update_startup_loading(
|
||||||
"Loading graphics",
|
"Loading graphics",
|
||||||
detail=f"Preparing theme {theme_index}",
|
detail=f"Preparing theme {theme_index}",
|
||||||
progress=0.3,
|
progress=0.3,
|
||||||
@@ -24,8 +29,8 @@ class Graphics():
|
|||||||
|
|
||||||
if not getattr(self, "common_assets_loaded", False):
|
if not getattr(self, "common_assets_loaded", False):
|
||||||
print("Loading graphics assets...")
|
print("Loading graphics assets...")
|
||||||
if getattr(self, "startup_loading_active", False):
|
if getattr(self.game, "startup_loading_active", False):
|
||||||
self._update_startup_loading(
|
self.game._update_startup_loading(
|
||||||
"Loading graphics",
|
"Loading graphics",
|
||||||
detail="Decoding sprites and tiles",
|
detail="Decoding sprites and tiles",
|
||||||
progress=0.4,
|
progress=0.4,
|
||||||
@@ -41,11 +46,11 @@ class Graphics():
|
|||||||
self.rat_assets_textures[sex] = {}
|
self.rat_assets_textures[sex] = {}
|
||||||
self.rat_image_sizes[sex] = {}
|
self.rat_image_sizes[sex] = {}
|
||||||
for direction in ["UP", "DOWN", "LEFT", "RIGHT"]:
|
for direction in ["UP", "DOWN", "LEFT", "RIGHT"]:
|
||||||
self.rat_assets[sex][direction] = self.render_engine.load_image(
|
self.rat_assets[sex][direction] = self.game.render_engine.load_image(
|
||||||
f"Rat/BMP_{sex}_{direction}.png",
|
f"Rat/BMP_{sex}_{direction}.png",
|
||||||
transparent_color=((125, 125, 125), (128, 128, 128)),
|
transparent_color=((125, 125, 125), (128, 128, 128)),
|
||||||
)
|
)
|
||||||
texture = self.render_engine.load_image(
|
texture = self.game.render_engine.load_image(
|
||||||
f"Rat/BMP_{sex}_{direction}.png",
|
f"Rat/BMP_{sex}_{direction}.png",
|
||||||
transparent_color=((125, 125, 125), (128, 128, 128)),
|
transparent_color=((125, 125, 125), (128, 128, 128)),
|
||||||
surface=False,
|
surface=False,
|
||||||
@@ -54,30 +59,36 @@ class Graphics():
|
|||||||
self.rat_image_sizes[sex][direction] = texture.size
|
self.rat_image_sizes[sex][direction] = texture.size
|
||||||
|
|
||||||
for n in range(5):
|
for n in range(5):
|
||||||
self.bomb_assets[n] = self.render_engine.load_image(
|
self.bomb_assets[n] = self.game.render_engine.load_image(
|
||||||
f"Rat/BMP_BOMB{n}.png",
|
f"Rat/BMP_BOMB{n}.png",
|
||||||
transparent_color=((125, 125, 125), (128, 128, 128)),
|
transparent_color=((125, 125, 125), (128, 128, 128)),
|
||||||
)
|
)
|
||||||
|
|
||||||
rat_asset_dir = bundle_path("assets", "Rat")
|
rat_asset_dir = bundle_path("assets", "Rat")
|
||||||
for file in os.listdir(rat_asset_dir):
|
for file in sorted(os.listdir(rat_asset_dir)):
|
||||||
if file.endswith(".png"):
|
if file.endswith(".png") and not file.startswith("."):
|
||||||
self.assets[file[:-4]] = self.render_engine.load_image(
|
# Check if it's one of our expected BMP files or other known assets
|
||||||
f"Rat/{file}",
|
# to avoid loading temporary or irrelevant PNGs
|
||||||
transparent_color=((125, 125, 125), (128, 128, 128)),
|
file_key = file[:-4]
|
||||||
)
|
try:
|
||||||
|
self.assets[file_key] = self.game.render_engine.load_image(
|
||||||
|
f"Rat/{file}",
|
||||||
|
transparent_color=((125, 125, 125), (128, 128, 128)),
|
||||||
|
)
|
||||||
|
except (FileNotFoundError, IOError) as e:
|
||||||
|
print(f"Warning: Could not load asset {file}: {e}")
|
||||||
|
|
||||||
print("Pre-generating blood stain pool...")
|
print("Pre-generating blood stain pool...")
|
||||||
if getattr(self, "startup_loading_active", False):
|
if getattr(self.game, "startup_loading_active", False):
|
||||||
self._update_startup_loading(
|
self.game._update_startup_loading(
|
||||||
"Loading graphics",
|
"Loading graphics",
|
||||||
detail="Generating blood pool",
|
detail="Generating blood pool",
|
||||||
progress=0.58,
|
progress=0.58,
|
||||||
)
|
)
|
||||||
self.blood_stain_textures = []
|
self.blood_stain_textures = []
|
||||||
for _ in range(10):
|
for _ in range(10):
|
||||||
blood_surface = self.render_engine.generate_blood_surface()
|
blood_surface = self.game.render_engine.generate_blood_surface()
|
||||||
blood_texture = self.render_engine.draw_blood_surface(blood_surface, (0, 0))
|
blood_texture = self.game.render_engine.draw_blood_surface(blood_surface, (0, 0))
|
||||||
if blood_texture:
|
if blood_texture:
|
||||||
self.blood_stain_textures.append(blood_texture)
|
self.blood_stain_textures.append(blood_texture)
|
||||||
|
|
||||||
@@ -88,33 +99,33 @@ class Graphics():
|
|||||||
|
|
||||||
if theme_index not in self.theme_assets_cache:
|
if theme_index not in self.theme_assets_cache:
|
||||||
print(f"Loading theme assets {theme_index}...")
|
print(f"Loading theme assets {theme_index}...")
|
||||||
if getattr(self, "startup_loading_active", False):
|
if getattr(self.game, "startup_loading_active", False):
|
||||||
self._update_startup_loading(
|
self.game._update_startup_loading(
|
||||||
"Loading graphics",
|
"Loading graphics",
|
||||||
detail=f"Loading theme {theme_index} art",
|
detail=f"Loading theme {theme_index} art",
|
||||||
progress=0.74,
|
progress=0.74,
|
||||||
)
|
)
|
||||||
self.theme_assets_cache[theme_index] = {
|
self.theme_assets_cache[theme_index] = {
|
||||||
"floor_tile": self.render_engine.create_color_surface((128, 128, 128)),
|
"floor_tile": self.game.render_engine.create_color_surface((128, 128, 128)),
|
||||||
"tunnel": self.render_engine.load_image("Rat/BMP_TUNNEL.png"),
|
"tunnel": self.game.render_engine.load_image("Rat/BMP_TUNNEL.png"),
|
||||||
"grasses": [
|
"grasses": [
|
||||||
self.render_engine.load_image(f"Rat/BMP_{theme_index}_GRASS_{i+1}.png", surface=True)
|
self.game.render_engine.load_image(f"Rat/BMP_{theme_index}_GRASS_{i+1}.png", surface=True)
|
||||||
for i in range(4)
|
for i in range(4)
|
||||||
],
|
],
|
||||||
"grass_textures": [
|
"grass_textures": [
|
||||||
self.render_engine.load_image(f"Rat/BMP_{theme_index}_GRASS_{i+1}.png")
|
self.game.render_engine.load_image(f"Rat/BMP_{theme_index}_GRASS_{i+1}.png")
|
||||||
for i in range(4)
|
for i in range(4)
|
||||||
],
|
],
|
||||||
"flowers": [
|
"flowers": [
|
||||||
self.render_engine.load_image(f"Rat/BMP_{theme_index}_FLOWER_{i+1}.png", surface=True)
|
self.game.render_engine.load_image(f"Rat/BMP_{theme_index}_FLOWER_{i+1}.png", surface=True)
|
||||||
for i in range(4)
|
for i in range(4)
|
||||||
],
|
],
|
||||||
"flower_textures": [
|
"flower_textures": [
|
||||||
self.render_engine.load_image(f"Rat/BMP_{theme_index}_FLOWER_{i+1}.png")
|
self.game.render_engine.load_image(f"Rat/BMP_{theme_index}_FLOWER_{i+1}.png")
|
||||||
for i in range(4)
|
for i in range(4)
|
||||||
],
|
],
|
||||||
"caves": {
|
"caves": {
|
||||||
direction: self.render_engine.load_image(
|
direction: self.game.render_engine.load_image(
|
||||||
f"Rat/BMP_{theme_index}_CAVE_{direction}.png",
|
f"Rat/BMP_{theme_index}_CAVE_{direction}.png",
|
||||||
transparent_color=((125, 125, 125), (128, 128, 128)),
|
transparent_color=((125, 125, 125), (128, 128, 128)),
|
||||||
surface=False,
|
surface=False,
|
||||||
@@ -122,7 +133,7 @@ class Graphics():
|
|||||||
for direction in ["UP", "DOWN", "LEFT", "RIGHT"]
|
for direction in ["UP", "DOWN", "LEFT", "RIGHT"]
|
||||||
},
|
},
|
||||||
"explosions": {
|
"explosions": {
|
||||||
direction: self.render_engine.load_image(
|
direction: self.game.render_engine.load_image(
|
||||||
f"Rat/BMP_{theme_index}_EXPLOSION_{direction}.png",
|
f"Rat/BMP_{theme_index}_EXPLOSION_{direction}.png",
|
||||||
transparent_color=((125, 125, 125), (128, 128, 128)),
|
transparent_color=((125, 125, 125), (128, 128, 128)),
|
||||||
surface=False,
|
surface=False,
|
||||||
@@ -130,15 +141,15 @@ class Graphics():
|
|||||||
for direction in ["UP", "DOWN", "LEFT", "RIGHT"]
|
for direction in ["UP", "DOWN", "LEFT", "RIGHT"]
|
||||||
},
|
},
|
||||||
"edges": {
|
"edges": {
|
||||||
direction: self.render_engine.load_image(f"Rat/BMP_{theme_index}_{direction}.png", surface=True)
|
direction: self.game.render_engine.load_image(f"Rat/BMP_{theme_index}_{direction}.png", surface=True)
|
||||||
for direction in ["N", "S", "E", "W"]
|
for direction in ["N", "S", "E", "W"]
|
||||||
},
|
},
|
||||||
"corners": {
|
"corners": {
|
||||||
direction: self.render_engine.load_image(f"Rat/BMP_{theme_index}_{direction}.png", surface=True)
|
direction: self.game.render_engine.load_image(f"Rat/BMP_{theme_index}_{direction}.png", surface=True)
|
||||||
for direction in ["NE", "NW", "SE", "SW"]
|
for direction in ["NE", "NW", "SE", "SW"]
|
||||||
},
|
},
|
||||||
"inner_corners": {
|
"inner_corners": {
|
||||||
direction: self.render_engine.load_image(f"Rat/BMP_{theme_index}_{direction}.png", surface=True)
|
direction: self.game.render_engine.load_image(f"Rat/BMP_{theme_index}_{direction}.png", surface=True)
|
||||||
for direction in ["EN", "ES", "WN", "WS"]
|
for direction in ["EN", "ES", "WN", "WS"]
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@@ -146,8 +157,8 @@ class Graphics():
|
|||||||
else:
|
else:
|
||||||
print(f"[gfx] theme cache hit -> reusing theme {theme_index}")
|
print(f"[gfx] theme cache hit -> reusing theme {theme_index}")
|
||||||
|
|
||||||
if getattr(self, "startup_loading_active", False):
|
if getattr(self.game, "startup_loading_active", False):
|
||||||
self._update_startup_loading(
|
self.game._update_startup_loading(
|
||||||
"Loading graphics",
|
"Loading graphics",
|
||||||
detail="Finishing render setup",
|
detail="Finishing render setup",
|
||||||
progress=0.84,
|
progress=0.84,
|
||||||
@@ -168,27 +179,27 @@ class Graphics():
|
|||||||
self.inner_corners = theme_assets["inner_corners"]
|
self.inner_corners = theme_assets["inner_corners"]
|
||||||
|
|
||||||
def get_theme_index(self):
|
def get_theme_index(self):
|
||||||
return self.current_level % 32 // 8 + 1
|
return self.game.current_level % 32 // 8 + 1
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
# ==================== RENDERING ====================
|
# ==================== RENDERING ====================
|
||||||
|
|
||||||
def draw_maze(self):
|
def draw_maze(self):
|
||||||
if self.background_texture is None:
|
if self.game.background_texture is None:
|
||||||
print(f"[gfx] generating background texture for level={self.current_level + 1} theme={self.loaded_theme_index}")
|
print(f"[gfx] generating background texture for level={self.game.current_level + 1} theme={self.loaded_theme_index}")
|
||||||
self.regenerate_background()
|
self.regenerate_background()
|
||||||
self.render_engine.draw_background(self.background_texture)
|
self.game.render_engine.draw_background(self.game.background_texture)
|
||||||
|
|
||||||
# Draw blood layer as sprites (optimized - no background regeneration)
|
# Draw blood layer as sprites (optimized - no background regeneration)
|
||||||
self.draw_blood_layer()
|
self.draw_blood_layer()
|
||||||
|
|
||||||
def draw_cave_foreground(self):
|
def draw_cave_foreground(self):
|
||||||
active_cave_explosions = {}
|
active_cave_explosions = {}
|
||||||
for unit in self.units.values():
|
for unit in self.game.units.values():
|
||||||
if unit.collision_layer != CollisionLayer.EXPLOSION:
|
if unit.collision_layer != CollisionLayer.EXPLOSION:
|
||||||
continue
|
continue
|
||||||
if not self.map.is_tunnel(*unit.position):
|
if not self.game.map.is_tunnel(*unit.position):
|
||||||
continue
|
continue
|
||||||
active_cave_explosions[unit.position] = getattr(unit, "cave_direction", None)
|
active_cave_explosions[unit.position] = getattr(unit, "cave_direction", None)
|
||||||
|
|
||||||
@@ -196,30 +207,30 @@ class Graphics():
|
|||||||
if (cell_x, cell_y) in active_cave_explosions:
|
if (cell_x, cell_y) in active_cave_explosions:
|
||||||
explosion_direction = active_cave_explosions[(cell_x, cell_y)] or direction
|
explosion_direction = active_cave_explosions[(cell_x, cell_y)] or direction
|
||||||
surface = self.explosions.get(explosion_direction, surface)
|
surface = self.explosions.get(explosion_direction, surface)
|
||||||
self.render_engine.draw_image(x, y, surface, anchor="nw", tag="cave")
|
self.game.render_engine.draw_image(x, y, surface, anchor="nw", tag="cave")
|
||||||
|
|
||||||
def draw_blood_layer(self):
|
def draw_blood_layer(self):
|
||||||
"""Draw all blood stains as sprites overlay (optimized)"""
|
"""Draw all blood stains as sprites overlay (optimized)"""
|
||||||
for blood_texture, x, y in self.blood_layer_sprites:
|
for blood_texture, x, y in self.blood_layer_sprites:
|
||||||
self.render_engine.draw_image(x, y, blood_texture, tag="blood")
|
self.game.render_engine.draw_image(x, y, blood_texture, tag="blood")
|
||||||
|
|
||||||
def regenerate_background(self):
|
def regenerate_background(self):
|
||||||
"""Generate or regenerate the background texture (static - no blood stains)"""
|
"""Generate or regenerate the background texture (static - no blood stains)"""
|
||||||
texture_tiles = []
|
texture_tiles = []
|
||||||
self.cave_foreground_tiles = []
|
self.cave_foreground_tiles = []
|
||||||
half_cell = self.cell_size // 2
|
half_cell = self.game.cell_size // 2
|
||||||
|
|
||||||
def draw(surface, x, y):
|
def draw(surface, x, y):
|
||||||
texture_tiles.append((surface, x, y))
|
texture_tiles.append((surface, x, y))
|
||||||
|
|
||||||
def draw_cave(surface, x, y, direction):
|
def draw_cave(surface, x, y, direction):
|
||||||
self.cave_foreground_tiles.append((x // self.cell_size, y // self.cell_size, direction, surface, x, y))
|
self.cave_foreground_tiles.append((x // self.game.cell_size, y // self.game.cell_size, direction, surface, x, y))
|
||||||
|
|
||||||
def occupied(x, y):
|
def occupied(x, y):
|
||||||
return self.map.in_bounds(x, y) and self.map.get_cell(x, y) != maze.MAP_EMPTY
|
return self.game.map.in_bounds(x, y) and self.game.map.get_cell(x, y) != maze.MAP_EMPTY
|
||||||
|
|
||||||
def is_tunnel(x, y):
|
def is_tunnel(x, y):
|
||||||
return self.map.in_bounds(x, y) and self.map.get_cell(x, y) == maze.MAP_TUNNEL
|
return self.game.map.in_bounds(x, y) and self.game.map.get_cell(x, y) == maze.MAP_TUNNEL
|
||||||
|
|
||||||
def random_wall():
|
def random_wall():
|
||||||
return random.choice(self.grasses)
|
return random.choice(self.grasses)
|
||||||
@@ -232,16 +243,16 @@ class Graphics():
|
|||||||
|
|
||||||
def random_flower_texture():
|
def random_flower_texture():
|
||||||
return random.choice(self.flower_textures)
|
return random.choice(self.flower_textures)
|
||||||
for y, row in enumerate(self.map.tiles):
|
for y, row in enumerate(self.game.map.tiles):
|
||||||
for x, cell in enumerate(row):
|
for x, cell in enumerate(row):
|
||||||
px = x * self.cell_size
|
px = x * self.game.cell_size
|
||||||
py = y * self.cell_size
|
py = y * self.game.cell_size
|
||||||
|
|
||||||
if cell == maze.MAP_EMPTY:
|
if cell == maze.MAP_EMPTY:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
if cell == maze.MAP_WALL:
|
if cell == maze.MAP_WALL:
|
||||||
if x == 0 or y == 0 or x == self.map.width - 1 or y == self.map.height - 1:
|
if x == 0 or y == 0 or x == self.game.map.width - 1 or y == self.game.map.height - 1:
|
||||||
draw(random_wall(), px, py)
|
draw(random_wall(), px, py)
|
||||||
|
|
||||||
if x > 0 and y > 0 and (not occupied(x - 1, y - 1) or not occupied(x, y - 1) or not occupied(x - 1, y)):
|
if x > 0 and y > 0 and (not occupied(x - 1, y - 1) or not occupied(x, y - 1) or not occupied(x - 1, y)):
|
||||||
@@ -257,7 +268,7 @@ class Graphics():
|
|||||||
else:
|
else:
|
||||||
draw(self.corners["NW"], px, py)
|
draw(self.corners["NW"], px, py)
|
||||||
|
|
||||||
if y < self.map.height - 1 and x < self.map.width - 1:
|
if y < self.game.map.height - 1 and x < self.game.map.width - 1:
|
||||||
south = occupied(x, y + 1)
|
south = occupied(x, y + 1)
|
||||||
east = occupied(x + 1, y)
|
east = occupied(x + 1, y)
|
||||||
southeast = occupied(x + 1, y + 1)
|
southeast = occupied(x + 1, y + 1)
|
||||||
@@ -266,8 +277,8 @@ class Graphics():
|
|||||||
random.randrange(10) != 0
|
random.randrange(10) != 0
|
||||||
or x == 0
|
or x == 0
|
||||||
or y == 0
|
or y == 0
|
||||||
or x == self.map.width - 2
|
or x == self.game.map.width - 2
|
||||||
or y == self.map.height - 2
|
or y == self.game.map.height - 2
|
||||||
or is_tunnel(x + 1, y)
|
or is_tunnel(x + 1, y)
|
||||||
or is_tunnel(x, y + 1)
|
or is_tunnel(x, y + 1)
|
||||||
or is_tunnel(x + 1, y + 1)
|
or is_tunnel(x + 1, y + 1)
|
||||||
@@ -285,7 +296,7 @@ class Graphics():
|
|||||||
else:
|
else:
|
||||||
draw(self.corners["SE"], px + half_cell, py + half_cell)
|
draw(self.corners["SE"], px + half_cell, py + half_cell)
|
||||||
|
|
||||||
if y > 0 and x < self.map.width - 1 and (not occupied(x + 1, y - 1) or not occupied(x, y - 1) or not occupied(x + 1, y)):
|
if y > 0 and x < self.game.map.width - 1 and (not occupied(x + 1, y - 1) or not occupied(x, y - 1) or not occupied(x + 1, y)):
|
||||||
north = occupied(x, y - 1)
|
north = occupied(x, y - 1)
|
||||||
east = occupied(x + 1, y)
|
east = occupied(x + 1, y)
|
||||||
if north or east:
|
if north or east:
|
||||||
@@ -298,7 +309,7 @@ class Graphics():
|
|||||||
else:
|
else:
|
||||||
draw(self.corners["NE"], px + half_cell, py)
|
draw(self.corners["NE"], px + half_cell, py)
|
||||||
|
|
||||||
if y < self.map.height - 1 and x > 0 and (not occupied(x - 1, y + 1) or not occupied(x, y + 1) or not occupied(x - 1, y)):
|
if y < self.game.map.height - 1 and x > 0 and (not occupied(x - 1, y + 1) or not occupied(x, y + 1) or not occupied(x - 1, y)):
|
||||||
south = occupied(x, y + 1)
|
south = occupied(x, y + 1)
|
||||||
west = occupied(x - 1, y)
|
west = occupied(x - 1, y)
|
||||||
if south or west:
|
if south or west:
|
||||||
@@ -337,27 +348,332 @@ class Graphics():
|
|||||||
draw_cave(self.caves["UP"], px, py, "UP")
|
draw_cave(self.caves["UP"], px, py, "UP")
|
||||||
|
|
||||||
# Blood stains now handled separately as overlay layer
|
# Blood stains now handled separately as overlay layer
|
||||||
self.background_texture = self.render_engine.create_texture(texture_tiles, fill_color=(128, 128, 128))
|
self.game.background_texture = self.game.render_engine.create_texture(texture_tiles, fill_color=(128, 128, 128))
|
||||||
|
|
||||||
def add_blood_stain(self, position):
|
def add_blood_stain(self, position):
|
||||||
"""Add a blood stain as sprite overlay (opti mized - no background regeneration)"""
|
"""Add a blood stain as sprite overlay (optimized - no background regeneration)"""
|
||||||
# Pick random blood texture from pre-generated pool
|
# Pick random blood texture from pre-generated pool
|
||||||
if not self.blood_stain_textures:
|
if not self.blood_stain_textures:
|
||||||
return
|
return
|
||||||
|
|
||||||
blood_texture = random.choice(self.blood_stain_textures)
|
blood_texture = random.choice(self.blood_stain_textures)
|
||||||
x = position[0] * self.cell_size
|
x = position[0] * self.game.cell_size
|
||||||
y = position[1] * self.cell_size
|
y = position[1] * self.game.cell_size
|
||||||
|
|
||||||
# Add to blood layer sprites instead of regenerating background
|
# Add to blood layer sprites instead of regenerating background
|
||||||
self.blood_layer_sprites.append((blood_texture, x, y))
|
self.blood_layer_sprites.append((blood_texture, x, y))
|
||||||
|
|
||||||
def scroll_cursor(self, x=0, y=0):
|
def scroll_cursor(self, x=0, y=0):
|
||||||
if self.pointer[0] + x > self.map.width or self.pointer[1] + y > self.map.height:
|
if self.game.pointer[0] + x > self.game.map.width or self.game.pointer[1] + y > self.game.map.height:
|
||||||
return
|
return
|
||||||
|
|
||||||
self.pointer = (
|
self.game.pointer = (
|
||||||
max(1, min(self.map.width-2, self.pointer[0] + x)),
|
max(1, min(self.game.map.width-2, self.game.pointer[0] + x)),
|
||||||
max(1, min(self.map.height-2, self.pointer[1] + y))
|
max(1, min(self.game.map.height-2, self.game.pointer[1] + y))
|
||||||
|
)
|
||||||
|
self.game.render_engine.scroll_view(self.game.pointer)
|
||||||
|
|
||||||
|
# ==================== MENU RENDERING ====================
|
||||||
|
|
||||||
|
def _menu_font(self, size):
|
||||||
|
clamped = max(10, min(69, int(size)))
|
||||||
|
return self.game.render_engine.fonts[clamped]
|
||||||
|
|
||||||
|
def _draw_start_menu_difficulty_selector(self, x, y, width, height):
|
||||||
|
colors = config.START_MENU_COLORS
|
||||||
|
difficulty_config = self.game._difficulty_config()
|
||||||
|
accent = difficulty_config["accent"]
|
||||||
|
render_engine = self.game.render_engine
|
||||||
|
center_x = x + width // 2
|
||||||
|
compact_selector = height <= 72
|
||||||
|
title_font = self._menu_font(render_engine.target_size[1] // 38)
|
||||||
|
value_font = self._menu_font(render_engine.target_size[1] // 23)
|
||||||
|
arrow_font = value_font
|
||||||
|
section_gap = 4 if compact_selector else 6
|
||||||
|
title_line_height = max(14, render_engine.target_size[1] // 32)
|
||||||
|
value_line_height = max(18, render_engine.target_size[1] // 24)
|
||||||
|
current_y = y + 2
|
||||||
|
|
||||||
|
render_engine.draw_text("Difficulty", title_font, ("center", current_y), colors["muted"])
|
||||||
|
current_y += title_line_height + section_gap
|
||||||
|
|
||||||
|
arrow_offset = max(34, min(52, width // 7))
|
||||||
|
arrow_y = current_y - (1 if compact_selector else 0)
|
||||||
|
render_engine.draw_text("<", arrow_font, (center_x - arrow_offset, arrow_y), colors["muted"])
|
||||||
|
render_engine.draw_text(">", arrow_font, (center_x + arrow_offset, arrow_y), colors["muted"])
|
||||||
|
|
||||||
|
render_engine.draw_text(
|
||||||
|
difficulty_config["label"],
|
||||||
|
value_font,
|
||||||
|
("center", current_y),
|
||||||
|
accent,
|
||||||
|
)
|
||||||
|
current_y += value_line_height
|
||||||
|
|
||||||
|
def _draw_start_menu_slider(self, x, y, width, height, setting_name, label, value, selected):
|
||||||
|
colors = config.START_MENU_COLORS
|
||||||
|
style = config.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.game.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.game, "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.game.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 = config.START_MENU_COLORS
|
||||||
|
render_engine = self.game.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(config.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.game, setting_name),
|
||||||
|
index == self.game.start_menu_selection,
|
||||||
|
)
|
||||||
|
|
||||||
|
cards_bottom = cards_y + len(config.START_MENU_AUDIO_OPTIONS) * card_height + (len(config.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 = config.START_MENU_COLORS
|
||||||
|
render_engine = self.game.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.game.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.game.profile_integration.current_profile
|
||||||
|
device_id = self.game.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"],
|
||||||
|
)
|
||||||
|
|
||||||
|
difficulty_width = max(360, min(panel_width - 160, 760))
|
||||||
|
difficulty_height = 54 if compact_menu else max(72, target_height // 9)
|
||||||
|
difficulty_x = target_width // 2 - difficulty_width // 2
|
||||||
|
difficulty_y = info_y + line_gap * len(subtitle_lines) + (12 if compact_menu else 18)
|
||||||
|
self._draw_start_menu_difficulty_selector(
|
||||||
|
difficulty_x,
|
||||||
|
difficulty_y,
|
||||||
|
difficulty_width,
|
||||||
|
difficulty_height,
|
||||||
|
)
|
||||||
|
|
||||||
|
cta_y = difficulty_y + difficulty_height + (12 if compact_menu else 24)
|
||||||
|
render_engine.draw_text(
|
||||||
|
"Press Return to start",
|
||||||
|
cta_font,
|
||||||
|
("center", cta_y),
|
||||||
|
colors["text"],
|
||||||
|
)
|
||||||
|
if compact_menu:
|
||||||
|
render_engine.draw_text(
|
||||||
|
"Arrows change difficulty",
|
||||||
|
hint_font,
|
||||||
|
("center", cta_y + line_gap),
|
||||||
|
colors["muted"],
|
||||||
|
)
|
||||||
|
render_engine.draw_text(
|
||||||
|
"Esc quits M toggles audio",
|
||||||
|
hint_font,
|
||||||
|
("center", cta_y + line_gap + 18),
|
||||||
|
colors["muted"],
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
render_engine.draw_text(
|
||||||
|
"Arrows change difficulty 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.game.current_level + 1} | Points: {self.game.points}",
|
||||||
|
f"Rats in maze: {self.game.unit_manager.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",
|
||||||
)
|
)
|
||||||
self.render_engine.scroll_view(self.pointer)
|
|
||||||
@@ -8,19 +8,22 @@ SCORES_FILE = persistent_data_path("scores.txt", default_text="")
|
|||||||
|
|
||||||
|
|
||||||
class Scoring:
|
class Scoring:
|
||||||
|
def __init__(self, game):
|
||||||
|
self.game = game
|
||||||
|
|
||||||
# ==================== SCORING ====================
|
# ==================== SCORING ====================
|
||||||
|
|
||||||
def save_score(self):
|
def save_score(self):
|
||||||
# Save to traditional scores.txt file
|
# Save to traditional scores.txt file
|
||||||
with SCORES_FILE.open("a", encoding="utf-8") as f:
|
with SCORES_FILE.open("a", encoding="utf-8") as f:
|
||||||
player_name = getattr(self, 'profile_integration', None)
|
profile_integration = getattr(self.game, 'profile_integration', None)
|
||||||
if player_name and hasattr(player_name, 'get_profile_name'):
|
if profile_integration and hasattr(profile_integration, 'get_profile_name'):
|
||||||
name = player_name.get_profile_name()
|
name = profile_integration.get_profile_name()
|
||||||
device_id = player_name.get_device_id()
|
device_id = profile_integration.get_device_id()
|
||||||
f.write(f"{datetime.datetime.now()} - {self.points} - {name} - {device_id}\n")
|
f.write(f"{datetime.datetime.now()} - {self.game.points} - {name} - {device_id}\n")
|
||||||
else:
|
else:
|
||||||
f.write(f"{datetime.datetime.now()} - {self.points} - Guest\n")
|
f.write(f"{datetime.datetime.now()} - {self.game.points} - Guest\n")
|
||||||
|
|
||||||
def read_score(self):
|
def read_score(self):
|
||||||
table = []
|
table = []
|
||||||
try:
|
try:
|
||||||
@@ -40,6 +43,6 @@ class Scoring:
|
|||||||
except FileNotFoundError:
|
except FileNotFoundError:
|
||||||
pass
|
pass
|
||||||
return table[:5] # Return top 5 scores instead of 3
|
return table[:5] # Return top 5 scores instead of 3
|
||||||
|
|
||||||
def add_point(self, value):
|
def add_point(self, value):
|
||||||
self.points += value
|
self.game.points += value
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
from enum import Enum, auto
|
||||||
|
|
||||||
|
class GameState(Enum):
|
||||||
|
START_MENU = auto()
|
||||||
|
PLAYING = auto()
|
||||||
|
PAUSED = auto()
|
||||||
|
GAME_OVER = auto()
|
||||||
|
VICTORY = auto()
|
||||||
|
|
||||||
|
class StateMachine:
|
||||||
|
def __init__(self, game):
|
||||||
|
self.game = game
|
||||||
|
self.current_state = GameState.START_MENU
|
||||||
|
|
||||||
|
def transition_to(self, new_state):
|
||||||
|
print(f"[state] Transitioning from {self.current_state} to {new_state}")
|
||||||
|
self.current_state = new_state
|
||||||
|
|
||||||
|
# Sincronizzazione per compatibilità con KeyBindings e logica esistente
|
||||||
|
if new_state == GameState.PLAYING:
|
||||||
|
self.game.game_status = "game"
|
||||||
|
self.game.menu_screen = None
|
||||||
|
elif new_state == GameState.PAUSED:
|
||||||
|
self.game.game_status = "paused"
|
||||||
|
self.game.menu_screen = None
|
||||||
|
elif new_state == GameState.START_MENU:
|
||||||
|
self.game.game_status = "start_menu"
|
||||||
|
self.game.menu_screen = "start"
|
||||||
|
elif new_state in [GameState.GAME_OVER, GameState.VICTORY]:
|
||||||
|
self.game.game_status = "paused" # Legacy status used for key handling in end screens
|
||||||
|
self.game.menu_screen = None
|
||||||
|
|
||||||
|
def update(self):
|
||||||
|
# Dispatch alla logica originale ripristinata in MiceMaze/Graphics
|
||||||
|
if self.current_state == GameState.START_MENU:
|
||||||
|
self.game.graphics.render_start_menu()
|
||||||
|
elif self.current_state == GameState.PAUSED:
|
||||||
|
self.game.graphics.render_pause_menu()
|
||||||
|
elif self.current_state in [GameState.GAME_OVER, GameState.VICTORY]:
|
||||||
|
# Delega al metodo game_over originale che gestisce i dialoghi specifici
|
||||||
|
self.game.game_over()
|
||||||
|
|
||||||
@@ -5,23 +5,26 @@ from units import gas, rat, bomb, mine
|
|||||||
|
|
||||||
|
|
||||||
class UnitManager:
|
class UnitManager:
|
||||||
|
def __init__(self, game):
|
||||||
|
self.game = game
|
||||||
|
|
||||||
def _spawnable_rat_positions(self):
|
def _spawnable_rat_positions(self):
|
||||||
positions = []
|
positions = []
|
||||||
for y in range(1, self.map.height - 1):
|
for y in range(1, self.game.map.height - 1):
|
||||||
for x in range(1, self.map.width - 1):
|
for x in range(1, self.game.map.width - 1):
|
||||||
if not self.map.is_empty(x, y):
|
if not self.game.map.is_empty(x, y):
|
||||||
continue
|
continue
|
||||||
for dx, dy in [(-1, 0), (1, 0), (0, -1), (0, 1)]:
|
for dx, dy in [(-1, 0), (1, 0), (0, -1), (0, 1)]:
|
||||||
nx = x + dx
|
nx = x + dx
|
||||||
ny = y + dy
|
ny = y + dy
|
||||||
if self.map.in_bounds(nx, ny) and self.map.is_empty(nx, ny):
|
if self.game.map.in_bounds(nx, ny) and self.game.map.is_empty(nx, ny):
|
||||||
positions.append((x, y))
|
positions.append((x, y))
|
||||||
break
|
break
|
||||||
return positions
|
return positions
|
||||||
|
|
||||||
def has_weapon_at(self, position):
|
def has_weapon_at(self, position):
|
||||||
"""Check if there's a weapon (bomb, gas, mine) at the given position"""
|
"""Check if there's a weapon (bomb, gas, mine) at the given position"""
|
||||||
for unit in self.units.values():
|
for unit in self.game.units.values():
|
||||||
if unit.position == position:
|
if unit.position == position:
|
||||||
# Check if it's a weapon type (not a rat or points)
|
# Check if it's a weapon type (not a rat or points)
|
||||||
if isinstance(unit, (bomb.Timer, bomb.NuclearBomb, gas.Gas, mine.Mine)):
|
if isinstance(unit, (bomb.Timer, bomb.NuclearBomb, gas.Gas, mine.Mine)):
|
||||||
@@ -30,9 +33,9 @@ class UnitManager:
|
|||||||
|
|
||||||
def can_place_weapon_at(self, position):
|
def can_place_weapon_at(self, position):
|
||||||
x, y = position
|
x, y = position
|
||||||
if not self.map.in_bounds(x, y):
|
if not self.game.map.in_bounds(x, y):
|
||||||
return False
|
return False
|
||||||
if not self.map.is_empty(x, y):
|
if not self.game.map.is_empty(x, y):
|
||||||
return False
|
return False
|
||||||
if self.has_weapon_at(position):
|
if self.has_weapon_at(position):
|
||||||
return False
|
return False
|
||||||
@@ -40,19 +43,19 @@ class UnitManager:
|
|||||||
|
|
||||||
def count_rats(self):
|
def count_rats(self):
|
||||||
count = 0
|
count = 0
|
||||||
for unit in self.units.values():
|
for unit in self.game.units.values():
|
||||||
if isinstance(unit, rat.Rat):
|
if isinstance(unit, rat.Rat):
|
||||||
count += 1
|
count += 1
|
||||||
return count
|
return count
|
||||||
|
|
||||||
def spawn_gas(self, parent_id=None):
|
def spawn_gas(self, parent_id=None):
|
||||||
if not self.can_place_weapon_at(self.pointer):
|
if not self.can_place_weapon_at(self.game.pointer):
|
||||||
return
|
return
|
||||||
if self.ammo["gas"]["count"] <= 0:
|
if self.game.ammo["gas"]["count"] <= 0:
|
||||||
return
|
return
|
||||||
self.ammo["gas"]["count"] -= 1
|
self.game.ammo["gas"]["count"] -= 1
|
||||||
self.render_engine.play_sound("GAS.WAV")
|
self.game.render_engine.play_sound("GAS.WAV")
|
||||||
self.spawn_unit(gas.Gas, self.pointer, parent_id=parent_id)
|
self.spawn_unit(gas.Gas, self.game.pointer, parent_id=parent_id)
|
||||||
|
|
||||||
def spawn_rat(self, position=None):
|
def spawn_rat(self, position=None):
|
||||||
if position is None:
|
if position is None:
|
||||||
@@ -68,9 +71,9 @@ class UnitManager:
|
|||||||
# Try nearby positions
|
# Try nearby positions
|
||||||
for dx, dy in [(0,1), (1,0), (0,-1), (-1,0), (1,1), (-1,-1), (1,-1), (-1,1)]:
|
for dx, dy in [(0,1), (1,0), (0,-1), (-1,0), (1,1), (-1,-1), (1,-1), (-1,1)]:
|
||||||
alt_pos = (position[0] + dx, position[1] + dy)
|
alt_pos = (position[0] + dx, position[1] + dy)
|
||||||
if not self.map.in_bounds(alt_pos[0], alt_pos[1]):
|
if not self.game.map.in_bounds(alt_pos[0], alt_pos[1]):
|
||||||
continue
|
continue
|
||||||
if self.map.is_empty(alt_pos[0], alt_pos[1]) and not self.has_weapon_at(alt_pos):
|
if self.game.map.is_empty(alt_pos[0], alt_pos[1]) and not self.has_weapon_at(alt_pos):
|
||||||
position = alt_pos
|
position = alt_pos
|
||||||
break
|
break
|
||||||
else:
|
else:
|
||||||
@@ -84,45 +87,45 @@ class UnitManager:
|
|||||||
def spawn_bomb(self, position):
|
def spawn_bomb(self, position):
|
||||||
if not self.can_place_weapon_at(position):
|
if not self.can_place_weapon_at(position):
|
||||||
return
|
return
|
||||||
if self.ammo["bomb"]["count"] <= 0:
|
if self.game.ammo["bomb"]["count"] <= 0:
|
||||||
return
|
return
|
||||||
self.render_engine.play_sound("PUTDOWN.WAV")
|
self.game.render_engine.play_sound("PUTDOWN.WAV")
|
||||||
self.spawn_unit(bomb.Timer, position)
|
self.spawn_unit(bomb.Timer, position)
|
||||||
self.ammo["bomb"]["count"] -= 1
|
self.game.ammo["bomb"]["count"] -= 1
|
||||||
|
|
||||||
def spawn_nuclear_bomb(self, position):
|
def spawn_nuclear_bomb(self, position):
|
||||||
"""Spawn a nuclear bomb at the specified position"""
|
"""Spawn a nuclear bomb at the specified position"""
|
||||||
if self.ammo["nuclear"]["count"] <= 0:
|
if self.game.ammo["nuclear"]["count"] <= 0:
|
||||||
return
|
return
|
||||||
if not self.can_place_weapon_at(position):
|
if not self.can_place_weapon_at(position):
|
||||||
return
|
return
|
||||||
self.render_engine.play_sound("NUCLEAR.WAV")
|
self.game.render_engine.play_sound("NUCLEAR.WAV")
|
||||||
self.ammo["nuclear"]["count"] -= 1
|
self.game.ammo["nuclear"]["count"] -= 1
|
||||||
self.spawn_unit(bomb.NuclearBomb, position)
|
self.spawn_unit(bomb.NuclearBomb, position)
|
||||||
|
|
||||||
def spawn_mine(self, position):
|
def spawn_mine(self, position):
|
||||||
if self.ammo["mine"]["count"] <= 0:
|
if self.game.ammo["mine"]["count"] <= 0:
|
||||||
return
|
return
|
||||||
if not self.can_place_weapon_at(position):
|
if not self.can_place_weapon_at(position):
|
||||||
return
|
return
|
||||||
self.render_engine.play_sound("PUTDOWN.WAV")
|
self.game.render_engine.play_sound("PUTDOWN.WAV")
|
||||||
self.ammo["mine"]["count"] -= 1
|
self.game.ammo["mine"]["count"] -= 1
|
||||||
self.spawn_unit(mine.Mine, position, on_bottom=True)
|
self.spawn_unit(mine.Mine, position, on_bottom=True)
|
||||||
|
|
||||||
def spawn_unit(self, unit, position, on_bottom=False, **kwargs):
|
def spawn_unit(self, unit, position, on_bottom=False, **kwargs):
|
||||||
id = uuid.uuid4()
|
id = uuid.uuid4()
|
||||||
if on_bottom:
|
if on_bottom:
|
||||||
self.units = {id: unit(self, position, id, **kwargs), **self.units}
|
self.game.units = {id: unit(self.game, position, id, **kwargs), **self.game.units}
|
||||||
else:
|
else:
|
||||||
self.units[id] = unit(self, position, id, **kwargs)
|
self.game.units[id] = unit(self.game, position, id, **kwargs)
|
||||||
|
|
||||||
def choose_start(self):
|
def choose_start(self):
|
||||||
if not hasattr(self, '_valid_positions') or self._valid_positions is None:
|
if not hasattr(self.game, '_valid_positions') or self.game._valid_positions is None:
|
||||||
self._valid_positions = self._spawnable_rat_positions()
|
self.game._valid_positions = self._spawnable_rat_positions()
|
||||||
print(f"[flow] choose_start computed {len(self._valid_positions)} spawnable cells", flush=True)
|
print(f"[flow] choose_start computed {len(self.game._valid_positions)} spawnable cells", flush=True)
|
||||||
if not self._valid_positions:
|
if not self.game._valid_positions:
|
||||||
return None
|
return None
|
||||||
return random.choice(self._valid_positions)
|
return random.choice(self.game._valid_positions)
|
||||||
|
|
||||||
def get_unit_by_id(self, id):
|
def get_unit_by_id(self, id):
|
||||||
return self.units.get(id) or None
|
return self.game.units.get(id) or None
|
||||||
|
|||||||
@@ -0,0 +1,182 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Deploy Mice! to a Koriki CFW device over SSH.
|
||||||
|
#
|
||||||
|
# Requirements on host:
|
||||||
|
# sshpass, tar, ssh
|
||||||
|
#
|
||||||
|
# Target: koriki@<IP> (default 10.0.0.199)
|
||||||
|
# - ARMv7l Linux, glibc 2.28
|
||||||
|
# - Python 3.11 archive already present at /mnt/SDCARD/python_armv7.tar.gz
|
||||||
|
# - SDL2 libs in /mnt/SDCARD/Koriki/lib/
|
||||||
|
# - Internet access via WiFi
|
||||||
|
#
|
||||||
|
# Usage:
|
||||||
|
# ./packaging/deploy_koriki.sh [TARGET_IP]
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
TARGET_IP="${1:-10.0.0.199}"
|
||||||
|
TARGET_USER="${TARGET_USER:-koriki}"
|
||||||
|
TARGET_PASS="${TARGET_PASS:-koriki}"
|
||||||
|
SDCARD="/mnt/SDCARD"
|
||||||
|
PYTHON_ARCHIVE="${SDCARD}/python_armv7.tar.gz"
|
||||||
|
PYTHON_DIR="${SDCARD}/python"
|
||||||
|
GAME_DIR="${SDCARD}/Ports/mice"
|
||||||
|
KORIKI_LIB="${SDCARD}/Koriki/lib"
|
||||||
|
VENDOR_DIR="${GAME_DIR}/vendor"
|
||||||
|
|
||||||
|
ROOT_DIR=$(CDPATH= cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd)
|
||||||
|
|
||||||
|
ssh_cmd() {
|
||||||
|
sshpass -p "$TARGET_PASS" ssh \
|
||||||
|
-o StrictHostKeyChecking=no \
|
||||||
|
-o ConnectTimeout=10 \
|
||||||
|
-o PreferredAuthentications=password \
|
||||||
|
-o PubkeyAuthentication=no \
|
||||||
|
"${TARGET_USER}@${TARGET_IP}" "$@"
|
||||||
|
}
|
||||||
|
|
||||||
|
log() { printf '==> %s\n' "$*"; }
|
||||||
|
|
||||||
|
# ── 1. Estrai Python 3.11 ──────────────────────────────────────────────────────
|
||||||
|
log "Step 1: Extracting Python 3.11 on device"
|
||||||
|
ssh_cmd "
|
||||||
|
if [ ! -f '${PYTHON_DIR}/bin/python3.11' ]; then
|
||||||
|
echo 'Extracting Python archive...'
|
||||||
|
tar -xzf '${PYTHON_ARCHIVE}' -C '${SDCARD}'
|
||||||
|
echo 'Done'
|
||||||
|
else
|
||||||
|
echo 'Python 3.11 already extracted, skipping'
|
||||||
|
fi
|
||||||
|
"
|
||||||
|
|
||||||
|
# ── 1b. Fix FAT32: i symlink non possono essere creati su vfat; li sostituiamo
|
||||||
|
# con copie reali dei file critici
|
||||||
|
log "Step 1b: Fixing missing symlinks on FAT32 (copying critical binaries)"
|
||||||
|
ssh_cmd "
|
||||||
|
PYBIN='${PYTHON_DIR}/bin'
|
||||||
|
PYLIB='${PYTHON_DIR}/lib'
|
||||||
|
# Copie necessarie all'interprete
|
||||||
|
[ ! -f \"\${PYBIN}/python3\" ] && cp \"\${PYBIN}/python3.11\" \"\${PYBIN}/python3\"
|
||||||
|
[ ! -f \"\${PYBIN}/python\" ] && cp \"\${PYBIN}/python3.11\" \"\${PYBIN}/python\"
|
||||||
|
[ ! -f \"\${PYBIN}/pip3\" ] && [ -f \"\${PYBIN}/pip3.11\" ] && cp \"\${PYBIN}/pip3.11\" \"\${PYBIN}/pip3\"
|
||||||
|
# Libreria condivisa
|
||||||
|
[ ! -f \"\${PYLIB}/libpython3.11.so\" ] && cp \"\${PYLIB}/libpython3.11.so.1.0\" \"\${PYLIB}/libpython3.11.so\"
|
||||||
|
echo 'Symlink workaround done'
|
||||||
|
"
|
||||||
|
|
||||||
|
# ── 2. Installa dipendenze Python ──────────────────────────────────────────────
|
||||||
|
log "Step 2: Installing Python dependencies (wheel download + extract)"
|
||||||
|
ssh_cmd "
|
||||||
|
PYTHON='${PYTHON_DIR}/bin/python3.11'
|
||||||
|
export TMPDIR='${GAME_DIR}/tmp'
|
||||||
|
export LD_LIBRARY_PATH='${PYTHON_DIR}/lib:${KORIKI_LIB}'
|
||||||
|
# pip --target su FAT32 puo fallire (rename di file temporanei).
|
||||||
|
# Workaround: scarichiamo wheel in /tmp e li estraiamo manualmente in vendor.
|
||||||
|
mkdir -p '${VENDOR_DIR}' '${GAME_DIR}/tmp' '${GAME_DIR}/tmp/mice_wheels'
|
||||||
|
rm -f '${GAME_DIR}/tmp/mice_wheels'/*.whl
|
||||||
|
\"\$PYTHON\" -m pip download --no-cache-dir \
|
||||||
|
--extra-index-url https://www.piwheels.org/simple/ \
|
||||||
|
--dest '${GAME_DIR}/tmp/mice_wheels' \
|
||||||
|
pysdl2 \
|
||||||
|
'Pillow>=10.0' \
|
||||||
|
'numpy>=1.26' \
|
||||||
|
pyaml \
|
||||||
|
requests
|
||||||
|
\"\$PYTHON\" - << 'PY'
|
||||||
|
import glob
|
||||||
|
import os
|
||||||
|
import zipfile
|
||||||
|
|
||||||
|
vendor = '${VENDOR_DIR}'
|
||||||
|
wheels = sorted(glob.glob('${GAME_DIR}/tmp/mice_wheels/*.whl'))
|
||||||
|
if not wheels:
|
||||||
|
raise SystemExit('No wheels downloaded')
|
||||||
|
|
||||||
|
for whl in wheels:
|
||||||
|
print('Extracting', os.path.basename(whl))
|
||||||
|
with zipfile.ZipFile(whl) as zf:
|
||||||
|
zf.extractall(vendor)
|
||||||
|
|
||||||
|
print('Dependencies extracted to', vendor)
|
||||||
|
PY
|
||||||
|
echo 'Dependencies installed (wheel extraction)'
|
||||||
|
"
|
||||||
|
|
||||||
|
# ── 3. Crea directory di gioco ─────────────────────────────────────────────────
|
||||||
|
log "Step 3: Creating game directory ${GAME_DIR}"
|
||||||
|
ssh_cmd "mkdir -p '${GAME_DIR}'"
|
||||||
|
|
||||||
|
# ── 4. Trasferisci il gioco ────────────────────────────────────────────────────
|
||||||
|
# Pipe diretta tar → tar: evita di scrivere file intermedi in /tmp (tmpfs 49MB)
|
||||||
|
log "Step 4: Transferring game files via direct pipe (no tmp file)"
|
||||||
|
tar czf - \
|
||||||
|
-C "$ROOT_DIR" \
|
||||||
|
--exclude='.git' \
|
||||||
|
--exclude='.venv' \
|
||||||
|
--exclude='venv' \
|
||||||
|
--exclude='__pycache__' \
|
||||||
|
--exclude='*.pyc' \
|
||||||
|
--exclude='dist' \
|
||||||
|
--exclude='logs' \
|
||||||
|
--exclude='packaging' \
|
||||||
|
--exclude='tools' \
|
||||||
|
--exclude='server' \
|
||||||
|
--exclude='*.tar.gz' \
|
||||||
|
. \
|
||||||
|
| sshpass -p "$TARGET_PASS" ssh \
|
||||||
|
-o StrictHostKeyChecking=no \
|
||||||
|
"${TARGET_USER}@${TARGET_IP}" \
|
||||||
|
"mkdir -p '${GAME_DIR}' && tar -xzf - -C '${GAME_DIR}' && echo 'Game extracted'"
|
||||||
|
|
||||||
|
# ── 5. Crea il launcher ────────────────────────────────────────────────────────
|
||||||
|
log "Step 5: Creating launcher script"
|
||||||
|
|
||||||
|
LAUNCHER_CONTENT="#!/bin/sh
|
||||||
|
# Mice! launcher for Koriki CFW
|
||||||
|
|
||||||
|
export PYTHON_DIR=\"${PYTHON_DIR}\"
|
||||||
|
export PATH=\"\${PYTHON_DIR}/bin:\$PATH\"
|
||||||
|
export PYTHONPATH=\"${VENDOR_DIR}:\${PYTHON_DIR}/lib/python3.11/site-packages\"
|
||||||
|
|
||||||
|
# Puntiamo PySDL2 alle librerie SDL2 di Koriki
|
||||||
|
export PYSDL2_DLL_PATH=\"${KORIKI_LIB}\"
|
||||||
|
export LD_LIBRARY_PATH=\"${KORIKI_LIB}:\${LD_LIBRARY_PATH:-}\"
|
||||||
|
|
||||||
|
# Root del progetto e dati persistenti
|
||||||
|
export MICE_PROJECT_ROOT=\"${GAME_DIR}\"
|
||||||
|
export MICE_DATA_DIR=\"\${SDCARD}/.mice_data\"
|
||||||
|
|
||||||
|
mkdir -p \"\${MICE_DATA_DIR}\"
|
||||||
|
|
||||||
|
cd \"\${MICE_PROJECT_ROOT}\"
|
||||||
|
exec \"\${PYTHON_DIR}/bin/python3\" rats.py \"\$@\"
|
||||||
|
"
|
||||||
|
|
||||||
|
ssh_cmd "cat > '${SDCARD}/Ports/mice.sh'" <<< "$LAUNCHER_CONTENT"
|
||||||
|
ssh_cmd "chmod +x '${SDCARD}/Ports/mice.sh'"
|
||||||
|
|
||||||
|
log "Launcher written to ${SDCARD}/Ports/mice.sh"
|
||||||
|
|
||||||
|
# ── 6. Test rapido ─────────────────────────────────────────────────────────────
|
||||||
|
log "Step 6: Quick smoke test"
|
||||||
|
ssh_cmd "
|
||||||
|
export PATH='${PYTHON_DIR}/bin:\$PATH'
|
||||||
|
export PYSDL2_DLL_PATH='${KORIKI_LIB}'
|
||||||
|
export LD_LIBRARY_PATH='${PYTHON_DIR}/lib:${KORIKI_LIB}'
|
||||||
|
export PYTHONPATH='${VENDOR_DIR}:${PYTHON_DIR}/lib/python3.11/site-packages'
|
||||||
|
python3 -c \"
|
||||||
|
import sys
|
||||||
|
print('Python', sys.version)
|
||||||
|
import sdl2; print('PySDL2 OK:', sdl2.__version__)
|
||||||
|
import numpy; print('NumPy OK:', numpy.__version__)
|
||||||
|
import PIL; print('Pillow OK:', PIL.__version__)
|
||||||
|
import yaml; print('pyaml OK')
|
||||||
|
import requests; print('requests OK')
|
||||||
|
print('All dependencies OK')
|
||||||
|
\"
|
||||||
|
"
|
||||||
|
|
||||||
|
log "Deployment complete!"
|
||||||
|
log "Run the game from Ports > mice on Koriki, or manually:"
|
||||||
|
log " ssh ${TARGET_USER}@${TARGET_IP} '${SDCARD}/Ports/mice.sh'"
|
||||||
@@ -6,87 +6,15 @@ import os
|
|||||||
import json
|
import json
|
||||||
import time
|
import time
|
||||||
|
|
||||||
from engine import maze, sdl2 as engine, controls, graphics, unit_manager, scoring
|
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 engine.collision_system import CollisionSystem
|
||||||
from units import points
|
from units import points
|
||||||
from engine.user_profile_integration import UserProfileIntegration
|
from engine.user_profile_integration import UserProfileIntegration
|
||||||
from runtime_paths import bundle_path
|
from runtime_paths import bundle_path
|
||||||
|
|
||||||
|
|
||||||
LEVEL_MUSIC_CONFIG = "level_music"
|
class MiceMaze:
|
||||||
START_MENU_MUSIC = "High_Score_Garden.mp3"
|
|
||||||
RUN_COMPLETE_MUSIC = "Sunset_At_Pixel_Gardens.mp3"
|
|
||||||
START_MENU_ANIMATION = "anim/start_mice.gif"
|
|
||||||
SUPPORTED_MUSIC_EXTENSIONS = {".mp3", ".ogg", ".wav"}
|
|
||||||
BASE_INITIAL_RATS = 5
|
|
||||||
DEFAULT_DIFFICULTY = "easy"
|
|
||||||
DIFFICULTY_ALIASES = {
|
|
||||||
"medium": "normal",
|
|
||||||
"normale": "normal",
|
|
||||||
}
|
|
||||||
START_MENU_AUDIO_OPTIONS = (
|
|
||||||
("sound_volume", "Suono"),
|
|
||||||
("music_volume", "Musica"),
|
|
||||||
)
|
|
||||||
VOLUME_STEP = 5
|
|
||||||
GAME_END_LEVEL_CLEAR = "level_clear"
|
|
||||||
GAME_END_DEFEAT = "defeat"
|
|
||||||
GAME_END_RUN_COMPLETE = "run_complete"
|
|
||||||
DIFFICULTY_OPTIONS = (
|
|
||||||
{
|
|
||||||
"key": "easy",
|
|
||||||
"label": "Easy",
|
|
||||||
"starting_rats_multiplier": 1,
|
|
||||||
"speed_multiplier": 1.0,
|
|
||||||
"fill": (235, 246, 234),
|
|
||||||
"accent": (88, 148, 82),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"key": "normal",
|
|
||||||
"label": "Normal",
|
|
||||||
"starting_rats_multiplier": 2,
|
|
||||||
"speed_multiplier": 1.5,
|
|
||||||
"fill": (252, 242, 223),
|
|
||||||
"accent": (204, 146, 44),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"key": "hard",
|
|
||||||
"label": "Hard",
|
|
||||||
"starting_rats_multiplier": 3,
|
|
||||||
"speed_multiplier": 2.0,
|
|
||||||
"fill": (251, 229, 229),
|
|
||||||
"accent": (188, 68, 68),
|
|
||||||
},
|
|
||||||
)
|
|
||||||
DIFFICULTY_OPTIONS_BY_KEY = {option["key"]: option for option in DIFFICULTY_OPTIONS}
|
|
||||||
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 ====================
|
# ==================== INITIALIZATION ====================
|
||||||
|
|
||||||
@@ -95,6 +23,13 @@ class MiceMaze(
|
|||||||
self.render_engine.show_loading_screen(message, detail=detail, progress=progress)
|
self.render_engine.show_loading_screen(message, detail=detail, progress=progress)
|
||||||
|
|
||||||
def __init__(self, maze_file, level_index=0):
|
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
|
# Initialize user profile integration
|
||||||
self.profile_integration = UserProfileIntegration()
|
self.profile_integration = UserProfileIntegration()
|
||||||
self.map_source = maze_file
|
self.map_source = maze_file
|
||||||
@@ -106,35 +41,55 @@ class MiceMaze(
|
|||||||
self.audio = self.profile_integration.get_setting('sound_enabled', True)
|
self.audio = self.profile_integration.get_setting('sound_enabled', True)
|
||||||
self.sound_volume = self.profile_integration.get_setting('sound_volume', 50)
|
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.music_volume = self.profile_integration.get_setting('music_volume', self.sound_volume)
|
||||||
self.difficulty = DEFAULT_DIFFICULTY
|
self.difficulty = config.DEFAULT_DIFFICULTY
|
||||||
self.initial_rat_count = BASE_INITIAL_RATS
|
self.initial_rat_count = config.BASE_INITIAL_RATS
|
||||||
self.rat_speed_multiplier = 1.0
|
self.rat_speed_multiplier = 1.0
|
||||||
self._apply_difficulty(self.profile_integration.get_setting('difficulty', DEFAULT_DIFFICULTY), persist=False)
|
self._apply_difficulty(self.profile_integration.get_setting('difficulty', config.DEFAULT_DIFFICULTY), persist=False)
|
||||||
|
|
||||||
self.cell_size = 40
|
self.cell_size = 40
|
||||||
self.full_screen = False
|
self.full_screen = False
|
||||||
self.loaded_theme_index = None
|
|
||||||
self.start_menu_selection = 0
|
self.start_menu_selection = 0
|
||||||
|
self.points = 0
|
||||||
# Initialize render engine with profile-aware title
|
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.combined_scores = None
|
||||||
|
|
||||||
|
def _setup_engine(self):
|
||||||
player_name = self.profile_integration.get_profile_name()
|
player_name = self.profile_integration.get_profile_name()
|
||||||
window_title = f"Mice! - {player_name}"
|
window_title = f"Mice! - {player_name}"
|
||||||
|
|
||||||
self.render_engine = engine.GameWindow(self.map.width, self.map.height,
|
self.render_engine = engine.GameWindow(self.map.width, self.map.height,
|
||||||
self.cell_size, window_title,
|
self.cell_size, window_title,
|
||||||
key_callback=self.trigger)
|
key_callback=lambda action: self.controls.trigger(action))
|
||||||
self.render_engine.audio = self.audio
|
self.render_engine.audio = self.audio
|
||||||
self.startup_loading_active = True
|
self.startup_loading_active = True
|
||||||
self._update_startup_loading("Loading Mice!", detail="Applying player settings", progress=0.08)
|
|
||||||
|
|
||||||
# Apply profile settings
|
# Apply profile volumes
|
||||||
if hasattr(self.render_engine, 'set_sound_volume'):
|
if hasattr(self.render_engine, 'set_sound_volume'):
|
||||||
self.render_engine.set_sound_volume(self.sound_volume)
|
self.render_engine.set_sound_volume(self.sound_volume)
|
||||||
if hasattr(self.render_engine, 'set_music_volume'):
|
if hasattr(self.render_engine, 'set_music_volume'):
|
||||||
self.render_engine.set_music_volume(self.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)
|
def _setup_components(self):
|
||||||
self.initialize_keybindings()
|
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._update_startup_loading("Loading configuration", detail="Reading bundled settings", progress=0.14)
|
||||||
self.configs = self.get_config()
|
self.configs = self.get_config()
|
||||||
@@ -144,46 +99,26 @@ class MiceMaze(
|
|||||||
self.current_level_music = None
|
self.current_level_music = None
|
||||||
|
|
||||||
self._update_startup_loading("Loading graphics", detail="Preparing common assets", progress=0.26)
|
self._update_startup_loading("Loading graphics", detail="Preparing common assets", progress=0.26)
|
||||||
self.load_assets()
|
self.graphics.load_assets()
|
||||||
|
|
||||||
self._update_startup_loading("Loading start menu", detail="Preparing menu animation", progress=0.9)
|
self._update_startup_loading("Loading start menu", detail="Preparing menu animation", progress=0.9)
|
||||||
self.start_menu_animation = self.render_engine.load_animation(START_MENU_ANIMATION)
|
self.start_menu_animation = self.render_engine.load_animation(config.START_MENU_ANIMATION)
|
||||||
self.start_menu_animation_started_at = time.monotonic()
|
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._update_startup_loading("Starting", detail="Opening intro screen", progress=0.98)
|
||||||
self.render_engine.show_intro(bundle_path("assets", "Rat", "intro.png"))
|
self.render_engine.show_intro(bundle_path("assets", "Rat", "intro.png"))
|
||||||
self.startup_loading_active = False
|
self.startup_loading_active = False
|
||||||
|
|
||||||
self.pointer = (random.randint(1, self.map.width-2), random.randint(1, self.map.height-2))
|
self.pointer = (random.randint(1, self.map.width-2), random.randint(1, self.map.height-2))
|
||||||
self.scroll_cursor()
|
self.graphics.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.start_game()
|
||||||
self.background_texture = None
|
|
||||||
self.combined_scores = None
|
|
||||||
|
|
||||||
|
|
||||||
def get_config(self):
|
def get_config(self):
|
||||||
configs = {}
|
configs = {}
|
||||||
conf_dir = bundle_path("conf")
|
conf_dir = bundle_path("conf")
|
||||||
for file in os.listdir(conf_dir):
|
for file in sorted(os.listdir(conf_dir)):
|
||||||
if file.endswith(".json"):
|
if file.endswith(".json"):
|
||||||
with open(os.path.join(conf_dir, file), encoding="utf-8") as f:
|
with open(os.path.join(conf_dir, file), encoding="utf-8") as f:
|
||||||
configs[file[:-5]] = json.load(f)
|
configs[file[:-5]] = json.load(f)
|
||||||
@@ -194,7 +129,7 @@ class MiceMaze(
|
|||||||
tracks = []
|
tracks = []
|
||||||
for file_name in sorted(os.listdir(music_dir)):
|
for file_name in sorted(os.listdir(music_dir)):
|
||||||
_, extension = os.path.splitext(file_name)
|
_, extension = os.path.splitext(file_name)
|
||||||
if extension.lower() not in SUPPORTED_MUSIC_EXTENSIONS:
|
if extension.lower() not in config.SUPPORTED_MUSIC_EXTENSIONS:
|
||||||
continue
|
continue
|
||||||
if os.path.isfile(os.path.join(music_dir, file_name)):
|
if os.path.isfile(os.path.join(music_dir, file_name)):
|
||||||
tracks.append(file_name)
|
tracks.append(file_name)
|
||||||
@@ -202,7 +137,7 @@ class MiceMaze(
|
|||||||
|
|
||||||
def _resolve_level_music(self, level_index):
|
def _resolve_level_music(self, level_index):
|
||||||
level_number = level_index + 1
|
level_number = level_index + 1
|
||||||
level_music_config = self.configs.get(LEVEL_MUSIC_CONFIG, {})
|
level_music_config = self.configs.get(config.LEVEL_MUSIC_CONFIG, {})
|
||||||
configured_tracks = level_music_config.get("levels", {})
|
configured_tracks = level_music_config.get("levels", {})
|
||||||
configured_track = configured_tracks.get(str(level_number))
|
configured_track = configured_tracks.get(str(level_number))
|
||||||
|
|
||||||
@@ -222,21 +157,21 @@ class MiceMaze(
|
|||||||
return fallback_track
|
return fallback_track
|
||||||
|
|
||||||
def _normalize_difficulty(self, difficulty_key):
|
def _normalize_difficulty(self, difficulty_key):
|
||||||
difficulty_key = DIFFICULTY_ALIASES.get(difficulty_key, difficulty_key)
|
difficulty_key = config.DIFFICULTY_ALIASES.get(difficulty_key, difficulty_key)
|
||||||
if difficulty_key in DIFFICULTY_OPTIONS_BY_KEY:
|
if difficulty_key in config.DIFFICULTY_OPTIONS_BY_KEY:
|
||||||
return difficulty_key
|
return difficulty_key
|
||||||
return DEFAULT_DIFFICULTY
|
return config.DEFAULT_DIFFICULTY
|
||||||
|
|
||||||
def _difficulty_config(self):
|
def _difficulty_config(self):
|
||||||
return DIFFICULTY_OPTIONS_BY_KEY[self.difficulty]
|
return config.DIFFICULTY_OPTIONS_BY_KEY[self.difficulty]
|
||||||
|
|
||||||
def _apply_difficulty(self, difficulty_key, persist=True):
|
def _apply_difficulty(self, difficulty_key, persist=True):
|
||||||
normalized_difficulty = self._normalize_difficulty(difficulty_key)
|
normalized_difficulty = self._normalize_difficulty(difficulty_key)
|
||||||
difficulty_config = DIFFICULTY_OPTIONS_BY_KEY[normalized_difficulty]
|
difficulty_config = config.DIFFICULTY_OPTIONS_BY_KEY[normalized_difficulty]
|
||||||
self.difficulty = normalized_difficulty
|
self.difficulty = normalized_difficulty
|
||||||
self.initial_rat_count = max(
|
self.initial_rat_count = max(
|
||||||
1,
|
1,
|
||||||
int(round(BASE_INITIAL_RATS * difficulty_config["starting_rats_multiplier"])),
|
int(round(config.BASE_INITIAL_RATS * difficulty_config["starting_rats_multiplier"])),
|
||||||
)
|
)
|
||||||
self.rat_speed_multiplier = difficulty_config["speed_multiplier"]
|
self.rat_speed_multiplier = difficulty_config["speed_multiplier"]
|
||||||
if persist:
|
if persist:
|
||||||
@@ -245,20 +180,20 @@ class MiceMaze(
|
|||||||
def _can_adjust_start_difficulty(self):
|
def _can_adjust_start_difficulty(self):
|
||||||
if self.game_end[0]:
|
if self.game_end[0]:
|
||||||
return False
|
return False
|
||||||
return self.game_status == "start_menu" and self.menu_screen == "start"
|
return self.state_machine.current_state == GameState.START_MENU
|
||||||
|
|
||||||
def _cycle_difficulty(self, delta):
|
def _cycle_difficulty(self, delta):
|
||||||
if not self._can_adjust_start_difficulty():
|
if not self._can_adjust_start_difficulty():
|
||||||
return
|
return
|
||||||
|
|
||||||
current_index = 0
|
current_index = 0
|
||||||
for index, option in enumerate(DIFFICULTY_OPTIONS):
|
for index, option in enumerate(config.DIFFICULTY_OPTIONS):
|
||||||
if option["key"] == self.difficulty:
|
if option["key"] == self.difficulty:
|
||||||
current_index = index
|
current_index = index
|
||||||
break
|
break
|
||||||
|
|
||||||
next_index = (current_index + delta) % len(DIFFICULTY_OPTIONS)
|
next_index = (current_index + delta) % len(config.DIFFICULTY_OPTIONS)
|
||||||
self._apply_difficulty(DIFFICULTY_OPTIONS[next_index]["key"])
|
self._apply_difficulty(config.DIFFICULTY_OPTIONS[next_index]["key"])
|
||||||
|
|
||||||
def _is_dat_campaign(self):
|
def _is_dat_campaign(self):
|
||||||
return self.map.source_path.suffix.lower() == ".dat"
|
return self.map.source_path.suffix.lower() == ".dat"
|
||||||
@@ -273,7 +208,7 @@ class MiceMaze(
|
|||||||
|
|
||||||
def _record_run_result(self, completed):
|
def _record_run_result(self, completed):
|
||||||
if not self.run_recorded:
|
if not self.run_recorded:
|
||||||
self.save_score()
|
self.scoring.save_score()
|
||||||
self.profile_integration.update_game_stats(self.points, completed=completed)
|
self.profile_integration.update_game_stats(self.points, completed=completed)
|
||||||
self.run_recorded = True
|
self.run_recorded = True
|
||||||
self.combined_scores = self.profile_integration.get_device_leaderboard(5)
|
self.combined_scores = self.profile_integration.get_device_leaderboard(5)
|
||||||
@@ -282,9 +217,8 @@ class MiceMaze(
|
|||||||
if score is not None:
|
if score is not None:
|
||||||
self.points = max(0, int(score))
|
self.points = max(0, int(score))
|
||||||
self.current_level = max(0, self.total_levels - 1)
|
self.current_level = max(0, self.total_levels - 1)
|
||||||
self.game_status = "paused"
|
self.state_machine.transition_to(GameState.PAUSED)
|
||||||
self.menu_screen = None
|
self.game_end = (True, config.GAME_END_RUN_COMPLETE)
|
||||||
self.game_end = (True, GAME_END_RUN_COMPLETE)
|
|
||||||
self.run_recorded = True
|
self.run_recorded = True
|
||||||
self.combined_scores = self.profile_integration.get_device_leaderboard(5)
|
self.combined_scores = self.profile_integration.get_device_leaderboard(5)
|
||||||
print(
|
print(
|
||||||
@@ -325,8 +259,8 @@ class MiceMaze(
|
|||||||
self.background_texture = None
|
self.background_texture = None
|
||||||
|
|
||||||
# Clear blood layer on game start/restart
|
# Clear blood layer on game start/restart
|
||||||
self.blood_layer_sprites.clear()
|
self.graphics.blood_layer_sprites.clear()
|
||||||
self.cave_foreground_tiles.clear()
|
self.graphics.cave_foreground_tiles.clear()
|
||||||
self.game_end = (False, None)
|
self.game_end = (False, None)
|
||||||
self.game_status = "start_menu"
|
self.game_status = "start_menu"
|
||||||
self.menu_screen = "start"
|
self.menu_screen = "start"
|
||||||
@@ -335,10 +269,10 @@ class MiceMaze(
|
|||||||
self.unit_positions_before.clear()
|
self.unit_positions_before.clear()
|
||||||
self.points = 0
|
self.points = 0
|
||||||
self.pointer = (random.randint(1, self.map.width-2), random.randint(1, self.map.height-2))
|
self.pointer = (random.randint(1, self.map.width-2), random.randint(1, self.map.height-2))
|
||||||
self.scroll_cursor()
|
self.graphics.scroll_cursor()
|
||||||
|
|
||||||
for _ in range(self.initial_rat_count):
|
for _ in range(self.initial_rat_count):
|
||||||
self.spawn_rat()
|
self.unit_manager.spawn_rat()
|
||||||
|
|
||||||
def load_level(self, level_index, preserve_points=True, show_menu=False, menu_screen=None):
|
def load_level(self, level_index, preserve_points=True, show_menu=False, menu_screen=None):
|
||||||
target_level_index = self._normalize_target_level(level_index)
|
target_level_index = self._normalize_target_level(level_index)
|
||||||
@@ -359,20 +293,20 @@ class MiceMaze(
|
|||||||
self.map.height
|
self.map.height
|
||||||
)
|
)
|
||||||
|
|
||||||
if getattr(self, "loaded_theme_index", None) != next_theme_index:
|
if self.graphics.loaded_theme_index != next_theme_index:
|
||||||
print(
|
print(
|
||||||
f"[flow] theme switch needed: loaded_theme={getattr(self, 'loaded_theme_index', None)} "
|
f"[flow] theme switch needed: loaded_theme={self.graphics.loaded_theme_index} "
|
||||||
f"-> next_theme={next_theme_index}"
|
f"-> next_theme={next_theme_index}"
|
||||||
)
|
)
|
||||||
self.load_assets()
|
self.graphics.load_assets()
|
||||||
else:
|
else:
|
||||||
print(f"[flow] theme unchanged: reusing theme {next_theme_index}")
|
print(f"[flow] theme unchanged: reusing theme {next_theme_index}")
|
||||||
self.units.clear()
|
self.units.clear()
|
||||||
self.unit_positions.clear()
|
self.unit_positions.clear()
|
||||||
self.unit_positions_before.clear()
|
self.unit_positions_before.clear()
|
||||||
self.blood_stains = {}
|
self.blood_stains = {}
|
||||||
self.blood_layer_sprites.clear()
|
self.graphics.blood_layer_sprites.clear()
|
||||||
self.cave_foreground_tiles.clear()
|
self.graphics.cave_foreground_tiles.clear()
|
||||||
self.background_texture = None
|
self.background_texture = None
|
||||||
self.ammo = {
|
self.ammo = {
|
||||||
"bomb": {"count": 2, "max": 8},
|
"bomb": {"count": 2, "max": 8},
|
||||||
@@ -383,7 +317,7 @@ class MiceMaze(
|
|||||||
self.combined_scores = None
|
self.combined_scores = None
|
||||||
self.game_end = (False, None)
|
self.game_end = (False, None)
|
||||||
self.pointer = (random.randint(1, self.map.width-2), random.randint(1, self.map.height-2))
|
self.pointer = (random.randint(1, self.map.width-2), random.randint(1, self.map.height-2))
|
||||||
self.scroll_cursor()
|
self.graphics.scroll_cursor()
|
||||||
if not preserve_points:
|
if not preserve_points:
|
||||||
self.points = 0
|
self.points = 0
|
||||||
self.run_recorded = False
|
self.run_recorded = False
|
||||||
@@ -395,16 +329,18 @@ class MiceMaze(
|
|||||||
flush=True,
|
flush=True,
|
||||||
)
|
)
|
||||||
for _ in range(self.initial_rat_count):
|
for _ in range(self.initial_rat_count):
|
||||||
self.spawn_rat()
|
self.unit_manager.spawn_rat()
|
||||||
|
|
||||||
if show_menu:
|
if show_menu:
|
||||||
self.game_status = "start_menu"
|
if menu_screen == "start":
|
||||||
self.menu_screen = menu_screen or "level_intro"
|
self.state_machine.transition_to(GameState.START_MENU)
|
||||||
print(f"[flow] level loaded into menu state: menu_screen={self.menu_screen} points={self.points}")
|
else:
|
||||||
|
self.state_machine.transition_to(GameState.PLAYING)
|
||||||
|
print(f"[flow] level loaded into state: {self.state_machine.current_state} points={self.points}")
|
||||||
else:
|
else:
|
||||||
self.game_status = "game"
|
self.state_machine.transition_to(GameState.PLAYING)
|
||||||
self.menu_screen = None
|
print(f"[flow] level loaded directly into PLAYING: points={self.points}")
|
||||||
print(f"[flow] level loaded directly into gameplay: level={self.current_level + 1} points={self.points}")
|
|
||||||
|
|
||||||
def advance_level(self):
|
def advance_level(self):
|
||||||
print(f"[flow] advance_level called from level={self.current_level + 1} points={self.points}")
|
print(f"[flow] advance_level called from level={self.current_level + 1} points={self.points}")
|
||||||
@@ -414,8 +350,8 @@ class MiceMaze(
|
|||||||
|
|
||||||
if self._is_last_dat_level():
|
if self._is_last_dat_level():
|
||||||
print("[flow] advance_level -> reached end of DAT campaign")
|
print("[flow] advance_level -> reached end of DAT campaign")
|
||||||
self.game_end = (True, GAME_END_RUN_COMPLETE)
|
self.game_end = (True, config.GAME_END_RUN_COMPLETE)
|
||||||
self.game_status = "paused"
|
self.state_machine.transition_to(GameState.VICTORY)
|
||||||
self._record_run_result(completed=True)
|
self._record_run_result(completed=True)
|
||||||
return
|
return
|
||||||
|
|
||||||
@@ -425,14 +361,14 @@ class MiceMaze(
|
|||||||
|
|
||||||
def reset_game(self):
|
def reset_game(self):
|
||||||
print(
|
print(
|
||||||
f"[flow] reset_game called: game_end={self.game_end} game_status={self.game_status} "
|
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}"
|
f"menu_screen={self.menu_screen} points={self.points} level={self.current_level + 1}"
|
||||||
)
|
)
|
||||||
if self.game_end[0]:
|
if self.game_end[0]:
|
||||||
if self.game_end[1] == GAME_END_LEVEL_CLEAR:
|
if self.game_end[1] == config.GAME_END_LEVEL_CLEAR:
|
||||||
print("[flow] reset_game -> post-victory path")
|
print("[flow] reset_game -> post-victory path")
|
||||||
self.advance_level()
|
self.advance_level()
|
||||||
elif self.game_end[1] == GAME_END_RUN_COMPLETE:
|
elif self.game_end[1] == config.GAME_END_RUN_COMPLETE:
|
||||||
print("[flow] reset_game -> DAT run complete, returning to start menu")
|
print("[flow] reset_game -> DAT run complete, returning to start menu")
|
||||||
self.start_menu_animation_started_at = time.monotonic()
|
self.start_menu_animation_started_at = time.monotonic()
|
||||||
self.load_level(0, preserve_points=False, show_menu=True, menu_screen="start")
|
self.load_level(0, preserve_points=False, show_menu=True, menu_screen="start")
|
||||||
@@ -442,18 +378,14 @@ class MiceMaze(
|
|||||||
self.load_level(0, preserve_points=False, show_menu=True, menu_screen="start")
|
self.load_level(0, preserve_points=False, show_menu=True, menu_screen="start")
|
||||||
return
|
return
|
||||||
|
|
||||||
if self.game_status == "paused":
|
if self.state_machine.current_state == GameState.PAUSED:
|
||||||
print("[flow] reset_game -> unpausing current level")
|
print("[flow] reset_game -> unpausing current level")
|
||||||
self.game_status = "game"
|
self.state_machine.transition_to(GameState.PLAYING)
|
||||||
return
|
return
|
||||||
|
|
||||||
if self.game_status == "start_menu":
|
if self.state_machine.current_state == GameState.START_MENU:
|
||||||
print(f"[flow] reset_game -> leaving menu_screen={self.menu_screen} and entering gameplay")
|
print(f"[flow] reset_game -> entering gameplay")
|
||||||
if self.menu_screen == "start":
|
self.state_machine.transition_to(GameState.PLAYING)
|
||||||
self.load_level(0, preserve_points=False, show_menu=False)
|
|
||||||
return
|
|
||||||
self.game_status = "game"
|
|
||||||
self.menu_screen = None
|
|
||||||
return
|
return
|
||||||
|
|
||||||
print("[flow] reset_game -> hard reload current level")
|
print("[flow] reset_game -> hard reload current level")
|
||||||
@@ -477,310 +409,7 @@ class MiceMaze(
|
|||||||
def _can_adjust_audio_menu(self):
|
def _can_adjust_audio_menu(self):
|
||||||
if self.game_end[0]:
|
if self.game_end[0]:
|
||||||
return False
|
return False
|
||||||
return self.game_status == "paused"
|
return self.state_machine.current_state == GameState.PAUSED
|
||||||
|
|
||||||
def _draw_start_menu_difficulty_selector(self, x, y, width, height):
|
|
||||||
colors = START_MENU_COLORS
|
|
||||||
difficulty_config = self._difficulty_config()
|
|
||||||
accent = difficulty_config["accent"]
|
|
||||||
render_engine = self.render_engine
|
|
||||||
center_x = x + width // 2
|
|
||||||
compact_selector = height <= 72
|
|
||||||
title_font = self._menu_font(render_engine.target_size[1] // 38)
|
|
||||||
value_font = self._menu_font(render_engine.target_size[1] // 23)
|
|
||||||
arrow_font = value_font
|
|
||||||
section_gap = 4 if compact_selector else 6
|
|
||||||
title_line_height = max(14, render_engine.target_size[1] // 32)
|
|
||||||
value_line_height = max(18, render_engine.target_size[1] // 24)
|
|
||||||
current_y = y + 2
|
|
||||||
|
|
||||||
render_engine.draw_text("Difficulty", title_font, ("center", current_y), colors["muted"])
|
|
||||||
current_y += title_line_height + section_gap
|
|
||||||
|
|
||||||
arrow_offset = max(34, min(52, width // 7))
|
|
||||||
arrow_y = current_y - (1 if compact_selector else 0)
|
|
||||||
render_engine.draw_text("<", arrow_font, (center_x - arrow_offset, arrow_y), colors["muted"])
|
|
||||||
render_engine.draw_text(">", arrow_font, (center_x + arrow_offset, arrow_y), colors["muted"])
|
|
||||||
|
|
||||||
render_engine.draw_text(
|
|
||||||
difficulty_config["label"],
|
|
||||||
value_font,
|
|
||||||
("center", current_y),
|
|
||||||
accent,
|
|
||||||
)
|
|
||||||
current_y += value_line_height
|
|
||||||
|
|
||||||
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"],
|
|
||||||
)
|
|
||||||
|
|
||||||
difficulty_width = max(360, min(panel_width - 160, 760))
|
|
||||||
difficulty_height = 54 if compact_menu else max(72, target_height // 9)
|
|
||||||
difficulty_x = target_width // 2 - difficulty_width // 2
|
|
||||||
difficulty_y = info_y + line_gap * len(subtitle_lines) + (12 if compact_menu else 18)
|
|
||||||
self._draw_start_menu_difficulty_selector(
|
|
||||||
difficulty_x,
|
|
||||||
difficulty_y,
|
|
||||||
difficulty_width,
|
|
||||||
difficulty_height,
|
|
||||||
)
|
|
||||||
|
|
||||||
cta_y = difficulty_y + difficulty_height + (12 if compact_menu else 24)
|
|
||||||
render_engine.draw_text(
|
|
||||||
"Press Return to start",
|
|
||||||
cta_font,
|
|
||||||
("center", cta_y),
|
|
||||||
colors["text"],
|
|
||||||
)
|
|
||||||
if compact_menu:
|
|
||||||
render_engine.draw_text(
|
|
||||||
"Arrows change difficulty",
|
|
||||||
hint_font,
|
|
||||||
("center", cta_y + line_gap),
|
|
||||||
colors["muted"],
|
|
||||||
)
|
|
||||||
render_engine.draw_text(
|
|
||||||
"Esc quits M toggles audio",
|
|
||||||
hint_font,
|
|
||||||
("center", cta_y + line_gap + 18),
|
|
||||||
colors["muted"],
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
render_engine.draw_text(
|
|
||||||
"Arrows change difficulty 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):
|
def _apply_volume_setting(self, setting_name, value):
|
||||||
clamped = max(0, min(100, int(value)))
|
clamped = max(0, min(100, int(value)))
|
||||||
@@ -799,7 +428,7 @@ class MiceMaze(
|
|||||||
return
|
return
|
||||||
if not self._can_adjust_audio_menu():
|
if not self._can_adjust_audio_menu():
|
||||||
return
|
return
|
||||||
self.start_menu_selection = (self.start_menu_selection - 1) % len(START_MENU_AUDIO_OPTIONS)
|
self.start_menu_selection = (self.start_menu_selection - 1) % len(config.START_MENU_AUDIO_OPTIONS)
|
||||||
|
|
||||||
def menu_down(self):
|
def menu_down(self):
|
||||||
if self._can_adjust_start_difficulty():
|
if self._can_adjust_start_difficulty():
|
||||||
@@ -807,12 +436,12 @@ class MiceMaze(
|
|||||||
return
|
return
|
||||||
if not self._can_adjust_audio_menu():
|
if not self._can_adjust_audio_menu():
|
||||||
return
|
return
|
||||||
self.start_menu_selection = (self.start_menu_selection + 1) % len(START_MENU_AUDIO_OPTIONS)
|
self.start_menu_selection = (self.start_menu_selection + 1) % len(config.START_MENU_AUDIO_OPTIONS)
|
||||||
|
|
||||||
def _adjust_selected_volume(self, delta):
|
def _adjust_selected_volume(self, delta):
|
||||||
if not self._can_adjust_audio_menu():
|
if not self._can_adjust_audio_menu():
|
||||||
return
|
return
|
||||||
setting_name, _ = START_MENU_AUDIO_OPTIONS[self.start_menu_selection]
|
setting_name, _ = config.START_MENU_AUDIO_OPTIONS[self.start_menu_selection]
|
||||||
current_value = getattr(self, setting_name)
|
current_value = getattr(self, setting_name)
|
||||||
self._apply_volume_setting(setting_name, current_value + delta)
|
self._apply_volume_setting(setting_name, current_value + delta)
|
||||||
|
|
||||||
@@ -820,48 +449,46 @@ class MiceMaze(
|
|||||||
if self._can_adjust_start_difficulty():
|
if self._can_adjust_start_difficulty():
|
||||||
self._cycle_difficulty(-1)
|
self._cycle_difficulty(-1)
|
||||||
return
|
return
|
||||||
self._adjust_selected_volume(-VOLUME_STEP)
|
self._adjust_selected_volume(-config.VOLUME_STEP)
|
||||||
|
|
||||||
def menu_right(self):
|
def menu_right(self):
|
||||||
if self._can_adjust_start_difficulty():
|
if self._can_adjust_start_difficulty():
|
||||||
self._cycle_difficulty(1)
|
self._cycle_difficulty(1)
|
||||||
return
|
return
|
||||||
self._adjust_selected_volume(VOLUME_STEP)
|
self._adjust_selected_volume(config.VOLUME_STEP)
|
||||||
|
|
||||||
def update_background_music(self):
|
def update_background_music(self):
|
||||||
if self.game_end[0] and self.game_end[1] == GAME_END_RUN_COMPLETE:
|
if self.game_end[0] and self.game_end[1] == config.GAME_END_RUN_COMPLETE:
|
||||||
self.render_engine.play_music(RUN_COMPLETE_MUSIC, loop=True)
|
self.render_engine.play_music(config.RUN_COMPLETE_MUSIC, loop=True)
|
||||||
return
|
return
|
||||||
if self.game_status == "game" and not self.game_end[0]:
|
if self.state_machine.current_state == GameState.PLAYING and not self.game_end[0]:
|
||||||
if self.current_level_music:
|
if self.current_level_music:
|
||||||
self.render_engine.play_music(self.current_level_music, loop=True)
|
self.render_engine.play_music(self.current_level_music, loop=True)
|
||||||
return
|
return
|
||||||
if self.game_status == "start_menu" and not self.game_end[0]:
|
if self.state_machine.current_state == GameState.START_MENU and not self.game_end[0]:
|
||||||
if self.menu_screen == "start":
|
self.render_engine.play_music(config.START_MENU_MUSIC, loop=True)
|
||||||
self.render_engine.play_music(START_MENU_MUSIC, loop=True)
|
return
|
||||||
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()
|
self.render_engine.pause_music()
|
||||||
|
|
||||||
def update_maze(self):
|
def update_maze(self):
|
||||||
self.update_background_music()
|
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():
|
if self.game_over():
|
||||||
return
|
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("unit")
|
||||||
self.render_engine.delete_tag("effect")
|
self.render_engine.delete_tag("effect")
|
||||||
self.render_engine.delete_tag("cave")
|
self.render_engine.delete_tag("cave")
|
||||||
@@ -873,7 +500,7 @@ class MiceMaze(
|
|||||||
|
|
||||||
# First pass: Register all units in collision system BEFORE move
|
# First pass: Register all units in collision system BEFORE move
|
||||||
# This allows bombs/gas to find victims during their move()
|
# This allows bombs/gas to find victims during their move()
|
||||||
for unit in self.units.values():
|
for unit in sorted(self.units.values(), key=lambda u: int(u.id)):
|
||||||
# Calculate bbox if not yet set (first frame)
|
# Calculate bbox if not yet set (first frame)
|
||||||
if not hasattr(unit, 'bbox') or unit.bbox == (0, 0, 0, 0):
|
if not hasattr(unit, 'bbox') or unit.bbox == (0, 0, 0, 0):
|
||||||
# Temporary bbox based on position
|
# Temporary bbox based on position
|
||||||
@@ -895,7 +522,7 @@ class MiceMaze(
|
|||||||
self.unit_positions_before.setdefault(unit.position_before, []).append(unit)
|
self.unit_positions_before.setdefault(unit.position_before, []).append(unit)
|
||||||
|
|
||||||
# Second pass: move all units (can now access collision system)
|
# Second pass: move all units (can now access collision system)
|
||||||
for unit in self.units.copy().values():
|
for unit in sorted(self.units.values(), key=lambda u: int(u.id)):
|
||||||
unit.move()
|
unit.move()
|
||||||
|
|
||||||
# Third pass: Update collision system with new positions after move
|
# Third pass: Update collision system with new positions after move
|
||||||
@@ -903,7 +530,7 @@ class MiceMaze(
|
|||||||
self.unit_positions.clear()
|
self.unit_positions.clear()
|
||||||
self.unit_positions_before.clear()
|
self.unit_positions_before.clear()
|
||||||
|
|
||||||
for unit in self.units.values():
|
for unit in sorted(self.units.values(), key=lambda u: int(u.id)):
|
||||||
# Register with updated positions/bbox from move()
|
# Register with updated positions/bbox from move()
|
||||||
self.collision_system.register_unit(
|
self.collision_system.register_unit(
|
||||||
unit.id,
|
unit.id,
|
||||||
@@ -916,23 +543,23 @@ class MiceMaze(
|
|||||||
self.unit_positions.setdefault(unit.position, []).append(unit)
|
self.unit_positions.setdefault(unit.position, []).append(unit)
|
||||||
self.unit_positions_before.setdefault(unit.position_before, []).append(unit)
|
self.unit_positions_before.setdefault(unit.position_before, []).append(unit)
|
||||||
# Fourth pass: check collisions and draw
|
# Fourth pass: check collisions and draw
|
||||||
for unit in self.units.copy().values():
|
for unit in sorted(self.units.values(), key=lambda u: int(u.id)):
|
||||||
unit.collisions()
|
unit.collisions()
|
||||||
unit.draw()
|
unit.draw()
|
||||||
|
|
||||||
self.draw_cave_foreground()
|
self.graphics.draw_cave_foreground()
|
||||||
self.render_engine.draw_pointer(self.pointer[0] * self.cell_size, self.pointer[1] * self.cell_size)
|
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.render_engine.update_status(f"Mice: {self.unit_manager.count_rats()} - Points: {self.points}")
|
||||||
self.refill_ammo()
|
self.refill_ammo()
|
||||||
self.render_engine.update_ammo(self.ammo, self.assets)
|
self.render_engine.update_ammo(self.ammo, self.graphics.assets)
|
||||||
self.scroll()
|
self.controls.scroll()
|
||||||
self.render_engine.new_cycle(50, self.update_maze)
|
self.render_engine.new_cycle(50, self.update_maze)
|
||||||
|
|
||||||
def run(self):
|
def run(self):
|
||||||
self.render_engine.mainloop(update=self.update_maze, bg_update=self.draw_maze)
|
self.render_engine.mainloop(update=self.update_maze, bg_update=self.graphics.draw_maze)
|
||||||
|
|
||||||
# ==================== GAME OVER LOGIC ====================
|
# ==================== GAME OVER LOGIC ====================
|
||||||
|
|
||||||
@@ -941,37 +568,36 @@ class MiceMaze(
|
|||||||
if self.combined_scores is None:
|
if self.combined_scores is None:
|
||||||
self.combined_scores = self.profile_integration.get_device_leaderboard(5)
|
self.combined_scores = self.profile_integration.get_device_leaderboard(5)
|
||||||
|
|
||||||
if self.game_end[1] == GAME_END_DEFEAT:
|
if self.game_end[1] == config.GAME_END_DEFEAT:
|
||||||
self.render_engine.dialog(
|
self.render_engine.dialog(
|
||||||
"Game Over: Mice are too many!",
|
"Game Over: Mice are too many!",
|
||||||
image=self.assets.get("lose", self.assets["BMP_WEWIN"]),
|
image=self.graphics.assets.get("lose", self.graphics.assets["BMP_WEWIN"]),
|
||||||
image_scale=0.48,
|
|
||||||
subtitle=f"Reached level: {self.current_level + 1}\nPress Return to go back to the start menu",
|
subtitle=f"Reached level: {self.current_level + 1}\nPress Return to go back to the start menu",
|
||||||
scores=self.combined_scores,
|
scores=self.combined_scores,
|
||||||
)
|
)
|
||||||
elif self.game_end[1] == GAME_END_RUN_COMPLETE:
|
elif self.game_end[1] == config.GAME_END_RUN_COMPLETE:
|
||||||
self.render_engine.dialog(
|
self.render_engine.dialog(
|
||||||
"THE END",
|
"THE END",
|
||||||
image=self.assets.get("end", self.assets["BMP_WEWIN"]),
|
image=self.graphics.assets.get("end", self.graphics.assets["BMP_WEWIN"]),
|
||||||
current_score=self.points,
|
current_score=self.points,
|
||||||
style="run_complete",
|
style="run_complete",
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
self.render_engine.dialog(
|
self.render_engine.dialog(
|
||||||
f"Level {self.current_level + 1} Clear! Points: {self.points}",
|
f"Level {self.current_level + 1} Clear! Points: {self.points}",
|
||||||
image=self.assets.get("clear", self.assets["BMP_WEWIN"]),
|
image=self.graphics.assets.get("clear", self.graphics.assets["BMP_WEWIN"]),
|
||||||
subtitle="Press Return for the next level",
|
subtitle="Press Return for the next level",
|
||||||
scores=self.combined_scores
|
scores=self.combined_scores
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
return True
|
return True
|
||||||
count_rats = self.count_rats()
|
count_rats = self.unit_manager.count_rats()
|
||||||
if count_rats > 200:
|
if count_rats > 200:
|
||||||
self.render_engine.stop_sound()
|
self.render_engine.stop_sound()
|
||||||
self.render_engine.play_sound("WEWIN.WAV")
|
self.render_engine.play_sound("WEWIN.WAV")
|
||||||
self.game_end = (True, GAME_END_DEFEAT)
|
self.game_end = (True, config.GAME_END_DEFEAT)
|
||||||
self.game_status = "paused"
|
self.state_machine.transition_to(GameState.GAME_OVER)
|
||||||
print(f"[flow] defeat reached: rats={count_rats} points={self.points} level={self.current_level + 1}")
|
print(f"[flow] defeat reached: rats={count_rats} points={self.points} level={self.current_level + 1}")
|
||||||
|
|
||||||
self._record_run_result(completed=False)
|
self._record_run_result(completed=False)
|
||||||
@@ -980,23 +606,22 @@ class MiceMaze(
|
|||||||
if not count_rats and not any(isinstance(unit, points.Point) for unit in self.units.values()):
|
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.stop_sound()
|
||||||
self.render_engine.play_sound("VICTORY.WAV")
|
self.render_engine.play_sound("VICTORY.WAV")
|
||||||
self.game_status = "paused"
|
|
||||||
|
|
||||||
if self._is_last_dat_level():
|
if self._is_last_dat_level():
|
||||||
self.render_engine.play_sound("WELLDONE.WAV", tag="effects")
|
self.render_engine.play_sound("WELLDONE.WAV", tag="effects")
|
||||||
self.game_end = (True, GAME_END_RUN_COMPLETE)
|
self.game_end = (True, config.GAME_END_RUN_COMPLETE)
|
||||||
|
self.state_machine.transition_to(GameState.VICTORY)
|
||||||
self._record_run_result(completed=True)
|
self._record_run_result(completed=True)
|
||||||
print(f"[flow] final DAT victory reached: points={self.points} level={self.current_level + 1}")
|
print(f"[flow] final DAT victory reached: points={self.points} level={self.current_level + 1}")
|
||||||
else:
|
else:
|
||||||
self.game_end = (True, GAME_END_LEVEL_CLEAR)
|
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)
|
self.combined_scores = self.profile_integration.get_device_leaderboard(5)
|
||||||
print(f"[flow] victory reached: points={self.points} level={self.current_level + 1}")
|
print(f"[flow] victory reached: points={self.points} level={self.current_level + 1}")
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def parse_args():
|
def parse_args():
|
||||||
def parse_debug_score(value):
|
def parse_debug_score(value):
|
||||||
try:
|
try:
|
||||||
@@ -1033,4 +658,3 @@ if __name__ == "__main__":
|
|||||||
if args.debug_run_complete_dialog:
|
if args.debug_run_complete_dialog:
|
||||||
solver.activate_debug_run_complete_dialog(score=args.debug_final_score)
|
solver.activate_debug_run_complete_dialog(score=args.debug_final_score)
|
||||||
solver.run()
|
solver.run()
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,88 @@
|
|||||||
|
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import unittest
|
||||||
|
import random
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
# Add current directory to path
|
||||||
|
sys.path.append(os.getcwd())
|
||||||
|
|
||||||
|
# Set SDL to use dummy video driver for headless environments
|
||||||
|
os.environ["SDL_VIDEODRIVER"] = "dummy"
|
||||||
|
os.environ["SDL_AUDIODRIVER"] = "dummy"
|
||||||
|
os.environ["MICE_DISABLE_JOYSTICK"] = "1"
|
||||||
|
|
||||||
|
from rats import MiceMaze
|
||||||
|
from engine.state_machine import GameState
|
||||||
|
from engine.sdl2 import GameWindow
|
||||||
|
|
||||||
|
def mock_init_audio(self):
|
||||||
|
self.music_enabled = False
|
||||||
|
self.audio_devs = {"base": 0, "effects": 0, "music": 0}
|
||||||
|
self.sound_volume = 0
|
||||||
|
self.music_volume = 0
|
||||||
|
|
||||||
|
class TestGameOverFlow(unittest.TestCase):
|
||||||
|
@classmethod
|
||||||
|
def setUpClass(cls):
|
||||||
|
GameWindow.show_intro = lambda *args, **kwargs: None
|
||||||
|
GameWindow.show_loading_screen = lambda *args, **kwargs: None
|
||||||
|
GameWindow._init_audio_system = mock_init_audio
|
||||||
|
GameWindow.play_sound = lambda *args, **kwargs: None
|
||||||
|
GameWindow.stop_sound = lambda *args, **kwargs: None
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
random.seed(42)
|
||||||
|
# Load a standard level
|
||||||
|
self.game = MiceMaze("assets/Rat/level.dat", level_index=0)
|
||||||
|
self.game.game_status = "game"
|
||||||
|
self.game.state_machine.transition_to(GameState.PLAYING)
|
||||||
|
|
||||||
|
def test_defeat_triggers_game_over_state(self):
|
||||||
|
"""Verify that having > 200 rats triggers GAME_OVER state, not PAUSED."""
|
||||||
|
print("\nTesting defeat condition (> 200 rats)...")
|
||||||
|
|
||||||
|
# Manually inject > 200 rats to trigger defeat
|
||||||
|
from units.rat import Male
|
||||||
|
for i in range(210):
|
||||||
|
self.game.unit_manager.spawn_unit(Male, (1, 1))
|
||||||
|
|
||||||
|
# Run one update cycle
|
||||||
|
self.game.update_maze()
|
||||||
|
|
||||||
|
# Check end condition
|
||||||
|
self.assertTrue(self.game.game_end[0], "Game should be marked as ended")
|
||||||
|
self.assertEqual(self.game.game_end[1], "defeat", "End reason should be defeat")
|
||||||
|
|
||||||
|
# CRITICAL CHECK: State must be GAME_OVER, not PAUSED
|
||||||
|
current_state = self.game.state_machine.current_state
|
||||||
|
print(f"Current State: {current_state}")
|
||||||
|
print(f"Legacy game_status: {self.game.game_status}")
|
||||||
|
|
||||||
|
self.assertEqual(current_state, GameState.GAME_OVER,
|
||||||
|
f"Game should be in GAME_OVER state, but was in {current_state}")
|
||||||
|
|
||||||
|
def test_victory_triggers_victory_state(self):
|
||||||
|
"""Verify that clearing all rats triggers VICTORY state."""
|
||||||
|
print("\nTesting victory condition (0 rats)...")
|
||||||
|
|
||||||
|
# Clear all units
|
||||||
|
self.game.units.clear()
|
||||||
|
|
||||||
|
# Run one update cycle
|
||||||
|
self.game.update_maze()
|
||||||
|
|
||||||
|
# Check end condition
|
||||||
|
self.assertTrue(self.game.game_end[0], "Game should be marked as ended")
|
||||||
|
self.assertEqual(self.game.game_end[1], "level_clear", "End reason should be level_clear")
|
||||||
|
|
||||||
|
# State must be VICTORY
|
||||||
|
current_state = self.game.state_machine.current_state
|
||||||
|
print(f"Current State: {current_state}")
|
||||||
|
|
||||||
|
self.assertEqual(current_state, GameState.VICTORY,
|
||||||
|
f"Game should be in VICTORY state, but was in {current_state}")
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
unittest.main()
|
||||||
@@ -0,0 +1,148 @@
|
|||||||
|
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import random
|
||||||
|
import unittest
|
||||||
|
import json
|
||||||
|
import time
|
||||||
|
from pathlib import Path
|
||||||
|
from PIL import Image
|
||||||
|
|
||||||
|
# Add current directory to path
|
||||||
|
sys.path.append(os.getcwd())
|
||||||
|
|
||||||
|
# Set SDL to use dummy video driver for headless environments
|
||||||
|
os.environ["SDL_VIDEODRIVER"] = "dummy"
|
||||||
|
os.environ["SDL_AUDIODRIVER"] = "dummy"
|
||||||
|
os.environ["MICE_DISABLE_JOYSTICK"] = "1"
|
||||||
|
|
||||||
|
from rats import MiceMaze
|
||||||
|
from engine.sdl2 import GameWindow
|
||||||
|
|
||||||
|
def mock_init_audio(self):
|
||||||
|
self.music_enabled = False
|
||||||
|
self.audio_devs = {"base": 0, "effects": 0, "music": 0}
|
||||||
|
self.sound_volume = 0
|
||||||
|
self.music_volume = 0
|
||||||
|
|
||||||
|
import uuid
|
||||||
|
|
||||||
|
# Global counter for deterministic UUIDs
|
||||||
|
_uuid_counter = 0
|
||||||
|
def mock_uuid4():
|
||||||
|
global _uuid_counter
|
||||||
|
val = _uuid_counter
|
||||||
|
_uuid_counter += 1
|
||||||
|
return val
|
||||||
|
|
||||||
|
class NonRegressionTest(unittest.TestCase):
|
||||||
|
@classmethod
|
||||||
|
def setUpClass(cls):
|
||||||
|
# Deterministic UUIDs
|
||||||
|
uuid.uuid4 = mock_uuid4
|
||||||
|
# Monkeypatch SDL2 methods that block or show windows
|
||||||
|
GameWindow.show_intro = lambda *args, **kwargs: None
|
||||||
|
GameWindow.show_loading_screen = lambda *args, **kwargs: None
|
||||||
|
# Disable audio and its effects
|
||||||
|
GameWindow._init_audio_system = mock_init_audio
|
||||||
|
GameWindow.play_sound = lambda *args, **kwargs: None
|
||||||
|
GameWindow.stop_sound = lambda *args, **kwargs: None
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
global _uuid_counter
|
||||||
|
_uuid_counter = 0
|
||||||
|
|
||||||
|
# Initial seed for constructor
|
||||||
|
random.seed(42)
|
||||||
|
|
||||||
|
# Initialize game
|
||||||
|
self.game = MiceMaze("assets/Rat/level.dat", level_index=0)
|
||||||
|
|
||||||
|
# Re-seed again to clear entropy consumed by asset loading (blood stains etc)
|
||||||
|
# This ensures the game logic starts from a consistent random state
|
||||||
|
random.seed(42)
|
||||||
|
_uuid_counter = 0 # Reset UUIDs too for initial spawns
|
||||||
|
self.game.start_game()
|
||||||
|
|
||||||
|
# Trigger background generation once to consume those random calls
|
||||||
|
# before the simulation starts, ensuring stability.
|
||||||
|
self.game.graphics.draw_maze()
|
||||||
|
|
||||||
|
# Override dynamic attributes
|
||||||
|
self.game.start_menu_animation_started_at = 0
|
||||||
|
|
||||||
|
# Skip menu and start gameplay
|
||||||
|
self.game.game_status = "game"
|
||||||
|
self.game.menu_screen = None
|
||||||
|
|
||||||
|
def test_simulation_run(self):
|
||||||
|
steps = 200
|
||||||
|
states = []
|
||||||
|
|
||||||
|
# Output directory
|
||||||
|
output_dir = Path("tests/non_regression_output")
|
||||||
|
output_dir.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
|
print(f"Starting simulation for {steps} steps...")
|
||||||
|
|
||||||
|
for i in range(steps):
|
||||||
|
# Advance game state
|
||||||
|
self.game.update_maze()
|
||||||
|
|
||||||
|
# Every 50 steps, record state and screenshot
|
||||||
|
if i % 50 == 0 or i == steps - 1:
|
||||||
|
state = self.dump_game_state()
|
||||||
|
state["frame"] = i
|
||||||
|
states.append(state)
|
||||||
|
|
||||||
|
# Visual snapshot
|
||||||
|
# Note: In dummy driver, RenderReadPixels might return empty/black
|
||||||
|
# but we'll try anyway. If it fails, we rely on the state JSON.
|
||||||
|
try:
|
||||||
|
self.game.graphics.draw_maze()
|
||||||
|
# Manually draw units because we are not in mainloop
|
||||||
|
for unit in list(self.game.units.values()):
|
||||||
|
unit.draw()
|
||||||
|
|
||||||
|
img = self.game.render_engine.capture_frame()
|
||||||
|
img_path = output_dir / f"frame_{i:04d}.png"
|
||||||
|
img.save(img_path)
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Warning: Could not capture frame at step {i}: {e}")
|
||||||
|
|
||||||
|
# Save states to JSON
|
||||||
|
states_path = output_dir / "states.json"
|
||||||
|
with open(states_path, "w") as f:
|
||||||
|
json.dump(states, f, indent=2)
|
||||||
|
|
||||||
|
print(f"Simulation complete. Outputs saved to {output_dir}")
|
||||||
|
|
||||||
|
def dump_game_state(self):
|
||||||
|
state = {
|
||||||
|
"points": self.game.points,
|
||||||
|
"unit_count": len(self.game.units),
|
||||||
|
"units": []
|
||||||
|
}
|
||||||
|
# Sort units by ID to be deterministic
|
||||||
|
sorted_units = sorted(self.game.units.items(), key=lambda x: int(x[0]))
|
||||||
|
|
||||||
|
for uid, unit in sorted_units:
|
||||||
|
unit_state = {
|
||||||
|
"id": str(uid),
|
||||||
|
"type": unit.__class__.__name__,
|
||||||
|
"pos": list(unit.position),
|
||||||
|
"pos_before": list(unit.position_before),
|
||||||
|
"partial_move": float(unit.partial_move),
|
||||||
|
"age": int(unit.age),
|
||||||
|
}
|
||||||
|
if hasattr(unit, "sex"):
|
||||||
|
unit_state["sex"] = unit.sex
|
||||||
|
if hasattr(unit, "direction"):
|
||||||
|
unit_state["direction"] = unit.direction
|
||||||
|
|
||||||
|
state["units"].append(unit_state)
|
||||||
|
|
||||||
|
return state
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
unittest.main()
|
||||||
@@ -0,0 +1,123 @@
|
|||||||
|
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import random
|
||||||
|
import unittest
|
||||||
|
import json
|
||||||
|
from pathlib import Path
|
||||||
|
from PIL import Image
|
||||||
|
import numpy as np
|
||||||
|
|
||||||
|
# Add current directory to path
|
||||||
|
sys.path.append(os.getcwd())
|
||||||
|
|
||||||
|
# Set SDL to use dummy video driver for headless environments
|
||||||
|
os.environ["SDL_VIDEODRIVER"] = "dummy"
|
||||||
|
os.environ["SDL_AUDIODRIVER"] = "dummy"
|
||||||
|
os.environ["MICE_DISABLE_JOYSTICK"] = "1"
|
||||||
|
|
||||||
|
from rats import MiceMaze
|
||||||
|
from engine.sdl2 import GameWindow
|
||||||
|
|
||||||
|
def mock_init_audio(self):
|
||||||
|
self.music_enabled = False
|
||||||
|
self.audio_devs = {"base": 0, "effects": 0, "music": 0}
|
||||||
|
self.sound_volume = 0
|
||||||
|
self.music_volume = 0
|
||||||
|
|
||||||
|
import uuid
|
||||||
|
|
||||||
|
# Global counter for deterministic UUIDs
|
||||||
|
_uuid_counter = 0
|
||||||
|
def mock_uuid4():
|
||||||
|
global _uuid_counter
|
||||||
|
val = _uuid_counter
|
||||||
|
_uuid_counter += 1
|
||||||
|
return val
|
||||||
|
|
||||||
|
class NonRegressionVerification(unittest.TestCase):
|
||||||
|
@classmethod
|
||||||
|
def setUpClass(cls):
|
||||||
|
# Deterministic UUIDs
|
||||||
|
uuid.uuid4 = mock_uuid4
|
||||||
|
GameWindow.show_intro = lambda *args, **kwargs: None
|
||||||
|
GameWindow.show_loading_screen = lambda *args, **kwargs: None
|
||||||
|
GameWindow._init_audio_system = mock_init_audio
|
||||||
|
GameWindow.play_sound = lambda *args, **kwargs: None
|
||||||
|
GameWindow.stop_sound = lambda *args, **kwargs: None
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
global _uuid_counter
|
||||||
|
_uuid_counter = 0
|
||||||
|
random.seed(42)
|
||||||
|
self.game = MiceMaze("assets/Rat/level.dat", level_index=0)
|
||||||
|
|
||||||
|
# Re-seed again to clear entropy consumed by asset loading
|
||||||
|
random.seed(42)
|
||||||
|
_uuid_counter = 0
|
||||||
|
self.game.start_game()
|
||||||
|
|
||||||
|
# Trigger background generation once to consume those random calls
|
||||||
|
# before the simulation starts, ensuring stability.
|
||||||
|
self.game.graphics.draw_maze()
|
||||||
|
|
||||||
|
self.game.game_status = "game"
|
||||||
|
self.game.menu_screen = None
|
||||||
|
|
||||||
|
def test_verify_against_golden_master(self):
|
||||||
|
golden_master_dir = Path("tests/golden_master")
|
||||||
|
if not golden_master_dir.exists():
|
||||||
|
self.skipTest("Golden master not found. Run recording first.")
|
||||||
|
|
||||||
|
with open(golden_master_dir / "states.json", "r") as f:
|
||||||
|
golden_states = json.load(f)
|
||||||
|
|
||||||
|
steps = 200
|
||||||
|
golden_idx = 0
|
||||||
|
|
||||||
|
print(f"Verifying against golden master for {steps} steps...")
|
||||||
|
|
||||||
|
for i in range(steps):
|
||||||
|
self.game.update_maze()
|
||||||
|
|
||||||
|
if i % 50 == 0 or i == steps - 1:
|
||||||
|
current_state = self.dump_game_state()
|
||||||
|
golden_state = golden_states[golden_idx]
|
||||||
|
|
||||||
|
# Compare unit count
|
||||||
|
self.assertEqual(current_state["unit_count"], golden_state["unit_count"],
|
||||||
|
f"Unit count mismatch at step {i}")
|
||||||
|
|
||||||
|
# Compare units
|
||||||
|
for u_idx, (curr_u, gold_u) in enumerate(zip(current_state["units"], golden_state["units"])):
|
||||||
|
self.assertEqual(curr_u["id"], gold_u["id"], f"Unit ID mismatch at step {i}, index {u_idx}")
|
||||||
|
self.assertEqual(curr_u["type"], gold_u["type"], f"Unit type mismatch at step {i}, unit {curr_u['id']}")
|
||||||
|
self.assertEqual(curr_u["pos"], gold_u["pos"], f"Unit pos mismatch at step {i}, unit {curr_u['id']}")
|
||||||
|
self.assertAlmostEqual(curr_u["partial_move"], gold_u["partial_move"], places=5,
|
||||||
|
msg=f"Unit partial_move mismatch at step {i}, unit {curr_u['id']}")
|
||||||
|
|
||||||
|
golden_idx += 1
|
||||||
|
|
||||||
|
print("Verification successful! No regressions detected.")
|
||||||
|
|
||||||
|
def dump_game_state(self):
|
||||||
|
state = {
|
||||||
|
"points": self.game.points,
|
||||||
|
"unit_count": len(self.game.units),
|
||||||
|
"units": []
|
||||||
|
}
|
||||||
|
# Sort units by ID to be deterministic
|
||||||
|
sorted_units = sorted(self.game.units.items(), key=lambda x: int(x[0]))
|
||||||
|
|
||||||
|
for uid, unit in sorted_units:
|
||||||
|
unit_state = {
|
||||||
|
"id": str(uid),
|
||||||
|
"type": unit.__class__.__name__,
|
||||||
|
"pos": list(unit.position),
|
||||||
|
"partial_move": float(unit.partial_move),
|
||||||
|
}
|
||||||
|
state["units"].append(unit_state)
|
||||||
|
return state
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
unittest.main()
|
||||||
|
After Width: | Height: | Size: 18 KiB |
|
After Width: | Height: | Size: 18 KiB |
|
After Width: | Height: | Size: 18 KiB |
|
After Width: | Height: | Size: 18 KiB |
|
After Width: | Height: | Size: 18 KiB |
@@ -0,0 +1,437 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"points": 0,
|
||||||
|
"unit_count": 5,
|
||||||
|
"units": [
|
||||||
|
{
|
||||||
|
"id": "0",
|
||||||
|
"type": "Female",
|
||||||
|
"pos": [
|
||||||
|
12,
|
||||||
|
1
|
||||||
|
],
|
||||||
|
"pos_before": [
|
||||||
|
13,
|
||||||
|
1
|
||||||
|
],
|
||||||
|
"partial_move": 0.1,
|
||||||
|
"age": 1,
|
||||||
|
"sex": "FEMALE",
|
||||||
|
"direction": "LEFT"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "2",
|
||||||
|
"type": "Male",
|
||||||
|
"pos": [
|
||||||
|
4,
|
||||||
|
11
|
||||||
|
],
|
||||||
|
"pos_before": [
|
||||||
|
5,
|
||||||
|
11
|
||||||
|
],
|
||||||
|
"partial_move": 0.1,
|
||||||
|
"age": 1,
|
||||||
|
"sex": "MALE",
|
||||||
|
"direction": "LEFT"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "3",
|
||||||
|
"type": "Male",
|
||||||
|
"pos": [
|
||||||
|
8,
|
||||||
|
30
|
||||||
|
],
|
||||||
|
"pos_before": [
|
||||||
|
7,
|
||||||
|
30
|
||||||
|
],
|
||||||
|
"partial_move": 0.1,
|
||||||
|
"age": 1,
|
||||||
|
"sex": "MALE",
|
||||||
|
"direction": "RIGHT"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "4",
|
||||||
|
"type": "Male",
|
||||||
|
"pos": [
|
||||||
|
19,
|
||||||
|
1
|
||||||
|
],
|
||||||
|
"pos_before": [
|
||||||
|
20,
|
||||||
|
1
|
||||||
|
],
|
||||||
|
"partial_move": 0.1,
|
||||||
|
"age": 1,
|
||||||
|
"sex": "MALE",
|
||||||
|
"direction": "LEFT"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "5",
|
||||||
|
"type": "Female",
|
||||||
|
"pos": [
|
||||||
|
9,
|
||||||
|
11
|
||||||
|
],
|
||||||
|
"pos_before": [
|
||||||
|
10,
|
||||||
|
11
|
||||||
|
],
|
||||||
|
"partial_move": 0.1,
|
||||||
|
"age": 1,
|
||||||
|
"sex": "FEMALE",
|
||||||
|
"direction": "LEFT"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"frame": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"points": 0,
|
||||||
|
"unit_count": 5,
|
||||||
|
"units": [
|
||||||
|
{
|
||||||
|
"id": "0",
|
||||||
|
"type": "Female",
|
||||||
|
"pos": [
|
||||||
|
7,
|
||||||
|
1
|
||||||
|
],
|
||||||
|
"pos_before": [
|
||||||
|
8,
|
||||||
|
1
|
||||||
|
],
|
||||||
|
"partial_move": 0.1,
|
||||||
|
"age": 51,
|
||||||
|
"sex": "FEMALE",
|
||||||
|
"direction": "LEFT"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "2",
|
||||||
|
"type": "Male",
|
||||||
|
"pos": [
|
||||||
|
6,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
"pos_before": [
|
||||||
|
5,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
"partial_move": 0.1,
|
||||||
|
"age": 51,
|
||||||
|
"sex": "MALE",
|
||||||
|
"direction": "RIGHT"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "3",
|
||||||
|
"type": "Male",
|
||||||
|
"pos": [
|
||||||
|
13,
|
||||||
|
30
|
||||||
|
],
|
||||||
|
"pos_before": [
|
||||||
|
12,
|
||||||
|
30
|
||||||
|
],
|
||||||
|
"partial_move": 0.1,
|
||||||
|
"age": 51,
|
||||||
|
"sex": "MALE",
|
||||||
|
"direction": "RIGHT"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "4",
|
||||||
|
"type": "Male",
|
||||||
|
"pos": [
|
||||||
|
18,
|
||||||
|
5
|
||||||
|
],
|
||||||
|
"pos_before": [
|
||||||
|
18,
|
||||||
|
4
|
||||||
|
],
|
||||||
|
"partial_move": 0.1,
|
||||||
|
"age": 51,
|
||||||
|
"sex": "MALE",
|
||||||
|
"direction": "DOWN"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "5",
|
||||||
|
"type": "Female",
|
||||||
|
"pos": [
|
||||||
|
4,
|
||||||
|
11
|
||||||
|
],
|
||||||
|
"pos_before": [
|
||||||
|
5,
|
||||||
|
11
|
||||||
|
],
|
||||||
|
"partial_move": 0.1,
|
||||||
|
"age": 51,
|
||||||
|
"sex": "FEMALE",
|
||||||
|
"direction": "LEFT"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"frame": 50
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"points": 0,
|
||||||
|
"unit_count": 5,
|
||||||
|
"units": [
|
||||||
|
{
|
||||||
|
"id": "0",
|
||||||
|
"type": "Female",
|
||||||
|
"pos": [
|
||||||
|
6,
|
||||||
|
5
|
||||||
|
],
|
||||||
|
"pos_before": [
|
||||||
|
6,
|
||||||
|
4
|
||||||
|
],
|
||||||
|
"partial_move": 0.1,
|
||||||
|
"age": 101,
|
||||||
|
"sex": "FEMALE",
|
||||||
|
"direction": "DOWN"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "2",
|
||||||
|
"type": "Male",
|
||||||
|
"pos": [
|
||||||
|
4,
|
||||||
|
5
|
||||||
|
],
|
||||||
|
"pos_before": [
|
||||||
|
5,
|
||||||
|
5
|
||||||
|
],
|
||||||
|
"partial_move": 0.1,
|
||||||
|
"age": 101,
|
||||||
|
"sex": "MALE",
|
||||||
|
"direction": "LEFT"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "3",
|
||||||
|
"type": "Male",
|
||||||
|
"pos": [
|
||||||
|
18,
|
||||||
|
30
|
||||||
|
],
|
||||||
|
"pos_before": [
|
||||||
|
17,
|
||||||
|
30
|
||||||
|
],
|
||||||
|
"partial_move": 0.1,
|
||||||
|
"age": 101,
|
||||||
|
"sex": "MALE",
|
||||||
|
"direction": "RIGHT"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "4",
|
||||||
|
"type": "Male",
|
||||||
|
"pos": [
|
||||||
|
18,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
"pos_before": [
|
||||||
|
19,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
"partial_move": 0.1,
|
||||||
|
"age": 101,
|
||||||
|
"sex": "MALE",
|
||||||
|
"direction": "LEFT"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "5",
|
||||||
|
"type": "Female",
|
||||||
|
"pos": [
|
||||||
|
3,
|
||||||
|
15
|
||||||
|
],
|
||||||
|
"pos_before": [
|
||||||
|
4,
|
||||||
|
15
|
||||||
|
],
|
||||||
|
"partial_move": 0.1,
|
||||||
|
"age": 101,
|
||||||
|
"sex": "FEMALE",
|
||||||
|
"direction": "LEFT"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"frame": 100
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"points": 0,
|
||||||
|
"unit_count": 5,
|
||||||
|
"units": [
|
||||||
|
{
|
||||||
|
"id": "0",
|
||||||
|
"type": "Female",
|
||||||
|
"pos": [
|
||||||
|
10,
|
||||||
|
6
|
||||||
|
],
|
||||||
|
"pos_before": [
|
||||||
|
10,
|
||||||
|
5
|
||||||
|
],
|
||||||
|
"partial_move": 0.1,
|
||||||
|
"age": 151,
|
||||||
|
"sex": "FEMALE",
|
||||||
|
"direction": "DOWN"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "2",
|
||||||
|
"type": "Male",
|
||||||
|
"pos": [
|
||||||
|
1,
|
||||||
|
3
|
||||||
|
],
|
||||||
|
"pos_before": [
|
||||||
|
1,
|
||||||
|
4
|
||||||
|
],
|
||||||
|
"partial_move": 0.1,
|
||||||
|
"age": 151,
|
||||||
|
"sex": "MALE",
|
||||||
|
"direction": "UP"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "3",
|
||||||
|
"type": "Male",
|
||||||
|
"pos": [
|
||||||
|
22,
|
||||||
|
29
|
||||||
|
],
|
||||||
|
"pos_before": [
|
||||||
|
22,
|
||||||
|
30
|
||||||
|
],
|
||||||
|
"partial_move": 0.1,
|
||||||
|
"age": 151,
|
||||||
|
"sex": "MALE",
|
||||||
|
"direction": "UP"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "4",
|
||||||
|
"type": "Male",
|
||||||
|
"pos": [
|
||||||
|
15,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
"pos_before": [
|
||||||
|
16,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
"partial_move": 0.1,
|
||||||
|
"age": 151,
|
||||||
|
"sex": "MALE",
|
||||||
|
"direction": "LEFT"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "5",
|
||||||
|
"type": "Female",
|
||||||
|
"pos": [
|
||||||
|
4,
|
||||||
|
15
|
||||||
|
],
|
||||||
|
"pos_before": [
|
||||||
|
3,
|
||||||
|
15
|
||||||
|
],
|
||||||
|
"partial_move": 0.1,
|
||||||
|
"age": 151,
|
||||||
|
"sex": "FEMALE",
|
||||||
|
"direction": "RIGHT"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"frame": 150
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"points": 0,
|
||||||
|
"unit_count": 5,
|
||||||
|
"units": [
|
||||||
|
{
|
||||||
|
"id": "0",
|
||||||
|
"type": "Female",
|
||||||
|
"pos": [
|
||||||
|
11,
|
||||||
|
7
|
||||||
|
],
|
||||||
|
"pos_before": [
|
||||||
|
11,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
"partial_move": 0.95,
|
||||||
|
"age": 200,
|
||||||
|
"sex": "FEMALE",
|
||||||
|
"direction": "UP"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "2",
|
||||||
|
"type": "Male",
|
||||||
|
"pos": [
|
||||||
|
3,
|
||||||
|
1
|
||||||
|
],
|
||||||
|
"pos_before": [
|
||||||
|
2,
|
||||||
|
1
|
||||||
|
],
|
||||||
|
"partial_move": 0.95,
|
||||||
|
"age": 200,
|
||||||
|
"sex": "MALE",
|
||||||
|
"direction": "RIGHT"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "3",
|
||||||
|
"type": "Male",
|
||||||
|
"pos": [
|
||||||
|
23,
|
||||||
|
26
|
||||||
|
],
|
||||||
|
"pos_before": [
|
||||||
|
22,
|
||||||
|
26
|
||||||
|
],
|
||||||
|
"partial_move": 0.95,
|
||||||
|
"age": 200,
|
||||||
|
"sex": "MALE",
|
||||||
|
"direction": "RIGHT"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "4",
|
||||||
|
"type": "Male",
|
||||||
|
"pos": [
|
||||||
|
13,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
"pos_before": [
|
||||||
|
13,
|
||||||
|
7
|
||||||
|
],
|
||||||
|
"partial_move": 0.95,
|
||||||
|
"age": 200,
|
||||||
|
"sex": "MALE",
|
||||||
|
"direction": "DOWN"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "5",
|
||||||
|
"type": "Female",
|
||||||
|
"pos": [
|
||||||
|
4,
|
||||||
|
11
|
||||||
|
],
|
||||||
|
"pos_before": [
|
||||||
|
4,
|
||||||
|
12
|
||||||
|
],
|
||||||
|
"partial_move": 0.95,
|
||||||
|
"age": 200,
|
||||||
|
"sex": "FEMALE",
|
||||||
|
"direction": "UP"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"frame": 199
|
||||||
|
}
|
||||||
|
]
|
||||||
|
After Width: | Height: | Size: 18 KiB |
|
After Width: | Height: | Size: 18 KiB |
|
After Width: | Height: | Size: 18 KiB |
|
After Width: | Height: | Size: 18 KiB |
|
After Width: | Height: | Size: 18 KiB |
@@ -0,0 +1,437 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"points": 0,
|
||||||
|
"unit_count": 5,
|
||||||
|
"units": [
|
||||||
|
{
|
||||||
|
"id": "0",
|
||||||
|
"type": "Female",
|
||||||
|
"pos": [
|
||||||
|
12,
|
||||||
|
1
|
||||||
|
],
|
||||||
|
"pos_before": [
|
||||||
|
13,
|
||||||
|
1
|
||||||
|
],
|
||||||
|
"partial_move": 0.1,
|
||||||
|
"age": 1,
|
||||||
|
"sex": "FEMALE",
|
||||||
|
"direction": "LEFT"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "2",
|
||||||
|
"type": "Male",
|
||||||
|
"pos": [
|
||||||
|
4,
|
||||||
|
11
|
||||||
|
],
|
||||||
|
"pos_before": [
|
||||||
|
5,
|
||||||
|
11
|
||||||
|
],
|
||||||
|
"partial_move": 0.1,
|
||||||
|
"age": 1,
|
||||||
|
"sex": "MALE",
|
||||||
|
"direction": "LEFT"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "3",
|
||||||
|
"type": "Male",
|
||||||
|
"pos": [
|
||||||
|
8,
|
||||||
|
30
|
||||||
|
],
|
||||||
|
"pos_before": [
|
||||||
|
7,
|
||||||
|
30
|
||||||
|
],
|
||||||
|
"partial_move": 0.1,
|
||||||
|
"age": 1,
|
||||||
|
"sex": "MALE",
|
||||||
|
"direction": "RIGHT"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "4",
|
||||||
|
"type": "Male",
|
||||||
|
"pos": [
|
||||||
|
19,
|
||||||
|
1
|
||||||
|
],
|
||||||
|
"pos_before": [
|
||||||
|
20,
|
||||||
|
1
|
||||||
|
],
|
||||||
|
"partial_move": 0.1,
|
||||||
|
"age": 1,
|
||||||
|
"sex": "MALE",
|
||||||
|
"direction": "LEFT"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "5",
|
||||||
|
"type": "Female",
|
||||||
|
"pos": [
|
||||||
|
9,
|
||||||
|
11
|
||||||
|
],
|
||||||
|
"pos_before": [
|
||||||
|
10,
|
||||||
|
11
|
||||||
|
],
|
||||||
|
"partial_move": 0.1,
|
||||||
|
"age": 1,
|
||||||
|
"sex": "FEMALE",
|
||||||
|
"direction": "LEFT"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"frame": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"points": 0,
|
||||||
|
"unit_count": 5,
|
||||||
|
"units": [
|
||||||
|
{
|
||||||
|
"id": "0",
|
||||||
|
"type": "Female",
|
||||||
|
"pos": [
|
||||||
|
7,
|
||||||
|
1
|
||||||
|
],
|
||||||
|
"pos_before": [
|
||||||
|
8,
|
||||||
|
1
|
||||||
|
],
|
||||||
|
"partial_move": 0.1,
|
||||||
|
"age": 51,
|
||||||
|
"sex": "FEMALE",
|
||||||
|
"direction": "LEFT"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "2",
|
||||||
|
"type": "Male",
|
||||||
|
"pos": [
|
||||||
|
6,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
"pos_before": [
|
||||||
|
5,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
"partial_move": 0.1,
|
||||||
|
"age": 51,
|
||||||
|
"sex": "MALE",
|
||||||
|
"direction": "RIGHT"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "3",
|
||||||
|
"type": "Male",
|
||||||
|
"pos": [
|
||||||
|
13,
|
||||||
|
30
|
||||||
|
],
|
||||||
|
"pos_before": [
|
||||||
|
12,
|
||||||
|
30
|
||||||
|
],
|
||||||
|
"partial_move": 0.1,
|
||||||
|
"age": 51,
|
||||||
|
"sex": "MALE",
|
||||||
|
"direction": "RIGHT"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "4",
|
||||||
|
"type": "Male",
|
||||||
|
"pos": [
|
||||||
|
18,
|
||||||
|
5
|
||||||
|
],
|
||||||
|
"pos_before": [
|
||||||
|
18,
|
||||||
|
4
|
||||||
|
],
|
||||||
|
"partial_move": 0.1,
|
||||||
|
"age": 51,
|
||||||
|
"sex": "MALE",
|
||||||
|
"direction": "DOWN"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "5",
|
||||||
|
"type": "Female",
|
||||||
|
"pos": [
|
||||||
|
4,
|
||||||
|
11
|
||||||
|
],
|
||||||
|
"pos_before": [
|
||||||
|
5,
|
||||||
|
11
|
||||||
|
],
|
||||||
|
"partial_move": 0.1,
|
||||||
|
"age": 51,
|
||||||
|
"sex": "FEMALE",
|
||||||
|
"direction": "LEFT"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"frame": 50
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"points": 0,
|
||||||
|
"unit_count": 5,
|
||||||
|
"units": [
|
||||||
|
{
|
||||||
|
"id": "0",
|
||||||
|
"type": "Female",
|
||||||
|
"pos": [
|
||||||
|
6,
|
||||||
|
5
|
||||||
|
],
|
||||||
|
"pos_before": [
|
||||||
|
6,
|
||||||
|
4
|
||||||
|
],
|
||||||
|
"partial_move": 0.1,
|
||||||
|
"age": 101,
|
||||||
|
"sex": "FEMALE",
|
||||||
|
"direction": "DOWN"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "2",
|
||||||
|
"type": "Male",
|
||||||
|
"pos": [
|
||||||
|
4,
|
||||||
|
5
|
||||||
|
],
|
||||||
|
"pos_before": [
|
||||||
|
5,
|
||||||
|
5
|
||||||
|
],
|
||||||
|
"partial_move": 0.1,
|
||||||
|
"age": 101,
|
||||||
|
"sex": "MALE",
|
||||||
|
"direction": "LEFT"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "3",
|
||||||
|
"type": "Male",
|
||||||
|
"pos": [
|
||||||
|
18,
|
||||||
|
30
|
||||||
|
],
|
||||||
|
"pos_before": [
|
||||||
|
17,
|
||||||
|
30
|
||||||
|
],
|
||||||
|
"partial_move": 0.1,
|
||||||
|
"age": 101,
|
||||||
|
"sex": "MALE",
|
||||||
|
"direction": "RIGHT"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "4",
|
||||||
|
"type": "Male",
|
||||||
|
"pos": [
|
||||||
|
18,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
"pos_before": [
|
||||||
|
19,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
"partial_move": 0.1,
|
||||||
|
"age": 101,
|
||||||
|
"sex": "MALE",
|
||||||
|
"direction": "LEFT"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "5",
|
||||||
|
"type": "Female",
|
||||||
|
"pos": [
|
||||||
|
3,
|
||||||
|
15
|
||||||
|
],
|
||||||
|
"pos_before": [
|
||||||
|
4,
|
||||||
|
15
|
||||||
|
],
|
||||||
|
"partial_move": 0.1,
|
||||||
|
"age": 101,
|
||||||
|
"sex": "FEMALE",
|
||||||
|
"direction": "LEFT"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"frame": 100
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"points": 0,
|
||||||
|
"unit_count": 5,
|
||||||
|
"units": [
|
||||||
|
{
|
||||||
|
"id": "0",
|
||||||
|
"type": "Female",
|
||||||
|
"pos": [
|
||||||
|
10,
|
||||||
|
6
|
||||||
|
],
|
||||||
|
"pos_before": [
|
||||||
|
10,
|
||||||
|
5
|
||||||
|
],
|
||||||
|
"partial_move": 0.1,
|
||||||
|
"age": 151,
|
||||||
|
"sex": "FEMALE",
|
||||||
|
"direction": "DOWN"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "2",
|
||||||
|
"type": "Male",
|
||||||
|
"pos": [
|
||||||
|
1,
|
||||||
|
3
|
||||||
|
],
|
||||||
|
"pos_before": [
|
||||||
|
1,
|
||||||
|
4
|
||||||
|
],
|
||||||
|
"partial_move": 0.1,
|
||||||
|
"age": 151,
|
||||||
|
"sex": "MALE",
|
||||||
|
"direction": "UP"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "3",
|
||||||
|
"type": "Male",
|
||||||
|
"pos": [
|
||||||
|
22,
|
||||||
|
29
|
||||||
|
],
|
||||||
|
"pos_before": [
|
||||||
|
22,
|
||||||
|
30
|
||||||
|
],
|
||||||
|
"partial_move": 0.1,
|
||||||
|
"age": 151,
|
||||||
|
"sex": "MALE",
|
||||||
|
"direction": "UP"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "4",
|
||||||
|
"type": "Male",
|
||||||
|
"pos": [
|
||||||
|
15,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
"pos_before": [
|
||||||
|
16,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
"partial_move": 0.1,
|
||||||
|
"age": 151,
|
||||||
|
"sex": "MALE",
|
||||||
|
"direction": "LEFT"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "5",
|
||||||
|
"type": "Female",
|
||||||
|
"pos": [
|
||||||
|
4,
|
||||||
|
15
|
||||||
|
],
|
||||||
|
"pos_before": [
|
||||||
|
3,
|
||||||
|
15
|
||||||
|
],
|
||||||
|
"partial_move": 0.1,
|
||||||
|
"age": 151,
|
||||||
|
"sex": "FEMALE",
|
||||||
|
"direction": "RIGHT"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"frame": 150
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"points": 0,
|
||||||
|
"unit_count": 5,
|
||||||
|
"units": [
|
||||||
|
{
|
||||||
|
"id": "0",
|
||||||
|
"type": "Female",
|
||||||
|
"pos": [
|
||||||
|
11,
|
||||||
|
7
|
||||||
|
],
|
||||||
|
"pos_before": [
|
||||||
|
11,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
"partial_move": 0.95,
|
||||||
|
"age": 200,
|
||||||
|
"sex": "FEMALE",
|
||||||
|
"direction": "UP"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "2",
|
||||||
|
"type": "Male",
|
||||||
|
"pos": [
|
||||||
|
3,
|
||||||
|
1
|
||||||
|
],
|
||||||
|
"pos_before": [
|
||||||
|
2,
|
||||||
|
1
|
||||||
|
],
|
||||||
|
"partial_move": 0.95,
|
||||||
|
"age": 200,
|
||||||
|
"sex": "MALE",
|
||||||
|
"direction": "RIGHT"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "3",
|
||||||
|
"type": "Male",
|
||||||
|
"pos": [
|
||||||
|
23,
|
||||||
|
26
|
||||||
|
],
|
||||||
|
"pos_before": [
|
||||||
|
22,
|
||||||
|
26
|
||||||
|
],
|
||||||
|
"partial_move": 0.95,
|
||||||
|
"age": 200,
|
||||||
|
"sex": "MALE",
|
||||||
|
"direction": "RIGHT"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "4",
|
||||||
|
"type": "Male",
|
||||||
|
"pos": [
|
||||||
|
13,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
"pos_before": [
|
||||||
|
13,
|
||||||
|
7
|
||||||
|
],
|
||||||
|
"partial_move": 0.95,
|
||||||
|
"age": 200,
|
||||||
|
"sex": "MALE",
|
||||||
|
"direction": "DOWN"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "5",
|
||||||
|
"type": "Female",
|
||||||
|
"pos": [
|
||||||
|
4,
|
||||||
|
11
|
||||||
|
],
|
||||||
|
"pos_before": [
|
||||||
|
4,
|
||||||
|
12
|
||||||
|
],
|
||||||
|
"partial_move": 0.95,
|
||||||
|
"age": 200,
|
||||||
|
"sex": "FEMALE",
|
||||||
|
"direction": "UP"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"frame": 199
|
||||||
|
}
|
||||||
|
]
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
set -u
|
||||||
|
|
||||||
|
section() {
|
||||||
|
printf '\n===== %s =====\n' "$1"
|
||||||
|
}
|
||||||
|
|
||||||
|
run_cmd() {
|
||||||
|
printf '\n$ %s\n' "$*"
|
||||||
|
"$@" 2>&1 || printf '[exit=%s]\n' "$?"
|
||||||
|
}
|
||||||
|
|
||||||
|
run_shell() {
|
||||||
|
printf '\n$ %s\n' "$1"
|
||||||
|
sh -c "$1" 2>&1 || printf '[exit=%s]\n' "$?"
|
||||||
|
}
|
||||||
|
|
||||||
|
section "system"
|
||||||
|
run_cmd date
|
||||||
|
run_cmd uname -a
|
||||||
|
run_shell 'cat /etc/os-release 2>/dev/null || true'
|
||||||
|
|
||||||
|
section "bluetooth binaries"
|
||||||
|
run_shell 'for path in /bin/bluetoothctl /usr/bin/bluetoothctl /usr/sbin/bluetoothd /usr/libexec/bluetooth/bluetoothd /usr/bin/dbus-daemon /bin/dbus-daemon /usr/bin/dbus-send /bin/dbus-send /usr/bin/btmgmt /bin/btmgmt /usr/bin/hciconfig /bin/hciconfig /usr/sbin/rfkill /bin/rfkill; do if [ -e "$path" ]; then ls -l "$path"; fi; done'
|
||||||
|
|
||||||
|
section "processes"
|
||||||
|
run_shell 'pidof bluetoothd || true'
|
||||||
|
run_shell 'pidof dbus-daemon || true'
|
||||||
|
run_shell 'ps w | grep -E "bluetoothd|dbus-daemon|pipewire|wireplumber|bluealsa" | grep -v grep || true'
|
||||||
|
|
||||||
|
section "dbus"
|
||||||
|
run_shell 'ls -l /run/dbus/system_bus_socket /var/run/dbus/system_bus_socket 2>/dev/null || true'
|
||||||
|
run_shell 'dbus-send --system --dest=org.freedesktop.DBus --type=method_call --print-reply / org.freedesktop.DBus.ListNames 2>/dev/null | head -n 40'
|
||||||
|
|
||||||
|
section "bluetoothctl"
|
||||||
|
run_shell 'timeout 8 bluetoothctl --version 2>/dev/null || bluetoothctl --version 2>/dev/null || true'
|
||||||
|
run_shell 'timeout 8 bluetoothctl show'
|
||||||
|
|
||||||
|
section "kernel"
|
||||||
|
run_shell 'lsmod | grep -i -E "bluetooth|btusb|btmtk|hci_uart|rtl" || true'
|
||||||
|
run_shell 'dmesg | grep -i -E "bluetooth|bluez|hci|rtl_bt|btusb|btmtk" | tail -n 120 || true'
|
||||||
|
|
||||||
|
section "controllers"
|
||||||
|
run_shell 'hciconfig -a'
|
||||||
|
run_shell 'btmgmt info'
|
||||||
|
run_shell 'rfkill list'
|
||||||
|
|
||||||
|
section "sysfs"
|
||||||
|
run_shell 'find /sys/class/bluetooth -maxdepth 2 -type f 2>/dev/null | head -n 50 || true'
|
||||||
|
run_shell 'find /sys/class/rfkill -maxdepth 2 -type f 2>/dev/null | head -n 50 || true'
|
||||||
|
|
||||||
|
section "firmware"
|
||||||
|
run_shell 'find /lib/firmware -maxdepth 3 -type f 2>/dev/null | grep -i -E "rtlbt|bluetooth|mt76|mt79|mediatek" | head -n 120 || true'
|
||||||
|
|
||||||
|
section "audio stack"
|
||||||
|
run_shell 'ps w | grep -E "pipewire|wireplumber|pulseaudio|bluealsa" | grep -v grep || true'
|
||||||
|
run_shell 'command -v wpctl >/dev/null 2>&1 && wpctl status || true'
|
||||||
|
run_shell 'command -v pactl >/dev/null 2>&1 && pactl info || true'
|
||||||
|
|
||||||
|
section "done"
|
||||||
|
printf '\nDiagnostic complete.\n'
|
||||||
@@ -36,7 +36,7 @@ class Bomb(Unit):
|
|||||||
n = 1
|
n = 1
|
||||||
if n < 0:
|
if n < 0:
|
||||||
n = 0
|
n = 0
|
||||||
image = self.game.bomb_assets[n]
|
image = self.game.graphics.bomb_assets[n]
|
||||||
image_size = self.game.render_engine.get_image_size(image)
|
image_size = self.game.render_engine.get_image_size(image)
|
||||||
self.rat_image = image
|
self.rat_image = image
|
||||||
partial_x, partial_y = 0, 0
|
partial_x, partial_y = 0, 0
|
||||||
@@ -70,7 +70,7 @@ class Timer(Bomb):
|
|||||||
print(f"Unit {target_unit.id} already dead")
|
print(f"Unit {target_unit.id} already dead")
|
||||||
|
|
||||||
# Bomb-specific behavior: create explosion
|
# Bomb-specific behavior: create explosion
|
||||||
self.game.spawn_unit(Explosion, target_unit.position)
|
self.game.unit_manager.spawn_unit(Explosion, target_unit.position)
|
||||||
|
|
||||||
# Collect all explosion positions using vectorized approach
|
# Collect all explosion positions using vectorized approach
|
||||||
explosion_positions = []
|
explosion_positions = []
|
||||||
@@ -94,7 +94,7 @@ class Timer(Bomb):
|
|||||||
|
|
||||||
# Create all explosions at once
|
# Create all explosions at once
|
||||||
for pos in explosion_positions:
|
for pos in explosion_positions:
|
||||||
self.game.spawn_unit(Explosion, pos)
|
self.game.unit_manager.spawn_unit(Explosion, pos)
|
||||||
|
|
||||||
# Use optimized collision system to get all rats in explosion area
|
# Use optimized collision system to get all rats in explosion area
|
||||||
# This replaces the nested loop with a single vectorized operation
|
# This replaces the nested loop with a single vectorized operation
|
||||||
@@ -105,7 +105,7 @@ class Timer(Bomb):
|
|||||||
|
|
||||||
# Kill all victims with score multiplier
|
# Kill all victims with score multiplier
|
||||||
for victim_id in victim_ids:
|
for victim_id in victim_ids:
|
||||||
victim = self.game.get_unit_by_id(victim_id)
|
victim = self.game.unit_manager.get_unit_by_id(victim_id)
|
||||||
if victim and victim.id in self.game.units:
|
if victim and victim.id in self.game.units:
|
||||||
# Determine position based on partial_move
|
# Determine position based on partial_move
|
||||||
victim_pos = victim.position if victim.partial_move >= 0.5 else victim.position_before
|
victim_pos = victim.position if victim.partial_move >= 0.5 else victim.position_before
|
||||||
@@ -128,7 +128,7 @@ class Explosion(Bomb):
|
|||||||
self.die()
|
self.die()
|
||||||
|
|
||||||
def draw(self):
|
def draw(self):
|
||||||
image = self.game.assets["BMP_EXPLOSION"]
|
image = self.game.graphics.assets["BMP_EXPLOSION"]
|
||||||
image_size = self.game.render_engine.get_image_size(image)
|
image_size = self.game.render_engine.get_image_size(image)
|
||||||
partial_x, partial_y = 0, 0
|
partial_x, partial_y = 0, 0
|
||||||
|
|
||||||
@@ -186,7 +186,7 @@ class NuclearBomb(Unit):
|
|||||||
|
|
||||||
def draw(self):
|
def draw(self):
|
||||||
"""Draw nuclear bomb on position"""
|
"""Draw nuclear bomb on position"""
|
||||||
image = self.game.assets["BMP_NUCLEAR"]
|
image = self.game.graphics.assets["BMP_NUCLEAR"]
|
||||||
image_size = self.game.render_engine.get_image_size(image)
|
image_size = self.game.render_engine.get_image_size(image)
|
||||||
|
|
||||||
x_pos = self.position[0] * self.game.cell_size + (self.game.cell_size - image_size[0]) // 2
|
x_pos = self.position[0] * self.game.cell_size + (self.game.cell_size - image_size[0]) // 2
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ class Gas(Unit):
|
|||||||
)
|
)
|
||||||
|
|
||||||
for victim_id in victim_ids:
|
for victim_id in victim_ids:
|
||||||
victim = self.game.get_unit_by_id(victim_id)
|
victim = self.game.unit_manager.get_unit_by_id(victim_id)
|
||||||
if victim and isinstance(victim, Rat):
|
if victim and isinstance(victim, Rat):
|
||||||
if victim.partial_move > 0.5:
|
if victim.partial_move > 0.5:
|
||||||
victim.gassed += 1
|
victim.gassed += 1
|
||||||
@@ -43,14 +43,14 @@ class Gas(Unit):
|
|||||||
)
|
)
|
||||||
|
|
||||||
for victim_id in victim_ids_before:
|
for victim_id in victim_ids_before:
|
||||||
victim = self.game.get_unit_by_id(victim_id)
|
victim = self.game.unit_manager.get_unit_by_id(victim_id)
|
||||||
if victim and isinstance(victim, Rat):
|
if victim and isinstance(victim, Rat):
|
||||||
if victim.partial_move < 0.5:
|
if victim.partial_move < 0.5:
|
||||||
victim.gassed += 1
|
victim.gassed += 1
|
||||||
|
|
||||||
if self.age % self.speed:
|
if self.age % self.speed:
|
||||||
return
|
return
|
||||||
parent = self.game.get_unit_by_id(self.parent_id)
|
parent = self.game.unit_manager.get_unit_by_id(self.parent_id)
|
||||||
if (parent) or self.parent_id is None:
|
if (parent) or self.parent_id is None:
|
||||||
print(f"Gas at {self.position} is spreading")
|
print(f"Gas at {self.position} is spreading")
|
||||||
# Spread gas to adjacent cells
|
# Spread gas to adjacent cells
|
||||||
@@ -61,7 +61,7 @@ class Gas(Unit):
|
|||||||
if not self.game.map.is_wall(new_x, new_y):
|
if not self.game.map.is_wall(new_x, new_y):
|
||||||
if not any(isinstance(unit, Gas) for unit in self.game.units.values() if unit.position == (new_x, new_y)):
|
if not any(isinstance(unit, Gas) for unit in self.game.units.values() if unit.position == (new_x, new_y)):
|
||||||
print(f"Spreading gas from {self.position} to ({new_x}, {new_y})")
|
print(f"Spreading gas from {self.position} to ({new_x}, {new_y})")
|
||||||
self.game.spawn_unit(Gas, (new_x, new_y), parent_id=self.parent_id if self.parent_id else self.id)
|
self.game.unit_manager.spawn_unit(Gas, (new_x, new_y), parent_id=self.parent_id if self.parent_id else self.id)
|
||||||
def collisions(self):
|
def collisions(self):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@@ -72,7 +72,7 @@ class Gas(Unit):
|
|||||||
|
|
||||||
|
|
||||||
def draw(self):
|
def draw(self):
|
||||||
image = self.game.assets["BMP_GAS"]
|
image = self.game.graphics.assets["BMP_GAS"]
|
||||||
image_size = self.game.render_engine.get_image_size(image)
|
image_size = self.game.render_engine.get_image_size(image)
|
||||||
self.rat_image = image
|
self.rat_image = image
|
||||||
partial_x, partial_y = 0, 0
|
partial_x, partial_y = 0, 0
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ class Mine(Unit):
|
|||||||
)
|
)
|
||||||
|
|
||||||
for victim_id in victim_ids:
|
for victim_id in victim_ids:
|
||||||
rat_unit = self.game.get_unit_by_id(victim_id)
|
rat_unit = self.game.unit_manager.get_unit_by_id(victim_id)
|
||||||
if rat_unit and hasattr(rat_unit, 'sex'): # Check if it's a rat
|
if rat_unit and hasattr(rat_unit, 'sex'): # Check if it's a rat
|
||||||
# Mine explodes and kills the rat
|
# Mine explodes and kills the rat
|
||||||
self.explode(rat_unit)
|
self.explode(rat_unit)
|
||||||
@@ -49,7 +49,7 @@ class Mine(Unit):
|
|||||||
return
|
return
|
||||||
|
|
||||||
# Use mine asset
|
# Use mine asset
|
||||||
image = self.game.assets["BMP_POISON"]
|
image = self.game.graphics.assets["BMP_POISON"]
|
||||||
image_size = self.game.render_engine.get_image_size(image)
|
image_size = self.game.render_engine.get_image_size(image)
|
||||||
|
|
||||||
# Center the mine in the cell
|
# Center the mine in the cell
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ class Point(Unit):
|
|||||||
|
|
||||||
|
|
||||||
def draw(self):
|
def draw(self):
|
||||||
image = self.game.assets[f"BMP_BONUS_{self.value}"]
|
image = self.game.graphics.assets[f"BMP_BONUS_{self.value}"]
|
||||||
image_size = self.game.render_engine.get_image_size(image)
|
image_size = self.game.render_engine.get_image_size(image)
|
||||||
self.rat_image = image
|
self.rat_image = image
|
||||||
partial_x, partial_y = 0, 0
|
partial_x, partial_y = 0, 0
|
||||||
|
|||||||
@@ -93,7 +93,7 @@ class Rat(Unit):
|
|||||||
sex = self.sex if self.age > AGE_THRESHOLD else "BABY"
|
sex = self.sex if self.age > AGE_THRESHOLD else "BABY"
|
||||||
|
|
||||||
# Get cached image size instead of calling get_image_size()
|
# Get cached image size instead of calling get_image_size()
|
||||||
image_size = self.game.rat_image_sizes[sex][self.direction]
|
image_size = self.game.graphics.rat_image_sizes[sex][self.direction]
|
||||||
|
|
||||||
# Calculate partial movement offset
|
# Calculate partial movement offset
|
||||||
if self.direction in ["UP", "DOWN"]:
|
if self.direction in ["UP", "DOWN"]:
|
||||||
@@ -130,7 +130,7 @@ class Rat(Unit):
|
|||||||
|
|
||||||
# Process each collision
|
# Process each collision
|
||||||
for _, other_id in collisions:
|
for _, other_id in collisions:
|
||||||
other_unit = self.game.get_unit_by_id(other_id)
|
other_unit = self.game.unit_manager.get_unit_by_id(other_id)
|
||||||
|
|
||||||
# Skip if not another Rat
|
# Skip if not another Rat
|
||||||
if not isinstance(other_unit, Rat):
|
if not isinstance(other_unit, Rat):
|
||||||
@@ -164,17 +164,17 @@ class Rat(Unit):
|
|||||||
|
|
||||||
# Rat-specific behavior: spawn points
|
# Rat-specific behavior: spawn points
|
||||||
if score not in [None, 0]:
|
if score not in [None, 0]:
|
||||||
self.game.add_point(score)
|
self.game.scoring.add_point(score)
|
||||||
self.game.spawn_unit(Point, death_position, value=score)
|
self.game.unit_manager.spawn_unit(Point, death_position, value=score)
|
||||||
|
|
||||||
# Add blood stain directly to background
|
# Add blood stain directly to background
|
||||||
self.game.add_blood_stain(death_position)
|
self.game.graphics.add_blood_stain(death_position)
|
||||||
|
|
||||||
def draw(self):
|
def draw(self):
|
||||||
"""Optimized draw using pre-calculated positions from move()"""
|
"""Optimized draw using pre-calculated positions from move()"""
|
||||||
sex = self.sex if self.age > AGE_THRESHOLD else "BABY"
|
sex = self.sex if self.age > AGE_THRESHOLD else "BABY"
|
||||||
image = self.game.rat_assets_textures[sex][self.direction]
|
image = self.game.graphics.rat_assets_textures[sex][self.direction]
|
||||||
image_size = self.game.rat_image_sizes[sex][self.direction]
|
image_size = self.game.graphics.rat_image_sizes[sex][self.direction]
|
||||||
|
|
||||||
# Calculate render position if not yet set (first frame)
|
# Calculate render position if not yet set (first frame)
|
||||||
if not hasattr(self, 'render_x'):
|
if not hasattr(self, 'render_x'):
|
||||||
@@ -275,7 +275,7 @@ class Rat(Unit):
|
|||||||
"""Calculate render position and bbox (used when render_x not yet set)"""
|
"""Calculate render position and bbox (used when render_x not yet set)"""
|
||||||
sex = self.sex if self.age > AGE_THRESHOLD else "BABY"
|
sex = self.sex if self.age > AGE_THRESHOLD else "BABY"
|
||||||
image_size = self.game.render_engine.get_image_size(
|
image_size = self.game.render_engine.get_image_size(
|
||||||
self.game.rat_assets_textures[sex][self.direction]
|
self.game.graphics.rat_assets_textures[sex][self.direction]
|
||||||
)
|
)
|
||||||
|
|
||||||
partial_x, partial_y = 0, 0
|
partial_x, partial_y = 0, 0
|
||||||
@@ -314,7 +314,7 @@ class Female(Rat):
|
|||||||
self.babies -= 1
|
self.babies -= 1
|
||||||
self.stop = 20
|
self.stop = 20
|
||||||
if self.partial_move > 0.2:
|
if self.partial_move > 0.2:
|
||||||
self.game.spawn_rat(self.position)
|
self.game.unit_manager.spawn_rat(self.position)
|
||||||
else:
|
else:
|
||||||
self.game.spawn_rat(self.position_before)
|
self.game.unit_manager.spawn_rat(self.position_before)
|
||||||
self.game.render_engine.play_sound("BIRTH.WAV")
|
self.game.render_engine.play_sound("BIRTH.WAV")
|
||||||