-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathosuHelpers.ts
272 lines (242 loc) · 8.58 KB
/
osuHelpers.ts
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
import { SupabaseClient } from "@supabase/supabase-js";
import { BeatmapSet, BeatmapSetDatabase } from "./beatmap.types";
import { RANK_INTERVAL, RANK_PER_DAY, RANK_PER_RUN, SPLIT } from "./config";
import { DAY, HOUR, MINUTE } from "./timeConstants";
import { beatmapSetToDatabase, databaseToSplitModes } from "./utils";
import { probabilityAfter } from "./utils/probability";
import { Database } from "./database.types";
// round milliseconds up or down to rank intervals and return new date
const roundMinutes = (milliseconds: number, down = false) =>
(down
? Math.floor(milliseconds / (RANK_INTERVAL * MINUTE))
: Math.ceil(milliseconds / (RANK_INTERVAL * MINUTE))) *
(RANK_INTERVAL * MINUTE);
// time from previous interval in seconds
const intervalTimeDelta = (date: Date) =>
(date.getUTCMinutes() % 20) * 60 + date.getSeconds();
// qualifiedMaps here is only one mode
export const adjustRankDates = (
qualifiedMaps: BeatmapSet[],
rankedMaps: BeatmapSet[],
start = 0,
) => {
const combined = rankedMaps.concat(qualifiedMaps);
for (let i = rankedMaps.length + start; i < combined.length; i++) {
const qualifiedMap = combined[i];
let compareMap: BeatmapSet | null = null;
// skip over unresolved maps
let count = 0;
for (const beatmapSet of combined.slice(0, i).reverse()) {
if (beatmapSet.unresolved) continue;
count++;
if (count === RANK_PER_DAY) compareMap = beatmapSet;
}
let compareDate = 0;
if (compareMap != null && compareMap.rankDate != null) {
compareDate = compareMap.rankDate.getTime() + DAY; // daily rank limit date
if (i >= rankedMaps.length + RANK_PER_DAY) {
compareDate += RANK_INTERVAL * MINUTE; // increase accuracy for maps further down in the queue
}
}
const prev = qualifiedMap.rankDateEarly;
qualifiedMap.rankDateEarly = new Date(
Math.max(qualifiedMap.queueDate!.getTime(), compareDate),
);
// don't calculate probability for maps using rounded compare date
qualifiedMap.probability = null;
if (
qualifiedMap.queueDate!.getTime() > compareDate ||
i < rankedMaps.length + RANK_PER_DAY
) {
qualifiedMap.probability = probabilityAfter(
intervalTimeDelta(qualifiedMap.rankDateEarly),
);
}
qualifiedMap.rankDate = new Date(
roundMinutes(qualifiedMap.rankDateEarly.getTime()),
);
if (i - RANK_PER_RUN >= 0 && !qualifiedMap.unresolved) {
const filteredMaps = combined.slice(0, i).filter((beatmapSet) =>
!beatmapSet.unresolved
).reverse();
// fix date for maps after the adjustment below
if (
filteredMaps[0].queueDate !== null &&
qualifiedMap.rankDate.getTime() <
roundMinutes(filteredMaps[0].rankDate!.getTime(), true)
) {
qualifiedMap.rankDate = new Date(
roundMinutes(filteredMaps[0].rankDate!.getTime(), true),
);
qualifiedMap.rankDateEarly = qualifiedMap.rankDate;
qualifiedMap.probability = 0;
}
// if 3 maps have the same time, the 3rd map is pushed to next interval
if (
filteredMaps
.slice(0, RANK_PER_RUN)
.every(
(beatmapSet) =>
roundMinutes(beatmapSet.rankDate!.getTime(), true) >=
roundMinutes(qualifiedMap.rankDateEarly!.getTime(), true),
)
) {
if (
filteredMaps
.slice(0, RANK_PER_RUN)
.every(
(beatmapSet) =>
roundMinutes(beatmapSet.rankDate!.getTime(), true) ===
roundMinutes(
filteredMaps[RANK_PER_RUN - 1].rankDate!.getTime(),
true,
),
)
) {
qualifiedMap.rankDate = new Date(
roundMinutes(filteredMaps[0].rankDate!.getTime(), true) +
RANK_INTERVAL * MINUTE,
);
} else {
qualifiedMap.rankDate = new Date(
roundMinutes(filteredMaps[0].rankDate!.getTime(), true),
);
}
qualifiedMap.rankDateEarly = qualifiedMap.rankDate;
qualifiedMap.probability = 0;
}
}
if (
prev?.getTime() !== qualifiedMap.rankDateEarly.getTime()
) {
console.log(qualifiedMap.id, "-", prev, qualifiedMap.rankDateEarly);
console.log(qualifiedMap.id, "- queueDate:", qualifiedMap.queueDate);
console.log(qualifiedMap.id, "- compareDate:", new Date(compareDate));
if (compareMap != null && compareMap.rankDate != null) {
console.log(
qualifiedMap.id,
`- compareMap.rankDate ${compareMap.id}:`,
compareMap.rankDate,
);
} else {
console.log(i - RANK_PER_DAY, rankedMaps.length);
}
}
}
};
export const calcEarlyProbability = (qualifiedMaps: BeatmapSet[][]) => {
const rankDates: { [key: number]: number[] } = {};
qualifiedMaps.forEach((beatmapSets) => {
for (const beatmapSet of beatmapSets) {
// assume map will be ranked early if probability > SPLIT to simplify calculations
const key = (beatmapSet.probability ?? 0) > SPLIT
? roundMinutes(beatmapSet.rankDateEarly!.getTime(), true)
: beatmapSet.rankDate!.getTime();
if (!(key in rankDates)) {
rankDates[key] = [0, 0, 0, 0];
}
rankDates[key][beatmapSet.mode] += 1;
}
});
qualifiedMaps.forEach((beatmapSets) => {
for (const beatmapSet of beatmapSets) {
const key = roundMinutes(beatmapSet.rankDateEarly!.getTime(), true);
if (
beatmapSet.probability !== null &&
beatmapSet.rankDateEarly!.getTime() !== beatmapSet.rankDate!.getTime()
) {
const otherModes = rankDates[key]?.filter((_, mode) =>
mode != beatmapSet.mode
);
const probability = probabilityAfter(
intervalTimeDelta(beatmapSet.rankDateEarly!),
otherModes,
);
beatmapSet.probability = probability;
}
}
});
};
export const adjustAllRankDates = (
qualifiedMaps: BeatmapSet[][],
rankedMaps: BeatmapSet[][],
) => {
const MODES = 4;
for (let mode = 0; mode < MODES; mode++) {
adjustRankDates(qualifiedMaps[mode], rankedMaps[mode]);
}
calcEarlyProbability(qualifiedMaps);
};
type StoredMapProperties = [number, number | null, number | null, boolean];
export const storeMapProperties = (qualifiedData: BeatmapSetDatabase[]) => {
const previousData: { [key: number]: StoredMapProperties } = {};
qualifiedData.forEach((beatmapSet) => {
previousData[beatmapSet.id] = [
beatmapSet.rank_date,
beatmapSet.rank_date_early,
beatmapSet.probability,
beatmapSet.unresolved,
];
});
return previousData;
};
export const getFormattedMapsFromDatabase = async (
supabase: SupabaseClient<Database>,
) => {
const { data: qualifiedData, error: errorQualified } = await supabase
.from("beatmapsets")
.select("*")
.not("queue_date", "is", null);
const { data: rankedData, error: errorRanked } = await supabase
.from("beatmapsets")
.select("*")
.is("queue_date", null)
.gt("rank_date", Math.floor((Date.now() - DAY - HOUR) / 1000));
if (!rankedData || !qualifiedData) {
throw new Error(
`missing data. errorQualified ${errorQualified}\nerrorRanked ${errorRanked}`,
);
}
const qualifiedMaps = databaseToSplitModes(
qualifiedData.sort((a, b) => a.queue_date! - b.queue_date!),
);
const rankedMaps = databaseToSplitModes(
rankedData.sort((a, b) => a.rank_date - b.rank_date),
);
return { qualifiedMaps, rankedMaps, qualifiedData, rankedData };
};
export const getUpdatedMaps = (
qualifiedMaps: BeatmapSet[][],
previousData: { [key: number]: StoredMapProperties },
) => {
const mapsToUpdate: BeatmapSetDatabase[] = [];
const updatedMapIds: number[] = [];
qualifiedMaps.forEach((beatmapSets) => {
beatmapSets.forEach((beatmapSet) => {
const currentData: StoredMapProperties = [
beatmapSet.rankDate!.getTime() / 1000,
beatmapSet.rankDateEarly!.getTime() / 1000,
beatmapSet.probability,
beatmapSet.unresolved,
];
// if rankDate/rankDateEarly/probability has changed or new qualified map
if (
!(beatmapSet.id in previousData) ||
previousData[beatmapSet.id].reduce(
(updated, value, i) => updated || currentData[i] !== value,
false,
)
) {
mapsToUpdate.push(beatmapSetToDatabase(beatmapSet));
updatedMapIds.push(beatmapSet.id);
console.log(
beatmapSet.id,
"-",
previousData[beatmapSet.id],
currentData,
);
}
});
});
return { mapsToUpdate, updatedMapIds };
};