Add chess-style Elo ratings for players
Each player's rating starts at 1500 and updates transactionally with every finished match: a team's rating is the mean of its two members and the standard K=32 formula decides the zero-sum delta applied to both members of a team. Ratings are per game type in a new player_rating table; match_player records each match's elo_delta. - GET /api/leaderboard exposes elo and sorts by it - GET /api/me/matches includes per-player elo deltas - new GET /api/me/ratings returns the caller's rating per game type - frontend: Elo column on the leaderboard, per-match delta in the history page, current rating in the lobby - python -m tavolo.backfill_elo recomputes all ratings from the recorded match history (one-off backfill for existing matches)
This commit is contained in:
+28
-6
@@ -133,12 +133,31 @@ loggers:
|
||||
scoped per game), both teams' final scores, winner, target score, hands
|
||||
played, start/finish timestamps.
|
||||
- `match_player` — one row per participant: the OIDC `sub`, display name,
|
||||
seat, team and whether they won. Unique per `(match, user_sub)`.
|
||||
seat, team, whether they won and the Elo change the match produced
|
||||
(`elo_delta`). Unique per `(match, user_sub)`.
|
||||
- `player_rating` — current Elo rating per `(user_sub, game_type)`, with
|
||||
the number of rated matches played.
|
||||
|
||||
When a match ends, the result is written transactionally to Postgres
|
||||
(once, guarded by a flag on the Redis state); the finished state stays in
|
||||
Redis until its TTL expires so clients can still fetch the final board.
|
||||
|
||||
### Elo ratings
|
||||
|
||||
Players carry a chess-style Elo rating per game type (`tavolo.elo`):
|
||||
everyone starts at 1500, a team's rating is the mean of its two members,
|
||||
and the standard formula `E = 1 / (1 + 10 ** ((R_opp - R_team) / 400))`
|
||||
with `K = 32` decides how many points the match result moves — the same
|
||||
delta for both members of a team, zero-sum between teams. Ratings update
|
||||
in the same transaction as the match result. To recompute every rating
|
||||
from the recorded match history (e.g. to backfill matches recorded before
|
||||
ratings existed):
|
||||
|
||||
```sh
|
||||
DATABASE_URL=postgres://tavolo:tavolo@localhost:5432/tavolo \
|
||||
.venv/bin/python -m tavolo.backfill_elo
|
||||
```
|
||||
|
||||
## REST API
|
||||
|
||||
All endpoints except `/api/health`, `/api/docs`, `/api/openapi.json`,
|
||||
@@ -150,8 +169,9 @@ All endpoints except `/api/health`, `/api/docs`, `/api/openapi.json`,
|
||||
| `POST` | `/api/games` | Create a lobby game. Optional body `{"game_type": "scopone_scientifico", "target_score": 11, "napola": true}`. Returns `{id, join_code}` |
|
||||
| `POST` | `/api/games/join` | Join with `{"code": "ABC123"}`. The fourth player triggers the deal |
|
||||
| `GET` | `/api/games/{id}` | Personalized snapshot (only your own hand is visible) |
|
||||
| `GET` | `/api/me/matches` | Cursor-paginated match history with final scores (`?limit=&cursor=&game_type=`) |
|
||||
| `GET` | `/api/leaderboard` | Aggregated wins / matches / team points per player (`?game_type=`) |
|
||||
| `GET` | `/api/me/matches` | Cursor-paginated match history with final scores and per-player Elo deltas (`?limit=&cursor=&game_type=`) |
|
||||
| `GET` | `/api/me/ratings` | The caller's Elo rating per game type |
|
||||
| `GET` | `/api/leaderboard` | Elo rating, aggregated wins / matches / team points per player, sorted by Elo (`?game_type=`) |
|
||||
|
||||
## WebSocket protocol
|
||||
|
||||
@@ -271,8 +291,10 @@ src/tavolo/
|
||||
├── openapi.py # shared OpenAPI parameter fragments
|
||||
├── tortoise_mixin.py # TortoiseORM lifecycle (HTTP + WebSocket)
|
||||
├── aerich_config.py # aerich CLI configuration
|
||||
├── models.py # Match, MatchPlayer (Postgres)
|
||||
├── stats.py # finished match -> Postgres persistence
|
||||
├── models.py # Match, MatchPlayer, PlayerRating (Postgres)
|
||||
├── elo.py # chess-style Elo math (1500 start, K=32)
|
||||
├── stats.py # finished match -> Postgres persistence + Elo update
|
||||
├── backfill_elo.py # recompute all ratings from the match history
|
||||
├── store.py # Redis / in-memory live-game store (+ deadline queue)
|
||||
├── deadlines.py # connection-independent timeout scheduler
|
||||
├── ws.py # WebSocket live-play endpoint
|
||||
@@ -283,5 +305,5 @@ src/tavolo/
|
||||
└── routes/
|
||||
├── health.py # GET /api/health
|
||||
├── games.py # lobby: create / join / snapshot
|
||||
└── stats.py # match history + leaderboard
|
||||
└── stats.py # match history + leaderboard + Elo ratings
|
||||
```
|
||||
|
||||
Reference in New Issue
Block a user