Player accounts: 100k, growing by 20% per year
Simultaneous games: 5k, growing by 20% per year
Daily chat messages: Average 20 messages per game, for 100k messages per day, growing by 20% per year
Storage for account info 100k times 100KB (include name, email, account ID, and image) -> 10GB, growing by 20% per year
Storage for games increases by 5k times 1KB (include each move and chat message ID) -> 5MB per day
Storage for chat messages increases by 100k times 100B -> 10MB per day
POST /api/v1.0/player
{"name": string, "email": string, "user_tag": string}
If all fields a valid and user_tag isn't already taken:
200 {"player_id": uuid, "name": string, "email": string, "user_tag": string}
Else: 400 Bad Request
GET /api/v1.0/player/{player_id}
200 {"player_id": uuid, "name": string, "email": string, "user_tag": string, ... (player stats)}
POST /api/v1.0/player-image/{user_id}
Content-Type: {MIME type for image}
{binary data for image}
If MIME type is valid image type and binary data is valid for the MIME type:
200 {"player_id": uuid, "image_size": 123}
Else: 400 Bad Request
GET /api/v1.0/ws/{user_id}
If there is an existing web socket connection for player_id, return that. Otherwise, set up a new web socket connection and return that.
---
All communications with the Web Socket connection will be with JSON objects, allowing for flexibility and extensibility in operations
Web Socket Operations for starting a game
Receive: {"action": "start_game", "game_id": uuid, "player_color": enum, "opponent_id": uuid}
Web Socket Operations for turn management
Receive: {"action": "player_turn", "active_player": uuid}
Web Socket Operations for making moves
Send: {"action": "make_move", "game_id": uuid, "old_position": string, "new_position": string}
Attempt to make the given move. If the positions refer to players King and the King is in the 1st rank and the move is more than 1 space along the 1st rank, it is considered to be a castling
If the move is valid:
Receive: {"action": "accept_move", "game_id": uuid, "player_id", {player_id}, "old_position": string, "new_position": string}
Else:
Receive: {"action": "reject_move", "game_id": uuid, "old_position": string, "new_position": string, "explanation": string}
Web Socket Operations for resigning
Send: {"action": "resign", "game_id": uuid}
Receive: {"action": "game_ended", "game_id": uuid, "winner": {opponent_id}, "explanation": "resigned"}
Web Socket Operations for proposing draw
Send: {"action": "propose_draw", "game_id": uuid}
If opponent rejects draw
Receive: {"action": "opponent_rejected_draw", "game_id": uuid}
else
Receive {"action": "opponent_accepted_draw", "game_id": uuid}
Receive: {"action": "game_ended", "game_id": uuid, "winner": None, "explanation": "draw"}
Web Socket Operations for opponent proposing draw
Receive {"action": "opponent_proposed_draw", "game_id": uuid}
If we accept:
Send: {"action": "accept_draw", "game_id": uuid}
Else:
Send {"action": "reject_draw", "game_id": uuid}
Web Socket Operations for game clock
Receive: {"action": "update_clocks", "game_id": uuid, "player_clock": int, "opponent_clock": int}
If player's clock runs out
Receive: {"action": "game_ended", "game_id": uuid, "winner": {opponent_id}, "explanation": "Clock expired"}
If opponent's clock runs out
Receive: {"action": "game_ended", "game_id": uuid, "winner": {player_id}, "explanation": "Clock expired"}
Web Socket Operations for other game ending scenarios
Receive: {"action": "game_ended", "game_id": uuid, "winner": {player_id}, "explanation": "Check Mate"}
Web Socket Operations for recovering state of game
Send: {"action": "recover_game_state", "game_id": uuid}
Receive: {"action": "receive_game_state", "game_id": uuid, "state": string}
The string holds the position of each piece on the board and the active player
We will have a SQL Database (MySQL) that holds player and game information
table Players
player_id: uuid -- Primary Key
username: string UNIQUE -- Index
email: string
rank: int
table Games
game_id: uuid -- Primary Key
player_1: uuid -- Index
player_2: uuid -- Index
player_1_clock: int
player_2_clock: int
player_1_web_socket: socket_connection_path
player_2_web_socket: socket_connection_path
board_layout: string (representation of positions of all pieces on the board)
outcome: enum
We will have a NoSQL Database (Cassandra) that holds all game activity (moves, chat, draw invitations)
table Moves
game_id: uuid -- Primary Key
time: timestamp -- Index
action: enum (move, chat, etc.)
start_position: string (relevant for standard chess moves)
end_position: string (relevant for standard chess moves)
message: string
We have an API Gateway that serves as a Load Balancer between service nodes and as a Rate Limiter (to defend against bad actors)
There is a Players Service that handles creation and modification of player details
There is a Connections Service that serves Web Socket Connections when a player wishes to play a game. These web sockets handle all communication for starting a game, making moves, sending chat messages, and ending the game
There is a Games Service that responds to incoming Web Socket Traffic (accepting game invites, making moves, sending chat messages) and routes outgoing Web Socket Traffic. When a game is started, it creates an entry in the Games table in MySQL. As moves are made, the Games Service updates the Games table with remaining clock, player turn, and board state. The Games Service also determines when a game has been won or drawn.
The Matchmaker Service attempts to find a suitable opponent for a player requesting a game, using player rank--and the requested game characteristics (e.g. clock approach)--to find a good match
When a player attempts a move, it is validated with the Move Validation Service (which holds the logic for legal chess moves)
There are caches in front of both databases to improve performance
Player Move Flow:
When a Player attempts to make a move, the following sequence occurs:
Player Chat Flow:
When a Player sends a text chat to their opponent, the following sequence occurs:
The Move Validation Service is the game rules master for the system. It takes a board layout, game context, and proposed move as input and then determines whether this is a legal move--and whether it ends the game. It uses an existing games library that provides that logic.
We choose an API Gateway to provide redundancy across service (via its Load Balancing capabilities) and to protect against DoS attacks and other misuse (via its Rate Limiter capabilities)
We use a SQL DB (MySQL) for the player and game info to ensure that access to these persistent components is transactional
We use a NoSQL DB (Cassandra) for move activity to support sharding and rapid updating
We use caches to improve DB performance
Web Sockets are used for interactive game play responsiveness
The biggest failure scenario is connectivity between the players and the Games Service. This is especially important because games are long running and could potentially take multiple days to complete. If a connection is lost, the Player can refresh their connection through the Connections Service, receiving a new web socket connection (which will also be updated in the Games table in SQL). The player will also be able to restore their local game state through the Games Service.