# tavolo A platform for multiplayer card games. Two games are included: **scopone scientifico** — the four-player, fixed-partnership Italian card game — and **briscola**, in its classic two-player and four-player partnership (*briscola a quattro*) variants: - **`server/`** — backend: Python + [kaya](https://github.com/woggioni/kaya) framework, OIDC login, live games in Redis, match statistics in Postgres. A monorepo of four distributions sharing the `tavolo` namespace: the game-independent `tavolo-platform`, the `tavolo-scopone` and `tavolo-briscola` games, and the `tavolo-app` composition root wiring them together. See [server/README.md](server/README.md). - **`web/`** — frontend: Rust + [Sycamore](https://sycamore.dev) compiled to WebAssembly, built with [Trunk](https://trunkrs.dev) and styled with [Bootstrap](https://getbootstrap.com) 5.3 (MIT, vendored under `web/vendor/` so builds work offline) plus the custom felt theme in `web/theme.css`. Card images are the CC0 *woodcut napoletane* deck traced from a 1902 Naples print ([SONDLecT/woodcut-napoletane](https://github.com/SONDLecT/woodcut-napoletane)). ## Run the whole stack ```sh docker compose up --build ``` Postgres, Redis, a mock OIDC provider (test users `alice`, `bob`, `carol`, `dave`), the database migrator and the app all come up together. The app — frontend and API — listens on `http://127.0.0.1:8080`. Because both the browser and the app talk to the OIDC issuer at `http://mockoauth:8180/tavolo`, add a host entry once: ```sh echo "127.0.0.1 mockoauth" | sudo tee -a /etc/hosts ``` ## Between hands When a hand ends but the match is not decided, the game pauses on a **scoring summary screen**: scopone players see how each category was won (carte, denara, settebello, primiera, scope — plus napola and asso pigliatutto when enabled), briscola players see the deal's points and any *patta*; both see the running totals and must click "Understood" before the next hand is dealt. If someone is away the next hand is dealt automatically after `HAND_ACK_TIMEOUT_SECONDS` (default 30s). The match-ending hand is explained on the final screen. ## On your turn Every turn shows a countdown (`TURN_TIMEOUT_SECONDS`, default 30s). If a player does not move — disconnected or fallen asleep — the server plays a random legal card for them (choosing among the legal captures when the rules require a capture), so one absent player cannot stall the table. ## Watching Anyone who has the game URL (`/game/`) can watch, logged in or not: spectators see the table, the score and every player's captured cards (and scope for scopone), but never a hand and never the join code. The "Copy watch link" button in the waiting room and in the game HUD copies the URL to share. A spectator who opens the URL before the match starts sees a "game not started yet" waiting room. ## Development Backend (from `server/`): ```sh cd server python3 -m venv .venv .venv/bin/pip install -r requirements.txt .venv/bin/pip install -e ./packages/tavolo-platform -e ./packages/tavolo-scopone -e ./packages/tavolo-briscola -e './packages/tavolo-app[dev]' .venv/bin/python -m unittest discover -s tests -t . .venv/bin/python -m unittest discover -s packages/tavolo-platform/tests .venv/bin/python -m unittest discover -s packages/tavolo-scopone/tests .venv/bin/python -m unittest discover -s packages/tavolo-briscola/tests ``` Frontend (from `web/`), with the backend running on :8080: ```sh cd web trunk serve # SPA on http://localhost:8000, /api /auth /ws proxied ``` Trunk proxies `/api`, `/auth` and `/ws` to `127.0.0.1:8080` (see `web/Trunk.toml`). For the login redirect to land back on the dev server, run the backend with: ```sh OIDC_0_PROVIDER=default \ OIDC_0_ISSUER=http://localhost:8180/tavolo \ OIDC_0_CLIENT_ID=tavolo \ OIDC_0_REDIRECT_URI=http://localhost:8080/auth/default/callback \ OIDC_0_POST_LOGIN_REDIRECT=http://localhost:8000/ \ OIDC_0_POST_LOGOUT_REDIRECT=http://localhost:8000/ \ .venv/bin/granian --host 127.0.0.1 --port 8080 tavolo.app:app ``` Card images are committed under `web/assets/cards/`; `web/fetch-cards.sh` re-downloads them if needed.