class SequenceShot(Shot): def __init__(self, machine, name, config): """SequenceShot is where you need certain switches to be hit in the right order, possibly within a time limit. Subclass of `Shot` Parameters ---------- machine : object The MachineController object name : str The name of this shot config : dict The dictionary that holds the configuration for this shot. """ super(SequenceShot, self).__init__(machine, name, config) self.delay = DelayManager() self.progress_index = 0 """Tracks how far along through this sequence the current shot is.""" self.configure() self.enable() self.active_delay = False def configure(self): """Configures the shot.""" # convert our switches config to a list if 'Switches' in self.config: self.config['Switches'] = \ self.machine.string_to_list(self.config['Switches']) # convert our timout to ms if 'Time' in self.config: self.config['Time'] = Timing.string_to_ms(self.config['Time']) else: self.config['Time'] = 0 def enable(self): """Enables the shot. If it's not enabled, the switch handlers aren't active and the shot event will not be posted.""" self.log.debug("Enabling") # create the switch handlers for switch in self.config['Switches']: self.machine.switch_controller.add_switch_handler( switch, self._switch_handler, return_info=True) self.progress_index = 0 self.active = True def disable(self): """Disables the shot. If it's disabled, the switch handlers aren't active and the shot event will not be posted.""" self.log.debug("Disabling") self.active = False for switch in self.config['Switches']: self.machine.switch_controller.remove_switch_handler( switch, self.switch_handler) self.progress_index = 0 def _switch_handler(self, switch_name, state, ms): # does this current switch meet the next switch in the progress index? if switch_name == self.config['Switches'][self.progress_index]: # are we at the end? if self.progress_index == len(self.config['Switches']) - 1: self.confirm_shot() else: # does this shot specific a time limit? if self.config['Time']: # do we need to set a delay? if not self.active_delay: self.delay.reset(name='shot_timer', ms=self.config['Time'], callback=self.reset) self.active_delay = True # advance the progress index self.progress_index += 1 def confirm_shot(self): """Called when the shot is complete to confirm and reset it.""" # kill the delay self.delay.remove('shot_timer') # reset our shot self.reset() # post the success event if self.machine.auditor.enabled: self.machine.auditor.audit('Shots', self.name) self.machine.events.post('shot_' + self.name) def reset(self): """Resets the progress without disabling the shot.""" self.log.debug("Resetting this shot") self.progress_index = 0 self.active_delay = False
class Playfield(BallDevice): config_section = 'playfields' collection = 'playfields' class_label = 'playfield' # noinspection PyMissingConstructor def __init__(self, machine, name, config, collection=None, validate=True): self.log = logging.getLogger('playfield') self.machine = machine self.name = name.lower() self.tags = list() self.label = None self.debug = False self.config = dict() if validate: self.config = self.machine.config_processor.process_config2( self.config_section, config, self.name) else: self.config = config if self.config['debug']: self.debug = True self.log.debug("Enabling debug logging for this device") self.log.debug("Configuring device with settings: '%s'", config) self.tags = self.config['tags'] self.label = self.config['label'] self.delay = DelayManager() self.machine.ball_devices[name] = self if 'default' in self.config['tags']: self.machine.playfield = self # Attributes self._balls = 0 self.num_balls_requested = 0 self.queued_balls = list() self._playfield = True # Set up event handlers # Watch for balls added to the playfield for device in self.machine.ball_devices: for target in device.config['eject_targets']: if target == self.name: self.machine.events.add_handler( event='balldevice_' + device.name + '_ball_eject_success', handler=self._source_device_eject_success) self.machine.events.add_handler( event='balldevice_' + device.name + '_ball_eject_failed', handler=self._source_device_eject_failed) self.machine.events.add_handler( event='balldevice_' + device.name + '_ball_eject_attempt', handler=self._source_device_eject_attempt) break # Watch for balls removed from the playfield self.machine.events.add_handler('balldevice_captured_from_' + self.name, self._ball_removed_handler) # Watch for any switch hit which indicates a ball on the playfield self.machine.events.add_handler('sw_' + self.name + '_active', self.playfield_switch_hit) self.machine.events.add_handler('init_phase_2', self._initialize) def _initialize(self): self.ball_controller = self.machine.ball_controller for device in self.machine.playfield_transfers: if device.config['eject_target'] == self.name: self.machine.events.add_handler( event='balldevice_' + device.name + '_ball_eject_success', handler=self._source_device_eject_success) self.machine.events.add_handler( event='balldevice_' + device.name + '_ball_eject_attempt', handler=self._source_device_eject_attempt) @property def balls(self): return self._balls @balls.setter def balls(self, balls): prior_balls = self._balls ball_change = balls - prior_balls if ball_change: self.log.debug("Ball count change. Prior: %s, Current: %s, Change:" " %s", prior_balls, balls, ball_change) if balls > 0: self._balls = balls #self.ball_search_schedule() elif balls == 0: self._balls = 0 #self.ball_search_disable() else: self.log.warning("Playfield balls went to %s. Resetting to 0, but " "FYI that something's weird", balls) self._balls = 0 #self.ball_search_disable() self.log.debug("New Ball Count: %s. (Prior count: %s)", self._balls, prior_balls) if ball_change > 0: self.machine.events.post_relay('balldevice_' + self.name + '_ball_enter', balls=ball_change) if ball_change: self.machine.events.post(self.name + '_ball_count_change', balls=balls, change=ball_change) def count_balls(self, **kwargs): """Used to count the number of balls that are contained in a ball device. Since this is the playfield device, this method always returns zero. Returns: 0 """ return 0 def get_additional_ball_capacity(self): """Used to find out how many more balls this device can hold. Since this is the playfield device, this method always returns 999. Returns: 999 """ return 999 def add_ball(self, balls=1, source_name=None, source_device=None, trigger_event=None, player_controlled=False): """Adds live ball(s) to the playfield. Args: balls: Integer of the number of balls you'd like to add. source_name: Optional string name of the ball device you'd like to add the ball(s) from. source_device: Optional ball device object you'd like to add the ball(s) from. trigger_event: The optional name of an event that MPF will wait for before adding the ball into play. Typically used with player- controlled eject tag events. If None, the ball will be added immediately. player_controlled: Boolean which specifies whether this event is player controlled. (See not below for details) Returns: True if it's able to process the add_ball() request, False if it cannot. Both source_name and source_device args are included to give you two options for specifying the source of the ball(s) to be added. You don't need to supply both. (it's an "either/or" thing.) Both of these args are optional, so if you don't supply them then MPF will look for a device tagged with 'ball_add_live'. If you don't provide a source and you don't have a device with the 'ball_add_live' tag, MPF will quit. This method does *not* increase the game controller's count of the number of balls in play. So if you want to add balls (like in a multiball scenario), you need to call this method along with ``self.machine.game.add_balls_in_play()``.) MPF tracks the number of balls in play separately from the actual balls on the playfield because there are numerous situations where the two counts are not the same. For example, if a ball is in a VUK while some animation is playing, there are no balls on the playfield but still one ball in play, or if the player has a two-ball multiball and they shoot them both into locks, there are still two balls in play even though there are no balls on the playfield. The opposite can also be true, like when the player tilts then there are still balls on the playfield but no balls in play. Explanation of the player_controlled parameter: Set player_controlled to True to indicate that MPF should wait for the player to eject the ball from the source_device rather than firing a coil. The logic works like this: If the source_device does not have an eject_coil defined, then it's assumed that player_controlled is the only option. (e.g. this is a traditional plunger.) If the source_device does have an eject_coil defined, then there are two ways the eject could work. (1) there could be a "launch" button of some kind that's used to fire the eject coil, or (2) the device could be the auto/manual combo style where there's a mechanical plunger but also a coil which can eject the ball. If player_controlled is true and the device has an eject_coil, MPF will look for the player_controlled_eject_tag and eject the ball when a switch with that tag is activated. If there is no player_controlled_eject_tag, MPF assumes it's a manual plunger and will wait for the ball to disappear from the device based on the device's ball count decreasing. """ if balls < 1: self.log.error("Received request to add %s balls, which doesn't " "make sense. Not adding any balls...") return False # Figure out which device we'll get a ball from if source_device: pass elif source_name and source_name in self.machine.ball_devices: source_device = self.machine.ball_devices[source_name] else: for device in self.machine.ball_devices.items_tagged('ball_add_live'): if self in device.config['eject_targets']: source_device = device break if not source_device: self.log.critical("Received request to add a ball to the playfield" ", but no source device was passed and no ball " "devices are tagged with 'ball_add_live'. Cannot" " add a ball.") return False self.log.debug("Received request to add %s ball(s). Source device: %s." " Wait for event: %s. Player-controlled: %s", balls, source_device.name, trigger_event, player_controlled) if player_controlled: source_device.setup_player_controlled_eject(balls=balls, target=self, trigger_event=trigger_event) else: source_device.eject(balls=balls, target=self, get_ball=True) return True def playfield_switch_hit(self): """A switch tagged with '<this playfield name>_active' was just hit, indicating that there is at least one ball on the playfield. """ if not self.balls: if not self.num_balls_requested: if self.machine.config['machine']['glass_off_mode']: self.log.debug("Playfield_active switch hit with no balls " "expected. glass_off_mode is enabled, so " "this will be ignored.") else: self.log.debug("Playfield_active switch hit with no balls " "expected. glass_off_mode is not enabled, " "setting playfield ball count to 1") self.balls = 1 self.machine.events.post('unexpected_ball_on_' + self.name) def _ball_added_handler(self, balls): self.log.debug("%s ball(s) added to the playfield", balls) self.balls += balls def _ball_removed_handler(self, balls): self.log.debug("%s ball(s) removed from the playfield", balls) self.balls -= balls def _source_device_eject_attempt(self, balls, target, **kwargs): # A source device is attempting to eject a ball. We need to know if it's # headed to the playfield. if target == self: self.log.debug("A source device is attempting to eject %s ball(s)" " to the playfield.", balls) self.num_balls_requested += balls def _source_device_eject_failed(self, balls, target, **kwargs): # A source device failed to eject a ball. We need to know if it was # headed to the playfield. if target == self: self.log.debug("A source device has failed to eject %s ball(s)" " to the playfield.", balls) self.num_balls_requested -= balls def _source_device_eject_success(self, balls, target): # A source device has just confirmed that it has successfully ejected a # ball. Note that we don't care what type of confirmation it used. # (Playfield switch hit, count of its ball switches, etc.) if target == self: self.log.debug("A source device has confirmed it's ejected %s " "ball(s) to the playfield.", balls) self.balls += balls self.num_balls_requested -= balls if self.num_balls_requested < 0: self.log.critical("num_balls_requested is %s, which doesn't " "make sense. Quitting...", self.num_balls_requested) raise Exception("num_balls_requested is %s, which doesn't make " "sense. Quitting...", self.num_balls_requested) def ok_to_confirm_ball_via_playfield_switch(self): """Used to check whether it's ok for a ball device which ejects to the playfield to confirm its eject via a playfield switch being hit. Returns: True or False Right now this is simple. If there are no playfield balls, then any playfield switch hit is assumed to be from the newly-ejected ball. If there are other balls on the playfield, then we can't use this confirmation method since we don't know whether a playfield switch hit is from the newly-ejected ball(s) or a current previously-live playfield ball. """ if not self.balls: return True else: return False # todo look for other incoming balls? # BALL SEARCH -------------------------------------------------------------- # todo make ball search work with plunger lanes with no switches. i.e. we # don't want ball search to start until a switch is hit? def ball_search_schedule(self, secs=None, force=False): """Schedules a ball search to start. By default it will schedule it based on the time configured in the machine configuration files. If a ball search is already scheduled, this method will reset that schedule to the new time passed. Args: secs: Schedules the ball search that many secs from now. force : Boolean to force a ball search. Set True to force a ball search. Otherwise it will only schedule it if self.flag_no_ball_search is False. Default is False """ if self.machine.config['ball_search']: if not self.flag_no_ball_search or force is True: if secs is not None: start_ms = secs * 1000 else: start_ms = (self.machine.config['ball_search'] ['secs until ball search start'] * 1000) self.log.debug("Scheduling a ball search for %s secs from now", start_ms / 1000.0) self.delay.reset("ball_search_start", ms=start_ms, callback=self.ball_search_begin) def ball_search_disable(self): """Disables ball search. Note this is used to prevent a future ball search from happening (like when all balls become contained). This method is not used to cancel an existing ball search. (Use `ball_search_end` for that.) """ self.log.debug("Disabling Ball Search") self.delay.remove('ball_search_start') def ball_search_begin(self, force=False): """Begin the ball search process""" if not self.flag_no_ball_search: self.log.debug("Received request to start ball search") # ball search should only start if we have uncontained balls if self.balls or force: # todo add audit self.flag_ball_search_in_progress = True self.machine.events.post("ball_search_begin_phase1") # todo set delay to start phase 2 else: self.log.debug("We got request to start ball search, but we " "have no balls uncontained. WTF??") def ball_search_failed(self): """Ball Search did not find the ball.""" self.log.debug("Ball Search failed to find a ball. Disabling.") self.ball_search_end() self.ball_lost() def ball_search_end(self): """End the ball search, either because we found the ball or are giving up.""" self.log.debug("Ball search ending") self.flag_ball_search_in_progress = False self.machine.events.post("ball_search_end") # todo cancel the delay for phase 2 if we had one def ball_lost(self): """Mark a ball as lost""" self.num_balls_known = self.balls self.num_balls_missing = (self.machine.config['machine'] ['balls installed'] - self.balls) self.num_balls_live = 0 # since desired count doesn't change, this will relaunch them self.log.debug("Ball(s) Marked Lost. Known: %s, Missing: %s", self.num_balls_known, self.num_balls_missing) # todo audit balls lost def ball_found(self, num=1): """Used when a previously missing ball is found. Updates the balls known and balls missing variables. Parameters ---------- num : int Specifies how many balls have been found. Default is 1. """ self.log.debug("HEY!! We just found %s lost ball(s).", num) self.num_balls_known += num self.num_balls_missing -= num self.log.debug("New ball counts. Known: %s, Missing: %s", self.num_balls_known, self.num_balls_missing) self.ball_update_all_counts() # is this necessary? todo def eject(self, *args, **kwargs): pass def eject_all(self, *args, **kwargs): pass
class Playfield(BallDevice): def __init__(self, machine, name, collection): self.log = logging.getLogger('Playfield') self.machine = machine self.name = name self.tags = list() self.config = defaultdict(lambda: None) self.config['eject_targets'] = list() self.ball_controller = self.machine.ball_controller self.delay = DelayManager() # Add the playfield ball device to the existing device collection collection_object = getattr(self.machine, collection)[name] = self # Attributes self._balls = 0 self.num_balls_requested = 0 self.player_controlled_eject_in_progress = None self.queued_balls = list() # Set up event handlers # Watch for balls added to the playfield for device in self.machine.balldevices: for target in device.config['eject_targets']: if target == self.name: self.machine.events.add_handler( event='balldevice_' + device.name + '_ball_eject_success', handler=self._source_device_eject_success) self.machine.events.add_handler( event='balldevice_' + device.name + '_ball_eject_attempt', handler=self._source_device_eject_attempt) break # Watch for balls removed from the playfield self.machine.events.add_handler('balldevice_captured_from_playfield', self._ball_removed_handler) # Watch for any switch hit which indicates a ball on the playfield self.machine.events.add_handler('sw_playfield_active', self.playfield_switch_hit) @property def balls(self): return self._balls @balls.setter def balls(self, balls): prior_balls = self._balls ball_change = balls - prior_balls if ball_change: self.log.debug("Ball count change. Prior: %s, Current: %s, Change: " "%s", prior_balls, balls, ball_change) if balls > 0: self._balls = balls #self.ball_search_schedule() elif balls == 0: self._balls = 0 #self.ball_search_disable() else: self.log.warning("Playfield balls went to %s. Resetting to 0, but " "FYI that something's weird", balls) self._balls = 0 #self.ball_search_disable() self.log.debug("New Ball Count: %s. (Prior count: %s)", self._balls, prior_balls) if ball_change > 0: self.machine.events.post_relay('balldevice_' + self.name + '_ball_enter', balls=ball_change) if ball_change: self.machine.events.post('playfield_ball_count_change', balls=balls, change=ball_change) def count_balls(self, **kwargs): """Used to count the number of balls that are contained in a ball device. Since this is the playfield device, this method always returns zero. Returns: 0 """ return 0 def get_additional_ball_capacity(self): """Used to find out how many more balls this device can hold. Since this is the playfield device, this method always returns 999. Returns: 999 """ return 999 def add_ball(self, balls=1, source_name=None, source_device=None, trigger_event=None): """Adds live ball(s) to the playfield. Args: balls: Integer of the number of balls you'd like to add. source_name: Optional string name of the ball device you'd like to add the ball(s) from. source_device: Optional ball device object you'd like to add the ball(s) from. trigger_event: The optional name of an event that MPF will wait for before adding the ball into play. Typically used with player- controlled eject tag events. If None, the ball will be added immediately. Returns: True if it's able to process the add_ball() request, False if it cannot. Both source_name and source_device args are included to give you two options for specifying the source of the ball(s) to be added. You don't need to supply both. (it's an "either/or" thing.) Both of these args are optional, so if you don't supply them then MPF will look for a device tagged with 'ball_add_live'. If you don't provide a source and you don't have a device with the 'ball_add_live' tag, MPF will quit. This method does *not* increase the game controller's count of the number of balls in play. So if you want to add balls (like in a ball scenario(, you need to call this method along with ``self.machine.game.add_balls_in_play()``.) MPF tracks the number of balls in play separately from the actual balls on the playfield because there are numerous situations where the two counts are not the same. For example, if a ball is in a VUK while some animation is playing, there are no balls on the playfield but still one ball in play, or if the player has a two-ball multiball and they shoot them both into locks, there are still two balls in play even though there are no balls on the playfield, or if the player tilts then there are still balls on the playfield but no balls in play. """ if balls < 1: self.log.error("Received request to add %s balls, which doesn't " "make sense. Not adding any balls...") return False # Figure out which device we'll get a ball from if source_device: pass elif source_name and source_name in self.machine.balldevices: source_device = self.machine.balldevices[source_name] else: for device in self.machine.balldevices.items_tagged('ball_add_live'): source_device = device break if not source_device: self.log.critical("Received request to add a ball to the playfield, " "but no source device was passed and no ball " "devices are tagged with 'ball_add_live'. Cannot " "add a ball.") raise Exception("Received request to add a ball to the playfield, " "but no source device was passed and no ball " "devices are tagged with 'ball_add_live'. Cannot " "add a ball.") # If there's a player controlled eject in progress for this device, we # hold this request until it's over. if self.player_controlled_eject_in_progress == source_device: self.queued_balls.append((balls, source_name, source_device, trigger_event)) self.log.debug("An add_ball() request came in while there was a " "current player-controlled eject in progress for the" "same device. Will queue the eject request") return True self.log.debug("Received request to add %s ball(s). Source device: %s. " "Wait for event: %s", balls, source_device.name, trigger_event) # If we don't have a coil that's fired by the player, and we our source # device has the ability to eject, then we do the eject now. # Some examples: # Plunger lane w/ switch and coil: ball_add_live device is plunger lane, # we don't eject now since *not* player_controlled is true. # Plunger lane w/ switch. No coil: ball_add_live device is plunger lane, # we don't eject now since there's no eject_coil for that device. # Plunger lane has no switch: ball_add_live device is trough, we do # eject now since there's no player_controlled tag and the device has an # eject coil. if trigger_event and source_device.config['eject_coil']: self.setup_player_controlled_eject(balls, source_device, trigger_event) else: # if there's no trigger, eject right away # if there's no eject coil, that's ok. We still need to setup the # eject so the device will be expecting the ball to disappear source_device.eject(balls=balls, target=self, get_ball=True) return True def setup_player_controlled_eject(self, balls, device, trigger_event): """Used to set up an eject from a ball device which will eject a ball to the playfield. Args: balls: Integer of the number of balls this device should eject. device: The ball device object that will eject the ball(s) when a switch with the player-controlled eject tag is hit. trigger_event: The name of the MPF event that will trigger the eject. When this method it called, MPF will set up an event handler to look for the trigger_event. """ self.log.debug("Setting up a player controlled eject. Balls: %s, Device" ": %s, Trigger Event: %s", balls, device, trigger_event) if not device.balls: device.request_ball(balls=balls) self.machine.events.add_handler(trigger_event, self.player_eject_request, balls=balls, device=device) self.player_controlled_eject_in_progress = device def remove_player_controlled_eject(self): """Removed the player-controlled eject so a player hitting a switch no longer calls the device(s) to eject a ball. """ self.log.debug("Removing player-controlled eject.") self.machine.events.remove_handler(self.player_eject_request) self.player_controlled_eject_in_progress = None # Need to do this in case one of these queued balls is also a player # controlled eject which would re-add it to the queue while iterating. # So we clear it and pass them all to add_ball() and then let the queue # rebuild with what's left if it needs to. ball_list = self.queued_balls self.queued_balls = list() for item in ball_list: self.add_ball(balls=item[0], source_name=item[1], source_device=item[2], trigger_event=item[3]) def player_eject_request(self, balls, device): """A player has hit a switch tagged with the player_eject_request_tag. Args: balls: Integer of the number of balls that will be ejected. device: The ball device object that will eject the ball(s). """ self.log.debug("Received player eject request. Balls: %s, Device: %s", balls, device.name) device.eject(balls, target=self) def playfield_switch_hit(self): """A switch tagged with 'playfield_active' was just hit, indicating that there is at least one ball on the playfield. """ if not self.balls: if not self.num_balls_requested: self.log.debug("PF switch hit with no balls expected. Setting " "pf balls to 1.") self.balls = 1 self.machine.events.post('unexpected_ball_on_playfield') def _ball_added_handler(self, balls): self.log.debug("%s ball(s) added to the playfield", balls) self.balls += balls def _ball_removed_handler(self, balls): self.log.debug("%s ball(s) removed from the playfield", balls) self.balls -= balls def _source_device_eject_attempt(self, balls, target, **kwargs): # A source device is attempting to eject a ball. We need to know if it's # headed to the playfield. if target == self: self.log.debug("A source device is attempting to ejected %s ball(s)" " to the playfield.", balls) self.num_balls_requested += balls def _source_device_eject_success(self, balls, target): # A source device has just confirmed that it has successfully ejected a # ball. Note that we don't care what type of confirmation it used. # (Playfield switch hit, count of its ball switches, etc.) if target == self: self.log.debug("A source device has confirmed it's ejected %s " "ball(s) to the playfield.", balls) self.balls += balls self.num_balls_requested -= balls if self.num_balls_requested < 0: self.log.critical("num_balls_requested is %s, which doesn't " "make sense. Quitting...", self.num_balls_requested) raise Exception("num_balls_requested is %s, which doesn't make " "sense. Quitting...", self.num_balls_requested) self.remove_player_controlled_eject() def ok_to_confirm_ball_via_playfield_switch(self): """Used to check whether it's ok for a ball device which ejects to the playfield to confirm its eject via a playfield switch being hit. Returns: True or False Right now this is simple. If there are no playfield balls, then any playfield switch hit is assumed to be from the newly-ejected ball. If there are other balls on the playfield, then we can't use this confirmation method since we don't know whether a playfield switch hit is from the newly-ejected ball(s) or a current previously-live playfield ball. """ if not self.balls: return True else: return False # todo look for other incoming balls? # BALL SEARCH -------------------------------------------------------------- # todo make ball search work with plunger lanes with no switches. i.e. we # don't want ball search to start until a switch is hit? def ball_search_schedule(self, secs=None, force=False): """Schedules a ball search to start. By default it will schedule it based on the time configured in the machine configuration files. If a ball search is already scheduled, this method will reset that schedule to the new time passed. Args: secs: Schedules the ball search that many secs from now. force : Boolean to force a ball search. Set True to force a ball search. Otherwise it will only schedule it if self.flag_no_ball_search is False. Default is False """ if self.machine.config['ballsearch']: if not self.flag_no_ball_search or force is True: if secs is not None: start_ms = secs * 1000 else: start_ms = (self.machine.config['ballsearch'] ['secs until ball search start'] * 1000) self.log.debug("Scheduling a ball search for %s secs from now", start_ms / 1000.0) self.delay.reset("ball_search_start", ms=start_ms, callback=self.ball_search_begin) def ball_search_disable(self): """Disables ball search. Note this is used to prevent a future ball search from happening (like when all balls become contained). This method is not used to cancel an existing ball search. (Use `ball_search_end` for that.) """ self.log.debug("Disabling Ball Search") self.delay.remove('ball_search_start') def ball_search_begin(self, force=False): """Begin the ball search process""" if not self.flag_no_ball_search: self.log.debug("Received request to start ball search") # ball search should only start if we have uncontained balls if self.balls or force: # todo add audit self.flag_ball_search_in_progress = True self.machine.events.post("ball_search_begin_phase1") # todo set delay to start phase 2 else: self.log.debug("We got request to start ball search, but we " "have no balls uncontained. WTF??") def ball_search_failed(self): """Ball Search did not find the ball.""" self.log.debug("Ball Search failed to find a ball. Disabling.") self.ball_search_end() self.ball_lost() def ball_search_end(self): """End the ball search, either because we found the ball or are giving up.""" self.log.debug("Ball search ending") self.flag_ball_search_in_progress = False self.machine.events.post("ball_search_end") # todo cancel the delay for phase 2 if we had one def ball_lost(self): """Mark a ball as lost""" self.num_balls_known = self.balls self.num_balls_missing = self.machine.config['machine']\ ['balls installed'] - self.balls self.num_balls_live = 0 # since desired count doesn't change, this will relaunch them self.log.debug("Ball(s) Marked Lost. Known: %s, Missing: %s", self.num_balls_known, self.num_balls_missing) # todo audit balls lost def ball_found(self, num=1): """Used when a previously missing ball is found. Updates the balls known and balls missing variables. Parameters ---------- num : int Specifies how many balls have been found. Default is 1. """ self.log.debug("HEY!! We just found %s lost ball(s).", num) self.num_balls_known += num self.num_balls_missing -= num self.log.debug("New ball counts. Known: %s, Missing: %s", self.num_balls_known, self.num_balls_missing) self.ball_update_all_counts() # is this necessary? todo def eject(self, *args, **kwargs): pass def eject_all(self, *args, **kwargs): pass
class Playfield(BallDevice): config_section = 'playfields' collection = 'playfields' class_label = 'playfield' # noinspection PyMissingConstructor def __init__(self, machine, name, config, collection=None, validate=True): self.log = logging.getLogger('playfield') self.machine = machine self.name = name.lower() self.tags = list() self.label = None self.debug = False self.config = dict() self._count_consistent = True self.unexpected_balls = 0 if validate: self.config = self.machine.config_processor.process_config2( self.config_section, config, self.name) else: self.config = config if self.config['debug']: self.debug = True self.log.debug("Enabling debug logging for this device") self.log.debug("Configuring device with settings: '%s'", config) self.tags = self.config['tags'] self.label = self.config['label'] self.delay = DelayManager() self.machine.ball_devices[name] = self if 'default' in self.config['tags']: self.machine.playfield = self # Attributes self._balls = 0 self.available_balls = 0 self.num_balls_requested = 0 self.queued_balls = list() self._playfield = True # Set up event handlers # Watch for balls added to the playfield for device in self.machine.ball_devices: for target in device.config['eject_targets']: if target == self.name: self.machine.events.add_handler( event='balldevice_' + device.name + '_ball_eject_success', handler=self._source_device_eject_success) self.machine.events.add_handler( event='balldevice_' + device.name + '_ball_eject_failed', handler=self._source_device_eject_failed) self.machine.events.add_handler( event='balldevice_' + device.name + '_ball_eject_attempt', handler=self._source_device_eject_attempt) break # Watch for balls removed from the playfield self.machine.events.add_handler( 'balldevice_captured_from_' + self.name, self._ball_removed_handler) # Watch for any switch hit which indicates a ball on the playfield self.machine.events.add_handler('sw_' + self.name + '_active', self.playfield_switch_hit) self.machine.events.add_handler('init_phase_2', self._initialize) def _initialize(self): self.ball_controller = self.machine.ball_controller for device in self.machine.playfield_transfers: if device.config['eject_target'] == self.name: self.machine.events.add_handler( event='balldevice_' + device.name + '_ball_eject_success', handler=self._source_device_eject_success) self.machine.events.add_handler( event='balldevice_' + device.name + '_ball_eject_attempt', handler=self._source_device_eject_attempt) def add_missing_balls(self, balls): # if we catched an unexpected balls before do not add a ball if self.unexpected_balls: self.unexpected_balls -= 1 balls -= 1 self.balls += balls @property def balls(self): return self._balls @balls.setter def balls(self, balls): prior_balls = self._balls ball_change = balls - prior_balls if ball_change: self.log.debug( "Ball count change. Prior: %s, Current: %s, Change:" " %s", prior_balls, balls, ball_change) if balls >= 0: self._balls = balls else: self.log.warning( "Playfield balls went to %s. Resetting to 0, but " "FYI that something's weird", balls) self._balls = 0 self.unexpected_balls = 0 self.log.debug("New Ball Count: %s. (Prior count: %s)", self._balls, prior_balls) if ball_change > 0: self.machine.events.post_relay('balldevice_' + self.name + '_ball_enter', new_balls=ball_change, unclaimed_balls=ball_change) if ball_change: self.machine.events.post(self.name + '_ball_count_change', balls=balls, change=ball_change) def count_balls(self, **kwargs): """Used to count the number of balls that are contained in a ball device. Since this is the playfield device, this method always returns zero. Returns: 0 """ return 0 def get_additional_ball_capacity(self): """Used to find out how many more balls this device can hold. Since this is the playfield device, this method always returns 999. Returns: 999 """ return 999 def add_ball(self, balls=1, source_name=None, source_device=None, player_controlled=False, reset=False): """Adds live ball(s) to the playfield. Args: balls: Integer of the number of balls you'd like to add. source_name: Optional string name of the ball device you'd like to add the ball(s) from. source_device: Optional ball device object you'd like to add the ball(s) from. player_controlled: Boolean which specifies whether this event is player controlled. (See not below for details) reset: Boolean which controls whether the source device should reset its state to idle Returns: True if it's able to process the add_ball() request, False if it cannot. Both source_name and source_device args are included to give you two options for specifying the source of the ball(s) to be added. You don't need to supply both. (it's an "either/or" thing.) Both of these args are optional, so if you don't supply them then MPF will look for a device tagged with 'ball_add_live'. If you don't provide a source and you don't have a device with the 'ball_add_live' tag, MPF will quit. This method does *not* increase the game controller's count of the number of balls in play. So if you want to add balls (like in a multiball scenario), you need to call this method along with ``self.machine.game.add_balls_in_play()``.) MPF tracks the number of balls in play separately from the actual balls on the playfield because there are numerous situations where the two counts are not the same. For example, if a ball is in a VUK while some animation is playing, there are no balls on the playfield but still one ball in play, or if the player has a two-ball multiball and they shoot them both into locks, there are still two balls in play even though there are no balls on the playfield. The opposite can also be true, like when the player tilts then there are still balls on the playfield but no balls in play. Explanation of the player_controlled parameter: Set player_controlled to True to indicate that MPF should wait for the player to eject the ball from the source_device rather than firing a coil. The logic works like this: If the source_device does not have an eject_coil defined, then it's assumed that player_controlled is the only option. (e.g. this is a traditional plunger.) If the source_device does have an eject_coil defined, then there are two ways the eject could work. (1) there could be a "launch" button of some kind that's used to fire the eject coil, or (2) the device could be the auto/manual combo style where there's a mechanical plunger but also a coil which can eject the ball. If player_controlled is true and the device has an eject_coil, MPF will look for the player_controlled_eject_tag and eject the ball when a switch with that tag is activated. If there is no player_controlled_eject_tag, MPF assumes it's a manual plunger and will wait for the ball to disappear from the device based on the device's ball count decreasing. """ if balls < 1: self.log.error("Received request to add %s balls, which doesn't " "make sense. Not adding any balls...") return False # Figure out which device we'll get a ball from if source_device: pass elif source_name and source_name in self.machine.ball_devices: source_device = self.machine.ball_devices[source_name] else: for device in self.machine.ball_devices.items_tagged( 'ball_add_live'): if self in device.config['eject_targets']: source_device = device break if not source_device: self.log.critical("Received request to add a ball to the playfield" ", but no source device was passed and no ball " "devices are tagged with 'ball_add_live'. Cannot" " add a ball.") return False if reset: source_device.stop() self.log.debug( "Received request to add %s ball(s). Source device: %s." " Player-controlled: %s", balls, source_device.name, player_controlled) if player_controlled: source_device.setup_player_controlled_eject(balls=balls, target=self) else: source_device.eject(balls=balls, target=self, get_ball=True) return True def mark_playfield_active(self): self.machine.events.post_boolean(self.name + "_active") def playfield_switch_hit(self, **kwargs): """A switch tagged with '<this playfield name>_active' was just hit, indicating that there is at least one ball on the playfield. """ if (not self.balls or (kwargs.get('balls') and self.balls - kwargs['balls'] < 0)): self.mark_playfield_active() if not self.num_balls_requested: if self.machine.game: self.unexpected_balls = 1 if self.machine.config['machine']['glass_off_mode']: self.log.debug("Playfield_active switch hit with no balls " "expected. glass_off_mode is enabled, so " "this will be ignored.") else: self.log.debug("Playfield_active switch hit with no balls " "expected. glass_off_mode is not enabled, " "setting playfield ball count to 1") self.balls = 1 self.machine.events.post('unexpected_ball_on_' + self.name) # def _ball_added_handler(self, balls): # self.log.debug("%s ball(s) added to the playfield", balls) # self.balls += balls def _ball_removed_handler(self, balls, **kwargs): self._count_consistent = False # somebody got a ball from us so we obviously had one self.machine.events.post('sw_' + self.name + "_active", callback=self._ball_removed_handler2, balls=balls) def _ball_removed_handler2(self, balls): self.log.debug("%s ball(s) removed from the playfield", balls) self.balls -= balls self._count_consistent = True def _source_device_eject_attempt(self, balls, target, **kwargs): # A source device is attempting to eject a ball. We need to know if it's # headed to the playfield. if target == self: self.log.debug( "A source device is attempting to eject %s ball(s)" " to the playfield.", balls) self.num_balls_requested += balls def _source_device_eject_failed(self, balls, target, **kwargs): # A source device failed to eject a ball. We need to know if it was # headed to the playfield. if target == self: self.log.debug( "A source device has failed to eject %s ball(s)" " to the playfield.", balls) self.num_balls_requested -= balls def _source_device_eject_success(self, balls, target): # A source device has just confirmed that it has successfully ejected a # ball. Note that we don't care what type of confirmation it used. # (Playfield switch hit, count of its ball switches, etc.) if target == self: self.log.debug( "A source device has confirmed it's ejected %s " "ball(s) to the playfield.", balls) self.balls += balls self.num_balls_requested -= balls if self.num_balls_requested < 0: self.log.critical( "num_balls_requested is %s, which doesn't " "make sense. Quitting...", self.num_balls_requested) raise Exception( "num_balls_requested is %s, which doesn't make " "sense. Quitting...", self.num_balls_requested) def ok_to_confirm_ball_via_playfield_switch(self): """Used to check whether it's ok for a ball device which ejects to the playfield to confirm its eject via a playfield switch being hit. Returns: True or False Right now this is simple. If there are no playfield balls, then any playfield switch hit is assumed to be from the newly-ejected ball. If there are other balls on the playfield, then we can't use this confirmation method since we don't know whether a playfield switch hit is from the newly-ejected ball(s) or a current previously-live playfield ball. """ if not self.balls: return True else: return False # todo look for other incoming balls? # BALL SEARCH -------------------------------------------------------------- # todo make ball search work with plunger lanes with no switches. i.e. we # don't want ball search to start until a switch is hit? def ball_search_schedule(self, secs=None, force=False): """Schedules a ball search to start. By default it will schedule it based on the time configured in the machine configuration files. If a ball search is already scheduled, this method will reset that schedule to the new time passed. Args: secs: Schedules the ball search that many secs from now. force : Boolean to force a ball search. Set True to force a ball search. Otherwise it will only schedule it if self.flag_no_ball_search is False. Default is False """ if self.machine.config['ball_search']: if not self.flag_no_ball_search or force is True: if secs is not None: start_ms = secs * 1000 else: start_ms = (self.machine.config['ball_search'] ['secs until ball search start'] * 1000) self.log.debug("Scheduling a ball search for %s secs from now", start_ms / 1000.0) self.delay.reset("ball_search_start", ms=start_ms, callback=self.ball_search_begin) def ball_search_disable(self): """Disables ball search. Note this is used to prevent a future ball search from happening (like when all balls become contained). This method is not used to cancel an existing ball search. (Use `ball_search_end` for that.) """ self.log.debug("Disabling Ball Search") self.delay.remove('ball_search_start') def ball_search_begin(self, force=False): """Begin the ball search process""" if not self.flag_no_ball_search: self.log.debug("Received request to start ball search") # ball search should only start if we have uncontained balls if self.balls or force: # todo add audit self.flag_ball_search_in_progress = True self.machine.events.post("ball_search_begin_phase1") # todo set delay to start phase 2 else: self.log.debug("We got request to start ball search, but we " "have no balls uncontained. WTF??") def ball_search_failed(self): """Ball Search did not find the ball.""" self.log.debug("Ball Search failed to find a ball. Disabling.") self.ball_search_end() self.ball_lost() def ball_search_end(self): """End the ball search, either because we found the ball or are giving up.""" self.log.debug("Ball search ending") self.flag_ball_search_in_progress = False self.machine.events.post("ball_search_end") # todo cancel the delay for phase 2 if we had one def ball_lost(self): """Mark a ball as lost""" self.num_balls_known = self.balls self.num_balls_missing = ( self.machine.config['machine']['balls installed'] - self.balls) self.num_balls_live = 0 # since desired count doesn't change, this will relaunch them self.log.debug("Ball(s) Marked Lost. Known: %s, Missing: %s", self.num_balls_known, self.num_balls_missing) # todo audit balls lost def ball_found(self, num=1): """Used when a previously missing ball is found. Updates the balls known and balls missing variables. Parameters ---------- num : int Specifies how many balls have been found. Default is 1. """ self.log.debug("HEY!! We just found %s lost ball(s).", num) self.num_balls_known += num self.num_balls_missing -= num self.log.debug("New ball counts. Known: %s, Missing: %s", self.num_balls_known, self.num_balls_missing) self.ball_update_all_counts() # is this necessary? todo def eject(self, *args, **kwargs): pass def eject_all(self, *args, **kwargs): pass def is_playfield(self): return True def add_incoming_ball(self, source): pass def remove_incoming_ball(self, source): pass
class BallController(object): """Base class for the Ball Controller which is used to keep track of all the balls in a pinball machine. Parameters ---------- machine : :class:`MachineController` A reference to the instance of the MachineController object. """ def __init__(self, machine): self.machine = machine self.log = logging.getLogger("BallController") self.log.debug("Loading the BallController") self.delay = DelayManager() self.game = None # Properties: # self.num_balls_contained # self.num_balls_live # self.num_balls_desired_live self._num_balls_live = 0 # do not update this. Use the property self._num_balls_desired_live = 0 # do not update. Use the property self._num_balls_known = -999 # do not update. Use the property self.num_balls_in_transit = 0 # Balls currently in transit from one ball device to another # Not currently implemented self.num_balls_missing = 0 # Balls lost and/or not installed. self.flag_ball_search_in_progress = False #True if there's currently a ball search in progress. self.flag_no_ball_search = False #Ball search is enabled and disabled automatically based on whether #any balls are uncontained. Set this flag_no_ball_search to True if for #some reason you don't want the ball search to be enabled. BTW I can't #think of why you'd ever want this. The automatic stuff works great, and #you need to keep it enabled even after tilts and stuff. Maybe for some #kind of maintainance mode or something? # register for events self.machine.events.add_handler('request_to_start_game', self.request_to_start_game) self.machine.events.add_handler('sw_ballLive', self.ball_live_hit) self.machine.events.add_handler('machine_reset_phase_2', self.reset) self.machine.events.add_handler('timer_tick', self._tick) @property def num_balls_contained(self): balls = 0 for device in self.machine.balldevices: balls += device.num_balls_contained if balls > self._num_balls_known: self.num_balls_known = balls if balls < 0: return -999 else: return balls # todo figure out how to do this with a generator @property def num_balls_live(self): return self._num_balls_live @num_balls_live.setter def num_balls_live(self, balls): """The number of balls that are actually live (i.e. loose and bouncing around) at this moment. This is not necessarily the same as the number of balls in play that the Game object tracks. For example during a game if the player shoots a ball into a ball device, there will be no live balls during that moment even though there is still a ball in play. And if the player tilts, there will be no balls in play but we'll still have balls live until they all roll into the drain. """ prior_count = self._num_balls_live if balls > 0: self._num_balls_live = balls self.ball_search_schedule() else: self._num_balls_live = 0 self.ball_search_disable() self.log.debug("New Live Ball Count: %s. (Prior count: %s)", self._num_balls_live, prior_count) # todo add support for finding missing balls @property def num_balls_desired_live(self): return self._num_balls_desired_live @num_balls_desired_live.setter def num_balls_desired_live(self, balls): """How many balls the ball controller will try to keep live. If this number is ever greater than the number live, the ball controller will automatically add more live. If it's less, then the ball controller will not automatically eject a ball if it enters the drain device. """ prior_count = self._num_balls_desired_live if balls > 0: self._num_balls_desired_live = balls else: self._num_balls_desired_live = 0 self.log.debug("New Desired Live Ball Count: %s. (Prior count: %s)", self._num_balls_desired_live, prior_count) # todo should we ensure that this value never exceed the number of balls # known? Or is that a crutch that will obscure programming errors? # I think we should do it but then log it as a warning. @property def num_balls_known(self): if self.num_balls_contained > self._num_balls_known: self._num_balls_known = self.num_balls_contained return self._num_balls_known @num_balls_known.setter def num_balls_known(self, balls): """How many balls the machine knows about. Could vary from the number of balls installed based on how many are *actually* in the machine, or to compensate for balls that are lost or stuck. """ self._num_balls_known = balls def reset(self): """Resets the BallController. Current this just gets an initial count of the balls and sends all the balls to their 'home' position. """ # If there are no ball devices, then the ball controller has no work to # do and will create errors, so we just abort. if not hasattr(self.machine, 'balldevices'): return self.num_balls_known = self.num_balls_contained # remove any old handlers self.machine.events.remove_handler(self._ball_add_live_handler) # add handlers to watch for balls ejected to the playfield for device in self.machine.balldevices: if device.config['confirm_eject_type'] != 'device': # This device ejects to the playfield self.machine.events.add_handler( 'balldevice_' + device.name + '_ball_eject_attempt', self._ball_add_live_handler) self.machine.events.add_handler( 'balldevice_' + device.name + '_ball_eject_failed', self._ball_remove_live_handler) if 'drain' in device.tags: # device is used to drain balls from pf self.machine.events.add_handler( 'balldevice_' + device.name + '_ball_enter', self._ball_drained_handler) if not device.config['feeder_device']: # This device receives balls from the playfield self.machine.events.add_handler('balldevice_' + device.name + '_ball_enter', self._ball_remove_live_handler, priority=100) if 'Allow start with loose balls' not in self.machine.config['Game']: self.machine.config['Game']['Allow start with loose balls'] = False # todo where do we figure out balls missing? self.num_balls_live = 0 self.num_balls_desired_live = 0 def set_live_count(self, balls=None, from_tag='ball_add_live', device=None): """Tells the ball controller how many balls you want live.""" self.log.debug( "Setting desired live to: %s. (from_tag: %s, device: %s)", balls, from_tag, device) self.log.debug("Previous desired live count: %s", self.num_balls_desired_live) if balls is not None: self.num_balls_desired_live = balls if self.num_balls_desired_live <= self.num_balls_live: # no live balls to add return balls_to_add = self.num_balls_desired_live - self.num_balls_live # set which ball device we're working with if not device: device = self.machine.balldevices.items_tagged('ball_add_live')[0] self.log.debug("Will add ball from device: %s", device.name) # todo what if there isn't one? Need a clean error # can we eject from this device? Grab a ball if not if not device.num_balls_contained: self.log.debug("Asking device %s to stage 1 ball", device.name) device.num_balls_desired = 1 # this will stage a ball self.log.debug("Subtracting 1 ball from %s's desired count", device.name) device.num_balls_desired -= balls_to_add # todo need to check how many balls ejectable this device has, and go # to another device if this one can't serve them all def add_live(self, balls=1, from_tag='ball_add_live', device=None): """Tells the ball controller to add a live ball. This method ensures you're not adding more balls live than you have available. By default it will try to add the ball(s) from devices tagged with 'ball_add_live'. This is a convenience method which calls set_live_count() """ self.log.debug( "Received request to add %s live ball(s). Current " "desired live: %s", balls, self.num_balls_desired_live) if (self.num_balls_desired_live < self.num_balls_known and self.num_balls_desired_live + balls <= self.num_balls_known): self.set_live_count(self.num_balls_desired_live + balls, from_tag, device) return True elif self.num_balls_desired_live + balls > self.num_balls_known: self.log.warning("Live ball request exceeds number of known balls") self.set_live_count(self.num_balls_known, from_tag, device) # should we return something here? I guess None is ok? else: self.log.debug("Cannot set new live ball count.") return False def stage_ball(self, tag='ball_add_live'): """Makes sure that ball devices with the tag passed have a ball.""" for device in self.machine.balldevices.items_tagged(tag): device.num_balls_desired = 1 def _tick(self): # ticks once per game loop. Tries to keep the number of live balls # matching the number of balls in play if self.num_balls_desired_live < 0: self.log.debug("Warning. num_balls_desired_live is negative. " "Resetting to 0.") # todo found a lost ball?? self.num_balls_desired_live = 0 # todo change num_balls_desired_live to a property? if self.num_balls_live != self.num_balls_desired_live: self.log.debug("(tick) Current Balls Live: %s, Balls Desired: %s", self.num_balls_live, self.num_balls_desired_live) self.set_live_count() def request_to_start_game(self): """Method registered for the *request_to_start_game* event. Checks to make sure that the balls are in all the right places and returns. If too many balls are missing (based on the config files 'Min Balls' setting), it will return False to reject the game start request. """ self.log.debug("Received request to start game.") self.log.debug("Balls contained: %s, Min balls needed: %s", self.num_balls_contained, self.machine.config['Machine']['Min Balls']) if self.num_balls_contained < self.machine.config['Machine'][ 'Min Balls']: self.log.debug( "BallController denies game start. Not enough balls") return False if self.machine.config['Game']['Allow start with loose balls']: return elif not self.are_balls_gathered(['home', 'trough']): self.gather_balls('home') self.log.debug("BallController denies game start. Balls are not in" " their home positions.") return False def are_balls_gathered(self, target=['home', 'trough']): """Checks to see if all the balls are contained in devices tagged with the parameter that was passed. Note if you pass a target that's not used in any ball devices, this method will return True. (Because you're asking if all balls are nowhere, and they always are. :) Args: target: String value of the tag you'd like to check. Default is 'home' """ self.log.debug( "Checking to see if all the balls are in devices tagged" " with '%s'", target) if type(target) is str: target = [target] count = 0 devices = set() for tag in target: for device in self.machine.balldevices.items_tagged(tag): devices.add(device) if len(devices) == 0: # didn't find any devices matching that tag, so we return True return True for device in devices: count += device.get_status('num_balls_contained') if count == self.machine.ball_controller.num_balls_known: self.log.debug("Yes, all balls are gathered") return True else: self.log.debug("No, all balls are not gathered") return False def gather_balls(self, target='home', antitarget=None): """Used to ensure that all balls are in (or not in) ball devices with the tag you pass. Typically this would be used after a game ends, or when the machine is reset or first starts up, to ensure that all balls are in devices tagged with 'home'. Args: target: A string of the tag name of the ball devices you want all the balls to end up in. Default is 'home'. antitarget: The opposite of target. Will eject all balls from all devices with the string you pass. Default is None. Note you can't pass both a target and antitarget in the same call. (If you do it will just use the target and ignore the antitarget.) TODO: Add support to actually move balls into position. e.g. STTNG, the lock at the top of the playfield wants to hold a ball before a game starts, so when a game ends the machine will auto eject one from the plunger with the diverter set so it's held in the rear lock. """ if not antitarget: # todo do we add the option of making the target a list? self.log.debug("Gathering all balls to devices tagged '%s'", target) for device in self.machine.balldevices: if (target in device.tags): device.num_balls_desired = device.config['ball_capacity'] else: device.num_balls_desired = 0 elif antitarget: self.log.debug("Emptying balls from devices tagged '%s'", antitarget) for device in self.machine.devices: if (target in device.tags): device.num_balls_desired = 0 else: device.num_balls_desired = device.config['ball_capacity'] def _ball_add_live_handler(self, balls): # Event handler which watches for device eject attempts to add # live balls if not balls: return # If our previous desired count was less or equal to our live count, # then this eject should increase the desired count. Why? Because # whatever caused this eject wants there to be more balls desired. # If the previous desired count was higher than this eject, then the # desired count shouldn't change, as these balls are fulfilling its # missing desired balls. # todo potential bug: What if prior desired was higher than prior live, # and we get a new live increase which takes it above the prior desired? # I *think* that should never happen since the ball controller would # try to launch a new ball if live fell below desired, but it's possible # we could get into this situation depending on staging times and stuff. # Let's log this as a warning for now and revisit this later. if ((self.num_balls_desired_live > self.num_balls_live) and balls > (self.num_balls_desired_live > self.num_balls_live)): self.log.warning("Ball add deficit warning. See note in " "_ball_add_live_handler() in ball_controller.py") if self.num_balls_desired_live <= self.num_balls_live: self.num_balls_desired_live += balls self.num_balls_live += balls self.machine.events.post('ball_live_added', total_live=self.num_balls_live) def _ball_remove_live_handler(self, balls=1): # Event handler which watches for device ball entry events self.num_balls_live -= balls self.num_balls_desired_live -= balls self.machine.events.post('ball_live_removed', total_live=self.num_balls_live) def _ball_drained_handler(self, balls): # This is a special handler which is called when balls enter devices # tagged with drain. It posts a ball_drain event and automatically # decrements the desired_balls_live counter. self.log.debug( "Ball Drain Handler. Previous desired live: %s. Will " "decrement by 1 and post 'ball_drain' relay event.", self.num_balls_desired_live) if not self.machine.tilted: self.num_balls_desired_live -= balls self.machine.events.post('ball_drain', ev_type='relay', callback=self._process_ball_drained, balls=balls) else: # received a drain while tilted self.machine.events.post('tilted_ball_drain') def _process_ball_drained(self, balls=None, ev_result=None): # We don't need to do anything here because other modules (ball save, # the game, etc. should jump in and do whatever they need to do when a # ball is drained. pass def ball_live_hit(self): """A ball just hit a playfield switch. This means we have a ball loose on the playfield. (It doesn't necessarily mean that ball is "live," as this could happen as a ball rolls towards the drain after a tilt.) This method is mainly used to continuously push out the start time of the ball search. If this method is called when a ball search is in progress, it will end the it. (Since this method means we found the stuck ball.) Note you shouldn't have to call this method manually. The switch controller will do it automatically each time a switch tagged with 'ball_live' has been activated. """ if self.num_balls_live: self.ball_search_schedule() if self.flag_ball_search_in_progress: self.log.debug("Just got a live playfield hit during ball search, " "so we're ending ball search.") self.ball_search_end() ''' ____ _ _ _____ _ | _ \ | | | / ____| | | | |_) | __ _| | | | (___ ___ __ _ _ __ ___| |__ | _ < / _` | | | \___ \ / _ \/ _` | '__/ __| '_ \ | |_) | (_| | | | ____) | __/ (_| | | | (__| | | | |____/ \__,_|_|_| |_____/ \___|\__,_|_| \___|_| |_| The following code interfaces with the ball search module (which actually performs the ball search). Here's the interface if you want to write your own: MPF will post events when it wants certain things to happen, like "ball_search_begin_1" "ball_search_begin_2" "ball_search_end" You can use the following methods from our machine controller. (These examples assume it's in your ball search module as self.machine.) self.machine.get_balldevice_status() Returns a dictionary of ball devices, with the device object as the key and the number of balls it contains at the value. If you want to access a balldevice, they're accessible via: self.machine.balldevices[<device>].x Valid methods incude eject() With force=True to force it to fire the eject coil even if it thinks there's no ball. # todo that should trigger an eject in progress which the game can use to figure out where the ball came from. This ball search module is not reponsible for "finding" a ball. It just manages all the actions that take place during a search. The MPF Ball Controller will "find" any balls that are knocked loose and will then cancel the search. If you create your own, you should receive the instance of the machine_ controller as an init paramter. You can fire coils via self.machine.coils[<coilname>].pulse() ''' # todo need to think about soft delay switches (flippers) def ball_search_schedule(self, secs=None, force=False): """Schedules a ball search to start. By default it will schedule it based on the time configured in the machine configuration files. If a ball search is already scheduled, this method will reset that schedule to the new time passed. Parameters ---------- secs : into Schedules the ball search that many secs from now. force : bool Set True to force a ball search. Otherwise it will only schedule it if self.flag_no_ball_search is False """ if self.machine.config['BallSearch']: if not self.flag_no_ball_search or force is True: if secs is not None: start_ms = secs * 1000 else: start_ms = (self.machine.config['BallSearch'] ['Secs until ball search start'] * 1000) self.log.debug("Scheduling a ball search for %s secs from now", start_ms / 1000.0) self.delay.reset("ball_search_start", ms=start_ms, callback=self.ball_search_begin) def ball_search_disable(self): """Disables ball search. Note this is used to prevent a future ball search from happening (like when all balls become contained.) This method is not used to cancel an existing ball search. (Use ball_search_end for that.) """ self.log.debug("Disabling Ball Search") self.delay.remove('ball_search_start') def ball_search_begin(self, force=False): """Begin the ball search process""" if not self.flag_no_ball_search: self.log.debug("Received request to start ball search") # ball search should only start if we have uncontained balls if self.num_balls_live or force: # todo add audit self.flag_ball_search_in_progress = True self.machine.events.post("ball_search_begin_phase1") # todo set delay to start phase 2 else: self.log.debug("We got request to start ball search, but we " "have no balls uncontained. WTF??") def ball_search_failed(self): """Ball Search did not find the ball.""" self.log.debug("Ball Search failed to find a ball. Disabling.") self.ball_search_end() self.ball_lost() def ball_search_end(self): """End the ball search, either because we found the ball or are giving up.""" self.log.debug("Ball search ending") self.flag_ball_search_in_progress = False self.machine.events.post("ball_search_end") # todo cancel the delay for phase 2 if we had one def ball_lost(self): """Mark a ball as lost""" self.num_balls_known = self.num_balls_contained self.num_balls_missing = self.machine.config['Machine']\ ['Balls Installed'] - self.num_balls_contained self.num_balls_live = 0 # since desired count doesn't change, this will relaunch them self.log.debug("Ball(s) Marked Lost. Known: %s, Missing: %s", self.num_balls_known, self.num_balls_missing) # todo audit balls lost def ball_found(self, num=1): """Used when a previously missing ball is found. Updates the balls known and balls missing variables. Parameters ---------- num : int Specifies how many balls have been found. Default is 1. """ self.log.debug("HEY!! We just found %s lost ball(s).", num) self.num_balls_known += num self.num_balls_missing -= num self.log.debug("New ball counts. Known: %s, Missing: %s", self.num_balls_known, self.num_balls_missing) self.ball_update_all_counts() # is this necessary? todo
class SequenceShot(Shot): def __init__(self, machine, name, config, priority): """SequenceShot is where you need certain switches to be hit in the right order, possibly within a time limit. Subclass of `Shot` Args: machine: The MachineController object name: String name of this shot. config: Dictionary that holds the configuration for this shot. """ super(SequenceShot, self).__init__(machine, name, config, priority) self.delay = DelayManager() self.progress_index = 0 """Tracks how far along through this sequence the current shot is.""" # convert our switches config to a list if 'switches' in self.config: self.config['switches'] = \ Config.string_to_list(self.config['switches']) # convert our timout to ms if 'time' in self.config: self.config['time'] = Timing.string_to_ms(self.config['time']) else: self.config['time'] = 0 self.active_delay = False self.enable() def enable(self): """Enables the shot. If it's not enabled, the switch handlers aren't active and the shot event will not be posted.""" super(SequenceShot, self).enable() # create the switch handlers for switch in self.config['switches']: self.machine.switch_controller.add_switch_handler( switch, self._switch_handler, return_info=True) self.progress_index = 0 def disable(self): """Disables the shot. If it's disabled, the switch handlers aren't active and the shot event will not be posted.""" super(SequenceShot, self).disable() for switch in self.config['switches']: self.machine.switch_controller.remove_switch_handler( switch, self.switch_handler) self.progress_index = 0 def _switch_handler(self, switch_name, state, ms): # does this current switch meet the next switch in the progress index? if switch_name == self.config['switches'][self.progress_index]: # are we at the end? if self.progress_index == len(self.config['switches']) - 1: self.confirm_shot() else: # does this shot specific a time limit? if self.config['time']: # do we need to set a delay? if not self.active_delay: self.delay.reset(name='shot_timer', ms=self.config['time'], callback=self.reset) self.active_delay = True # advance the progress index self.progress_index += 1 def confirm_shot(self): """Called when the shot is complete to confirm and reset it.""" # kill the delay self.delay.remove('shot_timer') # reset our shot self.reset() self.shot_made() def reset(self): """Resets the progress without disabling the shot.""" self.log.debug("Resetting this shot") self.progress_index = 0 self.active_delay = False
class Playfield(BallDevice): def __init__(self, machine, name, collection): self.log = logging.getLogger('Playfield') self.machine = machine self.name = name self.tags = list() self.config = defaultdict(lambda: None) self.config['eject_targets'] = list() self.ball_controller = self.machine.ball_controller self.delay = DelayManager() # Add the playfield ball device to the existing device collection collection_object = getattr(self.machine, collection)[name] = self # Attributes self._balls = 0 self.num_balls_requested = 0 self.player_controlled_eject_in_progress = None self.queued_balls = list() # Set up event handlers # Watch for balls added to the playfield for device in self.machine.balldevices: for target in device.config['eject_targets']: if target == self.name: self.machine.events.add_handler( event='balldevice_' + device.name + '_ball_eject_success', handler=self._source_device_eject_success) self.machine.events.add_handler( event='balldevice_' + device.name + '_ball_eject_attempt', handler=self._source_device_eject_attempt) break # Watch for balls removed from the playfield self.machine.events.add_handler('balldevice_captured_from_playfield', self._ball_removed_handler) # Watch for any switch hit which indicates a ball on the playfield self.machine.events.add_handler('sw_playfield_active', self.playfield_switch_hit) @property def balls(self): return self._balls @balls.setter def balls(self, balls): prior_balls = self._balls ball_change = balls - prior_balls if ball_change: self.log.debug( "Ball count change. Prior: %s, Current: %s, Change: " "%s", prior_balls, balls, ball_change) if balls > 0: self._balls = balls #self.ball_search_schedule() elif balls == 0: self._balls = 0 #self.ball_search_disable() else: self.log.warning( "Playfield balls went to %s. Resetting to 0, but " "FYI that something's weird", balls) self._balls = 0 #self.ball_search_disable() self.log.debug("New Ball Count: %s. (Prior count: %s)", self._balls, prior_balls) if ball_change > 0: self.machine.events.post_relay('balldevice_' + self.name + '_ball_enter', balls=ball_change) if ball_change: self.machine.events.post('playfield_ball_count_change', balls=balls, change=ball_change) def count_balls(self, **kwargs): """Used to count the number of balls that are contained in a ball device. Since this is the playfield device, this method always returns zero. Returns: 0 """ return 0 def get_additional_ball_capacity(self): """Used to find out how many more balls this device can hold. Since this is the playfield device, this method always returns 999. Returns: 999 """ return 999 def add_ball(self, balls=1, source_name=None, source_device=None, trigger_event=None): """Adds live ball(s) to the playfield. Args: balls: Integer of the number of balls you'd like to add. source_name: Optional string name of the ball device you'd like to add the ball(s) from. source_device: Optional ball device object you'd like to add the ball(s) from. trigger_event: The optional name of an event that MPF will wait for before adding the ball into play. Typically used with player- controlled eject tag events. If None, the ball will be added immediately. Returns: True if it's able to process the add_ball() request, False if it cannot. Both source_name and source_device args are included to give you two options for specifying the source of the ball(s) to be added. You don't need to supply both. (it's an "either/or" thing.) Both of these args are optional, so if you don't supply them then MPF will look for a device tagged with 'ball_add_live'. If you don't provide a source and you don't have a device with the 'ball_add_live' tag, MPF will quit. This method does *not* increase the game controller's count of the number of balls in play. So if you want to add balls (like in a ball scenario(, you need to call this method along with ``self.machine.game.add_balls_in_play()``.) MPF tracks the number of balls in play separately from the actual balls on the playfield because there are numerous situations where the two counts are not the same. For example, if a ball is in a VUK while some animation is playing, there are no balls on the playfield but still one ball in play, or if the player has a two-ball multiball and they shoot them both into locks, there are still two balls in play even though there are no balls on the playfield, or if the player tilts then there are still balls on the playfield but no balls in play. """ if balls < 1: self.log.error("Received request to add %s balls, which doesn't " "make sense. Not adding any balls...") return False # Figure out which device we'll get a ball from if source_device: pass elif source_name and source_name in self.machine.balldevices: source_device = self.machine.balldevices[source_name] else: for device in self.machine.balldevices.items_tagged( 'ball_add_live'): source_device = device break if not source_device: self.log.critical( "Received request to add a ball to the playfield, " "but no source device was passed and no ball " "devices are tagged with 'ball_add_live'. Cannot " "add a ball.") raise Exception("Received request to add a ball to the playfield, " "but no source device was passed and no ball " "devices are tagged with 'ball_add_live'. Cannot " "add a ball.") # If there's a player controlled eject in progress for this device, we # hold this request until it's over. if self.player_controlled_eject_in_progress == source_device: self.queued_balls.append( (balls, source_name, source_device, trigger_event)) self.log.debug( "An add_ball() request came in while there was a " "current player-controlled eject in progress for the" "same device. Will queue the eject request") return True self.log.debug( "Received request to add %s ball(s). Source device: %s. " "Wait for event: %s", balls, source_device.name, trigger_event) # If we don't have a coil that's fired by the player, and we our source # device has the ability to eject, then we do the eject now. # Some examples: # Plunger lane w/ switch and coil: ball_add_live device is plunger lane, # we don't eject now since *not* player_controlled is true. # Plunger lane w/ switch. No coil: ball_add_live device is plunger lane, # we don't eject now since there's no eject_coil for that device. # Plunger lane has no switch: ball_add_live device is trough, we do # eject now since there's no player_controlled tag and the device has an # eject coil. if trigger_event and source_device.config['eject_coil']: self.setup_player_controlled_eject(balls, source_device, trigger_event) else: # if there's no trigger, eject right away # if there's no eject coil, that's ok. We still need to setup the # eject so the device will be expecting the ball to disappear source_device.eject(balls=balls, target=self, get_ball=True) return True def setup_player_controlled_eject(self, balls, device, trigger_event): """Used to set up an eject from a ball device which will eject a ball to the playfield. Args: balls: Integer of the number of balls this device should eject. device: The ball device object that will eject the ball(s) when a switch with the player-controlled eject tag is hit. trigger_event: The name of the MPF event that will trigger the eject. When this method it called, MPF will set up an event handler to look for the trigger_event. """ self.log.debug( "Setting up a player controlled eject. Balls: %s, Device" ": %s, Trigger Event: %s", balls, device, trigger_event) if not device.balls: device.request_ball(balls=balls) self.machine.events.add_handler(trigger_event, self.player_eject_request, balls=balls, device=device) self.player_controlled_eject_in_progress = device def remove_player_controlled_eject(self): """Removed the player-controlled eject so a player hitting a switch no longer calls the device(s) to eject a ball. """ self.log.debug("Removing player-controlled eject.") self.machine.events.remove_handler(self.player_eject_request) self.player_controlled_eject_in_progress = None # Need to do this in case one of these queued balls is also a player # controlled eject which would re-add it to the queue while iterating. # So we clear it and pass them all to add_ball() and then let the queue # rebuild with what's left if it needs to. ball_list = self.queued_balls self.queued_balls = list() for item in ball_list: self.add_ball(balls=item[0], source_name=item[1], source_device=item[2], trigger_event=item[3]) def player_eject_request(self, balls, device): """A player has hit a switch tagged with the player_eject_request_tag. Args: balls: Integer of the number of balls that will be ejected. device: The ball device object that will eject the ball(s). """ self.log.debug("Received player eject request. Balls: %s, Device: %s", balls, device.name) device.eject(balls, target=self) def playfield_switch_hit(self): """A switch tagged with 'playfield_active' was just hit, indicating that there is at least one ball on the playfield. """ if not self.balls: if not self.num_balls_requested: self.log.debug("PF switch hit with no balls expected. Setting " "pf balls to 1.") self.balls = 1 self.machine.events.post('unexpected_ball_on_playfield') def _ball_added_handler(self, balls): self.log.debug("%s ball(s) added to the playfield", balls) self.balls += balls def _ball_removed_handler(self, balls): self.log.debug("%s ball(s) removed from the playfield", balls) self.balls -= balls def _source_device_eject_attempt(self, balls, target, **kwargs): # A source device is attempting to eject a ball. We need to know if it's # headed to the playfield. if target == self: self.log.debug( "A source device is attempting to ejected %s ball(s)" " to the playfield.", balls) self.num_balls_requested += balls def _source_device_eject_success(self, balls, target): # A source device has just confirmed that it has successfully ejected a # ball. Note that we don't care what type of confirmation it used. # (Playfield switch hit, count of its ball switches, etc.) if target == self: self.log.debug( "A source device has confirmed it's ejected %s " "ball(s) to the playfield.", balls) self.balls += balls self.num_balls_requested -= balls if self.num_balls_requested < 0: self.log.critical( "num_balls_requested is %s, which doesn't " "make sense. Quitting...", self.num_balls_requested) raise Exception( "num_balls_requested is %s, which doesn't make " "sense. Quitting...", self.num_balls_requested) self.remove_player_controlled_eject() def ok_to_confirm_ball_via_playfield_switch(self): """Used to check whether it's ok for a ball device which ejects to the playfield to confirm its eject via a playfield switch being hit. Returns: True or False Right now this is simple. If there are no playfield balls, then any playfield switch hit is assumed to be from the newly-ejected ball. If there are other balls on the playfield, then we can't use this confirmation method since we don't know whether a playfield switch hit is from the newly-ejected ball(s) or a current previously-live playfield ball. """ if not self.balls: return True else: return False # todo look for other incoming balls? # BALL SEARCH -------------------------------------------------------------- # todo make ball search work with plunger lanes with no switches. i.e. we # don't want ball search to start until a switch is hit? def ball_search_schedule(self, secs=None, force=False): """Schedules a ball search to start. By default it will schedule it based on the time configured in the machine configuration files. If a ball search is already scheduled, this method will reset that schedule to the new time passed. Args: secs: Schedules the ball search that many secs from now. force : Boolean to force a ball search. Set True to force a ball search. Otherwise it will only schedule it if self.flag_no_ball_search is False. Default is False """ if self.machine.config['ballsearch']: if not self.flag_no_ball_search or force is True: if secs is not None: start_ms = secs * 1000 else: start_ms = (self.machine.config['ballsearch'] ['secs until ball search start'] * 1000) self.log.debug("Scheduling a ball search for %s secs from now", start_ms / 1000.0) self.delay.reset("ball_search_start", ms=start_ms, callback=self.ball_search_begin) def ball_search_disable(self): """Disables ball search. Note this is used to prevent a future ball search from happening (like when all balls become contained). This method is not used to cancel an existing ball search. (Use `ball_search_end` for that.) """ self.log.debug("Disabling Ball Search") self.delay.remove('ball_search_start') def ball_search_begin(self, force=False): """Begin the ball search process""" if not self.flag_no_ball_search: self.log.debug("Received request to start ball search") # ball search should only start if we have uncontained balls if self.balls or force: # todo add audit self.flag_ball_search_in_progress = True self.machine.events.post("ball_search_begin_phase1") # todo set delay to start phase 2 else: self.log.debug("We got request to start ball search, but we " "have no balls uncontained. WTF??") def ball_search_failed(self): """Ball Search did not find the ball.""" self.log.debug("Ball Search failed to find a ball. Disabling.") self.ball_search_end() self.ball_lost() def ball_search_end(self): """End the ball search, either because we found the ball or are giving up.""" self.log.debug("Ball search ending") self.flag_ball_search_in_progress = False self.machine.events.post("ball_search_end") # todo cancel the delay for phase 2 if we had one def ball_lost(self): """Mark a ball as lost""" self.num_balls_known = self.balls self.num_balls_missing = self.machine.config['machine']\ ['balls installed'] - self.balls self.num_balls_live = 0 # since desired count doesn't change, this will relaunch them self.log.debug("Ball(s) Marked Lost. Known: %s, Missing: %s", self.num_balls_known, self.num_balls_missing) # todo audit balls lost def ball_found(self, num=1): """Used when a previously missing ball is found. Updates the balls known and balls missing variables. Parameters ---------- num : int Specifies how many balls have been found. Default is 1. """ self.log.debug("HEY!! We just found %s lost ball(s).", num) self.num_balls_known += num self.num_balls_missing -= num self.log.debug("New ball counts. Known: %s, Missing: %s", self.num_balls_known, self.num_balls_missing) self.ball_update_all_counts() # is this necessary? todo def eject(self, *args, **kwargs): pass def eject_all(self, *args, **kwargs): pass