summaryrefslogtreecommitdiff
path: root/bin/rtt-update-elo
diff options
context:
space:
mode:
authorTor Andersson <tor@ccxvii.net>2025-04-25 12:18:00 +0200
committerTor Andersson <tor@ccxvii.net>2025-04-25 17:56:43 +0200
commitfc3501382c2aa3ef5b692f4f55c2616f9cade3f5 (patch)
treea047bb94f8f1854c621fa0fba5ed62c8e0d0be14 /bin/rtt-update-elo
parented2361980b455d1825d811670f329cbcf5624927 (diff)
downloadserver-fc3501382c2aa3ef5b692f4f55c2616f9cade3f5.tar.gz
Move toolbox scripts to a "bin" directory.
Add a super "rtt" command to run the scripts.
Diffstat (limited to 'bin/rtt-update-elo')
-rwxr-xr-xbin/rtt-update-elo66
1 files changed, 66 insertions, 0 deletions
diff --git a/bin/rtt-update-elo b/bin/rtt-update-elo
new file mode 100755
index 0000000..538964a
--- /dev/null
+++ b/bin/rtt-update-elo
@@ -0,0 +1,66 @@
+#!/usr/bin/env -S node
+
+// Recompute Elo ratings from scratch!
+
+const sqlite3 = require("better-sqlite3")
+
+const db = new sqlite3("db")
+
+const SQL_SELECT_GAMES = db.prepare("select * from rated_games_view order by mtime")
+const SQL_SELECT_RATING = db.prepare("select * from player_rating_view where game_id=?")
+const SQL_INSERT_RATING = db.prepare("insert or replace into ratings (title_id,user_id,rating,count,last) values (?,?,?,?,?)")
+
+function is_winner(role, result) {
+ // NOTE: uses substring matching for multiple winners instead of splitting result on comma.
+ return (result === "Draw" || result === role || result.includes(role))
+}
+
+function elo_k(n) {
+ return 30
+}
+
+function elo_ev(a, players) {
+ // Generalized formula for multiple players.
+ // https://arxiv.org/pdf/2104.05422.pdf
+ let sum = 0
+ for (let p of players)
+ sum += Math.pow(10, p.rating / 400)
+ return Math.pow(10, a.rating / 400) / sum
+}
+
+function elo_change(a, players, s) {
+ return Math.round(elo_k(a.count) * (s - elo_ev(a, players)))
+}
+
+function update_elo_ratings(game) {
+ let players = SQL_SELECT_RATING.all(game.game_id)
+
+ if (game.player_count !== players.length)
+ return
+
+ if (!game.result || game.result === "None")
+ return
+
+ let winners = 0
+ for (let p of players)
+ if (is_winner(p.role, game.result))
+ winners ++
+
+ if (winners === 0)
+ return
+
+ for (let p of players)
+ if (is_winner(p.role, game.result))
+ p.change = elo_change(p, players, 1 / winners)
+ else
+ p.change = elo_change(p, players, 0)
+
+ for (let p of players)
+ SQL_INSERT_RATING.run(game.title_id, p.user_id, p.rating + p.change, p.count + 1, game.mtime)
+}
+
+db.exec("begin transaction")
+db.exec("delete from ratings")
+for (let game of SQL_SELECT_GAMES.all())
+ update_elo_ratings(game)
+db.exec("commit")