|
| 1 | +# Architecture |
| 2 | + |
| 3 | +This file provides a high-level overview of the code, inspired by |
| 4 | +[matklad's blog post](https://matklad.github.io/2021/02/06/ARCHITECTURE.md.html) of the same name. |
| 5 | +The purpose of this file is to provide a convenient 'mental map' of Wheatley's code to help new |
| 6 | +contributors familiarise themselves with the code base. It aims to answers general questions like |
| 7 | +"where do I find code to do X?" or "what does the code in Y do?", but doesn't go into great detail |
| 8 | +about the inner workings of each section. |
| 9 | + |
| 10 | +## Bird's Eye View of the Problem |
| 11 | + |
| 12 | +Wheatley is a 'bot' ringer for the Ringing Room platform. Wheatley also adapts to the actions of |
| 13 | +other human ringers _in real time_ rather than simply expecting them to keep up with a set pace. |
| 14 | +To achieve this goal, Wheatley has three tasks once the start-up is complete: |
| 15 | + |
| 16 | +1. Know what is being rung (i.e. what order are the bells expected to ring). This can be affected |
| 17 | + dynamically by human ringers making calls like `Bob`, `Single`, `Go`, etc. |
| 18 | +2. Listen for human-controlled bells being rung, and use this new information to update some |
| 19 | + internal sense of 'rhythm'. |
| 20 | +3. Combine, in real time, the expected order of the bells (from 1.) with the real-time data from |
| 21 | + human ringers (from 2.) in order to ring the Wheatley-controlled bells at the 'right' times. |
| 22 | + |
| 23 | +## Code Map |
| 24 | + |
| 25 | +The bulk of the code resides in the `wheatley/` directory (which the only code is shipped to users). |
| 26 | +However, there are some other pieces of code useful during development which reside in the main |
| 27 | +directory: |
| 28 | + |
| 29 | +- `run-wheatley`: An executable Python script which will run the Wheatley code directly as though it |
| 30 | + were run through `pip`. |
| 31 | +- `tests/*`: Unit tests for various pieces of Wheatley's code. This only tests code in the |
| 32 | + `wheatley/` folder. |
| 33 | +- `doctests`: Python script which invokes all the examples found in `README.md`, asserting that they |
| 34 | + don't crash. This prevents the examples from getting out of sync with the code. |
| 35 | +- `fuzz`, `fuzzing/*`: Fuzzing for CLI argument parsers. These feed the parsing fuctions with |
| 36 | + thousands of randomly generated inputs, asserting that they must produce well-defined errors. |
| 37 | + |
| 38 | +### `wheatley/{aliases.py, bell.py, calls.py, stroke.py}` |
| 39 | + |
| 40 | +These files contain little or no business logic, and instead provide datatypes (`bell.py`, |
| 41 | +`stroke.py`), type aliases (`aliases.py`) and/or constants that are used extensively throughout the |
| 42 | +code. These serve two purposes: |
| 43 | +1. They increase safety by providing an abstraction layer over the raw numbers and strings used to |
| 44 | + communicate with Ringing Room. |
| 45 | +2. `bell.py` prevents any ambiguity between the many different representations of bell names (i.e. |
| 46 | + is the 12th called `'T'` (name), `12` (number) or `11` (index)?) - Wheatley uses one `Bell` class |
| 47 | + which provides unambiguous conversions to and from these representations. |
| 48 | + |
| 49 | +### `wheatley/bot.py` |
| 50 | + |
| 51 | +This is the glue code that holds Wheatley together. The `Bot` class is a singleton that gets |
| 52 | +created at start-up and mediates interactions between other parts of the code (row generation, |
| 53 | +rhythm, interacting with Ringing Room, etc). It also runs the `mainloop` - an infinite loop in |
| 54 | +which the main thread gets stuck until Wheatley stops. |
| 55 | + |
| 56 | +**Architectural Invariant**: None of the code in `row_generation/*`, `rhythm/*` or `tower.py` can |
| 57 | +talk directly to each other; instead they all provide an interface that the `Bot` class to mediate |
| 58 | +interactions. |
| 59 | + |
| 60 | +### `wheatley/rhythm/*` |
| 61 | + |
| 62 | +This is where the rhythm detection happens. The specification of a rhythm is defined in |
| 63 | +`abstract_rhythm.py`, and there are two `Rhythm` classes which implement different behaviours: |
| 64 | +- `regression.py` uses regression to draw a linear line of best fit through the user's datapoints |
| 65 | + and then uses this line to decide where Wheatley's bell should ring. |
| 66 | +- `wait_for_user.py` adds waiting for user-controlled bells on top of an existing `Rhythm` class. |
| 67 | + |
| 68 | +**Architectural Invariant**: The rhythm module should never access the real time directly - it |
| 69 | +should use the times passed to each individual method. This is because `WaitForUserRhythm` lies to |
| 70 | +its internal rhythm in order to stop Wheatley from jumping back to the original rhythm if someone |
| 71 | +holds up for a long time. |
| 72 | + |
| 73 | +### `wheatley/row_generation/*` |
| 74 | + |
| 75 | +This is the code that determines _what_ Wheatley rings, and reacts to the calls `Bob` and `Single`. |
| 76 | +`row_generator.py` specifies the interface of a `RowGenerator`, and each different source of rows |
| 77 | +(method, CompLib, dixonoid, etc.) has its own file and class. |
| 78 | + |
| 79 | +**Architectural Invariant**: The `row_generation` module does _not_ handle adding cover bells or |
| 80 | +responding to calls like `Go`, `That's All`, `Stand`. These are both handled by the `Bot` class, |
| 81 | +since both cover bells and state transitions are indepedent of what row generator is being used. |
| 82 | + |
| 83 | +### `wheatley/{main.py, parsing.py}` |
| 84 | + |
| 85 | +This is the start-up code for Wheatley. It gets called once and is tasked with parsing the user's |
| 86 | +input and then using this to generate `Rhythm`, `RowGenerator`, `Tower` and `Bot` singletons. |
| 87 | +Finally, it enters the `Bot`'s mainloop, which never returns. |
| 88 | + |
| 89 | +`parsing.py` also contains some code for interpreting the SocketIO signals which change the controls |
| 90 | +in the integrated version, but this will likely be moved somewhere else. |
| 91 | + |
| 92 | +Wheatley has 3 main functions: |
| 93 | +- `server_main`: The integrated Ringing Room version's main function |
| 94 | +- `console_main`: The CLI version's main function |
| 95 | +- `main`: The root main function, which delegates to one of the other two main functions depending |
| 96 | + on whether or not Wheatley is running on a Ringing Room server |
| 97 | + |
| 98 | +**Architectural Invariant**: This is the only place where different code is executed between the |
| 99 | +CLI and integrated versions. 90% of the differences between versions are implemented by |
| 100 | +disconnecting callbacks during initialisation. |
| 101 | + |
| 102 | +### `wheatley/{tower.py, page_parsing.py}` |
| 103 | + |
| 104 | +**NOTE: This code is soon going to be replaced with |
| 105 | +[belltower](https://github.com/kneasle/belltower), which can be used for other projects.** |
| 106 | + |
| 107 | +These files handle all the direct contact with Ringing Room, and provide an abstraction barrier |
| 108 | +between the rest of the code and the internal workings of Ringing Room. The `Tower` class in |
| 109 | +`tower.py` handles run-time connections to Ringing Room, whereas `page_parsing.py` is used during |
| 110 | +start-up to parse information out of the HTML source of the Ringing Room pages. |
| 111 | + |
| 112 | +This abstraction layer means two things: |
| 113 | +1. 90% of Wheatley is completely platform indepedent - supporting a new platform (other than Ringing |
| 114 | + Room) would only require creating a new `Tower` class. |
| 115 | +2. If Ringing Room changes its API in any way, the corresponding changes to Wheatley are limited to |
| 116 | + just these files. |
| 117 | + |
| 118 | +**Architectural Invariant**: Every interaction with Ringing Room is handled through these modules - |
| 119 | +the rest of the code does't even know it's talking to Ringing Room. |
0 commit comments