-
Notifications
You must be signed in to change notification settings - Fork 9
/
Copy pathtop_raid_rank.py
172 lines (146 loc) · 4.99 KB
/
top_raid_rank.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
from bisect import bisect_right
from collections import defaultdict
from datetime import timedelta
from typing import Union
from pydantic import BaseModel, field_validator
from api_top_db_v2 import TopDBCached
from c_bosses import ALL_FIGHT_NAMES
from c_path import Directories
from c_player_classes import SPECS_DICT
from c_server_phase import Encounter
from h_debug import running_time
API_EXAMPLES = [
{
"server": "Lordaeron",
"boss": "The Lich King",
"mode": "25H",
"dps": {
"Safiyah": "12345.6",
"Nomadra": "12345.6",
},
"specs": {
"Safiyah": "Shadow Priest",
"Nomadra": "Balance Druid",
},
},
]
class RaidRankValidation(BaseModel):
server: str
boss: str
mode: str
dps: dict[str, Union[str, float]]
specs: dict[str, Union[str, int]]
model_config = {
"json_schema_extra": {
"examples": API_EXAMPLES,
}
}
@field_validator('server')
@classmethod
def validate_server(cls, server: str):
servers = Directories.top.files_stems()
if server not in servers:
_list = ', '.join(servers)
raise ValueError(f"[server] value value must be from [{_list}]")
return server
@field_validator('boss')
@classmethod
def validate_boss(cls, boss: str):
if boss not in ALL_FIGHT_NAMES:
_list = ', '.join(ALL_FIGHT_NAMES)
raise ValueError(f"[boss] value value must be from [{_list}]")
return boss
@field_validator('mode')
@classmethod
def validate_mode(cls, mode: str):
mode = mode.upper()
modes = ["10N", "10H", "25N", "25H"]
if mode not in modes:
_list = ', '.join(modes)
raise ValueError(f"[boss] value value must be from [{_list}]")
return mode
@field_validator('dps')
@classmethod
def dps_to_numbers(cls, dps: dict[str, Union[str, float]]):
return {
name: float(dps)
for name, dps in dps.items()
}
@field_validator('specs')
@classmethod
def specs_to_int(cls, specs: dict[str, Union[int, str]]):
z: dict[str, int] = {}
for name, spec_i in specs.items():
if spec_i in SPECS_DICT:
z[name] = SPECS_DICT[spec_i].index
elif type(spec_i) == int:
z[name] = spec_i
elif spec_i.isdigit():
z[name] = int(spec_i)
else:
raise ValueError(f"{name} has wrong spec")
return z
class RaidRank(TopDBCached):
cache: defaultdict[str, dict[str, dict[int, list[float]]]] = defaultdict(dict)
cooldown = timedelta(minutes=2)
def __init__(self, model: RaidRankValidation) -> None:
super().__init__(model.server)
self.model = model
self.encounter = Encounter(model.boss, model.mode)
self.table_name = self.encounter.table_name
@running_time
def points(self):
dps = self.model.dps
specs = self.model.specs
return {
player_name: self._format_rank(specs[player_name], player_dps)
for player_name, player_dps in dps.items()
if player_name in specs
}
def _format_rank(self, spec_i: int, dps: float):
spec_data = self._spec_data(spec_i)
total_raids = len(spec_data)
if not total_raids or not spec_data[-1]:
return {
"rank": 1,
"percentile": 100.0,
"from_spec_top1": 100.0,
"total_raids_for_spec": 1,
}
dps = dps + 0.01
rank_reversed = bisect_right(spec_data, dps)
rank = total_raids - rank_reversed + 1
percentile = rank_reversed / total_raids * 100
percentile = round(percentile, 2)
perc_from_top1 = dps / spec_data[-1] * 100
perc_from_top1 = round(perc_from_top1, 1)
return {
"rank": rank,
"percentile": percentile,
"from_spec_top1": perc_from_top1,
"total_raids_for_spec": total_raids,
}
def _cache(self):
if self.db_was_updated():
self.cache[self.server].clear()
server_data = self.cache[self.server]
if self.table_name not in server_data:
server_data[self.table_name] = {}
return server_data[self.table_name]
def _renew_data(self, spec_index: int) -> list[float]:
query = self.encounter.query_dps_spec(spec_index)
return sorted(x for x, in self.cursor.execute(query))
def _spec_data(self, spec_index: int) -> list[float]:
_data = self._cache()
if spec_index not in _data:
_data[spec_index] = self._renew_data(spec_index)
return _data[spec_index]
def _test1():
conf = API_EXAMPLES[0]
q = RaidRankValidation(**conf)
z = RaidRank(q)
for x, y in z.points().items():
print(x)
print(y)
if __name__ == "__main__":
_test1()