olympic 0.0.1 → 0.0.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/.gitattributes +17 -0
- data/.rspec +3 -0
- data/Gemfile +4 -1
- data/LICENSE.txt +1 -1
- data/Rakefile +2 -0
- data/lib/generators/olympic/install/install_generator.rb +22 -0
- data/lib/generators/olympic/install/templates/create_olympic_tables.rb +45 -0
- data/lib/olympic.rb +6 -0
- data/lib/olympic/bracket.rb +20 -0
- data/lib/olympic/bracket/base.rb +87 -0
- data/lib/olympic/bracket/single_elimination.rb +54 -0
- data/lib/olympic/bracket/single_elimination/information.rb +45 -0
- data/lib/olympic/coordinate.rb +0 -0
- data/lib/olympic/coordinate/standard.rb +17 -0
- data/lib/olympic/error.rb +7 -0
- data/lib/olympic/match.rb +71 -0
- data/lib/olympic/rating.rb +16 -0
- data/lib/olympic/rating/base.rb +26 -0
- data/lib/olympic/rating/glicko.rb +95 -0
- data/lib/olympic/rating/glicko/formula.rb +119 -0
- data/lib/olympic/settings.rb +80 -0
- data/lib/olympic/settings/class_methods.rb +19 -0
- data/lib/olympic/source.rb +19 -0
- data/lib/olympic/team.rb +23 -0
- data/lib/olympic/tournament.rb +31 -0
- data/lib/olympic/version.rb +1 -1
- data/olympic.gemspec +3 -0
- data/spec/helpers/match.rb +5 -0
- data/spec/helpers/schema.rb +61 -0
- data/spec/helpers/settings.rb +6 -0
- data/spec/helpers/source.rb +5 -0
- data/spec/helpers/team.rb +5 -0
- data/spec/helpers/tournament.rb +5 -0
- data/spec/olympic/rating/glicko_spec.rb +29 -0
- data/spec/olympic/settings_spec.rb +85 -0
- data/spec/olympic/tournament_spec.rb +43 -0
- data/spec/spec_helper.rb +37 -0
- metadata +93 -4
@@ -0,0 +1,26 @@
|
|
1
|
+
module Olympic
|
2
|
+
module Rating
|
3
|
+
# The base rating. A rating is used to describe the performance
|
4
|
+
# of a unit, e.g. one team. This is just an abstraction of the
|
5
|
+
# rating system, and as such, doesn't actually implement the API.
|
6
|
+
class Base
|
7
|
+
|
8
|
+
# Returns a hash of the fields that are required on a
|
9
|
+
# {Olympic::Team} to make it compatible with the rating system.
|
10
|
+
# The base API requires no fields. However, if it were to
|
11
|
+
# require a `:rating` decimal field, it would have a value of
|
12
|
+
# something like this:
|
13
|
+
#
|
14
|
+
# { rating: [:decimal, { null: false, default: 100.2 }] }
|
15
|
+
#
|
16
|
+
# The key is the name of the field, the value are options that
|
17
|
+
# are passed as options to the SQL column.
|
18
|
+
#
|
19
|
+
# @return [Hash{Symbol => Array<(Symbol, Hash)>}]
|
20
|
+
def self.required_fields
|
21
|
+
{}
|
22
|
+
end
|
23
|
+
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,95 @@
|
|
1
|
+
require "memoist"
|
2
|
+
require "forwardable"
|
3
|
+
require "olympic/rating/glicko/formula"
|
4
|
+
|
5
|
+
module Olympic
|
6
|
+
|
7
|
+
# note:
|
8
|
+
# Match#rating
|
9
|
+
# Match#outcome
|
10
|
+
# Match#derivation
|
11
|
+
#
|
12
|
+
module Rating
|
13
|
+
# The Glicko rating system. This is Glicko, not Glicko2.
|
14
|
+
#
|
15
|
+
# @see http://www.glicko.net/glicko/glicko.pdf
|
16
|
+
class Glicko < Base
|
17
|
+
|
18
|
+
extend Memoist
|
19
|
+
extend Forwardable
|
20
|
+
def_delegators :@team, :rating=, :derivation=
|
21
|
+
|
22
|
+
# (see Base.required_fields)
|
23
|
+
def self.required_fields
|
24
|
+
{
|
25
|
+
rating: [:float, { null: false,
|
26
|
+
default: Formula::DEFAULT_RATING }],
|
27
|
+
derivation: [:float, { null: false,
|
28
|
+
default: Formula::DEFAULT_RATING_DERIVATION }]
|
29
|
+
}
|
30
|
+
end
|
31
|
+
|
32
|
+
def initialize(team)
|
33
|
+
@team = team
|
34
|
+
end
|
35
|
+
|
36
|
+
# Updates the rating with the given match information. Matches
|
37
|
+
# should be an array of hashes that contain information about
|
38
|
+
# those matches.
|
39
|
+
#
|
40
|
+
# @param matches [Array<Olympic::Match>]
|
41
|
+
# @return [void]
|
42
|
+
def update(matches)
|
43
|
+
standardized = standardize_matches(matches)
|
44
|
+
Formula.new(self, standardized).call
|
45
|
+
end
|
46
|
+
|
47
|
+
def rating
|
48
|
+
@team.rating ||= 1500
|
49
|
+
end
|
50
|
+
|
51
|
+
def derivation
|
52
|
+
case
|
53
|
+
when unrated? then 350
|
54
|
+
when time_passed == 0 then @team.derivation
|
55
|
+
else
|
56
|
+
[
|
57
|
+
30,
|
58
|
+
Math.sqrt(@team.derivation ** 2 +
|
59
|
+
CERTAINTY_DECAY * time_passed),
|
60
|
+
350
|
61
|
+
].sort[1]
|
62
|
+
end
|
63
|
+
end
|
64
|
+
memoize :derivation
|
65
|
+
|
66
|
+
def time_passed
|
67
|
+
# TODO
|
68
|
+
0
|
69
|
+
end
|
70
|
+
|
71
|
+
def unrated?
|
72
|
+
@team.rating == nil && @team.derivation == nil
|
73
|
+
end
|
74
|
+
|
75
|
+
private
|
76
|
+
|
77
|
+
# @param matches [Array<Olympic::Match>]
|
78
|
+
# @return [Array<Hash>]
|
79
|
+
def standardize_matches(matches)
|
80
|
+
matches.map do |match|
|
81
|
+
raise Rating::Error, "Too many/few incoming sources" unless
|
82
|
+
match.incoming.size == 2
|
83
|
+
other = match.participants.find { |team| team != @team }
|
84
|
+
raise Rating::Error, "Could not find combatant" unless other
|
85
|
+
other = Glicko.new(other)
|
86
|
+
{
|
87
|
+
rating: other.rating,
|
88
|
+
derivation: other.derivation,
|
89
|
+
outcome: if match.winner == @team then 1 else 0 end
|
90
|
+
}
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|
@@ -0,0 +1,119 @@
|
|
1
|
+
require 'memoist'
|
2
|
+
|
3
|
+
module Olympic
|
4
|
+
module Rating
|
5
|
+
class Glicko < Base
|
6
|
+
class Formula
|
7
|
+
|
8
|
+
extend Memoist
|
9
|
+
|
10
|
+
# The default rating. Glicko states to use 1500 as the
|
11
|
+
# default.
|
12
|
+
DEFAULT_RATING = 1500
|
13
|
+
|
14
|
+
# The default RD. Glicko states to use 350 as the default.
|
15
|
+
DEFAULT_RATING_DERIVATION = 350
|
16
|
+
|
17
|
+
# This is the "certainty decay," or the constant value that
|
18
|
+
# is used in Step 1 of the Glicko rating system. It denotes
|
19
|
+
# the decay of certainty of a player's rating over a given
|
20
|
+
# time (see {Glicko} for a definition of time). The default
|
21
|
+
# chosen here expects a typical RD of 50, with 10 time units
|
22
|
+
# before a decay to 350. The formula would be (in LaTeX):
|
23
|
+
#
|
24
|
+
# c = \sqrt{\frac{350^2-50^2}{10}}
|
25
|
+
#
|
26
|
+
# However, in order to optimize operations, this is c^2. So
|
27
|
+
# the actual value of this would be:
|
28
|
+
#
|
29
|
+
# c = \frac{350^2-50^2}{10}
|
30
|
+
CERTAINTY_DECAY = 12_000
|
31
|
+
|
32
|
+
Q = Math.log2(10).fdiv(400)
|
33
|
+
Q2 = Q ** 2
|
34
|
+
|
35
|
+
def initialize(unit, matches)
|
36
|
+
@unit = unit
|
37
|
+
@matches = matches
|
38
|
+
end
|
39
|
+
|
40
|
+
def call
|
41
|
+
flush_cache
|
42
|
+
@unit.rating = new_rating
|
43
|
+
@unit.derivation = new_derivation
|
44
|
+
end
|
45
|
+
|
46
|
+
# This returns the current rating derivation of the unit. It
|
47
|
+
# clamps the rating derivation to between 30 and 350. The
|
48
|
+
# Glicko system states that it must be less than or equal to
|
49
|
+
# 350, but later recommends not letting the rating derivation
|
50
|
+
# drop below 30.
|
51
|
+
#
|
52
|
+
# @return [Numeric]
|
53
|
+
def derivation
|
54
|
+
if @unit.unrated?
|
55
|
+
350
|
56
|
+
elsif @unit.time_passed == 0
|
57
|
+
@unit.derivation
|
58
|
+
else
|
59
|
+
a = [
|
60
|
+
30,
|
61
|
+
Math.sqrt((@unit.derivation) ** 2 + CERTAINTY_DECAY * @unit.time_passed),
|
62
|
+
350
|
63
|
+
].sort[1]
|
64
|
+
end
|
65
|
+
end
|
66
|
+
memoize :derivation
|
67
|
+
|
68
|
+
private
|
69
|
+
|
70
|
+
def new_rating
|
71
|
+
@unit.rating +
|
72
|
+
Q.fdiv(1.0.fdiv(derivation ** 2) + 1.0.fdiv(delta)) * summation
|
73
|
+
end
|
74
|
+
memoize :new_rating
|
75
|
+
|
76
|
+
def new_derivation
|
77
|
+
Math.sqrt(1.0.fdiv(
|
78
|
+
1.0.fdiv(derivation ** 2) + 1.0.fdiv(delta)))
|
79
|
+
end
|
80
|
+
memoize :new_derivation
|
81
|
+
|
82
|
+
def summation
|
83
|
+
@matches.inject(0) do |memo, value|
|
84
|
+
e = epsilon(value[:rating], value[:derivation])
|
85
|
+
g = gamma(value[:derivation])
|
86
|
+
memo + g * (value[:outcome] - e)
|
87
|
+
end
|
88
|
+
end
|
89
|
+
memoize :summation
|
90
|
+
|
91
|
+
def delta
|
92
|
+
1.0.fdiv(Q2 * delta_summation)
|
93
|
+
end
|
94
|
+
memoize :delta
|
95
|
+
|
96
|
+
def delta_summation
|
97
|
+
@matches.inject(0) do |memo, value|
|
98
|
+
e = epsilon(value[:rating], value[:derivation])
|
99
|
+
g = gamma(value[:derivation])
|
100
|
+
memo + (g ** 2) * e * (1 - e)
|
101
|
+
end
|
102
|
+
end
|
103
|
+
memoize :delta_summation
|
104
|
+
|
105
|
+
def gamma(derivation)
|
106
|
+
1.0.fdiv Math.sqrt(1 + (3 * Q2 * (derivation ** 2)).fdiv(Math::PI ** 2))
|
107
|
+
end
|
108
|
+
memoize :gamma
|
109
|
+
|
110
|
+
def epsilon(rating, derivation)
|
111
|
+
1.0.fdiv(1 + 10 **
|
112
|
+
(gamma(derivation) * (@unit.rating - rating)).fdiv(-400))
|
113
|
+
end
|
114
|
+
memoize :epsilon
|
115
|
+
|
116
|
+
end
|
117
|
+
end
|
118
|
+
end
|
119
|
+
end
|
@@ -0,0 +1,80 @@
|
|
1
|
+
require 'forwardable'
|
2
|
+
require 'olympic/settings/class_methods'
|
3
|
+
|
4
|
+
module Olympic
|
5
|
+
class Settings
|
6
|
+
|
7
|
+
extend ClassMethods
|
8
|
+
extend Forwardable
|
9
|
+
|
10
|
+
def_delegators :@settings, :fetch
|
11
|
+
|
12
|
+
DEFAULT_SETTINGS = {
|
13
|
+
match_class: "Match",
|
14
|
+
match_primary_key: :id,
|
15
|
+
match_tournament_key: :tournament_id,
|
16
|
+
match_foreign_key: :match_id,
|
17
|
+
match_winner_key: :winner_id,
|
18
|
+
match_join_key: :match_id,
|
19
|
+
team_class: "Team",
|
20
|
+
team_primary_key: :id,
|
21
|
+
team_tournament_join: :teams_tournaments,
|
22
|
+
team_join_key: :team_id,
|
23
|
+
tournament_class: "Tournament",
|
24
|
+
tournament_primary_key: :id,
|
25
|
+
tournament_root_key: :root_id,
|
26
|
+
tournament_join_key: :tournament_id,
|
27
|
+
source_class: "Source",
|
28
|
+
source_primary_key: :id,
|
29
|
+
source_match_key: :match_id,
|
30
|
+
source_source_key: :source_id,
|
31
|
+
source_source_type: :source_type,
|
32
|
+
rating: :glicko
|
33
|
+
}
|
34
|
+
|
35
|
+
def initialize(settings = {})
|
36
|
+
@settings = DEFAULT_SETTINGS.clone.merge(settings)
|
37
|
+
@klass = {}
|
38
|
+
end
|
39
|
+
|
40
|
+
def build
|
41
|
+
yield self
|
42
|
+
self
|
43
|
+
end
|
44
|
+
|
45
|
+
def []=(key, value)
|
46
|
+
@klass = {}
|
47
|
+
@settings[key.to_s.to_sym] = value
|
48
|
+
end
|
49
|
+
|
50
|
+
def [](key)
|
51
|
+
@settings.fetch(key.to_s.to_sym)
|
52
|
+
end
|
53
|
+
|
54
|
+
def class_for(name)
|
55
|
+
@klass.fetch(name) do
|
56
|
+
@klass[name] = fetch(:"#{name}_class").constantize
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
def rating_system
|
61
|
+
Rating.for(rating)
|
62
|
+
end
|
63
|
+
|
64
|
+
def method_missing(method, *args, &block)
|
65
|
+
return super if args.length > 1 || block_given?
|
66
|
+
|
67
|
+
method = method.to_s
|
68
|
+
case method
|
69
|
+
when /\?\z/, /\!\z/
|
70
|
+
super
|
71
|
+
when /\A(.*)\=\z/
|
72
|
+
super unless args.length == 1
|
73
|
+
self[$+] = args[0]
|
74
|
+
else
|
75
|
+
super unless args.length == 0
|
76
|
+
self[method]
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
require 'forwardable'
|
2
|
+
|
3
|
+
module Olympic
|
4
|
+
class Settings
|
5
|
+
module ClassMethods
|
6
|
+
|
7
|
+
extend Forwardable
|
8
|
+
|
9
|
+
def_delegators :settings, :[], :[]=, :class_for, :fetch,
|
10
|
+
:rating_system, :method_missing
|
11
|
+
def_delegator :settings, :build, :configure
|
12
|
+
|
13
|
+
def settings
|
14
|
+
@_settings ||= Settings.new
|
15
|
+
end
|
16
|
+
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
module Olympic
|
2
|
+
module Source
|
3
|
+
|
4
|
+
extend ActiveSupport::Concern
|
5
|
+
|
6
|
+
included do
|
7
|
+
belongs_to :match,
|
8
|
+
class_name: Olympic::Settings[:match_class],
|
9
|
+
foreign_key: Olympic::Settings[:source_match_key],
|
10
|
+
primary_key: Olympic::Settings[:match_primary_key]
|
11
|
+
belongs_to :source,
|
12
|
+
polymorphic: true,
|
13
|
+
foreign_type: Olympic::Settings[:source_source_type],
|
14
|
+
foreign_key: Olympic::Settings[:source_source_key],
|
15
|
+
primary_key: Olympic::Settings[:source_primary_key]
|
16
|
+
end
|
17
|
+
|
18
|
+
end
|
19
|
+
end
|
data/lib/olympic/team.rb
ADDED
@@ -0,0 +1,23 @@
|
|
1
|
+
module Olympic
|
2
|
+
module Team
|
3
|
+
|
4
|
+
extend ActiveSupport::Concern
|
5
|
+
|
6
|
+
included do
|
7
|
+
has_and_belongs_to_many :tournaments,
|
8
|
+
class_name: Olympic::Settings[:tournament_class],
|
9
|
+
join_table: Olympic::Settings[:team_tournament_join],
|
10
|
+
foreign_key: Olympic::Settings[:team_join_key],
|
11
|
+
association_foreign_key: Olympic::Settings[:tournament_join_key]
|
12
|
+
has_many :sources,
|
13
|
+
class_name: Olympic::Settings[:source_class],
|
14
|
+
foreign_key: Olympic::Settings[:source_source_key],
|
15
|
+
foreign_type: Olympic::Settings[:source_source_type],
|
16
|
+
primary_key: Olympic::Settings[:team_primary_key],
|
17
|
+
as: :sources
|
18
|
+
has_many :matches,
|
19
|
+
class_name: Olympic::Settings[:match_class],
|
20
|
+
through: :sources
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
module Olympic
|
2
|
+
module Tournament
|
3
|
+
|
4
|
+
extend ActiveSupport::Concern
|
5
|
+
|
6
|
+
included do
|
7
|
+
has_many :matches,
|
8
|
+
class_name: Olympic::Settings[:match_class],
|
9
|
+
foreign_key: Olympic::Settings[:match_tournament_key],
|
10
|
+
primary_key: Olympic::Settings[:tournament_primary_key]
|
11
|
+
belongs_to :root,
|
12
|
+
class_name: Olympic::Settings[:match_class],
|
13
|
+
foreign_key: Olympic::Settings[:tournament_root_key],
|
14
|
+
primary_key: Olympic::Settings[:match_primary_key]
|
15
|
+
has_and_belongs_to_many :teams,
|
16
|
+
class_name: Olympic::Settings[:team_class],
|
17
|
+
join_table: Olympic::Settings[:team_tournament_join],
|
18
|
+
foreign_key: Olympic::Settings[:tournament_join_key],
|
19
|
+
association_foreign_key: Olympic::Settings[:team_join_key]
|
20
|
+
end
|
21
|
+
|
22
|
+
module ClassMethods
|
23
|
+
def create_tournament(teams:, bracket:)
|
24
|
+
tournament = new
|
25
|
+
Olympic::Bracket.for(bracket, tournament, teams).call
|
26
|
+
tournament
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
end
|
31
|
+
end
|
data/lib/olympic/version.rb
CHANGED
data/olympic.gemspec
CHANGED
@@ -18,6 +18,9 @@ Gem::Specification.new do |spec|
|
|
18
18
|
spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
|
19
19
|
spec.require_paths = ["lib"]
|
20
20
|
|
21
|
+
spec.add_dependency 'rails', '>= 3.0', '< 5.1'
|
22
|
+
spec.add_dependency 'memoist', '~> 0.12'
|
21
23
|
spec.add_development_dependency "bundler", "~> 1.6"
|
22
24
|
spec.add_development_dependency "rake"
|
25
|
+
spec.add_development_dependency "rspec", "~> 3.1"
|
23
26
|
end
|