A robust, real-time multiplayer Blackjack game server written in Rust. This backend powers the game logic, state management, and communication for a Blackjack platform.
- Language: Rust (2021 Edition)
- Framework: Axum (Web & WebSockets)
- Runtime: Tokio
- Serialization: Serde JSON
-
Clone & Run:
cargo run
Server starts at
http://127.0.0.1:3000. -
Configuration (.env):
APP_ADDRESS=0.0.0.0:3000 RUST_LOG=blackjack_backend=debug
This backend uses a persistent WebSocket connection. State is authoritative on the server.
POST /game/create
Create a new room before connecting.
Request:
{
"settings": {
"initial_chips": 1000,
"max_players": 5,
"deck_count": 6,
"approval_required": false,
"chat_enabled": true
}
}Response:
{ "game_id": "849201" }Endpoint: ws://<host>/ws/<game_id>
The server supports session persistence. You must store the credentials received in JoinedLobby to allow players to refresh the page without losing their spot.
- New Session:
Connect to
ws://localhost:3000/ws/849201. - Reconnection:
Append credentials to the URL:
ws://localhost:3000/ws/849201?player_id=<UUID>&secret=<SECRET>
Status Codes:
101: Connected.404: Game not found (ended or wrong ID).403: Game is full (and you are not reconnecting).
- Connect via WebSocket.
- If this is a fresh session, send
JoinGame. - Listen for
JoinedLobby. Saveyour_idandsecretto LocalStorage immediately. - Listen for
GameStateSnapshotto render the UI. - On page reload, read LocalStorage. If keys exist, connect using the Reconnection URL.
- If reconnection fails (socket closes or Error received), clear LocalStorage and connect normally.
All WebSocket messages are JSON.
Card
{ "suit": "Hearts", "rank": "Ace" }- Suits:
Hearts,Diamonds,Clubs,Spades - Ranks:
Two..Ten,Jack,Queen,King,Ace
Player
{
"id": "uuid-string",
"name": "Alice",
"chips": 1000,
"hands": [ ... ], // See Hand structure
"active_hand_index": 0, // Index of the hand currently being acted on
"status": "Playing", // Spectating, Sitting, Playing, PendingApproval
"is_admin": true, // Can perform admin actions
"is_connected": true // True if connected, false if offline/disconnected
}Hand
{
"cards": [ { "suit": "Spades", "rank": "Ten" }, ... ],
"bet": 100,
"status": "Playing" // Playing, Stood, Busted, Blackjack, Doubled
}GameSettings
{
"initial_chips": 1000,
"max_players": 5,
"deck_count": 1,
"approval_required": false,
"chat_enabled": true
}GamePhase
Values: Lobby, Betting, Playing, DealerTurn, Payout, GameOver
Wrap all messages in: { "action": "Name", "payload": { ... } }
| Action | Payload JSON | Description |
|---|---|---|
| JoinGame | { "username": "Bob" } |
Register as a player. Required if not reconnecting. |
| PlaceBet | { "amount": 50 } |
Bet chips. Valid only in Betting phase. |
| GameAction | { "action_type": "Hit" } |
Types: Hit, Stand, Double, Split. Valid in Playing phase. |
| Chat | { "message": "Hi" } |
Broadcast text to all players. |
| StartGame | null |
(Admin) Force start the game or skipped current player turn. |
| NextRound | null |
(Admin) Phase: Payout -> Betting. |
| ApprovePlayer | { "player_id": "..." } |
(Admin) Allow a PendingApproval player to join. |
| KickPlayer | { "player_id": "..." } |
(Admin) Remove and disconnect a player. |
| UpdateSettings | { "settings": { ... } } |
(Admin) Change rules mid-game. |
| AdminUpdateBalance | { "target_id": "...", "change_chips": 500 } |
(Admin) Modify player chips (use negative to deduct). |
| Ping | null |
Check latency/connection. Server replies with Pong. |
Note on Phases:
- Lobby: Players join. Admin starts game (
StartGame). - Betting: Players place bets (Status ->
Playing). Non-betters staySitting.- System Auto-Start: If everyone eligible and connected has bet. Offline players are ignored.
- Admin Force-Start: If some have bet, Admin can
StartGameto proceed. Non-betters skip the round.
- Playing: Players take actions.
- Admin can
StartGameto force-stand the current player (timeout).
- Admin can
- DealerTurn: Automatic.
- Payout: Results calculated.
- NextRound: Admin resets to
Betting(NextRound).
Wrapped in: { "event": "Name", "data": { ... } }
Sent on any change. This is the entire state needed to render the game.
{
"event": "GameStateSnapshot",
"data": {
"phase": "Playing",
"dealer_hand": [ {"suit": "Clubs", "rank": "Five"} ], // First card hidden if playing
"players": [ ... ], // Array of Player objects
"deck_remaining": 42,
"current_turn_player_id": "uuid-string", // Whose turn is it? (null if not Playing)
"settings": { ... }
}
}Hand Status Values:
Playing, Stood, Busted, Blackjack, Doubled, Won, Lost, Push
Sent only to YOU after a successful join or reconnect.
{
"event": "JoinedLobby",
"data": {
"game_id": "123456",
"your_id": "uuid-string",
"secret": "KEEP_THIS_SECRET_uuid", // Save this for reconnection!
"is_admin": false
}
}Sent when an action fails.
{
"event": "Error",
"data": { "msg": "Not enough chips to split." }
}Sent to Admins only when approval_required is true and someone joins.
{
"event": "PlayerRequest",
"data": { "id": "uuid", "name": "Stranger" }
}{
"event": "ChatBroadcast",
"data": { "from": "Alice", "msg": "gg" }
}Response to Ping.
{
"event": "Pong",
"data": null
}Sent when you are removed from the game by an admin. The connection is closed immediately after.
{
"event": "Kicked",
"data": null
}- Error Messages: Most error messages (e.g. "Not enough chips", "It is not your turn") are now sent only to the relevant player (unicast), instead of being broadcast to the entire room.
- Game State: Game state updates and chat messages are broadcast to all connected clients.