olympic 0.0.1 → 0.0.2

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,16 @@
1
+ require 'olympic/rating/base'
2
+ require 'olympic/rating/glicko'
3
+
4
+ module Olympic
5
+ module Rating
6
+
7
+ RATINGS = {
8
+ glicko: Glicko
9
+ }
10
+
11
+ def self.for(name)
12
+ RATINGS.fetch(name)
13
+ end
14
+
15
+ end
16
+ end
@@ -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
@@ -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
@@ -1,3 +1,3 @@
1
1
  module Olympic
2
- VERSION = "0.0.1"
2
+ VERSION = "0.0.2"
3
3
  end
@@ -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