Point Game
What is Point Game?
Poker's structure, blackjack's scoring, with a two-sided pot.
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.
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.
Discard on the board
As community cards appear, any private card matching the board is discarded face-up.
Split pots & side pots
The pot divides between the high and low sides, across side pots when players are all-in for different amounts.
One player action
Click
Client validates locally for UX, then sends a WebSocket message.
Route & auth
API Gateway routes to the game Lambda; the connection is identified by its connectionID.
Validate
Engine checks the action against authoritative state (right player, legal amount, etc.)
Apply & Log
New state written with a conditional write on the sequence number.
Schedule Timers
Schedule an Event Bridge to act as timer for moving the game long.
Broadcast
A privacy-filtered view is fanned out to every seat at the table.
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.
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.
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
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.
(additive)
Cost: N filtered payloads per action. Cost can be cut in half by sending action along with game state in one broadcast.
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
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.
WebSocket messages can easily be cut in half if we include more front end lifting.
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.
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.
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.
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.