-
Notifications
You must be signed in to change notification settings - Fork 2
/
Copy pathauto_rebalance.py
306 lines (246 loc) · 11 KB
/
auto_rebalance.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
"""
This is a plugin created by ShiN0
Copyright (c) 2018 ShiN0
<https://www.github.com/mgaertne/minqlx-plugin-tests>
You are free to modify this plugin to your own one, except for the version related code.
This plugin automatically rebalances new-joiners at round start based upon the ratings in the balance plugin.
It's intended to run with the default balance plugin, and will print an error on every round countdown if the
balance plugin is not loaded together with this one.
"""
import minqlx
from minqlx import Plugin
DEFAULT_RATING = 1500
SUPPORTED_GAMETYPES = ("ca", "ctf", "dom", "ft", "tdm", "duel", "ffa")
# noinspection PyPep8Naming
class auto_rebalance(Plugin):
"""
Auto rebalance plugin for minqlx
Rebalances new players joined since the last round countdown based upon the ratings in the balance plugin to yield
better balanced teams overall.
Uses:
* qlx_rebalanceScoreDiffThreshold (default: "3") The difference between red team score and blue team score
threshold that will trigger a team switch suggestion at round end.
* qlx_rebalanceWinningStreakThreshold (default: "3") The Threshold when a team is on a winning streak for
winning this amount of round in a row that will trigger a team switch suggestion at round end.
* qlx_rebalanceNumAnnouncements (default: "2") The number of announcements the plugin will make for a current
switch suggestion at round end
"""
def __init__(self):
"""
default constructor, adds the plugin hooks and initializes variables used
"""
super().__init__()
self.last_new_player_id = None
Plugin.set_cvar_once("qlx_rebalanceScoreDiffThreshold", "3")
Plugin.set_cvar_once("qlx_rebalanceWinningStreakThreshold", "3")
Plugin.set_cvar_once("qlx_rebalanceNumAnnouncements", "2")
self.score_diff_suggestion_threshold = (
Plugin.get_cvar("qlx_rebalanceScoreDiffThreshold", int) or 3
)
self.winning_streak_suggestion_threshold = (
Plugin.get_cvar("qlx_rebalanceWinningStreakThreshold", int) or 3
)
self.num_announcements = (
Plugin.get_cvar("qlx_rebalanceNumAnnouncements", int) or 2
)
self.add_hook("team_switch_attempt", self.handle_team_switch_attempt)
self.add_hook(
"round_start", self.handle_round_start, priority=minqlx.PRI_LOWEST
)
self.add_hook("round_end", self.handle_round_end, priority=minqlx.PRI_LOWEST)
for event in ["map", "game_countdown"]:
# noinspection PyTypeChecker
self.add_hook(event, self.handle_reset_winning_teams)
self.winning_teams = []
self.plugin_version = f"{self.name} Version: v0.1.1"
self.logger.info(self.plugin_version)
def handle_team_switch_attempt(self, player, old, new):
"""
Handles the case where a player switches from spectators to "red", "blue", or "any" team, and
the resulting teams would be suboptimal balanced.
:param: player: the player that attempted the switch
:param: old: the old team of the switching player
:param: new: the new team that swicthting player would be put onto
:return: minqlx.RET_NONE if the team switch should be allowed or minqlx.RET_STOP_EVENT if the switch should not
be allowed, and we put the player on a better-suited team.
"""
if not self.game:
return minqlx.RET_NONE
if self.last_new_player_id == player.steam_id and new in ["spectator", "free"]:
self.last_new_player_id = None
if self.game.state != "in_progress":
return minqlx.RET_NONE
if old not in ["spectator", "free"] or new not in ["red", "blue", "any"]:
return minqlx.RET_NONE
if "balance" not in self.plugins:
Plugin.msg(
"^1balance^7 plugin not loaded, ^1auto rebalance^7 not possible."
)
return minqlx.RET_NONE
gametype = self.game.type_short
if gametype not in SUPPORTED_GAMETYPES:
return minqlx.RET_NONE
teams = self.teams()
if len(teams["red"]) == len(teams["blue"]):
self.last_new_player_id = player.steam_id
return minqlx.RET_NONE
if not self.last_new_player_id:
return minqlx.RET_NONE
last_new_player = Plugin.player(self.last_new_player_id)
if not last_new_player:
self.last_new_player_id = None
return minqlx.RET_NONE
other_than_last_players_team = self.other_team(last_new_player.team)
new_player_team = teams[other_than_last_players_team].copy() + [player]
proposed_diff = self.calculate_player_average_difference(
gametype, teams[last_new_player.team].copy(), new_player_team
)
alternative_team_a = [
player
for player in teams[last_new_player.team]
if player != last_new_player
] + [player]
alternative_team_b = teams[other_than_last_players_team].copy() + [
last_new_player
]
alternative_diff = self.calculate_player_average_difference(
gametype, alternative_team_a, alternative_team_b
)
self.last_new_player_id = None
if proposed_diff > alternative_diff:
last_new_player.tell(
f"{last_new_player.clean_name}, you have been moved to "
f"{self.format_team(other_than_last_players_team)} to maintain team balance."
)
last_new_player.put(other_than_last_players_team)
if new in [last_new_player.team]:
return minqlx.RET_NONE
if new not in ["any"]:
player.tell(
f"{player.clean_name}, you have been moved to {self.format_team(last_new_player.team)} "
f"to maintain team balance."
)
player.put(last_new_player.team)
return minqlx.RET_STOP_ALL
if new not in ["any", other_than_last_players_team]:
player.put(other_than_last_players_team)
return minqlx.RET_STOP_ALL
return minqlx.RET_NONE
# noinspection PyMethodMayBeStatic
def other_team(self, team):
"""
Calculates the other playing team based upon the provided team string.
:param: team: the team the other playing team should be determined for
:return: the other playing team based upon the provided team string
"""
if team == "red":
return "blue"
return "red"
# noinspection PyMethodMayBeStatic
def format_team(self, team):
if team == "red":
return "^1red^7"
if team == "blue":
return "^4blue^7"
return f"^3{team}^7"
def calculate_player_average_difference(self, gametype, team1, team2):
"""
calculates the difference between the team averages of the two provided teams for the given gametype
the result will be absolute, i.e. always be greater than or equal to 0
:param: gametype: the gametype to calculate the teams' averages for
:param: team1: the first team to calculate the team averages for
:param: team2: the second team to calculate the team averages for
:return: the absolute difference between the two team's averages
"""
team1_avg = self.team_average(gametype, team1)
team2_avg = self.team_average(gametype, team2)
return abs(team1_avg - team2_avg)
def team_average(self, gametype, team):
"""
Calculates the average rating of a team.
:param: gametype: the gametype to determine the ratings for
:param: team: the list of players the average should be calculated for
:return: the average rating for the given team and gametype
"""
if not team or len(team) == 0:
return 0
# noinspection PyUnresolvedReferences
ratings = self.plugins["balance"].ratings
average = 0.0
for p in team:
if p.steam_id not in ratings:
average += DEFAULT_RATING
else:
average += ratings[p.steam_id][gametype]["elo"]
average /= len(team)
return average
def handle_round_start(self, _roundnumber):
"""
Remembers the steam ids of all players at round startup
"""
self.last_new_player_id = None
@minqlx.delay(1.5)
def handle_round_end(self, data):
"""
Triggered when a round has ended
:param: data: the round end data with the round number and which team won
"""
if not self.game:
return minqlx.RET_NONE
winning_team = data["TEAM_WON"].lower()
self.winning_teams.append(winning_team)
gametype = self.game.type_short
if gametype not in SUPPORTED_GAMETYPES:
return minqlx.RET_NONE
if (
self.game.roundlimit in [self.game.blue_score, self.game.red_score]
or self.game.blue_score < 0
or self.game.red_score < 0
):
return minqlx.RET_NONE
if abs(
self.game.red_score - self.game.blue_score
) < self.score_diff_suggestion_threshold and not self.team_is_on_a_winning_streak(
winning_team
):
return minqlx.RET_NONE
if self.announced_often_enough(winning_team):
return minqlx.RET_NONE
teams = self.teams()
if len(teams["red"]) != len(teams["blue"]):
return minqlx.RET_NONE
# noinspection PyProtectedMember
if "balance" in minqlx.Plugin._loaded_plugins:
# noinspection PyProtectedMember
b = Plugin._loaded_plugins["balance"]
players = {p.steam_id: gametype for p in teams["red"] + teams["blue"]}
# noinspection PyUnresolvedReferences
b.add_request(players, b.callback_teams, minqlx.CHAT_CHANNEL)
return minqlx.RET_NONE
def team_is_on_a_winning_streak(self, team):
"""
checks whether the given team is on a winning streak by comparing the last teams that won
:param: team: the team to check for a winning streak
:return True if the team is on a winning streak or False if not
"""
return self.winning_teams[
-self.winning_streak_suggestion_threshold:
] == self.winning_streak_suggestion_threshold * [team]
def announced_often_enough(self, winning_team):
if not self.game:
return False
maximum_announcements = (
self.winning_streak_suggestion_threshold + self.num_announcements
)
return abs(
self.game.red_score - self.game.blue_score
) > self.score_diff_suggestion_threshold + self.num_announcements or self.winning_teams[
-maximum_announcements:
] == maximum_announcements * [
winning_team
]
def handle_reset_winning_teams(self, *_args, **_kwargs):
"""
resets the winning teams list
"""
self.winning_teams = []