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)
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)
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"])
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
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
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)
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)
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)
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)
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)
def start_stroke(self) -> Stroke: return Stroke.from_index(self.start_index)
def test_from_index(self): stroke = HANDSTROKE for i in range(-100, 100): self.assertEqual(Stroke.from_index(i), stroke) stroke = stroke.opposite()
def test_constants(self): self.assertEqual(HANDSTROKE, Stroke(True)) self.assertEqual(BACKSTROKE, Stroke(False))