- 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>
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 4Series.Series— exactly 8Rounds; the starting player rotates per series/round.Round— a bidding (guess/"tip") phase then a play phase of8 - round_numberStashes. Each round deals fewer cards asround_numbergrows.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 inStash.get_winner(), and follow-suit logic inRound.play_cardforces playing a heart when you can't follow the led suit.Card_valuesare 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— asyncengine+async_sessionmaker, declarativeBase, andinit_db()(create_all). Connection string from envDATABASE_URL(defaultsqlite+aiosqlite:///bridzik.db; Docker sets PostgreSQL viaasyncpg). There are no migrations —create_allonly 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), 4playerN_idseats,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.wonis 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")andapp = socketio.ASGIApp(sio, other_asgi_app=_health_app); run withuvicorn api:app. The ASGI lifespan startup callsinit_db()then_restore_unfinished_games(). Multiple concurrent games via the module-globalgamesdict (keyed bygid) withGame/Playerwrapper 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_gamevia per-game token,rejoin_gamevia account), play (game_status,player_cards,add_guess,play_card), history (get_player_history,get_game_detail). Creating/joining/playing requires a logged-inaccounts[sid].
- Handlers: auth (
api/auth.py— TOTP auth (pyotp), passwordless.register_accountissues a secret +otpauth://URI (frontend renders the QR);confirm_account/loginverify the 6-digit code and return a session token stored onPlayer.auth_token;player_by_tokenresolves the token sent in the Socket.IOauthhandshake onconnect.api/history.py— persistence orchestration overdb/, reading values from the engine (never mutating it):record_game_started,record_completed_rounds(also tracksseries/roundposition and setsended_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 aBridzikto a position (cards re-dealt fresh — the only non-deterministic part);restore_game_core/get_unfinished_gamesdrive 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
Dockerfiletargets Python 3.14-slim and runsuvicorn api:app.docker-compose.yamlruns PostgreSQL (db, with apgdatavolume + 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.pywith aunittestcase intests/test_engine.py— keep the engine free of Flask/Socket.IO/DB. - Persistence/auth logic goes in
db/(data primitives) andapi/history.py+api/auth.py(logic that uses them); cover it intests/test_history.py, nottests/test_engine.py. - Raise
BridzikException(Slovak message) for rule violations; the API catches it and re-emits a Slovak error. Auth errors useAuthErrorthe same way.