Cycleboard
Cycleboard is a Live Workout Virtual Leaderboard that allows you to compete with other class members in real-time. it is split into two screens, the GAME screen and the TRAINER screen. there is a backend application written in python using the Flask framework and a frontend application written in Svelte (4) with skeleton CSS. the front and back end primarily communicate over a websocket connection, though a REST API is also used for some transactional operations.
Backend
Object Synchonization
the backend uses a shared object, synchronized over websockets between the server and all clients to store the the majority of the game state. the object is a dictionary within the object_socket module, changes made by the clients are sent to the server imediately. every 100ms the server sends a diff of the state to all clients, this diff is then applied to the client's local state. changes are not sent immediately to reduce the number of messages sent over the websocket connection to a manageable level. to prevent desyncronization, the server sends the full state to all clients every 10 seconds.
due to the fact that their can be multiple clients making changes, and their is latency between the server and the clients, their is no strong garuntee that state changes by any given client will be applied, for changes which must be applied, clients should use the send_message method in the object_socket module, which will send a message to the server through a side channel, which will then trigger a funciton on the server. this methodology is not used for the majority of the game state, as it requires each message function to be implemented on the server. for most actions, hard garuntees like this are not required, so the diff method is used.
Game Module
the game module contains the game logic for the cycleboard application, its responsibilities include: - starting, stopping, and pausing the session - handling rolling averages for all stats - calculating the class totals for all stats - calculating rider positions - recording a log of all rider stats - dropping riders who have not sent a message in the last 10 seconds
to do this it has 2 main public methods, init and get_game_data.
init
the init method is called when the application is started, it starts a thread to run the game loop. the game loop is responsible for the averaging and calculations above, and is run every 100ms.
averaging
when calculating the rolling averages, the game adds 1/10th of the new value to 9/10ths of the old value. this aproximates a Expontential Moving Average over 10 seconds, but without needing to store all legacy data.
class totals
the class totals for each stat is simply the sum of all the riders who are in game, whether a rider is in game is part of get_game_data (see below).
recording a log
each riders current instantanious stats are appended to a list in the ftp_data dictionary every 100ms when the game loop runs. this data is not part of the game state, as it gets very large very quickly. to access this data rest API endpoints are provided (see below).
get_game_data
the get_game_data method is called whenever the data for a rider is updated from another source. it looks at the new data for that rider, and calculates the new position, and status of the rider. a rider is considered in game if they have sent a message in the last 10 seconds, have set an FTP, and have picked a username (even the default one). it then appends its results to the rider data as a field called 'game'.
Frontend
Common Components
api.ts
provides the websocket connection, and rest API connection to the backend. provides 1 object, state, which is a svelte store that contains the current state of the application, and is synchronized with the backend. provides 3 functions: - send_message, which sends a message to the backend over the websocket connection for strong transactional garuntees. - api_get, which sends a get request to the backend over the rest API. - api_post, which sends a post request to the backend over the rest API.
leaderboard
Leaderboard.svelte
this is the common leaderboard component, it is used in both the GAME and TRAINER screens. it handles creating a div to contain the rider elements, creating a header with the stat names, and then creating the rider elements in a loop. it has a 3 props, riders, which is a list of rider data to display, admin, which is a boolean to determine if the TRAINER mode should be available on each rider, and flipped, which is a boolean to determine if the leaderboard should be flipped horizontaly (used in the GAME screen for the right hand side).
Rider.svelte
this is the common rider component used in the leaderboard component. it provides a div to contain the riders name, position, and stats. each rider element also has an onclick event to switch it to a TRAINER input, which is a different mode of the same component. when in TRAINER mode, the rider displays stats not shown publicly in GAME mode (FTP and Resistance), it also provides a slider to change the riders resistance.
each rider element is implemented as a grid, with the stats split into collumns.
GAME Screen
the GAME screen is where the user can see the leaderboard and their position in the class. the leaderboard is updated in real-time over websocket connection.