@quenty/elo 1.1.0 → 2.0.0
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 +8 -0
- package/LICENSE.md +1 -1
- package/package.json +7 -2
- package/src/Shared/EloMatchResult.lua +15 -0
- package/src/Shared/EloMatchResultUtils.lua +31 -0
- package/src/Shared/EloUtils.lua +127 -57
- package/src/Shared/EloUtils.spec.lua +7 -7
- package/src/Shared/EloUtils.story.lua +358 -0
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,14 @@
|
|
|
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
|
+
# [2.0.0](https://github.com/Quenty/NevermoreEngine/compare/@quenty/elo@1.1.0...@quenty/elo@2.0.0) (2023-10-15)
|
|
7
|
+
|
|
8
|
+
**Note:** Version bump only for package @quenty/elo
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
|
|
6
14
|
# 1.1.0 (2022-03-10)
|
|
7
15
|
|
|
8
16
|
|
package/LICENSE.md
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@quenty/elo",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "2.0.0",
|
|
4
4
|
"description": "Elo rating utility library.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"Roblox",
|
|
@@ -28,5 +28,10 @@
|
|
|
28
28
|
"publishConfig": {
|
|
29
29
|
"access": "public"
|
|
30
30
|
},
|
|
31
|
-
"
|
|
31
|
+
"devDependencies": {
|
|
32
|
+
"@quenty/blend": "^7.0.0",
|
|
33
|
+
"@quenty/loader": "^7.0.0",
|
|
34
|
+
"@quenty/maid": "^2.6.0"
|
|
35
|
+
},
|
|
36
|
+
"gitHead": "c0034bed881fc1445e04366dd0b0abb49bc42e88"
|
|
32
37
|
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
--[=[
|
|
2
|
+
@class EloMatchResult
|
|
3
|
+
]=]
|
|
4
|
+
|
|
5
|
+
local require = require(script.Parent.loader).load(script)
|
|
6
|
+
|
|
7
|
+
return table.freeze(setmetatable({
|
|
8
|
+
PLAYER_ONE_WIN = 1;
|
|
9
|
+
DRAW = 0.5;
|
|
10
|
+
PLAYER_TWO_WIN = 0;
|
|
11
|
+
}, {
|
|
12
|
+
__index = function()
|
|
13
|
+
error("Bad index onto EloMatchResult")
|
|
14
|
+
end;
|
|
15
|
+
}))
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
--[=[
|
|
2
|
+
@class EloMatchResultUtils
|
|
3
|
+
]=]
|
|
4
|
+
|
|
5
|
+
local require = require(script.Parent.loader).load(script)
|
|
6
|
+
|
|
7
|
+
local EloMatchResult = require("EloMatchResult")
|
|
8
|
+
|
|
9
|
+
local EloMatchResultUtils = {}
|
|
10
|
+
|
|
11
|
+
function EloMatchResultUtils.isEloMatchResult(matchResult)
|
|
12
|
+
return matchResult == EloMatchResult.PLAYER_ONE_WIN
|
|
13
|
+
or matchResult == EloMatchResult.PLAYER_TWO_WIN
|
|
14
|
+
or matchResult == EloMatchResult.DRAW
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
function EloMatchResultUtils.isEloMatchResultList(eloMatchResultList)
|
|
18
|
+
if type(eloMatchResultList) ~= "table" then
|
|
19
|
+
return false
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
for _, eloMatchResult in pairs(eloMatchResultList) do
|
|
23
|
+
if not EloMatchResultUtils.isEloMatchResult(eloMatchResult) then
|
|
24
|
+
return false
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
return true
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
return EloMatchResultUtils
|
package/src/Shared/EloUtils.lua
CHANGED
|
@@ -5,34 +5,29 @@
|
|
|
5
5
|
```lua
|
|
6
6
|
local config = EloUtils.createConfig()
|
|
7
7
|
|
|
8
|
-
local
|
|
9
|
-
local
|
|
8
|
+
local playerOneRating = 1400
|
|
9
|
+
local playerTwoRating = 1800
|
|
10
10
|
|
|
11
11
|
-- Update rating!
|
|
12
|
-
|
|
12
|
+
playerOneRating, playerTwoRating = EloUtils.getNewElo(
|
|
13
13
|
config,
|
|
14
|
-
|
|
15
|
-
|
|
14
|
+
playerOneRating,
|
|
15
|
+
playerTwoRating,
|
|
16
16
|
{
|
|
17
|
-
|
|
17
|
+
EloMatchResult.PLAYER_ONE_WIN;
|
|
18
18
|
})
|
|
19
19
|
|
|
20
20
|
-- New rankings!
|
|
21
|
-
print(
|
|
21
|
+
print(playerOneRating, playerTwoRating)
|
|
22
22
|
```
|
|
23
23
|
]=]
|
|
24
24
|
|
|
25
|
-
local
|
|
25
|
+
local require = require(script.Parent.loader).load(script)
|
|
26
|
+
|
|
27
|
+
local EloMatchResult = require("EloMatchResult")
|
|
28
|
+
local EloMatchResultUtils = require("EloMatchResultUtils")
|
|
26
29
|
|
|
27
|
-
EloUtils
|
|
28
|
-
WIN = 1;
|
|
29
|
-
DRAW = 0.5;
|
|
30
|
-
LOSS = 0;
|
|
31
|
-
}, {
|
|
32
|
-
__index = function()
|
|
33
|
-
error("Bad index onto EloUtils.Scores")
|
|
34
|
-
end;
|
|
35
|
-
})
|
|
30
|
+
local EloUtils = {}
|
|
36
31
|
|
|
37
32
|
--[=[
|
|
38
33
|
@interface EloConfig
|
|
@@ -40,6 +35,7 @@ EloUtils.Scores = setmetatable({
|
|
|
40
35
|
.kfactor number | function
|
|
41
36
|
.initial number
|
|
42
37
|
.ratingFloor number
|
|
38
|
+
.groupMultipleResultAsOne boolean
|
|
43
39
|
@within EloUtils
|
|
44
40
|
]=]
|
|
45
41
|
|
|
@@ -56,6 +52,7 @@ function EloUtils.createConfig(config)
|
|
|
56
52
|
kfactor = config.kfactor or EloUtils.standardKFactorFormula;
|
|
57
53
|
initial = config.initial or 1400;
|
|
58
54
|
ratingFloor = config.ratingFloor or 100;
|
|
55
|
+
groupMultipleResultAsOne = false;
|
|
59
56
|
}
|
|
60
57
|
end
|
|
61
58
|
|
|
@@ -73,38 +70,60 @@ end
|
|
|
73
70
|
Gets the new score for the player and opponent after a series of matches.
|
|
74
71
|
|
|
75
72
|
@param config EloConfig
|
|
76
|
-
@param
|
|
77
|
-
@param
|
|
78
|
-
@param
|
|
79
|
-
@return number --
|
|
80
|
-
@return number --
|
|
73
|
+
@param playerOneRating number
|
|
74
|
+
@param playerTwoRating number
|
|
75
|
+
@param eloMatchResultList { EloMatchResult }
|
|
76
|
+
@return number -- playerOneRating
|
|
77
|
+
@return number -- playerTwoRating
|
|
81
78
|
]=]
|
|
82
|
-
function EloUtils.
|
|
79
|
+
function EloUtils.getNewElo(config, playerOneRating, playerTwoRating, eloMatchResultList)
|
|
83
80
|
assert(EloUtils.isEloConfig(config), "Bad config")
|
|
84
|
-
assert(type(
|
|
85
|
-
assert(type(
|
|
86
|
-
assert(
|
|
81
|
+
assert(type(playerOneRating) == "number", "Bad playerOneRating")
|
|
82
|
+
assert(type(playerTwoRating) == "number", "Bad playerTwoRating")
|
|
83
|
+
assert(EloMatchResultUtils.isEloMatchResultList(eloMatchResultList), "Bad eloMatchResultList")
|
|
87
84
|
|
|
88
|
-
|
|
89
|
-
|
|
85
|
+
local newPlayerOneRating = EloUtils.getNewPlayerOneScore(config, playerOneRating, playerTwoRating, eloMatchResultList)
|
|
86
|
+
local newPlayerTwoRating = EloUtils.getNewPlayerOneScore(config, playerTwoRating, playerOneRating, EloUtils.fromOpponentPerspective(eloMatchResultList))
|
|
87
|
+
return newPlayerOneRating, newPlayerTwoRating
|
|
90
88
|
end
|
|
91
89
|
|
|
90
|
+
--[=[
|
|
91
|
+
Gets the change in elo for the given players and the results
|
|
92
|
+
|
|
93
|
+
@param config EloConfig
|
|
94
|
+
@param playerOneRating number
|
|
95
|
+
@param playerTwoRating number
|
|
96
|
+
@param eloMatchResultList { EloMatchResult }
|
|
97
|
+
@return number -- playerOneRating
|
|
98
|
+
@return number -- playerTwoRating
|
|
99
|
+
]=]
|
|
100
|
+
function EloUtils.getEloChange(config, playerOneRating, playerTwoRating, eloMatchResultList)
|
|
101
|
+
assert(EloUtils.isEloConfig(config), "Bad config")
|
|
102
|
+
assert(type(playerOneRating) == "number", "Bad playerOneRating")
|
|
103
|
+
assert(type(playerTwoRating) == "number", "Bad playerTwoRating")
|
|
104
|
+
assert(EloMatchResultUtils.isEloMatchResultList(eloMatchResultList), "Bad eloMatchResultList")
|
|
105
|
+
|
|
106
|
+
local newPlayerOneRating, newPlayerTwoRating = EloUtils.getNewElo(config, playerOneRating, playerTwoRating, eloMatchResultList)
|
|
107
|
+
local playerOneChange = newPlayerOneRating - playerOneRating
|
|
108
|
+
local playerTwoChange = newPlayerTwoRating - playerTwoRating
|
|
109
|
+
return playerOneChange, playerTwoChange
|
|
110
|
+
end
|
|
92
111
|
|
|
93
112
|
--[=[
|
|
94
113
|
Gets the new score for the player after a series of matches.
|
|
95
114
|
|
|
96
115
|
@param config EloConfig
|
|
97
|
-
@param
|
|
98
|
-
@param
|
|
99
|
-
@param
|
|
116
|
+
@param playerOneRating number
|
|
117
|
+
@param playerTwoRating number
|
|
118
|
+
@param eloMatchResultList { EloMatchResult }
|
|
100
119
|
]=]
|
|
101
|
-
function EloUtils.
|
|
120
|
+
function EloUtils.getNewPlayerOneScore(config, playerOneRating, playerTwoRating, eloMatchResultList)
|
|
102
121
|
assert(EloUtils.isEloConfig(config), "Bad config")
|
|
103
|
-
assert(type(
|
|
104
|
-
assert(type(
|
|
105
|
-
assert(
|
|
122
|
+
assert(type(playerOneRating) == "number", "Bad playerOneRating")
|
|
123
|
+
assert(type(playerTwoRating) == "number", "Bad playerTwoRating")
|
|
124
|
+
assert(EloMatchResultUtils.isEloMatchResultList(eloMatchResultList), "Bad eloMatchResultList")
|
|
106
125
|
|
|
107
|
-
return math.max(config.ratingFloor,
|
|
126
|
+
return math.max(config.ratingFloor, playerOneRating + EloUtils.getPlayerOneScoreAdjustment(config, playerOneRating, playerTwoRating, eloMatchResultList))
|
|
108
127
|
end
|
|
109
128
|
|
|
110
129
|
--[=[
|
|
@@ -115,16 +134,16 @@ end
|
|
|
115
134
|
:::
|
|
116
135
|
|
|
117
136
|
@param config EloConfig
|
|
118
|
-
@param
|
|
119
|
-
@param
|
|
137
|
+
@param playerOneRating number
|
|
138
|
+
@param playerTwoRating number
|
|
120
139
|
@return number
|
|
121
140
|
]=]
|
|
122
|
-
function EloUtils.
|
|
141
|
+
function EloUtils.getPlayerOneExpected(config, playerOneRating, playerTwoRating)
|
|
123
142
|
assert(EloUtils.isEloConfig(config), "Bad config")
|
|
124
|
-
assert(type(
|
|
125
|
-
assert(type(
|
|
143
|
+
assert(type(playerOneRating) == "number", "Bad playerOneRating")
|
|
144
|
+
assert(type(playerTwoRating) == "number", "Bad playerTwoRating")
|
|
126
145
|
|
|
127
|
-
local diff =
|
|
146
|
+
local diff = playerTwoRating - playerOneRating
|
|
128
147
|
return 1 / (1 + 10 ^ (diff / config.factor))
|
|
129
148
|
end
|
|
130
149
|
|
|
@@ -132,46 +151,97 @@ end
|
|
|
132
151
|
Gets the score adjustment for a given player's base.
|
|
133
152
|
|
|
134
153
|
@param config EloConfig
|
|
135
|
-
@param
|
|
136
|
-
@param
|
|
137
|
-
@param
|
|
154
|
+
@param playerOneRating number
|
|
155
|
+
@param playerTwoRating number
|
|
156
|
+
@param eloMatchResultList { EloMatchResult }
|
|
138
157
|
@return number
|
|
139
158
|
]=]
|
|
140
|
-
function EloUtils.
|
|
159
|
+
function EloUtils.getPlayerOneScoreAdjustment(config, playerOneRating, playerTwoRating, eloMatchResultList)
|
|
141
160
|
assert(EloUtils.isEloConfig(config), "Bad config")
|
|
142
|
-
assert(type(
|
|
143
|
-
assert(type(
|
|
144
|
-
assert(
|
|
161
|
+
assert(type(playerOneRating) == "number", "Bad playerOneRating")
|
|
162
|
+
assert(type(playerTwoRating) == "number", "Bad playerTwoRating")
|
|
163
|
+
assert(EloMatchResultUtils.isEloMatchResultList(eloMatchResultList), "Bad eloMatchResultList")
|
|
145
164
|
|
|
146
165
|
local adjustment = 0
|
|
147
|
-
local expected = EloUtils.
|
|
166
|
+
local expected = EloUtils.getPlayerOneExpected(config, playerOneRating, playerTwoRating)
|
|
167
|
+
|
|
168
|
+
if config.groupMultipleResultAsOne then
|
|
169
|
+
local wins = EloUtils.countPlayerOneWins(eloMatchResultList)
|
|
170
|
+
local losses = EloUtils.countPlayerTwoWins(eloMatchResultList)
|
|
171
|
+
local score = wins > losses and 1 or 0
|
|
172
|
+
local multiplier = 1
|
|
148
173
|
|
|
149
|
-
|
|
150
|
-
|
|
174
|
+
if wins > losses then
|
|
175
|
+
multiplier = wins
|
|
176
|
+
else
|
|
177
|
+
multiplier = losses
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
adjustment = multiplier*(score - expected)
|
|
181
|
+
else
|
|
182
|
+
for _, score in pairs(eloMatchResultList) do
|
|
183
|
+
adjustment = adjustment + (score - expected)
|
|
184
|
+
end
|
|
151
185
|
end
|
|
152
186
|
|
|
153
|
-
local kfactor = EloUtils.extractKFactor(config,
|
|
187
|
+
local kfactor = EloUtils.extractKFactor(config, playerOneRating)
|
|
154
188
|
return kfactor*adjustment
|
|
155
189
|
end
|
|
156
190
|
|
|
157
191
|
--[=[
|
|
158
192
|
Flips the scores for the opponent
|
|
159
193
|
|
|
160
|
-
@param
|
|
194
|
+
@param eloMatchResultList { EloMatchResult }
|
|
161
195
|
@return { number }
|
|
162
196
|
]=]
|
|
163
|
-
function EloUtils.fromOpponentPerspective(
|
|
164
|
-
assert(
|
|
197
|
+
function EloUtils.fromOpponentPerspective(eloMatchResultList)
|
|
198
|
+
assert(EloMatchResultUtils.isEloMatchResultList(eloMatchResultList), "Bad eloMatchResultList")
|
|
165
199
|
|
|
166
200
|
local newScores = {}
|
|
167
201
|
|
|
168
|
-
for index, score in pairs(
|
|
202
|
+
for index, score in pairs(eloMatchResultList) do
|
|
169
203
|
newScores[index] = 1 - score
|
|
170
204
|
end
|
|
171
205
|
|
|
172
206
|
return newScores
|
|
173
207
|
end
|
|
174
208
|
|
|
209
|
+
--[=[
|
|
210
|
+
Counts the number of wins for player one
|
|
211
|
+
|
|
212
|
+
@param eloMatchResultList { EloMatchResult }
|
|
213
|
+
@return { number }
|
|
214
|
+
]=]
|
|
215
|
+
function EloUtils.countPlayerOneWins(eloMatchResultList)
|
|
216
|
+
assert(EloMatchResultUtils.isEloMatchResultList(eloMatchResultList), "Bad eloMatchResultList")
|
|
217
|
+
|
|
218
|
+
local count = 0
|
|
219
|
+
for _, score in pairs(eloMatchResultList) do
|
|
220
|
+
if score == EloMatchResult.PLAYER_ONE_WIN then
|
|
221
|
+
count = count + 1
|
|
222
|
+
end
|
|
223
|
+
end
|
|
224
|
+
return count
|
|
225
|
+
end
|
|
226
|
+
|
|
227
|
+
--[=[
|
|
228
|
+
Counts the number of wins for player two
|
|
229
|
+
|
|
230
|
+
@param eloMatchResultList { EloMatchResult }
|
|
231
|
+
@return { number }
|
|
232
|
+
]=]
|
|
233
|
+
function EloUtils.countPlayerTwoWins(eloMatchResultList)
|
|
234
|
+
assert(EloMatchResultUtils.isEloMatchResultList(eloMatchResultList), "Bad eloMatchResultList")
|
|
235
|
+
|
|
236
|
+
local count = 0
|
|
237
|
+
for _, score in pairs(eloMatchResultList) do
|
|
238
|
+
if score == EloMatchResult.PLAYER_TWO_WIN then
|
|
239
|
+
count = count + 1
|
|
240
|
+
end
|
|
241
|
+
end
|
|
242
|
+
return count
|
|
243
|
+
end
|
|
244
|
+
|
|
175
245
|
--[=[
|
|
176
246
|
Standard kfactor formula for use in the elo config.
|
|
177
247
|
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
local EloUtils = require(script.Parent.EloUtils)
|
|
7
7
|
|
|
8
8
|
return function()
|
|
9
|
-
describe("EloUtils.
|
|
9
|
+
describe("EloUtils.getNewElo", function()
|
|
10
10
|
local config = EloUtils.createConfig()
|
|
11
11
|
|
|
12
12
|
local playerRating = 1400
|
|
@@ -17,12 +17,12 @@ return function()
|
|
|
17
17
|
local newPlayerDrawRating, newOpponentDrawRating
|
|
18
18
|
|
|
19
19
|
it("should change on win", function()
|
|
20
|
-
newPlayerWinRating, newOpponentWinRating = EloUtils.
|
|
20
|
+
newPlayerWinRating, newOpponentWinRating = EloUtils.getNewElo(
|
|
21
21
|
config,
|
|
22
22
|
playerRating,
|
|
23
23
|
opponentRating,
|
|
24
24
|
{
|
|
25
|
-
EloUtils.
|
|
25
|
+
EloUtils.MatchResult.PLAYER_ONE_WIN;
|
|
26
26
|
})
|
|
27
27
|
|
|
28
28
|
expect(newPlayerWinRating > playerRating).to.equal(true)
|
|
@@ -30,12 +30,12 @@ return function()
|
|
|
30
30
|
end)
|
|
31
31
|
|
|
32
32
|
it("should change on a loss", function()
|
|
33
|
-
newPlayerLossRating, newOpponentLossRating = EloUtils.
|
|
33
|
+
newPlayerLossRating, newOpponentLossRating = EloUtils.getNewElo(
|
|
34
34
|
config,
|
|
35
35
|
playerRating,
|
|
36
36
|
opponentRating,
|
|
37
37
|
{
|
|
38
|
-
EloUtils.
|
|
38
|
+
EloUtils.MatchResult.PLAYER_TWO_WIN;
|
|
39
39
|
})
|
|
40
40
|
|
|
41
41
|
expect(newPlayerLossRating < playerRating).to.equal(true)
|
|
@@ -43,12 +43,12 @@ return function()
|
|
|
43
43
|
end)
|
|
44
44
|
|
|
45
45
|
it("should change on a draw", function()
|
|
46
|
-
newPlayerDrawRating, newOpponentDrawRating = EloUtils.
|
|
46
|
+
newPlayerDrawRating, newOpponentDrawRating = EloUtils.getNewElo(
|
|
47
47
|
config,
|
|
48
48
|
playerRating,
|
|
49
49
|
opponentRating,
|
|
50
50
|
{
|
|
51
|
-
EloUtils.
|
|
51
|
+
EloUtils.MatchResult.DRAW;
|
|
52
52
|
})
|
|
53
53
|
|
|
54
54
|
expect(newPlayerDrawRating > playerRating).to.equal(true)
|
|
@@ -0,0 +1,358 @@
|
|
|
1
|
+
--[[
|
|
2
|
+
@class EloUtils.story
|
|
3
|
+
]]
|
|
4
|
+
|
|
5
|
+
local require = require(game:GetService("ServerScriptService"):FindFirstChild("LoaderUtils", true).Parent).load(script)
|
|
6
|
+
|
|
7
|
+
local Maid = require("Maid")
|
|
8
|
+
local Blend = require("Blend")
|
|
9
|
+
local EloUtils = require("EloUtils")
|
|
10
|
+
|
|
11
|
+
local function TextLabel(props)
|
|
12
|
+
return Blend.New "TextLabel" {
|
|
13
|
+
Size = props.Size;
|
|
14
|
+
TextXAlignment = props.TextXAlignment;
|
|
15
|
+
TextYAlignment = props.TextYAlignment;
|
|
16
|
+
BackgroundTransparency = 1;
|
|
17
|
+
Font = Enum.Font.FredokaOne;
|
|
18
|
+
AnchorPoint = props.AnchorPoint;
|
|
19
|
+
Position = props.Position;
|
|
20
|
+
TextColor3 = props.TextColor3;
|
|
21
|
+
TextSize = props.TextSize or 20;
|
|
22
|
+
Text = props.Text;
|
|
23
|
+
RichText = props.RichText;
|
|
24
|
+
};
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
local function MatchResultCard(props)
|
|
28
|
+
return Blend.New "Frame" {
|
|
29
|
+
Name = "MatchResultCard";
|
|
30
|
+
Size = UDim2.new(0, 140, 0, 0);
|
|
31
|
+
AutomaticSize = Enum.AutomaticSize.Y;
|
|
32
|
+
BackgroundTransparency = 1;
|
|
33
|
+
|
|
34
|
+
TextLabel {
|
|
35
|
+
Name = "PlayerLabel";
|
|
36
|
+
Size = UDim2.new(1, 0, 0, 30);
|
|
37
|
+
TextXAlignment = Enum.TextXAlignment.Center;
|
|
38
|
+
BackgroundTransparency = 1;
|
|
39
|
+
TextSize = 25;
|
|
40
|
+
TextColor3 = Blend.Computed(props.Change, function(change)
|
|
41
|
+
if change > 0 then
|
|
42
|
+
return Color3.fromRGB(61, 83, 60)
|
|
43
|
+
else
|
|
44
|
+
return Color3.fromRGB(103, 39, 39)
|
|
45
|
+
end
|
|
46
|
+
end);
|
|
47
|
+
Text = Blend.Computed(props.OldElo, props.NewElo, props.IsWinner, function(oldElo, newElo, winner)
|
|
48
|
+
if winner then
|
|
49
|
+
return string.format("%d → %d", oldElo, newElo)
|
|
50
|
+
else
|
|
51
|
+
return string.format("%d → %d", oldElo, newElo)
|
|
52
|
+
end
|
|
53
|
+
end);
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
Blend.New "Frame" {
|
|
57
|
+
Name = "Elo change";
|
|
58
|
+
BackgroundColor3 = Blend.Computed(props.Change, function(change)
|
|
59
|
+
if change > 0 then
|
|
60
|
+
return Color3.fromRGB(61, 83, 60)
|
|
61
|
+
else
|
|
62
|
+
return Color3.fromRGB(103, 39, 39)
|
|
63
|
+
end
|
|
64
|
+
end);
|
|
65
|
+
Size = UDim2.new(0, 60, 0, 30);
|
|
66
|
+
|
|
67
|
+
Blend.New "UICorner" {
|
|
68
|
+
CornerRadius = UDim.new(0.5, 0);
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
TextLabel {
|
|
72
|
+
Size = UDim2.new(1, 0, 0, 30);
|
|
73
|
+
BackgroundTransparency = 1;
|
|
74
|
+
TextColor3 = Blend.Computed(props.Change, function(change)
|
|
75
|
+
if change > 0 then
|
|
76
|
+
return Color3.fromRGB(221, 255, 223)
|
|
77
|
+
else
|
|
78
|
+
return Color3.fromRGB(255, 219, 219)
|
|
79
|
+
end
|
|
80
|
+
end);
|
|
81
|
+
Text = Blend.Computed(props.Change, function(change)
|
|
82
|
+
if change > 0 then
|
|
83
|
+
return string.format("+%d", change)
|
|
84
|
+
else
|
|
85
|
+
return string.format("%d", change)
|
|
86
|
+
end
|
|
87
|
+
end);
|
|
88
|
+
};
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
Blend.New "UIListLayout" {
|
|
92
|
+
FillDirection = Enum.FillDirection.Vertical;
|
|
93
|
+
HorizontalAlignment = Enum.HorizontalAlignment.Center;
|
|
94
|
+
Padding = UDim.new(0, 5);
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
local function PlayerScoreChange(props)
|
|
100
|
+
local playerOneWin = EloUtils.countPlayerOneWins(props.MatchResults) > EloUtils.countPlayerTwoWins(props.MatchResults)
|
|
101
|
+
|
|
102
|
+
return Blend.New "Frame" {
|
|
103
|
+
Name = "PlayerScoreChange";
|
|
104
|
+
Size = UDim2.new(0, 0, 0, 0);
|
|
105
|
+
AutomaticSize = Enum.AutomaticSize.XY;
|
|
106
|
+
BackgroundColor3 = Color3.new(0.9, 0.9, 0.9);
|
|
107
|
+
BackgroundTransparency = 0;
|
|
108
|
+
|
|
109
|
+
Blend.New "UIPadding" {
|
|
110
|
+
PaddingTop = UDim.new(0, 10);
|
|
111
|
+
PaddingBottom = UDim.new(0, 10);
|
|
112
|
+
PaddingLeft = UDim.new(0, 10);
|
|
113
|
+
PaddingRight = UDim.new(0, 10);
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
Blend.New "UICorner" {
|
|
117
|
+
CornerRadius = UDim.new(0, 10);
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
Blend.New "UIGradient" {
|
|
121
|
+
Color = Blend.Computed(playerOneWin, function(winner)
|
|
122
|
+
if winner then
|
|
123
|
+
return ColorSequence.new(Color3.fromRGB(208, 255, 194), Color3.fromRGB(255, 197, 197))
|
|
124
|
+
else
|
|
125
|
+
return ColorSequence.new(Color3.fromRGB(255, 197, 197), Color3.fromRGB(208, 255, 194))
|
|
126
|
+
end
|
|
127
|
+
end)
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
Blend.New "UIListLayout" {
|
|
131
|
+
FillDirection = Enum.FillDirection.Horizontal;
|
|
132
|
+
VerticalAlignment = Enum.VerticalAlignment.Center;
|
|
133
|
+
Padding = UDim.new(0, 5);
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
MatchResultCard({
|
|
137
|
+
IsWinner = playerOneWin;
|
|
138
|
+
NewElo = props.PlayerOne.New;
|
|
139
|
+
OldElo = props.PlayerOne.Old;
|
|
140
|
+
Change = props.PlayerOne.New - props.PlayerOne.Old;
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
Blend.New "Frame" {
|
|
144
|
+
Name = "MatchResults";
|
|
145
|
+
Size = UDim2.new(0, 90, 0, 40);
|
|
146
|
+
BackgroundColor3 = Color3.fromRGB(185, 185, 185);
|
|
147
|
+
|
|
148
|
+
Blend.New "UICorner" {
|
|
149
|
+
CornerRadius = UDim.new(0.5, 0);
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
TextLabel {
|
|
153
|
+
RichText = true;
|
|
154
|
+
Size = UDim2.new(1, 0, 0, 30);
|
|
155
|
+
TextColor3 = Color3.fromRGB(24, 24, 24);
|
|
156
|
+
AnchorPoint = Vector2.new(0.5, 0.5);
|
|
157
|
+
Position = UDim2.fromScale(0.5, 0.5);
|
|
158
|
+
BackgroundTransparency = 1;
|
|
159
|
+
Text = Blend.Computed(props.MatchResults, function(matchScores)
|
|
160
|
+
local playerOneWins = EloUtils.countPlayerOneWins(matchScores)
|
|
161
|
+
local playerTwoWins = EloUtils.countPlayerTwoWins(matchScores)
|
|
162
|
+
|
|
163
|
+
if playerOneWins > playerTwoWins then
|
|
164
|
+
return string.format("<font color='#355024'><stroke color='#9dd59a'>%d</stroke></font> - %d", playerOneWins, playerTwoWins)
|
|
165
|
+
else
|
|
166
|
+
return string.format("%d - <font color='#355024'><stroke color='#9dd59a'>%d</stroke></font>", playerOneWins, playerTwoWins)
|
|
167
|
+
end
|
|
168
|
+
end);
|
|
169
|
+
TextSize = 20;
|
|
170
|
+
};
|
|
171
|
+
};
|
|
172
|
+
|
|
173
|
+
MatchResultCard({
|
|
174
|
+
IsWinner = not playerOneWin;
|
|
175
|
+
NewElo = props.PlayerTwo.New;
|
|
176
|
+
OldElo = props.PlayerTwo.Old;
|
|
177
|
+
Change = props.PlayerTwo.New - props.PlayerTwo.Old;
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
local function EloGroup(props)
|
|
183
|
+
return Blend.New "Frame" {
|
|
184
|
+
Name = "EloGroup";
|
|
185
|
+
AutomaticSize = Enum.AutomaticSize.XY;
|
|
186
|
+
BackgroundTransparency = 1;
|
|
187
|
+
|
|
188
|
+
Blend.New "Frame" {
|
|
189
|
+
BackgroundColor3 = Color3.new(0.1, 0.1, 0.1);
|
|
190
|
+
AutomaticSize = Enum.AutomaticSize.XY;
|
|
191
|
+
|
|
192
|
+
Blend.New "UICorner" {
|
|
193
|
+
CornerRadius = UDim.new(0, 15);
|
|
194
|
+
};
|
|
195
|
+
|
|
196
|
+
Blend.New "UIStroke" {
|
|
197
|
+
Color = Color3.fromRGB(69, 170, 156);
|
|
198
|
+
Thickness = 3;
|
|
199
|
+
};
|
|
200
|
+
|
|
201
|
+
Blend.New "UIListLayout" {
|
|
202
|
+
FillDirection = Enum.FillDirection.Vertical;
|
|
203
|
+
HorizontalAlignment = Enum.HorizontalAlignment.Center;
|
|
204
|
+
Padding = UDim.new(0, 5);
|
|
205
|
+
};
|
|
206
|
+
|
|
207
|
+
Blend.New "Frame" {
|
|
208
|
+
Name = "Children";
|
|
209
|
+
AutomaticSize = Enum.AutomaticSize.XY;
|
|
210
|
+
BackgroundTransparency = 1;
|
|
211
|
+
|
|
212
|
+
Blend.New "UIListLayout" {
|
|
213
|
+
FillDirection = Enum.FillDirection.Vertical;
|
|
214
|
+
HorizontalAlignment = Enum.HorizontalAlignment.Center;
|
|
215
|
+
Padding = UDim.new(0, 5);
|
|
216
|
+
};
|
|
217
|
+
|
|
218
|
+
props.Items;
|
|
219
|
+
};
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
Blend.New "UIPadding" {
|
|
223
|
+
PaddingTop = UDim.new(0, 25);
|
|
224
|
+
PaddingBottom = UDim.new(0, 10);
|
|
225
|
+
PaddingLeft = UDim.new(0, 10);
|
|
226
|
+
PaddingRight = UDim.new(0, 10);
|
|
227
|
+
};
|
|
228
|
+
};
|
|
229
|
+
|
|
230
|
+
Blend.New "UIPadding" {
|
|
231
|
+
PaddingTop = UDim.new(0, 30);
|
|
232
|
+
};
|
|
233
|
+
|
|
234
|
+
Blend.New "Frame" {
|
|
235
|
+
Name = "Header";
|
|
236
|
+
Size = UDim2.new(0, 200, 0, 30);
|
|
237
|
+
AnchorPoint = Vector2.new(0.5, 0.5);
|
|
238
|
+
Position = UDim2.fromScale(0.5, 0);
|
|
239
|
+
BackgroundColor3 = Color3.new(0.1, 0.1, 0.1);
|
|
240
|
+
|
|
241
|
+
Blend.New "UIStroke" {
|
|
242
|
+
Color = Color3.fromRGB(69, 170, 156);
|
|
243
|
+
Thickness = 3;
|
|
244
|
+
};
|
|
245
|
+
|
|
246
|
+
Blend.New "UICorner" {
|
|
247
|
+
CornerRadius = UDim.new(0.5, 0);
|
|
248
|
+
};
|
|
249
|
+
|
|
250
|
+
TextLabel({
|
|
251
|
+
TextColor3 = Color3.new(1, 1, 1);
|
|
252
|
+
Text = props.HeaderText;
|
|
253
|
+
Size = UDim2.fromScale(1, 1);
|
|
254
|
+
})
|
|
255
|
+
};
|
|
256
|
+
}
|
|
257
|
+
end
|
|
258
|
+
|
|
259
|
+
return function(target)
|
|
260
|
+
local maid = Maid.new()
|
|
261
|
+
|
|
262
|
+
local options = {}
|
|
263
|
+
local config = EloUtils.createConfig()
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
for playerOneElo=800, 2400, 200 do
|
|
267
|
+
for playerTwoEloDiff=-400, 400, 100 do
|
|
268
|
+
local groupOptions = {}
|
|
269
|
+
local playerTwoElo = playerOneElo + playerTwoEloDiff
|
|
270
|
+
|
|
271
|
+
local matchResultTypes = {
|
|
272
|
+
string.format("%d wins vs %d", playerOneElo, playerTwoElo);
|
|
273
|
+
|
|
274
|
+
{
|
|
275
|
+
results = { EloUtils.MatchResult.PLAYER_ONE_WIN }
|
|
276
|
+
};
|
|
277
|
+
{
|
|
278
|
+
results = { EloUtils.MatchResult.PLAYER_ONE_WIN, EloUtils.MatchResult.PLAYER_ONE_WIN, EloUtils.MatchResult.PLAYER_TWO_WIN }
|
|
279
|
+
};
|
|
280
|
+
{
|
|
281
|
+
results = { EloUtils.MatchResult.PLAYER_ONE_WIN, EloUtils.MatchResult.PLAYER_ONE_WIN }
|
|
282
|
+
};
|
|
283
|
+
|
|
284
|
+
string.format("%d loses vs %d", playerOneElo, playerTwoElo);
|
|
285
|
+
|
|
286
|
+
{
|
|
287
|
+
results = { EloUtils.MatchResult.PLAYER_TWO_WIN }
|
|
288
|
+
};
|
|
289
|
+
{
|
|
290
|
+
results = { EloUtils.MatchResult.PLAYER_TWO_WIN, EloUtils.MatchResult.PLAYER_TWO_WIN, EloUtils.MatchResult.PLAYER_ONE_WIN }
|
|
291
|
+
};
|
|
292
|
+
{
|
|
293
|
+
results = { EloUtils.MatchResult.PLAYER_TWO_WIN, EloUtils.MatchResult.PLAYER_TWO_WIN }
|
|
294
|
+
};
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
for _, matchResultType in pairs(matchResultTypes) do
|
|
298
|
+
if type(matchResultType) == "string" then
|
|
299
|
+
table.insert(groupOptions, TextLabel({
|
|
300
|
+
TextColor3 = Color3.new(1, 1, 1);
|
|
301
|
+
Text = matchResultType;
|
|
302
|
+
Size = UDim2.new(0, 100, 0, 30);
|
|
303
|
+
}))
|
|
304
|
+
|
|
305
|
+
continue
|
|
306
|
+
end
|
|
307
|
+
|
|
308
|
+
local matchResults = matchResultType.results
|
|
309
|
+
|
|
310
|
+
local scoreA, scoreB = EloUtils.getNewElo(config, playerOneElo, playerTwoElo, matchResults)
|
|
311
|
+
table.insert(groupOptions, PlayerScoreChange({
|
|
312
|
+
MatchResults = matchResults;
|
|
313
|
+
PlayerOne = {
|
|
314
|
+
Old = playerOneElo;
|
|
315
|
+
New = scoreA;
|
|
316
|
+
};
|
|
317
|
+
PlayerTwo = {
|
|
318
|
+
Old = playerTwoElo;
|
|
319
|
+
New = scoreB;
|
|
320
|
+
};
|
|
321
|
+
}))
|
|
322
|
+
end
|
|
323
|
+
|
|
324
|
+
table.insert(options, EloGroup {
|
|
325
|
+
HeaderText = string.format("%d vs %d", playerOneElo, playerTwoElo);
|
|
326
|
+
Items = groupOptions;
|
|
327
|
+
})
|
|
328
|
+
end
|
|
329
|
+
end
|
|
330
|
+
|
|
331
|
+
maid:GiveTask(Blend.mount(target, {
|
|
332
|
+
Blend.New "ScrollingFrame" {
|
|
333
|
+
Size = UDim2.new(1, 0, 1, 0);
|
|
334
|
+
BackgroundTransparency = 1;
|
|
335
|
+
CanvasSize = UDim2.new(0, 0, 0, 0);
|
|
336
|
+
AutomaticCanvasSize = Enum.AutomaticSize.Y;
|
|
337
|
+
|
|
338
|
+
Blend.New "UIPadding" {
|
|
339
|
+
PaddingTop = UDim.new(0, 10);
|
|
340
|
+
PaddingBottom = UDim.new(0, 10);
|
|
341
|
+
PaddingLeft = UDim.new(0, 10);
|
|
342
|
+
PaddingRight = UDim.new(0, 10);
|
|
343
|
+
};
|
|
344
|
+
|
|
345
|
+
Blend.New "UIListLayout" {
|
|
346
|
+
FillDirection = Enum.FillDirection.Vertical;
|
|
347
|
+
HorizontalAlignment = Enum.HorizontalAlignment.Center;
|
|
348
|
+
Padding = UDim.new(0, 10);
|
|
349
|
+
};
|
|
350
|
+
|
|
351
|
+
options;
|
|
352
|
+
}
|
|
353
|
+
}))
|
|
354
|
+
|
|
355
|
+
return function()
|
|
356
|
+
maid:DoCleaning()
|
|
357
|
+
end
|
|
358
|
+
end
|