forked from Hanabi/hanabi-league
Add loss minimization rating algorithm with data and results
This commit is contained in:
parent
4e459c4888
commit
11ec939510
4 changed files with 25985 additions and 1 deletions
25134
data/games.json
Normal file
25134
data/games.json
Normal file
File diff suppressed because it is too large
Load diff
468
results/min_loss_rating_list.json
Normal file
468
results/min_loss_rating_list.json
Normal file
|
@ -0,0 +1,468 @@
|
||||||
|
{
|
||||||
|
"players": [
|
||||||
|
{
|
||||||
|
"id": 1,
|
||||||
|
"rating": 1685.2675415133688,
|
||||||
|
"username": "asaelr",
|
||||||
|
"\u03c3": 66.31240370576553
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 6,
|
||||||
|
"rating": 1431.613182299928,
|
||||||
|
"username": "ElenaDhynho",
|
||||||
|
"\u03c3": 29.98866321273064
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 7,
|
||||||
|
"rating": 1567.0774916128976,
|
||||||
|
"username": "Eliclax",
|
||||||
|
"\u03c3": 126.30005538960775
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 8,
|
||||||
|
"rating": 1712.039271618434,
|
||||||
|
"username": "Fafrd",
|
||||||
|
"\u03c3": 59.80163231910586
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 9,
|
||||||
|
"rating": 1361.5113611693616,
|
||||||
|
"username": "Feich59",
|
||||||
|
"\u03c3": 144.39407407480272
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 10,
|
||||||
|
"rating": 1579.6672353819206,
|
||||||
|
"username": "gw12346",
|
||||||
|
"\u03c3": 270.3079683864935
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 11,
|
||||||
|
"rating": 1423.2932864211616,
|
||||||
|
"username": "hakha",
|
||||||
|
"\u03c3": 85.28363174484276
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 12,
|
||||||
|
"rating": 1872.6154944614254,
|
||||||
|
"username": "Helana",
|
||||||
|
"\u03c3": 143.85959224548074
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 13,
|
||||||
|
"rating": 1527.081117882728,
|
||||||
|
"username": "hnter",
|
||||||
|
"\u03c3": 86.70665567737284
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 14,
|
||||||
|
"rating": 1659.6782951052335,
|
||||||
|
"username": "inseres",
|
||||||
|
"\u03c3": 54.91823796429652
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 15,
|
||||||
|
"rating": 1742.9546724334089,
|
||||||
|
"username": "Jay",
|
||||||
|
"\u03c3": 193.84935304947757
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 16,
|
||||||
|
"rating": 1667.9465689249616,
|
||||||
|
"username": "kimbifille",
|
||||||
|
"\u03c3": 15.161265503279141
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 17,
|
||||||
|
"rating": 1649.333225086678,
|
||||||
|
"username": "Kowalski1337",
|
||||||
|
"\u03c3": 235.3362393234084
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 18,
|
||||||
|
"rating": 1677.9132076067115,
|
||||||
|
"username": "macanek",
|
||||||
|
"\u03c3": 208.0527366434835
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 19,
|
||||||
|
"rating": 1836.1454620500044,
|
||||||
|
"username": "MarkusKahlsen",
|
||||||
|
"\u03c3": 297.71026337163414
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 20,
|
||||||
|
"rating": 1605.4748903814996,
|
||||||
|
"username": "Neema",
|
||||||
|
"\u03c3": 119.12840085642274
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 21,
|
||||||
|
"rating": 1552.9114478185877,
|
||||||
|
"username": "newduke",
|
||||||
|
"\u03c3": 109.77086600282932
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 22,
|
||||||
|
"rating": 1224.413480638347,
|
||||||
|
"username": "NishaNoire",
|
||||||
|
"\u03c3": 261.66467716062857
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 25,
|
||||||
|
"rating": 1484.5905774236187,
|
||||||
|
"username": "omegaxis",
|
||||||
|
"\u03c3": 216.00665850825928
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 27,
|
||||||
|
"rating": 1802.0629338345077,
|
||||||
|
"username": "pianoblook",
|
||||||
|
"\u03c3": 123.37987103512832
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 28,
|
||||||
|
"rating": 1664.2567844629314,
|
||||||
|
"username": "posij118",
|
||||||
|
"\u03c3": 80.37917530567991
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 29,
|
||||||
|
"rating": 1702.5018997700438,
|
||||||
|
"username": "purplejoe",
|
||||||
|
"\u03c3": 100.77153063473354
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 31,
|
||||||
|
"rating": 1892.418588622356,
|
||||||
|
"username": "rahsosprout",
|
||||||
|
"\u03c3": 104.5777908934287
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 32,
|
||||||
|
"rating": 1835.7346031461793,
|
||||||
|
"username": "Ramanujan",
|
||||||
|
"\u03c3": 95.89853187628516
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 33,
|
||||||
|
"rating": 1443.2077282559123,
|
||||||
|
"username": "ricardodd",
|
||||||
|
"\u03c3": 39.34358984367649
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 34,
|
||||||
|
"rating": 1040.187091444465,
|
||||||
|
"username": "RIMBarisax",
|
||||||
|
"\u03c3": 366.4360349731977
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 35,
|
||||||
|
"rating": 1837.7815078205804,
|
||||||
|
"username": "rz",
|
||||||
|
"\u03c3": 199.78453636449262
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 36,
|
||||||
|
"rating": 1696.3214849037167,
|
||||||
|
"username": "Sagnik Saha",
|
||||||
|
"\u03c3": 136.01387381633026
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 37,
|
||||||
|
"rating": 1605.7373915962864,
|
||||||
|
"username": "sodiumdebt",
|
||||||
|
"\u03c3": 181.85538044442802
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 38,
|
||||||
|
"rating": 2117.1508966520855,
|
||||||
|
"username": "spring",
|
||||||
|
"\u03c3": 57.24559998584215
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 39,
|
||||||
|
"rating": 1512.0428621984959,
|
||||||
|
"username": "StKildaFan",
|
||||||
|
"\u03c3": 123.53879720946766
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 40,
|
||||||
|
"rating": 1688.979579509527,
|
||||||
|
"username": "str8tsknacker",
|
||||||
|
"\u03c3": 139.30078280466165
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 41,
|
||||||
|
"rating": 1773.724951734035,
|
||||||
|
"username": "Sturm",
|
||||||
|
"\u03c3": 116.05248815088189
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 42,
|
||||||
|
"rating": 1510.323393769054,
|
||||||
|
"username": "TimeHoodie",
|
||||||
|
"\u03c3": 105.6566442712854
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 43,
|
||||||
|
"rating": 1719.6120753928153,
|
||||||
|
"username": "vEnhance",
|
||||||
|
"\u03c3": 256.98388483269576
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 44,
|
||||||
|
"rating": 1605.1854946464819,
|
||||||
|
"username": "vermling",
|
||||||
|
"\u03c3": 39.05915568336428
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 45,
|
||||||
|
"rating": 1842.1832125075996,
|
||||||
|
"username": "wateroffire",
|
||||||
|
"\u03c3": 78.80111543154953
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 46,
|
||||||
|
"rating": 1603.6026225502553,
|
||||||
|
"username": "WillFlame",
|
||||||
|
"\u03c3": 367.7052871728946
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 47,
|
||||||
|
"rating": 1747.3129095523243,
|
||||||
|
"username": "Yagami Black",
|
||||||
|
"\u03c3": 124.1498189921402
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 48,
|
||||||
|
"rating": 1249.7131073005326,
|
||||||
|
"username": "youisme",
|
||||||
|
"\u03c3": 119.28343113428912
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 51,
|
||||||
|
"rating": 1252.822074342758,
|
||||||
|
"username": "Libster",
|
||||||
|
"\u03c3": 262.48847926040565
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 52,
|
||||||
|
"rating": 1710.980099683423,
|
||||||
|
"username": "maxeymo",
|
||||||
|
"\u03c3": 61.64513100559617
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 53,
|
||||||
|
"rating": 1784.677252501476,
|
||||||
|
"username": "joano580",
|
||||||
|
"\u03c3": 290.4564102793344
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 54,
|
||||||
|
"rating": 1483.9387182979765,
|
||||||
|
"username": "ReaverSe",
|
||||||
|
"\u03c3": 67.78852340589508
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 55,
|
||||||
|
"rating": 1739.6678580930318,
|
||||||
|
"username": "benzloeb",
|
||||||
|
"\u03c3": 153.68915774257988
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 56,
|
||||||
|
"rating": 1554.8486203468085,
|
||||||
|
"username": "percolate",
|
||||||
|
"\u03c3": 168.9058732288808
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 58,
|
||||||
|
"rating": 1454.7815820698524,
|
||||||
|
"username": "Random Guy JCI",
|
||||||
|
"\u03c3": 14.92058859287638
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 59,
|
||||||
|
"rating": 1635.1817290999422,
|
||||||
|
"username": "aara",
|
||||||
|
"\u03c3": 113.14163640238154
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 60,
|
||||||
|
"rating": 1541.843324991505,
|
||||||
|
"username": "amattias",
|
||||||
|
"\u03c3": 173.9589588268638
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 64,
|
||||||
|
"rating": 1559.44040061339,
|
||||||
|
"username": "wtfitsnotbutter",
|
||||||
|
"\u03c3": 240.16937231126366
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 65,
|
||||||
|
"rating": 1441.4652215451595,
|
||||||
|
"username": "Alix_Eisenhardt",
|
||||||
|
"\u03c3": 125.35709305406003
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 67,
|
||||||
|
"rating": 1640.3718839938117,
|
||||||
|
"username": "Vivarus",
|
||||||
|
"\u03c3": 121.95895258848924
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 69,
|
||||||
|
"rating": 1582.1212409759808,
|
||||||
|
"username": "FrozenStella",
|
||||||
|
"\u03c3": 145.14226927184038
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 70,
|
||||||
|
"rating": 1557.096282467627,
|
||||||
|
"username": "GameConqueror",
|
||||||
|
"\u03c3": 99.69687144224555
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 71,
|
||||||
|
"rating": 1662.4377474617993,
|
||||||
|
"username": "seungapark",
|
||||||
|
"\u03c3": 145.15724952841202
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 72,
|
||||||
|
"rating": 1508.3443601141673,
|
||||||
|
"username": "sinuni_hung",
|
||||||
|
"\u03c3": 61.81213974983676
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 73,
|
||||||
|
"rating": 1803.5327953459553,
|
||||||
|
"username": "TheDaniMan",
|
||||||
|
"\u03c3": 134.82661666333226
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 79,
|
||||||
|
"rating": 1482.0644773366396,
|
||||||
|
"username": "Kaznad",
|
||||||
|
"\u03c3": 405.28035311445757
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 80,
|
||||||
|
"rating": 1086.1355090178276,
|
||||||
|
"username": "Kernel",
|
||||||
|
"\u03c3": 211.39242310002254
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 81,
|
||||||
|
"rating": 1290.2977493468493,
|
||||||
|
"username": "Le Codex",
|
||||||
|
"\u03c3": 128.6528429208025
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 82,
|
||||||
|
"rating": 1554.5865377995306,
|
||||||
|
"username": "Nipalup",
|
||||||
|
"\u03c3": 186.7194364889647
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"variants": [
|
||||||
|
{
|
||||||
|
"id": 0,
|
||||||
|
"rating": 1344.571218042761,
|
||||||
|
"num_players": 3,
|
||||||
|
"name": "No Variant",
|
||||||
|
"num_suits": 5,
|
||||||
|
"\u03c3": 31.335534554470627
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 1,
|
||||||
|
"rating": 1380.3108266495492,
|
||||||
|
"num_players": 3,
|
||||||
|
"name": "6 Suits",
|
||||||
|
"num_suits": 6,
|
||||||
|
"\u03c3": 33.55653633693724
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 51,
|
||||||
|
"rating": 1751.8509769680584,
|
||||||
|
"num_players": 3,
|
||||||
|
"name": "Clue Starved (6 Suits)",
|
||||||
|
"num_suits": 6,
|
||||||
|
"\u03c3": 11.424872579830076
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 52,
|
||||||
|
"rating": 1677.1056853935763,
|
||||||
|
"num_players": 3,
|
||||||
|
"name": "Clue Starved (5 Suits)",
|
||||||
|
"num_suits": 5,
|
||||||
|
"\u03c3": 40.42763640571963
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 0,
|
||||||
|
"rating": 1436.6029781659563,
|
||||||
|
"num_players": 4,
|
||||||
|
"name": "No Variant",
|
||||||
|
"num_suits": 5,
|
||||||
|
"\u03c3": 45.77891871651097
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 1,
|
||||||
|
"rating": 1408.254084838167,
|
||||||
|
"num_players": 4,
|
||||||
|
"name": "6 Suits",
|
||||||
|
"num_suits": 6,
|
||||||
|
"\u03c3": 14.953524984939428
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 51,
|
||||||
|
"rating": 1820.7964465875705,
|
||||||
|
"num_players": 4,
|
||||||
|
"name": "Clue Starved (6 Suits)",
|
||||||
|
"num_suits": 6,
|
||||||
|
"\u03c3": 81.2114821701358
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 52,
|
||||||
|
"rating": 1719.526642788024,
|
||||||
|
"num_players": 4,
|
||||||
|
"name": "Clue Starved (5 Suits)",
|
||||||
|
"num_suits": 5,
|
||||||
|
"\u03c3": 38.5663698641606
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 0,
|
||||||
|
"rating": 1373.7429515570884,
|
||||||
|
"num_players": 5,
|
||||||
|
"name": "No Variant",
|
||||||
|
"num_suits": 5,
|
||||||
|
"\u03c3": 72.00085092075848
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 1,
|
||||||
|
"rating": 1522.642598263524,
|
||||||
|
"num_players": 5,
|
||||||
|
"name": "6 Suits",
|
||||||
|
"num_suits": 6,
|
||||||
|
"\u03c3": 36.55838248550461
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 51,
|
||||||
|
"rating": 1895.801292104732,
|
||||||
|
"num_players": 5,
|
||||||
|
"name": null,
|
||||||
|
"num_suits": 6,
|
||||||
|
"\u03c3": 14.915817466505452
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 52,
|
||||||
|
"rating": 1968.4174574449692,
|
||||||
|
"num_players": 5,
|
||||||
|
"name": "Clue Starved (5 Suits)",
|
||||||
|
"num_suits": 5,
|
||||||
|
"\u03c3": 50.09621953978423
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
|
@ -28,7 +28,7 @@ DB_TABLE_NAMES = [
|
||||||
|
|
||||||
DATABASE_SCHEMA_PATH = 'install/database_schema.sql'
|
DATABASE_SCHEMA_PATH = 'install/database_schema.sql'
|
||||||
DEFAULT_DB_CONFIG_PATH = 'install/default_db_config.yaml'
|
DEFAULT_DB_CONFIG_PATH = 'install/default_db_config.yaml'
|
||||||
DEFAULT_CONFIG_PATH = 'install/default_config.yaml'
|
DEFAULT_CONFIG_PATH = '../install/default_config.yaml'
|
||||||
|
|
||||||
VARIANTS_JSON_URL = 'https://raw.githubusercontent.com/Hanabi-Live/hanabi-live/main/packages/game/src/json/variants.json'
|
VARIANTS_JSON_URL = 'https://raw.githubusercontent.com/Hanabi-Live/hanabi-live/main/packages/game/src/json/variants.json'
|
||||||
|
|
||||||
|
|
382
src/minimize_loss.py
Normal file
382
src/minimize_loss.py
Normal file
|
@ -0,0 +1,382 @@
|
||||||
|
from typing import Iterable, List
|
||||||
|
|
||||||
|
from config import config_manager
|
||||||
|
from constants import UNWINNABLE_SEED_FRACTION
|
||||||
|
|
||||||
|
from dataclasses import dataclass
|
||||||
|
|
||||||
|
import json
|
||||||
|
import datetime
|
||||||
|
|
||||||
|
import numpy as np
|
||||||
|
from numpy.typing import NDArray
|
||||||
|
from scipy.optimize import minimize, Bounds
|
||||||
|
|
||||||
|
MAX_RATING_DIFF = 1200
|
||||||
|
RATING_PRIOR = 1600
|
||||||
|
# This allows us to do integer optimization on more fine-grained grid.
|
||||||
|
LCM_FACTOR = 60
|
||||||
|
NUM_RANDOM_INITS = 50
|
||||||
|
NUM_CV_ITERS = 50
|
||||||
|
|
||||||
|
VARIANT_DEFAULT_RATINGS = np.array([1400, 1400, 1800, 1800]).reshape((1, 4))
|
||||||
|
VARIANT_NUM_PLAYERS_MODIFIERS = np.array([0, 0, 100]).reshape((3, 1))
|
||||||
|
|
||||||
|
# As of season 1, this is [1400 1400 1800 1800 1400 1400 1800 1800 1500 1500 1900 1900].
|
||||||
|
VARIANT_RATING_PRIORS = np.ravel(
|
||||||
|
VARIANT_DEFAULT_RATINGS + VARIANT_NUM_PLAYERS_MODIFIERS
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class GlobalInfo:
|
||||||
|
rated_ids: Iterable[int]
|
||||||
|
variant_ids: Iterable[int]
|
||||||
|
player_counts: Iterable[int]
|
||||||
|
user_names: Iterable[str]
|
||||||
|
game_counts: Iterable[int]
|
||||||
|
|
||||||
|
@property
|
||||||
|
def rating_list_length(self):
|
||||||
|
return len(self.rated_ids) + len(self.variant_ids) * len(self.player_counts)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def rated_id_indices_in_rating_list(self):
|
||||||
|
return {id: index for index, id in dict(enumerate(self.rated_ids)).items()}
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class GameRow:
|
||||||
|
game_id: int
|
||||||
|
num_players: int
|
||||||
|
users: List[str]
|
||||||
|
user_ids: List[int]
|
||||||
|
user_rating_changes: List[float]
|
||||||
|
user_ratings_after: List[float]
|
||||||
|
variant_id: int
|
||||||
|
variant_name: str
|
||||||
|
num_suits: id
|
||||||
|
rating_type: int
|
||||||
|
seed: str
|
||||||
|
score: int
|
||||||
|
num_turns: int
|
||||||
|
datetime_finished: datetime.datetime
|
||||||
|
league_id: int
|
||||||
|
num_bdrs: int
|
||||||
|
num_crits_lost: int
|
||||||
|
game_outcomes: List[str]
|
||||||
|
variant_rating_change: float
|
||||||
|
variant_rating_after: float
|
||||||
|
|
||||||
|
|
||||||
|
def calculate_loss(
|
||||||
|
rating_list: Iterable[int],
|
||||||
|
game_list: Iterable[GameRow],
|
||||||
|
global_info: GlobalInfo,
|
||||||
|
p_win_lookup_table: NDArray[np.float32],
|
||||||
|
player_rating_priors: Iterable[int],
|
||||||
|
player_prior_weight: float,
|
||||||
|
variant_rating_priors: Iterable[int],
|
||||||
|
variant_prior_weight: float,
|
||||||
|
):
|
||||||
|
loss = np.float32(0)
|
||||||
|
|
||||||
|
for game in game_list:
|
||||||
|
team_ratings = [
|
||||||
|
rating_list[global_info.rated_id_indices_in_rating_list[user_id]]
|
||||||
|
for user_id in game["user_ids"]
|
||||||
|
]
|
||||||
|
team_rating = sum(team_ratings) / len(team_ratings)
|
||||||
|
|
||||||
|
variant_rating = rating_list[
|
||||||
|
calculate_variant_index_in_rating_list(
|
||||||
|
len(game["users"]), game["variant_id"], global_info
|
||||||
|
)
|
||||||
|
]
|
||||||
|
|
||||||
|
rating_diff = int(np.round(LCM_FACTOR * (team_rating - variant_rating)))
|
||||||
|
p_win = calculate_p_win(rating_diff, p_win_lookup_table)
|
||||||
|
|
||||||
|
game_won = game["game_outcomes"].count("Win") > 0
|
||||||
|
loss += calculate_loss_for_one_game(p_win, game_won)
|
||||||
|
|
||||||
|
# Add two dummy single-player games for each player with prior_weight * square-root of number of games weight - one won and lost
|
||||||
|
for player_rating_prior, rated_id, game_count in zip(
|
||||||
|
player_rating_priors, global_info.rated_ids, global_info.game_counts
|
||||||
|
):
|
||||||
|
team_rating = rating_list[global_info.rated_id_indices_in_rating_list[rated_id]]
|
||||||
|
variant_rating = player_rating_prior
|
||||||
|
|
||||||
|
rating_diff = int(np.round(LCM_FACTOR * (team_rating - variant_rating)))
|
||||||
|
p_win = calculate_p_win(rating_diff, p_win_lookup_table)
|
||||||
|
|
||||||
|
loss += (
|
||||||
|
np.sqrt(game_count)
|
||||||
|
* player_prior_weight
|
||||||
|
* calculate_loss_for_one_game(p_win, True)
|
||||||
|
)
|
||||||
|
loss += (
|
||||||
|
np.sqrt(game_count)
|
||||||
|
* player_prior_weight
|
||||||
|
* calculate_loss_for_one_game(p_win, False)
|
||||||
|
)
|
||||||
|
|
||||||
|
# Add two dummy single-player games for each player with prior_weight * square-root of number of games weight - one won and lost
|
||||||
|
for variant_rating, variant_rating_prior in zip(
|
||||||
|
rating_list[len(global_info.rated_ids) :],
|
||||||
|
variant_rating_priors,
|
||||||
|
):
|
||||||
|
team_rating = variant_rating_prior
|
||||||
|
|
||||||
|
rating_diff = int(np.round(LCM_FACTOR * (team_rating - variant_rating)))
|
||||||
|
p_win = calculate_p_win(rating_diff, p_win_lookup_table)
|
||||||
|
|
||||||
|
loss += (
|
||||||
|
np.sqrt(game_count)
|
||||||
|
* variant_prior_weight
|
||||||
|
* calculate_loss_for_one_game(p_win, True)
|
||||||
|
)
|
||||||
|
loss += (
|
||||||
|
np.sqrt(game_count)
|
||||||
|
* variant_prior_weight
|
||||||
|
* calculate_loss_for_one_game(p_win, False)
|
||||||
|
)
|
||||||
|
|
||||||
|
return loss
|
||||||
|
|
||||||
|
|
||||||
|
def calculate_loss_gradient(rating_list, *args):
|
||||||
|
y = calculate_loss(rating_list, *args)
|
||||||
|
eye = np.eye(len(rating_list), dtype=int)
|
||||||
|
|
||||||
|
gradient = [
|
||||||
|
calculate_loss(rating_list + eye[i], *args) - y for i in range(len(rating_list))
|
||||||
|
]
|
||||||
|
|
||||||
|
return gradient
|
||||||
|
|
||||||
|
|
||||||
|
def calculate_minloss_ratings(
|
||||||
|
game_list: List[GameRow],
|
||||||
|
global_info: GlobalInfo,
|
||||||
|
p_win_lookup_table: NDArray[np.float32],
|
||||||
|
player_rating_priors: int | Iterable[int] = RATING_PRIOR,
|
||||||
|
player_random_init_stdev: int = 0,
|
||||||
|
player_prior_weight=0.5,
|
||||||
|
variant_rating_priors: int | Iterable[int] = RATING_PRIOR,
|
||||||
|
variant_prior_weight=0.5,
|
||||||
|
variant_random_init_stdev: int = 0,
|
||||||
|
):
|
||||||
|
player_rating_priors = np.broadcast_to(player_rating_priors, len(rated_ids))
|
||||||
|
variant_rating_priors = np.broadcast_to(
|
||||||
|
variant_rating_priors, len(variant_ids) * len(player_counts)
|
||||||
|
)
|
||||||
|
|
||||||
|
prior_rating_list = np.concatenate(
|
||||||
|
(
|
||||||
|
player_rating_priors
|
||||||
|
+ np.random.normal(0, player_random_init_stdev, len(player_rating_priors)),
|
||||||
|
variant_rating_priors
|
||||||
|
+ np.random.normal(
|
||||||
|
0, variant_random_init_stdev, len(variant_rating_priors)
|
||||||
|
),
|
||||||
|
),
|
||||||
|
dtype=np.float32,
|
||||||
|
)
|
||||||
|
|
||||||
|
rating_list = minimize(
|
||||||
|
calculate_loss,
|
||||||
|
prior_rating_list,
|
||||||
|
(
|
||||||
|
game_list,
|
||||||
|
global_info,
|
||||||
|
p_win_lookup_table,
|
||||||
|
player_rating_priors,
|
||||||
|
player_prior_weight,
|
||||||
|
variant_rating_priors,
|
||||||
|
variant_prior_weight,
|
||||||
|
),
|
||||||
|
bounds=Bounds(RATING_PRIOR - MAX_RATING_DIFF, RATING_PRIOR + MAX_RATING_DIFF),
|
||||||
|
jac=calculate_loss_gradient,
|
||||||
|
tol=0.0001,
|
||||||
|
)
|
||||||
|
|
||||||
|
return rating_list
|
||||||
|
|
||||||
|
|
||||||
|
def calculate_p_win_lookup_table(rating_diff_indices: Iterable[int]):
|
||||||
|
lookup_table = np.full(2 * MAX_RATING_DIFF * LCM_FACTOR + 1, 0.5, dtype=np.float32)
|
||||||
|
|
||||||
|
for rating_diff_index in rating_diff_indices:
|
||||||
|
p_win = calculate_p_win(rating_diff_index, None)
|
||||||
|
lookup_table[rating_diff_index] = p_win
|
||||||
|
|
||||||
|
return lookup_table
|
||||||
|
|
||||||
|
|
||||||
|
def calculate_p_win(
|
||||||
|
rating_diff_index: int, lookup_table: NDArray[np.float32] | None = None
|
||||||
|
):
|
||||||
|
if lookup_table is None:
|
||||||
|
return (1 - UNWINNABLE_SEED_FRACTION) * (
|
||||||
|
1 / (1 + 10 ** (-(rating_diff_index / LCM_FACTOR) / 400))
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
return lookup_table[rating_diff_index]
|
||||||
|
|
||||||
|
|
||||||
|
# Cross-entropy loss
|
||||||
|
def calculate_loss_for_one_game(p_win: np.float32, game_won: bool):
|
||||||
|
if game_won:
|
||||||
|
loss = -np.log(p_win)
|
||||||
|
else:
|
||||||
|
loss = -np.log(1 - p_win)
|
||||||
|
|
||||||
|
return loss
|
||||||
|
|
||||||
|
|
||||||
|
def calculate_variant_index_in_rating_list(
|
||||||
|
player_count: int, variant_id: int, global_info: GlobalInfo
|
||||||
|
):
|
||||||
|
return (
|
||||||
|
len(global_info.rated_ids)
|
||||||
|
+ global_info.player_counts.index(player_count) * len(global_info.variant_ids)
|
||||||
|
+ global_info.variant_ids.index(variant_id)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def calculate_player_count_and_variant_id_from_variant_index(
|
||||||
|
index: int, global_info: GlobalInfo
|
||||||
|
):
|
||||||
|
num_players = global_info.player_counts[index // len(global_info.variant_ids)]
|
||||||
|
variant_id = global_info.variant_ids[index % len(global_info.variant_ids)]
|
||||||
|
|
||||||
|
return num_players, variant_id
|
||||||
|
|
||||||
|
|
||||||
|
def write_data(random_init_rating_lists, cv_rating_lists):
|
||||||
|
with open("../results/min_loss_rating_list.json", "w") as f:
|
||||||
|
rating_list = np.average(random_init_rating_lists, axis=0)
|
||||||
|
rating_stdevs = np.std(cv_rating_lists, axis=0, ddof=1)
|
||||||
|
|
||||||
|
players = []
|
||||||
|
for rated_id, user_name, rating, rating_stdev in zip(
|
||||||
|
global_info.rated_ids, global_info.user_names, rating_list, rating_stdevs
|
||||||
|
):
|
||||||
|
players.append(
|
||||||
|
{
|
||||||
|
"id": rated_id,
|
||||||
|
"rating": rating,
|
||||||
|
"username": user_name,
|
||||||
|
"stdev": rating_stdev,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
variants = []
|
||||||
|
|
||||||
|
for i, (rating, rating_stdev) in enumerate(
|
||||||
|
zip(
|
||||||
|
rating_list[len(global_info.rated_ids) :],
|
||||||
|
rating_stdevs[len(global_info.rated_ids) :],
|
||||||
|
)
|
||||||
|
):
|
||||||
|
(
|
||||||
|
num_players,
|
||||||
|
variant_id,
|
||||||
|
) = calculate_player_count_and_variant_id_from_variant_index(i, global_info)
|
||||||
|
variant_name = None
|
||||||
|
|
||||||
|
for game in game_list:
|
||||||
|
if (
|
||||||
|
len(game["users"]) == num_players
|
||||||
|
and game["variant_id"] == variant_id
|
||||||
|
):
|
||||||
|
variant_name = game["variant_name"]
|
||||||
|
num_suits = game["num_suits"]
|
||||||
|
|
||||||
|
variants.append(
|
||||||
|
{
|
||||||
|
"id": variant_id,
|
||||||
|
"rating": rating,
|
||||||
|
"num_players": num_players,
|
||||||
|
"name": variant_name,
|
||||||
|
"num_suits": num_suits,
|
||||||
|
"stdev": rating_stdev,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
s = json.dumps({"players": players, "variants": variants})
|
||||||
|
f.write(s)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
with open("../data/games.json") as game_data:
|
||||||
|
game_list = json.loads(game_data.read())
|
||||||
|
|
||||||
|
p_win_lookup_table = calculate_p_win_lookup_table(
|
||||||
|
np.arange(-MAX_RATING_DIFF * LCM_FACTOR, MAX_RATING_DIFF * LCM_FACTOR + 1)
|
||||||
|
)
|
||||||
|
|
||||||
|
config = config_manager.get_config()
|
||||||
|
|
||||||
|
rated_ids_dupes = [ID for game in game_list for ID in game["user_ids"]]
|
||||||
|
rated_ids = list(set(rated_ids_dupes))
|
||||||
|
|
||||||
|
variant_ids_dupes = [game["variant_id"] for game in game_list]
|
||||||
|
variant_ids = list(set(variant_ids_dupes))
|
||||||
|
|
||||||
|
nums_players = [game["num_players"] for game in game_list]
|
||||||
|
|
||||||
|
player_counts = list(range(config.min_player_count, config.max_player_count + 1))
|
||||||
|
game_counts = np.concatenate(
|
||||||
|
[rated_ids_dupes.count(ID) for ID in rated_ids],
|
||||||
|
np.zeros(len(variant_ids) * len(player_counts)),
|
||||||
|
)
|
||||||
|
|
||||||
|
user_names = list(np.full((len(rated_ids),), ""))
|
||||||
|
for game in game_list:
|
||||||
|
for user_id, user_name in zip(game["user_ids"], game["users"]):
|
||||||
|
user_names[rated_ids.index(user_id)] = user_name
|
||||||
|
|
||||||
|
global_info = GlobalInfo(
|
||||||
|
rated_ids, variant_ids, player_counts, user_names, game_counts
|
||||||
|
)
|
||||||
|
|
||||||
|
for variant_id, num_players in zip(variant_ids_dupes, nums_players):
|
||||||
|
game_counts[
|
||||||
|
len(rated_ids)
|
||||||
|
+ calculate_variant_index_in_rating_list(
|
||||||
|
num_players, variant_id, global_info
|
||||||
|
)
|
||||||
|
] += 1
|
||||||
|
global_info.game_counts = game_counts
|
||||||
|
|
||||||
|
random_init_rating_lists = []
|
||||||
|
cv_rating_lists = []
|
||||||
|
|
||||||
|
for _ in range(NUM_RANDOM_INITS):
|
||||||
|
random_init_rating_lists.append(
|
||||||
|
calculate_minloss_ratings(
|
||||||
|
game_list,
|
||||||
|
global_info,
|
||||||
|
p_win_lookup_table,
|
||||||
|
variant_rating_priors=VARIANT_RATING_PRIORS,
|
||||||
|
player_random_init_stdev=200,
|
||||||
|
variant_random_init_stdev=200,
|
||||||
|
).x
|
||||||
|
)
|
||||||
|
|
||||||
|
for _ in range(NUM_CV_ITERS):
|
||||||
|
cv_game_list = np.random.choice(game_list, len(game_list), replace=True)
|
||||||
|
cv_rating_lists.append(
|
||||||
|
calculate_minloss_ratings(
|
||||||
|
cv_game_list,
|
||||||
|
global_info,
|
||||||
|
p_win_lookup_table,
|
||||||
|
variant_rating_priors=VARIANT_RATING_PRIORS,
|
||||||
|
player_random_init_stdev=200,
|
||||||
|
variant_random_init_stdev=200,
|
||||||
|
).x
|
||||||
|
)
|
||||||
|
|
||||||
|
write_data(random_init_rating_lists, cv_rating_lists)
|
Loading…
Reference in a new issue