55import psycopg
66
77from core import Plugin , utils , get_translation , Node , Group , Report
8+ from datetime import datetime
89from discord import app_commands
910from plugins .competitive import rating
1011from psycopg .rows import dict_row
@@ -27,52 +28,71 @@ async def install(self) -> bool:
2728 async def init_trueskill (self ):
2829 # we need to calculate the TrueSkill values for players
2930 self .log .warning ("Calculating TrueSkill values for players... Please do NOT stop your bot!" )
30- ratings : dict [str , Rating ] = dict ()
31- async with self .apool .connection () as conn :
32- async with conn .cursor (row_factory = dict_row ) as cursor :
33- size = 1000
34- await cursor .execute ("""
35- SELECT p1.discord_id AS init_discord_id, m.init_id,
36- p2.discord_id AS target_discord_id, m.target_id
37- FROM missionstats m, players p1, players p2
38- WHERE p1.ucid = m.init_id
39- AND p2.ucid = m.target_id
40- AND event = 'S_EVENT_KILL' AND init_id != '-1' AND target_id != '-1'
41- AND init_side <> target_side
42- AND init_cat = 'Airplanes' AND target_cat = 'Airplanes'
43- ORDER BY id
44- """ )
45- rows = await cursor .fetchmany (size = size )
46- while len (rows ) > 0 :
47- for row in rows :
48- init_id = row ['init_discord_id' ] if row ['init_discord_id' ] != - 1 else row ['init_id' ]
49- target_id = row ['target_discord_id' ] if row ['target_discord_id' ] != - 1 else row ['target_id' ]
50- if init_id not in ratings :
51- ratings [init_id ] = rating .create_rating ()
52- if target_id not in ratings :
53- ratings [target_id ] = rating .create_rating ()
54- ratings [init_id ], ratings [target_id ] = rating .rate_1vs1 (
55- ratings [init_id ], ratings [target_id ])
56- rows = await cursor .fetchmany (size = size )
57- async with conn .transaction ():
58- for player_id , skill in ratings .items ():
59- if isinstance (player_id , str ):
60- await conn .execute ("""
61- INSERT INTO trueskill (player_ucid, skill_mu, skill_sigma)
62- VALUES (%s, %s, %s)
63- ON CONFLICT (player_ucid) DO UPDATE
64- SET skill_mu = EXCLUDED.skill_mu, skill_sigma = EXCLUDED.skill_sigma
65- """ , (player_id , skill .mu , skill .sigma ))
66- else :
67- cursor = await conn .execute ("SELECT ucid FROM players WHERE discord_id = %s" , (player_id , ))
31+ ratings : dict [str , Rating ] = {}
32+
33+ batch_size = 10000 # missionstats rows per batch
34+
35+ last_id = 0
36+ total_processed = 0
37+
38+ while True :
39+ async with self .apool .connection () as conn :
40+ async with conn .transaction ():
41+ async with conn .cursor (row_factory = dict_row ) as cursor :
42+ await cursor .execute ("""
43+ SELECT id, init_id, target_id, time
44+ FROM missionstats
45+ WHERE event = 'S_EVENT_KILL'
46+ AND init_id != '-1'
47+ AND target_id != '-1'
48+ AND init_side <> target_side
49+ AND init_cat = 'Airplanes'
50+ AND target_cat = 'Airplanes'
51+ AND id > %s
52+ ORDER BY id
53+ LIMIT %s
54+ """ , (last_id , batch_size ))
6855 rows = await cursor .fetchall ()
56+ if not rows :
57+ break
58+
59+ # Compute rating changes and collect upserts for this batch
60+ insert_params : list [tuple [str , float , float , datetime ]] = []
6961 for row in rows :
70- await conn .execute ("""
71- INSERT INTO trueskill (player_ucid, skill_mu, skill_sigma)
72- VALUES (%s, %s, %s)
62+ init_id = row ['init_id' ]
63+ target_id = row ['target_id' ]
64+
65+ if init_id not in ratings :
66+ ratings [init_id ] = rating .create_rating ()
67+ if target_id not in ratings :
68+ ratings [target_id ] = rating .create_rating ()
69+
70+ ratings [init_id ], ratings [target_id ] = rating .rate_1vs1 (
71+ ratings [init_id ], ratings [target_id ]
72+ )
73+
74+ init_rating = ratings [init_id ]
75+ target_rating = ratings [target_id ]
76+ t = row ['time' ]
77+
78+ insert_params .append ((init_id , float (init_rating .mu ), float (init_rating .sigma ), t ))
79+ insert_params .append ((target_id , float (target_rating .mu ), float (target_rating .sigma ), t ))
80+
81+ last_id = row ['id' ]
82+
83+ if insert_params :
84+ await cursor .executemany ("""
85+ INSERT INTO trueskill (player_ucid, skill_mu, skill_sigma, time)
86+ VALUES (%s, %s, %s, %s)
7387 ON CONFLICT (player_ucid) DO UPDATE
74- SET skill_mu = EXCLUDED.skill_mu, skill_sigma = EXCLUDED.skill_sigma
75- """ , (row [0 ], skill .mu , skill .sigma ))
88+ SET skill_mu = EXCLUDED.skill_mu,
89+ skill_sigma = EXCLUDED.skill_sigma,
90+ time = EXCLUDED.time
91+ """ , insert_params )
92+
93+ total_processed += len (rows )
94+ self .log .info (f"- { total_processed } missionstats rows processed (up to id { last_id } )" )
95+
7696 self .log .info ("TrueSkill values calculated." )
7797
7898 async def update_ucid (self , conn : psycopg .AsyncConnection , old_ucid : str , new_ucid : str ) -> None :
@@ -259,6 +279,26 @@ async def delete(self, interaction: discord.Interaction,
259279 # noinspection PyUnresolvedReferences
260280 await interaction .followup .send (_ ("TrueSkill:tm: ratings deleted." ), ephemeral = True )
261281
282+ @trueskill .command (description = _ ('Regenerate TrueSkill:tm: ratings' ))
283+ @utils .app_has_role ('Admin' )
284+ @app_commands .guild_only ()
285+ async def regenerate (self , interaction : discord .Interaction ):
286+ if not await utils .yn_question (interaction ,
287+ question = _ ("Do you really want to regenerate **all** TrueSkill:tm: ratings?" ),
288+ message = _ ("This can take a while." )):
289+ await interaction .followup .send (_ ("Aborted." ), ephemeral = True )
290+ return
291+ ephemeral = utils .get_ephemeral (interaction )
292+ async with self .apool .connection () as conn :
293+ async with conn .transaction ():
294+ await conn .execute ("TRUNCATE trueskill CASCADE" )
295+ await conn .execute ("TRUNCATE trueskill_hist CASCADE" )
296+ await interaction .followup .send (_ ("TrueSkill:tm: ratings deleted.\n Generating new values now ..." ),
297+ ephemeral = ephemeral )
298+ channel = interaction .channel
299+ await self .init_trueskill ()
300+ await channel .send (_ ("TrueSkill:tm: ratings regenerated." ))
301+
262302
263303async def setup (bot : DCSServerBot ):
264304 await bot .add_cog (Competitive (bot , CompetitiveListener ))
0 commit comments