elo_rankable 0.2.0 โ 0.2.1
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 +4 -4
- data/.rspec +3 -3
- data/.rubocop.yml +37 -37
- data/CHANGELOG.md +67 -63
- data/LICENSE.txt +21 -21
- data/README.md +305 -311
- data/Rakefile +12 -12
- data/elo_rankable.gemspec +36 -36
- data/lib/elo_rankable/calculator.rb +67 -67
- data/lib/elo_rankable/configuration.rb +42 -42
- data/lib/elo_rankable/elo_ranking.rb +23 -23
- data/lib/elo_rankable/has_elo_ranking.rb +71 -71
- data/lib/elo_rankable/version.rb +5 -5
- data/lib/elo_rankable.rb +96 -96
- data/lib/generators/elo_rankable/install/install_generator.rb +21 -21
- data/lib/generators/elo_rankable/install/templates/create_elo_rankings.rb +16 -16
- metadata +5 -5
data/README.md
CHANGED
|
@@ -1,311 +1,305 @@
|
|
|
1
|
-
# elo_rankable
|
|
2
|
-
|
|
3
|
-
[](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
|
-
###
|
|
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
|
-
##
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
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
|
+
[](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]
|