Files
tavolo/server/packages/tavolo-platform
woggioni 2bdf247cb3 Restore cursor value types before keyset filtering
Cursors are JSON, so a datetime sort key reached the next request as a
string. Tortoise's DatetimeField.to_db_value does not parse strings, so
asyncpg rejected the string where a timestamptz parameter was expected;
sqlite tolerated it, hiding the bug in tests. Coerce decoded cursor
values through their model field's to_python_value before building the
keyset filter.
2026-09-28 04:26:00 +08:00
..

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

Development (from server/)

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