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
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 8fc5a712fa617f6848bd48f1884a92edb90e861e
|
4
|
+
data.tar.gz: 70b248949c0213508364d451bc79ce7156001f83
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: ad34729ac639b8086370cead8746cf4ba2dc24162b5ae0c74b113dc70013c99852d4fe011311d578531468940934669e43a3333cc345a53df8e7f5db68783c47
|
7
|
+
data.tar.gz: 238d2fd84dc60ae9984f77417cf83fb56032e171a37f0e459bcbd94162467c0a8a59b0a78ea7b24b1ef4cfc971c2d2c886574d047bc5fa1b85922452da58a938
|
data/.gitattributes
ADDED
@@ -0,0 +1,17 @@
|
|
1
|
+
# Auto detect text files and perform LF normalization
|
2
|
+
* text=auto
|
3
|
+
|
4
|
+
# Custom for Visual Studio
|
5
|
+
*.cs diff=csharp
|
6
|
+
|
7
|
+
# Standard to msysgit
|
8
|
+
*.doc diff=astextplain
|
9
|
+
*.DOC diff=astextplain
|
10
|
+
*.docx diff=astextplain
|
11
|
+
*.DOCX diff=astextplain
|
12
|
+
*.dot diff=astextplain
|
13
|
+
*.DOT diff=astextplain
|
14
|
+
*.pdf diff=astextplain
|
15
|
+
*.PDF diff=astextplain
|
16
|
+
*.rtf diff=astextplain
|
17
|
+
*.RTF diff=astextplain
|
data/.rspec
ADDED
data/Gemfile
CHANGED
data/LICENSE.txt
CHANGED
data/Rakefile
CHANGED
@@ -0,0 +1,22 @@
|
|
1
|
+
module Olympic
|
2
|
+
module Generators
|
3
|
+
class InstallGenerator < ::Rails::Generators::Base
|
4
|
+
include Rails::Generators::Migration
|
5
|
+
|
6
|
+
desc "Create a migration to add olympic tables."
|
7
|
+
source_root File.expand_path("../templates", __FILE__)
|
8
|
+
|
9
|
+
# Implement the required interface for Rails::Generators::Migration.
|
10
|
+
# This is _literally_ documented _nowhere_. Wtf.
|
11
|
+
def self.next_migration_number(dirname)
|
12
|
+
next_migration_number = current_migration_number(dirname) + 1
|
13
|
+
ActiveRecord::Migration.next_migration_number(next_migration_number)
|
14
|
+
end
|
15
|
+
|
16
|
+
def generate_migration
|
17
|
+
migration_template "create_olympic_tables.rb", "db/migrate/create_olympic_tables.rb"
|
18
|
+
end
|
19
|
+
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,45 @@
|
|
1
|
+
class CreateOlympicTables < ActiveRecord::Migration
|
2
|
+
|
3
|
+
def change
|
4
|
+
create_table :teams do |t|
|
5
|
+
# edit this.
|
6
|
+
<%- if Olympic::Settings.rating_system -%>
|
7
|
+
<%- Olympic::Settings.rating_system.required_fields.each do |key, value| -%>
|
8
|
+
t.<%= value.first %> <%= key.inspect %>, <%= value.last.inspect[1..-2] %>
|
9
|
+
<%- end -%>
|
10
|
+
<%- end -%>
|
11
|
+
raise NotImplementedError, "Update teams table in #{__FILE__}"
|
12
|
+
end
|
13
|
+
|
14
|
+
create_table :tournaments do |t|
|
15
|
+
t.string :bracket
|
16
|
+
t.integer :root_id
|
17
|
+
end
|
18
|
+
|
19
|
+
create_table :teams_tournaments, id: false do |t|
|
20
|
+
t.integer :team_id
|
21
|
+
t.integer :tournament_id
|
22
|
+
end
|
23
|
+
|
24
|
+
create_table :matches_teams, id: false do |t|
|
25
|
+
t.integer :match_id
|
26
|
+
t.integer :team_id
|
27
|
+
end
|
28
|
+
|
29
|
+
create_table :sources do |t|
|
30
|
+
t.integer :match_id
|
31
|
+
t.integer :source_id
|
32
|
+
t.string :source_type
|
33
|
+
end
|
34
|
+
|
35
|
+
create_table :matches do |t|
|
36
|
+
t.integer :tournament_id
|
37
|
+
t.integer :winner_id
|
38
|
+
t.integer :depth
|
39
|
+
t.integer :offset
|
40
|
+
t.integer :value
|
41
|
+
t.text :data
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
end
|
data/lib/olympic.rb
CHANGED
@@ -0,0 +1,20 @@
|
|
1
|
+
require 'olympic/bracket/base'
|
2
|
+
require 'olympic/bracket/single_elimination'
|
3
|
+
require 'olympic/bracket/double_elimination'
|
4
|
+
require 'olympic/bracket/round_robin'
|
5
|
+
|
6
|
+
module Olympic
|
7
|
+
module Bracket
|
8
|
+
|
9
|
+
BRACKETS = {
|
10
|
+
single_elimination: SingleElimination,
|
11
|
+
double_elimination: DoubleElimination,
|
12
|
+
round_robin: RoundRobin
|
13
|
+
}
|
14
|
+
|
15
|
+
def self.for(name, tournament, teams)
|
16
|
+
BRACKETS.fetch(name).new(tournament, teams)
|
17
|
+
end
|
18
|
+
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,87 @@
|
|
1
|
+
require 'olympic/bracket/single_elimination'
|
2
|
+
|
3
|
+
module Olympic
|
4
|
+
module Bracket
|
5
|
+
# A basic bracket. This defines the basic API that all brackets
|
6
|
+
# should have. In order for a bracket to be compatible with
|
7
|
+
# Olympic, only these things need to be implemented.
|
8
|
+
class Base
|
9
|
+
|
10
|
+
# Initialize the bracket.
|
11
|
+
#
|
12
|
+
# @param tournament [Olympic::Tournament]
|
13
|
+
# @param teams [Array<Olympic::Team>]
|
14
|
+
def initialize(tournament, teams)
|
15
|
+
@tournament = tournament
|
16
|
+
@teams = teams
|
17
|
+
end
|
18
|
+
|
19
|
+
# @!method call
|
20
|
+
# Initializes the tournament to handle the bracket. Sets up
|
21
|
+
# all matches and sources in accordance to the bracket
|
22
|
+
# itself.
|
23
|
+
#
|
24
|
+
# @return [void]
|
25
|
+
#
|
26
|
+
# @!method matches
|
27
|
+
# Returns the number of expected matches in the bracket. If
|
28
|
+
# the number of matches is variable, it will return an array
|
29
|
+
# of numbers, which contains potential values for the number
|
30
|
+
# of matches in the bracket.
|
31
|
+
#
|
32
|
+
# @return [Numeric, Array<Numeric>]
|
33
|
+
#
|
34
|
+
# @!method rounds
|
35
|
+
# Returns the number of expected rounds in the bracket. If
|
36
|
+
# the number of rounds is variable, it will return an array
|
37
|
+
# of numbers, which contains potential values for the number
|
38
|
+
# of rounds in the bracket.
|
39
|
+
#
|
40
|
+
# @return [Numeric, Array<Numeric>]
|
41
|
+
#
|
42
|
+
# @!method rounded_teams
|
43
|
+
# In an elimination tournament, the number of teams has to be
|
44
|
+
# rounded to the nearest power of two to calculate the number
|
45
|
+
# of byes. If this is not necessary, it will return the
|
46
|
+
# number of teams.
|
47
|
+
#
|
48
|
+
# @return [Numeric]
|
49
|
+
#
|
50
|
+
# @!method byes
|
51
|
+
# The number of byes that are required in the bracket. This
|
52
|
+
# should be greater than or equal to zero.
|
53
|
+
#
|
54
|
+
# @return [Numeric]
|
55
|
+
#
|
56
|
+
# @!method first_round_matches
|
57
|
+
# The number of matches in the first round. This will always
|
58
|
+
# be a finite number, so this method will never return an
|
59
|
+
# array of numbers.
|
60
|
+
#
|
61
|
+
# @return [Numeric]
|
62
|
+
%(call matches rounds rounded_teams byes
|
63
|
+
first_round_matches).map(&:to_sym).each do |name|
|
64
|
+
define_method(name) do
|
65
|
+
_not_implemented_error(name)
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
private
|
70
|
+
|
71
|
+
# Raises an error stating that the given method wasn't
|
72
|
+
# implemented. This is the default action the API methods on
|
73
|
+
# this class takes; a subclass should redefine the methods to
|
74
|
+
# perform an action.
|
75
|
+
#
|
76
|
+
# @note This should not be used outside of the base class. If
|
77
|
+
# A `NotImplementedError` is to be raised outside of the base
|
78
|
+
# class, it should be raised with `raise`.
|
79
|
+
# @raise [NotImplementError] on call.
|
80
|
+
# @return [void]
|
81
|
+
def _not_implemented_error(name)
|
82
|
+
raise NotImplementedError, "`#{name}` is not implemented " \
|
83
|
+
"on `#{self.class}`"
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
@@ -0,0 +1,54 @@
|
|
1
|
+
require 'memoist'
|
2
|
+
require 'olympic/bracket/single_elimination/information'
|
3
|
+
|
4
|
+
module Olympic
|
5
|
+
module Bracket
|
6
|
+
|
7
|
+
# A single elimination tournament. A single elimination
|
8
|
+
# tournament is a tournament of a set of teams in which the
|
9
|
+
# first loss results in elimination from the tournament. Single
|
10
|
+
# elimination tournaments have the problem in which when the
|
11
|
+
# number of teams is not equal to a power of two, at least one bye
|
12
|
+
# is required.
|
13
|
+
class SingleElimination < Base
|
14
|
+
extend Memoist
|
15
|
+
include Information
|
16
|
+
|
17
|
+
# Creates a single elimination tournament, by setting up the
|
18
|
+
# needed matches (and their respective sources).
|
19
|
+
def call
|
20
|
+
matches = pair(initial_sources)
|
21
|
+
@tournament.root = matches.sort_by(&:depth).last
|
22
|
+
end
|
23
|
+
|
24
|
+
private
|
25
|
+
|
26
|
+
def pair(incoming, depth = 1)
|
27
|
+
round = incoming.each_slice(2).each_with_index.
|
28
|
+
map { |pair, i|
|
29
|
+
match = Settings.class_for(:match).
|
30
|
+
create(incoming: pair,
|
31
|
+
tournament: @tournament,
|
32
|
+
depth: depth,
|
33
|
+
offset: i) }
|
34
|
+
|
35
|
+
if round.length > 1
|
36
|
+
outgoing = round.
|
37
|
+
map { |match| Settings.class_for(:source).create(source: match) }
|
38
|
+
round + pair(outgoing, depth + 1)
|
39
|
+
else
|
40
|
+
round
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
# The initial sources. The "sources" in this case are the
|
45
|
+
# teams, which are mapped to an actual {Olympic::Source}.
|
46
|
+
#
|
47
|
+
# @return [Array<Olympic::Source>] The initial sources.
|
48
|
+
def initial_sources
|
49
|
+
@teams.map { |team| Settings.class_for(:source).create(source: team) }
|
50
|
+
end
|
51
|
+
memoize :initial_sources
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
@@ -0,0 +1,45 @@
|
|
1
|
+
require 'memoist'
|
2
|
+
|
3
|
+
module Olympic
|
4
|
+
module Bracket
|
5
|
+
class SingleElimination
|
6
|
+
|
7
|
+
# General information about the tournament.
|
8
|
+
#
|
9
|
+
# @see Base
|
10
|
+
module Information
|
11
|
+
extend Memoist
|
12
|
+
|
13
|
+
# (see Base#matches)
|
14
|
+
def matches
|
15
|
+
@teams.size - 1
|
16
|
+
end
|
17
|
+
memoize :matches
|
18
|
+
|
19
|
+
# (see Base#rounds)
|
20
|
+
def rounds
|
21
|
+
Math.log2(@teams.size).ceil
|
22
|
+
end
|
23
|
+
memoize :rounds
|
24
|
+
|
25
|
+
# (see Base#rounded_teams)
|
26
|
+
def rounded_teams
|
27
|
+
2 ** rounds
|
28
|
+
end
|
29
|
+
memoize :rounded_teams
|
30
|
+
|
31
|
+
# (see Base#byes)
|
32
|
+
def byes
|
33
|
+
rounded_teams - @teams.size
|
34
|
+
end
|
35
|
+
memoize :byes
|
36
|
+
|
37
|
+
# (see Base#first_round_matches)
|
38
|
+
def first_round_matches
|
39
|
+
@teams.size - 2 ** Math.log2(@teams.size).floor
|
40
|
+
end
|
41
|
+
memoize :first_round_matches
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
File without changes
|
@@ -0,0 +1,71 @@
|
|
1
|
+
module Olympic
|
2
|
+
module Match
|
3
|
+
|
4
|
+
extend ActiveSupport::Concern
|
5
|
+
|
6
|
+
included do
|
7
|
+
belongs_to :tournament,
|
8
|
+
class_name: Olympic::Settings[:tournament_class],
|
9
|
+
foreign_key: Olympic::Settings[:match_tournament_key],
|
10
|
+
primary_key: Olympic::Settings[:match_primary_key]
|
11
|
+
# An outgoing source is a source that this match is a part of.
|
12
|
+
# There are many of these, unfortunately.
|
13
|
+
has_many :outgoing,
|
14
|
+
class_name: Olympic::Settings[:source_class],
|
15
|
+
foreign_key: Olympic::Settings[:source_source_key],
|
16
|
+
foreign_type: Olympic::Settings[:source_source_type],
|
17
|
+
primary_key: Olympic::Settings[:match_primary_key],
|
18
|
+
as: :sources
|
19
|
+
# An incoming source is a source that this match is owning.
|
20
|
+
# These are the sources that will participate in the match.
|
21
|
+
has_many :incoming,
|
22
|
+
class_name: Olympic::Settings[:source_class],
|
23
|
+
foreign_key: Olympic::Settings[:source_match_key],
|
24
|
+
primary_key: Olympic::Settings[:match_primary_key]
|
25
|
+
belongs_to :winner,
|
26
|
+
class_name: Olympic::Settings[:team_class],
|
27
|
+
foreign_key: Olympic::Settings[:match_winner_key],
|
28
|
+
primary_key: Olympic::Settings[:team_primary_key]
|
29
|
+
end
|
30
|
+
|
31
|
+
# If this match is ready to commence. This means that the
|
32
|
+
# participants are defined.
|
33
|
+
#
|
34
|
+
# @return [Boolean]
|
35
|
+
def ready?
|
36
|
+
!!participants
|
37
|
+
end
|
38
|
+
|
39
|
+
# If this match has a winner defined; or, in other words, is
|
40
|
+
# completed.
|
41
|
+
#
|
42
|
+
# @return [Boolean]
|
43
|
+
def completed?
|
44
|
+
winner?
|
45
|
+
end
|
46
|
+
|
47
|
+
# Returns the teams that are a part of the match. If the teams
|
48
|
+
# cannot be decided, it will return nil.
|
49
|
+
#
|
50
|
+
# @return [Array<Olympic::Team>, nil]
|
51
|
+
def participants
|
52
|
+
winners = incoming.includes(:source).map do |income|
|
53
|
+
source = income.source
|
54
|
+
case source
|
55
|
+
when Settings.class_for(:team)
|
56
|
+
source
|
57
|
+
when Settings.class_for(:match)
|
58
|
+
source.winner
|
59
|
+
else
|
60
|
+
raise Olympic::Error, "Unknown source #{source}"
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
if winners.any? { |winner| winner == nil }
|
65
|
+
nil
|
66
|
+
else
|
67
|
+
winners
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|