tournament-system 2.0.0 → 2.1.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
  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