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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 7da812aeb5ed511131c9e45fc831e8f9b0f2e180
4
- data.tar.gz: 365cb097c222baefc6a5f73bab75f982218c69cb
3
+ metadata.gz: 768d7d740fb6a7c2bec21c85c8d9267029874025
4
+ data.tar.gz: f4ecb036fee09e7ce76acf9fb459285751adf480
5
5
  SHA512:
6
- metadata.gz: c7b30d85ab5424e794c440a50c85880a4bb164a8c3d347d3f85b166c49ce2588210187fa1400fd87a78ff6f25bd5476b43cc955dbfae329c00d4da5b8146722e
7
- data.tar.gz: 0f487a75cd8820df5ac4b6acbd593db7bb0ea6c769cb66c8d36c6a75244d7665432fc079782b21eca08952ce029ec62d16155ff3380aec06e4dfa5e54dd3629d
6
+ metadata.gz: 2c7045cb22a5097dfbc2e1823750fd5e2a13e445dffa634c5ce5c96bcd922d34e73e8698d6983749e74012c0f172b7e93ebcc5bfdc1ec17b7eb434bece7af8da
7
+ data.tar.gz: 7a89d1d96e43e02f60c9e123a6f799ddc0f7970d6b011973e97ce2232e27ac2612fd95ca473075cca28882bc81774185b526dd1a8ca4a74213b982f95d08948e
data/.reek CHANGED
@@ -11,3 +11,6 @@ TooManyStatements:
11
11
  # Reek isn't good at detecting these, especially with state and blocks
12
12
  DuplicateMethodCall:
13
13
  max_calls: 2
14
+
15
+ NestedIterators:
16
+ max_allowed_nesting: 2
@@ -2,6 +2,9 @@ AllCops:
2
2
  DisplayCopNames: true
3
3
  TargetRubyVersion: 2.3
4
4
 
5
+ Metrics/LineLength:
6
+ Max: 120
7
+
5
8
  # This is fine, really
6
9
  Style/FileName:
7
10
  Enabled: false
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
- # with Dutch pairings (default) with a minimum pair size of 6 (default 4)
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: { min_pair_size: 6 }
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/pairers/halves'
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 {Pairers::Halves} for pairing after rotating.
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
- Pairers::Halves.pair(rotated, bottom_reversed: true)
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 {Pairers::Adjacent}.
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
- # Specifically it provides algorithms for grouping teams.
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 [void]
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
@@ -21,29 +21,28 @@ module Tournament
21
21
  # rubocop:disable Lint/UnusedMethodArgument
22
22
  # :nocov:
23
23
 
24
- # Get all matches. Required to implement.
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. Required to implement.
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. Required to implement.
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. Required to implement.
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. Required to implement.
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. Required to
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 [void]
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
- # {#get_match_winner} and {#get_match_teams} to determine which team lost.
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 [void]
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 [void]
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/pairers/adjacent'
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::Pairers::Adjacent.pair(teams)
49
+ driver.create_matches Algorithm::GroupPairing.adjacent(teams)
50
50
  end
51
51
 
52
52
  def preliminary_finals(driver)
@@ -10,7 +10,7 @@ module Tournament
10
10
  #
11
11
  # @param driver [Driver]
12
12
  # @option options [Integer] round the round to generate
13
- # @return [void]
13
+ # @return [nil]
14
14
  def generate(driver, options = {})
15
15
  round = options[:round] || guess_round(driver)
16
16
 
@@ -1,5 +1,5 @@
1
1
  require 'tournament/algorithm/single_bracket'
2
- require 'tournament/algorithm/pairers/adjacent'
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 [void]
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::Pairers::Adjacent.pair(teams)
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
@@ -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 [void]
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 winner.
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/pairers/multi'
3
- require 'tournament/algorithm/pairers/halves'
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 [Integer] min_pair_size see
16
- # {Algorithm::Swiss#merge_small_groups for more details}
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
- scores = driver.scores_hash
25
- groups = Algorithm::Swiss.group_teams_by_score(teams, scores)
56
+ teams
57
+ end
26
58
 
27
- min_pair_size = options[:min_pair_size] || 4
28
- Algorithm::Swiss.merge_small_groups(groups, min_pair_size)
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
- pair_groups driver, groups
65
+ groups.map do |group|
66
+ Algorithm::GroupPairing.slide(group)
67
+ end.reduce(:+)
33
68
  end
34
69
 
35
- private
70
+ # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity
36
71
 
37
- def pairer_funcs(driver, group)
38
- scores = driver.scores_hash
39
- matches = driver.matches_hash
72
+ def generate_best_pairings(state)
73
+ teams = state.teams
40
74
 
41
- [
42
- -> () { Algorithm::Pairers::Halves.pair(group) },
43
- lambda do
44
- Algorithm::Pairers::BestMinDuplicates.pair(group, scores, matches)
45
- end,
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
- def pair_groups(driver, groups)
50
- groups.map { |group| pair_group(driver, group) }.reduce(:+)
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
- def pair_group(driver, group)
54
- Algorithm::Pairers::Multi.pair(pairer_funcs(driver, group)) do |matches|
55
- duplicates = driver.count_duplicate_matches(matches)
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
- # Early return when there are no duplicates, prefer earlier pairers
58
- return matches if duplicates.zero?
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
- -duplicates
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
@@ -1,4 +1,4 @@
1
1
  module Tournament
2
2
  # The current version of this gem.
3
- VERSION = '0.2.1'.freeze
3
+ VERSION = '1.0.0'.freeze
4
4
  end
@@ -23,5 +23,6 @@ Gem::Specification.new do |spec|
23
23
  spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
24
24
  spec.require_paths = ['lib']
25
25
 
26
+ spec.add_runtime_dependency 'graph_matching', '~> 0.1.1'
26
27
  spec.add_development_dependency 'bundler', '~> 1.14'
27
28
  end
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.2.1
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-01 00:00:00.000000000 Z
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,7 +0,0 @@
1
- module Tournament
2
- module Algorithm
3
- # This module provides multiple pairing algorithms
4
- module Pairers
5
- end
6
- end
7
- end
@@ -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