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

Multiple eject device handling #1763

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion mpf/devices/ball_device/ball_device.py
Original file line number Diff line number Diff line change
Expand Up @@ -141,7 +141,8 @@ async def handle_mechanical_eject_during_idle(self):
async def lost_idle_ball(self):
"""Lost an ball while the device was idle."""
# handle lost balls
self.warning_log("Ball disappeared while idle. This should not normally happen.")
if self.state == "idle":
self.warning_log("Ball disappeared while idle. This should not normally happen.")
self.available_balls -= 1
self.config['ball_missing_target'].add_missing_balls(1)
await self._balls_missing(1)
Expand Down
20 changes: 20 additions & 0 deletions mpf/devices/ball_device/outgoing_balls_handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -424,6 +424,26 @@ async def _eject_ball(self, eject_request: OutgoingBall, eject_try: int) -> bool
result = await self._handle_confirm(eject_request, ball_eject_process, incoming_ball_at_target,
eject_try)
await self.ball_device.ball_count_handler.end_eject(ball_eject_process, result)

# Check if more balls left than expected, meaning the ejector kicked out multiple
# balls. If so, tag those missing balls as lost (except for mechanical ejects, which
# may be expected, and troughs, which may jam)
if "trough" not in self.ball_device.tags and not self.ball_device.config['mechanical_eject']:
new_balls = await self.ball_device.ball_count_handler.counter.count_balls()
old_balls = self.ball_device.counted_balls

if new_balls < old_balls:
self.info_log("Found %s physical balls and %s expected balls", new_balls, old_balls)
# Post that the ball is lost
await self.ball_device.lost_idle_ball()
# Cancel the eject queue for the lost ball
for _ in range(0, old_balls - new_balls):
if not self._eject_queue.empty():
self._eject_queue.get_nowait()
self._eject_queue.task_done()
self.info_log("Necessary queue requests are cancelled. Updating ball count to %s." % new_balls)
self.ball_device.ball_count_handler._set_ball_count(new_balls)

return result
except asyncio.CancelledError:
ball_eject_process.cancel()
Expand Down
12 changes: 6 additions & 6 deletions mpf/devices/multiball_lock.py
Original file line number Diff line number Diff line change
Expand Up @@ -319,15 +319,15 @@ def _lock_ball(self, unclaimed_balls: int, new_available_balls: int, device: "Ba
'''

# schedule eject of new balls for all physically locked balls
if self.config['balls_to_replace'] == -1 or self.locked_balls <= self.config['balls_to_replace']:
self.debug_log("{} locked balls and {} to replace, requesting {} new balls"
.format(self.locked_balls, self.config['balls_to_replace'], balls_to_lock_physically))
if self.config['balls_to_replace'] == -1 or new_locked_balls <= self.config['balls_to_replace']:
self.info_log("%s locked balls and %s to replace, requesting %s new balls",
new_locked_balls, self.config['balls_to_replace'], balls_to_lock_physically)
self._request_new_balls(balls_to_lock_physically)
else:
self.debug_log("{} locked balls exceeds {} to replace, not requesting any balls"
.format(self.locked_balls, self.config['balls_to_replace']))
self.info_log("%s locked balls exceeds %s to replace, not requesting any balls",
new_locked_balls, self.config['balls_to_replace'])

self.debug_log("Locked %s balls virtually and %s balls physically", balls_to_lock, balls_to_lock_physically)
self.info_log("Locked %s balls virtually and %s balls physically", balls_to_lock, balls_to_lock_physically)

return {'unclaimed_balls': unclaimed_balls - balls_to_lock_physically}

Expand Down
16 changes: 15 additions & 1 deletion mpf/tests/machine_files/multiball_locks/config/config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ coils:
number:
eject_coil4:
number:
eject_coil5:
number:

switches:
s_ball_switch1:
Expand Down Expand Up @@ -40,7 +42,10 @@ switches:
number:
s_lockb2:
number:

s_lockp1:
number:
s_lockp2:
number:
playfields:
playfield:
default_source_device: bd_trough
Expand All @@ -64,10 +69,19 @@ ball_devices:
eject_coil: eject_coil4
ball_switches: s_lockb1, s_lockb2
eject_timeouts: 2s
bd_lock_physical:
eject_coil: eject_coil5
ball_switches: s_lockp1, s_lockp2
eject_timeouts: 2s
eject_events: eject_lock

multiballs:
mb:
ball_count: 2
shoot_again: 0
start_events: mb_start
ball_locks: bd_lock
physical:
ball_count: 3
start_events: physical_mb_start
ball_locks: bd_lock_physical
Original file line number Diff line number Diff line change
Expand Up @@ -24,3 +24,10 @@ multiball_locks:
balls_to_lock: 2
locked_ball_counting_strategy: virtual_only
blocking_facility: foo
lock_physical:
lock_devices: bd_lock_physical
balls_to_lock: 2
locked_ball_counting_strategy: virtual_only
ball_lost_action: add_to_play
disable_events: disable_lock_physical
reset_count_for_current_player_events: physical_mb_start
71 changes: 71 additions & 0 deletions mpf/tests/test_MultiballLock.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
"""Test multiball_locks."""
import logging
from mpf.tests.MpfGameTestCase import MpfGameTestCase
from mpf.tests.MpfTestCase import test_config

Expand Down Expand Up @@ -73,6 +74,76 @@ def test_filling_two(self):
self._last_event_kwargs["multiball_lock_lock_default_locked_ball"])
self.assertEventCalledWith("multiball_lock_lock_default_full", balls=2)


def test_multiple_eject(self):
self.fill_troughs()
self.start_game()
self.mock_event("multiball_lock_lock_default_locked_ball")
self.mock_event("multiball_lock_lock_default_full")
self.post_event("start_default")

lock_device = self.machine.ball_devices["bd_lock_physical"]
mb_lock = self.machine.multiball_locks["lock_physical"]
pf = self.machine.ball_devices["playfield"]

# Add a ball by activating the switch
self.hit_switch_and_run("s_lockp1", 10)
self.assertEqual(1, lock_device.balls)
self.assertEqual(1, mb_lock.locked_balls)
self.assertEqual(1, pf.balls)
self.assertEqual(1, self.machine.game.balls_in_play)
# Add another ball
self.hit_switch_and_run("s_lockp2", 1)
self.assertEqual(2, lock_device.balls)
self.assertEqual(2, mb_lock.locked_balls)
self.advance_time_and_run(3)
# Start the multiball
self.post_event("physical_mb_start")
# self.advance_time_and_run()
# Fake an extra ball leaving
with self.assertLogs("ball_device.bd_lock_physical", level="WARNING") as log:
self.release_switch_and_run("s_lockp2", 10)
self.assertEqual(0, lock_device.balls)
self.assertEqual(0, mb_lock.locked_balls)
self.assertEqual(3, pf.balls)
self.assertEqual(3, self.machine.game.balls_in_play)
# Assert that no warning was logged about the handler
self.assertFalse(log.output)
# log something to prevent the test from breaking
logging.getLogger("ball_device.bd_lock_physical").warning("DEBUG")

def test_lost_ball_add_to_play(self):
self.fill_troughs()
self.start_game()
self.mock_event("multiball_lock_lock_default_locked_ball")
self.mock_event("multiball_lock_lock_default_full")
self.post_event("start_default")

lock_device = self.machine.ball_devices["bd_lock_physical"]
mb_lock = self.machine.multiball_locks["lock_physical"]
pf = self.machine.ball_devices["playfield"]

# Add a ball by activating the switch
self.hit_switch_and_run("s_lockp1", 10)
self.assertEqual(1, lock_device.balls)
self.assertEqual(1, mb_lock.locked_balls)
self.assertEqual(1, pf.balls)
self.assertEqual(1, self.machine.game.balls_in_play)
# Disable the lock
self.post_event("disable_lock_physical")
self.advance_time_and_run(1)
# Add another ball
self.hit_switch_and_run("s_lockp2", 1)
self.assertEqual(1, lock_device.balls)
self.assertEqual(1, mb_lock.locked_balls)
self.advance_time_and_run(3)
# Fake an extra ball leaving
self.release_switch_and_run("s_lockp2", 10)
self.assertEqual(0, lock_device.balls)
self.assertEqual(1, mb_lock.locked_balls)
self.assertEqual(2, pf.balls)
self.assertEqual(2, self.machine.game.balls_in_play)

def test_filling_three(self):
self.fill_troughs()
self.start_game()
Expand Down
Loading