Move the game-independent machinery (lobby, live-game store, websocket, deadline scheduler, match history, leaderboards) into a new tavolo-platform distribution behind a GameEngine contract, the scopone scientifico rules plus a platform adapter into tavolo-scopone, and keep only the composition root in tavolo-app. The three distributions share the tavolo namespace (PEP 420, kaya-style monorepo). Match history becomes fully generic: Match carries the engine's result JSON and MatchPlayer points/details instead of scopone-shaped team columns (migration 3 backfills existing rows). Lobby creation takes an opaque per-game options object and websocket actions dispatch to the session's engine. Tests: platform suite runs against a DummyEngine toy game, scopone keeps the rules tests plus new adapter tests, server/tests covers the wired stack end to end (194 tests, was 143).
65 lines
3.2 KiB
Markdown
65 lines
3.2 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
|
|
example is
|
|
[`tavolo-scopone`](../tavolo-scopone/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.
|