Skip to content

Commit 2341ec3

Browse files
committed
gametypes: better managing
All gametype information is now in one file
1 parent af53b62 commit 2341ec3

24 files changed

+245
-86
lines changed

contrib/set_rating.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
#!/usr/bin/env python3
22

33
import argparse
4+
45
from qllr.db import db_connect
56

67
parser = argparse.ArgumentParser()

qllr/blueprints/scoreboard/methods.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
from qllr.common import DATETIME_FORMAT, convert_timestamp_to_tuple
66
from qllr.exceptions import MatchNotFound
7+
from qllr.gametypes import GAMETYPE_RULES
78
from qllr.settings import USE_AVG_PERF
89

910

@@ -236,10 +237,13 @@ async def get_scoreboard(con: Connection, match_id: str):
236237
"""
237238
weapons_available = await con.fetchval(query, match_id)
238239

240+
rules = GAMETYPE_RULES[summary["gt_short"]]
239241
return {
240242
"summary": summary,
241243
"player_stats": {"weapons": player_weapon_stats, "medals": player_medal_stats},
242244
"team_stats": {"overall": overall_stats},
243245
"weapons_available": weapons_available,
244246
"medals_available": medals_available,
247+
"medals_in_scoreboard_mid": rules.medals_in_scoreboard_mid(),
248+
"medals_in_scoreboard_right": rules.medals_in_scoreboard_right(),
245249
}

qllr/db.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import asyncio
2-
from collections.abc import MutableMapping
32
from collections import OrderedDict
3+
from collections.abc import MutableMapping
44
from typing import List
55
from urllib.parse import urlparse
66

qllr/gametypes.py

Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
GAMETYPE_RULES = {}
2+
3+
4+
def detect_by_match_report(data):
5+
for short, gt in GAMETYPE_RULES.items():
6+
if gt.force_by_match_report(data):
7+
return short
8+
9+
return data["game_meta"]["G"]
10+
11+
12+
class AbstractGametype:
13+
def calculate_player_perf(self, player_data, time_factor):
14+
raise NotImplementedError() # pragma: nocover
15+
16+
def force_by_match_report(self, data):
17+
return False
18+
19+
def override_min_player_count(self):
20+
return None
21+
22+
def extra_factor(self, matches, wins, losses):
23+
return 1
24+
25+
def medals_in_scoreboard_mid(self):
26+
return []
27+
28+
def medals_in_scoreboard_right(self):
29+
return []
30+
31+
32+
# gametype rules are defined below
33+
34+
35+
class GametypeAD(AbstractGametype):
36+
def calc_player_perf(self, player_data, time_factor):
37+
frags_count = int(player_data["scoreboard-kills"])
38+
capture_count = int(player_data["medal-captures"])
39+
damage_dealt = int(player_data["scoreboard-pushes"])
40+
return (damage_dealt / 100 + frags_count + capture_count) * time_factor
41+
42+
def medals_in_scoreboard_mid(self):
43+
return ["captures", "defends"]
44+
45+
46+
class GametypeCA(AbstractGametype):
47+
def calc_player_perf(self, player_data, time_factor):
48+
frags_count = int(player_data["scoreboard-kills"])
49+
damage_dealt = int(player_data["scoreboard-pushes"])
50+
return (damage_dealt / 100 + 0.25 * frags_count) * time_factor
51+
52+
53+
class GametypeCTF(AbstractGametype):
54+
def calc_player_perf(self, player_data, time_factor):
55+
score = int(player_data["scoreboard-score"])
56+
damage_dealt = int(player_data["scoreboard-pushes"])
57+
damage_taken = int(player_data["scoreboard-destroyed"])
58+
win = 1 if "win" in player_data else 0
59+
60+
damage_dealt = int(player_data["scoreboard-pushes"])
61+
return (
62+
(damage_dealt / damage_taken * (score + damage_dealt / 20) * time_factor)
63+
/ 2.35
64+
+ win * 300,
65+
)
66+
67+
def medals_in_scoreboard_mid(self):
68+
return ["captures", "assists", "defends"]
69+
70+
71+
class GametypeFT(AbstractGametype):
72+
def calc_player_perf(self, player_data, time_factor):
73+
damage_dealt = int(player_data["scoreboard-pushes"])
74+
frags_count = int(player_data["scoreboard-kills"])
75+
deaths_count = int(player_data["scoreboard-deaths"])
76+
assists_count = int(player_data["medal-assists"])
77+
78+
damage_dealt = int(player_data["scoreboard-pushes"])
79+
return (
80+
damage_dealt / 100 + 0.5 * (frags_count - deaths_count) + 2 * assists_count
81+
) * time_factor
82+
83+
def medals_in_scoreboard_mid(self):
84+
return ["assists"]
85+
86+
87+
class GametypeTDM(AbstractGametype):
88+
def calc_player_perf(self, player_data, time_factor):
89+
damage_dealt = int(player_data["scoreboard-pushes"])
90+
frags_count = int(player_data["scoreboard-kills"])
91+
deaths_count = int(player_data["scoreboard-deaths"])
92+
damage_taken = int(player_data["scoreboard-destroyed"])
93+
94+
return (
95+
0.5 * (frags_count - deaths_count)
96+
+ 0.004 * (damage_dealt - damage_taken)
97+
+ 0.003 * damage_dealt
98+
) * time_factor
99+
100+
def extra_factor(self, matches, wins, losses):
101+
return 1 + (0.15 * (wins / matches - losses / matches))
102+
103+
def medals_in_scoreboard_right(self):
104+
return ["excellent", "impressive"]
105+
106+
107+
class GametypeTDM2V2(GametypeTDM):
108+
def force_by_match_report(self, data):
109+
return data["game_meta"]["G"] == "tdm" and len(data["players"]) == 4
110+
111+
def override_min_player_count(self):
112+
return 4
113+
114+
115+
GAMETYPE_RULES["ad"] = GametypeAD()
116+
GAMETYPE_RULES["ca"] = GametypeCA()
117+
GAMETYPE_RULES["ctf"] = GametypeCTF()
118+
GAMETYPE_RULES["ft"] = GametypeFT()
119+
GAMETYPE_RULES["tdm"] = GametypeTDM()
120+
GAMETYPE_RULES["tdm2v2"] = GametypeTDM2V2()

qllr/settings.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22
from starlette.datastructures import URL, CommaSeparatedStrings
33
from trueskill import MU, SIGMA
44

5+
from .gametypes import GAMETYPE_RULES
6+
57
config = Config(".env")
68

79
SUPPORTED_GAMETYPES = ("ad", "ca", "ctf", "ft", "tdm", "tdm2v2")
@@ -34,6 +36,5 @@
3436
)
3537

3638
INITIAL_R2_VALUE = INITIAL_R1_MEAN.copy()
37-
MIN_PLAYER_COUNT_IN_MATCH_TO_RATE["tdm2v2"] = 4
3839

3940
AVG_PERF_GAMETYPES = [gt for gt in SUPPORTED_GAMETYPES if USE_AVG_PERF[gt]]

qllr/submission.py

Lines changed: 5 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
from .common import log_exception
1010
from .db import cache, db_connect, get_db_pool
1111
from .exceptions import *
12+
from .gametypes import GAMETYPE_RULES, detect_by_match_report
1213
from .settings import (
1314
INITIAL_R1_DEVIATION,
1415
INITIAL_R1_MEAN,
@@ -89,10 +90,6 @@ def parse_stats_submission(body):
8990
return {"game_meta": game_meta, "players": players, "teams": teams}
9091

9192

92-
def is_tdm2v2(data):
93-
return data["game_meta"]["G"] == "tdm" and len(data["players"]) == 4
94-
95-
9693
async def get_factory_id(con: Connection, factory: str):
9794
stmt = await con.prepare(
9895
"SELECT factory_id FROM factories WHERE factory_short = $1"
@@ -129,44 +126,13 @@ def count_player_match_perf(gametype, player_data, match_duration):
129126
player_data[k] = 0
130127

131128
alive_time = int(player_data["alivetime"])
132-
score = int(player_data["scoreboard-score"])
133-
damage_dealt = int(player_data["scoreboard-pushes"])
134-
damage_taken = int(player_data["scoreboard-destroyed"])
135-
frags_count = int(player_data["scoreboard-kills"])
136-
deaths_count = int(player_data["scoreboard-deaths"])
137-
capture_count = int(player_data["medal-captures"])
138-
defends_count = int(player_data["medal-defends"])
139-
assists_count = int(player_data["medal-assists"])
140-
win = 1 if "win" in player_data else 0
141129

142130
if alive_time < match_duration / 2:
143131
return None
144132
else:
145133
time_factor = 1200.0 / alive_time
146134

147-
return {
148-
"ad": (damage_dealt / 100 + frags_count + capture_count) * time_factor,
149-
"ca": (damage_dealt / 100 + 0.25 * frags_count) * time_factor,
150-
"ctf": (damage_dealt / damage_taken * (score + damage_dealt / 20) * time_factor)
151-
/ 2.35
152-
+ win * 300,
153-
"ft": (
154-
damage_dealt / 100 + 0.5 * (frags_count - deaths_count) + 2 * assists_count
155-
)
156-
* time_factor,
157-
"tdm2v2": (
158-
0.5 * (frags_count - deaths_count)
159-
+ 0.004 * (damage_dealt - damage_taken)
160-
+ 0.003 * damage_dealt
161-
)
162-
* time_factor,
163-
"tdm": (
164-
0.5 * (frags_count - deaths_count)
165-
+ 0.004 * (damage_dealt - damage_taken)
166-
+ 0.003 * damage_dealt
167-
)
168-
* time_factor,
169-
}[gametype]
135+
return GAMETYPE_RULES[gametype].calc_player_perf(player_data, time_factor)
170136

171137

172138
def count_multiple_players_match_perf(gametype, all_players_data, match_duration):
@@ -195,12 +161,6 @@ def count_multiple_players_match_perf(gametype, all_players_data, match_duration
195161
async def _calc_ratings_avg_perf(
196162
con: Connection, match_id: str, gametype_id: int, map_id: Optional[int] = None
197163
):
198-
def extra_factor(gametype, matches, wins, losses):
199-
try:
200-
return {"tdm": (1 + (0.15 * (wins / matches - losses / matches)))}[gametype]
201-
except KeyError:
202-
return 1
203-
204164
if map_id is None:
205165
ratings_subquery = """
206166
SELECT steam_id, r2_value AS rating
@@ -279,7 +239,8 @@ def extra_factor(gametype, matches, wins, losses):
279239

280240
row = await con.fetchrow(query, steam_id, gametype_id, match_id)
281241
gametype = [k for k, v in cache.GAMETYPE_IDS.items() if v == gametype_id][0]
282-
new_rating = row[3] * extra_factor(gametype, row[0], row[1], row[2])
242+
rules = GAMETYPE_RULES[gametype]
243+
new_rating = row[3] * rules.extra_factor(row[0], row[1], row[2])
283244

284245
result[steam_id] = {"old": old_rating, "new": new_rating, "team": team}
285246

@@ -543,13 +504,10 @@ async def _submit_match(data):
543504
raise InvalidMatchReport("Match id not given")
544505

545506
try:
546-
gametype = data["game_meta"]["G"]
507+
gametype = detect_by_match_report(data)
547508
except KeyError:
548509
raise InvalidMatchReport("Gametype not given")
549510

550-
if is_tdm2v2(data):
551-
gametype = "tdm2v2"
552-
553511
if gametype not in cache.GAMETYPE_IDS:
554512
raise InvalidMatchReport("Gametype not accepted: {}".format(gametype))
555513

templates/scoreboard.html

Lines changed: 12 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -46,20 +46,15 @@
4646
<th class="s s1">Score</th>
4747
<th class="s s1">Frags</th>
4848
<th class="s s1">Deaths</th>
49-
{% if gametype in ("ctf", "ad") %}
50-
<th class="s s1">Captures</th>
51-
{% if gametype != "ad" %}
52-
<th class="s s1">Assists</th>
53-
{% endif %}
54-
<th class="s s1">Defends</th>
55-
{% endif %}
49+
{% for m in medals_in_scoreboard_mid %}
50+
<th class="s s1" style="text-transform: capitalize">{{ m }}</th>
51+
{% endfor %}
5652
<th class="s s1">Damage Dealt</th>
5753
<th class="s s1">Damage Taken</th>
5854
<th class="s s1">Time</th>
59-
{% if gametype == "tdm" %}
60-
<th class="s s1">Excellent</th>
61-
<th class="s s1">Impressive</th>
62-
{% endif %}
55+
{% for m in medals_in_scoreboard_right %}
56+
<th class="s s1" style="text-transform: capitalize">{{ m }}</th>
57+
{% endfor %}
6358
<th class="s s1">Old rating</th>
6459
<th class="s s1">New rating</th>
6560
<th class="s s1">Diff</th>
@@ -76,20 +71,15 @@
7671
<td class="s s1">{{ player['stats']['score'] }}</td>
7772
<td class="s s1">{{ player['stats']['frags'] }}</td>
7873
<td class="s s1">{{ player['stats']['deaths'] }}</td>
79-
{% if gametype in ("ctf", "ad") %}
80-
<td class="s s1">{{ medal( player, 'captures' ) }}</td>
81-
{% if gametype != "ad" %}
82-
<td class="s s1">{{ medal( player, 'assists' ) }}</td>
83-
{% endif %}
84-
<td class="s s1">{{ medal( player, 'defends' ) }}</td>
85-
{% endif %}
74+
{% for m in medals_in_scoreboard_mid %}
75+
<td class="s s1">{{ medal(player, m) }}</td>
76+
{% endfor %}
8677
<td class="s s1">{{ player['stats']['damage_dealt'] }}</td>
8778
<td class="s s1">{{ player['stats']['damage_taken'] }}</td>
8879
<td class="s s1">{{ player['stats']['alive_time'] | seconds_to_mmss }}</td>
89-
{% if gametype == "tdm" %}
90-
<td class="s s1">{{ medal( player, 'excellent' ) }}</td>
91-
<td class="s s1">{{ medal( player, 'impressive' ) }}</td>
92-
{% endif %}
80+
{% for m in medals_in_scoreboard_right %}
81+
<td class="s s1">{{ medal(player, m) }}</td>
82+
{% endfor %}
9383
<td class="s s1">{{ rating( player['rating'], 'old' ) }}</td>
9484
<td class="s s1">{{ rating( player['rating'], 'new' ) }}</td>
9585
<td class="s s1">{{ rating_diff( player['rating'] ) }}</td>

tests/samples/scoreboard_sample02.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,9 @@
11
{
2+
"medals_in_scoreboard_mid": [
3+
"captures",
4+
"defends"
5+
],
6+
"medals_in_scoreboard_right": [],
27
"medals_available": [
38
"combokill",
49
"defends",

tests/samples/scoreboard_sample03.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,9 @@
11
{
2+
"medals_in_scoreboard_mid": [
3+
"captures",
4+
"defends"
5+
],
6+
"medals_in_scoreboard_right": [],
27
"medals_available": [
38
"accuracy",
49
"captures",

tests/samples/scoreboard_sample08.json

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,9 @@
11
{
2+
"medals_in_scoreboard_mid": [],
3+
"medals_in_scoreboard_right": [
4+
"excellent",
5+
"impressive"
6+
],
27
"medals_available": [
38
"combokill",
49
"excellent",
@@ -471,4 +476,4 @@
471476
"rg",
472477
"pg"
473478
]
474-
}
479+
}

0 commit comments

Comments
 (0)