Skip to content

fire0clop/joker

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

26 Commits
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

joker banner

Joker 2 — Online Multiplayer Card Game

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.

Swift SwiftUI iOS Python FastAPI WebSockets PostgreSQL Redis License


Overview

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.


Game Rules (as implemented by the engine)

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

Features

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

Architecture

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).


Tech Stack

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
Email aiosmtplib + Jinja2 templates (MailHog in dev)
Observability structlog, Sentry, Prometheus instrumentator
Infra (dev) Docker Compose — Postgres · Redis · MinIO · MailHog

Project Structure

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

Getting Started

Backend

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 info

Interactive API docs at http://localhost:8010/docs · MailHog UI at http://localhost:8025 · MinIO console at http://localhost:9001.

iOS client

Prerequisites: Xcode 15+, iOS 16.6+ target. No SwiftPM/CocoaPods dependencies — open and run.

cd frontend
open Jefferson.xcodeproj

The client points at the backend via Jefferson/Core/Constants.swift:

  • DEBUG → a local dev host on port 8010 (REST + ws://). Update devHost to 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.


API & Realtime Protocol

REST (prefix /api/v1)

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}.

WebSocket channels

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.


Status & Roadmap

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

License

Released under the MIT License. Copyright © 2026 Egor Fomenko.

About

Server-authoritative multiplayer Joker (Kozel) card game — FastAPI realtime backend and native SwiftUI iOS client.

Topics

Resources

License

Stars

Watchers

Forks

Packages

 
 
 

Contributors

Languages