summaryrefslogtreecommitdiff
path: root/tools/elo.js
diff options
context:
space:
mode:
authorTor Andersson <tor@ccxvii.net>2023-09-12 23:56:45 +0200
committerTor Andersson <tor@ccxvii.net>2023-09-13 20:06:36 +0200
commitb1b753e317daa10e03a6a3b210d185539fac176b (patch)
treeb389ae89f689af3a7d0fb77cb89d46271e18bfc5 /tools/elo.js
parent6407378d92eb8880e35e8ee33e1801136a1a44a7 (diff)
downloadserver-b1b753e317daa10e03a6a3b210d185539fac176b.tar.gz
Calculate Elo ratings.
Primarily for use with future matchmaking system to provide better games for everyone. Show top 5 players of each game on the game pages.
Diffstat (limited to 'tools/elo.js')
-rw-r--r--tools/elo.js58
1 files changed, 58 insertions, 0 deletions
diff --git a/tools/elo.js b/tools/elo.js
new file mode 100644
index 0000000..95ca58b
--- /dev/null
+++ b/tools/elo.js
@@ -0,0 +1,58 @@
+#!/usr/bin/env -S node
+
+const sqlite3 = require("better-sqlite3")
+
+const db = new sqlite3("db")
+
+const SQL_SELECT_GAMES = db.prepare("select * from games where status>1 and user_count=player_count and player_count>1 order by xtime")
+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 elo_k(n) {
+ return n < 10 ? 60 : 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
+
+ let winner = null
+ for (let p of players)
+ if (p.role === game.result)
+ winner = p
+
+ if (winner) {
+ for (let p of players) {
+ if (p === winner)
+ p.change = elo_change(p, players, 1)
+ else
+ p.change = elo_change(p, players, 0)
+ }
+ } else {
+ for (let p of players)
+ p.change = elo_change(p, players, 1 / game.player_count)
+ }
+
+ for (let p of players)
+ SQL_INSERT_RATING.run(game.title_id, p.user_id, p.rating + p.change, p.count+1, game.xtime)
+}
+
+db.exec("begin transaction")
+db.exec("delete from ratings")
+for (let game of SQL_SELECT_GAMES.all())
+ update_elo_ratings(game)
+db.exec("commit")