Play Zwick — a fast-paced, real-time multiplayer card game. Everyone plays at once: clear your Main pile first, call Zwick, and race to 75 points.
Zwick is an open-source Gleam application built on the BEAM. It is deliberately small enough to study, while still using OTP supervision, WebSockets, server-authoritative game state, and graceful recovery.
Open zwick.fly.dev, choose Quick Play or create a private room, and share its link. Games have four seats; after a short wait, bots can fill vacant seats so a game can begin.
- Gleam for the application and game rules
- Erlang/OTP for isolated actors, dynamic workers, and supervision
- Mist for HTTP and WebSockets
- Lustre for server-rendered UI
- Fly.io for the deployed service and persistent game snapshots
%%{init: {'theme': 'base', 'themeVariables': {'primaryColor': '#dbeafe', 'primaryTextColor': '#1e3a5f', 'primaryBorderColor': '#3b82f6', 'lineColor': '#64748b', 'secondaryColor': '#f1f5f9', 'tertiaryColor': '#e0f2fe', 'background': '#ffffff', 'mainBkg': '#dbeafe', 'nodeBorder': '#2563eb', 'clusterBkg': '#eff6ff', 'clusterBorder': '#bfdbfe', 'titleColor': '#1e3a5f', 'edgeLabelBackground': '#f8fafc'}}}%%
flowchart TD
Root[Root supervisor] --> Core[Core supervisor]
Root --> Bots[Bot factory]
Root --> Metrics[Metrics actor]
Root --> Sessions[Session registry]
Root --> Snapshots[Snapshot manager]
Root --> Server[Mist server]
subgraph CoreGroup[Rest-for-one core]
Factory[Game factory] --> Matchmaker[Matchmaker]
end
Core --> Factory
Factory --> Games[Game servers]
Bots --> BotPlayers[Bot players]
Matchmaker --> Games
Matchmaker --> BotPlayers
Server --> Matchmaker
Server --> Sessions
Server --> Metrics
Snapshots -. save and restore .-> Sessions
The root supervisor keeps the service available; the core group restarts the matchmaker when its game-factory dependency restarts, while game servers and bot players are created dynamically for each match.
%%{init: {'theme': 'base', 'themeVariables': {'primaryColor': '#dbeafe', 'primaryTextColor': '#1e3a5f', 'primaryBorderColor': '#3b82f6', 'lineColor': '#64748b', 'secondaryColor': '#f1f5f9', 'tertiaryColor': '#e0f2fe', 'background': '#ffffff', 'mainBkg': '#dbeafe', 'nodeBorder': '#2563eb', 'clusterBkg': '#eff6ff', 'clusterBorder': '#bfdbfe', 'titleColor': '#1e3a5f', 'edgeLabelBackground': '#f8fafc'}}}%%
flowchart LR
Player[/Player browser/] --> Router[HTTP and WebSocket]
Router --> Matchmaker[Matchmaker]
Matchmaker --> Factory[Game factory]
Factory --> Game[Game server]
Game --> Router
Router --> Player
Router --> Registry[Session registry]
Router --> Metrics[Metrics actor]
The browser keeps a WebSocket to the Mist server; the matchmaker forms four-player games, and each game server owns and broadcasts its authoritative state.
- Game isolation: each match runs in its own actor, so failures are contained to that match rather than shared game state.
- Graceful restarts: on
SIGTERM, the snapshot manager serializes active games and browser sessions toSNAPSHOT_DIR(the Fly volume mounted at/data), then restores them at startup. - Capacity:
SERVER_CAPdefaults to 200 and counts queued players plus active games. Fly is configured for 200 soft / 250 hard concurrent connections. - Metrics:
GET /admin/metricsreturns JSON with active-game and active-player counts.
Install Gleam, then:
gleam deps download
gleam run
gleam test
gleam format --check src testThe server listens on http://localhost:3000 by default. Set PORT to choose another port; SERVER_CAP controls the admission limit; and SNAPSHOT_DIR chooses where graceful-restart snapshots are written.
| Path | Purpose |
|---|---|
src/domain/ |
Pure game rules and card-domain types |
src/internal/ |
OTP actors, matchmaking, bots, snapshots, and metrics |
src/web/ |
HTTP routes, WebSockets, and server-rendered UI |
test/ |
Unit, integration, WebSocket, and snapshot tests |
fly.toml |
Fly.io runtime and volume configuration |
deploy/fly-topology.env |
Versioned Fly Machine regions and desired per-region count |
docs/deployment.md |
Deployment topology, regional data behavior, and drift checks |