woggioni 61f3539c4e Reconnect the game websocket after connectivity loss
The socket had no recovery path: a mid-game drop left the table showing
stale state with no indication, and plays were silently swallowed by the
dead channel. Surface the server close code from ws::connect and add a
reconnect driver in the game page: transient losses retry with
exponential backoff (capped, then a manual Retry), while deliberate
closes (session expired, game gone) stop retrying. Reconnects are free
resyncs because the server pushes a full state snapshot on connect.

Also gate card clicks while disconnected, show a connection banner, and
drop the ticking interval and pending retries on unmount.
2026-09-18 19:16:39 +08:00

tavolo

A platform for multiplayer card games. The first game is scopone scientifico — the four-player, fixed-partnership Italian card game:

  • server/ — backend: Python + kaya framework, OIDC login, live games in Redis, match statistics in Postgres. See server/README.md.
  • web/ — frontend: Rust + Sycamore compiled to WebAssembly, built with Trunk. Card images are the CC0 woodcut napoletane deck traced from a 1902 Naples print (SONDLecT/woodcut-napoletane).

Run the whole stack

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:

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: every player sees how each category was won (carte, denara, settebello, primiera, scope — plus napola when enabled) with 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 (randomizing among the legal captures when the rules require a capture), so one absent player cannot stall the table.

Development

Backend (from server/):

cd server
python3 -m venv .venv
.venv/bin/pip install -r requirements.txt
.venv/bin/pip install -e '.[dev]'
.venv/bin/python -m unittest discover -s tests -t .
.venv/bin/mypy src

Frontend (from web/), with the backend running on :8080:

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:

OIDC_POST_LOGIN_REDIRECT=http://localhost:8000/ \
OIDC_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.

S
Description
Scopone scientifico multiplayer backend built on the kaya framework
Readme
4 MiB
Languages
Python 72.7%
Rust 21.3%
CSS 3.8%
Dockerfile 1.7%
Shell 0.4%
Other 0.1%