Refactor code structure for improved readability and maintainability
This commit is contained in:
Binary file not shown.
|
After Width: | Height: | Size: 84 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 614 KiB |
@@ -27,6 +27,30 @@
|
||||
"keydown_M": "toggle_audio",
|
||||
"keydown_F": "toggle_full_screen"
|
||||
},
|
||||
"keybinding_level_intro": {
|
||||
"keydown_Return": "reset_game",
|
||||
"keydown_Escape": "quit_game",
|
||||
"keydown_M": "toggle_audio",
|
||||
"keydown_F": "toggle_full_screen"
|
||||
},
|
||||
"keybinding_level_clear": {
|
||||
"keydown_Return": "reset_game",
|
||||
"keydown_Escape": "quit_game",
|
||||
"keydown_M": "toggle_audio",
|
||||
"keydown_F": "toggle_full_screen"
|
||||
},
|
||||
"keybinding_defeat": {
|
||||
"keydown_Return": "reset_game",
|
||||
"keydown_Escape": "quit_game",
|
||||
"keydown_M": "toggle_audio",
|
||||
"keydown_F": "toggle_full_screen"
|
||||
},
|
||||
"keybinding_run_complete": {
|
||||
"keydown_Return": "reset_game",
|
||||
"keydown_Escape": "quit_game",
|
||||
"keydown_M": "toggle_audio",
|
||||
"keydown_F": "toggle_full_screen"
|
||||
},
|
||||
"keybinding_paused": {
|
||||
"keydown_Return": "reset_game",
|
||||
"keydown_Escape": "quit_game",
|
||||
|
||||
@@ -25,6 +25,30 @@ keybinding_start_menu:
|
||||
controllerbuttondown_misc1: quit_game
|
||||
controllerbuttondown_guide: quit_game
|
||||
|
||||
keybinding_level_intro:
|
||||
controllerbuttondown_a: reset_game
|
||||
controllerbuttondown_start: reset_game
|
||||
controllerbuttondown_misc1: quit_game
|
||||
controllerbuttondown_guide: quit_game
|
||||
|
||||
keybinding_level_clear:
|
||||
controllerbuttondown_a: reset_game
|
||||
controllerbuttondown_start: reset_game
|
||||
controllerbuttondown_misc1: quit_game
|
||||
controllerbuttondown_guide: quit_game
|
||||
|
||||
keybinding_defeat:
|
||||
controllerbuttondown_a: reset_game
|
||||
controllerbuttondown_start: reset_game
|
||||
controllerbuttondown_misc1: quit_game
|
||||
controllerbuttondown_guide: quit_game
|
||||
|
||||
keybinding_run_complete:
|
||||
controllerbuttondown_a: reset_game
|
||||
controllerbuttondown_start: reset_game
|
||||
controllerbuttondown_misc1: quit_game
|
||||
controllerbuttondown_guide: quit_game
|
||||
|
||||
keybinding_paused:
|
||||
controllerbuttondown_start: toggle_pause
|
||||
controllerbuttondown_dpad_up: menu_up
|
||||
|
||||
@@ -26,6 +26,30 @@ keybinding_start_menu:
|
||||
keydown_M: toggle_audio
|
||||
keydown_F: toggle_full_screen
|
||||
|
||||
keybinding_level_intro:
|
||||
keydown_Return: reset_game
|
||||
keydown_Escape: quit_game
|
||||
keydown_M: toggle_audio
|
||||
keydown_F: toggle_full_screen
|
||||
|
||||
keybinding_level_clear:
|
||||
keydown_Return: reset_game
|
||||
keydown_Escape: quit_game
|
||||
keydown_M: toggle_audio
|
||||
keydown_F: toggle_full_screen
|
||||
|
||||
keybinding_defeat:
|
||||
keydown_Return: reset_game
|
||||
keydown_Escape: quit_game
|
||||
keydown_M: toggle_audio
|
||||
keydown_F: toggle_full_screen
|
||||
|
||||
keybinding_run_complete:
|
||||
keydown_Return: reset_game
|
||||
keydown_Escape: quit_game
|
||||
keydown_M: toggle_audio
|
||||
keydown_F: toggle_full_screen
|
||||
|
||||
keybinding_paused:
|
||||
keydown_Return: reset_game
|
||||
keydown_Escape: quit_game
|
||||
|
||||
@@ -24,6 +24,22 @@ keybinding_start_menu:
|
||||
joybuttondown_16: toggle_pause
|
||||
joybuttondown_12: quit_game
|
||||
|
||||
keybinding_level_intro:
|
||||
joybuttondown_13: reset_game
|
||||
joybuttondown_12: quit_game
|
||||
|
||||
keybinding_level_clear:
|
||||
joybuttondown_13: reset_game
|
||||
joybuttondown_12: quit_game
|
||||
|
||||
keybinding_defeat:
|
||||
joybuttondown_13: reset_game
|
||||
joybuttondown_12: quit_game
|
||||
|
||||
keybinding_run_complete:
|
||||
joybuttondown_13: reset_game
|
||||
joybuttondown_12: quit_game
|
||||
|
||||
keybinding_paused:
|
||||
joybuttondown_13: reset_game
|
||||
joybuttondown_16: toggle_pause
|
||||
|
||||
@@ -20,6 +20,22 @@ keybinding_start_menu:
|
||||
joybuttondown_10: toggle_pause
|
||||
joybuttondown_11: quit_game
|
||||
|
||||
keybinding_level_intro:
|
||||
joybuttondown_9: reset_game
|
||||
joybuttondown_11: quit_game
|
||||
|
||||
keybinding_level_clear:
|
||||
joybuttondown_9: reset_game
|
||||
joybuttondown_11: quit_game
|
||||
|
||||
keybinding_defeat:
|
||||
joybuttondown_9: reset_game
|
||||
joybuttondown_11: quit_game
|
||||
|
||||
keybinding_run_complete:
|
||||
joybuttondown_9: reset_game
|
||||
joybuttondown_11: quit_game
|
||||
|
||||
keybinding_paused:
|
||||
joybuttondown_9: reset_game
|
||||
joybuttondown_10: toggle_pause
|
||||
|
||||
+28
-1
@@ -196,6 +196,28 @@ def resolve_keybindings(preferred_profile=None, preferred_file=None, render_engi
|
||||
|
||||
|
||||
class KeyBindings:
|
||||
def _binding_sections_for_action(self):
|
||||
game_end_active, game_end_reason = getattr(self, "game_end", (False, None))
|
||||
if game_end_active:
|
||||
if game_end_reason == "level_clear":
|
||||
return ["keybinding_level_clear", "keybinding_paused"]
|
||||
if game_end_reason == "defeat":
|
||||
return ["keybinding_defeat", "keybinding_paused"]
|
||||
if game_end_reason == "run_complete":
|
||||
return ["keybinding_run_complete", "keybinding_paused"]
|
||||
return ["keybinding_paused"]
|
||||
|
||||
if getattr(self, "game_status", None) == "start_menu":
|
||||
if getattr(self, "menu_screen", None) == "level_intro":
|
||||
return ["keybinding_level_intro", "keybinding_start_menu"]
|
||||
return ["keybinding_start_menu"]
|
||||
|
||||
status = getattr(self, "game_status", None)
|
||||
if status:
|
||||
return [f"keybinding_{status}"]
|
||||
|
||||
return []
|
||||
|
||||
def initialize_keybindings(self):
|
||||
preferred_profile = None
|
||||
preferred_file = None
|
||||
@@ -253,7 +275,12 @@ class KeyBindings:
|
||||
if not hasattr(self, "bindings"):
|
||||
self.initialize_keybindings()
|
||||
|
||||
value = self.bindings.get(f"keybinding_{self.game_status}", {}).get(action)
|
||||
value = None
|
||||
for section_name in self._binding_sections_for_action():
|
||||
value = self.bindings.get(section_name, {}).get(action)
|
||||
if value:
|
||||
break
|
||||
|
||||
if not value:
|
||||
return None
|
||||
|
||||
|
||||
+23
-3
@@ -494,9 +494,29 @@ class GameWindow:
|
||||
|
||||
if image := kwargs.get("image"):
|
||||
image_width, image_height = self.get_image_size(image)
|
||||
image_x = self.target_size[0] // 2 - image_width // 2
|
||||
self.draw_image(image_x - self.w_offset, content_y - self.h_offset, image, "win")
|
||||
content_y += image_height + max(14, panel_height // 30)
|
||||
image_scale = kwargs.get("image_scale")
|
||||
image_max_width = kwargs.get("image_max_width")
|
||||
image_max_height = kwargs.get("image_max_height")
|
||||
|
||||
scale_limit = 1.0
|
||||
if image_scale is not None:
|
||||
scale_limit = min(scale_limit, max(0.01, float(image_scale)))
|
||||
if image_max_width:
|
||||
scale_limit = min(scale_limit, float(image_max_width) / image_width)
|
||||
if image_max_height:
|
||||
scale_limit = min(scale_limit, float(image_max_height) / image_height)
|
||||
|
||||
render_width = max(1, int(round(image_width * scale_limit)))
|
||||
render_height = max(1, int(round(image_height * scale_limit)))
|
||||
image_x = self.target_size[0] // 2 - render_width // 2
|
||||
self.draw_image(
|
||||
image_x - self.w_offset,
|
||||
content_y - self.h_offset,
|
||||
image,
|
||||
"win",
|
||||
dest_size=(render_width, render_height),
|
||||
)
|
||||
content_y += render_height + max(14, panel_height // 30)
|
||||
|
||||
if subtitle := kwargs.get("subtitle"):
|
||||
subtitle_sprites = [
|
||||
|
||||
@@ -409,7 +409,7 @@ class MiceMaze(
|
||||
def advance_level(self):
|
||||
print(f"[flow] advance_level called from level={self.current_level + 1} points={self.points}")
|
||||
if not self._is_dat_campaign():
|
||||
self.load_level(self.current_level, preserve_points=True, show_menu=True, menu_screen="level_intro")
|
||||
self.load_level(self.current_level, preserve_points=True, show_menu=False)
|
||||
return
|
||||
|
||||
if self._is_last_dat_level():
|
||||
@@ -421,7 +421,7 @@ class MiceMaze(
|
||||
|
||||
next_level = self.current_level + 1
|
||||
print(f"[flow] advancing to level={next_level + 1}")
|
||||
self.load_level(next_level, preserve_points=True, show_menu=True, menu_screen="level_intro")
|
||||
self.load_level(next_level, preserve_points=True, show_menu=False)
|
||||
|
||||
def reset_game(self):
|
||||
print(
|
||||
@@ -437,8 +437,9 @@ class MiceMaze(
|
||||
self.start_menu_animation_started_at = time.monotonic()
|
||||
self.load_level(0, preserve_points=False, show_menu=True, menu_screen="start")
|
||||
else:
|
||||
print("[flow] reset_game -> restart from level 1 after defeat")
|
||||
self.load_level(0, preserve_points=False, show_menu=False)
|
||||
print("[flow] reset_game -> defeat, returning to start menu")
|
||||
self.start_menu_animation_started_at = time.monotonic()
|
||||
self.load_level(0, preserve_points=False, show_menu=True, menu_screen="start")
|
||||
return
|
||||
|
||||
if self.game_status == "paused":
|
||||
@@ -943,9 +944,10 @@ class MiceMaze(
|
||||
if self.game_end[1] == GAME_END_DEFEAT:
|
||||
self.render_engine.dialog(
|
||||
"Game Over: Mice are too many!",
|
||||
image=self.assets["BMP_WEWIN"],
|
||||
subtitle=f"Reached level: {self.current_level + 1}\nPress Return to restart from level 1",
|
||||
scores=self.combined_scores
|
||||
image=self.assets.get("lose", self.assets["BMP_WEWIN"]),
|
||||
image_scale=0.48,
|
||||
subtitle=f"Reached level: {self.current_level + 1}\nPress Return to go back to the start menu",
|
||||
scores=self.combined_scores,
|
||||
)
|
||||
elif self.game_end[1] == GAME_END_RUN_COMPLETE:
|
||||
self.render_engine.dialog(
|
||||
@@ -957,7 +959,7 @@ class MiceMaze(
|
||||
else:
|
||||
self.render_engine.dialog(
|
||||
f"Level {self.current_level + 1} Clear! Points: {self.points}",
|
||||
image=self.assets["BMP_WEWIN"],
|
||||
image=self.assets.get("clear", self.assets["BMP_WEWIN"]),
|
||||
subtitle="Press Return for the next level",
|
||||
scores=self.combined_scores
|
||||
)
|
||||
|
||||
@@ -25,7 +25,7 @@ def build_dummy(level_index):
|
||||
dummy.game_end = (False, None)
|
||||
dummy.menu_screen = None
|
||||
dummy.units = {}
|
||||
dummy.assets = {"BMP_WEWIN": object(), "end": object()}
|
||||
dummy.assets = {"BMP_WEWIN": object(), "end": object(), "clear": object(), "lose": object()}
|
||||
dummy.explosions = {"LEFT": object(), "RIGHT": object(), "UP": object(), "DOWN": object()}
|
||||
dummy.count_rats = lambda: 0
|
||||
dummy.start_menu_animation_started_at = 0
|
||||
@@ -89,8 +89,29 @@ class FinalLevelFlowTests(unittest.TestCase):
|
||||
self.assertEqual(saves, [1234])
|
||||
self.assertEqual(load_calls, [])
|
||||
|
||||
def test_advance_level_starts_next_dat_level_immediately(self):
|
||||
dummy, _, _, _, _, load_calls = build_dummy(4)
|
||||
|
||||
dummy.advance_level()
|
||||
|
||||
self.assertEqual(
|
||||
load_calls,
|
||||
[((5,), {"preserve_points": True, "show_menu": False})],
|
||||
)
|
||||
|
||||
def test_advance_level_starts_non_dat_level_immediately(self):
|
||||
dummy, _, _, _, _, load_calls = build_dummy(0)
|
||||
dummy.map = SimpleNamespace(source_path=Path("/tmp/level.json"))
|
||||
|
||||
dummy.advance_level()
|
||||
|
||||
self.assertEqual(
|
||||
load_calls,
|
||||
[((0,), {"preserve_points": True, "show_menu": False})],
|
||||
)
|
||||
|
||||
def test_regular_level_clear_keeps_run_open(self):
|
||||
dummy, sounds, _, stats, saves, _ = build_dummy(4)
|
||||
dummy, sounds, dialogs, stats, saves, _ = build_dummy(4)
|
||||
|
||||
result = dummy.game_over()
|
||||
|
||||
@@ -108,6 +129,41 @@ class FinalLevelFlowTests(unittest.TestCase):
|
||||
],
|
||||
)
|
||||
|
||||
|
||||
dummy.game_over()
|
||||
|
||||
self.assertEqual(dialogs[-1][0][0], "Level 5 Clear! Points: 1234")
|
||||
self.assertIs(dialogs[-1][1]["image"], dummy.assets["clear"])
|
||||
|
||||
def test_defeat_dialog_uses_lose_art(self):
|
||||
dummy, sounds, dialogs, stats, saves, _ = build_dummy(4)
|
||||
dummy.count_rats = lambda: 201
|
||||
|
||||
result = dummy.game_over()
|
||||
|
||||
self.assertTrue(result)
|
||||
self.assertEqual(dummy.game_end, (True, "defeat"))
|
||||
self.assertEqual(stats, [(1234, False)])
|
||||
self.assertEqual(saves, [1234])
|
||||
self.assertEqual(
|
||||
sounds,
|
||||
[
|
||||
("stop",),
|
||||
(("WEWIN.WAV",), {}),
|
||||
],
|
||||
)
|
||||
|
||||
dummy.game_over()
|
||||
|
||||
self.assertEqual(dialogs[-1][0][0], "Game Over: Mice are too many!")
|
||||
self.assertIs(dialogs[-1][1]["image"], dummy.assets["lose"])
|
||||
self.assertEqual(dialogs[-1][1]["image_scale"], 0.48)
|
||||
self.assertEqual(dialogs[-1][1]["scores"], dummy.combined_scores)
|
||||
self.assertEqual(
|
||||
dialogs[-1][1]["subtitle"],
|
||||
"Reached level: 5\nPress Return to go back to the start menu",
|
||||
)
|
||||
|
||||
def test_final_dat_level_records_score_and_shows_dedicated_dialog(self):
|
||||
dummy, sounds, dialogs, stats, saves, _ = build_dummy(maze.DEFAULT_LEVELS_PER_DAT_FILE - 1)
|
||||
|
||||
@@ -147,15 +203,17 @@ class FinalLevelFlowTests(unittest.TestCase):
|
||||
[((0,), {"preserve_points": False, "show_menu": True, "menu_screen": "start"})],
|
||||
)
|
||||
|
||||
def test_toggle_pause_ignores_transition_screen_between_levels(self):
|
||||
dummy, _, _, _, _, _ = build_dummy(2)
|
||||
dummy.game_status = "start_menu"
|
||||
dummy.menu_screen = "level_intro"
|
||||
def test_return_after_defeat_goes_back_to_start_menu(self):
|
||||
dummy, _, _, _, _, load_calls = build_dummy(4)
|
||||
dummy.game_end = (True, "defeat")
|
||||
dummy.game_status = "paused"
|
||||
|
||||
dummy.toggle_pause()
|
||||
dummy.reset_game()
|
||||
|
||||
self.assertEqual(dummy.game_status, "start_menu")
|
||||
self.assertEqual(dummy.menu_screen, "level_intro")
|
||||
self.assertEqual(
|
||||
load_calls,
|
||||
[((0,), {"preserve_points": False, "show_menu": True, "menu_screen": "start"})],
|
||||
)
|
||||
|
||||
def test_toggle_pause_ignores_game_end_dialogs(self):
|
||||
dummy, _, _, _, _, _ = build_dummy(2)
|
||||
|
||||
@@ -82,6 +82,61 @@ class KeybindingProfileTests(unittest.TestCase):
|
||||
f"incomplete weapon bindings in {config_path.name}: {sorted(actions)}",
|
||||
)
|
||||
|
||||
def test_shipped_profiles_define_level_dialog_bindings(self):
|
||||
dummy = DummyBindings()
|
||||
conf_dir = Path(__file__).resolve().parent / "conf"
|
||||
required_sections = {
|
||||
"keybinding_level_intro",
|
||||
"keybinding_level_clear",
|
||||
"keybinding_defeat",
|
||||
"keybinding_run_complete",
|
||||
}
|
||||
|
||||
for config_path in sorted(conf_dir.glob("keybindings*.json")) + sorted(conf_dir.glob("keybindings*.yaml")):
|
||||
bindings = controls._load_bindings_from_file(config_path)
|
||||
validated = dummy._validate_bindings(bindings, config_path)
|
||||
|
||||
for section_name in required_sections:
|
||||
section = validated.get(section_name, {})
|
||||
self.assertTrue(section, f"missing {section_name} in {config_path.name}")
|
||||
values = set(section.values())
|
||||
self.assertIn("reset_game", values, f"missing confirm binding in {section_name} for {config_path.name}")
|
||||
self.assertIn("quit_game", values, f"missing quit binding in {section_name} for {config_path.name}")
|
||||
|
||||
def test_trigger_uses_level_intro_context(self):
|
||||
dummy = DummyBindings()
|
||||
calls = []
|
||||
dummy.reset_game = lambda: calls.append("reset")
|
||||
dummy.quit_game = lambda: calls.append("quit")
|
||||
dummy.bindings = {
|
||||
"keybinding_level_intro": {"keydown_Return": "reset_game"},
|
||||
"keybinding_start_menu": {"keydown_Return": "quit_game"},
|
||||
}
|
||||
dummy.game_status = "start_menu"
|
||||
dummy.menu_screen = "level_intro"
|
||||
dummy.game_end = (False, None)
|
||||
|
||||
dummy.trigger("keydown_Return")
|
||||
|
||||
self.assertEqual(calls, ["reset"])
|
||||
|
||||
def test_trigger_uses_level_clear_context(self):
|
||||
dummy = DummyBindings()
|
||||
calls = []
|
||||
dummy.reset_game = lambda: calls.append("reset")
|
||||
dummy.quit_game = lambda: calls.append("quit")
|
||||
dummy.bindings = {
|
||||
"keybinding_level_clear": {"keydown_Return": "reset_game"},
|
||||
"keybinding_paused": {"keydown_Return": "quit_game"},
|
||||
}
|
||||
dummy.game_status = "paused"
|
||||
dummy.menu_screen = None
|
||||
dummy.game_end = (True, "level_clear")
|
||||
|
||||
dummy.trigger("keydown_Return")
|
||||
|
||||
self.assertEqual(calls, ["reset"])
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Reference in New Issue
Block a user