woggioni c40ebd84d8 Fix deadline consumer startup under granian RSGI
The mixin received the event loop from Kaya but ignored it, calling
asyncio.get_running_loop() instead. Under granian RSGI __rsgi_init__ runs
before the loop starts, so that raised RuntimeError and killed the
worker. Use the loop passed to setup(), falling back to the running loop
for the lazy calls from sync_deadline().
2026-09-18 02:33:00 +00: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%