A full-stack, real-time implementation of Joker (Джокер / Козёл) — the classic 4-player Eastern-European trick-taking card game with bidding, trumps, jokers, and pulka-based scoring. Server-authoritative game engine in FastAPI, native SwiftUI iOS client.
Joker (also known as Джокер or Козёл) is a 4-player trick-taking card game played over a fixed sequence of 26 deals grouped into 4 pulkas. Before each deal players bid the exact number of tricks they expect to take; hitting your bid exactly is richly rewarded, missing it is punished. Two jokers act as wild cards that can be played "highest" (beats everything, names the led suit) or "lowest" (throws off), adding a deep layer of tactical decision-making on top of standard trump play.
This repository contains both halves of a production-grade product:
- Backend (
backend/) — a server-authoritative FastAPI service. All game logic lives on the server; the client is pure visualization and input. It owns the rules engine, matchmaking, realtime game channels, economy, ELO rating, seasons, tournaments, clubs, chat, and anti-collusion. - iOS client (
frontend/) — a native SwiftUI app (Xcode target Jefferson) with a hand-rolled REST + WebSocket networking layer, Keychain-backed auth, transparent token refresh, and MVVM feature modules.
The design principle throughout: the client never computes a legal move or a score — it renders state pushed by the server over WebSockets.
The rules engine (backend/app/services/game_engine.py) is pure Python — no I/O, no DB, no Redis — and is the single source of truth.
| Concept | Implementation |
|---|---|
| Deck | 36 regular cards (6…A in ♥♦♣♠) + 2 Jokers = 38 cards |
| Players | 4 |
| Deal sequence | 26 deals across 4 pulkas — Pulka 1: 1,2,…,8 cards · Pulka 2: 9×5 · Pulka 3: 8,7,…,1 · Pulka 4: 9×5 |
| Tuzovanie | Cards dealt one-by-one; first ace determines the opening dealer |
| Trump | Suit of the first non-joker undealt card |
| Bidding | Each player bids 0…cards_per_player; the last bidder is constrained so total bids can't equal the trick count |
| Following suit | Must follow the led suit if able; jokers may always be played |
| Joker modes | highest (beats everything; when leading, names the suit) · lowest (throws off, rank −1) |
| Exact bid | bid × 100 points (a bid of 0 met = 50 points) |
| Missed bid | tricks_taken × 10 points; a bid ≥ 1 with 0 tricks taken sets a штанга (penalty) |
| 3 штанги | Remove the best deal score from the current pulka and reset the counter |
| Premium | Hit every bid in a pulka → bonus equal to your top deal score; your left neighbour loses the same amount |
| Game over | Final standings ranked by score → ELO deltas + chip payouts settled |
Gameplay
- Server-authoritative rules engine with full pulka/deal/trick/joker/scoring logic
- Realtime game channel over WebSockets — bidding, card play, trick resolution, live turn timers
- Reconnect & rejoin: game state survives disconnects (Redis + PostgreSQL snapshots)
- Server-side move timeouts with auto-play, and disconnect/abandon handling
- AI bots (
EASY…EXPERT) fill seats when matchmaking times out
Matchmaking & Lobby
- Ranked matchmaking queue by table stake level, plus private rooms with 6-char join codes
- Room composition over a dedicated lobby WebSocket (
ROOM_UPDATED,MATCH_FOUND,QUEUE_STATUS) - Six table tiers from BEGINNER to VIP, each with its own stake
Progression & Economy
- Zero-sum chip economy with place-based payouts and an auto "lifebuoy" top-up
- ELO rating with calibrated K-factors and 6 leagues (Bronze → Master)
- Seasons with soft ELO reset and reward tiers; daily bonuses
- Cosmetic shop (card backs, tables, avatars, frames, effects) backed by S3/MinIO
Social
- Friends, requests, blocking, online presence, and game invites
- Direct-message & club chat over a chat WebSocket with typing indicators and read receipts
- Clubs, tournaments (daily/weekly/season) with brackets, and global/friends/season leaderboards
Platform
- JWT auth (short access token + rotating refresh tokens), email verification, password reset
- Apple Sign-In & Google OAuth, multi-device session management
- Anti-collusion event logging, in-app + push notifications, admin stats
- Celery workers for game timers, matchmaking sweeps, season rollover, and email
Server-authoritative: the iOS clients render state and send intents; the FastAPI engine validates every action and broadcasts the resulting state.
┌──────────────────────────────────────────────┐
iOS clients ×4 │ FastAPI service │
┌─────────────┐ │ │
│ SwiftUI │ │ REST /api/v1/* ───► API routers │
│ (Jefferson) │◄──┼── HTTPS ──────────────► auth · lobby · games │──┐
│ │ │ shop · social · … │ │
│ APIClient │ │ │ │
│ WebSocket │◄──┼══ WS /ws/game/{id} ═══► ConnectionManager │ │
│ Client │ │ WS /ws/lobby ═══► + GameEventDispatcher │ │
│ Keychain │ │ WS /ws/chat/{id} ═══► │ │
└─────────────┘ │ │ │ │
│ ▼ │ │
│ GameEngine (pure Python rules) │ │
└──────────────┬───────────────┬───────────────┘ │
│ │ │
┌─────────────▼───┐ ┌───────▼────────┐ ┌──────▼──────┐
│ Redis 7 │ │ PostgreSQL 16 │ │ Celery │
│ live game state │ │ users · games │ │ workers+beat │
│ pub/sub · queue │ │ deals · economy│ │ timers·email │
│ cache · locks │ │ (SQLAlchemy 2) │ │ matchmaking │
└─────────────────┘ └────────────────┘ └──────────────┘
│
┌──────▼──────┐
│ S3 / MinIO │
│ skins·avatars│
└──────────────┘
Realtime flow (a single move):
PLAY_CARD (client → /ws/game/{id}) → GameEngine validates & mutates state → state persisted to Redis (with a snapshot to Postgres) → ConnectionManager publishes over Redis pub/sub → CARD_PLAYED / TRICK_COMPLETE / TURN_START broadcast to the four seated clients (private HAND_UPDATE sent per position).
| Layer | Technology |
|---|---|
| iOS client | Swift 5, SwiftUI, MVVM + @Observable, iOS 16.6+ (iPhone/iPad), zero third-party deps |
| iOS networking | URLSession REST client, native URLSessionWebSocketTask, AsyncStream events, exponential-backoff reconnect |
| iOS storage | Keychain (tokens/device id), UserDefaults (profile cache) |
| API framework | FastAPI 0.115, Uvicorn/Gunicorn, Pydantic 2 |
| Realtime | Native WebSockets + Redis pub/sub fan-out |
| Persistence | PostgreSQL 16 · SQLAlchemy 2.0 (async, asyncpg) · Alembic migrations |
| State / cache / broker | Redis 7 (game state, cache, Celery broker, distributed locks) |
| Background jobs | Celery 5 (+ Beat, Flower) |
| Auth | JWT (HS256) via python-jose, passlib[bcrypt], Apple Sign-In, Google OAuth |
| Object storage | S3 / MinIO via boto3 |
aiosmtplib + Jinja2 templates (MailHog in dev) |
|
| Observability | structlog, Sentry, Prometheus instrumentator |
| Infra (dev) | Docker Compose — Postgres · Redis · MinIO · MailHog |
joker/
├── backend/ # FastAPI service (all game logic)
│ ├── app/
│ │ ├── main.py # App, middleware, router + WS wiring
│ │ ├── config.py # Pydantic settings (env vars)
│ │ ├── database.py # Async SQLAlchemy engine/session
│ │ ├── redis_client.py # Redis connection pool
│ │ ├── api/ # REST routers (auth, games, lobby, shop, …)
│ │ ├── ws/ # WebSocket handlers
│ │ │ ├── game_ws.py # /ws/game/{game_id}
│ │ │ ├── lobby_ws.py # /ws/lobby
│ │ │ ├── chat_ws.py # /ws/chat/{room_id}
│ │ │ ├── connection_manager.py
│ │ │ └── game_event_dispatcher.py
│ │ ├── services/
│ │ │ ├── game_engine.py # Pure rules engine (deck/deals/joker/scoring)
│ │ │ ├── game_launcher.py # Game bootstrap + stake handling
│ │ │ ├── game_state_service.py
│ │ │ ├── lobby_service.py # Rooms + matchmaking
│ │ │ ├── matchmaking.py
│ │ │ ├── rating_service.py # ELO + leagues
│ │ │ ├── economy_service.py # Chips (zero-sum payouts)
│ │ │ ├── anti_collusion.py
│ │ │ └── … # auth, user, shop, bot, notification, s3
│ │ ├── models/ # SQLAlchemy ORM tables
│ │ ├── schemas/ # Pydantic DTOs (incl. ws_events.py)
│ │ ├── tasks/ # Celery app + game/notification/analytics jobs
│ │ └── templates/email/ # Jinja2 email templates
│ ├── docker-compose.dev.yml # Postgres · Redis · MinIO · MailHog
│ ├── Dockerfile
│ ├── alembic.ini
│ ├── requirements.txt
│ └── .env.example
│
└── frontend/ # Native iOS client (Xcode target "Jefferson")
└── Jefferson/
├── JeffersonApp.swift # @main entry
├── ContentView.swift # Auth-state root router
├── Core/ # AppState, DependencyContainer, Constants
├── Network/
│ ├── APIClient.swift # REST client
│ ├── WebSocketClient.swift # game/lobby/chat channels
│ ├── TokenRefreshInterceptor.swift
│ ├── APIEndpoints.swift # 50+ endpoints
│ └── NetworkModels/ # Codable request/response models
├── Services/ # Auth, Game, Lobby, User, Social, Shop, …
├── Storage/ # KeychainStorage, UserDefaultsStorage
├── Features/ # MVVM modules: Auth, MainMenu, Lobby, Game,
│ # Profile, Leaderboard, Shop, Friends, Chat,
│ # Notifications, Rules
├── Components/ # KZ* reusable UI kit
└── Extensions/ # Theming, fonts, formatting
Prerequisites: Python 3.12, Docker (for Postgres / Redis / MinIO / MailHog).
cd backend
# 1. Configuration
cp .env.example .env # then edit secrets for your environment
# 2. Start infrastructure (Postgres 16 · Redis 7 · MinIO · MailHog)
docker compose -f docker-compose.dev.yml up -d
# 3. Python environment
python -m venv .venv && source .venv/bin/activate
pip install -r requirements.txt
# 4. Database migrations
alembic upgrade head
# 5. Run the API (default dev port 8010, matching the iOS client)
uvicorn app.main:app --reload --host 0.0.0.0 --port 8010
# 6. Background workers (separate shells)
celery -A app.tasks.celery_app worker -l info
celery -A app.tasks.celery_app beat -l infoInteractive API docs at http://localhost:8010/docs · MailHog UI at http://localhost:8025 · MinIO console at http://localhost:9001.
Prerequisites: Xcode 15+, iOS 16.6+ target. No SwiftPM/CocoaPods dependencies — open and run.
cd frontend
open Jefferson.xcodeprojThe client points at the backend via Jefferson/Core/Constants.swift:
- DEBUG → a local dev host on port
8010(REST +ws://). UpdatedevHostto your Mac's Bonjour name or LAN address so a physical device can reach it. - RELEASE → the production API base (
https://+wss://).
Build & run on a simulator or device; register an account (emails are captured by MailHog in dev), then queue for a match or create a private room.
| Router | Base | Highlights |
|---|---|---|
| Auth | /auth |
register, verify-email, login, apple, google, refresh, logout[-all], forgot/reset/change-password, check-nickname |
| Users | /users |
me, me/heartbeat, me/avatar, me/stats, me/games, me/transactions, me/achievements, search, {id} |
| Games | /games |
active, history, {id}, {id}/state, {id}/hand, {id}/scoresheet, {id}/replay, {id}/forfeit |
| Lobby | /lobby |
tables, queue/join·leave·status, rooms (create/join/leave/start/kick), my-room, my-game |
| Social | /friends, /chat, /clubs |
friends & requests, DM/club rooms & messages, club create/join |
| Meta | /shop, /leaderboard, /seasons, /tournaments, /notifications, /admin |
economy, rankings, seasons, brackets, inbox, push tokens |
Health: GET /health → {status, version, database, redis}.
All channels authenticate via a JWT passed as a query parameter: ?token=<access_jwt>.
| Channel | URL |
|---|---|
| Game | /ws/game/{game_id} |
| Lobby | /ws/lobby |
| Chat | /ws/chat/{room_id} |
Game — client → server
{ "type": "PLACE_BID", "bid": 3 }
{ "type": "PLAY_CARD", "card": "AH" }
{ "type": "PLAY_CARD", "card": "J1", "joker_mode": "highest", "joker_suit": "H" }
{ "type": "JOKER_MODE", "mode": "lowest" }
{ "type": "CHAT_MESSAGE", "content": "gg", "message_type": "QUICK_PHRASE" }
{ "type": "PING" }
{ "type": "LEAVE_GAME" }Game — server → client (envelope { "type", "data" })
GAME_STATE · HAND_UPDATE · TRUMP_REVEALED
BIDDING_START · BID_PLACED · BIDDING_DONE
TURN_START · CARD_PLAYED · TRICK_COMPLETE
DEAL_COMPLETE · PULKA_COMPLETE · GAME_OVER
JOKER_CHOICE · PREMIUM_AWARDED · PENALTY_APPLIED
TURN_TIMER · TURN_TIMEOUT
PLAYER_CONNECTED · PLAYER_DISCONNECTED · PLAYER_RECONNECTED · PLAYER_ABANDONED
CHAT_MESSAGE · SYSTEM_MESSAGE · ERROR · PONG
Lobby — server → client: ROOM_UPDATED, ROOM_STARTED, MATCH_FOUND (carries game_id + WS url), QUEUE_STATUS.
Chat — client → server: SEND_MESSAGE, TYPING_START/STOP, READ_MESSAGES, PING. Server → client: NEW_MESSAGE, USER_TYPING, USER_STOPPED_TYPING, MESSAGE_DELETED, USER_READ.
The backend is feature-complete across the rules engine, realtime layer, matchmaking, economy, rating, social, and background jobs. The iOS client has a complete networking/auth/architecture foundation with feature modules in varying stages of UI polish.
Backend — done
- ✅ Pure rules engine (deals, pulkas, jokers, штанга, premium, final settlement)
- ✅ Game / lobby / chat WebSocket channels with Redis pub/sub fan-out
- ✅ Matchmaking, private rooms, bot fill, reconnect & timeout handling
- ✅ JWT + OAuth auth, economy, ELO/leagues, seasons, tournaments, clubs
- ✅ Celery timers, matchmaking sweeps, season rollover, email
iOS — done
- ✅ REST + WebSocket clients, transparent token refresh, Keychain storage
- ✅ Auth flow, main menu, shop, chat, friends, rules, reusable UI kit
- ✅ Core game session view model wired to realtime events
Roadmap (iOS)
- ⏳ Full in-game card rendering kit (card / hand / trick views, tuzovanie & bidding UI)
- ⏳ Apple / Google sign-in SDK wiring (server endpoints already live)
- ⏳ Leaderboard, social, and notification screens (services already implemented)
- ⏳ Push-notification registration, skin image loading, dark mode
Released under the MIT License. Copyright © 2026 Egor Fomenko.