← Back to game

How This Game Works

A peek behind the curtain

The Big Picture

This tic-tac-toe game lets two people play each other from different devices, anywhere in the world. But here's the surprising part: there's no traditional server running this game. There's no computer sitting in a data center humming away 24/7, waiting for you to make a move.

Instead, the game uses something called serverless architecture, a design where small pieces of code spring to life only when needed, handle one task, and then vanish.

Think of it this way

A traditional server is like hiring a team of on-call chauffeurs 24/7 to drive you on one trip per month. Serverless is like calling an Uber. You only pay for the rides you actually take, and for a small game like this, those rides add up to essentially nothing.

The Architecture

There are three main parts to this system. Here's how they connect:

How the pieces fit together
📱 Your Phone the website
💻 Friend's Laptop the website
asks "anything new?"
🌎 CloudFront content delivery
serves the page
📦 S3 Bucket stores the webpage
API Gateway the front door
wakes up the right worker
λ Lambda create game
λ Lambda make move
λ Lambda join game
reads & writes
🗃 DynamoDB the database

Let's walk through each layer.

1. The Website (What You See)

The entire game (the board, the buttons, the animations) is a single HTML file. No app to download, no framework, no build tools. Just one file of about 2,300 lines that your browser reads and turns into the game you see. It's stored in an S3 bucket, which is basically Amazon's version of a file folder in the cloud.

CloudFront is a network of servers around the world that keeps copies of this file close to you geographically, so the page loads fast whether you're in Tokyo or Toronto. When you visit the site, you're probably loading it from a server just a few hundred miles away.

2. The API (The Messenger)

When you create a game, make a move, or join a room, your browser sends a message to the API Gateway. Think of it as a receptionist at a hotel. It receives your request, figures out what kind of help you need, and routes you to the right person.

The API has six "doors" you can knock on:

The six API endpoints

create Start a new game and get a room code
join Enter someone else's room
move Place your X or O
get game Check the current state of the board
list games Browse open or live games
rematch Ask for a rematch after someone wins

3. The Workers (Lambda Functions)

Behind each of those doors is a small Lambda function, a tiny piece of code that exists only to do one specific job. When you make a move, a Lambda function wakes up, validates your move, checks if someone won, saves the result, and goes back to sleep. This entire process takes about 50–200 milliseconds, less time than it takes to blink.

Think of it this way

Lambda functions are like light bulbs. They only use electricity when you flip the switch, and they turn off the instant you're done. You don't pay for a light bulb when it's off.

4. The Database (DynamoDB)

Every game's state (whose turn it is, where all the X's and O's are, the score, the room code) lives in DynamoDB, Amazon's database. It's incredibly fast (single-digit millisecond responses) and, like the rest of this system, you only pay for what you actually use.

The Journey of a Move

Here's exactly what happens when you tap a square:

You tap a square

Your browser instantly shows your mark on the board (it's optimistic and assumes the move will work). At the same time, it sends a message to the API: "Player James places an X at column 2, row 1."

API Gateway receives the message

The request arrives at Amazon's front door. The Gateway checks that it's a valid request and wakes up the "make move" Lambda function.

Lambda validates the move

The function loads the game from the database and checks three things: Is it actually your turn? Is the square empty? Is the move within bounds? If anything's wrong, it rejects the move.

Lambda checks for a winner

The function scans four directions from your move (horizontal, vertical, and both diagonals) counting consecutive marks. If it finds enough in a row, you win. If the board is full, it's a draw.

The database is updated

The new board state is written to DynamoDB. This uses a clever trick called optimistic locking. It only saves if nobody else has moved since the function started working. This prevents the impossible situation where two people move simultaneously.

Your opponent sees the move

Your opponent's browser has been asking "anything new?" every 2 seconds. On the next check, it sees the new move and draws your mark on their screen. The whole round-trip, from your tap to their screen, takes about 2–4 seconds.

How Two Players Stay In Sync

There's no live, always-open connection between the two players. Instead, the game uses polling, a simple technique where each player's browser repeatedly asks the server, "Has anything changed?"

Polling, the heartbeat of the game
📱 Player A
any news?
🗃 Database
any news?
💻 Player B
every 2 seconds

This is simpler (and cheaper) than keeping a persistent live connection. The tradeoff is a small delay: your opponent sees your move within 2–4 seconds rather than instantly. For tic-tac-toe, that's perfectly fine.

Inside the Database

Every game is a single record in the database. Here's roughly what one looks like:

room clever-otter-a3f2b1
status active
board_size 5
current_turn O
player_x James
player_o Alex
x_positions [[2,3], [1,1], [4,0]]
o_positions [[2,2], [3,1]]
moves 5
score X:1 O:0 Draws:0
...and a few more fields

The room code (like clever-otter-a3f2b1) is generated by combining a random adjective, a random animal, and a short string of characters. It's designed to be easy to say out loud or text to a friend, unlike a random string of numbers and letters.

The database also keeps separate index records that make it fast to look up "all games that are waiting for a player" or "all games currently being played" without scanning every game ever created.

Room Codes & Sharing

When you create a game, you get a shareable link with your room code baked in. When someone opens that link, the game figures out what to do:

If you're one of the two players (the site remembers), you're dropped right back into the game. If you're anyone else, you become a spectator and can watch the game unfold in real time. And if the game is still waiting for an opponent, you'll be prompted to join.

This means the same link serves three different purposes, so there's no need for separate "play" and "watch" URLs.

Why This Costs (Almost) Nothing to Run

This is the fun part. Every piece of this system is billed on a pay-per-use model. Here's what that looks like for a small game:

Service What it does Cost
S3 Stores the single webpage file ~$0.01/mo
CloudFront Delivers it fast worldwide free tier
API Gateway Receives player requests free tier
Lambda Runs the game logic free tier
DynamoDB Stores all game data ~$0.01/mo
1K games All services combined, after free tier ~$0.11
1K games If we switched to WebSockets (~3× cheaper) ~$0.04
Total: a few pennies per month

AWS gives you a generous free tier: 1 million Lambda requests per month, 1 million API calls, 25 GB of database storage, and 1 TB of content delivery. A game of tic-tac-toe uses about 20–30 API calls (including polling). You'd need tens of thousands of games per month to even start getting billed.

Most of that cost comes from polling, each player's browser asking "anything new?" every 2 seconds. A future upgrade to WebSockets would eliminate that overhead entirely, since the server would push updates only when a move actually happens. That would cut the cost per thousand games from about eleven cents to about four.

Put another way

Running a traditional server for this game would be like renting a whole apartment just to store a single book. Serverless is like paying a tiny fee each time someone reads a page, and the first million page-reads are free.

A Few More Fun Details

Infinite mode

Beyond the usual 3×3 to 12×12 boards, there's an infinite grid mode. The board extends forever in all directions, and you need five in a row to win. The database doesn't store a grid, just a simple list of coordinates, so infinite mode barely uses any more memory than a tiny 3×3 game.

Preventing cheating

Every move is validated on the server. Even if someone modified their browser's code to try to place a mark out of turn or on an occupied square, the Lambda function would reject it. The database also uses "optimistic locking" so two simultaneous moves can't both succeed. Exactly one will go through, and the other gets rejected.

Session recovery

If you accidentally close the tab or refresh the page, the game remembers you. Your name, room code, and which side you're playing are saved in your browser. When you come back, you're dropped right back into the game with no re-joining needed.

The whole thing is one file

The entire front-end (all the styling, layout, game logic, animations, and online play code) lives in a single HTML file. No build step, no bundler, no node_modules. It's deployed by uploading that one file to Amazon's cloud storage.

Infrastructure as code

All the cloud resources (the database, the Lambda functions, the API, the storage bucket, the CDN) are defined in code using a tool called Terraform. This means the entire infrastructure can be recreated from scratch with a single command. Nothing is configured by hand.

The Full Stack at a Glance

front-end Vanilla HTML, CSS & JavaScript, no frameworks
hosting Amazon S3 + CloudFront CDN
api Amazon API Gateway (REST)
logic 6 AWS Lambda functions (Python)
database Amazon DynamoDB (NoSQL)
infrastructure Terraform + Terragrunt
sync HTTP polling every 2 seconds