New tavolo-scopa package with a pure rules engine and the platform adapters for two-player scopa and four-player fixed-partnership scopa a quattro: three cards each and four on the table, capture by rank or sum, sweep for scope and score carte/denara/settebello/primiera. The optional asso pigliatutto creation option turns either variant into scopa d'assi (an ace sweeps the whole table and scores no scopa), reusing scopone's rule semantics. Wire both engines into the app and the Docker install, add the scopa table view to the SPA (duo and four-seat layouts, deck count, captured/scope stats, hand-summary and final screens, player names in the two-player summary), and cover the rules, the platform contract, the REST/WebSocket flows, match persistence and Elo.
66 lines
3.3 KiB
Markdown
66 lines
3.3 KiB
Markdown
# tavolo-platform
|
|
|
|
The game-independent half of tavolo: lobby, live play, match history and
|
|
leaderboards. Everything here works for any game that implements the
|
|
[`GameEngine`](src/tavolo/platform/engine.py) contract; the package
|
|
itself ships no game.
|
|
|
|
## Contents
|
|
|
|
- `engine.py` — the platform↔game contract: `GameEngine` (ABC),
|
|
`GameSession` (the platform-owned envelope with an opaque `state` blob),
|
|
`Seat`, `Deadline` (kind + due time + revalidation token) and the
|
|
`MatchResult`/`PlayerResult` outcome types.
|
|
- `registry.py` — `GameRegistry`: game id → engine lookup, single source
|
|
of truth for which games exist.
|
|
- `mixin.py` — `Platform` (registry + store + scheduler + OIDC, the
|
|
collaborators every endpoint needs) and `PlatformMixin`, the kaya mixin
|
|
that registers all routes and the websocket endpoint on an app.
|
|
- `routes/` — `health` (`GET /api/health`), `me` (`GET /api/me`), `games`
|
|
(lobby: `GET /api/game-types`, `POST /api/games`, `POST /api/games/join`,
|
|
`GET /api/games/{id}`) and `stats` (`GET /api/me/matches`,
|
|
`GET /api/leaderboard`, `GET /api/me/ratings`).
|
|
- `ws.py` — the live-play websocket (`/ws/games/{id}`): connection
|
|
lifecycle, the message envelope and the publish/subscribe fan-out.
|
|
Game-specific actions are dispatched to the session's engine.
|
|
- `store.py` — live-session persistence: `RedisGameStore` (production)
|
|
and `InMemoryGameStore` (tests/dev) over a JSON envelope plus the
|
|
engine's opaque state blob, with per-game locks, change signals and the
|
|
shared deadline queue.
|
|
- `deadlines.py` — `DeadlineScheduler`: enqueue the deadline an engine
|
|
declares after each mutation; a per-loop consumer fires due entries
|
|
through `engine.fire_deadline` under the per-game lock. Engines
|
|
revalidate the token, so stale or duplicate deliveries are harmless.
|
|
- `models.py` — `Match` (`game_type`, timestamps, game-specific JSON
|
|
`result`), `MatchPlayer` (seat, team, won, points, Elo delta, JSON
|
|
`details`) and `PlayerRating` (Elo per `(user_sub, game_type)`).
|
|
- `stats.py` — `save_match_result` (engine `MatchResult` → Postgres,
|
|
transactionally, with Elo) and `apply_elo`.
|
|
- `elo.py` — chess-style Elo math generalized to two-team matches.
|
|
- `backfill_elo.py` — rebuild every rating from the match history:
|
|
`python -m tavolo.platform.backfill_elo --database-url postgres://…`.
|
|
- `auth.py`, `http.py`, `pagination.py`, `openapi.py`,
|
|
`tortoise_mixin.py` — OIDC helpers, JSON helpers, keyset pagination,
|
|
shared OpenAPI fragments, the TortoiseORM lifecycle mixin.
|
|
|
|
## Adding a game
|
|
|
|
Implement `GameEngine` (see `engine.py` for the full contract), register
|
|
it in a `GameRegistry`, and mount `PlatformMixin(Platform(...))` on a
|
|
`KayaApp` — see the composition root in `tavolo.app`. The canonical
|
|
examples are [`tavolo-scopone`](../tavolo-scopone/README.md),
|
|
[`tavolo-scopa`](../tavolo-scopa/README.md) and
|
|
[`tavolo-briscola`](../tavolo-briscola/README.md).
|
|
|
|
## Development (from `server/`)
|
|
|
|
```sh
|
|
.venv/bin/python -m unittest discover -s packages/tavolo-platform/tests
|
|
.venv/bin/python -m mypy -p tavolo.platform
|
|
```
|
|
|
|
Tests run fully in-process against a `DummyEngine` (a two-player toy game
|
|
in `tests/helpers.py`): in-memory sqlite, in-memory stores, a patched
|
|
OIDC user and `httpx` / `httpx-ws` ASGI transports. No test in this
|
|
package may import a real game.
|