-
Notifications
You must be signed in to change notification settings - Fork 140
/
multiball_lock.py
363 lines (297 loc) · 15.6 KB
/
multiball_lock.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
"""Contains the BallLock device class."""
from typing import List, Optional
from mpf.core.enable_disable_mixin import EnableDisableMixin
from mpf.core.device_monitor import DeviceMonitor
from mpf.core.events import event_handler
from mpf.core.mode_device import ModeDevice
MYPY = False
if MYPY: # pragma: no cover
from mpf.devices.ball_device.ball_device import BallDevice # pylint: disable-msg=cyclic-import,unused-import
from mpf.devices.playfield import Playfield # pylint: disable-msg=cyclic-import,unused-import
@DeviceMonitor("locked_balls")
class MultiballLock(EnableDisableMixin, ModeDevice):
"""Ball lock device which locks balls for a multiball."""
config_section = 'multiball_locks'
collection = 'multiball_locks'
class_label = 'multiball_lock'
__slots__ = ["lock_devices", "source_playfield", "_events", "_locked_balls", "_source_devices",
"_player_var_name"]
def __init__(self, machine, name):
"""Initialize ball lock."""
super().__init__(machine, name)
self.lock_devices = []
self.source_playfield = None # type: Optional[Playfield]
self._source_devices = None # type: Optional[List[BallDevice]]
# initialize variables
self._events = {}
self._locked_balls = 0
# Locked balls in case we are keep_virtual_ball_count_per_player is false
self._player_var_name = '{}_locked_balls'.format(name)
async def _initialize(self):
# load lock_devices
await super()._initialize()
self.lock_devices = []
for device in self.config['lock_devices']:
self.lock_devices.append(device)
self._events[device] = []
self.source_playfield = self.config['source_playfield']
self._source_devices = self.config['source_devices']
self.machine.events.add_handler("player_turn_starting", self._player_turn_starting)
self.machine.events.add_handler("ball_ending", self._ball_ending)
for device in self.lock_devices:
self.machine.events.add_handler(f'balldevice_{device.name}_ball_missing',
self._lost_ball, device=device)
def _enable(self):
"""Enable the lock.
If the lock is not enabled, no balls will be locked.
"""
self.debug_log("Enabling...")
self._register_handlers()
def _ball_ending(self, queue, **kwargs):
del kwargs
if self.config["empty_lock_devices_on_ball_end"]:
total_ball_to_drain = 0
for device in self.lock_devices:
total_ball_to_drain += device.available_balls
device.eject(device.available_balls)
if total_ball_to_drain > 0:
self.log.info("Ejected %s balls to empty lock devices. "
"Waiting for them to drain!", total_ball_to_drain)
queue.wait()
self.machine.events.add_handler("ball_drain", self._wait_for_drain, queue=queue,
ball_counter={"remaining": total_ball_to_drain})
def _player_turn_starting(self, queue, **kwargs):
del kwargs
# reset locked balls
self._locked_balls = 0
# check if the lock is physically full and not virtually full and release balls in that case
if self._physically_remaining_space <= 0 and not self.is_virtually_full:
self.log.info("Will release a ball because the lock is physically full but not virtually for the player.")
# TODO: eject to next playfield
self.lock_devices[0].eject()
queue.wait()
self.machine.events.add_handler("ball_drain", self._wait_for_drain, queue=queue,
ball_counter={"remaining": 1})
def _wait_for_drain(self, queue, ball_counter, balls, **kwargs):
del kwargs
if balls <= 0:
return {'balls': balls}
balls_to_ignore = min(ball_counter["remaining"], balls)
ball_counter["remaining"] -= balls_to_ignore
self.info_log("Ignoring %s drained balls", balls_to_ignore)
if ball_counter["remaining"] == 0:
self.debug_log("Ball of lock drained.")
queue.clear()
self.machine.events.remove_handler_by_event('ball_drain', self._wait_for_drain)
return {'balls': balls - balls_to_ignore}
def _disable(self):
"""Disable the lock.
If the lock is not enabled, no balls will be locked.
"""
self.debug_log("Disabling...")
self._unregister_handlers()
@event_handler(1)
def event_reset_all_counts(self, **kwargs):
"""Event handler for reset_all_counts event."""
del kwargs
self.reset_all_counts()
def reset_all_counts(self):
"""Reset the locked balls for all players."""
if self.config['locked_ball_counting_strategy'] not in ("virtual_only", "min_virtual_physical"):
raise AssertionError("Count is only tracked per player")
for player in self.machine.game.player_list:
player[self._player_var_name] = 0
@event_handler(2)
def event_reset_count_for_current_player(self, **kwargs):
"""Event handler for reset_count_for_current_player event."""
del kwargs
self.reset_count_for_current_player()
def reset_count_for_current_player(self):
"""Reset the locked balls for the current player."""
if self.config['locked_ball_counting_strategy'] in ("virtual_only", "min_virtual_physical", "no_virtual"):
self.locked_balls = 0
else:
raise AssertionError("Cannot reset physical balls")
@property
def locked_balls(self):
"""Return the number of locked balls for the current player."""
if not self.machine.game:
# this is required for the monitor because it will query this variable outside of a game
# remove when #893 is fixed
return None
if self.config['locked_ball_counting_strategy'] == "virtual_only":
return self.machine.game.player[self._player_var_name]
if self.config['locked_ball_counting_strategy'] == "min_virtual_physical":
return min(self.machine.game.player[self._player_var_name], self._physically_locked_balls)
if self.config['locked_ball_counting_strategy'] == "physical_only":
return self._physically_locked_balls
return self._locked_balls
@locked_balls.setter
def locked_balls(self, value):
"""Set the number of locked balls for the current player."""
if self.config['locked_ball_counting_strategy'] in ("virtual_only", "min_virtual_physical"):
self.machine.game.player[self._player_var_name] = value
elif self.config['locked_ball_counting_strategy'] in "no_virtual":
self._locked_balls = value
else:
raise AssertionError("Cannot write locked_balls for strategy {}".format(
self.config['locked_ball_counting_strategy']))
def _register_handlers(self):
priority = (self.mode.priority if self.mode else 0) + \
self.config['priority']
blocking_facility = self.config['blocking_facility']
# register on ball_enter of lock_devices
for device in self.lock_devices:
self.machine.events.add_handler(
'balldevice_' + device.name + '_ball_enter',
self._lock_ball, device=device, priority=priority,
blocking_facility=blocking_facility)
self.machine.events.add_handler(
'balldevice_' + device.name + '_ball_entered',
self._post_events, device=device, priority=priority,
blocking_facility=blocking_facility)
def _unregister_handlers(self):
# unregister ball_enter handlers
self.machine.events.remove_handler(self._lock_ball)
self.machine.events.remove_handler(self._post_events)
@property
def is_virtually_full(self):
"""Return true if lock is full."""
return self.remaining_virtual_space_in_lock <= 0
@property
def remaining_virtual_space_in_lock(self):
"""Return the remaining capacity of the lock."""
balls = self.config['balls_to_lock'] - self.locked_balls
if balls < 0:
balls = 0
return balls
@property
def _max_balls_locked_by_any_player(self):
"""Return the highest number of balls locked for all players."""
max_balls = 0
for player in self.machine.game.player_list:
if max_balls < player[self._player_var_name]:
max_balls = player[self._player_var_name]
return max_balls
@property
def _physically_locked_balls(self):
"""Return the number of physically locked balls."""
balls = 0
for device in self.lock_devices:
balls += device.available_balls
return balls
@property
def _physically_remaining_space(self):
"""Return the space in the physically locks."""
balls = 0
for device in self.lock_devices:
balls += device.capacity - device.available_balls
return balls
def _lock_ball(self, unclaimed_balls: int, new_available_balls: int, device: "BallDevice", **kwargs):
"""Handle result of the _ball_enter event of lock_devices."""
del kwargs
# if there are no balls do not claim anything
if unclaimed_balls <= 0:
return {'unclaimed_balls': unclaimed_balls}
# MPF will make sure that devices get one event per ball
assert unclaimed_balls == 1
if not self.machine.game or not self.machine.game.player:
# bail out if we are outside of a game
return {'unclaimed_balls': unclaimed_balls}
# if already full do not take any balls
if self.is_virtually_full:
self.debug_log("Cannot lock balls. Lock is full.")
return {'unclaimed_balls': unclaimed_balls}
# first take care of virtual ball count in lock
capacity = self.remaining_virtual_space_in_lock
# take ball up to capacity limit
if unclaimed_balls > capacity:
balls_to_lock = capacity
else:
balls_to_lock = unclaimed_balls
new_locked_balls = self.locked_balls + 1
# post event for ball capture
self._events[device].append({"event": 'multiball_lock_' + self.name + '_locked_ball',
"total_balls_locked": new_locked_balls})
'''event: multiball_lock_(name)_locked_ball
desc: The multiball lock device (name) has just locked one additional ball.
args:
total_balls_locked: The current total number of balls this device
has locked.
'''
if self.config['locked_ball_counting_strategy'] != "physical_only":
self.locked_balls = new_locked_balls
# now check how many balls we want physically in the lock
balls_to_lock_physically = balls_to_lock
if self._physically_remaining_space < new_available_balls:
# we cannot lock if there isn't any space left
balls_to_lock_physically = 0
self.debug_log("Will not keep the ball. Device is full. Remaining space: %s. Balls to lock: %s",
self._physically_remaining_space, balls_to_lock)
if (self.config['locked_ball_counting_strategy'] in ("virtual_only", "min_virtual_physical") and
self._max_balls_locked_by_any_player < self._physically_locked_balls + new_available_balls):
# only keep ball if any player could use it
self.debug_log("Will not keep ball because no player could use it. Max locked balls by any player "
"is %s and we physically got %s", self._max_balls_locked_by_any_player,
self._physically_locked_balls)
balls_to_lock_physically = 0
if self.config['locked_ball_counting_strategy'] == "min_virtual_physical":
# do not lock if the lock would be physically full but not virtually
if (self._physically_remaining_space <= new_available_balls and
self.config['balls_to_lock'] - self.machine.game.player[self._player_var_name] > 0):
self.debug_log("Will not keep ball because the lock would be physically full but virtually still "
"has space for this player.")
balls_to_lock_physically = 0
elif (self.config['locked_ball_counting_strategy'] != "physical_only" and
not self.is_virtually_full and self._physically_remaining_space <= new_available_balls):
# do not lock if the lock would be physically full but not virtually
balls_to_lock_physically = 0
self.debug_log("Will not keep ball because the lock would be physically full but virtually still "
"has space for this player.")
# check if we are full now and post event if yes
if (self.config['locked_ball_counting_strategy'] == "physical_only" and
new_locked_balls >= self.config['balls_to_lock']) or \
self.remaining_virtual_space_in_lock == 0:
self._events[device].append({'event': 'multiball_lock_' + self.name + '_full',
'balls': new_locked_balls})
'''event: multiball_lock_(name)_full
desc: The multiball lock device (name) is now full.
args:
balls: The number of balls currently locked in this device.
'''
# schedule eject of new balls for all physically locked balls
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.info_log("%s locked balls exceeds %s to replace, not requesting any balls",
new_locked_balls, self.config['balls_to_replace'])
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}
def _lost_ball(self, device, balls, **kwargs):
del kwargs
self.info_log("Ball device %s lost %s balls, %s has %s locked balls and action %s",
device.name, balls, self.name, self.locked_balls,
self.config['ball_lost_action'])
if self.locked_balls and self.config['ball_lost_action'] == "add_to_play":
self.info_log("Ball device %s lost %s balls, adding to balls_in_play", device.name, balls)
self.machine.game.balls_in_play += balls
# Do not claim the ball
return {'balls': balls}
def _post_events(self, device, **kwargs):
"""Post events on callback from _ball_entered handler.
Events are delayed to this handler because we want the ball device to have accounted for the balls.
"""
del kwargs
for event in self._events[device]:
self.machine.events.post(**event)
self._events[device] = []
def _request_new_balls(self, balls):
"""Request new ball to playfield."""
balls_added = 0
for device in self._source_devices:
balls_to_add = max(min(device.available_balls, balls - balls_added), 0)
device.eject(balls=balls_to_add, target=self.source_playfield)
balls_added += balls_to_add
self.source_playfield.add_ball(balls=max(balls - balls_added, 0))