REAL-TIME MULTIPLAYER CARD GAME · SERVERLESS ON AWS

Point Game

Point Game logo — a shield with playing cards and poker chips
POINT GAME01
THE GAME

What is Point Game?

Poker's structure, blackjack's scoring, with a two-sided pot.

1

Blackjack-style values

Each card scores a point value: Ace is 1 or 11, face cards 10, the rest at rank. The value of your hand is the sum.

2

High and low, at once

The strongest high total and the weakest low total each win. Players secretly declare High, Low, or Both before the reveal. To win both you must win or tie the high and low.

3

Discard on the board

As community cards appear, any private card matching the board is discarded face-up.

4

Split pots & side pots

The pot divides between the high and low sides, across side pots when players are all-in for different amounts.

POINT GAME02
SYSTEM DESIGN
Architecture: Client to CloudFront/S3, Cognito, API Gateway (REST + WebSocket) to Web Lambda and Game Lambda to DynamoDB
Design principle:  every action is a stateless cycle. read state → validate → apply → persist → broadcast. Nothing lives in memory
POINT GAME03
HOT PATH

One player action

1

Click

Client validates locally for UX, then sends a WebSocket message.

2

Route & auth

API Gateway routes to the game Lambda; the connection is identified by its connectionID.

3

Validate

Engine checks the action against authoritative state (right player, legal amount, etc.)

4

Apply & Log

New state written with a conditional write on the sequence number.

5

Schedule Timers

Schedule an Event Bridge to act as timer for moving the game long.

6

Broadcast

A privacy-filtered view is fanned out to every seat at the table.

Steps 4-6 are three separate chunks. If the Lambda dies between them, the system must be able to self heal. This can be done with client resyncs, outbox + atomic writes, or a broadcasting sweeper.
POINT GAME04
TOPICS

Five interesting topics

Concurrency

Sequence-versioned optimistic writes keep two racing actions from ever corrupting a pot.

Turn timers

Scheduled events fire and ensure stale events are dropped.

Private views

State is filtered per recipient so no hole card or declaration ever leaves the server until showdown.

Inter-round queue

Joins, leaves, and config changes wait in a queue and flush cleanly between hands.

Showdown

Side pots, split pots, and multiple winners create a very complicated showdown algorithm.

POINT GAME05
DEEP DIVE · CONCURRENCY

Optimistic concurrency, no locks

Every state write is a compare-and-set on a version number. Two actions read version N. Both try to write expecting N. DynamoDB serializes writes to the item, so exactly one succeeds and bumps to N+1 then the other is rejected and its client resyncs.

No stuck tables

A crashed lock-holder could freeze a hand.

Contention is very unlikely

Poker has one legal actor at a time; OCC really guards player-vs-timer races.

One-line mechanism

Concurrency control is a single conditional expression, easy in DynamoDB.

EXAMPLE
Player A · reads v5
Player B · reads v5
DynamoDB · write if gameSeq = 5
A wins
gameSeq → 6
B rejected
Stale gameSeq → resync
POINT GAME06
DEEP DIVE · TIMERS

All timers fire

Lambda can't wait, so the turn clock lives in EventBridge and is verified when it fires. timerSeq != gameSeq due to other game state updates.

Turn begins

tag state timerSeq = 7

Schedule EventBridge event

fires in 31s · payload timerSeq 7

Event fires · Lambda compares payload to state

current timerSeq still 7?

MATCH — player has not acted

force the c/f through the same game lambda

MISMATCH – player already acted

do nothing

Why not cancel the timer when a player acts? Cancellation is its own race and adds an API call to every action. Letting stale events fire and do nothing is simple, cheap, and always correct.
POINT GAME07
DEEP DIVE · PRIVACY

Filter game state per player

Anything sent to a client is public. So the server creates a public state then adds private information per recipient before it ever hits the wire.

AUTHORITATIVE STATE
Deck order
A's hole cards
B's hole cards
C's hole cards
All declarations
Pot & board
Privacy
filter
(additive)
Player A sees
Own cards
B, C cards hidden
Declarations hidden
Board cards
Player B sees
Own cards
A, C cards hidden
Declarations hidden
Board cards

Cost: N filtered payloads per action. Cost can be cut in half by sending action along with game state in one broadcast.

POINT GAME08
DATA MODEL

Nine Tables

Single-table design optimizes for join-shaped access, but this system never has reads in that way. Separating out into many tables makes design and implementation simpler.

Game State

PK tableID · one item

Action Log

PK handID · SK seq

Hand Snapshots

replay anchors

Connection Store

connID ↔ player + GSI

Turn Timers

scheduled deadlines

Inter-Round Queue

PK tableID · SK seq

Users

identity + GSI

Ledger

chip movements

Game Tables

config & status

If we wanted to move towards a single table design, all tables in blue could be combined using the tableID as the PK, and using the SK to classify

POINT GAME09
COST

Cheapest exactly where it lives

At scale, DBs generally push the cost for serverless design. Here, every action costs only a small read and write, which stays cheap even at scale because of the turn-based nature. The real driver is WebSocket messages: one player action fans out to every seat at the table.

$0
idle cost per table
1
conditional write = concurrency
9
purpose-built tables
EST. MONTHLY COST · 1,000 CONCURRENT PLAYERS
WebSocket msgs$1,370
DynamoDB writes$850
Lambda$250
Connections$11

WebSocket messages can easily be cut in half if we include more front end lifting.

POINT GAME10
CLOSING

Defined as much by the roads not taken

Although each alternative below has strengths, they were rejected for specific reasons.

Stateful game server

State in memory, single-writer, trivial timers.

WHY NOT

Rejected: idle cost, plus deploys and crash recovery is hard, especially to not lose live hands.

SQS FIFO per table

Serializes every write, zero conflicts, ever.

WHY NOT

Rejected: adds queue latency and is favored for lots of throughput. More complex without the need for it

Single-table design

The typical DynamoDB pattern with fewer tables.

WHY NOT

Rejected: it pays off only for join-shaped access this system never has.

Serverless.  Every piece is right-sized to the game. Stateless compute because actions are seconds apart, a key-value store because access is always for a key, scheduled events because turns just need an okay clock. The result costs almost nothing at rest and stays simple under load.

POINT GAMEpointgame.live   11