1616# You should have received a copy of the GNU General Public License
1717# along with minqlx. If not, see <http://www.gnu.org/licenses/>.
1818
19+ from statistics import mean
1920import minqlx
2021import requests
2122import itertools
2829CACHE_EXPIRE = 60 * 10 # 10 minutes TTL.
2930DEFAULT_RATING = 1500
3031UNTRACKED_RATING = 9999
32+ AD_ELO_THRESHOLD = 1400
3133SUPPORTED_GAMETYPES = ("ad" , "ca" , "ctf" , "dom" , "ft" , "tdm" )
3234# Externally supported game types. Used by !getrating for game types the API works with.
3335EXT_SUPPORTED_GAMETYPES = ("ad" , "ca" , "ctf" , "dom" , "ft" , "tdm" , "duel" , "ffa" )
@@ -82,7 +84,7 @@ def handle_round_countdown(self, *args, **kwargs):
8284 def f ():
8385 self .execute_suggestion ()
8486 f ()
85-
87+
8688 self .in_countdown = True
8789
8890 def handle_round_start (self , * args , ** kwargs ):
@@ -100,7 +102,7 @@ def f():
100102 if len (players ["red" ] + players ["blue" ]) % 2 != 0 :
101103 self .msg ("Teams were ^6NOT^7 balanced due to the total number of players being an odd number." )
102104 return
103-
105+
104106 players = dict ([(p .steam_id , gt ) for p in players ["red" ] + players ["blue" ]])
105107 self .add_request (players , self .callback_balance , minqlx .CHAT_CHANNEL )
106108 f ()
@@ -166,7 +168,7 @@ def fetch_ratings(self, players, request_id):
166168 last_status = res .status_code
167169 if res .status_code != requests .codes .ok :
168170 continue
169-
171+
170172 js = res .json ()
171173 if "players" not in js :
172174 last_status = - 1
@@ -181,14 +183,14 @@ def fetch_ratings(self, players, request_id):
181183 with self .ratings_lock :
182184 if sid not in self .ratings :
183185 self .ratings [sid ] = {}
184-
186+
185187 for gt in p :
186188 p [gt ]["time" ] = t
187189 p [gt ]["local" ] = False
188190 self .ratings [sid ][gt ] = p [gt ]
189191 if self .ratings [sid ][gt ]["elo" ] == 0 and self .ratings [sid ][gt ]["games" ] == 0 :
190192 self .ratings [sid ][gt ]["elo" ] = DEFAULT_RATING
191-
193+
192194 if sid in players and gt == players [sid ]:
193195 # The API gave us the game type we wanted, so we remove it.
194196 del players [sid ]
@@ -304,13 +306,13 @@ def callback_getrating(self, players, channel, gametype):
304306 name = player .name
305307 else :
306308 name = sid
307-
309+
308310 channel .reply ("{} has a rating of ^6{}^7 in {}." .format (name , self .ratings [sid ][gametype ]["elo" ], gametype .upper ()))
309311
310312 def cmd_setrating (self , player , msg , channel ):
311313 if len (msg ) < 3 :
312314 return minqlx .RET_USAGE
313-
315+
314316 try :
315317 sid = int (msg [1 ])
316318 target_player = None
@@ -323,7 +325,7 @@ def cmd_setrating(self, player, msg, channel):
323325 except minqlx .NonexistentPlayerError :
324326 player .tell ("Invalid client ID. Use either a client ID or a SteamID64." )
325327 return minqlx .RET_STOP_ALL
326-
328+
327329 try :
328330 rating = int (msg [2 ])
329331 except ValueError :
@@ -334,7 +336,7 @@ def cmd_setrating(self, player, msg, channel):
334336 name = target_player .name
335337 else :
336338 name = sid
337-
339+
338340 gt = self .game .type_short
339341 self .db [RATING_KEY .format (sid , gt )] = rating
340342
@@ -350,7 +352,7 @@ def cmd_setrating(self, player, msg, channel):
350352 def cmd_remrating (self , player , msg , channel ):
351353 if len (msg ) < 2 :
352354 return minqlx .RET_USAGE
353-
355+
354356 try :
355357 sid = int (msg [1 ])
356358 target_player = None
@@ -363,12 +365,12 @@ def cmd_remrating(self, player, msg, channel):
363365 except minqlx .NonexistentPlayerError :
364366 player .tell ("Invalid client ID. Use either a client ID or a SteamID64." )
365367 return minqlx .RET_STOP_ALL
366-
368+
367369 if target_player :
368370 name = target_player .name
369371 else :
370372 name = sid
371-
373+
372374 gt = self .game .type_short
373375 del self .db [RATING_KEY .format (sid , gt )]
374376
@@ -389,7 +391,7 @@ def cmd_balance(self, player, msg, channel):
389391 if len (teams ["red" ] + teams ["blue" ]) % 2 != 0 :
390392 player .tell ("The total number of players should be an even number." )
391393 return minqlx .RET_STOP_ALL
392-
394+
393395 players = dict ([(p .steam_id , gt ) for p in teams ["red" ] + teams ["blue" ]])
394396 self .add_request (players , self .callback_balance , minqlx .CHAT_CHANNEL )
395397
@@ -453,12 +455,12 @@ def cmd_teams(self, player, msg, channel):
453455 if gt not in SUPPORTED_GAMETYPES :
454456 player .tell ("This game mode is not supported by the balance plugin." )
455457 return minqlx .RET_STOP_ALL
456-
458+
457459 teams = self .teams ()
458460 if len (teams ["red" ]) != len (teams ["blue" ]):
459461 player .tell ("Both teams should have the same number of players." )
460462 return minqlx .RET_STOP_ALL
461-
463+
462464 teams = dict ([(p .steam_id , gt ) for p in teams ["red" ] + teams ["blue" ]])
463465 self .add_request (teams , self .callback_teams , channel )
464466
@@ -514,7 +516,7 @@ def cmd_agree(self, player, msg, channel):
514516 """After the bot suggests a switch, players in question can use this to agree to the switch."""
515517 if self .suggested_pair and not all (self .suggested_agree ):
516518 p1 , p2 = self .suggested_pair
517-
519+
518520 if p1 == player :
519521 self .suggested_agree [0 ] = True
520522 elif p2 == player :
@@ -534,7 +536,7 @@ def cmd_ratings(self, player, msg, channel):
534536 if gt not in EXT_SUPPORTED_GAMETYPES :
535537 player .tell ("This game mode is not supported by the balance plugin." )
536538 return minqlx .RET_STOP_ALL
537-
539+
538540 players = dict ([(p .steam_id , gt ) for p in self .players ()])
539541 self .add_request (players , self .callback_ratings , channel )
540542
@@ -569,6 +571,29 @@ def callback_ratings(self, players, channel):
569571
570572 def suggest_switch (self , teams , gametype ):
571573 """Suggest a switch based on average team ratings."""
574+
575+ if gametype == "ad" :
576+ # when there is an even amount of players with elo >= AD_ELO_THRESHOLD
577+ # make sure they are divided evenly over the teams
578+ elos_red = [
579+ (p , self .ratings [p .steam_id ][gametype ]["elo" ]) for p in teams ["red" ]
580+ ]
581+ elos_red_high = sorted ([p for p in elos_red if p [1 ] >= AD_ELO_THRESHOLD ], key = lambda x : x [1 ])
582+ elos_red_low = sorted ([p for p in elos_red if p [1 ] < AD_ELO_THRESHOLD ], key = lambda x : x [1 ])
583+ elos_blue = [
584+ (p , self .ratings [p .steam_id ][gametype ]["elo" ]) for p in teams ["blue" ]
585+ ]
586+ elos_blue_high = sorted ([p for p in elos_blue if p [1 ] >= AD_ELO_THRESHOLD ], key = lambda x : x [1 ])
587+ elos_blue_low = sorted ([p for p in elos_blue if p [1 ] < AD_ELO_THRESHOLD ], key = lambda x : x [1 ])
588+ total_high_elos = len (elos_red_high ) + len (elos_blue_high )
589+
590+ if total_high_elos > 1 and len (elos_red_high ) != len (elos_blue_high ):
591+ if len (elos_red_high ) > len (elos_blue_high ):
592+ return ((elos_red_high [- 1 ], elos_blue_low [- 1 ]), 0 )
593+ else :
594+ return ((elos_red_low [- 1 ], elos_blue_high [- 1 ]), 0 )
595+
596+
572597 avg_red = self .team_average (teams ["red" ], gametype )
573598 avg_blue = self .team_average (teams ["blue" ], gametype )
574599 cur_diff = abs (avg_red - avg_blue )
@@ -599,9 +624,11 @@ def team_average(self, team, gametype):
599624 """Calculates the average rating of a team."""
600625 avg = 0
601626 if team :
602- for p in team :
603- avg += self .ratings [p .steam_id ][gametype ]["elo" ]
604- avg /= len (team )
627+ elos = [self .ratings [p .steam_id ][gametype ]["elo" ] for p in team ]
628+ # for ad we skip the high elo players when calculating the avg
629+ if gametype == "ad" :
630+ elos = [elo for elo in elos if elo < AD_ELO_THRESHOLD ]
631+ avg = mean (elos )
605632
606633 return avg
607634
@@ -612,9 +639,9 @@ def execute_suggestion(self):
612639 p2 .update ()
613640 except minqlx .NonexistentPlayerError :
614641 return
615-
642+
616643 if p1 .team != "spectator" and p2 .team != "spectator" :
617644 self .switch (self .suggested_pair [0 ], self .suggested_pair [1 ])
618-
645+
619646 self .suggested_pair = None
620647 self .suggested_agree = [False , False ]
0 commit comments