olympic 0.0.1 → 0.0.2
Sign up to get free protection for your applications and to get access to all the features.
- 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
|