tournament-system 2.0.0 → 2.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 52196b11f89aad9f8e4dbf67094ba9e99dff805e6d7d95dc840213d802bfb511
4
- data.tar.gz: 739375eaad6f1093c3e47ec6e630ea1394670582f1f6e84d85917327bc30358e
3
+ metadata.gz: 9917e1eba50ce274c68fd9dc17d30ce7545667e09be47ddad077b46841446bfb
4
+ data.tar.gz: 02a331c1672c1fc6265c8d28a813df74e96d89ec14a925f83f0a9ae4fdca69a2
5
5
  SHA512:
6
- metadata.gz: 80e6f521ef7293504a69d2d0287200d535ce5833cbbc4d78c60878de30d18f1862aea1101460e9aeac0c396d18818c5a199764a875bcebbbc6298b8ab5f30ae3
7
- data.tar.gz: 88b4831a41dea4acce69ffcde2e10eed2edbb5ec5a435c9d97b47fb5aad85e24beffb3aa76a809eadca053769c354611798edac2e79dd57382145c256d0d517a
6
+ metadata.gz: 423871fd41ae8c97d16cca8a55999f97325b8e208c7a964cdd2202b241af9dc59dc2332c195f05ce956045123786b6c5ebe3483639e378d2763cf1aeed51417c
7
+ data.tar.gz: 64ab7e6df86f5614a1912d741be1642e7031208ccb0fa99430afd761b9f995f5dcc167fb647e7d25cd2594264620af011bf9d8b9230a6c3aa0564c112cebbd24
@@ -1,14 +1,10 @@
1
1
  AllCops:
2
2
  DisplayCopNames: true
3
- TargetRubyVersion: 2.3
3
+ TargetRubyVersion: 2.5
4
4
 
5
5
  Metrics/LineLength:
6
6
  Max: 120
7
7
 
8
- # This is fine, really
9
- Naming/FileName:
10
- Enabled: false
11
-
12
8
  # 'old' style
13
9
  Style/EmptyMethod:
14
10
  EnforcedStyle: expanded
@@ -17,7 +13,7 @@ Style/EmptyMethod:
17
13
  Bundler/OrderedGems:
18
14
  Enabled: false
19
15
 
20
- # Not interchangeable
16
+ # `module_function` and `extend self` aren't actually interchangeable
21
17
  Style/ModuleFunction:
22
18
  Enabled: false
23
19
 
@@ -26,8 +22,11 @@ Style/FrozenStringLiteralComment:
26
22
  Enabled: false
27
23
 
28
24
  # Doesn't really make sense for multiline
29
- Style/TrailingCommaInLiteral:
30
- Enabled: false
25
+ Style/TrailingCommaInHashLiteral:
26
+ EnforcedStyleForMultiline: consistent_comma
27
+
28
+ Style/TrailingCommaInArrayLiteral:
29
+ EnforcedStyleForMultiline: consistent_comma
31
30
 
32
31
  # Tests should be as long as they need to be
33
32
  Metrics/BlockLength:
data/Gemfile CHANGED
@@ -12,7 +12,7 @@ gem 'rspec'
12
12
  gem 'simplecov'
13
13
 
14
14
  # Linting
15
- gem 'rubocop', '~> 0.52.1'
15
+ gem 'rubocop', '~> 0.56.0'
16
16
  gem 'reek'
17
17
 
18
18
  group :test do
data/README.md CHANGED
@@ -87,7 +87,7 @@ TournamentSystem::RoundRobin.generate driver
87
87
  # Generate a round for a swiss system tournament, pushing byes to the bottom
88
88
  # half (bottom half teams will bye before the top half)
89
89
  TournamentSystem::Swiss.generate driver, pairer: TournamentSystem::Swiss::Dutch,
90
- pair_options: { push_byes_to: :bottom_half }
90
+ pair_options: { push_byes_to: :bottom_half }
91
91
 
92
92
  # Alternatively use the accelerated swiss system
93
93
  TournamentSystem::Swiss.generate driver, pairer: TournamentSystem::Swiss::AcceleratedDutch
@@ -5,6 +5,7 @@ require 'tournament_system/swiss'
5
5
  require 'tournament_system/round_robin'
6
6
  require 'tournament_system/page_playoff'
7
7
  require 'tournament_system/single_elimination'
8
+ require 'tournament_system/double_elimination'
8
9
 
9
10
  # This library is split into two parts, there's the actual algorithms that implement various tournament systems
10
11
  # ({Algorithm}) and a data abstraction layer for generating matches using various tournament systems in a
@@ -0,0 +1,129 @@
1
+ require 'ostruct'
2
+
3
+ module TournamentSystem
4
+ module Algorithm
5
+ # This module provides algorithms for dealing with double bracket elimination tournaments.
6
+ module DoubleBracket
7
+ extend self
8
+
9
+ # Get the number of rounds required for a double bracket tournament.
10
+ #
11
+ # @param teams_count [Number]
12
+ # @return [Number]
13
+ def total_rounds(teams_count)
14
+ Math.log2(teams_count).ceil * 2
15
+ end
16
+
17
+ # Get the maximum number of teams that can be processed in the given number of rounds.
18
+ #
19
+ # @param rounds [Number]
20
+ # @return [Number]
21
+ def max_teams(rounds)
22
+ 2**(rounds / 2)
23
+ end
24
+
25
+ # Guess the next round number given the number of teams and matches played so far.
26
+ # Due to the complexity of double elimination, this practically runs through the tournament one round at a time,
27
+ # but is still faster as it only handles numbers and not concrete teams.
28
+ #
29
+ # @param teams_count [Number]
30
+ # @param matches_count [Number]
31
+ # @return [Number]
32
+ # @raise [ArgumentError] when the number of matches does not add up
33
+ def guess_round(teams_count, matches_count)
34
+ counting_state = OpenStruct.new(winners: teams_count, losers: 0)
35
+
36
+ round_number = count_iterations do |round|
37
+ round_size = count_round(round, counting_state)
38
+
39
+ next false if round_size > matches_count || round_size.zero?
40
+
41
+ matches_count -= round_size
42
+ end
43
+
44
+ raise ArgumentError, "Invalid number of matches, was off by #{matches_count}" unless matches_count.zero?
45
+
46
+ round_number
47
+ end
48
+
49
+ # Determines whether a given round is a minor round, ie. the top and bottom bracket have a round.
50
+ # Use this in combination with {#major_round?} to determine the type of round.
51
+ # The first round is neither minor nor major.
52
+ #
53
+ # @param round [Number]
54
+ # @return [Boolean]
55
+ def minor_round?(round)
56
+ round.odd?
57
+ end
58
+
59
+ # Determines whether a given round is a major round, ie. only the bottom bracket has a round.
60
+ # Use this in combination with {#minor_round?} to determine the type of round.
61
+ # The first round is neither major nor minor.
62
+ #
63
+ # @param round [Number]
64
+ # @return [Boolean]
65
+ def major_round?(round)
66
+ round.even? && round.positive?
67
+ end
68
+
69
+ # Seed the given teams for a double bracket tournament. Identical to {SingleBracket#seed}.
70
+ #
71
+ # @param teams [Array<team>]
72
+ # @return [Array<team>]
73
+ def seed(teams)
74
+ SingleBracket.seed(teams)
75
+ end
76
+
77
+ private
78
+
79
+ # Handle state transition for a round. Counts the number of winners and losers.
80
+ #
81
+ # @return [Number] the number of matches played this round.
82
+ def count_round(round, state)
83
+ if minor_round? round
84
+ count_minor_round(state)
85
+ elsif major_round? round
86
+ count_major_round(state)
87
+ else
88
+ count_first_round(state)
89
+ end
90
+ end
91
+
92
+ # @return [Number] the number of matches played this round.
93
+ def count_minor_round(state)
94
+ winner_matches = state.winners / 2
95
+ state.winners -= winner_matches
96
+
97
+ loser_matches = state.losers / 2
98
+ state.losers += winner_matches - loser_matches
99
+
100
+ winner_matches + loser_matches
101
+ end
102
+
103
+ # @return [Number] the number of matches played this round.
104
+ def count_major_round(state)
105
+ matches = state.losers / 2
106
+ state.losers -= matches
107
+ matches
108
+ end
109
+
110
+ # @return [Number] the number of matches played this round.
111
+ def count_first_round(state)
112
+ winners = state.winners
113
+ padded_teams = Util.padded_teams_pow2_count(winners)
114
+
115
+ matches = winners - padded_teams / 2
116
+ state.winners -= matches
117
+ state.losers += matches
118
+ matches
119
+ end
120
+
121
+ # Count the number of iterations until the block returns false.
122
+ def count_iterations
123
+ counter = 0
124
+ counter += 1 while (yield counter) != false
125
+ counter
126
+ end
127
+ end
128
+ end
129
+ end
@@ -14,7 +14,7 @@ module TournamentSystem
14
14
  # @param teams_count [Integer] the number of teams
15
15
  # @return [Integer] number of rounds needed for round robin
16
16
  def total_rounds(teams_count)
17
- Util.padded_teams_count(teams_count) - 1
17
+ Util.padded_teams_even_count(teams_count) - 1
18
18
  end
19
19
 
20
20
  # Guess the next round (starting at 0) for round robin.
@@ -23,7 +23,7 @@ module TournamentSystem
23
23
  # @param matches_count [Integer] the number of existing matches
24
24
  # @return [Integer] next round number
25
25
  def guess_round(teams_count, matches_count)
26
- matches_count / (Util.padded_teams_count(teams_count) / 2)
26
+ matches_count / (Util.padded_teams_even_count(teams_count) / 2)
27
27
  end
28
28
 
29
29
  # Rotate array using round robin.
@@ -2,8 +2,7 @@ require 'ostruct'
2
2
 
3
3
  module TournamentSystem
4
4
  module Algorithm
5
- # This module provides algorithms for dealing with single bracket
6
- # elimination tournament systems.
5
+ # This module provides algorithms for dealing with single bracket elimination tournament systems.
7
6
  module SingleBracket
8
7
  extend self
9
8
 
@@ -45,15 +44,14 @@ module TournamentSystem
45
44
  round.to_i
46
45
  end
47
46
 
48
- # Padd an array of teams to the next highest power of 2.
49
- #
50
- # @param teams [Array<team>]
51
- # @return [Array<team, nil>]
47
+ # @deprecated Please use {Util.padd_teams_pow2} instead.
52
48
  def padd_teams(teams)
53
- required = max_teams(total_rounds(teams.length))
49
+ message = 'NOTE: padd_teams is now deprecated in favour of Util.padd_teams_even.'\
50
+ 'It will be removed in the next major version'\
51
+ "SingleBracket.padd_teams called from #{Gem.location_of_caller.join(':')}"
52
+ warn message unless Gem::Deprecate.skip
54
53
 
55
- # Insert the padding at the bottom to give top teams byes first
56
- Array.new(required) { |index| teams[index] }
54
+ Util.padd_teams_pow2(teams)
57
55
  end
58
56
 
59
57
  # Seed teams for a single bracket tournament.
@@ -5,11 +5,21 @@ module TournamentSystem
5
5
  module Util
6
6
  extend self
7
7
 
8
+ # @deprecated Please use {#padd_teams_even} instead.
9
+ def padd_teams(teams)
10
+ message = 'NOTE: padd_teams is now deprecated in favour of padd_teams_even. '\
11
+ 'It will be removed in the next major version.'\
12
+ "Util.padd_teams called from #{Gem.location_of_caller.join(':')}"
13
+ warn message unless Gem::Deprecate.skip
14
+
15
+ padd_teams_even(teams)
16
+ end
17
+
8
18
  # Padd an array of teams to be even.
9
19
  #
10
20
  # @param teams [Array<team>]
11
21
  # @return [Array<team, nil>]
12
- def padd_teams(teams)
22
+ def padd_teams_even(teams)
13
23
  if teams.length.odd?
14
24
  teams + [nil]
15
25
  else
@@ -17,17 +27,51 @@ module TournamentSystem
17
27
  end
18
28
  end
19
29
 
30
+ # @deprecated Please use {#padded_teams_even_count}
31
+ def padded_teams_count(teams_count)
32
+ message = 'Node: padded_teams_count is now deprecated in favour of padded_teams_even_count. '\
33
+ 'It will be removed in the next major version.'\
34
+ "Util.padded_teams_count called from #{Gem.location_of_caller.join(':')}"
35
+ warn message unless Gem::Deprecate.skip
36
+
37
+ padded_teams_even_count(teams_count)
38
+ end
39
+
20
40
  # Padd the count of teams to be even.
21
41
  #
22
42
  # @example
23
- # padded_teams_count(teams.length/) == padd_teams(teams).length
43
+ # padded_teams_even_count(teams.length) == padd_teams_even(teams).length
24
44
  #
25
45
  # @param teams_count [Integer] the number of teams
26
46
  # @return [Integer]
27
- def padded_teams_count(teams_count)
47
+ def padded_teams_even_count(teams_count)
28
48
  (teams_count / 2.0).ceil * 2
29
49
  end
30
50
 
51
+ # pow2 is not uncommunicative
52
+ # :reek:UncommunicativeMethodName
53
+
54
+ # Padd an array of teams to the next power of 2.
55
+ #
56
+ # @param teams [Array<team>]
57
+ # @return [Array<team, nil>]
58
+ def padd_teams_pow2(teams)
59
+ required = padded_teams_pow2_count(teams.length)
60
+
61
+ Array.new(required) { |index| teams[index] }
62
+ end
63
+
64
+ # Padd the count of teams to be a power of 2.
65
+ #
66
+ # @example
67
+ # padded_teams_pow2_count(teams.length) == padd_teams_pow2(teams).length
68
+ #
69
+ # @param teams_count [Integer] the number of teams
70
+ # @return [Integer]
71
+ def padded_teams_pow2_count(teams_count)
72
+ 2**Math.log2(teams_count).ceil
73
+ end
74
+
31
75
  # rubocop:disable Metrics/MethodLength
32
76
 
33
77
  # Collect all values in an array with a minimum value.
@@ -0,0 +1,63 @@
1
+ require 'tournament_system/algorithm/double_bracket'
2
+ require 'tournament_system/algorithm/group_pairing'
3
+
4
+ module TournamentSystem
5
+ # Implements the double bracket elimination tournament system.
6
+ module DoubleElimination
7
+ extend self
8
+
9
+ # Generate matches with the given driver
10
+ #
11
+ # @param driver [Driver]
12
+ # @return [nil]
13
+ def generate(driver, _options = {})
14
+ round = guess_round driver
15
+
16
+ teams_padded = Algorithm::Util.padd_teams_pow2 driver.seeded_teams
17
+ teams_seeded = Algorithm::DoubleBracket.seed teams_padded
18
+
19
+ teams = if driver.matches.empty?
20
+ teams_seeded
21
+ else
22
+ get_round_teams driver, round, teams_seeded
23
+ end
24
+
25
+ driver.create_matches Algorithm::GroupPairing.adjacent(teams)
26
+ end
27
+
28
+ # The total number of rounds needed for a single elimination tournament with
29
+ # the given driver.
30
+ #
31
+ # @param driver [Driver]
32
+ # @return [Integer]
33
+ def total_rounds(driver)
34
+ Algorithm::DoubleBracket.total_rounds(driver.seeded_teams.length)
35
+ end
36
+
37
+ # Guess the next round number (starting at 0) from the state in driver.
38
+ #
39
+ # @param driver [Driver]
40
+ # @return [Integer]
41
+ def guess_round(driver)
42
+ Algorithm::DoubleBracket.guess_round(driver.seeded_teams.length,
43
+ driver.non_bye_matches .length)
44
+ end
45
+
46
+ private
47
+
48
+ def get_round_teams(driver, round, teams_seeded)
49
+ loss_counts = driver.loss_count_hash
50
+
51
+ winners = teams_seeded.select { |team| loss_counts[team].zero? }
52
+ losers = teams_seeded.select { |team| loss_counts[team] == 1 }
53
+
54
+ if Algorithm::DoubleBracket.minor_round?(round)
55
+ winners + losers
56
+ elsif Algorithm::DoubleBracket.major_round?(round)
57
+ losers
58
+ else
59
+ winners
60
+ end
61
+ end
62
+ end
63
+ end
@@ -1,5 +1,5 @@
1
1
  module TournamentSystem
2
- # :reek:UnusedParameters
2
+ # :reek:UnusedParameters :reek:TooManyMethods
3
3
 
4
4
  # An interface for external tournament data.
5
5
  #
@@ -159,6 +159,21 @@ module TournamentSystem
159
159
  get_match_teams(match).reject { |team| team == winner }.first
160
160
  end
161
161
 
162
+ # Determine whether a specific match was a bye? By default uses {#get_match_teams} to determine a bye. Override if
163
+ # you have better access to this information.
164
+ #
165
+ # @return [Boolean]
166
+ def match_bye?(match)
167
+ get_match_teams(match).include?(nil)
168
+ end
169
+
170
+ # Get a list of matches that weren't byes. Used by tournament systems.
171
+ #
172
+ # @return [Array<match>]
173
+ def non_bye_matches
174
+ matches.reject { |match| match_bye?(match) }
175
+ end
176
+
162
177
  # Get a hash of unique team pairs and their number of occurences. Used by tournament systems.
163
178
  #
164
179
  # @return [Hash{Set(team, team) => Integer}]
@@ -199,10 +214,10 @@ module TournamentSystem
199
214
  # Create a bunch of matches. Used by tournament systems.
200
215
  # @see #create_match
201
216
  #
202
- # @param matches [Array<Array(team, team)>] a collection of pairs
217
+ # @param pairs [Array<Array(team, team)>] a collection of pairs
203
218
  # @return [nil]
204
- def create_matches(matches)
205
- matches.each do |home_team, away_team|
219
+ def create_matches(pairs)
220
+ pairs.each do |home_team, away_team|
206
221
  create_match(home_team, away_team)
207
222
  end
208
223
  end
@@ -211,8 +226,14 @@ module TournamentSystem
211
226
  #
212
227
  # @return [Hash{team => Number}] a mapping from teams to scores
213
228
  def scores_hash
214
- @scores_hash = ranked_teams.map { |team| [team, get_team_score(team)] }
215
- .to_h
229
+ @scores_hash ||= ranked_teams.map { |team| [team, get_team_score(team)] }.to_h
230
+ end
231
+
232
+ # Get a hash of the number of losses of each team. Used by tournament systems.
233
+ #
234
+ # @return [Hash{team => Number}] a mapping from teams to losses
235
+ def loss_count_hash
236
+ @loss_count_hash ||= matches.each_with_object(Hash.new(0)) { |match, hash| hash[get_match_loser(match)] += 1 }
216
237
  end
217
238
 
218
239
  private
@@ -27,6 +27,9 @@ module TournamentSystem
27
27
  end
28
28
  end
29
29
 
30
+ # Rubocop doesn't handle _ as a parameter sink
31
+ # rubocop:disable Naming/UncommunicativeMethodParamName
32
+
30
33
  # The total number of rounds in a page playoff tournament
31
34
  #
32
35
  # @param _ for keeping the same interface as other tournament systems.
@@ -35,6 +38,8 @@ module TournamentSystem
35
38
  Algorithm::PagePlayoff::TOTAL_ROUNDS
36
39
  end
37
40
 
41
+ # rubocop:enable Naming/UncommunicativeMethodParamName
42
+
38
43
  # Guess the next round number (starting at 0) from the state in a driver.
39
44
  #
40
45
  # @param driver [Driver]
@@ -14,7 +14,7 @@ module TournamentSystem
14
14
  def generate(driver, options = {})
15
15
  round = options[:round] || guess_round(driver)
16
16
 
17
- teams = Algorithm::Util.padd_teams(driver.seeded_teams)
17
+ teams = Algorithm::Util.padd_teams_even(driver.seeded_teams)
18
18
 
19
19
  matches = Algorithm::RoundRobin.round_robin_pairing(teams, round)
20
20
 
@@ -14,7 +14,7 @@ module TournamentSystem
14
14
  round = guess_round(driver)
15
15
 
16
16
  teams = if driver.matches.empty?
17
- padded = Algorithm::SingleBracket.padd_teams driver.seeded_teams
17
+ padded = Algorithm::Util.padd_teams_pow2 driver.seeded_teams
18
18
  Algorithm::SingleBracket.seed padded
19
19
  else
20
20
  last_matches = previous_round_matches driver, round
@@ -1,4 +1,4 @@
1
1
  module TournamentSystem
2
2
  # The current version of this gem.
3
- VERSION = '2.0.0'.freeze
3
+ VERSION = '2.1.0'.freeze
4
4
  end
@@ -1,4 +1,4 @@
1
- lib = File.expand_path('../lib', __FILE__)
1
+ lib = File.expand_path('lib', __dir__)
2
2
  $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
3
3
  require 'tournament_system/version'
4
4
 
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: tournament-system
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.0.0
4
+ version: 2.1.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: 2018-01-31 00:00:00.000000000 Z
11
+ date: 2018-06-10 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: graph_matching
@@ -56,6 +56,7 @@ files:
56
56
  - Rakefile
57
57
  - lib/tournament_system.rb
58
58
  - lib/tournament_system/algorithm.rb
59
+ - lib/tournament_system/algorithm/double_bracket.rb
59
60
  - lib/tournament_system/algorithm/group_pairing.rb
60
61
  - lib/tournament_system/algorithm/matching.rb
61
62
  - lib/tournament_system/algorithm/page_playoff.rb
@@ -63,6 +64,7 @@ files:
63
64
  - lib/tournament_system/algorithm/single_bracket.rb
64
65
  - lib/tournament_system/algorithm/swiss.rb
65
66
  - lib/tournament_system/algorithm/util.rb
67
+ - lib/tournament_system/double_elimination.rb
66
68
  - lib/tournament_system/driver.rb
67
69
  - lib/tournament_system/driver_proxy.rb
68
70
  - lib/tournament_system/page_playoff.rb
@@ -93,7 +95,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
93
95
  version: '0'
94
96
  requirements: []
95
97
  rubyforge_project:
96
- rubygems_version: 2.7.3
98
+ rubygems_version: 2.7.6
97
99
  signing_key:
98
100
  specification_version: 4
99
101
  summary: Implements various tournament systems