elo_rankable 0.2.0 โ†’ 0.2.2

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.
data/README.md CHANGED
@@ -1,311 +1,305 @@
1
- # elo_rankable
2
-
3
- [![Gem Version](https://badge.fury.io/rb/elo_rankable.svg?icon=si%3Arubygems)](https://badge.fury.io/rb/elo_rankable)
4
-
5
- A Ruby gem that adds Elo rating capabilities to any ActiveRecord model using a simple `has_elo_ranking` declaration. It stores ratings in a separate `EloRanking` model to keep your host model clean, and provides domain-style methods for updating rankings after matches.
6
-
7
- ## Features
8
-
9
- - ๐ŸŽฏ **Simple Integration**: Add Elo rankings to any ActiveRecord model with one line
10
- - ๐Ÿ† **Multiple Match Types**: Support for 1v1, draws, multiplayer (ranked), and winner-vs-all matches
11
- - โš™๏ธ **Configurable**: Customizable base rating and K-factor strategies
12
- - ๐Ÿ“Š **Leaderboards**: Built-in scopes for rankings and top players
13
- - ๐Ÿงน **Clean Design**: Ratings stored separately from your main models
14
- - ๐Ÿ”„ **Polymorphic**: Works with any ActiveRecord model (User, Player, Team, etc.)
15
-
16
- ## Installation
17
-
18
- Add this line to your application's Gemfile:
19
-
20
- ```ruby
21
- gem 'elo_rankable'
22
- ```
23
-
24
- And then execute:
25
-
26
- ```bash
27
- $ bundle install
28
- ```
29
-
30
- Or install it yourself as:
31
-
32
- ```bash
33
- $ gem install elo_rankable
34
- ```
35
-
36
- ## Setup
37
-
38
- ### 1. Generate and run the migration
39
-
40
- ```bash
41
- $ rails generate elo_rankable:install
42
- $ rails db:migrate
43
- ```
44
-
45
- ### 2. Add to your models
46
-
47
- ```ruby
48
- class Player < ApplicationRecord
49
- has_elo_ranking
50
- end
51
-
52
- class Team < ApplicationRecord
53
- has_elo_ranking
54
- end
55
- ```
56
-
57
- ## Usage
58
-
59
- ### Basic 1v1 Matches
60
-
61
- ```ruby
62
- alice = Player.create!(name: "Alice")
63
- bob = Player.create!(name: "Bob")
64
-
65
- # Both players start with the default rating (1200)
66
- alice.elo_rating # => 1200
67
- bob.elo_rating # => 1200
68
-
69
- # Record a match where Alice beats Bob
70
- alice.beat!(bob)
71
-
72
- alice.elo_rating # => 1216
73
- bob.elo_rating # => 1184
74
-
75
- # Alternative syntax
76
- bob.lost_to!(alice) # Same effect as alice.beat!(bob)
77
-
78
- # Record a draw
79
- alice.draw_with!(bob)
80
-
81
- alice.elo_rating # => 1200 (no change for equal ratings)
82
- bob.elo_rating # => 1200 (no change for equal ratings)
83
-
84
- # Example with different ratings
85
- charlie = Player.create!(name: "Charlie")
86
- charlie.elo_ranking.update!(rating: 1400) # Charlie is higher rated
87
-
88
- charlie.draw_with!(alice) # Draw between 1400 vs 1200
89
-
90
- charlie.elo_rating # => 1392 (lost 8 points - draw hurts higher rated player)
91
- alice.elo_rating # => 1208 (gained 8 points - draw helps lower rated player)
92
- ```
93
-
94
- ### Multiplayer Matches (Ranked)
95
-
96
- For tournaments or matches where players finish in ranked order:
97
-
98
- ```ruby
99
- players = [first_place, second_place, third_place, fourth_place]
100
-
101
- # Higher-indexed players are treated as having lost to lower-indexed ones
102
- EloRankable.record_multiplayer_match(players)
103
-
104
- # This is equivalent to:
105
- # first_place.beat!(second_place)
106
- # first_place.beat!(third_place)
107
- # first_place.beat!(fourth_place)
108
- # second_place.beat!(third_place)
109
- # second_place.beat!(fourth_place)
110
- # third_place.beat!(fourth_place)
111
- ```
112
-
113
- ### Winner vs All Matches
114
-
115
- For matches where one player/team beats everyone else, but the losers don't compete against each other:
116
-
117
- ```ruby
118
- winner = Player.find_by(name: "Champion")
119
- losers = [player1, player2, player3]
120
-
121
- EloRankable.record_winner_vs_all(winner, losers)
122
-
123
- # Winner gains rating by beating each loser individually
124
- # Losers only lose rating to the winner, not to each other
125
- ```
126
-
127
- ### Global Draw Recording
128
-
129
- ```ruby
130
- EloRankable.record_draw(player1, player2)
131
- ```
132
-
133
- ### Accessing Rating Information
134
-
135
- ```ruby
136
- player = Player.first
137
-
138
- player.elo_rating # Current Elo rating
139
- player.games_played # Number of games played
140
- player.elo_ranking # Access to the full EloRanking record
141
- ```
142
-
143
- ### Accessing K-Factor Values
144
-
145
- ```ruby
146
- # Get the K-factor for a specific rating
147
- EloRankable.config.k_factor_for(1500) # => 32
148
- EloRankable.config.k_factor_for(2200) # => 20
149
- ```
150
-
151
- ### Leaderboards and Scopes
152
-
153
- ```ruby
154
- # Get players ordered by rating (highest first)
155
- top_players = Player.by_elo_rating
156
-
157
- # Get top 10 players
158
- top_10 = Player.top_rated(10)
159
-
160
- # Access EloRanking records directly
161
- top_ratings = EloRankable::EloRanking.by_rating.limit(10)
162
- ```
163
-
164
- ## Configuration
165
-
166
- ### Base Rating
167
-
168
- ```ruby
169
- EloRankable.configure do |config|
170
- config.base_rating = 1500 # Default is 1200
171
- end
172
- ```
173
-
174
- ### K-Factor Strategy
175
-
176
- The K-factor determines how much ratings change after each match. You can use a constant value or a dynamic strategy based on rating:
177
-
178
- #### Constant K-Factor
179
-
180
- ```ruby
181
- EloRankable.configure do |config|
182
- config.k_factor_for = 32
183
- end
184
- ```
185
-
186
- #### Dynamic K-Factor (Default)
187
-
188
- ```ruby
189
- EloRankable.configure do |config|
190
- config.k_factor_for = ->(rating) do
191
- if rating > 2400
192
- 10 # Masters: smaller changes
193
- elsif rating > 2000
194
- 20 # Experts: medium changes
195
- else
196
- 32 # Beginners: larger changes
197
- end
198
- end
199
- end
200
- ```
201
-
202
- ## Method Reference
203
-
204
- ### Instance Methods (added by `has_elo_ranking`)
205
-
206
- | Method | Description |
207
- |--------|-------------|
208
- | `beat!(other)` | Record a win against another player |
209
- | `lost_to!(other)` | Record a loss to another player |
210
- | `draw_with!(other)` | Record a draw with another player |
211
- | `elo_beat!(other)` | Alias for `beat!` |
212
- | `elo_lost_to!(other)` | Alias for `lost_to!` |
213
- | `elo_draw_with!(other)` | Alias for `draw_with!` |
214
- | `elo_rating` | Current Elo rating |
215
- | `games_played` | Number of games played |
216
- | `elo_ranking` | Associated EloRanking record |
217
-
218
- ### Class Methods (added by `has_elo_ranking`)
219
-
220
- | Scope | Description |
221
- |-------|-------------|
222
- | `by_elo_rating` | Order by Elo rating (highest first) |
223
- | `top_rated(limit)` | Get top N players by rating |
224
-
225
- ### Module Methods
226
-
227
- | Method | Description |
228
- |--------|-------------|
229
- | `EloRankable.record_multiplayer_match(players)` | Record ranked multiplayer match |
230
- | `EloRankable.record_winner_vs_all(winner, losers)` | Record winner-takes-all match |
231
- | `EloRankable.record_draw(player1, player2)` | Record a draw |
232
-
233
- ## How Elo Rating Works
234
-
235
- The Elo rating system calculates expected outcomes based on rating differences and adjusts ratings based on actual results:
236
-
237
- - **Expected Score**: Higher-rated players are expected to win more often
238
- - **Rating Change**: Beating a higher-rated opponent gives more points than beating a lower-rated one
239
- - **K-Factor**: Controls how much ratings can change (higher K = more volatile)
240
-
241
- ### Example Calculation
242
-
243
- ```ruby
244
- # Alice (1200) vs Bob (1200) - equal ratings
245
- alice.beat!(bob)
246
- # Alice: 1200 + 16 = 1216 (gained 16 points)
247
- # Bob: 1200 - 16 = 1184 (lost 16 points)
248
-
249
- # Alice (1400) vs Charlie (1200) - Alice favored
250
- alice.beat!(charlie)
251
- # Alice: 1400 + 8 = 1408 (gained 8 points - expected to win)
252
- # Charlie: 1200 - 8 = 1192 (lost 8 points)
253
-
254
- # Charlie (1192) beats Alice (1408) - upset!
255
- charlie.beat!(alice)
256
- # Charlie: 1192 + 24 = 1216 (gained 24 points - major upset)
257
- # Alice: 1408 - 24 = 1384 (lost 24 points)
258
- ```
259
-
260
-
261
- ## Error Handling
262
-
263
- The gem provides comprehensive validation with specific error types:
264
-
265
- ### EloRankable::InvalidMatchError
266
- - Thrown when match requirements aren't met (e.g., less than 2 players)
267
- - Winner appears in losers list
268
-
269
- ### ArgumentError
270
- - Nil players/opponents
271
- - Duplicate players in arrays
272
- - Players that don't respond to `elo_ranking`
273
- - Playing against yourself or destroyed records
274
-
275
- ```ruby
276
- # Examples that will raise errors:
277
- alice.beat!(nil) # ArgumentError: Cannot play against nil
278
- alice.beat!(alice) # ArgumentError: Cannot play against yourself
279
- EloRankable.record_multiplayer_match([alice]) # InvalidMatchError: Need at least 2 players
280
- ```
281
-
282
-
283
- ## Database Schema
284
-
285
- The gem creates an `elo_rankings` table:
286
-
287
- ```ruby
288
- create_table :elo_rankings do |t|
289
- t.references :rankable, polymorphic: true, null: false, index: true
290
- t.integer :rating, null: false, default: 1200
291
- t.integer :games_played, null: false, default: 0
292
- t.timestamps
293
- end
294
-
295
- add_index :elo_rankings, :rating
296
- add_index :elo_rankings, [:rankable_type, :rankable_id], unique: true
297
- ```
298
-
299
- ## Development
300
-
301
- After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
302
-
303
- To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and the created tag, and push the `.gem` file to [rubygems.org](https://rubygems.org).
304
-
305
- ## Contributing
306
-
307
- Bug reports and pull requests are welcome on GitHub at https://github.com/aberen/elo_rankable.
308
-
309
- ## License
310
-
311
- The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
1
+ # elo_rankable
2
+
3
+ [![Gem Version](https://badge.fury.io/rb/elo_rankable.svg?icon=si%3Arubygems)](https://badge.fury.io/rb/elo_rankable)
4
+
5
+ A Ruby gem that adds Elo rating capabilities to any ActiveRecord model using a simple `has_elo_ranking` declaration. It stores ratings in a separate `EloRanking` model to keep your host model clean, and provides domain-style methods for updating rankings after matches.
6
+
7
+ ## Features
8
+
9
+ - ๐ŸŽฏ **Simple Integration**: Add Elo rankings to any ActiveRecord model with one line
10
+ - ๐Ÿ† **Multiple Match Types**: Support for 1v1, draws, multiplayer (ranked), and winner-vs-all matches
11
+ - โš™๏ธ **Configurable**: Customizable base rating and K-factor strategies
12
+ - ๐Ÿ“Š **Leaderboards**: Built-in scopes for rankings and top players
13
+ - ๐Ÿงน **Clean Design**: Ratings stored separately from your main models
14
+ - ๐Ÿ”„ **Polymorphic**: Works with any ActiveRecord model (User, Player, Team, etc.)
15
+
16
+ ## Installation
17
+
18
+ Add this line to your application's Gemfile:
19
+
20
+ ```ruby
21
+ gem 'elo_rankable'
22
+ ```
23
+
24
+ And then execute:
25
+
26
+ ```bash
27
+ $ bundle install
28
+ ```
29
+
30
+ Or install it yourself as:
31
+
32
+ ```bash
33
+ $ gem install elo_rankable
34
+ ```
35
+
36
+ ## Setup
37
+
38
+ ### 1. Generate and run the migration
39
+
40
+ ```bash
41
+ $ rails generate elo_rankable:install
42
+ $ rails db:migrate
43
+ ```
44
+
45
+ ### 2. Add to your models
46
+
47
+ ```ruby
48
+ class Player < ApplicationRecord
49
+ has_elo_ranking
50
+ end
51
+
52
+ class Team < ApplicationRecord
53
+ has_elo_ranking
54
+ end
55
+ ```
56
+
57
+ ## Usage
58
+
59
+ ### Basic 1v1 Matches
60
+
61
+ ```ruby
62
+ alice = Player.create!(name: "Alice")
63
+ bob = Player.create!(name: "Bob")
64
+
65
+ # Both players start with the default rating (1200)
66
+ alice.elo_rating # => 1200
67
+ bob.elo_rating # => 1200
68
+
69
+ # Record a match where Alice beats Bob
70
+ alice.beat!(bob)
71
+
72
+ alice.elo_rating # => 1216
73
+ bob.elo_rating # => 1184
74
+
75
+ # Alternative syntax
76
+ bob.lost_to!(alice) # Same effect as alice.beat!(bob)
77
+
78
+ # Record a draw
79
+ alice.draw_with!(bob)
80
+
81
+ alice.elo_rating # => 1200 (no change for equal ratings)
82
+ bob.elo_rating # => 1200 (no change for equal ratings)
83
+
84
+ # Example with different ratings
85
+ charlie = Player.create!(name: "Charlie")
86
+ charlie.elo_ranking.update!(rating: 1400) # Charlie is higher rated
87
+
88
+ charlie.draw_with!(alice) # Draw between 1400 vs 1200
89
+
90
+ charlie.elo_rating # => 1392 (lost 8 points - draw hurts higher rated player)
91
+ alice.elo_rating # => 1208 (gained 8 points - draw helps lower rated player)
92
+ ```
93
+
94
+ ### Multiplayer Matches (Ranked)
95
+
96
+ For tournaments or matches where players finish in ranked order:
97
+
98
+ ```ruby
99
+ players = [first_place, second_place, third_place, fourth_place]
100
+
101
+ # Higher-indexed players are treated as having lost to lower-indexed ones
102
+ EloRankable.record_multiplayer_match(players)
103
+
104
+ # This is equivalent to:
105
+ # first_place.beat!(second_place)
106
+ # first_place.beat!(third_place)
107
+ # first_place.beat!(fourth_place)
108
+ # second_place.beat!(third_place)
109
+ # second_place.beat!(fourth_place)
110
+ # third_place.beat!(fourth_place)
111
+ ```
112
+
113
+ ### Winner vs All Matches
114
+
115
+ For matches where one player/team beats everyone else, but the losers don't compete against each other:
116
+
117
+ ```ruby
118
+ winner = Player.find_by(name: "Champion")
119
+ losers = [player1, player2, player3]
120
+
121
+ EloRankable.record_winner_vs_all(winner, losers)
122
+
123
+ # Winner gains rating by beating each loser individually
124
+ # Losers only lose rating to the winner, not to each other
125
+ ```
126
+
127
+ ### Global Draw Recording
128
+
129
+ ```ruby
130
+ EloRankable.record_draw(player1, player2)
131
+ ```
132
+
133
+ ### Accessing Rating Information
134
+
135
+ ```ruby
136
+ player = Player.first
137
+
138
+ player.elo_rating # Current Elo rating
139
+ player.games_played # Number of games played
140
+ player.elo_ranking # Access to the full EloRanking record
141
+ ```
142
+
143
+ ### Accessing K-Factor Values
144
+
145
+ ```ruby
146
+ # Get the K-factor for a specific rating
147
+ EloRankable.config.k_factor_for(1500) # => 32
148
+ EloRankable.config.k_factor_for(2200) # => 20
149
+ ```
150
+
151
+ ### Leaderboards and Scopes
152
+
153
+ ```ruby
154
+ # Get players ordered by rating (highest first)
155
+ top_players = Player.by_elo_rating
156
+
157
+ # Get top 10 players
158
+ top_10 = Player.top_rated(10)
159
+
160
+ # Access EloRanking records directly
161
+ top_ratings = EloRankable::EloRanking.by_rating.limit(10)
162
+ ```
163
+
164
+ ## Configuration
165
+
166
+ ### Base Rating
167
+
168
+ ```ruby
169
+ EloRankable.configure do |config|
170
+ config.base_rating = 1500 # Default is 1200
171
+ end
172
+ ```
173
+
174
+ ### K-Factor Strategy
175
+
176
+ The K-factor determines how much ratings change after each match. You can use a constant value or a dynamic strategy based on rating:
177
+
178
+ #### Constant K-Factor
179
+
180
+ ```ruby
181
+ EloRankable.configure do |config|
182
+ config.k_factor_for = 32
183
+ end
184
+ ```
185
+
186
+ #### Dynamic K-Factor (Default)
187
+
188
+ ```ruby
189
+ EloRankable.configure do |config|
190
+ config.k_factor_for = ->(rating) do
191
+ if rating > 2400
192
+ 10 # Masters: smaller changes
193
+ elsif rating > 2000
194
+ 20 # Experts: medium changes
195
+ else
196
+ 32 # Beginners: larger changes
197
+ end
198
+ end
199
+ end
200
+ ```
201
+
202
+ ## Method Reference
203
+
204
+ ### Instance Methods (added by `has_elo_ranking`)
205
+
206
+ | Method | Description |
207
+ |--------|-------------|
208
+ | `beat!(other)` | Record a win against another player |
209
+ | `lost_to!(other)` | Record a loss to another player |
210
+ | `draw_with!(other)` | Record a draw with another player |
211
+ | `elo_beat!(other)` | Alias for `beat!` |
212
+ | `elo_lost_to!(other)` | Alias for `lost_to!` |
213
+ | `elo_draw_with!(other)` | Alias for `draw_with!` |
214
+ | `elo_rating` | Current Elo rating |
215
+ | `games_played` | Number of games played |
216
+ | `elo_ranking` | Associated EloRanking record |
217
+
218
+ ### Class Methods (added by `has_elo_ranking`)
219
+
220
+ | Scope | Description |
221
+ |-------|-------------|
222
+ | `by_elo_rating` | Order by Elo rating (highest first) |
223
+ | `top_rated(limit)` | Get top N players by rating |
224
+
225
+ ### Module Methods
226
+
227
+ | Method | Description |
228
+ |--------|-------------|
229
+ | `EloRankable.record_multiplayer_match(players)` | Record ranked multiplayer match |
230
+ | `EloRankable.record_winner_vs_all(winner, losers)` | Record winner-takes-all match |
231
+ | `EloRankable.record_draw(player1, player2)` | Record a draw |
232
+
233
+ ## How Elo Rating Works
234
+
235
+ The Elo rating system calculates expected outcomes based on rating differences and adjusts ratings based on actual results:
236
+
237
+ - **Expected Score**: Higher-rated players are expected to win more often
238
+ - **Rating Change**: Beating a higher-rated opponent gives more points than beating a lower-rated one
239
+ - **K-Factor**: Controls how much ratings can change (higher K = more volatile)
240
+
241
+ ### Example Calculation
242
+
243
+ ```ruby
244
+ # Alice (1200) vs Bob (1200) - equal ratings
245
+ alice.beat!(bob)
246
+ # Alice: 1200 + 16 = 1216 (gained 16 points)
247
+ # Bob: 1200 - 16 = 1184 (lost 16 points)
248
+
249
+ # Alice (1400) vs Charlie (1200) - Alice favored
250
+ alice.beat!(charlie)
251
+ # Alice: 1400 + 8 = 1408 (gained 8 points - expected to win)
252
+ # Charlie: 1200 - 8 = 1192 (lost 8 points)
253
+
254
+ # Charlie (1192) beats Alice (1408) - upset!
255
+ charlie.beat!(alice)
256
+ # Charlie: 1192 + 24 = 1216 (gained 24 points - major upset)
257
+ # Alice: 1408 - 24 = 1384 (lost 24 points)
258
+ ```
259
+
260
+
261
+ ## Error Handling
262
+
263
+ The gem provides comprehensive validation with specific error types:
264
+
265
+ ### InvalidMatchError
266
+ - Thrown when match requirements aren't met (e.g., less than 2 players)
267
+ - Winner appears in losers list
268
+
269
+ ### ArgumentError
270
+ - Nil players/opponents
271
+ - Duplicate players in arrays
272
+ - Players that don't respond to `elo_ranking`
273
+ - Playing against yourself or destroyed records
274
+
275
+ ```ruby
276
+ # Examples that will raise errors:
277
+ alice.beat!(nil) # ArgumentError: Cannot play against nil
278
+ alice.beat!(alice) # ArgumentError: Cannot play against yourself
279
+ EloRankable.record_multiplayer_match([alice]) # InvalidMatchError: Need at least 2 players
280
+ ```
281
+
282
+
283
+ ## Database Schema
284
+
285
+ The gem creates an `elo_rankings` table:
286
+
287
+ ```ruby
288
+ create_table :elo_rankings do |t|
289
+ t.references :rankable, polymorphic: true, null: false, index: true
290
+ t.integer :rating, null: false, default: 1200
291
+ t.integer :games_played, null: false, default: 0
292
+ t.timestamps
293
+ end
294
+
295
+ add_index :elo_rankings, :rating
296
+ add_index :elo_rankings, [:rankable_type, :rankable_id], unique: true
297
+ ```
298
+
299
+ ## Contributing
300
+
301
+ Bug reports and pull requests are welcome on GitHub at https://github.com/aberen/elo_rankable.
302
+
303
+ ## License
304
+
305
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
data/Rakefile CHANGED
@@ -1,12 +1,12 @@
1
- # frozen_string_literal: true
2
-
3
- require 'bundler/gem_tasks'
4
- require 'rspec/core/rake_task'
5
-
6
- RSpec::Core::RakeTask.new(:spec)
7
-
8
- require 'rubocop/rake_task'
9
-
10
- RuboCop::RakeTask.new
11
-
12
- task default: %i[spec rubocop]
1
+ # frozen_string_literal: true
2
+
3
+ require 'bundler/gem_tasks'
4
+ require 'rspec/core/rake_task'
5
+
6
+ RSpec::Core::RakeTask.new(:spec)
7
+
8
+ require 'rubocop/rake_task'
9
+
10
+ RuboCop::RakeTask.new
11
+
12
+ task default: %i[spec rubocop]