Emberiségellenes Kártyák
A mobile-first, real-time multiplayer party card game in Hungarian, in the style of Cards Against Humanity,
with a machine-learning computer player that plays against people in the live game.
Live: emberisegelleneskartyak.hu · about 25 games a day · ~4,300
visitors in 90 days (autumn 2026)
Personal project, designed, built and operated end to end by one developer on a self-managed Linux server.
Highlights
- Real-time multiplayer in production: FastAPI and WebSockets, rooms in memory, reconnects, timers, voting
and czar modes, WebRTC voice chat.
- A full ML lifecycle around a deliberately simple model: anonymous gameplay logging → versioned,
validated snapshots → nightly training (local or a GCP Cloud Run Job) → MLflow tracking and model registry
→ a challenger/champion promotion with a statistical comparison → a model service → a computer player in the
live game. More below.
- Honest evaluation: time-based splits by game, a held-out set without model-influenced games, paired
bootstrap intervals over games, and baselines. The current model doubles random top-1 accuracy (20% vs
10%) but doesn't beat a popularity baseline significantly, and the docs say so.
- Delivery: about 720 automated tests (unit, ML, JavaScript, browser end-to-end against the production
image), 7 required CI checks, a tested image published to GHCR, a staging site that deploys itself with
automatic rollback, and an approval-gated release workflow for production.
- Privacy by design: no raw IP addresses (purpose-bound HMAC pseudonyms), retention enforced in code, an ML
log without player identifiers.
Features
- Rooms and lobby: 4-digit room codes, admin approval of joining players, card pack selection, round count
and timer, an optional computer player.
- Real-time game over WebSockets: czar or voting mode, multi-pick prompts (pick 2 / pick 3), per-phase timers,
reconnects, inactivity kick, anonymous reveal with the author shown after the pick.
- Card decks: many overlapping packs, deduplicated per game. A least-seen weighted shuffle keeps rarely dealt
cards coming up.
- Community cards: players submit and vote on card ideas; approved ones become a playable pack.
- Extras: in-room text chat, WebRTC voice chat (TURN relay), printable card PDFs, card search and feedback.
The computer player (ML lifecycle)
flowchart LR
G[(gameplay log<br>anonymous decisions)] --> X[export + validation<br>versioned snapshot]
X --> T[nightly training<br>3 embeddings x 2 models x 3 data variants]
T --> MF[(MLflow<br>tracking + registry)]
MF -- "@challenger" --> MS[model service<br>FastAPI]
MF -- "@champion, after ml.promote" --> MS
MS --> SG[staging bot]
MS --> PG[production bot]
SG & PG -- "its decisions, flagged" --> G
- The task: given a black prompt card and ten white cards, predict which one a human plays. Card texts are
embedded with multilingual sentence models (e5, MiniLM, Granite); a small PyTorch model scores each
(prompt, card) pair.
- Training: a nightly pipeline exports a snapshot, validates it (13 data checks), trains every combination
and registers the best on validation as
challenger. The same container runs as a GCP Cloud Run Job, pushed
from CI without stored keys (Workload Identity Federation).
- Keeping its data apart: the bot's own picks and votes are flagged in the log and never used as labels.
Validation and test only use games without a bot, so every model is measured against undisturbed human
behaviour; human picks from bot games and human votes on the bot's cards are separate, measured variants.
- Promotion: the nightly run scores challenger and champion on the same newest test games (paired bootstrap)
and moves the
champion alias automatically, but only when the challenger is significantly better: nightly
versions differ by less than the test noise. A worse model is never promoted, and rollback is one command. The service follows the aliases without a restart.
- Serving: card texts go to the service, so new cards are embedded on the fly. The bot plays within a hard
deadline and falls back to a random choice, which
/healthz counts. Worst case measured: a full turn with
brand-new cards, 5 games at once, 3.9 s against the shortest round timer of 15 s.
Details: docs/ml/computer-player.md (plan, data policy, results, lessons),
docs/ml/roadmap.md (the lifecycle and its milestones).
Architecture
flowchart LR
B[Phone / browser<br>vanilla JS] -- HTTPS + WSS --> N[nginx<br>TLS, reverse proxy]
B -. WebRTC voice .-> T[coturn<br>TURN relay]
N --> A[FastAPI + uvicorn<br>one process]
A --> R[(in-memory rooms<br>game state)]
A --> D[(SQLite: cards, chat,<br>analytics, gameplay log)]
A -- HTTP --> MS[model service<br>PyTorch, container]
MS --> MF[(MLflow<br>container)]
- Backend: Python 3.11, FastAPI, native WebSockets. Game state lives in memory; one process serves the game.
- Frontend: plain HTML, CSS and JavaScript, no framework and no build step.
- Data: SQLite, one file per concern. The card database is built offline and opened read-only.
- ML: PyTorch, sentence-transformers, MLflow (self-hosted), GCP Cloud Run Jobs and Cloud Storage.
- Operations: rootless Podman containers managed by systemd (Quadlet), nginx with Let's Encrypt, systemd
timers for backups, cleanup and nightly training. Templates in deploy/, runbook in
docs/runbooks/deploy.md.
The full picture is in docs/architecture.md.
This is the beginning of the README. Read the rest at the original. Read the full README ↗