66
77import time
88import logging
9- from typing import Optional , Any
9+ from typing import Optional , Any , List
1010
1111from wheatley import calls
1212from wheatley .aliases import JSON , Row
@@ -33,7 +33,7 @@ class Bot:
3333 logger_name = "BOT"
3434
3535 def __init__ (self , tower : RingingRoomTower , row_generator : RowGenerator , do_up_down_in : bool ,
36- stop_at_rounds : bool , rhythm : Rhythm , user_name : Optional [str ] = None ,
36+ stop_at_rounds : bool , call_comps : bool , rhythm : Rhythm , user_name : Optional [str ] = None ,
3737 server_instance_id : Optional [int ] = None ) -> None :
3838 """ Initialise a Bot with all the parts it needs to run. """
3939 # If this is None then Wheatley is in client mode, otherwise Wheatley is in server mode
@@ -43,11 +43,12 @@ def __init__(self, tower: RingingRoomTower, row_generator: RowGenerator, do_up_d
4343 self ._rhythm = rhythm
4444 self ._do_up_down_in = do_up_down_in
4545 self ._stop_at_rounds = stop_at_rounds
46+ self ._call_comps = call_comps
4647 self ._user_name = user_name
4748
4849 self .row_generator = row_generator
49- # This is the row generator that will be used after 'Look to' is called for the next time, allowing
50- # for changing the method or composition whilst Wheatley is running.
50+ # This is the row generator that will be used after 'Look to' is called for the next time,
51+ # allowing for changing the method or composition whilst Wheatley is running.
5152 self .next_row_generator : Optional [RowGenerator ] = None
5253
5354 self ._tower = tower
@@ -67,14 +68,24 @@ def __init__(self, tower: RingingRoomTower, row_generator: RowGenerator, do_up_d
6768
6869 self ._is_ringing = False
6970 self ._is_ringing_rounds = True
70- self ._should_start_method = False
71+ # This is used as a counter - once `Go` or `Look To` is received, the number of rounds left
72+ # is calculated and then decremented at the start of every subsequent row until it reaches
73+ # 0, at which point the method starts. We keep a counter rather than a simple flag so that
74+ # calls can be called **before** going into changes when Wheatley is calling (useful for
75+ # calling the first method name in spliced and early calls in Original, Erin, etc.). The
76+ # value `None` is used to represent the case where we don't know when we will be starting
77+ # the method (and therefore there it makes no sense to decrement this counter).
78+ self ._rounds_left_before_method : Optional [int ] = None
7179 self ._should_start_ringing_rounds = False
7280 self ._should_stand = False
7381
7482 self ._row_number = 0
7583 self ._place = 0
7684 self ._rounds : Row = rounds (MAX_BELL )
7785 self ._row : Row = self ._rounds
86+ # This is used because the row's calls are generated at the **end** of each row (or on
87+ # `Look To`), but need to be called at the **start** of the next row.
88+ self ._calls : List [str ] = []
7889
7990 self .logger = logging .getLogger (self .logger_name )
8091
@@ -171,24 +182,49 @@ def look_to_has_been_called(self, call_time: float) -> None:
171182 self .row_generator = self .next_row_generator or self .row_generator
172183 self .next_row_generator = None
173184
174- # Clear all the flags
185+ # Clear all the flags and counters
175186 self ._should_stand = False
176- self ._should_start_method = False
177187 self ._should_start_ringing_rounds = False
188+ # Set _rounds_left_before_method if we are ringing up-down-in (3 rounds for backstroke
189+ # start; 2 for handstroke)
190+ if not self ._do_up_down_in :
191+ self ._rounds_left_before_method = None
192+ elif self .row_generator .start_stroke ().is_hand ():
193+ self ._rounds_left_before_method = 2
194+ else :
195+ self ._rounds_left_before_method = 3
178196
179197 # Reset the state, so that Wheatley starts by ringing rounds
180198 self ._is_ringing = True
181199 self ._is_ringing_rounds = True
182200
183201 # Start at the first place of the first row
184- self ._row_number = 0
185- self ._place = 0
186- self .start_next_row ()
202+ self .start_next_row (is_first_row = True )
187203
188204 def _on_go (self ) -> None :
189205 """ Callback called when a user calls 'Go'. """
190206 if self ._is_ringing_rounds :
191- self ._should_start_method = True
207+ # Calculate how many more rows of rounds we should ring before going into changes (1 if
208+ # the person called 'Go' on the same stroke as the RowGenerator starts, otherwise 0).
209+ # These values are one less than expected because we are setting
210+ # _rounds_left_before_method **after** the row has started.
211+ self ._rounds_left_before_method = 1 if self .stroke == self .row_generator .start_stroke () else 0
212+ # Make sure to call all of the calls that we have missed in the right order (in case the
213+ # person calling `Go` called it stupidly late)
214+ early_calls = [
215+ (ind , calls )
216+ for (ind , calls ) in self .row_generator .early_calls ().items ()
217+ if ind > self ._rounds_left_before_method
218+ ]
219+ # Sort early calls by the number of rows **before** the method start. Note that we are
220+ # sorting by a quantity that counts **down** with time, hence the reversed sort.
221+ early_calls .sort (key = lambda x : x [0 ], reverse = True )
222+ # In this case, we don't want to wait until the next row before making these calls
223+ # because the rows on which these calls should have been called have already passed.
224+ # Therefore, we simply get them out as quickly as possible so they have the best chance
225+ # of being heard.
226+ for (_index , calls ) in early_calls :
227+ self ._make_calls (calls )
192228
193229 def _on_bob (self ) -> None :
194230 """ Callback called when a user calls 'Bob'. """
@@ -231,34 +267,81 @@ def expect_bell(self, index: int, bell: Bell) -> None:
231267 self .stroke
232268 )
233269
234- def start_next_row (self ) -> None :
235- """
236- Creates a new row from the row generator and tells the rhythm to expect the new bells.
237- """
238-
270+ def generate_next_row (self ) -> None :
271+ """ Creates a new row from the row generator and tells the rhythm to expect the new bells. """
239272 if self ._is_ringing_rounds :
240273 self ._row = self ._rounds
241274 else :
242- self ._row = self .row_generator .next_row (self .stroke )
275+ self ._row , self ._calls = self .row_generator .next_row_and_calls (self .stroke )
276+ # Add cover bells if needed
243277 if len (self ._row ) < len (self ._rounds ):
244- # Add cover bells if needed
245278 self ._row .extend (self ._rounds [len (self ._row ):])
246279
247- for (index , bell ) in enumerate (self ._row ):
248- self .expect_bell (index , bell )
280+ def start_next_row (self , is_first_row : bool ) -> None :
281+ # Generate the next row and update row indices
282+ self ._place = 0
283+ if is_first_row :
284+ self ._row_number = 0
285+ else :
286+ self ._row_number += 1
249287
250- def start_method (self ) -> None :
251- """
252- Called when the ringing is about to go into changes.
253- Resets the row_generator and starts the next row.
254- """
255- if self ._check_number_of_bells ():
288+ # Useful local variables
289+ has_just_rung_rounds = self ._row == self ._rounds
290+ next_stroke = Stroke .from_index (self ._row_number )
291+
292+ # Implement handbell-style stopping at rounds
293+ if self ._stop_at_rounds and has_just_rung_rounds and not self ._is_ringing_rounds :
294+ self ._should_stand = False
295+ self ._is_ringing = False
296+
297+ # Set any early calls specified by the row generator to be called at the start of the next
298+ # row
299+ if self ._rounds_left_before_method is not None :
300+ self ._calls = self .row_generator .early_calls ().get (self ._rounds_left_before_method ) or []
301+
302+ # Start the method if necessary
303+ if self ._rounds_left_before_method == 0 :
304+ # Sanity check that we are in fact starting on the correct stroke (which is no longer
305+ # trivially guaranteed since we use a counter rather than a flag to determine when to
306+ # start the method)
307+ assert next_stroke == self .row_generator .start_stroke ()
308+ self ._rounds_left_before_method = None
309+ self ._is_ringing_rounds = False
310+ # If the tower size somehow changed, then call 'Stand' but keep ringing rounds (Wheatley
311+ # calling 'Stand' will still generate a callback to `self._on_stand_next`, so we don't
312+ # need to handle that here)
313+ if not self ._check_number_of_bells ():
314+ self ._make_call ("Stand" )
315+ self ._is_ringing_rounds = True
256316 self .row_generator .reset ()
257- self .start_next_row ()
317+ if self ._rounds_left_before_method is not None :
318+ self ._rounds_left_before_method -= 1
319+
320+ # If we're starting a handstroke ...
321+ if next_stroke .is_hand ():
322+ # ... and 'Stand' has been called, then stand
323+ if self ._should_stand :
324+ self ._should_stand = False
325+ self ._is_ringing = False
326+ # ... and "That's All" has been called, then start ringing rounds.
327+ # TODO: Replace this with more intuitive behaviour
328+ if self ._should_start_ringing_rounds and not self ._is_ringing_rounds :
329+ self ._should_start_ringing_rounds = False
330+ self ._is_ringing_rounds = True
331+
332+ # If we've set `_is_ringing` to False, then no more rounds can happen so early return to
333+ # avoid erroneous calls
334+ if not self ._is_ringing :
335+ return
336+
337+ # Generate the next row, and tell the rhythm detection where the next row's bells are
338+ # expected to ring
339+ self .generate_next_row ()
340+ for (index , bell ) in enumerate (self ._row ):
341+ self .expect_bell (index , bell )
258342
259343 def tick (self ) -> None :
260344 """ Move the ringing on by one place """
261-
262345 bell = self ._row [self ._place ]
263346 user_controlled = self ._user_assigned_bell (bell )
264347
@@ -268,47 +351,16 @@ def tick(self) -> None:
268351 if not user_controlled :
269352 self ._tower .ring_bell (bell , self .stroke )
270353
354+ # If we are ringing the first bell in the row, then also make any calls that are needed.
355+ if self ._place == 0 :
356+ self ._make_calls (self ._calls )
357+
358+ # Move one place through the ringing
271359 self ._place += 1
272360
361+ # Start a new row if we get to a place that's bigger than the number of bells
273362 if self ._place >= self .number_of_bells :
274- # Determine if we're finishing a handstroke
275- has_just_rung_rounds = self ._row == self ._rounds
276-
277- # Generate the next row and update row indices
278- self ._row_number += 1
279- self ._place = 0
280- self .start_next_row ()
281-
282- next_stroke = Stroke .from_index (self ._row_number )
283-
284- # ===== SET FLAGS FOR HANDBELL-STYLE RINGING =====
285-
286- # Implement handbell-style 'up down in'
287- if self ._do_up_down_in and self ._is_ringing_rounds and self ._row_number == 2 :
288- self ._should_start_method = True
289-
290- # Implement handbell-style stopping at rounds
291- if self ._stop_at_rounds and has_just_rung_rounds and not self ._is_ringing_rounds :
292- self ._should_stand = False
293- self ._is_ringing = False
294-
295- # ===== CONVERT THE FLAGS INTO ACTIONS =====
296-
297- if self ._should_start_method and self ._is_ringing_rounds \
298- and next_stroke == self .row_generator .start_stroke ():
299- self ._should_start_method = False
300- self ._is_ringing_rounds = False
301- self .start_method ()
302-
303- # If we're starting a handstroke, we should convert all the flags into actions
304- if next_stroke .is_hand ():
305- if self ._should_stand :
306- self ._should_stand = False
307- self ._is_ringing = False
308-
309- if self ._should_start_ringing_rounds and not self ._is_ringing_rounds :
310- self ._should_start_ringing_rounds = False
311- self ._is_ringing_rounds = True
363+ self .start_next_row (is_first_row = False )
312364
313365 def main_loop (self ) -> None :
314366 """
@@ -346,3 +398,13 @@ def _user_assigned_bell(self, bell: Bell) -> bool:
346398 def _bot_assigned_bell (self , bell : Bell ) -> bool :
347399 """ Returns `True` if this bell **is** assigned to Wheatley. """
348400 return self ._tower .is_bell_assigned_to (bell , self ._user_name )
401+
402+ def _make_calls (self , calls : List [str ]) -> None :
403+ """ Broadcast a sequence of calls """
404+ for c in calls :
405+ self ._make_call (c )
406+
407+ def _make_call (self , call : str ) -> None :
408+ """ Broadcast a call, unless we've been told not to call anything. """
409+ if self ._call_comps :
410+ self ._tower .make_call (call )
0 commit comments