tournament-system 0.2.1 → 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|