Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Adds hurry up and grace period features to multiball ball save #1590

Merged
merged 9 commits into from
Aug 10, 2021
2 changes: 2 additions & 0 deletions mpf/config_spec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
79 changes: 72 additions & 7 deletions mpf/devices/multiball.py
Original file line number Diff line number Diff line change
Expand Up @@ -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."""
Expand All @@ -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."""
Expand All @@ -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):
Expand Down Expand Up @@ -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)
Expand All @@ -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',
atummons marked this conversation as resolved.
Show resolved Hide resolved
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

Expand Down Expand Up @@ -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.
Expand Down
8 changes: 8 additions & 0 deletions mpf/tests/machine_files/multiball/config/config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ modes:
- mode2
- mode3
- mode4
- mode5

multiballs:
mb1:
Expand Down Expand Up @@ -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
19 changes: 19 additions & 0 deletions mpf/tests/machine_files/multiball/modes/mode5/config/mode5.yaml
Original file line number Diff line number Diff line change
@@ -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
126 changes: 126 additions & 0 deletions mpf/tests/test_MultiBall.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")