@quenty/elo 7.19.0 → 7.19.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,17 @@
|
|
|
3
3
|
All notable changes to this project will be documented in this file.
|
|
4
4
|
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
|
5
5
|
|
|
6
|
+
## [7.19.1](https://github.com/Quenty/NevermoreEngine/compare/@quenty/elo@7.19.0...@quenty/elo@7.19.1) (2025-04-05)
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
### Bug Fixes
|
|
10
|
+
|
|
11
|
+
* Add types to packages ([2374fb2](https://github.com/Quenty/NevermoreEngine/commit/2374fb2b043cfbe0e9b507b3316eec46a4e353a0))
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
|
|
6
17
|
# [7.19.0](https://github.com/Quenty/NevermoreEngine/compare/@quenty/elo@7.18.2...@quenty/elo@7.19.0) (2025-04-02)
|
|
7
18
|
|
|
8
19
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@quenty/elo",
|
|
3
|
-
"version": "7.19.
|
|
3
|
+
"version": "7.19.1",
|
|
4
4
|
"description": "Elo rating utility library.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"Roblox",
|
|
@@ -29,12 +29,12 @@
|
|
|
29
29
|
"access": "public"
|
|
30
30
|
},
|
|
31
31
|
"devDependencies": {
|
|
32
|
-
"@quenty/blend": "^12.18.
|
|
33
|
-
"@quenty/loader": "^10.8.
|
|
34
|
-
"@quenty/maid": "^3.4.
|
|
32
|
+
"@quenty/blend": "^12.18.1",
|
|
33
|
+
"@quenty/loader": "^10.8.1",
|
|
34
|
+
"@quenty/maid": "^3.4.1"
|
|
35
35
|
},
|
|
36
36
|
"dependencies": {
|
|
37
37
|
"@quenty/probability": "^2.3.1"
|
|
38
38
|
},
|
|
39
|
-
"gitHead": "
|
|
39
|
+
"gitHead": "78c3ac0ab08dd18085b6e6e6e4f745e76ed99f68"
|
|
40
40
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
--!strict
|
|
1
2
|
--[=[
|
|
2
3
|
@class EloMatchResultUtils
|
|
3
4
|
]=]
|
|
@@ -8,18 +9,30 @@ local EloMatchResult = require("EloMatchResult")
|
|
|
8
9
|
|
|
9
10
|
local EloMatchResultUtils = {}
|
|
10
11
|
|
|
11
|
-
|
|
12
|
+
--[=[
|
|
13
|
+
Checks if the given match result is a valid EloMatchResult.
|
|
14
|
+
|
|
15
|
+
@param matchResult any
|
|
16
|
+
@return boolean
|
|
17
|
+
]=]
|
|
18
|
+
function EloMatchResultUtils.isEloMatchResult(matchResult: any): boolean
|
|
12
19
|
return matchResult == EloMatchResult.PLAYER_ONE_WIN
|
|
13
20
|
or matchResult == EloMatchResult.PLAYER_TWO_WIN
|
|
14
21
|
or matchResult == EloMatchResult.DRAW
|
|
15
22
|
end
|
|
16
23
|
|
|
17
|
-
|
|
24
|
+
--[=[
|
|
25
|
+
Checks if the given match result is a valid EloMatchResult list
|
|
26
|
+
|
|
27
|
+
@param eloMatchResultList any
|
|
28
|
+
@return boolean
|
|
29
|
+
]=]
|
|
30
|
+
function EloMatchResultUtils.isEloMatchResultList(eloMatchResultList: {number }): boolean
|
|
18
31
|
if type(eloMatchResultList) ~= "table" then
|
|
19
32
|
return false
|
|
20
33
|
end
|
|
21
34
|
|
|
22
|
-
for _, eloMatchResult in
|
|
35
|
+
for _, eloMatchResult in eloMatchResultList do
|
|
23
36
|
if not EloMatchResultUtils.isEloMatchResult(eloMatchResult) then
|
|
24
37
|
return false
|
|
25
38
|
end
|
package/src/Shared/EloUtils.lua
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
--!strict
|
|
1
2
|
--[=[
|
|
2
3
|
Utilities to compute elo scores for players
|
|
3
4
|
@class EloUtils
|
|
@@ -30,6 +31,8 @@ local Probability = require("Probability")
|
|
|
30
31
|
|
|
31
32
|
local EloUtils = {}
|
|
32
33
|
|
|
34
|
+
export type EloMatchResultList = { number }
|
|
35
|
+
|
|
33
36
|
--[=[
|
|
34
37
|
@interface EloConfig
|
|
35
38
|
.factor number
|
|
@@ -39,21 +42,36 @@ local EloUtils = {}
|
|
|
39
42
|
.groupMultipleResultAsOne boolean
|
|
40
43
|
@within EloUtils
|
|
41
44
|
]=]
|
|
45
|
+
export type EloConfig = {
|
|
46
|
+
factor: number,
|
|
47
|
+
kfactor: number | (rating: number) -> number,
|
|
48
|
+
initial: number,
|
|
49
|
+
ratingFloor: number,
|
|
50
|
+
groupMultipleResultAsOne: boolean,
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export type PartialEloConfig = {
|
|
54
|
+
factor: number?,
|
|
55
|
+
kfactor: (number | (rating: number) -> number)?,
|
|
56
|
+
initial: number?,
|
|
57
|
+
ratingFloor: number?,
|
|
58
|
+
groupMultipleResultAsOne: boolean?,
|
|
59
|
+
}
|
|
42
60
|
|
|
43
61
|
--[=[
|
|
44
62
|
Creates a new elo config.
|
|
45
63
|
@param config table? -- Optional table with defaults
|
|
46
64
|
@return EloConfig
|
|
47
65
|
]=]
|
|
48
|
-
function EloUtils.createConfig(config)
|
|
49
|
-
|
|
66
|
+
function EloUtils.createConfig(config: PartialEloConfig?): EloConfig
|
|
67
|
+
local partial: PartialEloConfig = config or {}
|
|
50
68
|
|
|
51
69
|
return {
|
|
52
|
-
factor =
|
|
53
|
-
kfactor =
|
|
54
|
-
initial =
|
|
55
|
-
ratingFloor =
|
|
56
|
-
groupMultipleResultAsOne = false
|
|
70
|
+
factor = partial.factor or 400,
|
|
71
|
+
kfactor = partial.kfactor or EloUtils.standardKFactorFormula,
|
|
72
|
+
initial = partial.initial or 1400,
|
|
73
|
+
ratingFloor = partial.ratingFloor or 100,
|
|
74
|
+
groupMultipleResultAsOne = false,
|
|
57
75
|
}
|
|
58
76
|
end
|
|
59
77
|
|
|
@@ -63,7 +81,7 @@ end
|
|
|
63
81
|
@param config any
|
|
64
82
|
@return boolean
|
|
65
83
|
]=]
|
|
66
|
-
function EloUtils.isEloConfig(config)
|
|
84
|
+
function EloUtils.isEloConfig(config: EloConfig): boolean
|
|
67
85
|
return type(config) == "table"
|
|
68
86
|
and type(config.factor) == "number"
|
|
69
87
|
and (type(config.kfactor) == "number" or type(config.kfactor) == "function")
|
|
@@ -78,10 +96,10 @@ end
|
|
|
78
96
|
@param eloConfig EloConfig
|
|
79
97
|
@return number
|
|
80
98
|
]=]
|
|
81
|
-
function EloUtils.getStandardDeviation(eloConfig)
|
|
99
|
+
function EloUtils.getStandardDeviation(eloConfig: EloConfig): number
|
|
82
100
|
assert(EloUtils.isEloConfig(eloConfig), "Bad eloConfig")
|
|
83
101
|
|
|
84
|
-
return 0.5*eloConfig.factor*math.sqrt(2)
|
|
102
|
+
return 0.5 * eloConfig.factor * math.sqrt(2)
|
|
85
103
|
end
|
|
86
104
|
|
|
87
105
|
--[=[
|
|
@@ -91,13 +109,13 @@ end
|
|
|
91
109
|
@param elo number
|
|
92
110
|
@return number
|
|
93
111
|
]=]
|
|
94
|
-
function EloUtils.getPercentile(eloConfig, elo)
|
|
112
|
+
function EloUtils.getPercentile(eloConfig: EloConfig, elo: number): number
|
|
95
113
|
assert(EloUtils.isEloConfig(eloConfig), "Bad eloConfig")
|
|
96
114
|
|
|
97
115
|
local standardDeviation = EloUtils.getStandardDeviation(eloConfig)
|
|
98
116
|
local mean = eloConfig.initial
|
|
99
117
|
|
|
100
|
-
local zScore = (elo - mean)/standardDeviation
|
|
118
|
+
local zScore = (elo - mean) / standardDeviation
|
|
101
119
|
return Probability.cdf(zScore)
|
|
102
120
|
end
|
|
103
121
|
|
|
@@ -108,14 +126,18 @@ end
|
|
|
108
126
|
@param percentile number
|
|
109
127
|
@return number
|
|
110
128
|
]=]
|
|
111
|
-
function EloUtils.percentileToElo(eloConfig, percentile)
|
|
129
|
+
function EloUtils.percentileToElo(eloConfig: EloConfig, percentile: number): number?
|
|
112
130
|
assert(EloUtils.isEloConfig(eloConfig), "Bad eloConfig")
|
|
113
131
|
|
|
114
132
|
local standardDeviation = EloUtils.getStandardDeviation(eloConfig)
|
|
115
133
|
local mean = eloConfig.initial
|
|
116
134
|
|
|
117
135
|
local zScore = Probability.percentileToZScore(percentile)
|
|
118
|
-
|
|
136
|
+
if zScore == nil then
|
|
137
|
+
return nil
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
return mean + zScore * standardDeviation
|
|
119
141
|
end
|
|
120
142
|
|
|
121
143
|
--[=[
|
|
@@ -128,14 +150,25 @@ end
|
|
|
128
150
|
@return number -- playerOneRating
|
|
129
151
|
@return number -- playerTwoRating
|
|
130
152
|
]=]
|
|
131
|
-
function EloUtils.getNewElo(
|
|
153
|
+
function EloUtils.getNewElo(
|
|
154
|
+
config: EloConfig,
|
|
155
|
+
playerOneRating: number,
|
|
156
|
+
playerTwoRating: number,
|
|
157
|
+
eloMatchResultList: EloMatchResultList
|
|
158
|
+
): (number, number)
|
|
132
159
|
assert(EloUtils.isEloConfig(config), "Bad config")
|
|
133
160
|
assert(type(playerOneRating) == "number", "Bad playerOneRating")
|
|
134
161
|
assert(type(playerTwoRating) == "number", "Bad playerTwoRating")
|
|
135
162
|
assert(EloMatchResultUtils.isEloMatchResultList(eloMatchResultList), "Bad eloMatchResultList")
|
|
136
163
|
|
|
137
|
-
local newPlayerOneRating =
|
|
138
|
-
|
|
164
|
+
local newPlayerOneRating =
|
|
165
|
+
EloUtils.getNewPlayerOneScore(config, playerOneRating, playerTwoRating, eloMatchResultList)
|
|
166
|
+
local newPlayerTwoRating = EloUtils.getNewPlayerOneScore(
|
|
167
|
+
config,
|
|
168
|
+
playerTwoRating,
|
|
169
|
+
playerOneRating,
|
|
170
|
+
EloUtils.fromOpponentPerspective(eloMatchResultList)
|
|
171
|
+
)
|
|
139
172
|
return newPlayerOneRating, newPlayerTwoRating
|
|
140
173
|
end
|
|
141
174
|
|
|
@@ -149,13 +182,14 @@ end
|
|
|
149
182
|
@return number -- playerOneRating
|
|
150
183
|
@return number -- playerTwoRating
|
|
151
184
|
]=]
|
|
152
|
-
function EloUtils.getEloChange(config, playerOneRating, playerTwoRating, eloMatchResultList)
|
|
185
|
+
function EloUtils.getEloChange(config: EloConfig, playerOneRating: number, playerTwoRating: number, eloMatchResultList)
|
|
153
186
|
assert(EloUtils.isEloConfig(config), "Bad config")
|
|
154
187
|
assert(type(playerOneRating) == "number", "Bad playerOneRating")
|
|
155
188
|
assert(type(playerTwoRating) == "number", "Bad playerTwoRating")
|
|
156
189
|
assert(EloMatchResultUtils.isEloMatchResultList(eloMatchResultList), "Bad eloMatchResultList")
|
|
157
190
|
|
|
158
|
-
local newPlayerOneRating, newPlayerTwoRating =
|
|
191
|
+
local newPlayerOneRating, newPlayerTwoRating =
|
|
192
|
+
EloUtils.getNewElo(config, playerOneRating, playerTwoRating, eloMatchResultList)
|
|
159
193
|
local playerOneChange = newPlayerOneRating - playerOneRating
|
|
160
194
|
local playerTwoChange = newPlayerTwoRating - playerTwoRating
|
|
161
195
|
return playerOneChange, playerTwoChange
|
|
@@ -169,13 +203,22 @@ end
|
|
|
169
203
|
@param playerTwoRating number
|
|
170
204
|
@param eloMatchResultList { EloMatchResult }
|
|
171
205
|
]=]
|
|
172
|
-
function EloUtils.getNewPlayerOneScore(
|
|
206
|
+
function EloUtils.getNewPlayerOneScore(
|
|
207
|
+
config: EloConfig,
|
|
208
|
+
playerOneRating: number,
|
|
209
|
+
playerTwoRating: number,
|
|
210
|
+
eloMatchResultList: EloMatchResultList
|
|
211
|
+
)
|
|
173
212
|
assert(EloUtils.isEloConfig(config), "Bad config")
|
|
174
213
|
assert(type(playerOneRating) == "number", "Bad playerOneRating")
|
|
175
214
|
assert(type(playerTwoRating) == "number", "Bad playerTwoRating")
|
|
176
215
|
assert(EloMatchResultUtils.isEloMatchResultList(eloMatchResultList), "Bad eloMatchResultList")
|
|
177
216
|
|
|
178
|
-
return math.max(
|
|
217
|
+
return math.max(
|
|
218
|
+
config.ratingFloor,
|
|
219
|
+
playerOneRating
|
|
220
|
+
+ EloUtils.getPlayerOneScoreAdjustment(config, playerOneRating, playerTwoRating, eloMatchResultList)
|
|
221
|
+
)
|
|
179
222
|
end
|
|
180
223
|
|
|
181
224
|
--[=[
|
|
@@ -190,7 +233,7 @@ end
|
|
|
190
233
|
@param playerTwoRating number
|
|
191
234
|
@return number
|
|
192
235
|
]=]
|
|
193
|
-
function EloUtils.getPlayerOneExpected(config, playerOneRating, playerTwoRating)
|
|
236
|
+
function EloUtils.getPlayerOneExpected(config: EloConfig, playerOneRating: number, playerTwoRating: number): number
|
|
194
237
|
assert(EloUtils.isEloConfig(config), "Bad config")
|
|
195
238
|
assert(type(playerOneRating) == "number", "Bad playerOneRating")
|
|
196
239
|
assert(type(playerTwoRating) == "number", "Bad playerTwoRating")
|
|
@@ -208,7 +251,12 @@ end
|
|
|
208
251
|
@param eloMatchResultList { EloMatchResult }
|
|
209
252
|
@return number
|
|
210
253
|
]=]
|
|
211
|
-
function EloUtils.getPlayerOneScoreAdjustment(
|
|
254
|
+
function EloUtils.getPlayerOneScoreAdjustment(
|
|
255
|
+
config: EloConfig,
|
|
256
|
+
playerOneRating: number,
|
|
257
|
+
playerTwoRating: number,
|
|
258
|
+
eloMatchResultList: EloMatchResultList
|
|
259
|
+
): number
|
|
212
260
|
assert(EloUtils.isEloConfig(config), "Bad config")
|
|
213
261
|
assert(type(playerOneRating) == "number", "Bad playerOneRating")
|
|
214
262
|
assert(type(playerTwoRating) == "number", "Bad playerTwoRating")
|
|
@@ -229,15 +277,15 @@ function EloUtils.getPlayerOneScoreAdjustment(config, playerOneRating, playerTwo
|
|
|
229
277
|
multiplier = losses
|
|
230
278
|
end
|
|
231
279
|
|
|
232
|
-
adjustment = multiplier*(score - expected)
|
|
280
|
+
adjustment = multiplier * (score - expected)
|
|
233
281
|
else
|
|
234
|
-
for _, score in
|
|
282
|
+
for _, score in eloMatchResultList do
|
|
235
283
|
adjustment = adjustment + (score - expected)
|
|
236
284
|
end
|
|
237
285
|
end
|
|
238
286
|
|
|
239
287
|
local kfactor = EloUtils.extractKFactor(config, playerOneRating)
|
|
240
|
-
return kfactor*adjustment
|
|
288
|
+
return kfactor * adjustment
|
|
241
289
|
end
|
|
242
290
|
|
|
243
291
|
--[=[
|
|
@@ -246,12 +294,12 @@ end
|
|
|
246
294
|
@param eloMatchResultList { EloMatchResult }
|
|
247
295
|
@return { number }
|
|
248
296
|
]=]
|
|
249
|
-
function EloUtils.fromOpponentPerspective(eloMatchResultList)
|
|
297
|
+
function EloUtils.fromOpponentPerspective(eloMatchResultList: EloMatchResultList): EloMatchResultList
|
|
250
298
|
assert(EloMatchResultUtils.isEloMatchResultList(eloMatchResultList), "Bad eloMatchResultList")
|
|
251
299
|
|
|
252
300
|
local newScores = {}
|
|
253
301
|
|
|
254
|
-
for index, score in
|
|
302
|
+
for index, score in eloMatchResultList do
|
|
255
303
|
newScores[index] = 1 - score
|
|
256
304
|
end
|
|
257
305
|
|
|
@@ -264,11 +312,11 @@ end
|
|
|
264
312
|
@param eloMatchResultList { EloMatchResult }
|
|
265
313
|
@return { number }
|
|
266
314
|
]=]
|
|
267
|
-
function EloUtils.countPlayerOneWins(eloMatchResultList)
|
|
315
|
+
function EloUtils.countPlayerOneWins(eloMatchResultList: EloMatchResultList): number
|
|
268
316
|
assert(EloMatchResultUtils.isEloMatchResultList(eloMatchResultList), "Bad eloMatchResultList")
|
|
269
317
|
|
|
270
318
|
local count = 0
|
|
271
|
-
for _, score in
|
|
319
|
+
for _, score in eloMatchResultList do
|
|
272
320
|
if score == EloMatchResult.PLAYER_ONE_WIN then
|
|
273
321
|
count = count + 1
|
|
274
322
|
end
|
|
@@ -282,11 +330,11 @@ end
|
|
|
282
330
|
@param eloMatchResultList { EloMatchResult }
|
|
283
331
|
@return { number }
|
|
284
332
|
]=]
|
|
285
|
-
function EloUtils.countPlayerTwoWins(eloMatchResultList)
|
|
333
|
+
function EloUtils.countPlayerTwoWins(eloMatchResultList: EloMatchResultList): number
|
|
286
334
|
assert(EloMatchResultUtils.isEloMatchResultList(eloMatchResultList), "Bad eloMatchResultList")
|
|
287
335
|
|
|
288
336
|
local count = 0
|
|
289
|
-
for _, score in
|
|
337
|
+
for _, score in eloMatchResultList do
|
|
290
338
|
if score == EloMatchResult.PLAYER_TWO_WIN then
|
|
291
339
|
count = count + 1
|
|
292
340
|
end
|
|
@@ -300,7 +348,7 @@ end
|
|
|
300
348
|
@param rating number
|
|
301
349
|
@return number
|
|
302
350
|
]=]
|
|
303
|
-
function EloUtils.standardKFactorFormula(rating)
|
|
351
|
+
function EloUtils.standardKFactorFormula(rating: number): number
|
|
304
352
|
if rating >= 2400 then
|
|
305
353
|
return 16
|
|
306
354
|
elseif rating >= 2100 then
|
|
@@ -317,7 +365,7 @@ end
|
|
|
317
365
|
@param rating number
|
|
318
366
|
@return number
|
|
319
367
|
]=]
|
|
320
|
-
function EloUtils.extractKFactor(config, rating)
|
|
368
|
+
function EloUtils.extractKFactor(config: EloConfig, rating: number): number
|
|
321
369
|
assert(EloUtils.isEloConfig(config), "Bad config")
|
|
322
370
|
assert(type(rating) == "number", "Bad rating")
|
|
323
371
|
|
|
@@ -21,7 +21,7 @@ local function TextLabel(props)
|
|
|
21
21
|
TextSize = props.TextSize or 20;
|
|
22
22
|
Text = props.Text;
|
|
23
23
|
RichText = props.RichText;
|
|
24
|
-
}
|
|
24
|
+
}
|
|
25
25
|
end
|
|
26
26
|
|
|
27
27
|
local function MatchResultCard(props)
|
|
@@ -294,7 +294,7 @@ return function(target)
|
|
|
294
294
|
};
|
|
295
295
|
}
|
|
296
296
|
|
|
297
|
-
for _, matchResultType in
|
|
297
|
+
for _, matchResultType in matchResultTypes do
|
|
298
298
|
if type(matchResultType) == "string" then
|
|
299
299
|
table.insert(groupOptions, TextLabel({
|
|
300
300
|
TextColor3 = Color3.new(1, 1, 1);
|