tournament-system 0.1.4 → 0.1.5
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/lib/tournament/algorithm/page_playoff.rb +34 -0
- data/lib/tournament/algorithm/pairers/adjacent.rb +21 -0
- data/lib/tournament/algorithm/pairers/best_min_duplicates.rb +55 -0
- data/lib/tournament/algorithm/pairers/halves.rb +32 -0
- data/lib/tournament/algorithm/pairers/multi.rb +33 -0
- data/lib/tournament/algorithm/pairers.rb +7 -0
- data/lib/tournament/algorithm/round_robin.rb +60 -0
- data/lib/tournament/algorithm/single_bracket.rb +104 -0
- data/lib/tournament/algorithm/swiss.rb +83 -0
- data/lib/tournament/algorithm/util.rb +100 -0
- data/lib/tournament/algorithm.rb +6 -0
- data/lib/tournament/driver.rb +100 -9
- data/lib/tournament/page_playoff.rb +21 -18
- data/lib/tournament/round_robin.rb +27 -33
- data/lib/tournament/single_elimination.rb +23 -44
- data/lib/tournament/swiss/dutch.rb +39 -37
- data/lib/tournament/swiss.rb +16 -21
- data/lib/tournament/version.rb +2 -1
- data/lib/tournament-system.rb +0 -2
- data/lib/tournament.rb +6 -0
- metadata +14 -7
- data/lib/tournament/seeder/none.rb +0 -12
- data/lib/tournament/seeder/random.rb +0 -14
- data/lib/tournament/seeder/single_bracket.rb +0 -39
- data/lib/tournament/seeder.rb +0 -10
- data/lib/tournament/swiss/common.rb +0 -118
@@ -1,61 +1,63 @@
|
|
1
|
+
require 'tournament/algorithm/swiss'
|
2
|
+
require 'tournament/algorithm/pairers/multi'
|
3
|
+
require 'tournament/algorithm/pairers/halves'
|
4
|
+
require 'tournament/algorithm/pairers/best_min_duplicates'
|
5
|
+
|
1
6
|
module Tournament
|
2
7
|
module Swiss
|
3
8
|
# A simplified Dutch pairing system implementation.
|
4
9
|
module Dutch
|
5
10
|
extend self
|
6
|
-
extend Common
|
7
11
|
|
8
|
-
|
9
|
-
|
12
|
+
# Pair teams using dutch pairing for a swiss system tournament.
|
13
|
+
#
|
14
|
+
# @param driver [Driver]
|
15
|
+
# @option options [Integer] min_pair_size see
|
16
|
+
# {Algorithm::Swiss#merge_small_groups for more details}
|
17
|
+
# @return [Array<Array(team, team)>] the generated pairs of teams
|
18
|
+
def pair(driver, options = {})
|
19
|
+
teams = driver.ranked_teams
|
20
|
+
|
21
|
+
# Special padding such that the bottom team gets a BYE
|
22
|
+
teams.insert(teams.length / 2, nil) if teams.length.odd?
|
10
23
|
|
11
|
-
|
24
|
+
scores = driver.scores_hash
|
25
|
+
groups = Algorithm::Swiss.group_teams_by_score(teams, scores)
|
12
26
|
|
13
27
|
min_pair_size = options[:min_pair_size] || 4
|
14
|
-
|
28
|
+
Algorithm::Swiss.merge_small_groups(groups, min_pair_size)
|
15
29
|
|
16
|
-
|
30
|
+
Algorithm::Swiss.rollover_groups(groups)
|
31
|
+
|
32
|
+
pair_groups driver, groups
|
17
33
|
end
|
18
34
|
|
19
35
|
private
|
20
36
|
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
top = teams[0...half]
|
25
|
-
bottom = teams[half..-1]
|
26
|
-
top << nil if top.length < bottom.length
|
37
|
+
def pairer_funcs(driver, group)
|
38
|
+
scores = driver.scores_hash
|
39
|
+
matches = driver.matches_hash
|
27
40
|
|
28
|
-
|
41
|
+
[
|
42
|
+
-> () { Algorithm::Pairers::Halves.pair(group) },
|
43
|
+
lambda do
|
44
|
+
Algorithm::Pairers::BestMinDuplicates.pair(group, scores, matches)
|
45
|
+
end,
|
46
|
+
]
|
29
47
|
end
|
30
48
|
|
31
|
-
def pair_groups(driver, groups
|
32
|
-
|
33
|
-
|
34
|
-
matches = []
|
35
|
-
each_group_with_rollover(groups, group_keys) do |group|
|
36
|
-
matches += pair_group(group, existing_matches)
|
37
|
-
end
|
38
|
-
|
39
|
-
matches
|
49
|
+
def pair_groups(driver, groups)
|
50
|
+
groups.map { |group| pair_group(driver, group) }.reduce(:+)
|
40
51
|
end
|
41
52
|
|
42
|
-
def pair_group(
|
43
|
-
|
53
|
+
def pair_group(driver, group)
|
54
|
+
Algorithm::Pairers::Multi.pair(pairer_funcs(driver, group)) do |matches|
|
55
|
+
duplicates = driver.count_duplicate_matches(matches)
|
44
56
|
|
45
|
-
|
46
|
-
|
47
|
-
dutch_pairing(perm_pairs)
|
48
|
-
end
|
49
|
-
else
|
50
|
-
pairs
|
51
|
-
end
|
52
|
-
end
|
57
|
+
# Early return when there are no duplicates, prefer earlier pairers
|
58
|
+
return matches if duplicates.zero?
|
53
59
|
|
54
|
-
|
55
|
-
if any_match_exists?(pairs, existing_matches)
|
56
|
-
first_permutation_pairing(teams, existing_matches)
|
57
|
-
else
|
58
|
-
pairs
|
60
|
+
-duplicates
|
59
61
|
end
|
60
62
|
end
|
61
63
|
end
|
data/lib/tournament/swiss.rb
CHANGED
@@ -1,4 +1,4 @@
|
|
1
|
-
require 'tournament/swiss
|
1
|
+
require 'tournament/algorithm/swiss'
|
2
2
|
require 'tournament/swiss/dutch'
|
3
3
|
|
4
4
|
module Tournament
|
@@ -6,34 +6,29 @@ module Tournament
|
|
6
6
|
module Swiss
|
7
7
|
extend self
|
8
8
|
|
9
|
+
# Generate matches with the given driver.
|
10
|
+
#
|
11
|
+
# @param driver [Driver]
|
12
|
+
# @option options [Pairer] pairer the pairing system to use, defaults to
|
13
|
+
# {Dutch}
|
14
|
+
# @option options [Hash] pair_options options for the chosen pairing system,
|
15
|
+
# see {Dutch} for more details
|
16
|
+
# @return [void]
|
9
17
|
def generate(driver, options = {})
|
10
18
|
pairer = options[:pairer] || Dutch
|
11
19
|
pairer_options = options[:pair_options] || {}
|
12
20
|
|
13
|
-
|
21
|
+
pairings = pairer.pair(driver, pairer_options)
|
14
22
|
|
15
|
-
pairings
|
16
|
-
|
17
|
-
create_matches driver, pairings
|
23
|
+
driver.create_matches(pairings)
|
18
24
|
end
|
19
25
|
|
26
|
+
# The minimum number of rounds to determine a winner.
|
27
|
+
#
|
28
|
+
# @param driver [Driver]
|
29
|
+
# @return [Integer]
|
20
30
|
def minimum_rounds(driver)
|
21
|
-
|
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
|
31
|
+
Algorithm::Swiss.minimum_rounds(driver.seeded_teams.length)
|
37
32
|
end
|
38
33
|
end
|
39
34
|
end
|
data/lib/tournament/version.rb
CHANGED
data/lib/tournament-system.rb
CHANGED
data/lib/tournament.rb
ADDED
@@ -0,0 +1,6 @@
|
|
1
|
+
# This library is split into two parts, there's the actual algorithms that
|
2
|
+
# implement various tournament systems ({Algorithm}) and a data abstraction
|
3
|
+
# layer for generating matches using various tournament systems in a
|
4
|
+
# data-independent way ({Driver}).
|
5
|
+
module Tournament
|
6
|
+
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: tournament-system
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.1.
|
4
|
+
version: 0.1.5
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Benjamin Schaaf
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2017-
|
11
|
+
date: 2017-07-07 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: bundler
|
@@ -41,16 +41,23 @@ files:
|
|
41
41
|
- README.md
|
42
42
|
- Rakefile
|
43
43
|
- lib/tournament-system.rb
|
44
|
+
- lib/tournament.rb
|
45
|
+
- lib/tournament/algorithm.rb
|
46
|
+
- lib/tournament/algorithm/page_playoff.rb
|
47
|
+
- lib/tournament/algorithm/pairers.rb
|
48
|
+
- lib/tournament/algorithm/pairers/adjacent.rb
|
49
|
+
- lib/tournament/algorithm/pairers/best_min_duplicates.rb
|
50
|
+
- lib/tournament/algorithm/pairers/halves.rb
|
51
|
+
- lib/tournament/algorithm/pairers/multi.rb
|
52
|
+
- lib/tournament/algorithm/round_robin.rb
|
53
|
+
- lib/tournament/algorithm/single_bracket.rb
|
54
|
+
- lib/tournament/algorithm/swiss.rb
|
55
|
+
- lib/tournament/algorithm/util.rb
|
44
56
|
- lib/tournament/driver.rb
|
45
57
|
- lib/tournament/page_playoff.rb
|
46
58
|
- 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
59
|
- lib/tournament/single_elimination.rb
|
52
60
|
- lib/tournament/swiss.rb
|
53
|
-
- lib/tournament/swiss/common.rb
|
54
61
|
- lib/tournament/swiss/dutch.rb
|
55
62
|
- lib/tournament/version.rb
|
56
63
|
- tournament-system.gemspec
|
@@ -1,39 +0,0 @@
|
|
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
|
-
unless (Math.log2(teams.length) % 1).zero?
|
11
|
-
raise ArgumentError, 'Need power-of-2 teams'
|
12
|
-
end
|
13
|
-
|
14
|
-
teams = teams.map.with_index { |team, index| SeedTeam.new(team, index) }
|
15
|
-
seed_bracket(teams).map(&:team)
|
16
|
-
end
|
17
|
-
|
18
|
-
private
|
19
|
-
|
20
|
-
# Structure for wrapping a team with it's seed index
|
21
|
-
SeedTeam = Struct.new(:team, :index)
|
22
|
-
|
23
|
-
# Recursively seed the top half of the teams
|
24
|
-
# and match teams reversed by index to the bottom half
|
25
|
-
def seed_bracket(teams)
|
26
|
-
return teams if teams.length <= 2
|
27
|
-
|
28
|
-
top_half, bottom_half = teams.each_slice(teams.length / 2).to_a
|
29
|
-
top_half = seed top_half
|
30
|
-
|
31
|
-
top_half.map do |team|
|
32
|
-
match = bottom_half[-team.index - 1]
|
33
|
-
|
34
|
-
[team, match]
|
35
|
-
end.flatten
|
36
|
-
end
|
37
|
-
end
|
38
|
-
end
|
39
|
-
end
|
data/lib/tournament/seeder.rb
DELETED
@@ -1,10 +0,0 @@
|
|
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
|
@@ -1,118 +0,0 @@
|
|
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, Metrics/AbcSize
|
31
|
-
# :reek:TooManyStatements
|
32
|
-
def merge_small_groups(groups, group_keys, min_size)
|
33
|
-
new_keys = []
|
34
|
-
|
35
|
-
group_keys.each_with_index do |key, index|
|
36
|
-
group = groups[key]
|
37
|
-
|
38
|
-
# Merge small groups into an adjacent group
|
39
|
-
if group.length < min_size
|
40
|
-
groups.delete(key)
|
41
|
-
|
42
|
-
# When there is an adjacent lesser group, merge into that one
|
43
|
-
new_key = group_keys[index + 1]
|
44
|
-
if new_key
|
45
|
-
groups[new_key] = group + groups[new_key]
|
46
|
-
# If there isn't, merge into the adjacent greater group
|
47
|
-
else
|
48
|
-
new_key = new_keys[-1]
|
49
|
-
|
50
|
-
if new_key
|
51
|
-
groups[new_key] += group
|
52
|
-
else
|
53
|
-
# If there are no new keys just use the current key
|
54
|
-
new_keys << key
|
55
|
-
groups[key] = group
|
56
|
-
end
|
57
|
-
end
|
58
|
-
# Leave larger groups the way they are
|
59
|
-
else
|
60
|
-
new_keys << key
|
61
|
-
groups[key] = group
|
62
|
-
end
|
63
|
-
end
|
64
|
-
|
65
|
-
new_keys
|
66
|
-
end
|
67
|
-
# rubocop:enable Metrics/MethodLength, Metrics/AbcSize
|
68
|
-
|
69
|
-
# Get a set of already played matches. Matches are also sets
|
70
|
-
def matches_set(driver)
|
71
|
-
existing_matches = Hash.new(0)
|
72
|
-
driver.matches.each do |match|
|
73
|
-
match_teams = Set.new driver.get_match_teams match
|
74
|
-
existing_matches[match_teams] += 1
|
75
|
-
end
|
76
|
-
existing_matches
|
77
|
-
end
|
78
|
-
|
79
|
-
# Check whether any match has already been played
|
80
|
-
def any_match_exists?(matches, existing_matches)
|
81
|
-
matches.any? { |match| existing_matches.include?(Set.new(match)) }
|
82
|
-
end
|
83
|
-
|
84
|
-
# Count the number of matches already played
|
85
|
-
def count_existing_matches(matches, existing_matches)
|
86
|
-
matches.map { |match| existing_matches[Set.new(match)] }.reduce(:+)
|
87
|
-
end
|
88
|
-
|
89
|
-
# Finds the first permutation of teams that has a unique pairing.
|
90
|
-
# If none are found, the pairing that has the least duplicate matches
|
91
|
-
# is returned.
|
92
|
-
# rubocop:disable Metrics/MethodLength
|
93
|
-
def first_permutation_pairing(teams, existing_matches)
|
94
|
-
min_dups = Float::INFINITY
|
95
|
-
best_matches = nil
|
96
|
-
|
97
|
-
# Find the first permutation that has no duplicate matches
|
98
|
-
# Or the permutation with the least duplicate matches
|
99
|
-
teams.permutation.each do |variation|
|
100
|
-
matches = (yield variation).to_a
|
101
|
-
dup_count = count_existing_matches(matches, existing_matches)
|
102
|
-
|
103
|
-
# Quick exit when there are no duplicates
|
104
|
-
return matches if dup_count.zero?
|
105
|
-
|
106
|
-
# Update best stats as we go along
|
107
|
-
if dup_count < min_dups
|
108
|
-
min_dups = dup_count
|
109
|
-
best_matches = matches
|
110
|
-
end
|
111
|
-
end
|
112
|
-
|
113
|
-
best_matches
|
114
|
-
end
|
115
|
-
# rubocop:enable Metrics/MethodLength
|
116
|
-
end
|
117
|
-
end
|
118
|
-
end
|