Files
bridzik/CLAUDE.md
T
tim c59dca754f Tidy up root-level files: drop dead config.py, group tests into tests/
- Remove config.py, an unused Flask SECRET_KEY leftover from before the
  legacy HTTP backend was replaced by the Socket.IO/ASGI server.
- Move tests.py / tests_history.py / test_socket.py into a tests/
  package as test_engine.py / test_history.py / test_socket.py, and
  update CLAUDE.md's documented commands to match.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-01 00:37:25 +02:00

7.1 KiB

CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

What this is

A web implementation of Bridžik, a Slovak 4-player trick-taking/bidding card game played with a 32-card Slovak/German-suited deck. Stack: a pure-Python game engine, a realtime Socket.IO ASGI server, an async SQLAlchemy persistence layer, and a React PWA frontend. Code, comments, and exception messages are mostly in Slovak — keep new strings consistent with that.

Commands

# Run the backend (ASGI Socket.IO on 0.0.0.0:5000)
python -m app                  # uvicorn with --reload (dev entrypoint)
uvicorn api:app --host 0.0.0.0 --port 5000   # what Docker/prod runs

# Run the whole stack (Postgres + backend :5000 + frontend :5173)
docker-compose up --build
docker-compose down -v         # also drops the pg volume — needed after a schema change

# Tests
python -m unittest tests.test_engine -v             # pure-engine unittest (no deps)
python -m unittest tests.test_history -v             # persistence + auth (needs sqlalchemy/aiosqlite/pyotp)
python -m unittest tests.test_engine.StashCase.test_get_winner   # single method

There is no linter or build step configured. The frontend is only ever run via Docker — never npm install/npm run dev on the host.

Architecture

Three independent layers, each usable without the one above it: engine (bridzik.py) ← persistence (db/) ← app/transport (api/).

Game engine — bridzik.py

Pure Python, no Flask/Socket.IO/DB dependency. All game rules live here and are exercised directly by tests/test_engine.py. State is a strict nested hierarchy, each level enforcing turn order and completion before delegating down:

  • Bridzik — a whole game = exactly 4 Series.
  • Series — exactly 8 Rounds; the starting player rotates per series/round.
  • Round — a bidding (guess/"tip") phase then a play phase of 8 - round_number Stashes. Each round deals fewer cards as round_number grows.
  • Stash (a "kopka" = one trick) — 4 cards, one per player; get_winner() resolves it.

Key rules encoded in the engine:

  • Card_colors.HEARTS (červeň) is the permanent trump — any heart beats any non-heart in Stash.get_winner(), and follow-suit logic in Round.play_card forces playing a heart when you can't follow the led suit.
  • Card_values are ordered C7 < C8 < C9 < C10 < LOWER < UPPER < KING < ACE via custom comparison dunders.
  • Scoring: a player who exactly matches their guess scores 10 + guess, else 0 (Round.get_points_summary).
  • The total of the 4 guesses may not equal the number of tricks (the last bidder is constrained) — see Round.add_player_guess.

Serialization: Card.JSONEncoder flattens a Card to {color, value} name strings. The double json.loads(json.dumps(...)) pattern in the API strips escaped slashes after custom encoding.

Persistence layer — db/

Async SQLAlchemy 2.0, independent of Socket.IO (mirrors how the engine is kept clean).

  • db/db.py — async engine + async_sessionmaker, declarative Base, and init_db() (create_all). Connection string from env DATABASE_URL (default sqlite+aiosqlite:///bridzik.db; Docker sets PostgreSQL via asyncpg). There are no migrationscreate_all only adds new tables, so a changed column needs a fresh DB.
  • db/models.py — 3 tables:
    • Player — account + auth: username (unique login), totp_secret, totp_last_step (TOTP replay guard), auth_token (session token for reconnect).
    • Game — one match: id (gid), 4 playerN_id seats, name, series/round (current position, used for restore), created_at, ended_at.
    • Guess — one player's bid+result in a round: series_number, round_number, guess, points. won is derived (points > 0). Unique on (game, series, round, player) → idempotent writes.

App / transport layer — api/

  • api/__init__.py — the Socket.IO server. sio = socketio.AsyncServer(async_mode="asgi") and app = socketio.ASGIApp(sio, other_asgi_app=_health_app); run with uvicorn api:app. The ASGI lifespan startup calls init_db() then _restore_unfinished_games(). Multiple concurrent games via the module-global games dict (keyed by gid) with Game/Player wrapper classes and Socket.IO rooms. Three module-global dicts are the source of truth: games, sessions (sid → seat), accounts (sid → logged-in identity). Never trust a client-supplied player number — the seat is derived from the connection.
    • Handlers: auth (register_account, confirm_account, login), lobby (create_game, register_player, leave_game, start_game, end_game), reconnect (reconnect_to_game via per-game token, rejoin_game via account), play (game_status, player_cards, add_guess, play_card), history (get_player_history, get_game_detail). Creating/joining/playing requires a logged-in accounts[sid].
  • api/auth.py — TOTP auth (pyotp), passwordless. register_account issues a secret + otpauth:// URI (frontend renders the QR); confirm_account/login verify the 6-digit code and return a session token stored on Player.auth_token; player_by_token resolves the token sent in the Socket.IO auth handshake on connect.
  • api/history.py — persistence orchestration over db/, reading values from the engine (never mutating it): record_game_started, record_completed_rounds (also tracks series/round position and sets ended_at), mark_game_ended, get_standings (DB-backed standings so the score survives a restart), get_player_history, get_game_detail. Restore: rebuild_core(series, round) reconstructs a Bridzik to a position (cards re-dealt fresh — the only non-deterministic part); restore_game_core / get_unfinished_games drive restore-on-startup.

Frontend — frontend/

React + Vite PWA (zustand store, react-router, socket.io-client, qrcode.react). Talks to the backend purely over Socket.IO; the socket auth token enables auto-login on reconnect. Runs only inside Docker (the frontend compose service does npm install && npm run dev). The Vite proxy target is VITE_BACKEND_URL (compose) or http://localhost:5000 (default).

Config & infra

  • Dockerfile targets Python 3.14-slim and runs uvicorn api:app.
  • docker-compose.yaml runs PostgreSQL (db, with a pgdata volume + healthcheck), the backend (DATABASE_URL → that Postgres), and the frontend.
  • requirements.txt: python-socketio, uvicorn, SQLAlchemy[asyncio], aiosqlite (dev), asyncpg (prod), pyotp.

Conventions

  • New game-rule logic belongs in bridzik.py with a unittest case in tests/test_engine.py — keep the engine free of Flask/Socket.IO/DB.
  • Persistence/auth logic goes in db/ (data primitives) and api/history.py + api/auth.py (logic that uses them); cover it in tests/test_history.py, not tests/test_engine.py.
  • Raise BridzikException (Slovak message) for rule violations; the API catches it and re-emits a Slovak error. Auth errors use AuthError the same way.