From 7fa309c3ff8bdcb7a005f1864e9bc7f8f352498d Mon Sep 17 00:00:00 2001 From: Anthony van Winkle Date: Wed, 17 Jul 2024 14:24:36 -0700 Subject: [PATCH 01/13] New feature 'filler_initials' for high score mode --- mpf/_version.py | 2 +- mpf/config_spec.yaml | 1 + mpf/modes/high_score/code/high_score.py | 13 ++++++++++++- 3 files changed, 14 insertions(+), 2 deletions(-) diff --git a/mpf/_version.py b/mpf/_version.py index 132991e40..16228f185 100644 --- a/mpf/_version.py +++ b/mpf/_version.py @@ -10,7 +10,7 @@ """ -__version__ = '0.57.2' # Also consider whether MPF-MC pyproject.toml should be updated +__version__ = '0.57.3.dev1' # Also consider whether MPF-MC pyproject.toml should be updated '''The full version of MPF.''' __short_version__ = '0.57' diff --git a/mpf/config_spec.yaml b/mpf/config_spec.yaml index e10c5a0fa..d9a8aae37 100644 --- a/mpf/config_spec.yaml +++ b/mpf/config_spec.yaml @@ -835,6 +835,7 @@ high_score: award_slide_display_time: single|ms|4s categories: dict|str:list| defaults: dict|str:list|None + filler_initials: list|str|None enter_initials_timeout: single|secs|20s reverse_sort: list|str|None reset_high_scores_events: list|event_handler|high_scores_reset,factory_reset diff --git a/mpf/modes/high_score/code/high_score.py b/mpf/modes/high_score/code/high_score.py index 807621c6e..3f38b0a47 100644 --- a/mpf/modes/high_score/code/high_score.py +++ b/mpf/modes/high_score/code/high_score.py @@ -1,5 +1,6 @@ """Contains the High Score mode code.""" import asyncio +from random import choice from mpf.core.async_mode import AsyncMode from mpf.core.player import Player @@ -256,7 +257,17 @@ async def _ask_player_for_initials(self, player: Player, award_label: str, value timeout=self.high_score_config['enter_initials_timeout'] ) # type: dict - return event_result["text"] if "text" in event_result else '' + input_initials = event_result["text"] if "text" in event_result else '' + + # If no initials were input, some can be randomly chosen from the 'filler_initials' config section + if not input_initials and self.high_score_config["filler_initials"]: + existing_initials = self.high_scores.keys() + unused_initials = [i for i in self.high_score_config["filler_initials"] if i not in existing_initials] + # If there aren't enough to choose something unique, just pick any from the fillers + if not unused_initials: + unused_initials = self.high_score_config["filler_initials"] + input_initials = choice(unused_initials) + return input_initials async def _show_award_slide(self, player_num, player_name: str, category_name: str, award: str, value: int) -> None: if not self.high_score_config['award_slide_display_time']: From 5d31c89dd4027844c9d739cc0133c2ea4ebd2d10 Mon Sep 17 00:00:00 2001 From: Anthony van Winkle Date: Wed, 17 Jul 2024 18:36:21 -0700 Subject: [PATCH 02/13] Handle high scores using 0.57 format and category_name --- mpf/modes/high_score/code/high_score.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/mpf/modes/high_score/code/high_score.py b/mpf/modes/high_score/code/high_score.py index 3f38b0a47..63ab9bdcb 100644 --- a/mpf/modes/high_score/code/high_score.py +++ b/mpf/modes/high_score/code/high_score.py @@ -196,7 +196,7 @@ async def _run(self) -> None: # ask player for initials if we do not know them if not player.initials: try: - player.initials = await self._ask_player_for_initials(player, award_names[i], value) + player.initials = await self._ask_player_for_initials(player, award_names[i], value, category_name) except asyncio.TimeoutError: del new_list[i] # no entry when the player missed the timeout @@ -242,7 +242,7 @@ def _assign_vars(self, category_name, player): return var_dict # pylint: disable-msg=too-many-arguments - async def _ask_player_for_initials(self, player: Player, award_label: str, value: int) -> str: + async def _ask_player_for_initials(self, player: Player, award_label: str, value: int, category_name: str) -> str: """Show text widget to ask player for initials.""" self.info_log("New high score. Player: %s, award_label: %s" ", Value: %s", player, award_label, value) @@ -261,7 +261,8 @@ async def _ask_player_for_initials(self, player: Player, award_label: str, value # If no initials were input, some can be randomly chosen from the 'filler_initials' config section if not input_initials and self.high_score_config["filler_initials"]: - existing_initials = self.high_scores.keys() + # High scores are stored as an array of [name, score] + existing_initials = [n[0] for n in self.high_scores[category_name]] unused_initials = [i for i in self.high_score_config["filler_initials"] if i not in existing_initials] # If there aren't enough to choose something unique, just pick any from the fillers if not unused_initials: From 563b6138bc220c06ceaa0fe76251caa964176f30 Mon Sep 17 00:00:00 2001 From: Anthony van Winkle Date: Wed, 17 Jul 2024 19:47:23 -0700 Subject: [PATCH 03/13] PIT support for multiple ejects and dynamic trough switch management --- .../platform_integration_test_runner.py | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/mpf/plugins/platform_integration_test_runner.py b/mpf/plugins/platform_integration_test_runner.py index cb05cf084..2155d302e 100644 --- a/mpf/plugins/platform_integration_test_runner.py +++ b/mpf/plugins/platform_integration_test_runner.py @@ -207,12 +207,16 @@ async def start_game(self, num_players=1): async def eject_and_plunge_ball(self, plunger_switch_name, plunger_lane_settle_time=2, **kwargs): """Shuffle the trough switches and plunger to simulate an eject.""" del kwargs - self.info_log("Ejecting and plunging ball...") - self.set_switch_sync(self.trough_switches[0], 0) + # Find all the trough switches that currently have balls + active_trough_switches = [s for s in self.trough_switches if self.machine.switches[s].state] + assert len(active_trough_switches), "Unable to eject a ball. Trough is empty." + self.info_log("Ejecting and plunging ball from trough %s to plunger switch %s...", + active_trough_switches[-1], plunger_switch_name) + self.set_switch_sync(active_trough_switches[0], 0) await asyncio.sleep(0.03) - self.set_switch_sync(self.trough_switches[-1], 0) + self.set_switch_sync(active_trough_switches[-1], 0) await asyncio.sleep(0.1) - self.set_switch_sync(self.trough_switches[0], 1) + self.set_switch_sync(active_trough_switches[0], 1) await asyncio.sleep(0.25) await self.set_switch(plunger_switch_name, 1, duration_secs=plunger_lane_settle_time) await asyncio.sleep(1) @@ -221,7 +225,11 @@ async def move_ball_from_drain_to_trough(self, **kwargs): """Move a ball from the drain device to the trough device.""" del kwargs drain_switches = self.machine.ball_devices.items_tagged('drain')[0].config.get('ball_switches') - self.set_switch_sync(drain_switches[-1], 0) + self.info_log("Found drain switches: %s of type %s", drain_switches, type(drain_switches)) + # If there's only one drain switch it might be a single value, rather than a list + drain_switch = drain_switches if type(drain_switches) is str else drain_switches[-1] + self.info_log("Setting drain switch '%s' to zero", drain_switch) + self.set_switch_sync(drain_switch, 0) await asyncio.sleep(0.25) self.set_switch_sync(self.trough_switches[-1], 1) await asyncio.sleep(0.25) From 8df70d9b9ca1dc933928ebe65576a4a44dedd67a Mon Sep 17 00:00:00 2001 From: Anthony van Winkle Date: Fri, 19 Jul 2024 12:37:38 -0700 Subject: [PATCH 04/13] List active modes in run loop startup, verify attract is running --- mpf/core/machine.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/mpf/core/machine.py b/mpf/core/machine.py index 442486a52..81d2167de 100644 --- a/mpf/core/machine.py +++ b/mpf/core/machine.py @@ -685,7 +685,11 @@ def run(self) -> None: if not self.initialize_mpf(): return - self.info_log("Starting the main run loop.") + self.info_log("Starting the main run loop with active modes: %s", + self.mode_controller.active_modes) + if not self.modes['attract'] in self.mode_controller.active_modes: + self.warning_log("Attract mode is not active, game will not be playable. " + "Please check your attract mode configuration.") self._run_loop() def stop_with_exception(self, exception) -> None: From 9079562615f804aa4843e764aedc851fd254a0dc Mon Sep 17 00:00:00 2001 From: Anthony van Winkle Date: Fri, 19 Jul 2024 12:56:23 -0700 Subject: [PATCH 05/13] New config option mpf: min_mpf_version --- mpf/config_spec.yaml | 1 + mpf/core/machine.py | 11 +++++++++++ 2 files changed, 12 insertions(+) diff --git a/mpf/config_spec.yaml b/mpf/config_spec.yaml index d9a8aae37..2aedefef2 100644 --- a/mpf/config_spec.yaml +++ b/mpf/config_spec.yaml @@ -1112,6 +1112,7 @@ mpf: core_modules: ignore config_players: ignore device_modules: ignore + min_mpf_version: single|str|None plugins: ignore platforms: ignore paths: ignore diff --git a/mpf/core/machine.py b/mpf/core/machine.py index 81d2167de..038b90bbb 100644 --- a/mpf/core/machine.py +++ b/mpf/core/machine.py @@ -6,6 +6,7 @@ import threading from typing import Any, Callable, Dict, List, Set, Optional +from packaging import version from pkg_resources import iter_entry_points from mpf._version import __version__ @@ -368,6 +369,16 @@ def _validate_config(self) -> None: self.validate_machine_config_section('machine') self.validate_machine_config_section('game') self.validate_machine_config_section('mpf') + self._validate_version() + + def _validate_version(self): + if not self.config['mpf']['min_mpf_version']: + return + min_version = version.parse(self.config['mpf']['min_mpf_version']) + mpf_version = version.parse(__version__) + if mpf_version < min_version: + raise AssertionError(f'MPF version mismatch. MPF version {mpf_version} found but game config ' + f'requires at least {min_version} ') def validate_machine_config_section(self, section: str) -> None: """Validate a config section.""" From 35a84ba29c54acc6ce1afab28bb0e9edca07e301 Mon Sep 17 00:00:00 2001 From: Anthony van Winkle Date: Thu, 25 Jul 2024 19:19:17 -0700 Subject: [PATCH 06/13] Option ignore_led_errors for FAST EXP boards --- mpf/config_spec.yaml | 1 + mpf/platforms/fast/fast_exp_board.py | 5 +++-- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/mpf/config_spec.yaml b/mpf/config_spec.yaml index 2aedefef2..ae98b7742 100644 --- a/mpf/config_spec.yaml +++ b/mpf/config_spec.yaml @@ -662,6 +662,7 @@ fast_exp_board: led_ports: list|subconfig(fast_led_port)|None led_fade_time: single|ms|0 led_hz: single|float|30 + ignore_led_errors: single|bool|false fast_breakout: port: single|enum(1,2,3)| model: single|str| diff --git a/mpf/platforms/fast/fast_exp_board.py b/mpf/platforms/fast/fast_exp_board.py index 4fc6e31c9..78adffb41 100644 --- a/mpf/platforms/fast/fast_exp_board.py +++ b/mpf/platforms/fast/fast_exp_board.py @@ -179,8 +179,9 @@ def update_leds(self): except Exception as e: self.log.error( f"Error decoding the following message for board {breakout_address} : {msg_header}{msg}") - self.log.debug("Attempted update that caused this error: %s", dirty_leds) - raise e + self.log.info("Attempted update that caused this error: %s", dirty_leds) + if not self.config['ignore_led_errors']: + raise e def set_led_fade(self, rate: int) -> None: """Set LED fade rate in ms.""" From a12184ec5bdec7ebaeb6ba1082c55e1021c26921 Mon Sep 17 00:00:00 2001 From: Anthony van Winkle Date: Sun, 28 Jul 2024 11:46:12 -0700 Subject: [PATCH 07/13] A few latent machine.switch iterations convert to values() or keys() --- mpf/platforms/fast/communicators/net_neuron.py | 2 +- mpf/platforms/virtual.py | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/mpf/platforms/fast/communicators/net_neuron.py b/mpf/platforms/fast/communicators/net_neuron.py index bb16fc1e7..34e1b5ee4 100644 --- a/mpf/platforms/fast/communicators/net_neuron.py +++ b/mpf/platforms/fast/communicators/net_neuron.py @@ -293,7 +293,7 @@ def update_switches_from_hw_data(self): This will silently sync the switch.hw_state. If the logical state changes, it will process it like any switch change. """ - for switch in self.machine.switches: + for switch in self.machine.switches.values(): hw_state = self.platform.hw_switch_data[switch.hw_switch.number] if hw_state != switch.hw_state: diff --git a/mpf/platforms/virtual.py b/mpf/platforms/virtual.py index fc04f8d6f..3ccb869d2 100644 --- a/mpf/platforms/virtual.py +++ b/mpf/platforms/virtual.py @@ -105,16 +105,16 @@ async def get_hw_switch_states(self): if 'virtual_platform_start_active_switches' in self.machine.config: initial_active_switches = [] - for switch in Util.string_to_list(self.machine.config['virtual_platform_start_active_switches']): - if switch not in self.machine.switches: + for switch_name in Util.string_to_list(self.machine.config['virtual_platform_start_active_switches']): + if switch_name not in self.machine.switches.keys(): if " " in switch: self.raise_config_error("MPF no longer supports lists separated by space in " "virtual_platform_start_active_switches. Please separate " - "switches by comma: {}.".format(switch), 1) + "switches by comma: {}.".format(switch_name), 1) else: self.raise_config_error("Switch {} used in virtual_platform_start_active_switches was not " - "found in switches section.".format(switch), 1) - initial_active_switches.append(self.machine.switches[switch].hw_switch.number) + "found in switches section.".format(switch_name), 1) + initial_active_switches.append(self.machine.switches[switch_name].hw_switch.number) for k in self.hw_switches: if k in initial_active_switches: From 73c8cc4f968779040b57c917dcebd2d594ac8be6 Mon Sep 17 00:00:00 2001 From: Alex Lobascio Date: Sun, 25 Aug 2024 01:17:21 -0700 Subject: [PATCH 08/13] add tests around shot_profile block option behavior The tests for the false and default case have the same logic, suggesting that mpf-docs:docs/config/shot_profiles.md is out of date about default block behavior. --- .../shots/config/test_shot_groups.yaml | 4 ++ .../shots/modes/base2/config/base2.yaml | 4 ++ .../shots/modes/mode1/config/mode1.yaml | 19 +++++- mpf/tests/test_Shots.py | 64 ++++++++++++++++++- 4 files changed, 89 insertions(+), 2 deletions(-) diff --git a/mpf/tests/machine_files/shots/config/test_shot_groups.yaml b/mpf/tests/machine_files/shots/config/test_shot_groups.yaml index 4b06f7f00..80767f464 100644 --- a/mpf/tests/machine_files/shots/config/test_shot_groups.yaml +++ b/mpf/tests/machine_files/shots/config/test_shot_groups.yaml @@ -12,6 +12,10 @@ switches: number: switch_4: number: + switch_5: + number: + switch_6: + number: s_rotate_l: number: s_rotate_r: diff --git a/mpf/tests/machine_files/shots/modes/base2/config/base2.yaml b/mpf/tests/machine_files/shots/modes/base2/config/base2.yaml index ddf6ae685..cf82e013d 100644 --- a/mpf/tests/machine_files/shots/modes/base2/config/base2.yaml +++ b/mpf/tests/machine_files/shots/modes/base2/config/base2.yaml @@ -20,6 +20,10 @@ shots: light: tag1 shot_4: switch: switch_1 + shot_5: + switch: switch_5 + shot_6: + switch: switch_6 led_1: switch: switch_1 show_tokens: diff --git a/mpf/tests/machine_files/shots/modes/mode1/config/mode1.yaml b/mpf/tests/machine_files/shots/modes/mode1/config/mode1.yaml index 299ab4a90..80bc73d12 100644 --- a/mpf/tests/machine_files/shots/modes/mode1/config/mode1.yaml +++ b/mpf/tests/machine_files/shots/modes/mode1/config/mode1.yaml @@ -24,6 +24,12 @@ shots: mode1_shot_3: switch: switch_3 profile: mode1_shot_3 + mode1_shot_5: + switch: switch_5 + profile: mode1_shot_5 + mode1_shot_6: + switch: switch_6 + profile: mode1_shot_6 shot_profiles: mode1_shot_2: @@ -32,10 +38,21 @@ shot_profiles: - name: mode1_one - name: mode1_two - name: mode1_three - mode1_shot_3: + mode1_shot_3: # Test block: True show: rainbow2 block: True states: - name: mode1_one - name: mode1_two - name: mode1_three + mode1_shot_5: # Test block: False + show: rainbow2 + block: False + states: + - name: mode1_one + - name: mode1_two + mode1_shot_6: # Test block default + show: rainbow2 + states: + - name: mode1_one + - name: mode1_two diff --git a/mpf/tests/test_Shots.py b/mpf/tests/test_Shots.py index 3103c28a1..bf48e916d 100644 --- a/mpf/tests/test_Shots.py +++ b/mpf/tests/test_Shots.py @@ -29,7 +29,7 @@ def stop_game(self): self.advance_time_and_run() self.assertIsNone(self.machine.game) - def test_block(self): + def test_block_true(self): self.mock_event("playfield_active") self.hit_and_release_switch("switch_3") self.advance_time_and_run(.1) @@ -60,6 +60,68 @@ def test_block(self): self.assertEqual("unlit", self.machine.shots["shot_3"].state_name) self.assertEqual("mode1_two", self.machine.shots["mode1_shot_3"].state_name) + def test_block_false(self): + self.mock_event("playfield_active") + self.hit_and_release_switch("switch_5") + self.advance_time_and_run(.1) + self.assertEventCalled("playfield_active") + + self.start_game() + self.assertEqual("unlit", self.machine.shots["shot_5"].state_name) + + self.hit_and_release_switch("switch_5") + self.advance_time_and_run(.1) + self.assertTrue(self.machine.shots["shot_5"].enabled) + self.assertEqual("lit", self.machine.shots["shot_5"].state_name) + + self.machine.shots["shot_5"].reset() + self.assertEqual("unlit", self.machine.shots["shot_5"].state_name) + + # Start the mode and make sure those shots load + self.start_mode("mode1") + + self.assertTrue(self.machine.shots["shot_5"].enabled) + self.assertTrue(self.machine.shots["mode1_shot_5"].enabled) + self.assertEqual("unlit", self.machine.shots["shot_5"].state_name) + self.assertEqual("mode1_one", self.machine.shots["mode1_shot_5"].state_name) + + self.hit_and_release_switch("switch_5") + self.advance_time_and_run(.1) + + self.assertEqual("lit", self.machine.shots["shot_5"].state_name) + self.assertEqual("mode1_two", self.machine.shots["mode1_shot_5"].state_name) + + def test_block_default(self): #Default behaves as false + self.mock_event("playfield_active") + self.hit_and_release_switch("switch_6") + self.advance_time_and_run(.1) + self.assertEventCalled("playfield_active") + + self.start_game() + self.assertEqual("unlit", self.machine.shots["shot_6"].state_name) + + self.hit_and_release_switch("switch_6") + self.advance_time_and_run(.1) + self.assertTrue(self.machine.shots["shot_6"].enabled) + self.assertEqual("lit", self.machine.shots["shot_6"].state_name) + + self.machine.shots["shot_6"].reset() + self.assertEqual("unlit", self.machine.shots["shot_6"].state_name) + + # Start the mode and make sure those shots load + self.start_mode("mode1") + + self.assertTrue(self.machine.shots["shot_6"].enabled) + self.assertTrue(self.machine.shots["mode1_shot_6"].enabled) + self.assertEqual("unlit", self.machine.shots["shot_6"].state_name) + self.assertEqual("mode1_one", self.machine.shots["mode1_shot_6"].state_name) + + self.hit_and_release_switch("switch_6") + self.advance_time_and_run(.1) + + self.assertEqual("lit", self.machine.shots["shot_6"].state_name) + self.assertEqual("mode1_two", self.machine.shots["mode1_shot_6"].state_name) + def test_loading_shots(self): # Make sure machine-wide shots load & mode-specific shots do not self.assertIn('shot_1', self.machine.shots) From 6ceda4cfe82dc5459b46d4263724985f07dafdd3 Mon Sep 17 00:00:00 2001 From: Alex Lobascio Date: Sun, 25 Aug 2024 01:28:12 -0700 Subject: [PATCH 09/13] refactor shot block tests to use common test structure --- mpf/tests/test_Shots.py | 97 +++++++++++------------------------------ 1 file changed, 25 insertions(+), 72 deletions(-) diff --git a/mpf/tests/test_Shots.py b/mpf/tests/test_Shots.py index bf48e916d..c48360421 100644 --- a/mpf/tests/test_Shots.py +++ b/mpf/tests/test_Shots.py @@ -29,98 +29,51 @@ def stop_game(self): self.advance_time_and_run() self.assertIsNone(self.machine.game) - def test_block_true(self): + def block_test(self, switch, shot, should_block): + high_priority_shot = "mode1_" + shot self.mock_event("playfield_active") - self.hit_and_release_switch("switch_3") + self.hit_and_release_switch(switch) self.advance_time_and_run(.1) self.assertEventCalled("playfield_active") self.start_game() - self.assertEqual("unlit", self.machine.shots["shot_3"].state_name) + self.assertEqual("unlit", self.machine.shots[shot].state_name) - self.hit_and_release_switch("switch_3") + self.hit_and_release_switch(switch) self.advance_time_and_run(.1) - self.assertTrue(self.machine.shots["shot_3"].enabled) - self.assertEqual("lit", self.machine.shots["shot_3"].state_name) + self.assertFalse(self.machine.shots[high_priority_shot].enabled) + self.assertTrue(self.machine.shots[shot].enabled) + self.assertEqual("lit", self.machine.shots[shot].state_name) - self.machine.shots["shot_3"].reset() - self.assertEqual("unlit", self.machine.shots["shot_3"].state_name) + self.machine.shots[shot].reset() + self.assertEqual("unlit", self.machine.shots[shot].state_name) # Start the mode and make sure those shots load self.start_mode("mode1") - self.assertTrue(self.machine.shots["shot_3"].enabled) - self.assertTrue(self.machine.shots["mode1_shot_3"].enabled) - self.assertEqual("unlit", self.machine.shots["shot_3"].state_name) - self.assertEqual("mode1_one", self.machine.shots["mode1_shot_3"].state_name) + self.assertTrue(self.machine.shots[shot].enabled) + self.assertTrue(self.machine.shots[high_priority_shot].enabled) + self.assertEqual("unlit", self.machine.shots[shot].state_name) + self.assertEqual("mode1_one", self.machine.shots[high_priority_shot].state_name) - self.hit_and_release_switch("switch_3") + self.hit_and_release_switch(switch) self.advance_time_and_run(.1) - self.assertEqual("unlit", self.machine.shots["shot_3"].state_name) - self.assertEqual("mode1_two", self.machine.shots["mode1_shot_3"].state_name) - - def test_block_false(self): - self.mock_event("playfield_active") - self.hit_and_release_switch("switch_5") - self.advance_time_and_run(.1) - self.assertEventCalled("playfield_active") + if should_block: + self.assertEqual("unlit", self.machine.shots[shot].state_name) + else: + self.assertEqual("lit", self.machine.shots[shot].state_name) - self.start_game() - self.assertEqual("unlit", self.machine.shots["shot_5"].state_name) + self.assertEqual("mode1_two", self.machine.shots[high_priority_shot].state_name) - self.hit_and_release_switch("switch_5") - self.advance_time_and_run(.1) - self.assertTrue(self.machine.shots["shot_5"].enabled) - self.assertEqual("lit", self.machine.shots["shot_5"].state_name) - - self.machine.shots["shot_5"].reset() - self.assertEqual("unlit", self.machine.shots["shot_5"].state_name) - - # Start the mode and make sure those shots load - self.start_mode("mode1") - - self.assertTrue(self.machine.shots["shot_5"].enabled) - self.assertTrue(self.machine.shots["mode1_shot_5"].enabled) - self.assertEqual("unlit", self.machine.shots["shot_5"].state_name) - self.assertEqual("mode1_one", self.machine.shots["mode1_shot_5"].state_name) - - self.hit_and_release_switch("switch_5") - self.advance_time_and_run(.1) + def test_block_true(self): + self.block_test("switch_3", "shot_3", True) - self.assertEqual("lit", self.machine.shots["shot_5"].state_name) - self.assertEqual("mode1_two", self.machine.shots["mode1_shot_5"].state_name) + def test_block_false(self): + self.block_test("switch_5", "shot_5", False) def test_block_default(self): #Default behaves as false - self.mock_event("playfield_active") - self.hit_and_release_switch("switch_6") - self.advance_time_and_run(.1) - self.assertEventCalled("playfield_active") - - self.start_game() - self.assertEqual("unlit", self.machine.shots["shot_6"].state_name) - - self.hit_and_release_switch("switch_6") - self.advance_time_and_run(.1) - self.assertTrue(self.machine.shots["shot_6"].enabled) - self.assertEqual("lit", self.machine.shots["shot_6"].state_name) - - self.machine.shots["shot_6"].reset() - self.assertEqual("unlit", self.machine.shots["shot_6"].state_name) - - # Start the mode and make sure those shots load - self.start_mode("mode1") - - self.assertTrue(self.machine.shots["shot_6"].enabled) - self.assertTrue(self.machine.shots["mode1_shot_6"].enabled) - self.assertEqual("unlit", self.machine.shots["shot_6"].state_name) - self.assertEqual("mode1_one", self.machine.shots["mode1_shot_6"].state_name) - - self.hit_and_release_switch("switch_6") - self.advance_time_and_run(.1) - - self.assertEqual("lit", self.machine.shots["shot_6"].state_name) - self.assertEqual("mode1_two", self.machine.shots["mode1_shot_6"].state_name) + self.block_test("switch_6", "shot_6", False) def test_loading_shots(self): # Make sure machine-wide shots load & mode-specific shots do not From 615c4be06217d2588dd9b2ec3dd94eac2f9638ef Mon Sep 17 00:00:00 2001 From: Anthony van Winkle Date: Sun, 1 Sep 2024 13:53:07 -0700 Subject: [PATCH 10/13] Correct calculation of bonus score for unset player vars --- mpf/modes/bonus/code/bonus.py | 9 ++++----- .../machine_files/bonus/modes/bonus/config/bonus.yaml | 4 ++++ mpf/tests/test_Bonus.py | 10 ++++++++++ 3 files changed, 18 insertions(+), 5 deletions(-) diff --git a/mpf/modes/bonus/code/bonus.py b/mpf/modes/bonus/code/bonus.py index 99ec5c270..3fe1f09a2 100644 --- a/mpf/modes/bonus/code/bonus.py +++ b/mpf/modes/bonus/code/bonus.py @@ -91,11 +91,10 @@ def _bonus_next_item(self): self._subtotal() return - # Calling player.vars.get() instead of player.get() bypasses the - # auto-fill zero and will throw if there is no player variable. - # The fallback value of 1 is used for bonus entries that don't use - # a player score, which are multiplied by one to get the bonus. - hits = self.player.vars.get(entry['player_score_entry'], 1) + # If a player_score_entry is provided, use player getattr to get a + # fallback value of zero if the variable is not set. Otherwise + # use 1 as the multiplier for non-player-score bonuses. + hits = self.player[entry['player_score_entry']] if entry['player_score_entry'] else 1 score = entry['score'].evaluate([]) * hits if (not score and entry['skip_if_zero']) or (score < 0 and entry['skip_if_negative']): diff --git a/mpf/tests/machine_files/bonus/modes/bonus/config/bonus.yaml b/mpf/tests/machine_files/bonus/modes/bonus/config/bonus.yaml index 7d9dcc207..6ee33d372 100644 --- a/mpf/tests/machine_files/bonus/modes/bonus/config/bonus.yaml +++ b/mpf/tests/machine_files/bonus/modes/bonus/config/bonus.yaml @@ -12,3 +12,7 @@ mode_settings: score: 5000 player_score_entry: modes reset_player_score_entry: False + - event: bonus_undefined_var + score: 5000 + skip_if_zero: false + player_score_entry: undefined_var diff --git a/mpf/tests/test_Bonus.py b/mpf/tests/test_Bonus.py index 098df096c..64234f340 100644 --- a/mpf/tests/test_Bonus.py +++ b/mpf/tests/test_Bonus.py @@ -38,6 +38,7 @@ def test_slam_tilt_in_service(self): def testBonus(self): self.mock_event("bonus_ramps") self.mock_event("bonus_modes") + self.mock_event("bonus_undefined_var") self.mock_event("bonus_subtotal") self.mock_event("bonus_multiplier") self.mock_event("bonus_total") @@ -78,6 +79,8 @@ def testBonus(self): self.assertEqual(3, self._last_event_kwargs["bonus_ramps"]["hits"]) self.assertEqual(10000, self._last_event_kwargs["bonus_modes"]["score"]) self.assertEqual(2, self._last_event_kwargs["bonus_modes"]["hits"]) + self.assertEqual(0, self._last_event_kwargs["bonus_undefined_var"]["score"]) + self.assertEqual(0, self._last_event_kwargs["bonus_undefined_var"]["hits"]) self.assertEqual(13000, self._last_event_kwargs["bonus_subtotal"]["score"]) self.assertEqual(5, self._last_event_kwargs["bonus_multiplier"]["multiplier"]) self.assertEqual(65000, self._last_event_kwargs["bonus_total"]["score"]) @@ -128,6 +131,7 @@ def testBonus(self): self.mock_event("bonus_start") self.mock_event("bonus_ramps") self.mock_event("bonus_modes") + self.mock_event("bonus_undefined_var") self.mock_event("bonus_subtotal") self.mock_event("bonus_multiplier") self.mock_event("bonus_total") @@ -157,6 +161,12 @@ def testBonus(self): self.assertEventNotCalled('bonus_multiplier') self.assertEventNotCalled('bonus_total') + self.advance_time_and_run(.5) + self.assertEventCalled('bonus_undefined_var') + self.assertEventNotCalled('bonus_subtotal') + self.assertEventNotCalled('bonus_multiplier') + self.assertEventNotCalled('bonus_total') + self.advance_time_and_run(.5) self.assertEventCalled('bonus_subtotal') self.assertEventNotCalled('bonus_multiplier') From 2db6840060bb1c34a343dbb6e09f88a4c38b1af6 Mon Sep 17 00:00:00 2001 From: Anthony van Winkle Date: Sun, 1 Sep 2024 13:59:27 -0700 Subject: [PATCH 11/13] Include tests for static bonuses --- .../bonus/modes/bonus/config/bonus.yaml | 2 ++ mpf/tests/test_Bonus.py | 22 ++++++++++++++----- 2 files changed, 18 insertions(+), 6 deletions(-) diff --git a/mpf/tests/machine_files/bonus/modes/bonus/config/bonus.yaml b/mpf/tests/machine_files/bonus/modes/bonus/config/bonus.yaml index 6ee33d372..7c92e475e 100644 --- a/mpf/tests/machine_files/bonus/modes/bonus/config/bonus.yaml +++ b/mpf/tests/machine_files/bonus/modes/bonus/config/bonus.yaml @@ -16,3 +16,5 @@ mode_settings: score: 5000 skip_if_zero: false player_score_entry: undefined_var + - event: bonus_static + score: 2000 diff --git a/mpf/tests/test_Bonus.py b/mpf/tests/test_Bonus.py index 64234f340..5a9aba7ba 100644 --- a/mpf/tests/test_Bonus.py +++ b/mpf/tests/test_Bonus.py @@ -39,6 +39,7 @@ def testBonus(self): self.mock_event("bonus_ramps") self.mock_event("bonus_modes") self.mock_event("bonus_undefined_var") + self.mock_event("bonus_static") self.mock_event("bonus_subtotal") self.mock_event("bonus_multiplier") self.mock_event("bonus_total") @@ -81,10 +82,12 @@ def testBonus(self): self.assertEqual(2, self._last_event_kwargs["bonus_modes"]["hits"]) self.assertEqual(0, self._last_event_kwargs["bonus_undefined_var"]["score"]) self.assertEqual(0, self._last_event_kwargs["bonus_undefined_var"]["hits"]) - self.assertEqual(13000, self._last_event_kwargs["bonus_subtotal"]["score"]) + self.assertEqual(2000, self._last_event_kwargs["bonus_static"]["score"]) + self.assertEqual(1, self._last_event_kwargs["bonus_static"]["hits"]) + self.assertEqual(15000, self._last_event_kwargs["bonus_subtotal"]["score"]) self.assertEqual(5, self._last_event_kwargs["bonus_multiplier"]["multiplier"]) - self.assertEqual(65000, self._last_event_kwargs["bonus_total"]["score"]) - self.assertEqual(66337, self.machine.game.player.score) + self.assertEqual(75000, self._last_event_kwargs["bonus_total"]["score"]) + self.assertEqual(76337, self.machine.game.player.score) # check resets self.assertEqual(0, self.machine.game.player.ramps) @@ -105,10 +108,10 @@ def testBonus(self): self.assertEqual(0, self._last_event_kwargs["bonus_ramps"]["hits"]) self.assertEqual(10000, self._last_event_kwargs["bonus_modes"]["score"]) self.assertEqual(2, self._last_event_kwargs["bonus_modes"]["hits"]) - self.assertEqual(10000, self._last_event_kwargs["bonus_subtotal"]["score"]) + self.assertEqual(12000, self._last_event_kwargs["bonus_subtotal"]["score"]) self.assertEqual(5, self._last_event_kwargs["bonus_multiplier"]["multiplier"]) - self.assertEqual(50000, self._last_event_kwargs["bonus_total"]["score"]) - self.assertEqual(116337, self.machine.game.player.score) + self.assertEqual(60000, self._last_event_kwargs["bonus_total"]["score"]) + self.assertEqual(136337, self.machine.game.player.score) # multiplier should stay the same self.assertEqual(0, self.machine.game.player.ramps) @@ -132,6 +135,7 @@ def testBonus(self): self.mock_event("bonus_ramps") self.mock_event("bonus_modes") self.mock_event("bonus_undefined_var") + self.mock_event("bonus_static") self.mock_event("bonus_subtotal") self.mock_event("bonus_multiplier") self.mock_event("bonus_total") @@ -167,6 +171,12 @@ def testBonus(self): self.assertEventNotCalled('bonus_multiplier') self.assertEventNotCalled('bonus_total') + self.advance_time_and_run(.5) + self.assertEventCalled('bonus_static') + self.assertEventNotCalled('bonus_subtotal') + self.assertEventNotCalled('bonus_multiplier') + self.assertEventNotCalled('bonus_total') + self.advance_time_and_run(.5) self.assertEventCalled('bonus_subtotal') self.assertEventNotCalled('bonus_multiplier') From 6bca1af5c0cf4461c02f46641a9b9f51afcf5305 Mon Sep 17 00:00:00 2001 From: Anthony van Winkle Date: Sun, 1 Sep 2024 14:09:08 -0700 Subject: [PATCH 12/13] Fix misnamed variable in virtual switch --- mpf/platforms/virtual.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mpf/platforms/virtual.py b/mpf/platforms/virtual.py index 3ccb869d2..7d8164a69 100644 --- a/mpf/platforms/virtual.py +++ b/mpf/platforms/virtual.py @@ -107,7 +107,7 @@ async def get_hw_switch_states(self): initial_active_switches = [] for switch_name in Util.string_to_list(self.machine.config['virtual_platform_start_active_switches']): if switch_name not in self.machine.switches.keys(): - if " " in switch: + if " " in switch_name: self.raise_config_error("MPF no longer supports lists separated by space in " "virtual_platform_start_active_switches. Please separate " "switches by comma: {}.".format(switch_name), 1) From f73e649ba6dedee7115e7465369b5a6acb2b25d4 Mon Sep 17 00:00:00 2001 From: Anthony van Winkle Date: Sun, 1 Sep 2024 14:23:58 -0700 Subject: [PATCH 13/13] Linter fixes --- mpf/modes/high_score/code/high_score.py | 3 ++- mpf/platforms/fast/fast_exp_board.py | 3 ++- mpf/plugins/platform_integration_test_runner.py | 2 +- 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/mpf/modes/high_score/code/high_score.py b/mpf/modes/high_score/code/high_score.py index 63ab9bdcb..23aa3f7bc 100644 --- a/mpf/modes/high_score/code/high_score.py +++ b/mpf/modes/high_score/code/high_score.py @@ -196,7 +196,8 @@ async def _run(self) -> None: # ask player for initials if we do not know them if not player.initials: try: - player.initials = await self._ask_player_for_initials(player, award_names[i], value, category_name) + player.initials = await self._ask_player_for_initials(player, award_names[i], + value, category_name) except asyncio.TimeoutError: del new_list[i] # no entry when the player missed the timeout diff --git a/mpf/platforms/fast/fast_exp_board.py b/mpf/platforms/fast/fast_exp_board.py index 78adffb41..98eab1619 100644 --- a/mpf/platforms/fast/fast_exp_board.py +++ b/mpf/platforms/fast/fast_exp_board.py @@ -2,6 +2,7 @@ import asyncio from base64 import b16decode +from binascii import Error as binasciiError from importlib import import_module from packaging import version @@ -176,7 +177,7 @@ def update_leds(self): try: self.communicator.send_bytes(b16decode(f'{msg_header}{msg}'), log_msg) - except Exception as e: + except binasciiError as e: self.log.error( f"Error decoding the following message for board {breakout_address} : {msg_header}{msg}") self.log.info("Attempted update that caused this error: %s", dirty_leds) diff --git a/mpf/plugins/platform_integration_test_runner.py b/mpf/plugins/platform_integration_test_runner.py index 2155d302e..bee98468c 100644 --- a/mpf/plugins/platform_integration_test_runner.py +++ b/mpf/plugins/platform_integration_test_runner.py @@ -227,7 +227,7 @@ async def move_ball_from_drain_to_trough(self, **kwargs): drain_switches = self.machine.ball_devices.items_tagged('drain')[0].config.get('ball_switches') self.info_log("Found drain switches: %s of type %s", drain_switches, type(drain_switches)) # If there's only one drain switch it might be a single value, rather than a list - drain_switch = drain_switches if type(drain_switches) is str else drain_switches[-1] + drain_switch = drain_switches if isinstance(drain_switches, str) else drain_switches[-1] self.info_log("Setting drain switch '%s' to zero", drain_switch) self.set_switch_sync(drain_switch, 0) await asyncio.sleep(0.25)