Files
tavolo/server/migrations/models/2_20260918120620_elo.py
woggioni a606f14550 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)
2026-09-18 13:36:17 +00:00

56 lines
2.8 KiB
Python

from tortoise import BaseDBAsyncClient
RUN_IN_TRANSACTION = True
async def upgrade(db: BaseDBAsyncClient) -> str:
return """
CREATE TABLE IF NOT EXISTS "player_rating" (
"id" UUID NOT NULL PRIMARY KEY,
"user_sub" VARCHAR(255) NOT NULL,
"game_type" VARCHAR(32) NOT NULL,
"rating" INT NOT NULL,
"matches_played" INT NOT NULL,
"updated_at" TIMESTAMPTZ NOT NULL,
CONSTRAINT "uid_player_rati_user_su_655b53" UNIQUE ("user_sub", "game_type")
);
CREATE INDEX IF NOT EXISTS "idx_player_rati_game_ty_3326d1" ON "player_rating" ("game_type", "rating");
COMMENT ON TABLE "player_rating" IS 'Current Elo rating of one player for one game type.';
ALTER TABLE "match_player" ADD "elo_delta" SMALLINT;"""
async def downgrade(db: BaseDBAsyncClient) -> str:
return """
ALTER TABLE "match_player" DROP COLUMN "elo_delta";
DROP TABLE IF EXISTS "player_rating";"""
MODELS_STATE = (
"eJztmltv4jgUgP9KlKeuNFsBvY1Wq5WAUg07pVSF7q6mqiyTGLAa7IztbAd1+9/Xdm7EuR"
"RoYUrFyww59nHsz87xufTJnlEXefywB4UztX+znmwCZ0j+yDZ8smzo+6lYCQQceWHPpMuI"
"CwYdIYVj6HEkRS7iDsO+wJSork3LoTPfQwK5llaz6NiiBKn/xBRZDE0wF4jJ5omchyXmPu"
"KHamyXOnJwTCavGyYg+HuAgKATJDsyOdjdvRRj4qIfiMeP/gMYY+S5GSDYVQNoOVADKtnt"
"bff8QvdUUxwBh3rBjKS9/bmYUpJ0DwLsHiod1TZBBDEol7CAiwSeF2GNReGMpUCwACVTdV"
"OBi8Yw8BR0+/dxQBzF2tJvUv8c/xFNbaEbAFf9IRh0hgDYuT1SUzB4RyKHErW/mAgF6uk5"
"HDcFoqW2ekH7S/Pm4Oj0F42AcjFhulHjsp+1IhQwVNXQU8pqv0JeOdjtKWTFsDNKBnM55X"
"Vox4Iq3Nyhvjx1gDsYEYHH2KGbgi0/sh/AQ2Qi1Fd61KiA/1fzJuTf0Pyp/CLD7/Qqamno"
"JrUNKXaB4AxAuRLKCsgPZtDzukQU0zd1jQ2QS3ibDUhtSgw3xrcJ4BM1iV+PGmenn2Wrnq"
"N6OKsgP+g1Ly+7V8MitqNXsB3t2ZaxfcRELh0oTKsYDENtUyZju1QzJqK+hIWolxqIes4+"
"QCZvzDXPsKG7P8NZtlNIXA58D85RgYtRzdbU3bPNsuXy8Ml1AyjyZM8lEYFnqJhsVtPg6k"
"aqh/GPHaRcAXPY7XUGw2bvWg0/4/y7p3k1hx3V0tDSuSE9ODWsSTKI9Xd3+MVSj9a3/lXH"
"dAqTfsNvtpoTDAQFhD4C6C4yicWxKLPNY0wwn661z4bqfqPf3UariGz8sBAtKMEIOg+PkL"
"kg05KeCG0SGc+fhlakePH1BnlQs8xv+2Lwe61H2t19T6V2GMRooLRBy4jmm2aNmSmBBE70"
"ktS71ZsKkJWlE1KiLyQVwouNLZdbuJb2GjvY13saJwQCjpiFif6th8wnE1bQK8ge3KXpD6"
"UDeDCy7/cphfeUUkj2ZYUAYVFnawmFrQUHjZOTJcID2as0QNBtWVfLxVx9rkA/r4Da1PuA"
"wVijVluGd61Wzlu1Ga4tKnJ2qsOFWGcfJuRTNKuc2X3ioOSc5hIHj6F/ZfhglHoIkpLcTK"
"FHNpIqO8i2Amar37/MuN2t7tDAettrdWLashMWWpw/vsijcuqegKvag4ziWkYhusTeC+83"
"tAmhB7qai7ao85aO2vs9xS/4ZbnYzeCbh3tBGcIT8hXNNeKunAgkTpFnYFYodxNqLjyTYg"
"Yfk2ghc6bk6uWaUWgH2s1Bu3nesZ/L4+FNRnphFHcjwya9yFyol2n/VBXrhVEeYGnXF4O9"
"dsAYIsLqeNQK9eLILRzMGlOmH5P6bz7wW3OMwiBwMV5Iq5FmIHiXrVRGC77fx4f7+PBnW6"
"MtBIjvt7T/E0m/fSk/NaNZxqVOYKqwvbCwflKrbdYFbNSPz44/H50eJ35gIqlyBkv8QFRe"
"oCvlmlfcHt9dgRv4qrCyTt0mq/kByzY2Q9DtE28e3bu7W8ZJig5LVnE26bU2EcPFf+kYtV"
"R6qjDt85KLWr7Pb/w3iKX2p9AfLLA5kVP3Okdw8zH9K21Ouf/3L2IcF6Wpyr2SBZUP6JNs"
"xPtTH9UKhKPuH5BufaliQL2iGFDPFwPkGwUiBZfon4P+VTHhBRXz9sSOsP6zPMx3sSxQAV"
"fByFyRMdODXvMfE3f7st8y7z41QKsorbXNy+z5fy/uTSo="
)