From a934d07be3e3009708924ed58103fc4aeeee3ed1 Mon Sep 17 00:00:00 2001 From: Grail Finder Date: Fri, 11 Jul 2025 21:38:38 +0300 Subject: [PATCH] Fix: rating update --- migrations/002_add_stats_elo.up.sql | 2 +- repos/players_test.go | 163 +++++++++++++++++++++++++--- 2 files changed, 151 insertions(+), 14 deletions(-) diff --git a/migrations/002_add_stats_elo.up.sql b/migrations/002_add_stats_elo.up.sql index c2b9346..092cdd8 100644 --- a/migrations/002_add_stats_elo.up.sql +++ b/migrations/002_add_stats_elo.up.sql @@ -2,7 +2,7 @@ ALTER TABLE player_stats ADD COLUMN rating REAL NOT NULL DEFAULT 1000.0; CREATE TRIGGER update_player_rating -BEFORE UPDATE OF games_played, games_won ON player_stats +AFTER UPDATE OF games_played, games_won ON player_stats WHEN NEW.games_played = OLD.games_played + 1 BEGIN UPDATE player_stats diff --git a/repos/players_test.go b/repos/players_test.go index 62d3f4e..d75892b 100644 --- a/repos/players_test.go +++ b/repos/players_test.go @@ -14,18 +14,99 @@ func setupPlayersTestDB(t *testing.T) (*sqlx.DB, func()) { db, err := sqlx.Connect("sqlite3", ":memory:") assert.NoError(t, err) - schema := ` - CREATE TABLE IF NOT EXISTS players ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - room_id TEXT, - username TEXT, - password TEXT NOT NULL DEFAULT '', - team TEXT, - role TEXT, - is_bot BOOLEAN - ); + // Load schema from migration files + schema001 := ` +-- migrations/001_initial_schema.up.sql - CREATE TABLE player_stats ( +CREATE TABLE rooms ( + id TEXT PRIMARY KEY, + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + creator_name TEXT NOT NULL, + team_turn TEXT NOT NULL DEFAULT '', + this_turn_limit INTEGER NOT NULL DEFAULT 0, + opened_this_turn INTEGER NOT NULL DEFAULT 0, + blue_counter INTEGER NOT NULL DEFAULT 0, + red_counter INTEGER NOT NULL DEFAULT 0, + red_turn BOOLEAN NOT NULL DEFAULT FALSE, + mime_done BOOLEAN NOT NULL DEFAULT FALSE, + is_running BOOLEAN NOT NULL DEFAULT FALSE, + is_over BOOLEAN NOT NULL DEFAULT FALSE, + team_won TEXT NOT NULL DEFAULT '', + room_link TEXT NOT NULL DEFAULT '' +); + +CREATE TABLE players ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + room_id TEXT, -- nullable + username TEXT NOT NULL UNIQUE, + password TEXT NOT NULL DEFAULT '', + team TEXT NOT NULL DEFAULT '', -- 'red' or 'blue' + role TEXT NOT NULL DEFAULT '', -- 'guesser' or 'mime' + is_bot BOOLEAN NOT NULL DEFAULT FALSE, + FOREIGN KEY (room_id) REFERENCES rooms(id) ON DELETE CASCADE +); + +CREATE TABLE word_cards ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + room_id TEXT NOT NULL, + word TEXT NOT NULL, + color TEXT NOT NULL DEFAULT '', + revealed BOOLEAN NOT NULL DEFAULT FALSE, + mime_view BOOLEAN NOT NULL DEFAULT FALSE, + FOREIGN KEY (room_id) REFERENCES rooms(id) ON DELETE CASCADE +); + +CREATE TABLE card_marks ( + card_id INTEGER NOT NULL, + username TEXT NOT NULL, + FOREIGN KEY (card_id) REFERENCES word_cards(id) ON DELETE CASCADE, + FOREIGN KEY (username) REFERENCES players(username) ON DELETE CASCADE, + PRIMARY KEY (card_id, username) +); + +CREATE TABLE actions ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + room_id TEXT NOT NULL, + actor TEXT NOT NULL, + actor_color TEXT NOT NULL DEFAULT '', + action_type TEXT NOT NULL, + word TEXT NOT NULL DEFAULT '', + word_color TEXT NOT NULL DEFAULT '', + number_associated TEXT NOT NULL DEFAULT '', -- for clues + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (room_id) REFERENCES rooms(id) ON DELETE CASCADE +); + +CREATE TABLE settings ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + room_id TEXT NOT NULL, + language TEXT NOT NULL DEFAULT 'en', + room_pass TEXT NOT NULL DEFAULT '', + turn_time INTEGER NOT NULL DEFAULT 60, -- seconds + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (room_id) REFERENCES rooms(id) ON DELETE CASCADE +); + +CREATE TABLE sessions( + id INTEGER PRIMARY KEY AUTOINCREMENT, + updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + lifetime INTEGER NOT NULL DEFAULT 3600, + token_key TEXT NOT NULL DEFAULT '' UNIQUE, -- encoded value + username TEXT NOT NULL, + FOREIGN KEY (username) REFERENCES players(username) ON DELETE CASCADE +); + +CREATE TABLE journal( + id INTEGER PRIMARY KEY AUTOINCREMENT, + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + entry TEXT NOT NULL DEFAULT '', + username TEXT NOT NULL, + room_id TEXT NOT NULL, + FOREIGN KEY (username) REFERENCES players(username) ON DELETE CASCADE, + FOREIGN KEY (room_id) REFERENCES rooms(id) ON DELETE CASCADE +); + +CREATE TABLE player_stats ( id INTEGER PRIMARY KEY AUTOINCREMENT, username TEXT NOT NULL UNIQUE, games_played INTEGER NOT NULL DEFAULT 0, @@ -40,9 +121,32 @@ func setupPlayersTestDB(t *testing.T) (*sqlx.DB, func()) { played_as_guesser INTEGER NOT NULL DEFAULT 0, FOREIGN KEY (username) REFERENCES players(username) ON DELETE CASCADE ); +` + _, err = db.Exec(schema001) + assert.NoError(t, err) - ` - _, err = db.Exec(schema) + schema002 := ` +ALTER TABLE player_stats +ADD COLUMN rating REAL NOT NULL DEFAULT 1000.0; + +CREATE TRIGGER update_player_rating +AFTER UPDATE OF games_played, games_won ON player_stats +WHEN NEW.games_played = OLD.games_played + 1 +BEGIN + UPDATE player_stats + SET rating = OLD.rating + + 32.0 * ( + CASE + WHEN NEW.games_won = OLD.games_won + 1 + THEN 1.0 - 0.5 -- Win term: 0.5 + ELSE 0.0 - 0.5 -- Loss term: -0.5 + END + ) + + 0.05 * (1000.0 - OLD.rating) + WHERE id = OLD.id; +END; +` + _, err = db.Exec(schema002) assert.NoError(t, err) return db, func() { @@ -50,6 +154,39 @@ func setupPlayersTestDB(t *testing.T) (*sqlx.DB, func()) { } } +func TestPlayerStatsRatingUpdate(t *testing.T) { + db, teardown := setupPlayersTestDB(t) + defer teardown() + + username := "test_player_rating" + _, err := db.Exec(`INSERT INTO players (username) VALUES (?)`, username) + assert.NoError(t, err) + + _, err = db.Exec(`INSERT INTO player_stats (username, games_played, games_won, rating) VALUES (?, 0, 0, 1000.0)`, username) + assert.NoError(t, err) + + // Simulate a win + _, err = db.Exec(`UPDATE player_stats SET games_played = 1, games_won = 1 WHERE username = ?`, username) + assert.NoError(t, err) + + var ratingAfterWin float64 + err = db.Get(&ratingAfterWin, `SELECT rating FROM player_stats WHERE username = ?`, username) + assert.NoError(t, err) + // Expected: 1000 + 32 * (1 - 0.5) + 0.05 * (1000 - 1000) = 1000 + 16 = 1016 + assert.InDelta(t, 1016.0, ratingAfterWin, 0.001) + + // Simulate a loss + _, err = db.Exec(`UPDATE player_stats SET games_played = 2, games_won = 1 WHERE username = ?`, username) + assert.NoError(t, err) + + var ratingAfterLoss float64 + err = db.Get(&ratingAfterLoss, `SELECT rating FROM player_stats WHERE username = ?`, username) + assert.NoError(t, err) + // Expected: 1016 + 32 * (0 - 0.5) + 0.05 * (1000 - 1016) = 1016 - 16 + 0.05 * (-16) = 1000 - 0.8 = 999.2 + assert.InDelta(t, 999.2, ratingAfterLoss, 0.001) +} + + func TestPlayersRepo_AddPlayer(t *testing.T) { db, teardown := setupPlayersTestDB(t) defer teardown()