diff --git a/mpf/config_spec.yaml b/mpf/config_spec.yaml index b15af7845..2758be7cb 100644 --- a/mpf/config_spec.yaml +++ b/mpf/config_spec.yaml @@ -972,6 +972,8 @@ multiballs: replace_balls_in_play: single|bool|false source_playfield: single|machine(ball_devices)|playfield shoot_again: single|template_ms|10s + hurry_up_time: single|template_ms|0 + grace_period: single|template_ms|0 ball_locks: list|machine(ball_devices)|None enable_events: event_handler|event_handler:ms|None disable_events: event_handler|event_handler:ms|None diff --git a/mpf/devices/multiball.py b/mpf/devices/multiball.py index 9b0acfd14..94234855f 100644 --- a/mpf/devices/multiball.py +++ b/mpf/devices/multiball.py @@ -9,7 +9,7 @@ from mpf.core.system_wide_device import SystemWideDevice -@DeviceMonitor("shoot_again", "balls_added_live", "balls_live_target") +@DeviceMonitor("shoot_again", "grace_period", "hurry_up", "balls_added_live", "balls_live_target") class Multiball(EnableDisableMixin, SystemWideDevice, ModeDevice): """Multiball device for MPF.""" @@ -18,7 +18,8 @@ class Multiball(EnableDisableMixin, SystemWideDevice, ModeDevice): collection = 'multiballs' class_label = 'multiball' - __slots__ = ["ball_locks", "source_playfield", "delay", "balls_added_live", "balls_live_target", "shoot_again"] + __slots__ = ["ball_locks", "source_playfield", "delay", "balls_added_live", "balls_live_target", "shoot_again", + "grace_period", "hurry_up"] def __init__(self, machine, name): """Initialise multiball.""" @@ -30,6 +31,8 @@ def __init__(self, machine, name): self.balls_added_live = 0 self.balls_live_target = 0 self.shoot_again = False + self.grace_period = False + self.hurry_up = False @property def can_exist_outside_of_game(self): @@ -133,11 +136,7 @@ def start(self): self.machine.events.add_handler('ball_drain', self._ball_drain_shoot_again, priority=1000) - # Register stop handler - if shoot_again_ms > 0: - self.delay.add(name='disable_shoot_again', - ms=shoot_again_ms, - callback=self.stop) + self.timer_start() self.machine.events.post("multiball_" + self.name + "_started", balls=self.balls_live_target) @@ -147,6 +146,58 @@ def start(self): balls: The number of balls in this multiball ''' + def timer_start(self) -> None: + """Start the timer. + + This is started when multiball starts if configured. + """ + self.machine.events.post('ball_save_{}_timer_start'.format(self.name)) + '''event: ball_save_(name)_timer_start + desc: The multiball ball save called (name) has just start its countdown timer. + ''' + + shoot_again_ms = self.config['shoot_again'].evaluate([]) + grace_period_ms = self.config['grace_period'].evaluate([]) + hurry_up_time_ms = self.config['hurry_up_time'].evaluate([]) + if shoot_again_ms > 0: + self.debug_log('Starting ball save timer: %ss', + shoot_again_ms) + # Register stop handler + self.delay.add(name='disable_shoot_again', + ms=(shoot_again_ms + + grace_period_ms), + callback=self.stop) + if grace_period_ms > 0: + self.grace_period = True + self.delay.add(name='grace_period', + ms=shoot_again_ms, + callback=self._grace_period) + if hurry_up_time_ms > 0: + self.hurry_up = True + self.delay.add(name='hurry_up', + ms=(shoot_again_ms - + hurry_up_time_ms), + callback=self._hurry_up) + + def _hurry_up(self) -> None: + self.debug_log("Starting Hurry Up") + + self.hurry_up = False + self.machine.events.post('multiball_{}_hurry_up'.format(self.name)) + '''event: multiball_(name)_hurry_up + desc: The multiball ball save called (name) has just entered its hurry up mode. + ''' + + def _grace_period(self) -> None: + self.debug_log("Starting Grace Period") + + self.grace_period = False + self.machine.events.post('multiball_{}_grace_period'.format(self.name)) + '''event: multiball_(name)_grace_period + desc: The multiball ball save called (name) has just entered its grace period + time. + ''' + def _ball_drain_shoot_again(self, balls, **kwargs): del kwargs @@ -203,6 +254,20 @@ def stop(self): # disable shoot again self.machine.events.remove_handler(self._ball_drain_shoot_again) + if self.grace_period: + self.machine.events.remove_handler(self._grace_period) + self.grace_period = False + self.machine.events.post("multiball_" + self.name + "_grace_period") + '''event: multiball_(name)_grace_period + desc: Grace period for multiball (name) has occurred due to mode stop. + ''' + if self.hurry_up: + self.machine.events.remove_handler(self._hurry_up) + self.hurry_up = False + self.machine.events.post("multiball_" + self.name + "_hurry_up") + '''event: multiball_(name)_hurry_up + desc: Hurry Up for multiball (name) has occurred due to mode stop. + ''' self.machine.events.post("multiball_" + self.name + "_shoot_again_ended") '''event: multiball_(name)_shoot_again_ended desc: Shoot again for multiball (name) has ended. diff --git a/mpf/tests/machine_files/multiball/config/config.yaml b/mpf/tests/machine_files/multiball/config/config.yaml index 887d9f178..ea3d0d0f0 100644 --- a/mpf/tests/machine_files/multiball/config/config.yaml +++ b/mpf/tests/machine_files/multiball/config/config.yaml @@ -69,6 +69,7 @@ modes: - mode2 - mode3 - mode4 + - mode5 multiballs: mb1: @@ -109,3 +110,10 @@ multiballs: shoot_again: machine.shoot_again_sec * 1000 start_events: mb_placeholder_start stop_events: mb_placeholder_stop + mb_alltimers: + ball_count: 2 + shoot_again: 30s + hurry_up_time: 10s + grace_period: 5s + start_events: mb_alltimers_start + stop_events: mb_alltimers_stop diff --git a/mpf/tests/machine_files/multiball/modes/mode5/config/mode5.yaml b/mpf/tests/machine_files/multiball/modes/mode5/config/mode5.yaml new file mode 100644 index 000000000..42c9ba096 --- /dev/null +++ b/mpf/tests/machine_files/multiball/modes/mode5/config/mode5.yaml @@ -0,0 +1,19 @@ +#config_version=5 +mode: + start_events: start_mode5 + stop_events: stop_mode5 + +multiballs: + mb_mode5: + ball_count: 2 + shoot_again: 30s + hurry_up_time: 10s + grace_period: 5s + start_events: mb_mode5_start + stop_events: mb_mode5_stop + + mb_mode5_lean: + ball_count: 2 + shoot_again: 30s + start_events: mb_mode5_lean_start + stop_events: mb_mode5_lean_stop diff --git a/mpf/tests/test_MultiBall.py b/mpf/tests/test_MultiBall.py index d93728314..555f8ed8a 100644 --- a/mpf/tests/test_MultiBall.py +++ b/mpf/tests/test_MultiBall.py @@ -1032,3 +1032,129 @@ def testShootAgainPlaceholder(self): self.assertAvailableBallsOnPlayfield(1) # mb should end self.assertEventCalled("multiball_mb_placeholder_ended") + + def testShootAgainHurryUpAndGracePeriod(self): + self.fill_troughs() + self.start_game() + self.assertAvailableBallsOnPlayfield(1) + self.mock_event("multiball_mb_alltimers_ended") + self.mock_event("multiball_mb_alltimers_shoot_again_ended") + self.mock_event("multiball_mb_alltimers_grace_period") + self.mock_event("multiball_mb_alltimers_hurry_up") + + # start mb 30s shoot again, 10s hurry up, 5s grace + self.post_event("mb_alltimers_start") + self.advance_time_and_run(5) + self.assertAvailableBallsOnPlayfield(2) + + # drain one ball + self.drain_one_ball() + self.advance_time_and_run(5) + # shoot again should bring it back + self.assertAvailableBallsOnPlayfield(2) + self.assertEventNotCalled("multiball_mb_alltimers_ended") + self.assertEventNotCalled("multiball_mb_alltimers_shoot_again_ended") + self.assertEventNotCalled("multiball_mb_alltimers_grace_period") + self.assertEventNotCalled("multiball_mb_alltimers_hurry_up") + + #advance time to hurry up + self.advance_time_and_run(10) + self.assertEventCalled("multiball_mb_alltimers_hurry_up") + self.assertAvailableBallsOnPlayfield(2) + self.assertEventNotCalled("multiball_mb_alltimers_ended") + self.assertEventNotCalled("multiball_mb_alltimers_shoot_again_ended") + self.assertEventNotCalled("multiball_mb_alltimers_grace_period") + + # drain one ball + self.drain_one_ball() + self.advance_time_and_run(5) + # shoot again should bring it back + self.assertAvailableBallsOnPlayfield(2) + + # wait 7s for shoot again to end, but within grace period + self.advance_time_and_run(7) + self.assertEventCalled("multiball_mb_alltimers_grace_period") + self.assertEventNotCalled("multiball_mb_alltimers_ended") + self.assertEventNotCalled("multiball_mb_alltimers_shoot_again_ended") + + # drain one ball after grace period has ended + self.advance_time_and_run(5) + self.drain_one_ball() + self.advance_time_and_run(5) + self.assertAvailableBallsOnPlayfield(1) + # mb should end + self.assertEventCalled("multiball_mb_alltimers_ended") + + def testShootAgainModeEnd(self): + self.fill_troughs() + self.start_game() + self.assertAvailableBallsOnPlayfield(1) + self.mock_event("multiball_mb_mode5_ended") + self.mock_event("multiball_mb_mode5_shoot_again_ended") + self.mock_event("multiball_mb_mode5_grace_period") + self.mock_event("multiball_mb_mode5_hurry_up") + + #start Mode5 + self.post_event("start_mode5") + + # start mb 30s shoot again, 10s hurry up, 5s grace + self.post_event("mb_mode5_start") + self.advance_time_and_run(5) + self.assertAvailableBallsOnPlayfield(2) + self.assertEventNotCalled("multiball_mb_mode5_ended") + self.assertEventNotCalled("multiball_mb_mode5_shoot_again_ended") + self.assertEventNotCalled("multiball_mb_mode5_grace_period") + self.assertEventNotCalled("multiball_mb_mode5_hurry_up") + + #stop Mode5 + self.post_event("stop_mode5") + self.advance_time_and_run(5) + self.assertEventNotCalled("multiball_mb_mode5_ended") + self.assertEventCalled("multiball_mb_mode5_shoot_again_ended") + self.assertEventCalled("multiball_mb_mode5_grace_period") + self.assertEventCalled("multiball_mb_mode5_hurry_up") + + # drain one ball + self.drain_one_ball() + self.advance_time_and_run(5) + # shoot again should not bring it back + self.assertAvailableBallsOnPlayfield(1) + self.assertEventCalled("multiball_mb_mode5_ended") + + def testShootAgainModeEndNoGracePeriodOrHurryUp(self): + self.fill_troughs() + self.start_game() + self.assertAvailableBallsOnPlayfield(1) + self.mock_event("multiball_mb_mode5_lean_ended") + self.mock_event("multiball_mb_mode5_lean_shoot_again_ended") + self.mock_event("multiball_mb_mode5_lean_grace_period") + self.mock_event("multiball_mb_mode5_lean_hurry_up") + + #start Mode5 + self.post_event("start_mode5") + + # start mb 30s shoot again + self.post_event("mb_mode5_lean_start") + self.advance_time_and_run(5) + self.assertAvailableBallsOnPlayfield(2) + self.assertEventNotCalled("multiball_mb_mode5_lean_ended") + self.assertEventNotCalled("multiball_mb_mode5_lean_shoot_again_ended") + self.assertEventNotCalled("multiball_mb_mode5_lean_grace_period") + self.assertEventNotCalled("multiball_mb_mode5_lean_hurry_up") + + #stop Mode5 + self.post_event("stop_mode5") + self.advance_time_and_run(5) + self.assertEventNotCalled("multiball_mb_mode5_lean_ended") + self.assertEventCalled("multiball_mb_mode5_lean_shoot_again_ended") + self.assertEventNotCalled("multiball_mb_mode5_lean_grace_period") + self.assertEventNotCalled("multiball_mb_mode5_lean_hurry_up") + + # drain one ball + self.drain_one_ball() + self.advance_time_and_run(5) + # shoot again should not bring it back + self.assertAvailableBallsOnPlayfield(1) + self.assertEventCalled("multiball_mb_mode5_lean_ended") + self.assertEventNotCalled("multiball_mb_mode5_lean_grace_period") + self.assertEventNotCalled("multiball_mb_mode5_lean_hurry_up")