A game of TicTacToe as webapp. Two users load the client-side code (HTML, CSS, Javascript) from the server and can then play against each other. The game state is managed on the server, the client (browser) merely displays the state received from the server, and can issue commands to place a piece in a free cell.
The game consists of two systems: the server, and the client (web-browser).
The loads static client-side resources (HTML, CSS, Javascript) from the server. The Javascript then uses asynchronous fetch
to connect to the JSON
endpoints defined by the server, in order to join a game, to view it's state, and to take turns in setting pieces.
The server-side state of a game can change without a user event, e.g. because the other player set a piece. In order to update the client's
view of the game, it continuously polls the server for the current game state using the /view
endpoint.
The server has two roles:
- It serves the static resources needed by the client (HTML, CSS, Javascript).
- It responds to queries to its JSON endpoints by returning the JSON data corresponding to the underlying game.
The server is implemented in Python, on top of the Flask web app server.
There are two main parts in the Python code:
- The logic for a single game of TicTacToe is defined in
tictactoe.py
. app.py
defines the endpoints supported by the webapp- The root (
/
) URL is redirected to/static/tictactoe.html
- Flask automagically serves any files from the
/static
folder as static resources. - The three JSON endpoints are defined using
@app.route
annotations.
- The root (
The diagram below shows the sequence of events during the initial loading of the game (static resources), and during the game play (JSON fetched via asynchronous javascript fetch).
Click for an interactive version that allows expansion of the various steps.
When the user navigates to the root of the web server, the following happens:
- The browser loads the web page:
- The browser requests the root resource using HTTP (
GET /
). - The server sends a HTTP redirect, telling the browser to go to
/static/tictactoe.html
instead. - The browser requests that resource (
GET /static/tictactoe.html
) - The browser parses the received HTML and notices that there are more files to be requested. It requests the static CSS and JS files from the server.
- The browser assembles the webpage and renders it to the user.
- At the same time, the Javascript is loaded and starts executing in the background
- The browser requests the root resource using HTTP (
- Background joining of a game:
- The background JS uses
fetch
to load the/join
resource. - The server finds or creates a game waiting for players and returns its information as JSON data.
- The JSON data is parsed in the background thread and the HTML is updated accordingly
- The background JS uses
- When the user clicks a cell
- The background JS uses
fetch
to load the/set/<game_id>/<player>/<cell>
resource. - The server checks the requests and if OK modifies the gamestate accordingly, returning JSON data for the updated game.
- The JSON data is parsed in the background thread and the HTML is updated accordingly
- The background JS uses
Since every machine with access to the server can request resources, the server should protect itself against unauthorized requests.
The following may easily be checked for in the game logic:
- is it the user's turn?
- is the cell free at all?
- is the game still playing?
But even more important is the question whether the request is even coming from the right user.
- Otherwise, a malicious user could issue a bad request on behalf of its competitor, resulting in a favorable game situation.
Checking for whether the user is really who they claim is not easy. We could verify if the request comes from the same IP address as the initial /join
request, but that can be forged. Also, this would not work if all clients reside behind the same NAT router, as they would all appear to come from the same address.
The canonical answer to this problem is to use session cookies. In the response to the /join
request, a small piece of data (cookie) is sent back to the client, and the client is asked to include that cookie in each of its subsequent requests.
In order to not allow a malicious user to forge the cookie of another player, the cookie contents are encrypted.
Flask provides sessions, which are built on top of cookies, for this purpose.