tournament-system 0.2.1 → 1.0.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 +4 -4
- data/.reek +3 -0
- data/.rubocop.yml +3 -0
- data/README.md +4 -4
- data/lib/tournament/algorithm/group_pairing.rb +66 -0
- data/lib/tournament/algorithm/matching.rb +105 -0
- data/lib/tournament/algorithm/round_robin.rb +3 -3
- data/lib/tournament/algorithm/single_bracket.rb +1 -1
- data/lib/tournament/algorithm/swiss.rb +4 -39
- data/lib/tournament/algorithm/util.rb +0 -41
- data/lib/tournament/driver.rb +14 -19
- data/lib/tournament/page_playoff.rb +2 -2
- data/lib/tournament/round_robin.rb +1 -1
- data/lib/tournament/single_elimination.rb +3 -3
- data/lib/tournament/swiss.rb +2 -2
- data/lib/tournament/swiss/dutch.rb +80 -30
- data/lib/tournament/version.rb +1 -1
- data/tournament-system.gemspec +1 -0
- metadata +18 -7
- data/lib/tournament/algorithm/pairers.rb +0 -7
- data/lib/tournament/algorithm/pairers/adjacent.rb +0 -21
- data/lib/tournament/algorithm/pairers/best_min_duplicates.rb +0 -55
- data/lib/tournament/algorithm/pairers/halves.rb +0 -32
- data/lib/tournament/algorithm/pairers/multi.rb +0 -33
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 768d7d740fb6a7c2bec21c85c8d9267029874025
|
4
|
+
data.tar.gz: f4ecb036fee09e7ce76acf9fb459285751adf480
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 2c7045cb22a5097dfbc2e1823750fd5e2a13e445dffa634c5ce5c96bcd922d34e73e8698d6983749e74012c0f172b7e93ebcc5bfdc1ec17b7eb434bece7af8da
|
7
|
+
data.tar.gz: 7a89d1d96e43e02f60c9e123a6f799ddc0f7970d6b011973e97ce2232e27ac2612fd95ca473075cca28882bc81774185b526dd1a8ca4a74213b982f95d08948e
|
data/.reek
CHANGED
data/.rubocop.yml
CHANGED
data/README.md
CHANGED
@@ -14,7 +14,7 @@ It is designed to easily fit into any memory model you might already have.
|
|
14
14
|
Add this line to your application's Gemfile:
|
15
15
|
|
16
16
|
```ruby
|
17
|
-
gem 'tournament-system'
|
17
|
+
gem 'tournament-system', '~> 1.0.0'
|
18
18
|
```
|
19
19
|
|
20
20
|
And then execute:
|
@@ -78,10 +78,10 @@ Tournament::SingleElimination.generate driver
|
|
78
78
|
# Generate a round for a round robin tournament
|
79
79
|
Tournament::RoundRobin.generate driver
|
80
80
|
|
81
|
-
# Generate a round for a swiss system tournament
|
82
|
-
#
|
81
|
+
# Generate a round for a swiss system tournament, pushing byes to the bottom
|
82
|
+
# half (bottom half teams will bye before the top half)
|
83
83
|
Tournament::Swiss.generate driver, pairer: Tournament::Swiss::Dutch,
|
84
|
-
pair_options: {
|
84
|
+
pair_options: { push_byes_to: :bottom_half }
|
85
85
|
|
86
86
|
# Generate a round for a page playoff system, with an optional bronze match
|
87
87
|
Tournament::PagePlayoff.generate driver, bronze_match: true
|
@@ -0,0 +1,66 @@
|
|
1
|
+
module Tournament
|
2
|
+
module Algorithm
|
3
|
+
# This module provides group pairing algorithms
|
4
|
+
module GroupPairing
|
5
|
+
extend self
|
6
|
+
|
7
|
+
# Adjacent pairing (aka. King Of The Hill pairing)
|
8
|
+
#
|
9
|
+
# Pair adjacent teams.
|
10
|
+
#
|
11
|
+
# @example
|
12
|
+
# adjacent([1, 2, 3, 4]) #=> [[1, 2], [3, 4]]
|
13
|
+
#
|
14
|
+
# @param teams [Array<team>]
|
15
|
+
# @return [Array<Array(team, team)>]
|
16
|
+
def adjacent(teams)
|
17
|
+
teams.each_slice(2).to_a
|
18
|
+
end
|
19
|
+
|
20
|
+
# Fold pairing (aka. Slaughter pairing)
|
21
|
+
#
|
22
|
+
# Pair the top team with the bottom team
|
23
|
+
#
|
24
|
+
# @example
|
25
|
+
# fold([1, 2, 3, 4]) #=> [[1, 4], [2, 3]]
|
26
|
+
#
|
27
|
+
# @param teams [Array<team>]
|
28
|
+
# @return [Array<Array(team, team)>]
|
29
|
+
def fold(teams)
|
30
|
+
top, bottom = teams.each_slice(teams.length / 2).to_a
|
31
|
+
|
32
|
+
bottom.reverse!
|
33
|
+
|
34
|
+
top.zip(bottom).to_a
|
35
|
+
end
|
36
|
+
|
37
|
+
# Slide pairing (aka cross pairing).
|
38
|
+
#
|
39
|
+
# Pair the top half of teams with the bottom half, respectively.
|
40
|
+
#
|
41
|
+
# @example
|
42
|
+
# pair([1, 2, 3, 4]) #=> [[1, 3], [2, 4]]
|
43
|
+
#
|
44
|
+
# @param teams [Array<team>]
|
45
|
+
# @return [Array<Array(team, team)>]
|
46
|
+
def slide(teams)
|
47
|
+
top, bottom = teams.each_slice(teams.length / 2).to_a
|
48
|
+
|
49
|
+
top.zip(bottom).to_a
|
50
|
+
end
|
51
|
+
|
52
|
+
# Random pairing
|
53
|
+
#
|
54
|
+
# Pair teams randomly.
|
55
|
+
#
|
56
|
+
# @example
|
57
|
+
# pair([1, 2, 3, 4, 5, 6]) #=> [[1, 4], [2, 6], [3, 5]]
|
58
|
+
#
|
59
|
+
# @param teams [Array<team>]
|
60
|
+
# @return [Array<Array(team, team)>]
|
61
|
+
def random(teams)
|
62
|
+
adjacent(teams.shuffle)
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
@@ -0,0 +1,105 @@
|
|
1
|
+
require 'graph_matching'
|
2
|
+
|
3
|
+
module Tournament
|
4
|
+
module Algorithm
|
5
|
+
# Implements graph matching algorithms for tournament systems.
|
6
|
+
module Matching
|
7
|
+
extend self
|
8
|
+
|
9
|
+
# rubocop:disable Metrics/MethodLength
|
10
|
+
# :reek:NestedIterators
|
11
|
+
|
12
|
+
# Iterate all perfect matchings of a specific size. A perfect matchings is a unique grouping of all elements with
|
13
|
+
# a specific group size.
|
14
|
+
#
|
15
|
+
# Note that iterating all perfect matchings can be expensive. The total amount of matchings for size +n+ is given
|
16
|
+
# by +(n - 1)!!+, a double factorial.
|
17
|
+
#
|
18
|
+
# @example
|
19
|
+
# all_perfect_matchings([1, 2, 3, 4]).includes?([[1, 2], [3, 4]]) #=> true
|
20
|
+
#
|
21
|
+
# @param array [Array<element>]
|
22
|
+
# @overload all_perfect_matchings(array, size)
|
23
|
+
# @return [Enumerator<Array<element>>] enumerator for all perfect matches
|
24
|
+
# @overload all_perfect_matchings(array, size) { |group| ... }
|
25
|
+
# @yieldparam group [Array(element, size)] a group of elements
|
26
|
+
# @return [nil]
|
27
|
+
def all_perfect_matchings(array)
|
28
|
+
return to_enum(:all_perfect_matchings, array) unless block_given?
|
29
|
+
|
30
|
+
if array.empty?
|
31
|
+
yield []
|
32
|
+
return
|
33
|
+
end
|
34
|
+
|
35
|
+
array[1..-1].combination(1) do |group|
|
36
|
+
group.unshift array[0]
|
37
|
+
|
38
|
+
remaining = array.reject { |element| group.include?(element) }
|
39
|
+
all_perfect_matchings(remaining) do |groups|
|
40
|
+
yield [group] + groups
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
# rubbocop:enable Metrics/MethodLength
|
45
|
+
|
46
|
+
# Performs maximum weight perfect matching of a undirected complete graph composed of the given vertices.
|
47
|
+
#
|
48
|
+
# The block is called for every edge in the complete graph.
|
49
|
+
#
|
50
|
+
# Implementation performs in +O(v^3 log v)+.
|
51
|
+
#
|
52
|
+
# @param array [Array<element>]
|
53
|
+
# @yieldparam first [element] The first element of an edge to compute the weight of.
|
54
|
+
# @yieldparam second [element] The second element of an edge to compute the weight of.
|
55
|
+
# @return [Array<Array(element, element)>] A perfect matching with maximum weight
|
56
|
+
def maximum_weight_perfect_matching(array, &block)
|
57
|
+
edges = create_complete_graph(array, &block)
|
58
|
+
graph = GraphMatching::Graph::WeightedGraph[*edges]
|
59
|
+
|
60
|
+
# Get the maximum weighted maximum cardinality matching of the complete graph
|
61
|
+
matching = graph.maximum_weighted_matching(true)
|
62
|
+
|
63
|
+
# Converted matching back to input values (remember, indecies start from 1 in this case)
|
64
|
+
matching.edges.map do |edge|
|
65
|
+
edge.map do |index|
|
66
|
+
array[index - 1]
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
# Identical to {#maximum_weight_perfect_matching}, except instead of maximizing weight it minimizes it.
|
72
|
+
#
|
73
|
+
# This is simply achieved by negating the weight and then maximizing that.
|
74
|
+
#
|
75
|
+
# @param array [Array<element>]
|
76
|
+
# @yieldparam first [element] The first element of an edge to compute the weight of.
|
77
|
+
# @yieldparam second [element] The second element of an edge to compute the weight of.
|
78
|
+
# @return [Array<Array(element, element)>] A perfect matching with minimum weight
|
79
|
+
def minimum_weight_perfect_matching(array)
|
80
|
+
maximum_weight_perfect_matching(array) do |first, second|
|
81
|
+
-yield(first, second)
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
private
|
86
|
+
|
87
|
+
# Construct a complete graph from element indecies (starting from 1)
|
88
|
+
def create_complete_graph(array)
|
89
|
+
edges = []
|
90
|
+
|
91
|
+
array.each.with_index do |first, first_index|
|
92
|
+
next_index = first_index + 1
|
93
|
+
|
94
|
+
array[next_index..array.length].each.with_index do |second, second_index|
|
95
|
+
second_index += next_index
|
96
|
+
|
97
|
+
edges << [next_index, second_index + 1, yield(first, second)]
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
edges
|
102
|
+
end
|
103
|
+
end
|
104
|
+
end
|
105
|
+
end
|
@@ -1,5 +1,5 @@
|
|
1
1
|
require 'tournament/algorithm/util'
|
2
|
-
require 'tournament/algorithm/
|
2
|
+
require 'tournament/algorithm/group_pairing'
|
3
3
|
|
4
4
|
module Tournament
|
5
5
|
module Algorithm
|
@@ -45,7 +45,7 @@ module Tournament
|
|
45
45
|
|
46
46
|
# Rotates teams and pairs them for a round of round robin.
|
47
47
|
#
|
48
|
-
# Uses {
|
48
|
+
# Uses {GroupPairing#fold} for pairing after rotating.
|
49
49
|
#
|
50
50
|
# @param teams [Array<team>] teams playing
|
51
51
|
# @param round [Integer] the round number
|
@@ -53,7 +53,7 @@ module Tournament
|
|
53
53
|
def round_robin_pairing(teams, round)
|
54
54
|
rotated = round_robin(teams, round)
|
55
55
|
|
56
|
-
|
56
|
+
GroupPairing.fold(rotated)
|
57
57
|
end
|
58
58
|
end
|
59
59
|
end
|
@@ -66,7 +66,7 @@ module Tournament
|
|
66
66
|
# ones, the first team plays the second in the finals, the 3rd and 4th get
|
67
67
|
# nocked out in the semi-finals, etc.
|
68
68
|
#
|
69
|
-
# Designed to be used with {
|
69
|
+
# Designed to be used with {GroupPairing#adjacent}.
|
70
70
|
#
|
71
71
|
# @param teams [Array<Team>]
|
72
72
|
# @return [Array<team>]
|
@@ -1,14 +1,11 @@
|
|
1
1
|
module Tournament
|
2
2
|
module Algorithm
|
3
|
-
# This module provides algorithms for dealing with swiss tournament systems.
|
4
|
-
#
|
5
|
-
#
|
6
|
-
# @see Pairers Pairers for possible use with swiss.
|
3
|
+
# This module provides algorithms for dealing with swiss tournament systems. Specifically it provides algorithms for
|
4
|
+
# grouping teams.
|
7
5
|
module Swiss
|
8
6
|
extend self
|
9
7
|
|
10
|
-
# Calculates the minimum number of rounds needed to properly order teams
|
11
|
-
# using the swiss tournament system.
|
8
|
+
# Calculates the minimum number of rounds needed to properly order teams using the swiss tournament system.
|
12
9
|
#
|
13
10
|
# @param teams_count [Integer] the number of teams
|
14
11
|
# @return [Integer]
|
@@ -29,44 +26,12 @@ module Tournament
|
|
29
26
|
sorted_keys.map { |key| groups[key] }
|
30
27
|
end
|
31
28
|
|
32
|
-
# rubocop:disable Metrics/AbcSize, Metrics/MethodLength
|
33
|
-
# :reek:DuplicateMethodCall
|
34
|
-
|
35
|
-
# Merge small groups to the right (if possible) such that all groups
|
36
|
-
# are larger than min_size.
|
37
|
-
# Merging is performed in-place.
|
38
|
-
#
|
39
|
-
# @param groups [Array<Array<team>>] groups of teams
|
40
|
-
# @param min_size [Integer] the minimum size of groups
|
41
|
-
# @return [void]
|
42
|
-
def merge_small_groups(groups, min_size)
|
43
|
-
# Iterate all groups up until the last
|
44
|
-
index = 0
|
45
|
-
while index < groups.length - 1
|
46
|
-
# Merge group to the right until this group is large enough
|
47
|
-
while groups[index].length < min_size && index + 1 < groups.length
|
48
|
-
groups[index] += groups.delete_at(index + 1)
|
49
|
-
end
|
50
|
-
|
51
|
-
index += 1
|
52
|
-
end
|
53
|
-
|
54
|
-
# Merge the last group to the left if its too small
|
55
|
-
if groups[-1].length < min_size && groups.length != 1
|
56
|
-
last = groups.delete_at(-1)
|
57
|
-
groups[-1] += last
|
58
|
-
end
|
59
|
-
|
60
|
-
nil
|
61
|
-
end
|
62
|
-
# rubocop:enable Metrics/AbcSize, Metrics/MethodLength
|
63
|
-
|
64
29
|
# Rollover the last person in each group if its odd.
|
65
30
|
# This assumes that the total number of players in all groups is even.
|
66
31
|
# Rollover is performed in-place.
|
67
32
|
#
|
68
33
|
# @param groups [Array<Array<team>>] groups of teams
|
69
|
-
# @return [
|
34
|
+
# @return [nil]
|
70
35
|
def rollover_groups(groups)
|
71
36
|
groups.each_with_index do |group, index|
|
72
37
|
# Move last from the current group to the front of the next group
|
@@ -54,47 +54,6 @@ module Tournament
|
|
54
54
|
min_elements
|
55
55
|
end
|
56
56
|
# rubocop:enable Metrics/MethodLength
|
57
|
-
|
58
|
-
# rubocop:disable Metrics/MethodLength
|
59
|
-
# :reek:NestedIterators
|
60
|
-
|
61
|
-
# Iterate all perfect matches of a specific size.
|
62
|
-
# A perfect matching is a unique grouping of all elements with a specific
|
63
|
-
# group size. For example +[[1, 2], [3, 4]]+ is a perfect match of
|
64
|
-
# +[1, 2, 3, 4]+ with group size of +2+.
|
65
|
-
#
|
66
|
-
# This is useful for example when a pairing algorithm needs to iterate all
|
67
|
-
# possible pairings of teams to determine which is the best.
|
68
|
-
#
|
69
|
-
# Warning, this currently only works for +size = 2+.
|
70
|
-
#
|
71
|
-
# @param array [Array<element>]
|
72
|
-
# @param size [Integer] the size of groups
|
73
|
-
# @overload all_perfect_matches(array, size)
|
74
|
-
# @return [Enumerator<Array<element>>] enumerator for all perfect
|
75
|
-
# matches
|
76
|
-
# @overload all_perfect_matches(array, size) { |group| ... }
|
77
|
-
# @yieldparam group [Array(element, size)] a group of elements
|
78
|
-
# @return [void]
|
79
|
-
def all_perfect_matches(array, size)
|
80
|
-
size = 2
|
81
|
-
return to_enum(:all_perfect_matches, array, size) unless block_given?
|
82
|
-
|
83
|
-
if array.empty?
|
84
|
-
yield []
|
85
|
-
return
|
86
|
-
end
|
87
|
-
|
88
|
-
array[1..-1].combination(size - 1) do |group|
|
89
|
-
group.unshift array[0]
|
90
|
-
|
91
|
-
remaining = array.reject { |element| group.include?(element) }
|
92
|
-
all_perfect_matches(remaining, size) do |groups|
|
93
|
-
yield [group] + groups
|
94
|
-
end
|
95
|
-
end
|
96
|
-
end
|
97
|
-
# rubbocop:enable Metrics/MethodLength
|
98
57
|
end
|
99
58
|
end
|
100
59
|
end
|
data/lib/tournament/driver.rb
CHANGED
@@ -21,29 +21,28 @@ module Tournament
|
|
21
21
|
# rubocop:disable Lint/UnusedMethodArgument
|
22
22
|
# :nocov:
|
23
23
|
|
24
|
-
# Get all matches.
|
24
|
+
# Required to implement: Get all matches.
|
25
25
|
#
|
26
26
|
# @return [Array<match>]
|
27
27
|
def matches
|
28
28
|
raise 'Not Implemented'
|
29
29
|
end
|
30
30
|
|
31
|
-
# Get the teams with their initial seedings.
|
31
|
+
# Required to implement: Get the teams with their initial seedings.
|
32
32
|
#
|
33
33
|
# @return [Array<team>]
|
34
34
|
def seeded_teams
|
35
35
|
raise 'Not Implemented'
|
36
36
|
end
|
37
37
|
|
38
|
-
# Get the teams ranked by their current position in the tournament.
|
39
|
-
# Required to implement.
|
38
|
+
# Required to implement: Get the teams ranked by their current position in the tournament.
|
40
39
|
#
|
41
40
|
# @return [Array<team>]
|
42
41
|
def ranked_teams
|
43
42
|
raise 'Not Implemented'
|
44
43
|
end
|
45
44
|
|
46
|
-
# Get the winning team of a match.
|
45
|
+
# Required to implement: Get the winning team of a match.
|
47
46
|
#
|
48
47
|
# @param match [] a match, eg. one returned by {#matches}
|
49
48
|
# @return [team, nil] the winner of the match if applicable
|
@@ -51,7 +50,7 @@ module Tournament
|
|
51
50
|
raise 'Not Implemented'
|
52
51
|
end
|
53
52
|
|
54
|
-
# Get the pair of teams playing for a match.
|
53
|
+
# Required to implement: Get the pair of teams playing for a match.
|
55
54
|
#
|
56
55
|
# @param match [] a match, eg. one returned by {#matches}
|
57
56
|
# @return [Array(team, team)] the pair of teams playing in the match
|
@@ -59,7 +58,7 @@ module Tournament
|
|
59
58
|
raise 'Not Implemented'
|
60
59
|
end
|
61
60
|
|
62
|
-
# Get a specific score for a team.
|
61
|
+
# Required to implement: Get a specific score for a team.
|
63
62
|
#
|
64
63
|
# @param team [] a team, eg. one returned by {#seeded_teams}
|
65
64
|
# @return [Number] the score of the team
|
@@ -67,8 +66,7 @@ module Tournament
|
|
67
66
|
raise 'Not Implemented'
|
68
67
|
end
|
69
68
|
|
70
|
-
# Called when a match is created by a tournament system.
|
71
|
-
# implement.
|
69
|
+
# Required to implement: Called when a match is created by a tournament system.
|
72
70
|
#
|
73
71
|
# @example rails
|
74
72
|
# def build_match(home_team, away_team)
|
@@ -78,7 +76,7 @@ module Tournament
|
|
78
76
|
# @param home_team [team] the home team for the match, never +nil+
|
79
77
|
# @param away_team [team, nil] the away team for the match, may be +nil+ for
|
80
78
|
# byes.
|
81
|
-
# @return [
|
79
|
+
# @return [nil]
|
82
80
|
def build_match(home_team, away_team)
|
83
81
|
raise 'Not Implemented'
|
84
82
|
end
|
@@ -86,9 +84,8 @@ module Tournament
|
|
86
84
|
# :nocov:
|
87
85
|
# rubocop:enable Lint/UnusedMethodArgument
|
88
86
|
|
89
|
-
# Get the losing team of a specific match. By default uses
|
90
|
-
#
|
91
|
-
# Override if you have better access to this information.
|
87
|
+
# Get the losing team of a specific match. By default uses {#get_match_winner} and {#get_match_teams} to determine
|
88
|
+
# which team lost. Override if you have better access to this information.
|
92
89
|
#
|
93
90
|
# @return [team, nil] the lower of the match, if applicable
|
94
91
|
def get_match_loser(match)
|
@@ -98,16 +95,14 @@ module Tournament
|
|
98
95
|
get_match_teams(match).reject { |team| team == winner }.first
|
99
96
|
end
|
100
97
|
|
101
|
-
# Get a hash of unique team pairs and their number of occurences. Used by
|
102
|
-
# tournament systems.
|
98
|
+
# Get a hash of unique team pairs and their number of occurences. Used by tournament systems.
|
103
99
|
#
|
104
100
|
# @return [Hash{Set(team, team) => Integer}]
|
105
101
|
def matches_hash
|
106
102
|
@matches_hash ||= build_matches_hash
|
107
103
|
end
|
108
104
|
|
109
|
-
# Count the number of times each pair of teams has played already. Used by
|
110
|
-
# tournament systems.
|
105
|
+
# Count the number of times each pair of teams has played already. Used by tournament systems.
|
111
106
|
#
|
112
107
|
# @param matches [Array<match>]
|
113
108
|
# @return [Integer] the number of duplicate matches
|
@@ -121,7 +116,7 @@ module Tournament
|
|
121
116
|
#
|
122
117
|
# @param home_team [team, nil]
|
123
118
|
# @param away_team [team, nil]
|
124
|
-
# @return [
|
119
|
+
# @return [nil]
|
125
120
|
# @raise when both teams are +nil+
|
126
121
|
def create_match(home_team, away_team)
|
127
122
|
home_team, away_team = away_team, home_team unless home_team
|
@@ -134,7 +129,7 @@ module Tournament
|
|
134
129
|
# @see #create_match
|
135
130
|
#
|
136
131
|
# @param matches [Array<Array(team, team)>] a collection of pairs
|
137
|
-
# @return [
|
132
|
+
# @return [nil]
|
138
133
|
def create_matches(matches)
|
139
134
|
matches.each do |home_team, away_team|
|
140
135
|
create_match(home_team, away_team)
|
@@ -1,5 +1,5 @@
|
|
1
1
|
require 'tournament/algorithm/page_playoff'
|
2
|
-
require 'tournament/algorithm/
|
2
|
+
require 'tournament/algorithm/group_pairing'
|
3
3
|
|
4
4
|
module Tournament
|
5
5
|
# Implements the page playoff system.
|
@@ -46,7 +46,7 @@ module Tournament
|
|
46
46
|
private
|
47
47
|
|
48
48
|
def semi_finals(driver, teams)
|
49
|
-
driver.create_matches Algorithm::
|
49
|
+
driver.create_matches Algorithm::GroupPairing.adjacent(teams)
|
50
50
|
end
|
51
51
|
|
52
52
|
def preliminary_finals(driver)
|
@@ -1,5 +1,5 @@
|
|
1
1
|
require 'tournament/algorithm/single_bracket'
|
2
|
-
require 'tournament/algorithm/
|
2
|
+
require 'tournament/algorithm/group_pairing'
|
3
3
|
|
4
4
|
module Tournament
|
5
5
|
# Implements the single bracket elimination tournament system.
|
@@ -9,7 +9,7 @@ module Tournament
|
|
9
9
|
# Generate matches with the given driver
|
10
10
|
#
|
11
11
|
# @param driver [Driver]
|
12
|
-
# @return [
|
12
|
+
# @return [nil]
|
13
13
|
def generate(driver, _options = {})
|
14
14
|
round = guess_round(driver)
|
15
15
|
|
@@ -21,7 +21,7 @@ module Tournament
|
|
21
21
|
get_match_winners driver, last_matches
|
22
22
|
end
|
23
23
|
|
24
|
-
driver.create_matches Algorithm::
|
24
|
+
driver.create_matches Algorithm::GroupPairing.adjacent(teams)
|
25
25
|
end
|
26
26
|
|
27
27
|
# The total number of rounds needed for a single elimination tournament with
|
data/lib/tournament/swiss.rb
CHANGED
@@ -13,7 +13,7 @@ module Tournament
|
|
13
13
|
# {Dutch}
|
14
14
|
# @option options [Hash] pair_options options for the chosen pairing system,
|
15
15
|
# see {Dutch} for more details
|
16
|
-
# @return [
|
16
|
+
# @return [nil]
|
17
17
|
def generate(driver, options = {})
|
18
18
|
pairer = options[:pairer] || Dutch
|
19
19
|
pairer_options = options[:pair_options] || {}
|
@@ -23,7 +23,7 @@ module Tournament
|
|
23
23
|
driver.create_matches(pairings)
|
24
24
|
end
|
25
25
|
|
26
|
-
# The minimum number of rounds to determine a
|
26
|
+
# The minimum number of rounds to determine a number of winners.
|
27
27
|
#
|
28
28
|
# @param driver [Driver]
|
29
29
|
# @return [Integer]
|
@@ -1,7 +1,8 @@
|
|
1
|
+
require 'ostruct'
|
2
|
+
|
1
3
|
require 'tournament/algorithm/swiss'
|
2
|
-
require 'tournament/algorithm/
|
3
|
-
require 'tournament/algorithm/
|
4
|
-
require 'tournament/algorithm/pairers/best_min_duplicates'
|
4
|
+
require 'tournament/algorithm/matching'
|
5
|
+
require 'tournament/algorithm/group_pairing'
|
5
6
|
|
6
7
|
module Tournament
|
7
8
|
module Swiss
|
@@ -11,55 +12,104 @@ module Tournament
|
|
11
12
|
|
12
13
|
# Pair teams using dutch pairing for a swiss system tournament.
|
13
14
|
#
|
15
|
+
# Teams are initially grouped by their score and then slide paired ({Tournament::Algorithm::GroupPairing.slide}).
|
16
|
+
# If that fails to produce unique matches it will match teams by the minimum score difference, aniling duplicate
|
17
|
+
# matches (default) and optionally pushing byes to a certain side.
|
18
|
+
#
|
14
19
|
# @param driver [Driver]
|
15
|
-
# @option options [
|
16
|
-
#
|
20
|
+
# @option options [Boolean] allow_duplicates removes the penalty of duplicate matches from the pairing algorithm
|
21
|
+
# @option options [:none, :top_half, :bottom_half] push_byes_to adds a penalty to the pairing algorithm for when a
|
22
|
+
# bye match is not with a team in the desired position.
|
17
23
|
# @return [Array<Array(team, team)>] the generated pairs of teams
|
18
24
|
def pair(driver, options = {})
|
25
|
+
state = build_state(driver, options)
|
26
|
+
|
27
|
+
dutch_pairings = generate_dutch_pairings(state)
|
28
|
+
|
29
|
+
duplicates = driver.count_duplicate_matches(dutch_pairings)
|
30
|
+
|
31
|
+
if duplicates.zero?
|
32
|
+
dutch_pairings
|
33
|
+
else
|
34
|
+
generate_best_pairings(state)
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
private
|
39
|
+
|
40
|
+
def build_state(driver, options)
|
41
|
+
OpenStruct.new(
|
42
|
+
driver: driver,
|
43
|
+
teams: get_teams(driver),
|
44
|
+
scores: driver.scores_hash,
|
45
|
+
allow_duplicates: options.fetch(:allow_duplicates, false),
|
46
|
+
push_byes_to: options.fetch(:push_byes_to, :none)
|
47
|
+
)
|
48
|
+
end
|
49
|
+
|
50
|
+
def get_teams(driver)
|
19
51
|
teams = driver.ranked_teams.dup
|
20
52
|
|
21
|
-
# Special padding such that the bottom team gets a BYE
|
53
|
+
# Special padding such that the bottom team gets a BYE with dutch
|
22
54
|
teams.insert(teams.length / 2, nil) if teams.length.odd?
|
23
55
|
|
24
|
-
|
25
|
-
|
56
|
+
teams
|
57
|
+
end
|
26
58
|
|
27
|
-
|
28
|
-
Algorithm::Swiss.
|
59
|
+
def generate_dutch_pairings(state)
|
60
|
+
groups = Algorithm::Swiss.group_teams_by_score(state.teams, state.scores)
|
29
61
|
|
62
|
+
# Make sure all groups are at least of size 2
|
30
63
|
Algorithm::Swiss.rollover_groups(groups)
|
31
64
|
|
32
|
-
|
65
|
+
groups.map do |group|
|
66
|
+
Algorithm::GroupPairing.slide(group)
|
67
|
+
end.reduce(:+)
|
33
68
|
end
|
34
69
|
|
35
|
-
|
70
|
+
# rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity
|
36
71
|
|
37
|
-
def
|
38
|
-
|
39
|
-
matches = driver.matches_hash
|
72
|
+
def generate_best_pairings(state)
|
73
|
+
teams = state.teams
|
40
74
|
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
end
|
75
|
+
state.matches = state.driver.matches_hash
|
76
|
+
|
77
|
+
state.score_range = state.scores.keys.max - state.scores.keys.min
|
78
|
+
state.average_score_difference = state.score_range / teams.length.to_f
|
79
|
+
|
80
|
+
state.team_index_map = teams.map.with_index.to_h
|
48
81
|
|
49
|
-
|
50
|
-
|
82
|
+
Algorithm::Matching.minimum_weight_perfect_matching(teams) do |home_team, away_team|
|
83
|
+
cost_function(state, home_team, away_team)
|
84
|
+
end
|
51
85
|
end
|
52
86
|
|
53
|
-
|
54
|
-
|
55
|
-
|
87
|
+
# This function will be called a lot, so it needs to be pretty fast (run in +O(1)+)
|
88
|
+
def cost_function(state, home_team, away_team)
|
89
|
+
match_set = Set[home_team, away_team]
|
90
|
+
cost = 0
|
91
|
+
|
92
|
+
# Reduce score distance between teams
|
93
|
+
cost += (state.scores[home_team] || 0 - state.scores[away_team] || 0).abs
|
94
|
+
|
95
|
+
# The cost of a duplicate is the score range + 1
|
96
|
+
cost += (state.score_range + 1) * state.matches[match_set] unless state.allow_duplicates
|
56
97
|
|
57
|
-
|
58
|
-
|
98
|
+
# Add cost for bye matches not on the preferred side
|
99
|
+
push_byes_to = state.push_byes_to
|
100
|
+
if match_set.include?(nil) && push_byes_to != :none
|
101
|
+
index = state.team_index_map[home_team || away_team]
|
59
102
|
|
60
|
-
|
103
|
+
if (push_byes_to == :bottom_half && index > state.teams.length / 2) ||
|
104
|
+
(push_byes_to == :top_half && index < state.teams.length / 2)
|
105
|
+
cost += state.score_range
|
106
|
+
end
|
61
107
|
end
|
108
|
+
|
109
|
+
cost
|
62
110
|
end
|
111
|
+
|
112
|
+
# rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity
|
63
113
|
end
|
64
114
|
end
|
65
115
|
end
|
data/lib/tournament/version.rb
CHANGED
data/tournament-system.gemspec
CHANGED
metadata
CHANGED
@@ -1,15 +1,29 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: tournament-system
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 1.0.0
|
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-09-
|
11
|
+
date: 2017-09-29 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: graph_matching
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: 0.1.1
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: 0.1.1
|
13
27
|
- !ruby/object:Gem::Dependency
|
14
28
|
name: bundler
|
15
29
|
requirement: !ruby/object:Gem::Requirement
|
@@ -43,12 +57,9 @@ files:
|
|
43
57
|
- lib/tournament-system.rb
|
44
58
|
- lib/tournament.rb
|
45
59
|
- lib/tournament/algorithm.rb
|
60
|
+
- lib/tournament/algorithm/group_pairing.rb
|
61
|
+
- lib/tournament/algorithm/matching.rb
|
46
62
|
- 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
63
|
- lib/tournament/algorithm/round_robin.rb
|
53
64
|
- lib/tournament/algorithm/single_bracket.rb
|
54
65
|
- lib/tournament/algorithm/swiss.rb
|
@@ -1,21 +0,0 @@
|
|
1
|
-
module Tournament
|
2
|
-
module Algorithm
|
3
|
-
module Pairers
|
4
|
-
# Basic pairer for adjacent teams.
|
5
|
-
module Adjacent
|
6
|
-
extend self
|
7
|
-
|
8
|
-
# Pair adjacent teams.
|
9
|
-
#
|
10
|
-
# @example
|
11
|
-
# pair([1, 2, 3, 4]) #=> [[1, 2], [3, 4]]
|
12
|
-
#
|
13
|
-
# @param teams [Array<team>]
|
14
|
-
# @return [Array<Array(team, team)>]
|
15
|
-
def pair(teams)
|
16
|
-
teams.each_slice(2).to_a
|
17
|
-
end
|
18
|
-
end
|
19
|
-
end
|
20
|
-
end
|
21
|
-
end
|
@@ -1,55 +0,0 @@
|
|
1
|
-
require 'tournament/algorithm/util'
|
2
|
-
|
3
|
-
module Tournament
|
4
|
-
module Algorithm
|
5
|
-
module Pairers
|
6
|
-
# Complex pairing system that tries to maximise uniqueness and
|
7
|
-
# total score difference.
|
8
|
-
module BestMinDuplicates
|
9
|
-
extend self
|
10
|
-
|
11
|
-
# Pair by minimising the number of duplicate matches,
|
12
|
-
# then minimising the score difference.
|
13
|
-
#
|
14
|
-
# @see Util.all_perfect_matches
|
15
|
-
#
|
16
|
-
# @param teams [Array<team>] the teams to pair
|
17
|
-
# @param scores [Hash{team => Numer}] a mapping from teams to scores
|
18
|
-
# @param matches_counts [Hash{Set(team, team) => Integer}]
|
19
|
-
# A mapping from unique matches to their number of previous
|
20
|
-
# occurrences.
|
21
|
-
def pair(teams, scores, matches_counts)
|
22
|
-
# enumerate all unique pairings using round robin
|
23
|
-
perfect_matches = Util.all_perfect_matches(teams, 2)
|
24
|
-
|
25
|
-
# Find all possible pairings with minimum duplicates
|
26
|
-
min_pairings = perfect_matches.group_by do |matches|
|
27
|
-
count_duplicate_matches(matches_counts, matches)
|
28
|
-
end.min_by(&:first).last
|
29
|
-
|
30
|
-
# Pick the pairings with least total score difference
|
31
|
-
min_pairings = Util.all_min_by(min_pairings) do |matches|
|
32
|
-
score_difference(scores, matches)
|
33
|
-
end
|
34
|
-
|
35
|
-
# Pick the last one as its usually the most diverse
|
36
|
-
min_pairings.last
|
37
|
-
end
|
38
|
-
|
39
|
-
private
|
40
|
-
|
41
|
-
def count_duplicate_matches(matches_counts, matches)
|
42
|
-
matches.map { |match| matches_counts[Set.new match] || 0 }.reduce(:+)
|
43
|
-
end
|
44
|
-
|
45
|
-
def score_difference(scores, matches)
|
46
|
-
# Default to a score of 0
|
47
|
-
score_h = Hash.new { |hash, key| hash[key] = scores[key] || 0 }
|
48
|
-
|
49
|
-
matches.map { |match| (score_h[match[0]] - score_h[match[1]]).abs }
|
50
|
-
.reduce(:+)
|
51
|
-
end
|
52
|
-
end
|
53
|
-
end
|
54
|
-
end
|
55
|
-
end
|
@@ -1,32 +0,0 @@
|
|
1
|
-
module Tournament
|
2
|
-
module Algorithm
|
3
|
-
module Pairers
|
4
|
-
# Basic pairer that matches the top half with the bottom half
|
5
|
-
module Halves
|
6
|
-
extend self
|
7
|
-
|
8
|
-
# Pair the top half of teams with the bottom half.
|
9
|
-
#
|
10
|
-
# @example
|
11
|
-
# pair([1, 2, 3, 4]) #=> [[1, 3], [2, 4]]
|
12
|
-
#
|
13
|
-
# @example
|
14
|
-
# pair([1, 2, 3, 4], bottom_reversed: true) #=> [[1, 4], [2, 3]]
|
15
|
-
#
|
16
|
-
# @param teams [Array<team>]
|
17
|
-
# @param bottom_reversed [Boolean] whether to reverse the bottom half
|
18
|
-
# before pairing both halves.
|
19
|
-
# @return [Array<Array(team, team)>]
|
20
|
-
# :reek:BooleanParameter
|
21
|
-
# :reek:ControlParameter
|
22
|
-
def pair(teams, bottom_reversed: false)
|
23
|
-
top, bottom = teams.each_slice(teams.length / 2).to_a
|
24
|
-
|
25
|
-
bottom.reverse! if bottom_reversed
|
26
|
-
|
27
|
-
top.zip(bottom).to_a
|
28
|
-
end
|
29
|
-
end
|
30
|
-
end
|
31
|
-
end
|
32
|
-
end
|
@@ -1,33 +0,0 @@
|
|
1
|
-
module Tournament
|
2
|
-
module Algorithm
|
3
|
-
module Pairers
|
4
|
-
# Modular pairer for combining multiple pairing algorithms.
|
5
|
-
module Multi
|
6
|
-
extend self
|
7
|
-
|
8
|
-
# Generic way of combining multiple pairing algorithms.
|
9
|
-
#
|
10
|
-
# Given a set of pair functions, generate pairs using each of them and
|
11
|
-
# use the block to determine a value for this pairing. The function will
|
12
|
-
# return the pairing generated that produced the highest score.
|
13
|
-
#
|
14
|
-
# @param pair_funcs [Array<lambda>] the array of pairers
|
15
|
-
# @yieldparam pairings [] a set of pairings generated by +pair_funcs+
|
16
|
-
# @yieldreturn [Number] a score for the set of pairings
|
17
|
-
# @return the set of pairings with the highest score
|
18
|
-
def pair(pair_funcs)
|
19
|
-
# Score all pairings
|
20
|
-
pairings = pair_funcs.map do |pair_func|
|
21
|
-
matches = pair_func.call
|
22
|
-
score = yield matches
|
23
|
-
|
24
|
-
[matches, score]
|
25
|
-
end
|
26
|
-
|
27
|
-
# Use the pairings with the highest score
|
28
|
-
pairings.max_by { |pair| pair[1] }.first
|
29
|
-
end
|
30
|
-
end
|
31
|
-
end
|
32
|
-
end
|
33
|
-
end
|