Skip to content

Instantly share code, notes, and snippets.

@psterckx
Last active July 29, 2023 13:08
Show Gist options
  • Save psterckx/b40040d5abe9e218ebd6849527389968 to your computer and use it in GitHub Desktop.
Save psterckx/b40040d5abe9e218ebd6849527389968 to your computer and use it in GitHub Desktop.
Online Multiplayer Tic Tac Toe

Online Multiplayer Tic Tac Toe ๐ŸŽฎ

Frontend Repo - https://github.com/psterckx/tictactoe-frontend
Backend Repo - https://github.com/psterckx/tictactoe-backend

How to play

Open this link https://tictactoe.psterckx.be in two separate tabs (you can also open the link on two different devices or ask a friend to play with you).

Click Start game in both tabs. Both clients will establish connections to the backend and request a game. Then the clients will be matched with each other and a game will start.

Alternate between the tabs and try to beat yourself at tic tac toe. ๐Ÿ˜‰

Once the game is over, you can either leave or click Play again in both tabs to request another game.

What is this?

An online multiplayer tic tac toe game.

But why??

I wanted to explore building an online multiplayer game that leveraged websockets. I'm also interested in serverless and I wanted to see how you could build a websocket API with AWS API Gateway and AWS Lambda.

How does it work?

This project uses websockets to create the real-time multiplayer game experience. Websockets are ideal for multiplayer online games since they allow the both the client and the server to send messages to each other, whereas with the HTTP protocol, client must request the information from the backend. Websockets also provide lower latency messaging than HTTP, which is another reason to use it for online games and other real-time web applications.

The backend uses a few components to coordinate the tic tac toe game.

  • API Gateway WebSocket API - allows clients to connect to our API and invokes our lambda functions (more about this in a moment)
  • DynamoDB table - stores the connections and games
  • SQS queue - used for matching clients
  • Lambda functions - connection and game management logic

Let's walkthrough what happens behind the scenes during a game.

When Start game is clicked, the client attempts to open the websocket connection to the server. However, since our tech stack is entirely serverless, how is this accomplished?

AWS API Gateway provides a WebSocket API that allows you to invoke Lambda functions based on incoming messages from a client. API Gateway determines which lambda function to invoke based on the value of the specified route key of the JSON payload. The route key is determined by evaluating the route selection expression. For example, if the route selection expression is ${request.body.action} and the client sends the payload {"action": "requestGame"}, API Gateway will invoke the Lambda function corresponding to the requestGame route.

There are also three predefined routes provided: $connect, $disconnect, and $default. So when our client clicks Start game in the UI, the client connects to the websocket API and API Gateway calls the $connect route which invokes the Lambda function configured for that route.

We could create a lambda function for each of our routes, but to keep this project simple, I just created two lambda functions. One function, the ws-handler handles most of the routes - clients connecting, client or server disconnecting, and in-game events (for example, when a player marks a square in our tic tac toe game). The second lambda function, called matcher handles the requestGame route. This lambda function matches two players together and starts new a game.

To keep things clear, I'll refer to messages from client to server as actions and messages from server to client as events. Here is the list of actions with the corresponding routes and lambda functions.

API Gateway Websocket Routes ("actions")

Route Lambda function Description
$connect ws-handler Client connects to the WebSocket API.
$disconnect ws-handler Client or server disconnects from the WebSocket API.
markSquare ws-handler Client marks a square on the board.
requestGame matcher Client requests a new game.

So back to our walk through... the client connects to our websocket API and API Gateway invokes the ws-handler lambda function. The lambda function persists the connect details in the our DynamoDB tictactoe-table table. We can't store the connection details in-memory because if and when the lambda environment is shutdown, all the in-memory data will be lost.

Once the connection is established, the client sends a requestGame action over the websocket connection. API Gateway then invokes the matcher lambda function associated with our requestGame route. The matcher logic is pretty simple - it tries to retrieve an existing connection from our SQS queue. If it finds one, then it matches the two connections, deletes the connections from the queue, and starts a game by creating the game in our DynamoDB table and broadcasting the START_GAME message to both clients. It also picks one of the two connections and sends the BEGIN_TURN message to that client.

If the function does not find an existing connection in the SQS queue, then it adds the connection who requested the game to the queue and sends the event WAITING_FOR_GAME to that client.

Let's say another client has connected and we've started a game. Now the logic is pretty simple. We wait for player 1 to mark a square, check for the win/draw condition, then if the win/draw condition is not met, we request player 2 to mark a square. Whenever a player marks their square, we also broadcast the state of the game to both players so the clients can update their UIs. Using our events and actions, the logic would flow like this.

  1. Server sends event START_GAME to both players (start)
  2. Server sends event BEGIN_TURN to player 1 or 2 (alternates)
  3. Client sends action markSquare from player 1 or 2 (alternates)
  4. Check for win/draw
    • If win/draw condition is not met
      • Update game state in the table
      • Server sends event GAME_UPDATED to both players
      • Go to step 2, alternating the player
    • If win/draw condition is met
      • Update game state in the table
      • Server sends event WIN, LOSE, or DRAW to each player depending on outcome (end)

So for this simple game, the only events we send to the client are BEGIN_TURN, GAME_UPDATED, WIN, LOSE, and DRAW. There is actually one more event that let's us handle what happens when a player disconnects during a game, but I'll leave that and some other implementation details for you to discover in the code. ๐Ÿ˜„

Once the game is over, clicking the Play again button in the UI simply sends the requestGame action to the server, and the process can repeat!

Tech stack

Frontend

  • Vue 3 (the best frontend framework)
  • Tailwindcss (a CSS utility framework that makes styling a breeze)

Backend

Considerations and Learnings

More complex games will have a lot more actions and events. This tic tac toe game has a limited number of actions and events, which makes it pretty easy to manage. But you can imagine with a more complex game, say a board game like Catan, the number of actions and events would increase quickly. You'll need to keep your code organized and standardized to manage all the types of actions and events and the corresponding backend and client-side logic.

The game is a state machine. I was wondering if you could use AWS Step Functions to manage the state of the game and use their wait for callback with task token pattern to wait for messages from a client. You can actually integrate your WebSocket API routes with other AWS services besides lambda, so you could invoke an SNS topic that tells your Step Function to continue the execution. This architecture would probably involve more lambda functions and the latency would probably be higher, so this would be ideal for games that don't require fast response times like board games. However, using Step Function as your state machine could help separate components and reduce the amount of code, especially for more complex games/applications.

Are lambda functions the best choice for real-time applications using websockets? If you're building a real-time, websocket-based application and you want to optimize for performance, lambda functions probably aren't the best choice. Real-time applications using websockets usually send lots of requests back and forth between the client and server, so at some volume of traffic you'll probably have little "down time" where your lambda functions aren't being invoked and you lose the "only pay for what you use" advantage of lambda functions. With high enough volumes of traffic, the cost of the lambda function invocations could be greater than if you were to use EC2 instances or a containerized solution. Also, a large EC2 instance or a container running on a large EC2 instance would be able to provide lower latency than lambda functions. However, for smaller projects or applications where latency is of lower importance, lambda functions could be the right choice.

Thanks for reading!

If you liked this, check out my other projects at petersterckx.com/projects.

@supermarsx
Copy link

ok game, 4 in line would be way more interesting, tic tac toe is pretty limited in terms of outcomes. Cheers for the effort, looks good and well made.

@psterckx
Copy link
Author

psterckx commented Jun 4, 2022

ok game, 4 in line would be way more interesting, tic tac toe is pretty limited in terms of outcomes. Cheers for the effort, looks good and well made.

Thanks! ๐Ÿ˜„ Yes, tic tac toe is quite simple but I wanted to focus on the backend architecture more than the game logic for this project. I'll probably try out a more complex game at some point.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment