Ejemplo n.º 1
0
 def test_equality(self):
     for i in [True, False]:
         for j in [True, False]:
             # Strokes containing `i` and `j` should be equal precisely when `i == j`
             self.assertEqual(Stroke(i) == Stroke(j), i == j)
             # Strokes containing `i` and `j` should be not equal precisely when `i != j`
             self.assertEqual(Stroke(i) != Stroke(j), i != j)
Ejemplo n.º 2
0
 def _on_bell_ring(self, bell: Bell, stroke: Stroke) -> None:
     """Callback called when the Tower receives a signal that a bell has been rung."""
     if self._user_assigned_bell(bell):
         # This will give us the stroke _after_ the bell rings, we have to invert it, because
         # otherwise this will always expect the bells on the wrong stroke and no ringing will
         # ever happen
         self._rhythm.on_bell_ring(bell, stroke.opposite(), time.time())
    def __init__(self, comp_id: int, access_key: Optional[str] = None) -> None:
        # Generate URL and request the rows
        url = self.complib_url + str(comp_id) + "/rows"
        if access_key:
            url += "?accessKey=" + access_key
        request_rows = requests.get(url)
        # Check for the status of the requests
        if request_rows.status_code == 404:
            raise InvalidCompError(comp_id)
        if request_rows.status_code == 403:
            raise PrivateCompError(comp_id)
        request_rows.raise_for_status()
        # Parse the request responses as JSON
        response_rows = json.loads(request_rows.text)
        unparsed_rows = response_rows['rows']
        # Determine the start row of the composition
        num_starting_rounds = 0
        while unparsed_rows[num_starting_rounds][0] == unparsed_rows[0][0]:
            num_starting_rounds += 1
        self._start_stroke = Stroke.from_index(num_starting_rounds)

        # Derive the rows, calls and stage from the JSON response
        self.loaded_rows: List[Tuple[Row, Optional[str]]] = [
            (Row([Bell.from_str(bell)
                  for bell in row]), None if call == '' else call) for row,
            call, property_bitmap in unparsed_rows[num_starting_rounds:]
        ]
        stage = response_rows['stage']

        # Variables from which the summary string is generated
        self.comp_id = comp_id
        self.comp_title = response_rows['title']
        self.is_comp_private = access_key is not None

        super().__init__(stage)
Ejemplo n.º 4
0
 def _on_global_bell_state(self, data: JSON) -> None:
     """
     Callback called when receiving an update to the global tower state.
     Cannot have further callbacks assigned to it.
     """
     global_bell_state: List[bool] = data["global_bell_state"]
     self._update_bell_state([Stroke(x) for x in global_bell_state])
     for invoke_callback in self.invoke_on_reset:
         invoke_callback()
    def __init__(self,
                 comp_id: int,
                 access_key: Optional[str] = None,
                 substituted_method_id: Optional[int] = None) -> None:
        def process_call_string(calls: str) -> List[str]:
            """Parse a sequence of calls, and remove 'Stand'."""
            stripped_calls = [x.strip() for x in calls.split(";")]
            return [c for c in stripped_calls if c != "Stand"]

        # Generate URL from the params
        query_sections: List[str] = []
        if access_key:
            query_sections.append(f"accessKey={access_key}")
        if substituted_method_id:
            query_sections.append(
                f"substitutedmethodid={substituted_method_id}")
        url = self.complib_url + str(comp_id) + "/rows"
        if query_sections:
            url += "?" + "&".join(query_sections)
        # Create an HTTP request for the rows, and deal with potential error codes
        request_rows = requests.get(url)
        if request_rows.status_code == 404:
            raise InvalidCompError(comp_id)
        if request_rows.status_code == 403:
            raise PrivateCompError(comp_id)
        request_rows.raise_for_status()
        # Parse the request responses as JSON
        response_rows = json.loads(request_rows.text)
        unparsed_rows = response_rows["rows"]
        # Determine the start row of the composition
        num_starting_rounds = 0
        while unparsed_rows[num_starting_rounds][0] == unparsed_rows[0][0]:
            num_starting_rounds += 1
        self._start_stroke = Stroke.from_index(num_starting_rounds)
        # Derive the rows, calls and stage from the JSON response
        loaded_rows: List[Tuple[Row, List[str]]] = [
            (Row([Bell.from_str(bell) for bell in row]),
             [] if calls == "" else process_call_string(calls))
            for row, calls, _property_bitmap in unparsed_rows
        ]
        # Convert these parsed rows into a format that we can read more easily when ringing.  I.e.
        # load the non-rounds rows as-is, but keep the calls that should be called in rounds
        self.loaded_rows = loaded_rows[num_starting_rounds:]
        self._early_calls = {
            num_starting_rounds - i: calls
            for i, (_row,
                    calls) in enumerate(loaded_rows[:num_starting_rounds])
            if calls != []
        }
        # Set the variables from which the summary string is generated
        self.comp_id = comp_id
        self.comp_title = response_rows["title"]
        self.is_comp_private = access_key is not None
        # Propogate the initialisation up to the parent class
        super().__init__(response_rows["stage"])
Ejemplo n.º 6
0
    def _gen_row(self, previous_row: Row, stroke: Stroke, index: int) -> Row:
        leading_bell = previous_row[0].number
        pn_index = 0 if stroke.is_hand() else 1

        if self._has_bob and self.bob_rules.get(leading_bell):
            place_notation = self.bob_rules[leading_bell][pn_index]

            if stroke.is_back():
                self.reset_calls()
        elif self._has_single and self.single_rules.get(leading_bell):
            place_notation = self.single_rules[leading_bell][pn_index]

            if stroke.is_back():
                self.reset_calls()
        elif self.plain_rules.get(leading_bell):
            place_notation = self.plain_rules[leading_bell][pn_index]
        else:
            place_notation = self.plain_rules[0][pn_index]

        row = self.permute(previous_row, place_notation)
        return row
Ejemplo n.º 7
0
    def tick(self) -> None:
        """ Move the ringing on by one place """

        bell = self._row[self._place]
        user_controlled = self._user_assigned_bell(bell)

        self._rhythm.wait_for_bell_time(time.time(), bell, self._row_number, self._place,
                                        user_controlled, self.stroke)

        if not user_controlled:
            self._tower.ring_bell(bell, self.stroke)

        self._place += 1

        if self._place >= self.number_of_bells:
            # Determine if we're finishing a handstroke
            has_just_rung_rounds = self._row == self._rounds

            # Generate the next row and update row indices
            self._row_number += 1
            self._place = 0
            self.start_next_row()

            next_stroke = Stroke.from_index(self._row_number)

            # ===== SET FLAGS FOR HANDBELL-STYLE RINGING =====

            # Implement handbell-style 'up down in'
            if self._do_up_down_in and self._is_ringing_rounds and self._row_number == 2:
                self._should_start_method = True

            # Implement handbell-style stopping at rounds
            if self._stop_at_rounds and has_just_rung_rounds and not self._is_ringing_rounds:
                self._should_stand = False
                self._is_ringing = False

            # ===== CONVERT THE FLAGS INTO ACTIONS =====

            if self._should_start_method and self._is_ringing_rounds \
               and next_stroke == self.row_generator.start_stroke():
                self._should_start_method = False
                self._is_ringing_rounds = False
                self.start_method()

            # If we're starting a handstroke, we should convert all the flags into actions
            if next_stroke.is_hand():
                if self._should_stand:
                    self._should_stand = False
                    self._is_ringing = False

                if self._should_start_ringing_rounds and not self._is_ringing_rounds:
                    self._should_start_ringing_rounds = False
                    self._is_ringing_rounds = True
Ejemplo n.º 8
0
    def expect_bell(self, expected_bell: Bell, row_number: int, place: int, expected_stroke: Stroke) -> None:
        """
        Indicates that a given Bell is expected to be rung at a given row, place and stroke.
        Used by the rhythm so that when that bell is rung later, it can tell where that bell
        _should_ have been in the ringing, and so can use that knowledge to inform the speed of the
        ringing.
        """
        self._inner_rhythm.expect_bell(expected_bell, row_number, place, expected_stroke)

        if expected_stroke != self._current_stroke:
            self._current_stroke = expected_stroke
            self._expected_bells[expected_stroke].clear()
            self._early_bells[expected_stroke.opposite()].clear()

        if expected_bell not in self._early_bells[expected_stroke]:
            self._expected_bells[expected_stroke].add(expected_bell)
Ejemplo n.º 9
0
 def _on_bell_rung(self, data: JSON) -> None:
     """ Callback called when the client receives a signal that a bell has been rung. """
     # Unpack the data and assign it expected types
     global_bell_state: List[bool] = data['global_bell_state']
     who_rang_raw: int = data["who_rang"]
     # Run callbacks for updating the bell state
     self._update_bell_state([Stroke(b) for b in global_bell_state])
     # Convert 'who_rang' to a Bell
     who_rang = Bell.from_number(who_rang_raw)
     # Only run the callbacks if the bells exist
     for bell_ring_callback in self.invoke_on_bell_rung:
         new_stroke = self.get_stroke(who_rang)
         if new_stroke is None:
             self.logger.warning(
                 f"Bell {who_rang} rang, but the tower only has {self.number_of_bells} bells."
             )
         else:
             bell_ring_callback(who_rang, new_stroke)
Ejemplo n.º 10
0
    def start_next_row(self, is_first_row: bool) -> None:
        """Updates state of bot ready to ring the next row / stop ringing"""
        # Generate the next row and update row indices
        self._place = 0
        if is_first_row:
            self._row_number = 0
        else:
            self._row_number += 1

        # Useful local variables
        has_just_rung_rounds = self._row == self._rounds
        next_stroke = Stroke.from_index(self._row_number)

        # Implement handbell-style stopping at rounds
        if self._stop_at_rounds and has_just_rung_rounds and not self._is_ringing_opening_row:
            self._should_stand = True

        # Set any early calls specified by the row generator to be called at the start of the next
        # row
        if self._rounds_left_before_method is not None:
            self._calls = self.row_generator.early_calls().get(
                self._rounds_left_before_method) or []

        # Start the method if necessary
        if self._rounds_left_before_method == 0:
            # Sanity check that we are in fact starting on the correct stroke (which is no longer
            # trivially guaranteed since we use a counter rather than a flag to determine when to
            # start the method)
            assert next_stroke == self.row_generator.start_stroke()
            self._rounds_left_before_method = None
            self._is_ringing_rounds = False
            self._is_ringing_opening_row = False
            # If the tower size somehow changed, then call 'Stand' but keep ringing rounds (Wheatley
            # calling 'Stand' will still generate a callback to `self._on_stand_next`, so we don't
            # need to handle that here)
            if not self._check_number_of_bells():
                self._make_call("Stand")
                self._is_ringing_rounds = True
            self.row_generator.reset()
        if self._rounds_left_before_method is not None:
            self._rounds_left_before_method -= 1

        # If we're starting a handstroke ...
        if next_stroke.is_hand():
            # ... and 'Stand' has been called, then stand
            if self._should_stand:
                self._should_stand = False
                self._is_ringing = False

        # There are two cases for coming round:
        # 1. Someone calls 'That's All' and rounds appears
        #       (or)
        # 2. Someone calls 'That's All', one clear row has elapsed
        if self._rows_left_before_rounds == 0 or (
                has_just_rung_rounds
                and self._rows_left_before_rounds is not None):
            self._rows_left_before_rounds = None
            self._is_ringing_rounds = True
        if self._rows_left_before_rounds is not None:
            self._rows_left_before_rounds -= 1

        # If we've set `_is_ringing` to False, then no more rounds can happen so early return to
        # avoid erroneous calls
        if not self._is_ringing:
            return

        # Generate the next row, and tell the rhythm detection where the next row's bells are
        # expected to ring
        self.generate_next_row()
        for (index, bell) in enumerate(self._row):
            self.expect_bell(index, bell)
Ejemplo n.º 11
0
 def stroke(self) -> Stroke:
     """Returns true if the current row (determined by self._row_number) represents a handstroke."""
     return Stroke.from_index(self._row_number)
Ejemplo n.º 12
0
 def _gen_row(self, previous_row: Row, stroke: Stroke, index: int) -> Row:
     if stroke.is_hand():
         return self.permute(previous_row, Places([]))
     return self.permute(previous_row, Places([1, self.stage]))
    def next_row(self, stroke: Stroke) -> Row:
        if not self.called_go and stroke.is_back() and random.choices(
            [True, False], [1, 3]):
            self.tower.make_call(calls.GO)

        return super().next_row(stroke)
Ejemplo n.º 14
0
 def start_stroke(self) -> Stroke:
     return Stroke.from_index(self.start_index)
Ejemplo n.º 15
0
 def test_from_index(self):
     stroke = HANDSTROKE
     for i in range(-100, 100):
         self.assertEqual(Stroke.from_index(i), stroke)
         stroke = stroke.opposite()
Ejemplo n.º 16
0
 def test_constants(self):
     self.assertEqual(HANDSTROKE, Stroke(True))
     self.assertEqual(BACKSTROKE, Stroke(False))