tournament-system 0.1.0
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 +7 -0
- data/.gitignore +9 -0
- data/.reek +13 -0
- data/.rspec +3 -0
- data/.rubocop.yml +31 -0
- data/.travis.yml +6 -0
- data/Gemfile +20 -0
- data/LICENSE +21 -0
- data/README.md +96 -0
- data/Rakefile +13 -0
- data/lib/tournament-system.rb +9 -0
- data/lib/tournament/driver.rb +60 -0
- data/lib/tournament/page_playoff.rb +74 -0
- data/lib/tournament/round_robin.rb +59 -0
- data/lib/tournament/seeder.rb +10 -0
- data/lib/tournament/seeder/none.rb +12 -0
- data/lib/tournament/seeder/random.rb +14 -0
- data/lib/tournament/seeder/single_bracket.rb +24 -0
- data/lib/tournament/single_elimination.rb +51 -0
- data/lib/tournament/swiss.rb +39 -0
- data/lib/tournament/swiss/common.rb +110 -0
- data/lib/tournament/swiss/dutch.rb +62 -0
- data/lib/tournament/version.rb +3 -0
- data/tournament-system.gemspec +27 -0
- metadata +81 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 6c0d51a4fcebbc88d39dfa086acb16f1dc16360b
|
4
|
+
data.tar.gz: da0471359dc5f85ecc551a2ce3e8136b0090bd75
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: c3b7e6f7165e6713e68b7c8a18217ebf3dd1d984ecb362ef4dfa47d6cb7f49a4a78c08ce117027681599124a5f6646b65e23223f5184774ad233f9bcd9a4c05c
|
7
|
+
data.tar.gz: a45707695701c74e6c779eb9d248703f2fbcbbc78aa559d0582b19f144914e03b574a1d020dd8f941de2f3b58b9a544103682edc6aca32234667e1257d67cefe
|
data/.gitignore
ADDED
data/.reek
ADDED
@@ -0,0 +1,13 @@
|
|
1
|
+
# These don't work when using modules as stateless namespaces
|
2
|
+
UtilityFunction:
|
3
|
+
enabled: false
|
4
|
+
FeatureEnvy:
|
5
|
+
enabled: false
|
6
|
+
|
7
|
+
# Doesn't work well with case statements
|
8
|
+
TooManyStatements:
|
9
|
+
max_statements: 10
|
10
|
+
|
11
|
+
# Reek isn't good at detecting these, especially with state and blocks
|
12
|
+
DuplicateMethodCall:
|
13
|
+
max_calls: 2
|
data/.rspec
ADDED
data/.rubocop.yml
ADDED
@@ -0,0 +1,31 @@
|
|
1
|
+
AllCops:
|
2
|
+
TargetRubyVersion: 2.3
|
3
|
+
|
4
|
+
# This is fine, really
|
5
|
+
Style/FileName:
|
6
|
+
Enabled: false
|
7
|
+
|
8
|
+
# 'old' style
|
9
|
+
Style/EmptyMethod:
|
10
|
+
EnforcedStyle: expanded
|
11
|
+
|
12
|
+
# It makes much more sense to group them according to purpose
|
13
|
+
Bundler/OrderedGems:
|
14
|
+
Enabled: false
|
15
|
+
|
16
|
+
# Not interchangeable
|
17
|
+
Style/ModuleFunction:
|
18
|
+
Enabled: false
|
19
|
+
|
20
|
+
# Doesn't really matter
|
21
|
+
Style/FrozenStringLiteralComment:
|
22
|
+
Enabled: false
|
23
|
+
|
24
|
+
# Doesn't really make sense for multiline
|
25
|
+
Style/TrailingCommaInLiteral:
|
26
|
+
Enabled: false
|
27
|
+
|
28
|
+
# Tests should be as long as they need to be
|
29
|
+
Metrics/BlockLength:
|
30
|
+
Exclude:
|
31
|
+
- 'spec/**/*'
|
data/.travis.yml
ADDED
data/Gemfile
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
source 'https://rubygems.org'
|
2
|
+
|
3
|
+
gemspec
|
4
|
+
|
5
|
+
# For build scripts
|
6
|
+
gem 'rake'
|
7
|
+
|
8
|
+
# Use rspec for tests
|
9
|
+
gem 'rspec'
|
10
|
+
|
11
|
+
# Test coverage
|
12
|
+
gem 'simplecov'
|
13
|
+
|
14
|
+
# Linting
|
15
|
+
gem 'rubocop', '~> 0.47.1'
|
16
|
+
gem 'reek'
|
17
|
+
|
18
|
+
group :test do
|
19
|
+
gem 'coveralls', require: false
|
20
|
+
end
|
data/LICENSE
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
The MIT License (MIT)
|
2
|
+
|
3
|
+
Copyright (c) 2017 Benjamin Schaaf
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
13
|
+
all copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
21
|
+
THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,96 @@
|
|
1
|
+
# Tournament System
|
2
|
+
|
3
|
+
[](https://travis-ci.org/ozfortress/tournament-system)
|
4
|
+
[](https://coveralls.io/github/ozfortress/tournament-system?branch=master)
|
5
|
+
|
6
|
+
This is a simple gem that implements numerous tournament systems.
|
7
|
+
|
8
|
+
It is designed to easily fit into any memory model you might already have.
|
9
|
+
|
10
|
+
## Installation
|
11
|
+
|
12
|
+
Add this line to your application's Gemfile:
|
13
|
+
|
14
|
+
```ruby
|
15
|
+
gem 'tournament-system'
|
16
|
+
```
|
17
|
+
|
18
|
+
And then execute:
|
19
|
+
|
20
|
+
```bash
|
21
|
+
$ bundle
|
22
|
+
```
|
23
|
+
|
24
|
+
Or install it yourself as:
|
25
|
+
|
26
|
+
```bash
|
27
|
+
$ gem install tournament-system
|
28
|
+
```
|
29
|
+
|
30
|
+
## Usage
|
31
|
+
|
32
|
+
First you need to implement a driver to handle the interface between your data
|
33
|
+
and the tournament systems:
|
34
|
+
|
35
|
+
```ruby
|
36
|
+
class Driver < Tournament::Driver
|
37
|
+
def matches_for_round(round)
|
38
|
+
...
|
39
|
+
end
|
40
|
+
|
41
|
+
def seeded_teams
|
42
|
+
...
|
43
|
+
end
|
44
|
+
|
45
|
+
def ranked_teams
|
46
|
+
...
|
47
|
+
end
|
48
|
+
|
49
|
+
def get_match_winner(match)
|
50
|
+
...
|
51
|
+
end
|
52
|
+
|
53
|
+
def get_match_teams(match)
|
54
|
+
...
|
55
|
+
end
|
56
|
+
|
57
|
+
def get_team_score(team)
|
58
|
+
...
|
59
|
+
end
|
60
|
+
|
61
|
+
def build_match(home_team, away_team)
|
62
|
+
...
|
63
|
+
end
|
64
|
+
end
|
65
|
+
```
|
66
|
+
|
67
|
+
Then you can simply generate matches for any tournament system using a driver
|
68
|
+
instance:
|
69
|
+
|
70
|
+
```ruby
|
71
|
+
driver = Driver.new
|
72
|
+
|
73
|
+
# Generate the 3rd round of a single elimination tournament
|
74
|
+
Tournament::SingleElimination.generate driver, round: 2
|
75
|
+
|
76
|
+
# Generate a round for a round robin tournament, guesses round automatically
|
77
|
+
Tournament::RoundRobin.generate driver
|
78
|
+
|
79
|
+
# Generate a round for a swiss system tournament
|
80
|
+
# with Dutch pairings (default) with a minimum pair size of 6
|
81
|
+
Tournament::Swiss.generate driver, pairer: Tournament::Swiss::Dutch,
|
82
|
+
pair_options: { min_pair_size: 6 }
|
83
|
+
|
84
|
+
# Generate a round for a page playoff system, with an optional bronze match
|
85
|
+
Tournament::PagePlayoff.generate driver, bronze_match: true
|
86
|
+
```
|
87
|
+
|
88
|
+
## Contributing
|
89
|
+
|
90
|
+
Bug reports and pull requests are welcome on GitHub at
|
91
|
+
https://github.com/ozfortress/tournament-system.
|
92
|
+
|
93
|
+
## License
|
94
|
+
|
95
|
+
The gem is available as open source under the terms of the
|
96
|
+
[MIT License](http://opensource.org/licenses/MIT).
|
data/Rakefile
ADDED
@@ -0,0 +1,13 @@
|
|
1
|
+
require 'bundler/gem_tasks'
|
2
|
+
|
3
|
+
require 'rspec/core/rake_task'
|
4
|
+
require 'rubocop/rake_task'
|
5
|
+
require 'reek/rake/task'
|
6
|
+
|
7
|
+
RSpec::Core::RakeTask.new(:rspec)
|
8
|
+
RuboCop::RakeTask.new
|
9
|
+
Reek::Rake::Task.new
|
10
|
+
|
11
|
+
task default: :test
|
12
|
+
task test: %w(rspec lint)
|
13
|
+
task lint: %w(rubocop reek)
|
@@ -0,0 +1,60 @@
|
|
1
|
+
module Tournament
|
2
|
+
# An interface for external tournament data.
|
3
|
+
#
|
4
|
+
# To use any tournament system implemented in this gem, simply subclass this
|
5
|
+
# class and implement the interface functions.
|
6
|
+
# :reek:UnusedParameters
|
7
|
+
class Driver
|
8
|
+
# rubocop:disable Lint/UnusedMethodArgument
|
9
|
+
|
10
|
+
# Get the matches played for a particular round
|
11
|
+
def matches_for_round(round)
|
12
|
+
raise 'Not Implemented'
|
13
|
+
end
|
14
|
+
|
15
|
+
# Get the teams playing with their initial seedings
|
16
|
+
def seeded_teams
|
17
|
+
raise 'Not Implemented'
|
18
|
+
end
|
19
|
+
|
20
|
+
# Get the teams playing, ranked by their current position in the tournament
|
21
|
+
def ranked_teams
|
22
|
+
raise 'Not Implemented'
|
23
|
+
end
|
24
|
+
|
25
|
+
# Get the winning team of a match
|
26
|
+
def get_match_winner(match)
|
27
|
+
raise 'Not Implemented'
|
28
|
+
end
|
29
|
+
|
30
|
+
# Get both teams playing for a match
|
31
|
+
def get_match_teams(match)
|
32
|
+
raise 'Not Implemented'
|
33
|
+
end
|
34
|
+
|
35
|
+
# Get a specific score for a team
|
36
|
+
def get_team_score(team)
|
37
|
+
raise 'Not Implemented'
|
38
|
+
end
|
39
|
+
|
40
|
+
# Handle for matches that are created by tournament systems
|
41
|
+
def build_match(home_team, away_team)
|
42
|
+
raise 'Not Implemented'
|
43
|
+
end
|
44
|
+
|
45
|
+
# rubocop:enable Lint/UnusedMethodArgument
|
46
|
+
|
47
|
+
# Get the losing team of a specific match
|
48
|
+
def get_match_loser(match)
|
49
|
+
winner = get_match_winner(match)
|
50
|
+
get_match_teams(match).reject { |team| team == winner }.first
|
51
|
+
end
|
52
|
+
|
53
|
+
def create_match(home_team, away_team)
|
54
|
+
home_team, away_team = away_team, home_team unless home_team
|
55
|
+
raise 'Invalid match' unless home_team
|
56
|
+
|
57
|
+
build_match(home_team, away_team)
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
@@ -0,0 +1,74 @@
|
|
1
|
+
module Tournament
|
2
|
+
# Implements the page playoff system.
|
3
|
+
module PagePlayoff
|
4
|
+
extend self
|
5
|
+
|
6
|
+
def generate(driver, options = {})
|
7
|
+
teams = driver.ranked_teams
|
8
|
+
raise 'Page Playoffs only works with 4 teams' if teams.length != 4
|
9
|
+
|
10
|
+
round = options[:round] || guess_round(driver)
|
11
|
+
|
12
|
+
case round
|
13
|
+
when 0 then semi_finals(driver, teams)
|
14
|
+
when 1 then preliminary_finals(driver)
|
15
|
+
when 2 then grand_finals(driver, options)
|
16
|
+
else
|
17
|
+
raise 'Invalid round number'
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
def total_rounds
|
22
|
+
3
|
23
|
+
end
|
24
|
+
|
25
|
+
def guess_round(driver)
|
26
|
+
count = driver.matches.length
|
27
|
+
|
28
|
+
case count
|
29
|
+
when 0 then 0
|
30
|
+
when 2 then 1
|
31
|
+
when 3 then 2
|
32
|
+
else
|
33
|
+
raise 'Invalid number of matches'
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
private
|
38
|
+
|
39
|
+
def create_matches(driver, matches)
|
40
|
+
matches.each do |match|
|
41
|
+
driver.create_match match[0], match[1]
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
def semi_finals(driver, teams)
|
46
|
+
create_matches driver, [[teams[0], teams[1]], [teams[2], teams[3]]]
|
47
|
+
end
|
48
|
+
|
49
|
+
def preliminary_finals(driver)
|
50
|
+
matches = driver.matches
|
51
|
+
top_loser = driver.get_match_loser matches[0]
|
52
|
+
bottom_winner = driver.get_match_winner matches[1]
|
53
|
+
|
54
|
+
driver.create_match top_loser, bottom_winner
|
55
|
+
end
|
56
|
+
|
57
|
+
def grand_finals(driver, options)
|
58
|
+
matches = driver.matches
|
59
|
+
top_winner = driver.get_match_winner matches[0]
|
60
|
+
bottom_winner = driver.get_match_winner matches[2]
|
61
|
+
|
62
|
+
driver.create_match top_winner, bottom_winner
|
63
|
+
|
64
|
+
bronze_finals(driver, matches) if options[:bronze_match]
|
65
|
+
end
|
66
|
+
|
67
|
+
def bronze_finals(driver, matches)
|
68
|
+
prelim_loser = driver.get_match_loser matches[2]
|
69
|
+
bottom_semi_loser = driver.get_match_loser matches[1]
|
70
|
+
|
71
|
+
driver.create_match prelim_loser, bottom_semi_loser
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
@@ -0,0 +1,59 @@
|
|
1
|
+
module Tournament
|
2
|
+
# Implements the round-robin tournament system.
|
3
|
+
# Requires a consistent seeder, defaulting to Seeder::None
|
4
|
+
module RoundRobin
|
5
|
+
extend self
|
6
|
+
|
7
|
+
def generate(driver, options = {})
|
8
|
+
round = options[:round] || guess_round(driver)
|
9
|
+
|
10
|
+
teams = seed_teams driver.seeded_teams, options
|
11
|
+
|
12
|
+
teams = rotate_to_round teams, round
|
13
|
+
|
14
|
+
create_matches driver, teams, round
|
15
|
+
end
|
16
|
+
|
17
|
+
def total_rounds(driver)
|
18
|
+
team_count(driver) - 1
|
19
|
+
end
|
20
|
+
|
21
|
+
def guess_round(driver)
|
22
|
+
match_count = driver.matches.length
|
23
|
+
|
24
|
+
match_count / (team_count(driver) / 2)
|
25
|
+
end
|
26
|
+
|
27
|
+
private
|
28
|
+
|
29
|
+
def team_count(driver)
|
30
|
+
count = driver.seeded_teams.length
|
31
|
+
count += 1 if count.odd?
|
32
|
+
count
|
33
|
+
end
|
34
|
+
|
35
|
+
def seed_teams(teams, options)
|
36
|
+
teams << nil if teams.length.odd?
|
37
|
+
|
38
|
+
seeder = options[:seeder] || Seeder::None
|
39
|
+
seeder.seed teams
|
40
|
+
end
|
41
|
+
|
42
|
+
def rotate_to_round(teams, round)
|
43
|
+
rotateable = teams[1..-1]
|
44
|
+
|
45
|
+
[teams[0]] + rotateable.rotate(-round)
|
46
|
+
end
|
47
|
+
|
48
|
+
def create_matches(driver, teams, round)
|
49
|
+
teams[0...teams.length / 2].each_with_index do |home_team, index|
|
50
|
+
away_team = teams[-index - 1]
|
51
|
+
|
52
|
+
# Alternate home/away
|
53
|
+
home_team, away_team = away_team, home_team if round.odd?
|
54
|
+
|
55
|
+
driver.create_match(home_team, away_team)
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
@@ -0,0 +1,10 @@
|
|
1
|
+
require 'tournament/seeder/none'
|
2
|
+
require 'tournament/seeder/random'
|
3
|
+
require 'tournament/seeder/single_bracket'
|
4
|
+
|
5
|
+
module Tournament
|
6
|
+
# Module containing tournament seeders.
|
7
|
+
# Seeders are used by systems to define initial conditions.
|
8
|
+
module Seeder
|
9
|
+
end
|
10
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
module Tournament
|
2
|
+
module Seeder
|
3
|
+
# A seeder for a single-bracket tournament system.
|
4
|
+
# Seeds teams such that the highest expected placing teams should get
|
5
|
+
# furthest in the bracket.
|
6
|
+
module SingleBracket
|
7
|
+
extend self
|
8
|
+
|
9
|
+
def seed(teams)
|
10
|
+
groups = teams.each_slice(2)
|
11
|
+
|
12
|
+
top_half = groups.map { |pair| pair[0] }
|
13
|
+
bottom_half = groups.map { |pair| pair[1] }
|
14
|
+
|
15
|
+
if top_half.length > 2
|
16
|
+
top_half = seed top_half
|
17
|
+
bottom_half = seed bottom_half
|
18
|
+
end
|
19
|
+
|
20
|
+
top_half + bottom_half
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,51 @@
|
|
1
|
+
module Tournament
|
2
|
+
# Implements the single bracket elimination tournament system.
|
3
|
+
module SingleElimination
|
4
|
+
extend self
|
5
|
+
|
6
|
+
def generate(driver, options = {})
|
7
|
+
round = options[:round] || raise('Missing option :round')
|
8
|
+
|
9
|
+
teams = if driver.matches.empty?
|
10
|
+
seed_teams driver.seeded_teams, options
|
11
|
+
else
|
12
|
+
last_matches = driver.matches_for_round(round - 1)
|
13
|
+
get_match_winners driver, last_matches
|
14
|
+
end
|
15
|
+
|
16
|
+
create_matches driver, teams
|
17
|
+
end
|
18
|
+
|
19
|
+
def total_rounds(driver)
|
20
|
+
total_rounds_for_teams(driver.seeded_teams)
|
21
|
+
end
|
22
|
+
|
23
|
+
private
|
24
|
+
|
25
|
+
def seed_teams(teams, options)
|
26
|
+
padding = 2**total_rounds_for_teams(teams) - teams.length
|
27
|
+
teams = [nil] * padding + teams
|
28
|
+
|
29
|
+
seeder = options[:seeder] || Seeder::SingleBracket
|
30
|
+
seeder.seed teams
|
31
|
+
end
|
32
|
+
|
33
|
+
def total_rounds_for_teams(teams)
|
34
|
+
team_count = teams.length
|
35
|
+
|
36
|
+
Math.log2(team_count).ceil
|
37
|
+
end
|
38
|
+
|
39
|
+
def get_match_winners(driver, matches)
|
40
|
+
matches.map { |match| driver.get_match_winner(match) }
|
41
|
+
end
|
42
|
+
|
43
|
+
def create_matches(driver, teams)
|
44
|
+
teams.each_slice(2) do |slice|
|
45
|
+
next if slice.all?(&:nil?)
|
46
|
+
|
47
|
+
driver.create_match(slice[0], slice[1])
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
require 'tournament/swiss/common'
|
2
|
+
require 'tournament/swiss/dutch'
|
3
|
+
|
4
|
+
module Tournament
|
5
|
+
# Implements the swiss tournament system
|
6
|
+
module Swiss
|
7
|
+
extend self
|
8
|
+
|
9
|
+
def generate(driver, options = {})
|
10
|
+
pairer = options[:pairer] || Dutch
|
11
|
+
pairer_options = options[:pair_options] || {}
|
12
|
+
|
13
|
+
teams = seed_teams driver.ranked_teams, options
|
14
|
+
|
15
|
+
pairings = pairer.pair driver, teams, pairer_options
|
16
|
+
|
17
|
+
create_matches driver, pairings
|
18
|
+
end
|
19
|
+
|
20
|
+
def minimum_rounds(driver)
|
21
|
+
team_count = driver.seeded_teams.length
|
22
|
+
|
23
|
+
Math.log2(team_count).ceil
|
24
|
+
end
|
25
|
+
|
26
|
+
private
|
27
|
+
|
28
|
+
def seed_teams(teams, options)
|
29
|
+
seeder = options[:seeder] || Seeder::None
|
30
|
+
seeder.seed teams
|
31
|
+
end
|
32
|
+
|
33
|
+
def create_matches(driver, pairings)
|
34
|
+
pairings.each do |pair|
|
35
|
+
driver.create_match(pair[0], pair[1])
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
@@ -0,0 +1,110 @@
|
|
1
|
+
module Tournament
|
2
|
+
module Swiss
|
3
|
+
# Common functions for swiss pairing systems..\
|
4
|
+
module Common
|
5
|
+
extend self
|
6
|
+
|
7
|
+
# Iterate over each group, letting a team rollover into the next group
|
8
|
+
# if a group has an odd number of teams
|
9
|
+
def each_group_with_rollover(groups, group_keys)
|
10
|
+
group_keys.each_with_index do |key, index|
|
11
|
+
group = groups[key]
|
12
|
+
# Drop teams to next group get an even number
|
13
|
+
next_key = group_keys[index + 1]
|
14
|
+
groups[next_key] << group.pop if group.length.odd? && next_key
|
15
|
+
|
16
|
+
yield group
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
# Groups teams by the score given by the driver
|
21
|
+
def group_teams_by_score(driver, teams)
|
22
|
+
groups = teams.group_by { |team| driver.get_team_score team }
|
23
|
+
group_keys = groups.keys.sort.reverse.to_a
|
24
|
+
|
25
|
+
[groups, group_keys]
|
26
|
+
end
|
27
|
+
|
28
|
+
# Merges small groups to the right (if possible) such that all groups
|
29
|
+
# are larger than min_size.
|
30
|
+
# rubocop:disable Metrics/MethodLength :reek:TooManyStatements
|
31
|
+
def merge_small_groups(groups, group_keys, min_size)
|
32
|
+
new_keys = []
|
33
|
+
|
34
|
+
group_keys.each_with_index do |key, index|
|
35
|
+
group = groups[key]
|
36
|
+
|
37
|
+
# Merge small groups into an adjacent group
|
38
|
+
if group.length < min_size
|
39
|
+
groups.delete(key)
|
40
|
+
|
41
|
+
# When there is an adjacent lesser group, merge into that one
|
42
|
+
new_key = group_keys[index + 1]
|
43
|
+
if new_key
|
44
|
+
groups[new_key] = group + groups[new_key]
|
45
|
+
# If there isn't, merge into the adjacent greater group
|
46
|
+
else
|
47
|
+
new_key = group_keys[index - 1]
|
48
|
+
groups[new_key] += group
|
49
|
+
end
|
50
|
+
# Leave larger groups the way they are
|
51
|
+
else
|
52
|
+
new_keys << key
|
53
|
+
groups[key] = group
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
new_keys
|
58
|
+
end
|
59
|
+
# rubocop:enable Metrics/MethodLength, Metrics/AbcSize
|
60
|
+
|
61
|
+
# Get a set of already played matches. Matches are also sets
|
62
|
+
def matches_set(driver)
|
63
|
+
existing_matches = Set.new
|
64
|
+
driver.matches.each do |match|
|
65
|
+
match_teams = driver.get_match_teams match
|
66
|
+
existing_matches.add Set.new match_teams
|
67
|
+
end
|
68
|
+
existing_matches
|
69
|
+
end
|
70
|
+
|
71
|
+
# Check whether any match has already been played
|
72
|
+
def any_match_exists?(matches, existing_matches)
|
73
|
+
matches.any? { |match| existing_matches.include?(Set.new(match)) }
|
74
|
+
end
|
75
|
+
|
76
|
+
# Count the number of matches already played
|
77
|
+
def count_existing_matches(matches, existing_matches)
|
78
|
+
matches.count { |match| existing_matches.include?(Set.new(match)) }
|
79
|
+
end
|
80
|
+
|
81
|
+
# Finds the first permutation of teams that has a unique pairing.
|
82
|
+
# If none are found, the pairing that has the least duplicate matches
|
83
|
+
# is returned.
|
84
|
+
# rubocop:disable Metrics/MethodLength
|
85
|
+
def first_permutation_pairing(teams, existing_matches)
|
86
|
+
min_dups = Float::INFINITY
|
87
|
+
best_matches = nil
|
88
|
+
|
89
|
+
# Find the first permutation that has no duplicate matches
|
90
|
+
# Or the permutation with the least duplicate matches
|
91
|
+
teams.permutation.each do |variation|
|
92
|
+
matches = (yield variation).to_a
|
93
|
+
dup_count = count_existing_matches(matches, existing_matches)
|
94
|
+
|
95
|
+
# Quick exit when there are no duplicates
|
96
|
+
return matches if dup_count.zero?
|
97
|
+
|
98
|
+
# Update best stats as we go along
|
99
|
+
if dup_count < min_dups
|
100
|
+
min_dups = dup_count
|
101
|
+
best_matches = matches
|
102
|
+
end
|
103
|
+
end
|
104
|
+
# rubocop:enable Metrics/MethodLength
|
105
|
+
|
106
|
+
best_matches
|
107
|
+
end
|
108
|
+
end
|
109
|
+
end
|
110
|
+
end
|
@@ -0,0 +1,62 @@
|
|
1
|
+
module Tournament
|
2
|
+
module Swiss
|
3
|
+
# A simplified Dutch pairing system implementation.
|
4
|
+
module Dutch
|
5
|
+
extend self
|
6
|
+
extend Common
|
7
|
+
|
8
|
+
def pair(driver, teams, options = {})
|
9
|
+
return dutch_pairing(teams) if driver.matches.empty?
|
10
|
+
|
11
|
+
groups, group_keys = group_teams_by_score(driver, teams)
|
12
|
+
|
13
|
+
min_pair_size = options[:min_pair_size] || 4
|
14
|
+
group_keys = merge_small_groups(groups, group_keys, min_pair_size)
|
15
|
+
|
16
|
+
pair_groups driver, groups, group_keys
|
17
|
+
end
|
18
|
+
|
19
|
+
private
|
20
|
+
|
21
|
+
def dutch_pairing(teams)
|
22
|
+
half = teams.length / 2
|
23
|
+
top = teams[0...half]
|
24
|
+
bottom = teams[half..-1]
|
25
|
+
top << nil if top.length < bottom.length
|
26
|
+
|
27
|
+
top.zip(bottom).to_a
|
28
|
+
end
|
29
|
+
|
30
|
+
def pair_groups(driver, groups, group_keys)
|
31
|
+
existing_matches = matches_set(driver)
|
32
|
+
|
33
|
+
matches = []
|
34
|
+
each_group_with_rollover(groups, group_keys) do |group|
|
35
|
+
matches += pair_group(group, existing_matches)
|
36
|
+
end
|
37
|
+
|
38
|
+
matches
|
39
|
+
end
|
40
|
+
|
41
|
+
def pair_group(group, existing_matches)
|
42
|
+
pairs = dutch_pairing(group)
|
43
|
+
|
44
|
+
if any_match_exists?(pairs, existing_matches)
|
45
|
+
first_permutation_pairing(group, existing_matches) do |perm_pairs|
|
46
|
+
dutch_pairing(perm_pairs)
|
47
|
+
end
|
48
|
+
else
|
49
|
+
pairs
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
def fix_matches(teams, pairs, existing_matches)
|
54
|
+
if any_match_exists?(pairs, existing_matches)
|
55
|
+
first_permutation_pairing(teams, existing_matches)
|
56
|
+
else
|
57
|
+
pairs
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
|
3
|
+
lib = File.expand_path('../lib', __FILE__)
|
4
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
5
|
+
require 'tournament-system'
|
6
|
+
|
7
|
+
Gem::Specification.new do |spec|
|
8
|
+
spec.name = 'tournament-system'
|
9
|
+
spec.version = Tournament::VERSION
|
10
|
+
spec.authors = ['Benjamin Schaaf']
|
11
|
+
spec.email = ['ben.schaaf@gmail.com']
|
12
|
+
|
13
|
+
spec.summary = 'Implements various tournament systems'
|
14
|
+
# TODO: Write a description
|
15
|
+
# spec.description =
|
16
|
+
spec.homepage = 'https://github.com/ozfortress/tournament-system'
|
17
|
+
spec.license = 'MIT'
|
18
|
+
|
19
|
+
spec.files = `git ls-files -z`.split("\x0").reject do |f|
|
20
|
+
f.match(%r{^(test|spec|features)/})
|
21
|
+
end
|
22
|
+
spec.bindir = 'exe'
|
23
|
+
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
24
|
+
spec.require_paths = ['lib']
|
25
|
+
|
26
|
+
spec.add_development_dependency 'bundler', '~> 1.14'
|
27
|
+
end
|
metadata
ADDED
@@ -0,0 +1,81 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: tournament-system
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Benjamin Schaaf
|
8
|
+
autorequire:
|
9
|
+
bindir: exe
|
10
|
+
cert_chain: []
|
11
|
+
date: 2017-03-13 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: bundler
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '1.14'
|
20
|
+
type: :development
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '1.14'
|
27
|
+
description:
|
28
|
+
email:
|
29
|
+
- ben.schaaf@gmail.com
|
30
|
+
executables: []
|
31
|
+
extensions: []
|
32
|
+
extra_rdoc_files: []
|
33
|
+
files:
|
34
|
+
- ".gitignore"
|
35
|
+
- ".reek"
|
36
|
+
- ".rspec"
|
37
|
+
- ".rubocop.yml"
|
38
|
+
- ".travis.yml"
|
39
|
+
- Gemfile
|
40
|
+
- LICENSE
|
41
|
+
- README.md
|
42
|
+
- Rakefile
|
43
|
+
- lib/tournament-system.rb
|
44
|
+
- lib/tournament/driver.rb
|
45
|
+
- lib/tournament/page_playoff.rb
|
46
|
+
- lib/tournament/round_robin.rb
|
47
|
+
- lib/tournament/seeder.rb
|
48
|
+
- lib/tournament/seeder/none.rb
|
49
|
+
- lib/tournament/seeder/random.rb
|
50
|
+
- lib/tournament/seeder/single_bracket.rb
|
51
|
+
- lib/tournament/single_elimination.rb
|
52
|
+
- lib/tournament/swiss.rb
|
53
|
+
- lib/tournament/swiss/common.rb
|
54
|
+
- lib/tournament/swiss/dutch.rb
|
55
|
+
- lib/tournament/version.rb
|
56
|
+
- tournament-system.gemspec
|
57
|
+
homepage: https://github.com/ozfortress/tournament-system
|
58
|
+
licenses:
|
59
|
+
- MIT
|
60
|
+
metadata: {}
|
61
|
+
post_install_message:
|
62
|
+
rdoc_options: []
|
63
|
+
require_paths:
|
64
|
+
- lib
|
65
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
66
|
+
requirements:
|
67
|
+
- - ">="
|
68
|
+
- !ruby/object:Gem::Version
|
69
|
+
version: '0'
|
70
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
71
|
+
requirements:
|
72
|
+
- - ">="
|
73
|
+
- !ruby/object:Gem::Version
|
74
|
+
version: '0'
|
75
|
+
requirements: []
|
76
|
+
rubyforge_project:
|
77
|
+
rubygems_version: 2.6.10
|
78
|
+
signing_key:
|
79
|
+
specification_version: 4
|
80
|
+
summary: Implements various tournament systems
|
81
|
+
test_files: []
|