Developers

A game server you can reason about

Puzzle Crash runs on a server-authoritative HTTP API. The same pure game engine runs on the server (to decide) and in the client (to animate). This page is an overview; the full reference lives in the repository.

Architecture

Engine

Pure TypeScript, deterministic, no I/O. Emits semantic events (PIECE_PLACED, LINES_CLEARED, LEVEL_COMPLETED, CASHOUT…) that drive the client’s effects.

Server

Fastify, PostgreSQL and Redis. Each action runs in one transaction under a session lock: validate → apply → ledger → append history.

Client

Sends intents with the expected version and an idempotency key; renders the authoritative state and events it gets back.

Conventions

Auth
Authorization: Bearer <token> from POST /v1/players (demo identity)
Idempotency
Every state-changing call carries Idempotency-Key. A retry with the same key returns the original result and never applies twice.
Concurrency
Actions carry expectedVersion. A stale version returns 409 VERSION_CONFLICT with the current session.
Validation
Strict JSON schemas; unknown fields, wrong types or out-of-range values are rejected. No type coercion.
Rate limits
Per player (else per IP), Redis-backed; 429 RATE_LIMITED.

Endpoints

MethodPathPurpose
POST/v1/playersCreate a demo player with starting demo credits
GET/v1/meBalance and any pending session notice
POST/v1/sessionsStart a round: debit stake, commit server seed, deal hand 0
GET/v1/sessions/activeRecover the live round after a reload or crash
POST/v1/sessions/:id/actionsPLACE · CONTINUE · CASHOUT
POST/v1/sessions/:id/heartbeatPresence: keep the round alive while on screen
POST/v1/sessions/:id/leaveDeliberate exit: starts the 5-minute leave grace
GET/v1/sessions/:id/verificationRevealed seed, dealt hands, replay and hash-chain checks
GET/v1/sessionsRound history
GET/v1/configPublished rules: multiplier tables, cash-out levels, piece probabilities

Session lifecycle

A round moves between a small set of states. Only the server moves it.

PLACE level done CONTINUE CASHOUT(automatic if grace runs out) no piece fits grace ran out, cash-out locked PLAYING LEVEL_COMPLETE CASHED_OUT GAME_OVER EXPIRED

Example

Create a player, start a round and place the first piece:

API=https://puzzlecrash.urbanrats.com
TOKEN=$(curl -s -X POST $API/v1/players | jq -r .token)

S=$(curl -s -X POST $API/v1/sessions \
  -H "authorization: Bearer $TOKEN" -H "idempotency-key: start-$(date +%s)" \
  -H 'content-type: application/json' -d '{"difficulty":"medium","stake":100}')
ID=$(echo $S | jq -r .session.id)

curl -s -X POST $API/v1/sessions/$ID/actions \
  -H "authorization: Bearer $TOKEN" -H "idempotency-key: a1-$(date +%s)" \
  -H 'content-type: application/json' \
  -d '{"type":"PLACE","slot":0,"x":0,"y":0,"expectedVersion":0}'

Operations

  • Health: /healthz (liveness) and /readyz (database + Redis)
  • Prometheus metrics with alert rules for errors, latency, lock contention and payout anomalies
  • Structured JSON audit logs with secrets redacted
  • Docker images for server and web; migrations run automatically