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.
@@ -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