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.
- checksums.yaml +4 -4
- data/.rspec +3 -3
- data/.rubocop.yml +37 -37
- data/CHANGELOG.md +72 -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 +29 -21
- data/lib/generators/elo_rankable/install/templates/create_elo_rankings.rb +16 -16
- metadata +7 -7
data/lib/elo_rankable.rb
CHANGED
|
@@ -1,96 +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)
|
|
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)
|
|
@@ -1,21 +1,29 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require 'rails/generators
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
end
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'rails/generators'
|
|
4
|
+
require 'rails/generators/migration'
|
|
5
|
+
|
|
6
|
+
module EloRankable
|
|
7
|
+
module Generators
|
|
8
|
+
class InstallGenerator < Rails::Generators::Base
|
|
9
|
+
include Rails::Generators::Migration
|
|
10
|
+
|
|
11
|
+
desc 'Create migration for EloRankable'
|
|
12
|
+
|
|
13
|
+
source_root File.expand_path('templates', __dir__)
|
|
14
|
+
|
|
15
|
+
def self.next_migration_number(dirname)
|
|
16
|
+
next_migration_number = current_migration_number(dirname) + 1
|
|
17
|
+
ActiveRecord::Migration.next_migration_number(next_migration_number)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def self.default_generator_root
|
|
21
|
+
File.dirname(__FILE__)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def create_migration_file
|
|
25
|
+
migration_template 'create_elo_rankings.rb', 'db/migrate/create_elo_rankings.rb'
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
@@ -1,16 +1,16 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
class CreateEloRankings < ActiveRecord::Migration[
|
|
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
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class CreateEloRankings < ActiveRecord::Migration[<%= ActiveRecord::Migration.current_version %>]
|
|
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
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: elo_rankable
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.2.
|
|
4
|
+
version: 0.2.2
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Aberen
|
|
@@ -19,7 +19,7 @@ dependencies:
|
|
|
19
19
|
version: '6.0'
|
|
20
20
|
- - "<"
|
|
21
21
|
- !ruby/object:Gem::Version
|
|
22
|
-
version: '
|
|
22
|
+
version: '9.0'
|
|
23
23
|
type: :runtime
|
|
24
24
|
prerelease: false
|
|
25
25
|
version_requirements: !ruby/object:Gem::Requirement
|
|
@@ -29,7 +29,7 @@ dependencies:
|
|
|
29
29
|
version: '6.0'
|
|
30
30
|
- - "<"
|
|
31
31
|
- !ruby/object:Gem::Version
|
|
32
|
-
version: '
|
|
32
|
+
version: '9.0'
|
|
33
33
|
- !ruby/object:Gem::Dependency
|
|
34
34
|
name: activesupport
|
|
35
35
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -39,7 +39,7 @@ dependencies:
|
|
|
39
39
|
version: '6.0'
|
|
40
40
|
- - "<"
|
|
41
41
|
- !ruby/object:Gem::Version
|
|
42
|
-
version: '
|
|
42
|
+
version: '9.0'
|
|
43
43
|
type: :runtime
|
|
44
44
|
prerelease: false
|
|
45
45
|
version_requirements: !ruby/object:Gem::Requirement
|
|
@@ -49,8 +49,8 @@ dependencies:
|
|
|
49
49
|
version: '6.0'
|
|
50
50
|
- - "<"
|
|
51
51
|
- !ruby/object:Gem::Version
|
|
52
|
-
version: '
|
|
53
|
-
description: Adds
|
|
52
|
+
version: '9.0'
|
|
53
|
+
description: Adds Elo rating to any ActiveRecord model via has_elo_ranking. It stores
|
|
54
54
|
ratings in a separate EloRanking model to keep your host model clean, and provides
|
|
55
55
|
domain-style methods for updating rankings after matches.
|
|
56
56
|
email:
|
|
@@ -100,5 +100,5 @@ requirements: []
|
|
|
100
100
|
rubygems_version: 3.5.11
|
|
101
101
|
signing_key:
|
|
102
102
|
specification_version: 4
|
|
103
|
-
summary: Add
|
|
103
|
+
summary: Add Elo rating capabilities to any ActiveRecord model
|
|
104
104
|
test_files: []
|