text: """Contains the ShotGroup base class."""
  
  from collections import deque
  
  from mpf.core.device_monitor import DeviceMonitor
  
  from mpf.core.events import event_handler
  from mpf.core.mode import Mode
  from mpf.core.mode_device import ModeDevice
  from mpf.core.player import Player
  
  
  @DeviceMonitor("common_state", "rotation_enabled")
  class ShotGroup(ModeDevice):
  
      """Represents a group of shots in a pinball machine by grouping together multiple `Shot` class devices.
  
      This is used so you get get
      "group-level" functionality, like shot rotation, shot group completion,
      etc. This would be used for a group of rollover lanes, a bank of standups,
      etc.
      """
  
      config_section = 'shot_groups'
      collection = 'shot_groups'
      class_label = 'shot_group'
  
      __slots__ = ["rotation_enabled", "profile", "rotation_pattern"]
  
      def __init__(self, machine, name):
          """Initialise shot group."""
          super().__init__(machine, name)
  
          self.rotation_enabled = None
          self.profile = None
          self.rotation_pattern = None
  
      def add_control_events_in_mode(self, mode) -> None:
          """Remove enable here."""
  
      def device_loaded_in_mode(self, mode: Mode, player: Player):
          """Add device in mode."""
          super().device_loaded_in_mode(mode, player)
          self._check_for_complete()
          self.profile = self.config['shots'][0].profile
          self.rotation_pattern = deque(self.profile.config['rotation_pattern'])
          self.rotation_enabled = not self.config['enable_rotation_events']
          for shot in self.config['shots']:
              self.machine.events.add_handler("{}_hit".format(shot.name), self._hit)
  
      def device_removed_from_mode(self, mode):
          """Disable device when mode stops."""
          super().device_removed_from_mode(mode)
          self.machine.events.remove_handler(self._hit)
  
      @property
      def common_state(self):
          """Return common state if all shots in this group are in the same state.
  
          Will return None otherwise.
          """
          state = self.config['shots'][0].state_name
          for shot in self.config['shots']:
              if state != shot.state_name:
                  # shots do not have a common state
                  return None
  
          return state
  
      def _check_for_complete(self):
          """Check if all shots in this group are in the same state."""
          state = self.common_state
          if not state:
              # shots do not have a common state
              return
  
          # if we reached this point we got a common state
  
          self.debug_log(
              "Shot group is complete with state: %s", state)
  
          self.machine.events.post('{}_complete'.format(self.name), state=state)
          '''event: (name)_complete
          desc: All the member shots in the shot group called (name)
          are in the same state.
  
          args:
            state: name of the common state of all shots.
          '''
  
          self.machine.events.post('{}_{}_complete'.format(self.name, state))
          '''event: (name)_(state)_complete
          desc: All the member shots in the shot group called (name)
          are in the same state named (state).
          '''
  
      @event_handler(2)
      def event_enable(self, **kwargs):
          """Handle enable control event."""
          del kwargs
          self.enable()
  
      def enable(self):
          """Enable all member shots."""
          for shot in self.config['shots']:
              shot.enable()
  
      @event_handler(3)
      def event_disable(self, **kwargs):
          """Handle disable control event."""
          del kwargs
          self.disable()
  
      def disable(self):
          """Disable all member shots."""
          for shot in self.config['shots']:
              shot.disable()
  
      @event_handler(1)
      def event_reset(self, **kwargs):
          """Handle reset control event."""
          del kwargs
          self.reset()
  
      def reset(self):
          """Reset all member shots."""
          for shot in self.config['shots']:
              shot.reset()
  
      @event_handler(4)
      def event_restart(self, **kwargs):
          """Handle restart control event."""
          del kwargs
          self.restart()
  
      def restart(self):
          """Restart all member shots."""
          for shot in self.config['shots']:
              shot.restart()
  
      def _hit(self, advancing, **kwargs):
          """One of the member shots in this shot group was hit.
  
          Args:
              kwarg: {
                  profile: the current profile of the member shot that was hit
                  state: the current state of the member shot that was hit
                  advancing: boolean of whether the state is advancing
              }
          """
          if advancing:
              self._check_for_complete()
  
          self.machine.events.post(self.name + '_hit')
          '''event: (name)_hit
          desc: A member shots in the shot group called (name)
          has been hit.
          '''
          self.machine.events.post("{}_{}_hit".format(self.name, kwargs['state']))
          '''event: (name)_(state)_hit
          desc: A member shot with state (state) in the shot group (name)
          has been hit.
          '''
  
      @event_handler(9)
      def event_enable_rotation(self, **kwargs):
          """Handle enable_rotation control event."""
          del kwargs
          self.enable_rotation()
  
      def enable_rotation(self):
          """Enable shot rotation.
  
          If disabled, rotation events do not actually rotate the shots.
          """
          self.debug_log('Enabling rotation')
          self.rotation_enabled = True
  
      @event_handler(2)
      def event_disable_rotation(self, **kwargs):
          """Handle disable rotation control event."""
          del kwargs
          self.disable_rotation()
  
      def disable_rotation(self):
          """Disable shot rotation.
  
          If disabled, rotation events do not actually rotate the shots.
          """
          self.debug_log('Disabling rotation')
          self.rotation_enabled = False
  
      @event_handler(4)
      def event_rotate(self, direction=None, **kwargs):
          """Handle rotate control event."""
          del kwargs
          self.rotate(direction)
  
      def rotate(self, direction=None):
          """Rotate (or "shift") the state of all the shots in this group.
  
          This is used for things like lane change, where hitting the flipper
          button shifts all the states of the shots in the group to the left or
          right.
  
          This method actually transfers the current state of each shot profile
          to the left or the right, and the shot on the end rolls over to the
          taret on the other end.
  
          Args:
              direction: String that specifies whether the rotation direction is
                  to the left or right. Values are 'right' or 'left'. Default of
                  None will cause the shot group to rotate in the direction as
                  specified by the rotation_pattern.
  
          Note that this shot group must, and rotation_events for this
          shot group, must both be enabled for the rotation events to work.
          """
          if not self.rotation_enabled:
              self.debug_log("Received rotation request. "
                             "Rotation Enabled: %s. Will NOT rotate",
                             self.rotation_enabled)
  
              return
  
          # shot_state_list is deque of tuples (state num, show step num)
          shot_state_list = deque()
  
          shots_to_rotate = []
          for shot in self.config['shots']:
              if shot.can_rotate:
                  shots_to_rotate.append(shot)
                  shot_state_list.append(shot.state)
  
          # figure out which direction we're going to rotate
          if not direction:
              direction = self.rotation_pattern[0]
              self.rotation_pattern.rotate(-1)
              self.debug_log("Since no direction was specified, pulling from"
                             " rotation pattern: '%s'", direction)
  
          # rotate that list
          if direction.lower() in ('right', 'r'):
              shot_state_list.rotate(1)
          else:
              shot_state_list.rotate(-1)
  
          # step through all our shots and update their states
          for i, shot in enumerate(shots_to_rotate):
              shot.jump(state=shot_state_list[i], force=True)
  
      @event_handler(8)
      def event_rotate_right(self, **kwargs):
          """Handle rotate right control event."""
          del kwargs
          self.rotate_right()
  
      def rotate_right(self):
          """Rotate the state of the shots to the right.
  
          This method is the same as calling rotate('right')
          """
          self.rotate(direction='right')
  
      @event_handler(7)
      def event_rotate_left(self, **kwargs):
          """Handle rotate left control event."""
          del kwargs
          self.rotate_left()
  
      def rotate_left(self):
          """Rotate the state of the shots to the left.
  
          This method is the same as calling rotate('left')
          """
          self.rotate(direction='left')
  