Skip to content

Commit 08f4e18

Browse files
authored
Merge pull request #143 from kneasle/call-complib-comps
Call complib comps
2 parents 40b2ce5 + dbdefd8 commit 08f4e18

File tree

7 files changed

+199
-127
lines changed

7 files changed

+199
-127
lines changed

CHANGE_LOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
- Add support for loading private CompLib urls from 'share' links using `--complib-share-link
66
<link>`.
7+
- Wheatley now calls CompLib compositions (use `--no-calls` to suppress this)
78
- Add proper support for backstroke starts (with 3 rows of rounds for `--up-down-in`).
89
- Add `--start-index` to specify how many rows into a lead of a method Wheatley should start.
910
- Add `-v`/`--verbose` and `-q`/`--quiet` to change how much stuff Wheatley prints.

wheatley/bot.py

Lines changed: 128 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66

77
import time
88
import logging
9-
from typing import Optional, Any
9+
from typing import Optional, Any, List
1010

1111
from wheatley import calls
1212
from 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)

wheatley/main.py

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -195,14 +195,15 @@ def server_main(override_args: Optional[List[str]], stop_on_join_tower: bool) ->
195195
max_bells_in_dataset = 15
196196
handstroke_gap = 1
197197
use_wait = True
198+
call_comps = True
198199

199200
tower_url = "http://127.0.0.1:" + str(args.port)
200201

201202
tower = RingingRoomTower(args.room_id, tower_url)
202203
rhythm = create_rhythm(peal_speed, inertia, max_bells_in_dataset, handstroke_gap, use_wait,
203204
initial_inertia)
204-
bot = Bot(tower, PlaceHolderGenerator(), use_up_down_in, stop_at_rounds, rhythm, user_name="Wheatley",
205-
server_instance_id=args.id)
205+
bot = Bot(tower, PlaceHolderGenerator(), use_up_down_in, stop_at_rounds, call_comps, rhythm,
206+
user_name="Wheatley", server_instance_id=args.id)
206207

207208
with tower:
208209
tower.wait_loaded()
@@ -324,6 +325,11 @@ def console_main(override_args: Optional[List[str]], stop_on_join_tower: bool) -
324325
default, he will ring 'towerbell style', i.e. only taking instructions from the \
325326
ringing-room calls. This is equivalent to using the '-us' flags."
326327
)
328+
row_gen_group.add_argument(
329+
"--no-calls",
330+
action="store_true",
331+
help="If set, Wheatley will not call anything when ringing compositions."
332+
)
327333

328334
# Rhythm arguments
329335
rhythm_group = parser.add_argument_group("Rhythm arguments")
@@ -425,7 +431,7 @@ def console_main(override_args: Optional[List[str]], stop_on_join_tower: bool) -
425431
rhythm = create_rhythm(peal_speed, args.inertia, args.max_bells_in_dataset, args.handstroke_gap,
426432
not args.keep_going)
427433
bot = Bot(tower, row_generator, args.use_up_down_in or args.handbell_style,
428-
args.stop_at_rounds or args.handbell_style, rhythm, user_name=args.name)
434+
args.stop_at_rounds or args.handbell_style, not args.no_calls, rhythm, user_name=args.name)
429435

430436
# Catch keyboard interrupts and just print 'Bye!' instead a load of guff
431437
try:

wheatley/row_generation/__init__.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22

33
from .complib_composition_generator import ComplibCompositionGenerator
44
from .dixonoids_generator import DixonoidsGenerator
5-
from .go_and_stop_calling_generator import GoAndStopCallingGenerator
65
from .method_place_notation_generator import MethodPlaceNotationGenerator, generator_from_special_title
76
from .place_notation_generator import PlaceNotationGenerator
87
from .plain_hunt_generator import PlainHuntGenerator

0 commit comments

Comments
 (0)