woggioni 46f10f145a
CI / Build and push docker image (push) Successful in 2m9s
Show the OIDC profile picture with an identicon fallback
GET /api/me now carries the profile picture URL when the provider
supplies an absolute http(s) one; the SPA shows it in the lobby navbar
and draws a deterministic identicon from the sub otherwise, falling back
to the identicon when the image fails to load.

The identicon is a 5x5 mirrored grid seeded by an inline FNV-1a hash so
it stays stable across Rust versions and is unit-testable on the host.
The dev mock IdP hands alice and bob a picture claim.
2026-10-15 00:14:17 +08:00
2026-09-18 19:18:04 +08:00

tavolo

A platform for multiplayer card games. Four games are included: scopone scientifico — the four-player, fixed-partnership Italian card game — scopa, in its classic two-player and four-player partnership (scopa a quattro) variants, with the asso pigliatutto option turning either into scopa d'assi — briscola, in its classic two-player and four-player partnership (briscola a quattro) variants, and briscola chiamata — the five-player variant with a calling auction and a secret partner:

  • server/ — backend: Python + kaya framework, OIDC login, live games in Redis, match statistics in Postgres. A monorepo of five distributions sharing the tavolo namespace: the game-independent tavolo-platform, the tavolo-scopone, tavolo-scopa and tavolo-briscola games, and the tavolo-app composition root wiring them together. See server/README.md.
  • web/ — frontend: Rust + Sycamore compiled to WebAssembly, built with Trunk and styled with Bootstrap 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).

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: scopone and scopa 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. Briscola chiamata plays a single deal per match, so it goes straight from the last trick to the final scoring screen; a 60–60 patta is replayed immediately.

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. Briscola chiamata arms the same countdown for its auction turns and for the auction winner's called-card choice: an absent bidder passes, the last player standing with no bid is made to bid the ace, and a timed-out call names a random suit of the won rank.

Watching

Anyone who has the game URL (/game/<id>) 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. In briscola chiamata they also see no role labels and no side totals until the called card is played, so the secret partner stays secret. 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/):

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-scopa -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-scopa/tests
.venv/bin/python -m unittest discover -s packages/tavolo-briscola/tests

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_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.

S
Description
Scopone scientifico multiplayer backend built on the kaya framework
Readme
6 MiB
Languages
Python 73.5%
Rust 23.5%
CSS 1.9%
Dockerfile 0.6%
JavaScript 0.2%
Other 0.3%