CI / Build and push docker image (push) Successful in 1m37s
The auction timeout bid a random legal rank, which could hand the auction (and the call) to a disconnected player. An absent bidder now always passes; only the last player standing before any bid cannot pass, and is made to bid the ace - its suit is picked at random by the existing call timeout. This keeps a deal from stalling without a caller while never letting an absent player outbid someone. The deadline log line now says which of the two happened.
113 lines
4.7 KiB
Markdown
113 lines
4.7 KiB
Markdown
# tavolo
|
||
|
||
A platform for multiplayer card games. Three games are included:
|
||
**scopone scientifico** — the four-player, fixed-partnership Italian card
|
||
game — **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](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. 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/`):
|
||
|
||
```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.
|