rating-system 1.0.3

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.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: f6c7bc812ebc0d2ce970dd4b57183fafd0407892950a8f749d01d9d9d5c9a0dc
4
+ data.tar.gz: 11a8e8e6e0dc3c778e1142049c905bb5b22fb01f729e5ce15fd1561f93b1322a
5
+ SHA512:
6
+ metadata.gz: 2dbd68b9ee8a3e9366436932e2a308f7f97f65b4f944ca16fc925a542b7dcefdb377ff5025c68e3c5dbab072dc89aa5bfaa2c48cd7db803ca8f9de0ea925efec
7
+ data.tar.gz: 07b206ba45014cf5f8aafb5e9ecc5bb453f8dc3def14b434129fa1526283628343fd51d36468b3a244b6508c64aa949836084552ccd59242ee41c4c6dadc70d7
@@ -0,0 +1,12 @@
1
+ module RatingSystem
2
+ class Contestant
3
+ attr_accessor :user_id, :rank, :points, :rating, :delta, :seed, :need_rating
4
+
5
+ def initialize(user_id, rank, points, rating)
6
+ @user_id = user_id
7
+ @rank = rank
8
+ @points = points
9
+ @rating = rating
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,190 @@
1
+ module RatingSystem
2
+ class RatingCalculator
3
+ class << self
4
+ def get_new_ratings(contestants)
5
+ # previous_ratings
6
+ previous_ratings = {}
7
+ contestants.each { |c| previous_ratings[c[:user_id]] = c[:previous_rating] }
8
+
9
+ # standings_rows
10
+ standings_rows = []
11
+ total = 1_000_000
12
+
13
+ contestants.each do |c|
14
+ points = total - c[:position]
15
+ if points.negative?
16
+ raise StandardError, "Position of contestant is higher than length:\n ' #{c[:position]}', '#{c[:user_id]}"
17
+ end
18
+
19
+ standings_rows << RatingSystem::StandingsRow.new(c[:user_id], points)
20
+ end
21
+
22
+ rating_calculator = RatingSystem::RatingCalculator.new
23
+ rating_changes = rating_calculator.calculate_rating_changes(previous_ratings, standings_rows)
24
+
25
+ contestants.each do |c|
26
+ c[:delta] = rating_changes[c[:user_id]]
27
+ c[:new_rating] = c[:previous_rating].to_i + c[:delta].to_i
28
+ end
29
+
30
+ contestants
31
+ end
32
+
33
+ def run_benchmarks(contestant_count = 100)
34
+ start = Time.now
35
+ contestants = []
36
+ (1..contestant_count).each do |i|
37
+ h = {
38
+ position: i,
39
+ user_id: i,
40
+ previous_rating: 1500
41
+ }
42
+ contestants << h
43
+ end
44
+
45
+ RatingSystem::RatingCalculator.get_new_ratings(contestants)
46
+ finish = Time.now
47
+
48
+ diff = finish - start
49
+ p "total execution time: #{diff} seconds"
50
+ end
51
+ end
52
+
53
+ def calculate_rating_changes(previous_ratings, standings_rows)
54
+ contestants = []
55
+
56
+ standings_rows.each do |standings_row|
57
+ rank = standings_row.rank
58
+ user_id = standings_row.user_id
59
+ contestants << RatingSystem::Contestant.new(user_id, rank, standings_row.points, previous_ratings[user_id])
60
+ end
61
+
62
+ contestants = process(contestants)
63
+
64
+ rating_changes = {}
65
+ contestants.each { |contestant| rating_changes[contestant.user_id] = contestant.delta }
66
+
67
+ rating_changes
68
+ end
69
+
70
+ def process(contestants)
71
+ return if contestants.length.zero?
72
+
73
+ contestants = reassign_ranks(contestants)
74
+
75
+ contestants.each do |contestant|
76
+ contestant.seed = get_seed(contestants, contestant, contestant.rating)
77
+ # calculating geometric mean of rank and seed
78
+ mid_rank = Math.sqrt(contestant.rank * contestant.seed)
79
+ contestant.need_rating = get_rating_to_rank(contestants, contestant, mid_rank)
80
+ contestant.delta = ((contestant.need_rating - contestant.rating) / 2.00).truncate
81
+ end
82
+
83
+ contestants = adjust_inflation(contestants)
84
+ validate_deltas(contestants)
85
+ contestants
86
+ end
87
+
88
+ def adjust_inflation(contestants)
89
+ contestants = sort_by_rating_desc(contestants)
90
+
91
+ # Total sum should not be more than zero.
92
+ sum = 0
93
+ contestants.each { |c| sum += c.delta }
94
+
95
+ inc = (-sum / contestants.length - 1)
96
+ contestants.each { |c| c.delta += inc }
97
+
98
+ # Sum of top-4*sqrt should be adjusted to zero.
99
+ sum = 0
100
+ zero_sum_count = [4 * Math.sqrt(contestants.length).round, contestants.length].min.truncate
101
+
102
+ (0..zero_sum_count - 1).each { |i| sum += contestants[i].delta }
103
+
104
+ inc = [[(-sum / zero_sum_count).truncate, -10].max, 0].min
105
+ contestants.each { |c| c.delta += inc }
106
+ contestants
107
+ end
108
+
109
+ def reassign_ranks(contestants)
110
+ contestants = sort_by_points_desc(contestants)
111
+
112
+ contestants.each do |contestant|
113
+ contestant.rank = 0
114
+ contestant.delta = 0
115
+ end
116
+
117
+ first = 0
118
+ points = contestants.first.points
119
+
120
+ (1..contestants.length - 1).each do |i|
121
+ next if contestants[i].points >= points
122
+
123
+ (first..i).each { |j| contestants[j].rank = i }
124
+ first = i
125
+ points = contestants[i].points
126
+ end
127
+
128
+ rank = contestants.length
129
+ (first..contestants.length - 1).each { |j| contestants[j].rank = rank }
130
+
131
+ contestants
132
+ end
133
+
134
+ def sort_by_points_desc(contestants)
135
+ contestants.sort! { |a, b| b.points <=> a.points }
136
+ end
137
+
138
+ def get_elo_win_probability(ra, rb)
139
+ 1.0 / (1 + (10**((rb - ra) / 400.0)))
140
+ end
141
+
142
+ def get_rating_to_rank(contestants, contestant, rank)
143
+ left = 1
144
+ right = 8000
145
+
146
+ while (right - left) > 1
147
+ mid_rating = ((left + right) / 2.00).truncate
148
+
149
+ if get_seed(contestants, contestant, mid_rating) < rank
150
+ right = mid_rating
151
+ else
152
+ left = mid_rating
153
+ end
154
+ end
155
+
156
+ left
157
+ end
158
+
159
+ # get seed for a contestant if the constant has a particular rating
160
+ def get_seed(contestants, contestant, rating)
161
+ result = 1
162
+ contestants.each do |other|
163
+ result += get_elo_win_probability(other.rating, rating) if other.user_id != contestant.user_id
164
+ end
165
+ result
166
+ end
167
+
168
+ def sort_by_rating_desc(contestants)
169
+ contestants.sort! { |a, b| b.rating <=> a.rating }
170
+ end
171
+
172
+ def validate_deltas(contestants)
173
+ contestants = sort_by_points_desc(contestants)
174
+ (0..contestants.length - 1).each do |i|
175
+ (i + 1..contestants.length - 1).each do |j|
176
+ # Contestant i has better place than j
177
+ if contestants[i].rating > contestants[j].rating && (contestants[i].rating + contestants[i].delta < contestants[j].rating + contestants[j].delta)
178
+ raise StandardError,
179
+ "First rating invariant failed #{contestants[i].user_id} vs. #{contestants[j].user_id.user_id}"
180
+ end
181
+
182
+ if contestants[i].rating < contestants[j].rating && (contestants[i].delta < contestants[j].delta)
183
+ raise StandardError,
184
+ "Second rating invariant failed #{contestants[i].user_id} vs. #{contestants[j].user_id}"
185
+ end
186
+ end
187
+ end
188
+ end
189
+ end
190
+ end
@@ -0,0 +1,11 @@
1
+ module RatingSystem
2
+ class StandingsRow
3
+ attr_accessor :rank, :user_id, :points
4
+
5
+ def initialize(user_id, points)
6
+ @rank = 0.0
7
+ @user_id = user_id
8
+ @points = points
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'rating_system/contestant'
4
+ require_relative 'rating_system/rating_calculator'
5
+ require_relative 'rating_system/standings_row'
6
+
7
+ # A simple rating system for multiplayer games
8
+ module RatingSystem
9
+ end
metadata ADDED
@@ -0,0 +1,48 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: rating-system
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.3
5
+ platform: ruby
6
+ authors:
7
+ - Ramit Koul
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2024-06-16 00:00:00.000000000 Z
12
+ dependencies: []
13
+ description: A simple rating system for multiplayer games
14
+ email: ramitkaul@gmail.com
15
+ executables: []
16
+ extensions: []
17
+ extra_rdoc_files: []
18
+ files:
19
+ - lib/rating_system.rb
20
+ - lib/rating_system/contestant.rb
21
+ - lib/rating_system/rating_calculator.rb
22
+ - lib/rating_system/standings_row.rb
23
+ homepage: https://github.com/rkwap/rating-system
24
+ licenses:
25
+ - MIT
26
+ metadata:
27
+ allowed_push_host: https://rubygems.org
28
+ source_code_uri: https://github.com/rkwap/rating-system
29
+ post_install_message:
30
+ rdoc_options: []
31
+ require_paths:
32
+ - lib
33
+ required_ruby_version: !ruby/object:Gem::Requirement
34
+ requirements:
35
+ - - ">="
36
+ - !ruby/object:Gem::Version
37
+ version: 2.7.4
38
+ required_rubygems_version: !ruby/object:Gem::Requirement
39
+ requirements:
40
+ - - ">="
41
+ - !ruby/object:Gem::Version
42
+ version: '0'
43
+ requirements: []
44
+ rubygems_version: 3.2.33
45
+ signing_key:
46
+ specification_version: 4
47
+ summary: A simple rating system
48
+ test_files: []