@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 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
@@ -1,6 +1,6 @@
1
1
  MIT License
2
2
 
3
- Copyright (c) 2014-2021 Quenty
3
+ Copyright (c) 2014-2023 James Onnen (Quenty)
4
4
 
5
5
  Permission is hereby granted, free of charge, to any person obtaining a copy
6
6
  of this software and associated documentation files (the "Software"), to deal
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@quenty/elo",
3
- "version": "1.1.0",
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
- "gitHead": "0797de955876b050fb24875de0423453e268b2ce"
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
@@ -5,34 +5,29 @@
5
5
  ```lua
6
6
  local config = EloUtils.createConfig()
7
7
 
8
- local playerRating = 1400
9
- local opponentRating = 1800
8
+ local playerOneRating = 1400
9
+ local playerTwoRating = 1800
10
10
 
11
11
  -- Update rating!
12
- playerRating, opponentRating = EloUtils.getNewScores(
12
+ playerOneRating, playerTwoRating = EloUtils.getNewElo(
13
13
  config,
14
- playerRating,
15
- opponentRating,
14
+ playerOneRating,
15
+ playerTwoRating,
16
16
  {
17
- EloUtils.Scores.WIN;
17
+ EloMatchResult.PLAYER_ONE_WIN;
18
18
  })
19
19
 
20
20
  -- New rankings!
21
- print(playerRating, opponentRating)
21
+ print(playerOneRating, playerTwoRating)
22
22
  ```
23
23
  ]=]
24
24
 
25
- local EloUtils = {}
25
+ local require = require(script.Parent.loader).load(script)
26
+
27
+ local EloMatchResult = require("EloMatchResult")
28
+ local EloMatchResultUtils = require("EloMatchResultUtils")
26
29
 
27
- EloUtils.Scores = setmetatable({
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 playerRating number
77
- @param opponentRating number
78
- @param matchScores { number } -- 0 for loss, 1 for win, 0.5 for draw.
79
- @return number -- playerRating
80
- @return number -- opponentRating
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.getNewScores(config, playerRating, opponentRating, matchScores)
79
+ function EloUtils.getNewElo(config, playerOneRating, playerTwoRating, eloMatchResultList)
83
80
  assert(EloUtils.isEloConfig(config), "Bad config")
84
- assert(type(playerRating) == "number", "Bad playerRating")
85
- assert(type(opponentRating) == "number", "Bad opponentRating")
86
- assert(type(matchScores) == "table", "Bad matchScores")
81
+ assert(type(playerOneRating) == "number", "Bad playerOneRating")
82
+ assert(type(playerTwoRating) == "number", "Bad playerTwoRating")
83
+ assert(EloMatchResultUtils.isEloMatchResultList(eloMatchResultList), "Bad eloMatchResultList")
87
84
 
88
- return EloUtils.getNewScore(config, playerRating, opponentRating, matchScores),
89
- EloUtils.getNewScore(config, opponentRating, playerRating, EloUtils.fromOpponentPerspective(matchScores))
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 playerRating number
98
- @param opponentRating number
99
- @param matchScores { number } -- 0 for loss, 1 for win, 0.5 for draw.
116
+ @param playerOneRating number
117
+ @param playerTwoRating number
118
+ @param eloMatchResultList { EloMatchResult }
100
119
  ]=]
101
- function EloUtils.getNewScore(config, playerRating, opponentRating, matchScores)
120
+ function EloUtils.getNewPlayerOneScore(config, playerOneRating, playerTwoRating, eloMatchResultList)
102
121
  assert(EloUtils.isEloConfig(config), "Bad config")
103
- assert(type(playerRating) == "number", "Bad playerRating")
104
- assert(type(opponentRating) == "number", "Bad opponentRating")
105
- assert(type(matchScores) == "table", "Bad matchScores")
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, playerRating + EloUtils.getScoreAdjustment(config, playerRating, opponentRating, matchScores))
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 playerRating number
119
- @param opponentRating number
137
+ @param playerOneRating number
138
+ @param playerTwoRating number
120
139
  @return number
121
140
  ]=]
122
- function EloUtils.getExpected(config, playerRating, opponentRating)
141
+ function EloUtils.getPlayerOneExpected(config, playerOneRating, playerTwoRating)
123
142
  assert(EloUtils.isEloConfig(config), "Bad config")
124
- assert(type(playerRating) == "number", "Bad playerRating")
125
- assert(type(opponentRating) == "number", "Bad opponentRating")
143
+ assert(type(playerOneRating) == "number", "Bad playerOneRating")
144
+ assert(type(playerTwoRating) == "number", "Bad playerTwoRating")
126
145
 
127
- local diff = opponentRating - playerRating
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 playerRating number
136
- @param opponentRating number
137
- @param matchScores { number } -- 0 for loss, 1 for win, 0.5 for draw.
154
+ @param playerOneRating number
155
+ @param playerTwoRating number
156
+ @param eloMatchResultList { EloMatchResult }
138
157
  @return number
139
158
  ]=]
140
- function EloUtils.getScoreAdjustment(config, playerRating, opponentRating, matchScores)
159
+ function EloUtils.getPlayerOneScoreAdjustment(config, playerOneRating, playerTwoRating, eloMatchResultList)
141
160
  assert(EloUtils.isEloConfig(config), "Bad config")
142
- assert(type(playerRating) == "number", "Bad playerRating")
143
- assert(type(opponentRating) == "number", "Bad opponentRating")
144
- assert(type(matchScores) == "table", "Bad matchScores")
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.getExpected(config, playerRating, opponentRating)
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
- for _, score in pairs(matchScores) do
150
- adjustment = adjustment + (score - expected)
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, playerRating)
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 matchScores { number } -- 0 for loss, 1 for win, 0.5 for draw.
194
+ @param eloMatchResultList { EloMatchResult }
161
195
  @return { number }
162
196
  ]=]
163
- function EloUtils.fromOpponentPerspective(matchScores)
164
- assert(type(matchScores) == "table", "Bad matchScores")
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(matchScores) do
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.getNewScores", function()
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.getNewScores(
20
+ newPlayerWinRating, newOpponentWinRating = EloUtils.getNewElo(
21
21
  config,
22
22
  playerRating,
23
23
  opponentRating,
24
24
  {
25
- EloUtils.Scores.WIN;
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.getNewScores(
33
+ newPlayerLossRating, newOpponentLossRating = EloUtils.getNewElo(
34
34
  config,
35
35
  playerRating,
36
36
  opponentRating,
37
37
  {
38
- EloUtils.Scores.LOSS;
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.getNewScores(
46
+ newPlayerDrawRating, newOpponentDrawRating = EloUtils.getNewElo(
47
47
  config,
48
48
  playerRating,
49
49
  opponentRating,
50
50
  {
51
- EloUtils.Scores.DRAW;
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