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/elo_rankable.gemspec
CHANGED
|
@@ -1,36 +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. It stores ratings in a separate EloRanking model to keep your host model clean, and provides domain-style methods for updating rankings after matches.'
|
|
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', '<
|
|
35
|
-
spec.add_dependency 'activesupport', '>= 6.0', '<
|
|
36
|
-
end
|
|
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. It stores ratings in a separate EloRanking model to keep your host model clean, and provides domain-style methods for updating rankings after matches.'
|
|
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', '< 9.0'
|
|
35
|
+
spec.add_dependency 'activesupport', '>= 6.0', '< 9.0'
|
|
36
|
+
end
|
|
@@ -1,67 +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
|
|
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
|
|
@@ -1,42 +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
|
|
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
|
|
@@ -1,23 +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
|
|
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
|
|
@@ -1,71 +1,71 @@
|
|
|
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
|
-
raise ArgumentError, 'Cannot play against a destroyed record' if other_player.respond_to?(:destroyed?) && other_player.destroyed?
|
|
63
|
-
|
|
64
|
-
# Get the elo_ranking once and validate it
|
|
65
|
-
opponent_ranking = other_player.elo_ranking
|
|
66
|
-
raise ArgumentError, "Opponent's elo_ranking is not initialized" if opponent_ranking.nil?
|
|
67
|
-
raise ArgumentError, "Opponent's elo_ranking is not saved" unless opponent_ranking.persisted?
|
|
68
|
-
end
|
|
69
|
-
end
|
|
70
|
-
end
|
|
71
|
-
end
|
|
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
|
+
raise ArgumentError, 'Cannot play against a destroyed record' if other_player.respond_to?(:destroyed?) && other_player.destroyed?
|
|
63
|
+
|
|
64
|
+
# Get the elo_ranking once and validate it
|
|
65
|
+
opponent_ranking = other_player.elo_ranking
|
|
66
|
+
raise ArgumentError, "Opponent's elo_ranking is not initialized" if opponent_ranking.nil?
|
|
67
|
+
raise ArgumentError, "Opponent's elo_ranking is not saved" unless opponent_ranking.persisted?
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
data/lib/elo_rankable/version.rb
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module EloRankable
|
|
4
|
-
VERSION = '0.2.
|
|
5
|
-
end
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module EloRankable
|
|
4
|
+
VERSION = '0.2.1'
|
|
5
|
+
end
|