elo_rankable 0.1.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.
- checksums.yaml +7 -0
- data/.rspec +3 -0
- data/.rubocop.yml +35 -0
- data/CHANGELOG.md +59 -0
- data/LICENSE.txt +21 -0
- data/README.md +309 -0
- data/Rakefile +12 -0
- data/elo_rankable.gemspec +36 -0
- data/lib/elo_rankable/calculator.rb +67 -0
- data/lib/elo_rankable/configuration.rb +42 -0
- data/lib/elo_rankable/elo_ranking.rb +23 -0
- data/lib/elo_rankable/has_elo_ranking.rb +73 -0
- data/lib/elo_rankable/version.rb +5 -0
- data/lib/elo_rankable.rb +96 -0
- data/lib/generators/elo_rankable/install/install_generator.rb +21 -0
- data/lib/generators/elo_rankable/install/templates/create_elo_rankings.rb +16 -0
- metadata +102 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: ef2f0385ffa82f1a5e8b5d225e0e76252685f622f1cfe80f32894d76de088a33
|
|
4
|
+
data.tar.gz: de145551fe657a328ef79b41e67493a580ee6b1a6b266b775443b231b243173d
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: ba1215784c8f619cf9a1094478faa86bc5fb18050e8e5a1b0e790db5d30e8f552416b1719579c5815452945e926e7dc1df8c498e0035f97193c70afa3f2aeb78
|
|
7
|
+
data.tar.gz: 4ec3e6164342ad4854da73b420bed52fa210e572e9de9b002fda7014f1e239ef30b842e18ce9f545b4313582cc5a8d201ad42ca20b4022722b0ddb96531d17a1
|
data/.rspec
ADDED
data/.rubocop.yml
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
AllCops:
|
|
2
|
+
NewCops: enable
|
|
3
|
+
SuggestExtensions: false
|
|
4
|
+
TargetRubyVersion: 2.7
|
|
5
|
+
|
|
6
|
+
# Disable some overly strict metrics for gem development
|
|
7
|
+
Metrics/AbcSize:
|
|
8
|
+
Max: 30
|
|
9
|
+
|
|
10
|
+
Metrics/MethodLength:
|
|
11
|
+
Max: 15
|
|
12
|
+
|
|
13
|
+
Metrics/CyclomaticComplexity:
|
|
14
|
+
Max: 10
|
|
15
|
+
|
|
16
|
+
Metrics/PerceivedComplexity:
|
|
17
|
+
Max: 10
|
|
18
|
+
|
|
19
|
+
Metrics/BlockLength:
|
|
20
|
+
Exclude:
|
|
21
|
+
- 'spec/**/*'
|
|
22
|
+
- '*.gemspec'
|
|
23
|
+
|
|
24
|
+
# Allow longer variable names with numbers for test clarity
|
|
25
|
+
Naming/VariableNumber:
|
|
26
|
+
EnforcedStyle: normalcase
|
|
27
|
+
|
|
28
|
+
# Allow predicate method names like has_elo_ranking for ActiveRecord-style methods
|
|
29
|
+
Naming/PredicatePrefix:
|
|
30
|
+
AllowedMethods:
|
|
31
|
+
- has_elo_ranking
|
|
32
|
+
|
|
33
|
+
# Documentation is not always necessary for internal classes
|
|
34
|
+
Style/Documentation:
|
|
35
|
+
Enabled: false
|
data/CHANGELOG.md
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project will be documented in this file.
|
|
4
|
+
|
|
5
|
+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
6
|
+
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
|
+
|
|
8
|
+
## [0.1.2] - 2025-08-08
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
- Comprehensive input validation for all public methods
|
|
12
|
+
- Early validation in `record_multiplayer_match` and `record_winner_vs_all` methods
|
|
13
|
+
- Validation for `record_draw` method
|
|
14
|
+
- Defensive validation in Calculator methods
|
|
15
|
+
- Detection of duplicate players in multiplayer matches
|
|
16
|
+
- Validation for nil values in player arrays
|
|
17
|
+
- Validation for non-rankable objects
|
|
18
|
+
- Validation for destroyed/deleted players
|
|
19
|
+
|
|
20
|
+
### Fixed
|
|
21
|
+
- **BREAKING**: Improved error handling with descriptive ArgumentError exceptions instead of NoMethodError or database constraint violations
|
|
22
|
+
- Fixed validation gaps where errors would bubble up from Calculator instead of being caught early
|
|
23
|
+
- Fixed race conditions in validation logic by caching `elo_ranking` calls
|
|
24
|
+
- Improved validation in `validate_opponent!` method to properly handle destroyed records
|
|
25
|
+
|
|
26
|
+
### Changed
|
|
27
|
+
- **BREAKING**: `record_multiplayer_match` now raises `ArgumentError` for duplicate players instead of allowing self-matches
|
|
28
|
+
- **BREAKING**: `record_winner_vs_all` now validates winner and losers arrays upfront
|
|
29
|
+
- **BREAKING**: `record_draw` now validates both players upfront
|
|
30
|
+
- All validation errors now provide clear, descriptive error messages
|
|
31
|
+
|
|
32
|
+
## [0.1.1] - 2025-08-07
|
|
33
|
+
|
|
34
|
+
### Fixed
|
|
35
|
+
- Added proper input validation to `beat!`, `lost_to!`, and `draw_with!` methods
|
|
36
|
+
- Fixed SQLite3 gem version compatibility with ActiveRecord 8.0
|
|
37
|
+
|
|
38
|
+
### Added
|
|
39
|
+
- Comprehensive edge case testing for high ratings, negative ratings, and invalid inputs
|
|
40
|
+
- Performance tests for leaderboard queries and association loading
|
|
41
|
+
- Input validation with descriptive error messages for nil players and self-matches
|
|
42
|
+
|
|
43
|
+
### Changed
|
|
44
|
+
- Improved error handling with ArgumentError exceptions for invalid match scenarios
|
|
45
|
+
- Enhanced test suite with better coverage of edge cases and error conditions
|
|
46
|
+
|
|
47
|
+
## [0.1.0] - 2025-08-06
|
|
48
|
+
|
|
49
|
+
### Added
|
|
50
|
+
- Initial release
|
|
51
|
+
- `has_elo_ranking` macro for ActiveRecord models
|
|
52
|
+
- Support for 1v1 matches with `beat!`, `lost_to!`, and `draw_with!` methods
|
|
53
|
+
- Multiplayer ranked match support via `EloRankable.record_multiplayer_match`
|
|
54
|
+
- Winner-vs-all match support via `EloRankable.record_winner_vs_all`
|
|
55
|
+
- Configurable base rating and K-factor strategies
|
|
56
|
+
- Leaderboard scopes (`by_elo_rating`, `top_rated`)
|
|
57
|
+
- Polymorphic EloRanking model for clean separation
|
|
58
|
+
- Rails generator for database migration
|
|
59
|
+
- Comprehensive test suite
|
data/LICENSE.txt
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
The MIT License (MIT)
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 EloRankable Contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
|
13
|
+
all copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
21
|
+
THE SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,309 @@
|
|
|
1
|
+
# elo_rankable
|
|
2
|
+
|
|
3
|
+
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.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- 🎯 **Simple Integration**: Add Elo rankings to any ActiveRecord model with one line
|
|
8
|
+
- 🏆 **Multiple Match Types**: Support for 1v1, draws, multiplayer (ranked), and winner-vs-all matches
|
|
9
|
+
- ⚙️ **Configurable**: Customizable base rating and K-factor strategies
|
|
10
|
+
- 📊 **Leaderboards**: Built-in scopes for rankings and top players
|
|
11
|
+
- 🧹 **Clean Design**: Ratings stored separately from your main models
|
|
12
|
+
- 🔄 **Polymorphic**: Works with any ActiveRecord model (User, Player, Team, etc.)
|
|
13
|
+
|
|
14
|
+
## Installation
|
|
15
|
+
|
|
16
|
+
Add this line to your application's Gemfile:
|
|
17
|
+
|
|
18
|
+
```ruby
|
|
19
|
+
gem 'elo_rankable'
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
And then execute:
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
$ bundle install
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
Or install it yourself as:
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
$ gem install elo_rankable
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## Setup
|
|
35
|
+
|
|
36
|
+
### 1. Generate and run the migration
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
$ rails generate elo_rankable:install
|
|
40
|
+
$ rails db:migrate
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
### 2. Add to your models
|
|
44
|
+
|
|
45
|
+
```ruby
|
|
46
|
+
class Player < ApplicationRecord
|
|
47
|
+
has_elo_ranking
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
class Team < ApplicationRecord
|
|
51
|
+
has_elo_ranking
|
|
52
|
+
end
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
## Usage
|
|
56
|
+
|
|
57
|
+
### Basic 1v1 Matches
|
|
58
|
+
|
|
59
|
+
```ruby
|
|
60
|
+
alice = Player.create!(name: "Alice")
|
|
61
|
+
bob = Player.create!(name: "Bob")
|
|
62
|
+
|
|
63
|
+
# Both players start with the default rating (1200)
|
|
64
|
+
alice.elo_rating # => 1200
|
|
65
|
+
bob.elo_rating # => 1200
|
|
66
|
+
|
|
67
|
+
# Record a match where Alice beats Bob
|
|
68
|
+
alice.beat!(bob)
|
|
69
|
+
|
|
70
|
+
alice.elo_rating # => 1216
|
|
71
|
+
bob.elo_rating # => 1184
|
|
72
|
+
|
|
73
|
+
# Alternative syntax
|
|
74
|
+
bob.lost_to!(alice) # Same effect as alice.beat!(bob)
|
|
75
|
+
|
|
76
|
+
# Record a draw
|
|
77
|
+
alice.draw_with!(bob)
|
|
78
|
+
|
|
79
|
+
alice.elo_rating # => 1200 (no change for equal ratings)
|
|
80
|
+
bob.elo_rating # => 1200 (no change for equal ratings)
|
|
81
|
+
|
|
82
|
+
# Example with different ratings
|
|
83
|
+
charlie = Player.create!(name: "Charlie")
|
|
84
|
+
charlie.elo_ranking.update!(rating: 1400) # Charlie is higher rated
|
|
85
|
+
|
|
86
|
+
charlie.draw_with!(alice) # Draw between 1400 vs 1200
|
|
87
|
+
|
|
88
|
+
charlie.elo_rating # => 1392 (lost 8 points - draw hurts higher rated player)
|
|
89
|
+
alice.elo_rating # => 1208 (gained 8 points - draw helps lower rated player)
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
### Multiplayer Matches (Ranked)
|
|
93
|
+
|
|
94
|
+
For tournaments or matches where players finish in ranked order:
|
|
95
|
+
|
|
96
|
+
```ruby
|
|
97
|
+
players = [first_place, second_place, third_place, fourth_place]
|
|
98
|
+
|
|
99
|
+
# Higher-indexed players are treated as having lost to lower-indexed ones
|
|
100
|
+
EloRankable.record_multiplayer_match(players)
|
|
101
|
+
|
|
102
|
+
# This is equivalent to:
|
|
103
|
+
# first_place.beat!(second_place)
|
|
104
|
+
# first_place.beat!(third_place)
|
|
105
|
+
# first_place.beat!(fourth_place)
|
|
106
|
+
# second_place.beat!(third_place)
|
|
107
|
+
# second_place.beat!(fourth_place)
|
|
108
|
+
# third_place.beat!(fourth_place)
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
### Winner vs All Matches
|
|
112
|
+
|
|
113
|
+
For matches where one player/team beats everyone else, but the losers don't compete against each other:
|
|
114
|
+
|
|
115
|
+
```ruby
|
|
116
|
+
winner = Player.find_by(name: "Champion")
|
|
117
|
+
losers = [player1, player2, player3]
|
|
118
|
+
|
|
119
|
+
EloRankable.record_winner_vs_all(winner, losers)
|
|
120
|
+
|
|
121
|
+
# Winner gains rating by beating each loser individually
|
|
122
|
+
# Losers only lose rating to the winner, not to each other
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
### Global Draw Recording
|
|
126
|
+
|
|
127
|
+
```ruby
|
|
128
|
+
EloRankable.record_draw(player1, player2)
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
### Accessing Rating Information
|
|
132
|
+
|
|
133
|
+
```ruby
|
|
134
|
+
player = Player.first
|
|
135
|
+
|
|
136
|
+
player.elo_rating # Current Elo rating
|
|
137
|
+
player.games_played # Number of games played
|
|
138
|
+
player.elo_ranking # Access to the full EloRanking record
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
### Accessing K-Factor Values
|
|
142
|
+
|
|
143
|
+
```ruby
|
|
144
|
+
# Get the K-factor for a specific rating
|
|
145
|
+
EloRankable.config.k_factor_for(1500) # => 32
|
|
146
|
+
EloRankable.config.k_factor_for(2200) # => 20
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
### Leaderboards and Scopes
|
|
150
|
+
|
|
151
|
+
```ruby
|
|
152
|
+
# Get players ordered by rating (highest first)
|
|
153
|
+
top_players = Player.by_elo_rating
|
|
154
|
+
|
|
155
|
+
# Get top 10 players
|
|
156
|
+
top_10 = Player.top_rated(10)
|
|
157
|
+
|
|
158
|
+
# Access EloRanking records directly
|
|
159
|
+
top_ratings = EloRankable::EloRanking.by_rating.limit(10)
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
## Configuration
|
|
163
|
+
|
|
164
|
+
### Base Rating
|
|
165
|
+
|
|
166
|
+
```ruby
|
|
167
|
+
EloRankable.configure do |config|
|
|
168
|
+
config.base_rating = 1500 # Default is 1200
|
|
169
|
+
end
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
### K-Factor Strategy
|
|
173
|
+
|
|
174
|
+
The K-factor determines how much ratings change after each match. You can use a constant value or a dynamic strategy based on rating:
|
|
175
|
+
|
|
176
|
+
#### Constant K-Factor
|
|
177
|
+
|
|
178
|
+
```ruby
|
|
179
|
+
EloRankable.configure do |config|
|
|
180
|
+
config.k_factor_for = 32
|
|
181
|
+
end
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
#### Dynamic K-Factor (Default)
|
|
185
|
+
|
|
186
|
+
```ruby
|
|
187
|
+
EloRankable.configure do |config|
|
|
188
|
+
config.k_factor_for = ->(rating) do
|
|
189
|
+
if rating > 2400
|
|
190
|
+
10 # Masters: smaller changes
|
|
191
|
+
elsif rating > 2000
|
|
192
|
+
20 # Experts: medium changes
|
|
193
|
+
else
|
|
194
|
+
32 # Beginners: larger changes
|
|
195
|
+
end
|
|
196
|
+
end
|
|
197
|
+
end
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
## Method Reference
|
|
201
|
+
|
|
202
|
+
### Instance Methods (added by `has_elo_ranking`)
|
|
203
|
+
|
|
204
|
+
| Method | Description |
|
|
205
|
+
|--------|-------------|
|
|
206
|
+
| `beat!(other)` | Record a win against another player |
|
|
207
|
+
| `lost_to!(other)` | Record a loss to another player |
|
|
208
|
+
| `draw_with!(other)` | Record a draw with another player |
|
|
209
|
+
| `elo_beat!(other)` | Alias for `beat!` |
|
|
210
|
+
| `elo_lost_to!(other)` | Alias for `lost_to!` |
|
|
211
|
+
| `elo_draw_with!(other)` | Alias for `draw_with!` |
|
|
212
|
+
| `elo_rating` | Current Elo rating |
|
|
213
|
+
| `games_played` | Number of games played |
|
|
214
|
+
| `elo_ranking` | Associated EloRanking record |
|
|
215
|
+
|
|
216
|
+
### Class Methods (added by `has_elo_ranking`)
|
|
217
|
+
|
|
218
|
+
| Scope | Description |
|
|
219
|
+
|-------|-------------|
|
|
220
|
+
| `by_elo_rating` | Order by Elo rating (highest first) |
|
|
221
|
+
| `top_rated(limit)` | Get top N players by rating |
|
|
222
|
+
|
|
223
|
+
### Module Methods
|
|
224
|
+
|
|
225
|
+
| Method | Description |
|
|
226
|
+
|--------|-------------|
|
|
227
|
+
| `EloRankable.record_multiplayer_match(players)` | Record ranked multiplayer match |
|
|
228
|
+
| `EloRankable.record_winner_vs_all(winner, losers)` | Record winner-takes-all match |
|
|
229
|
+
| `EloRankable.record_draw(player1, player2)` | Record a draw |
|
|
230
|
+
|
|
231
|
+
## How Elo Rating Works
|
|
232
|
+
|
|
233
|
+
The Elo rating system calculates expected outcomes based on rating differences and adjusts ratings based on actual results:
|
|
234
|
+
|
|
235
|
+
- **Expected Score**: Higher-rated players are expected to win more often
|
|
236
|
+
- **Rating Change**: Beating a higher-rated opponent gives more points than beating a lower-rated one
|
|
237
|
+
- **K-Factor**: Controls how much ratings can change (higher K = more volatile)
|
|
238
|
+
|
|
239
|
+
### Example Calculation
|
|
240
|
+
|
|
241
|
+
```ruby
|
|
242
|
+
# Alice (1200) vs Bob (1200) - equal ratings
|
|
243
|
+
alice.beat!(bob)
|
|
244
|
+
# Alice: 1200 + 16 = 1216 (gained 16 points)
|
|
245
|
+
# Bob: 1200 - 16 = 1184 (lost 16 points)
|
|
246
|
+
|
|
247
|
+
# Alice (1400) vs Charlie (1200) - Alice favored
|
|
248
|
+
alice.beat!(charlie)
|
|
249
|
+
# Alice: 1400 + 8 = 1408 (gained 8 points - expected to win)
|
|
250
|
+
# Charlie: 1200 - 8 = 1192 (lost 8 points)
|
|
251
|
+
|
|
252
|
+
# Charlie (1192) beats Alice (1408) - upset!
|
|
253
|
+
charlie.beat!(alice)
|
|
254
|
+
# Charlie: 1192 + 24 = 1216 (gained 24 points - major upset)
|
|
255
|
+
# Alice: 1408 - 24 = 1384 (lost 24 points)
|
|
256
|
+
```
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
## Error Handling
|
|
260
|
+
|
|
261
|
+
The gem provides comprehensive validation with specific error types:
|
|
262
|
+
|
|
263
|
+
### EloRankable::InvalidMatchError
|
|
264
|
+
- Thrown when match requirements aren't met (e.g., less than 2 players)
|
|
265
|
+
- Winner appears in losers list
|
|
266
|
+
|
|
267
|
+
### ArgumentError
|
|
268
|
+
- Nil players/opponents
|
|
269
|
+
- Duplicate players in arrays
|
|
270
|
+
- Players that don't respond to `elo_ranking`
|
|
271
|
+
- Playing against yourself or destroyed records
|
|
272
|
+
|
|
273
|
+
```ruby
|
|
274
|
+
# Examples that will raise errors:
|
|
275
|
+
alice.beat!(nil) # ArgumentError: Cannot play against nil
|
|
276
|
+
alice.beat!(alice) # ArgumentError: Cannot play against yourself
|
|
277
|
+
EloRankable.record_multiplayer_match([alice]) # InvalidMatchError: Need at least 2 players
|
|
278
|
+
```
|
|
279
|
+
|
|
280
|
+
|
|
281
|
+
## Database Schema
|
|
282
|
+
|
|
283
|
+
The gem creates an `elo_rankings` table:
|
|
284
|
+
|
|
285
|
+
```ruby
|
|
286
|
+
create_table :elo_rankings do |t|
|
|
287
|
+
t.references :rankable, polymorphic: true, null: false, index: true
|
|
288
|
+
t.integer :rating, null: false, default: 1200
|
|
289
|
+
t.integer :games_played, null: false, default: 0
|
|
290
|
+
t.timestamps
|
|
291
|
+
end
|
|
292
|
+
|
|
293
|
+
add_index :elo_rankings, :rating
|
|
294
|
+
add_index :elo_rankings, [:rankable_type, :rankable_id], unique: true
|
|
295
|
+
```
|
|
296
|
+
|
|
297
|
+
## Development
|
|
298
|
+
|
|
299
|
+
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.
|
|
300
|
+
|
|
301
|
+
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).
|
|
302
|
+
|
|
303
|
+
## Contributing
|
|
304
|
+
|
|
305
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/aberen/elo_rankable.
|
|
306
|
+
|
|
307
|
+
## License
|
|
308
|
+
|
|
309
|
+
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
data/Rakefile
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'lib/elo_rankable/version'
|
|
4
|
+
|
|
5
|
+
Gem::Specification.new do |spec|
|
|
6
|
+
spec.name = 'elo_rankable'
|
|
7
|
+
spec.version = EloRankable::VERSION
|
|
8
|
+
spec.authors = ['Aberen']
|
|
9
|
+
spec.email = ['nijoergensen@gmail.com']
|
|
10
|
+
|
|
11
|
+
spec.summary = 'Add Elo rating capabilities to any ActiveRecord model'
|
|
12
|
+
spec.description = 'Adds Elo rating to any ActiveRecord model via has_elo_ranking.'
|
|
13
|
+
spec.homepage = 'https://github.com/Aberen/elo_rankable'
|
|
14
|
+
spec.license = 'MIT'
|
|
15
|
+
spec.required_ruby_version = '>= 2.7.0'
|
|
16
|
+
|
|
17
|
+
spec.metadata['homepage_uri'] = spec.homepage
|
|
18
|
+
spec.metadata['source_code_uri'] = spec.homepage
|
|
19
|
+
spec.metadata['changelog_uri'] = "#{spec.homepage}/blob/main/CHANGELOG.md"
|
|
20
|
+
spec.metadata['rubygems_mfa_required'] = 'true'
|
|
21
|
+
|
|
22
|
+
# Specify which files should be added to the gem when it is released.
|
|
23
|
+
spec.files = Dir.chdir(__dir__) do
|
|
24
|
+
`git ls-files -z`.split("\x0").reject do |f|
|
|
25
|
+
(File.expand_path(f) == __FILE__) ||
|
|
26
|
+
f.start_with?(*%w[bin/ test/ spec/ features/ .git .circleci appveyor Gemfile])
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
spec.bindir = 'exe'
|
|
30
|
+
spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) }
|
|
31
|
+
spec.require_paths = ['lib']
|
|
32
|
+
|
|
33
|
+
# Dependencies
|
|
34
|
+
spec.add_dependency 'activerecord', '>= 6.0', '< 8.0'
|
|
35
|
+
spec.add_dependency 'activesupport', '>= 6.0', '< 8.0'
|
|
36
|
+
end
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module EloRankable
|
|
4
|
+
class Calculator
|
|
5
|
+
class << self
|
|
6
|
+
# Calculate expected score for player A against player B
|
|
7
|
+
def expected_score(rating_a, rating_b)
|
|
8
|
+
1.0 / (1.0 + (10.0**((rating_b - rating_a) / 400.0)))
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
# Update ratings after a match where player1 beats player2
|
|
12
|
+
def update_ratings_for_win(winner, loser)
|
|
13
|
+
winner_rating = winner.elo_ranking
|
|
14
|
+
loser_rating = loser.elo_ranking
|
|
15
|
+
|
|
16
|
+
# Defensive validation - these should be caught earlier but just in case
|
|
17
|
+
raise ArgumentError, "Winner's elo_ranking is nil" if winner_rating.nil?
|
|
18
|
+
raise ArgumentError, "Loser's elo_ranking is nil" if loser_rating.nil?
|
|
19
|
+
|
|
20
|
+
winner_expected = expected_score(winner_rating.rating, loser_rating.rating)
|
|
21
|
+
loser_expected = expected_score(loser_rating.rating, winner_rating.rating)
|
|
22
|
+
|
|
23
|
+
winner_k = winner_rating.k_factor
|
|
24
|
+
loser_k = loser_rating.k_factor
|
|
25
|
+
|
|
26
|
+
# Winner gets 1 point, loser gets 0 points
|
|
27
|
+
winner_new_rating = winner_rating.rating + (winner_k * (1 - winner_expected))
|
|
28
|
+
loser_new_rating = loser_rating.rating + (loser_k * (0 - loser_expected))
|
|
29
|
+
|
|
30
|
+
update_ranking(winner_rating, winner_new_rating)
|
|
31
|
+
update_ranking(loser_rating, loser_new_rating)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# Update ratings after a draw
|
|
35
|
+
def update_ratings_for_draw(player1, player2)
|
|
36
|
+
player1_rating = player1.elo_ranking
|
|
37
|
+
player2_rating = player2.elo_ranking
|
|
38
|
+
|
|
39
|
+
# Defensive validation - these should be caught earlier but just in case
|
|
40
|
+
raise ArgumentError, "Player1's elo_ranking is nil" if player1_rating.nil?
|
|
41
|
+
raise ArgumentError, "Player2's elo_ranking is nil" if player2_rating.nil?
|
|
42
|
+
|
|
43
|
+
player1_expected = expected_score(player1_rating.rating, player2_rating.rating)
|
|
44
|
+
player2_expected = expected_score(player2_rating.rating, player1_rating.rating)
|
|
45
|
+
|
|
46
|
+
player1_k = player1_rating.k_factor
|
|
47
|
+
player2_k = player2_rating.k_factor
|
|
48
|
+
|
|
49
|
+
# Both players get 0.5 points in a draw
|
|
50
|
+
player1_new_rating = player1_rating.rating + (player1_k * (0.5 - player1_expected))
|
|
51
|
+
player2_new_rating = player2_rating.rating + (player2_k * (0.5 - player2_expected))
|
|
52
|
+
|
|
53
|
+
update_ranking(player1_rating, player1_new_rating)
|
|
54
|
+
update_ranking(player2_rating, player2_new_rating)
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
private
|
|
58
|
+
|
|
59
|
+
def update_ranking(elo_ranking, new_rating)
|
|
60
|
+
elo_ranking.update!(
|
|
61
|
+
rating: new_rating.round,
|
|
62
|
+
games_played: elo_ranking.games_played + 1
|
|
63
|
+
)
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module EloRankable
|
|
4
|
+
class Configuration
|
|
5
|
+
attr_accessor :base_rating
|
|
6
|
+
attr_reader :k_factor_strategy
|
|
7
|
+
|
|
8
|
+
def initialize
|
|
9
|
+
@base_rating = 1200
|
|
10
|
+
@k_factor_strategy = default_k_factor_strategy
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def k_factor_for=(strategy)
|
|
14
|
+
@k_factor_strategy = strategy
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def k_factor_for(rating)
|
|
18
|
+
case @k_factor_strategy
|
|
19
|
+
when Proc
|
|
20
|
+
@k_factor_strategy.call(rating)
|
|
21
|
+
when Numeric
|
|
22
|
+
@k_factor_strategy
|
|
23
|
+
else
|
|
24
|
+
raise ArgumentError, 'K-factor strategy must be a Proc or Numeric'
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
private
|
|
29
|
+
|
|
30
|
+
def default_k_factor_strategy
|
|
31
|
+
lambda do |rating|
|
|
32
|
+
if rating > 2400
|
|
33
|
+
10
|
|
34
|
+
elsif rating > 2000
|
|
35
|
+
20
|
|
36
|
+
else
|
|
37
|
+
32
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module EloRankable
|
|
4
|
+
class EloRanking < ActiveRecord::Base
|
|
5
|
+
belongs_to :rankable, polymorphic: true
|
|
6
|
+
|
|
7
|
+
validates :rating, presence: true, numericality: { greater_than: 0 }
|
|
8
|
+
validates :games_played, presence: true, numericality: { greater_than_or_equal_to: 0 }
|
|
9
|
+
|
|
10
|
+
scope :by_rating, -> { order(rating: :desc) }
|
|
11
|
+
scope :top, ->(limit = 10) { by_rating.limit(limit) }
|
|
12
|
+
|
|
13
|
+
def initialize(attributes = nil)
|
|
14
|
+
super
|
|
15
|
+
self.rating ||= EloRankable.config.base_rating
|
|
16
|
+
self.games_played ||= 0
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def k_factor
|
|
20
|
+
EloRankable.config.k_factor_for(rating)
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module EloRankable
|
|
4
|
+
module HasEloRanking
|
|
5
|
+
def has_elo_ranking
|
|
6
|
+
# Set up the polymorphic association
|
|
7
|
+
has_one :elo_ranking, as: :rankable, class_name: 'EloRankable::EloRanking', dependent: :destroy
|
|
8
|
+
|
|
9
|
+
# Include instance methods
|
|
10
|
+
include InstanceMethods
|
|
11
|
+
|
|
12
|
+
# Add scopes for leaderboards
|
|
13
|
+
scope :by_elo_rating, -> { joins(:elo_ranking).order('elo_rankings.rating DESC') }
|
|
14
|
+
scope :top_rated, ->(limit = 10) { by_elo_rating.limit(limit) }
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
module InstanceMethods
|
|
18
|
+
def elo_ranking
|
|
19
|
+
super || create_elo_ranking!(
|
|
20
|
+
rating: EloRankable.config.base_rating,
|
|
21
|
+
games_played: 0
|
|
22
|
+
)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def elo_rating
|
|
26
|
+
elo_ranking.rating
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def games_played
|
|
30
|
+
elo_ranking.games_played
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# Domain-style DSL methods
|
|
34
|
+
def beat!(other_player)
|
|
35
|
+
validate_opponent!(other_player)
|
|
36
|
+
EloRankable::Calculator.update_ratings_for_win(self, other_player)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def lost_to!(other_player)
|
|
40
|
+
validate_opponent!(other_player)
|
|
41
|
+
EloRankable::Calculator.update_ratings_for_win(other_player, self)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def draw_with!(other_player)
|
|
45
|
+
validate_opponent!(other_player)
|
|
46
|
+
EloRankable::Calculator.update_ratings_for_draw(self, other_player)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# Aliases for clarity
|
|
50
|
+
alias elo_beat! beat!
|
|
51
|
+
alias elo_lost_to! lost_to!
|
|
52
|
+
alias elo_draw_with! draw_with!
|
|
53
|
+
|
|
54
|
+
private
|
|
55
|
+
|
|
56
|
+
def validate_opponent!(other_player)
|
|
57
|
+
raise ArgumentError, 'Cannot play against nil' if other_player.nil?
|
|
58
|
+
raise ArgumentError, 'Cannot play against yourself' if other_player == self
|
|
59
|
+
raise ArgumentError, 'Opponent must respond to elo_ranking' unless other_player.respond_to?(:elo_ranking)
|
|
60
|
+
|
|
61
|
+
# Check if the opponent is destroyed/deleted
|
|
62
|
+
if other_player.respond_to?(:destroyed?) && other_player.destroyed?
|
|
63
|
+
raise ArgumentError, 'Cannot play against a destroyed record'
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# Get the elo_ranking once and validate it
|
|
67
|
+
opponent_ranking = other_player.elo_ranking
|
|
68
|
+
raise ArgumentError, "Opponent's elo_ranking is not initialized" if opponent_ranking.nil?
|
|
69
|
+
raise ArgumentError, "Opponent's elo_ranking is not saved" unless opponent_ranking.persisted?
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
end
|
data/lib/elo_rankable.rb
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'active_record'
|
|
4
|
+
require 'active_support'
|
|
5
|
+
|
|
6
|
+
require_relative 'elo_rankable/version'
|
|
7
|
+
require_relative 'elo_rankable/configuration'
|
|
8
|
+
require_relative 'elo_rankable/elo_ranking'
|
|
9
|
+
require_relative 'elo_rankable/calculator'
|
|
10
|
+
require_relative 'elo_rankable/has_elo_ranking'
|
|
11
|
+
|
|
12
|
+
# EloRankable provides methods for recording Elo-based ranking results for multiplayer matches,
|
|
13
|
+
# winner-vs-all matches, and draws between players. It expects player objects to respond to
|
|
14
|
+
# `elo_ranking` and `beat!` methods, and includes configuration support.
|
|
15
|
+
#
|
|
16
|
+
# Example usage:
|
|
17
|
+
# EloRankable.record_multiplayer_match([player1, player2, player3])
|
|
18
|
+
# EloRankable.record_winner_vs_all(winner, [loser1, loser2])
|
|
19
|
+
# EloRankable.record_draw(player1, player2)
|
|
20
|
+
#
|
|
21
|
+
# Configuration can be customized via EloRankable.configure.
|
|
22
|
+
#
|
|
23
|
+
# Errors:
|
|
24
|
+
# EloRankable::InvalidMatchError - Raised for invalid match scenarios.
|
|
25
|
+
# ArgumentError - Raised for invalid arguments or player objects.
|
|
26
|
+
module EloRankable
|
|
27
|
+
class Error < StandardError; end
|
|
28
|
+
class InvalidMatchError < Error; end
|
|
29
|
+
|
|
30
|
+
class << self
|
|
31
|
+
def config
|
|
32
|
+
@config ||= Configuration.new
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def configure
|
|
36
|
+
yield(config) if block_given?
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# Record a multiplayer match where players are ranked by their position in the array
|
|
40
|
+
# Higher-indexed players are treated as having lost to lower-indexed ones
|
|
41
|
+
def record_multiplayer_match(players)
|
|
42
|
+
raise InvalidMatchError, 'Need at least 2 players for a match' if players.length < 2
|
|
43
|
+
|
|
44
|
+
# Validate input array
|
|
45
|
+
raise ArgumentError, 'Players array cannot contain nil values' if players.any?(&:nil?)
|
|
46
|
+
|
|
47
|
+
# Check for duplicates
|
|
48
|
+
raise ArgumentError, 'Players array cannot contain duplicate players' if players.uniq.length != players.length
|
|
49
|
+
|
|
50
|
+
# Validate all players respond to elo_ranking
|
|
51
|
+
invalid_players = players.reject { |p| p.respond_to?(:elo_ranking) }
|
|
52
|
+
raise ArgumentError, 'All players must respond to elo_ranking' unless invalid_players.empty?
|
|
53
|
+
|
|
54
|
+
# Process all pairwise combinations
|
|
55
|
+
players.each_with_index do |player1, i|
|
|
56
|
+
players[(i + 1)..].each do |player2|
|
|
57
|
+
player1.beat!(player2)
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# Record a single winner vs all others match
|
|
63
|
+
def record_winner_vs_all(winner, losers)
|
|
64
|
+
# Validate winner
|
|
65
|
+
raise ArgumentError, 'Winner cannot be nil' if winner.nil?
|
|
66
|
+
raise ArgumentError, 'Winner must respond to elo_ranking' unless winner.respond_to?(:elo_ranking)
|
|
67
|
+
|
|
68
|
+
# Validate losers array
|
|
69
|
+
raise InvalidMatchError, 'Need at least 1 loser' if losers.empty?
|
|
70
|
+
raise ArgumentError, 'Losers array cannot contain nil values' if losers.any?(&:nil?)
|
|
71
|
+
raise InvalidMatchError, 'Winner cannot be in losers list' if losers.include?(winner)
|
|
72
|
+
|
|
73
|
+
# Validate all losers respond to elo_ranking
|
|
74
|
+
invalid_losers = losers.reject { |p| p.respond_to?(:elo_ranking) }
|
|
75
|
+
raise ArgumentError, 'All losers must respond to elo_ranking' unless invalid_losers.empty?
|
|
76
|
+
|
|
77
|
+
losers.each do |loser|
|
|
78
|
+
winner.beat!(loser)
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
# Record a draw between two players
|
|
83
|
+
def record_draw(player1, player2)
|
|
84
|
+
raise ArgumentError, 'Player1 cannot be nil' if player1.nil?
|
|
85
|
+
raise ArgumentError, 'Player2 cannot be nil' if player2.nil?
|
|
86
|
+
raise ArgumentError, 'Cannot record draw with same player' if player1 == player2
|
|
87
|
+
raise ArgumentError, 'Player1 must respond to elo_ranking' unless player1.respond_to?(:elo_ranking)
|
|
88
|
+
raise ArgumentError, 'Player2 must respond to elo_ranking' unless player2.respond_to?(:elo_ranking)
|
|
89
|
+
|
|
90
|
+
Calculator.update_ratings_for_draw(player1, player2)
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
# Hook into ActiveRecord
|
|
96
|
+
ActiveRecord::Base.extend(EloRankable::HasEloRanking) if defined?(ActiveRecord::Base)
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'rails/generators/active_record'
|
|
4
|
+
|
|
5
|
+
module EloRankable
|
|
6
|
+
module Generators
|
|
7
|
+
class InstallGenerator < ActiveRecord::Generators::Base
|
|
8
|
+
desc 'Create migration for EloRankable'
|
|
9
|
+
|
|
10
|
+
source_root File.expand_path('templates', __dir__)
|
|
11
|
+
|
|
12
|
+
def self.default_generator_root
|
|
13
|
+
File.dirname(__FILE__)
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def create_migration_file
|
|
17
|
+
migration_template 'create_elo_rankings.rb', 'db/migrate/create_elo_rankings.rb'
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class CreateEloRankings < ActiveRecord::Migration[6.0]
|
|
4
|
+
def change
|
|
5
|
+
create_table :elo_rankings do |t|
|
|
6
|
+
t.references :rankable, polymorphic: true, null: false, index: true
|
|
7
|
+
t.integer :rating, null: false, default: 1200
|
|
8
|
+
t.integer :games_played, null: false, default: 0
|
|
9
|
+
|
|
10
|
+
t.timestamps
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
add_index :elo_rankings, :rating
|
|
14
|
+
add_index :elo_rankings, %i[rankable_type rankable_id], unique: true
|
|
15
|
+
end
|
|
16
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: elo_rankable
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.2
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Aberen
|
|
8
|
+
autorequire:
|
|
9
|
+
bindir: exe
|
|
10
|
+
cert_chain: []
|
|
11
|
+
date: 2025-08-06 00:00:00.000000000 Z
|
|
12
|
+
dependencies:
|
|
13
|
+
- !ruby/object:Gem::Dependency
|
|
14
|
+
name: activerecord
|
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
|
16
|
+
requirements:
|
|
17
|
+
- - ">="
|
|
18
|
+
- !ruby/object:Gem::Version
|
|
19
|
+
version: '6.0'
|
|
20
|
+
- - "<"
|
|
21
|
+
- !ruby/object:Gem::Version
|
|
22
|
+
version: '8.0'
|
|
23
|
+
type: :runtime
|
|
24
|
+
prerelease: false
|
|
25
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
26
|
+
requirements:
|
|
27
|
+
- - ">="
|
|
28
|
+
- !ruby/object:Gem::Version
|
|
29
|
+
version: '6.0'
|
|
30
|
+
- - "<"
|
|
31
|
+
- !ruby/object:Gem::Version
|
|
32
|
+
version: '8.0'
|
|
33
|
+
- !ruby/object:Gem::Dependency
|
|
34
|
+
name: activesupport
|
|
35
|
+
requirement: !ruby/object:Gem::Requirement
|
|
36
|
+
requirements:
|
|
37
|
+
- - ">="
|
|
38
|
+
- !ruby/object:Gem::Version
|
|
39
|
+
version: '6.0'
|
|
40
|
+
- - "<"
|
|
41
|
+
- !ruby/object:Gem::Version
|
|
42
|
+
version: '8.0'
|
|
43
|
+
type: :runtime
|
|
44
|
+
prerelease: false
|
|
45
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
46
|
+
requirements:
|
|
47
|
+
- - ">="
|
|
48
|
+
- !ruby/object:Gem::Version
|
|
49
|
+
version: '6.0'
|
|
50
|
+
- - "<"
|
|
51
|
+
- !ruby/object:Gem::Version
|
|
52
|
+
version: '8.0'
|
|
53
|
+
description: Adds Elo rating to any ActiveRecord model via has_elo_ranking.
|
|
54
|
+
email:
|
|
55
|
+
- nijoergensen@gmail.com
|
|
56
|
+
executables: []
|
|
57
|
+
extensions: []
|
|
58
|
+
extra_rdoc_files: []
|
|
59
|
+
files:
|
|
60
|
+
- ".rspec"
|
|
61
|
+
- ".rubocop.yml"
|
|
62
|
+
- CHANGELOG.md
|
|
63
|
+
- LICENSE.txt
|
|
64
|
+
- README.md
|
|
65
|
+
- Rakefile
|
|
66
|
+
- elo_rankable.gemspec
|
|
67
|
+
- lib/elo_rankable.rb
|
|
68
|
+
- lib/elo_rankable/calculator.rb
|
|
69
|
+
- lib/elo_rankable/configuration.rb
|
|
70
|
+
- lib/elo_rankable/elo_ranking.rb
|
|
71
|
+
- lib/elo_rankable/has_elo_ranking.rb
|
|
72
|
+
- lib/elo_rankable/version.rb
|
|
73
|
+
- lib/generators/elo_rankable/install/install_generator.rb
|
|
74
|
+
- lib/generators/elo_rankable/install/templates/create_elo_rankings.rb
|
|
75
|
+
homepage: https://github.com/Aberen/elo_rankable
|
|
76
|
+
licenses:
|
|
77
|
+
- MIT
|
|
78
|
+
metadata:
|
|
79
|
+
homepage_uri: https://github.com/Aberen/elo_rankable
|
|
80
|
+
source_code_uri: https://github.com/Aberen/elo_rankable
|
|
81
|
+
changelog_uri: https://github.com/Aberen/elo_rankable/blob/main/CHANGELOG.md
|
|
82
|
+
rubygems_mfa_required: 'true'
|
|
83
|
+
post_install_message:
|
|
84
|
+
rdoc_options: []
|
|
85
|
+
require_paths:
|
|
86
|
+
- lib
|
|
87
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
88
|
+
requirements:
|
|
89
|
+
- - ">="
|
|
90
|
+
- !ruby/object:Gem::Version
|
|
91
|
+
version: 2.7.0
|
|
92
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
93
|
+
requirements:
|
|
94
|
+
- - ">="
|
|
95
|
+
- !ruby/object:Gem::Version
|
|
96
|
+
version: '0'
|
|
97
|
+
requirements: []
|
|
98
|
+
rubygems_version: 3.5.11
|
|
99
|
+
signing_key:
|
|
100
|
+
specification_version: 4
|
|
101
|
+
summary: Add Elo rating capabilities to any ActiveRecord model
|
|
102
|
+
test_files: []
|