rating-system 1.0.3

Sign up to get free protection for your applications and to get access to all the features.
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: []