tournament-system 0.1.4 → 0.1.5

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.
@@ -1,61 +1,63 @@
1
+ require 'tournament/algorithm/swiss'
2
+ require 'tournament/algorithm/pairers/multi'
3
+ require 'tournament/algorithm/pairers/halves'
4
+ require 'tournament/algorithm/pairers/best_min_duplicates'
5
+
1
6
  module Tournament
2
7
  module Swiss
3
8
  # A simplified Dutch pairing system implementation.
4
9
  module Dutch
5
10
  extend self
6
- extend Common
7
11
 
8
- def pair(driver, teams, options = {})
9
- return dutch_pairing(teams) if driver.matches.empty?
12
+ # Pair teams using dutch pairing for a swiss system tournament.
13
+ #
14
+ # @param driver [Driver]
15
+ # @option options [Integer] min_pair_size see
16
+ # {Algorithm::Swiss#merge_small_groups for more details}
17
+ # @return [Array<Array(team, team)>] the generated pairs of teams
18
+ def pair(driver, options = {})
19
+ teams = driver.ranked_teams
20
+
21
+ # Special padding such that the bottom team gets a BYE
22
+ teams.insert(teams.length / 2, nil) if teams.length.odd?
10
23
 
11
- groups, group_keys = group_teams_by_score(driver, teams)
24
+ scores = driver.scores_hash
25
+ groups = Algorithm::Swiss.group_teams_by_score(teams, scores)
12
26
 
13
27
  min_pair_size = options[:min_pair_size] || 4
14
- group_keys = merge_small_groups(groups, group_keys, min_pair_size)
28
+ Algorithm::Swiss.merge_small_groups(groups, min_pair_size)
15
29
 
16
- pair_groups driver, groups, group_keys
30
+ Algorithm::Swiss.rollover_groups(groups)
31
+
32
+ pair_groups driver, groups
17
33
  end
18
34
 
19
35
  private
20
36
 
21
- # Match the top half with the bottom half
22
- def dutch_pairing(teams)
23
- half = teams.length / 2
24
- top = teams[0...half]
25
- bottom = teams[half..-1]
26
- top << nil if top.length < bottom.length
37
+ def pairer_funcs(driver, group)
38
+ scores = driver.scores_hash
39
+ matches = driver.matches_hash
27
40
 
28
- top.zip(bottom).to_a
41
+ [
42
+ -> () { Algorithm::Pairers::Halves.pair(group) },
43
+ lambda do
44
+ Algorithm::Pairers::BestMinDuplicates.pair(group, scores, matches)
45
+ end,
46
+ ]
29
47
  end
30
48
 
31
- def pair_groups(driver, groups, group_keys)
32
- existing_matches = matches_set(driver)
33
-
34
- matches = []
35
- each_group_with_rollover(groups, group_keys) do |group|
36
- matches += pair_group(group, existing_matches)
37
- end
38
-
39
- matches
49
+ def pair_groups(driver, groups)
50
+ groups.map { |group| pair_group(driver, group) }.reduce(:+)
40
51
  end
41
52
 
42
- def pair_group(group, existing_matches)
43
- pairs = dutch_pairing(group)
53
+ def pair_group(driver, group)
54
+ Algorithm::Pairers::Multi.pair(pairer_funcs(driver, group)) do |matches|
55
+ duplicates = driver.count_duplicate_matches(matches)
44
56
 
45
- if any_match_exists?(pairs, existing_matches)
46
- first_permutation_pairing(group, existing_matches) do |perm_pairs|
47
- dutch_pairing(perm_pairs)
48
- end
49
- else
50
- pairs
51
- end
52
- end
57
+ # Early return when there are no duplicates, prefer earlier pairers
58
+ return matches if duplicates.zero?
53
59
 
54
- def fix_matches(teams, pairs, existing_matches)
55
- if any_match_exists?(pairs, existing_matches)
56
- first_permutation_pairing(teams, existing_matches)
57
- else
58
- pairs
60
+ -duplicates
59
61
  end
60
62
  end
61
63
  end
@@ -1,4 +1,4 @@
1
- require 'tournament/swiss/common'
1
+ require 'tournament/algorithm/swiss'
2
2
  require 'tournament/swiss/dutch'
3
3
 
4
4
  module Tournament
@@ -6,34 +6,29 @@ module Tournament
6
6
  module Swiss
7
7
  extend self
8
8
 
9
+ # Generate matches with the given driver.
10
+ #
11
+ # @param driver [Driver]
12
+ # @option options [Pairer] pairer the pairing system to use, defaults to
13
+ # {Dutch}
14
+ # @option options [Hash] pair_options options for the chosen pairing system,
15
+ # see {Dutch} for more details
16
+ # @return [void]
9
17
  def generate(driver, options = {})
10
18
  pairer = options[:pairer] || Dutch
11
19
  pairer_options = options[:pair_options] || {}
12
20
 
13
- teams = seed_teams driver.ranked_teams, options
21
+ pairings = pairer.pair(driver, pairer_options)
14
22
 
15
- pairings = pairer.pair driver, teams, pairer_options
16
-
17
- create_matches driver, pairings
23
+ driver.create_matches(pairings)
18
24
  end
19
25
 
26
+ # The minimum number of rounds to determine a winner.
27
+ #
28
+ # @param driver [Driver]
29
+ # @return [Integer]
20
30
  def minimum_rounds(driver)
21
- team_count = driver.seeded_teams.length
22
-
23
- Math.log2(team_count).ceil
24
- end
25
-
26
- private
27
-
28
- def seed_teams(teams, options)
29
- seeder = options[:seeder] || Seeder::None
30
- seeder.seed teams
31
- end
32
-
33
- def create_matches(driver, pairings)
34
- pairings.each do |pair|
35
- driver.create_match(pair[0], pair[1])
36
- end
31
+ Algorithm::Swiss.minimum_rounds(driver.seeded_teams.length)
37
32
  end
38
33
  end
39
34
  end
@@ -1,3 +1,4 @@
1
1
  module Tournament
2
- VERSION = '0.1.4'.freeze
2
+ # The current version of this gem.
3
+ VERSION = '0.1.5'.freeze
3
4
  end
@@ -1,8 +1,6 @@
1
1
  require 'tournament/version'
2
2
  require 'tournament/driver'
3
3
 
4
- require 'tournament/seeder'
5
-
6
4
  require 'tournament/swiss'
7
5
  require 'tournament/round_robin'
8
6
  require 'tournament/page_playoff'
data/lib/tournament.rb ADDED
@@ -0,0 +1,6 @@
1
+ # This library is split into two parts, there's the actual algorithms that
2
+ # implement various tournament systems ({Algorithm}) and a data abstraction
3
+ # layer for generating matches using various tournament systems in a
4
+ # data-independent way ({Driver}).
5
+ module Tournament
6
+ end
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: 0.1.4
4
+ version: 0.1.5
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-06-02 00:00:00.000000000 Z
11
+ date: 2017-07-07 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler
@@ -41,16 +41,23 @@ files:
41
41
  - README.md
42
42
  - Rakefile
43
43
  - lib/tournament-system.rb
44
+ - lib/tournament.rb
45
+ - lib/tournament/algorithm.rb
46
+ - 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
+ - lib/tournament/algorithm/round_robin.rb
53
+ - lib/tournament/algorithm/single_bracket.rb
54
+ - lib/tournament/algorithm/swiss.rb
55
+ - lib/tournament/algorithm/util.rb
44
56
  - lib/tournament/driver.rb
45
57
  - lib/tournament/page_playoff.rb
46
58
  - lib/tournament/round_robin.rb
47
- - lib/tournament/seeder.rb
48
- - lib/tournament/seeder/none.rb
49
- - lib/tournament/seeder/random.rb
50
- - lib/tournament/seeder/single_bracket.rb
51
59
  - lib/tournament/single_elimination.rb
52
60
  - lib/tournament/swiss.rb
53
- - lib/tournament/swiss/common.rb
54
61
  - lib/tournament/swiss/dutch.rb
55
62
  - lib/tournament/version.rb
56
63
  - tournament-system.gemspec
@@ -1,12 +0,0 @@
1
- module Tournament
2
- module Seeder
3
- # Implements a fall-through tournament seeder. Does no seeding whatsoever.
4
- module None
5
- extend self
6
-
7
- def seed(teams)
8
- teams
9
- end
10
- end
11
- end
12
- end
@@ -1,14 +0,0 @@
1
- module Tournament
2
- module Seeder
3
- # A random seeder.
4
- class Random
5
- def initialize(random = nil)
6
- @random = random
7
- end
8
-
9
- def seed(teams)
10
- teams.shuffle(random: @random)
11
- end
12
- end
13
- end
14
- end
@@ -1,39 +0,0 @@
1
- module Tournament
2
- module Seeder
3
- # A seeder for a single-bracket tournament system.
4
- # Seeds teams such that the highest expected placing teams should get
5
- # furthest in the bracket.
6
- module SingleBracket
7
- extend self
8
-
9
- def seed(teams)
10
- unless (Math.log2(teams.length) % 1).zero?
11
- raise ArgumentError, 'Need power-of-2 teams'
12
- end
13
-
14
- teams = teams.map.with_index { |team, index| SeedTeam.new(team, index) }
15
- seed_bracket(teams).map(&:team)
16
- end
17
-
18
- private
19
-
20
- # Structure for wrapping a team with it's seed index
21
- SeedTeam = Struct.new(:team, :index)
22
-
23
- # Recursively seed the top half of the teams
24
- # and match teams reversed by index to the bottom half
25
- def seed_bracket(teams)
26
- return teams if teams.length <= 2
27
-
28
- top_half, bottom_half = teams.each_slice(teams.length / 2).to_a
29
- top_half = seed top_half
30
-
31
- top_half.map do |team|
32
- match = bottom_half[-team.index - 1]
33
-
34
- [team, match]
35
- end.flatten
36
- end
37
- end
38
- end
39
- end
@@ -1,10 +0,0 @@
1
- require 'tournament/seeder/none'
2
- require 'tournament/seeder/random'
3
- require 'tournament/seeder/single_bracket'
4
-
5
- module Tournament
6
- # Module containing tournament seeders.
7
- # Seeders are used by systems to define initial conditions.
8
- module Seeder
9
- end
10
- end
@@ -1,118 +0,0 @@
1
- module Tournament
2
- module Swiss
3
- # Common functions for swiss pairing systems..\
4
- module Common
5
- extend self
6
-
7
- # Iterate over each group, letting a team rollover into the next group
8
- # if a group has an odd number of teams
9
- def each_group_with_rollover(groups, group_keys)
10
- group_keys.each_with_index do |key, index|
11
- group = groups[key]
12
- # Drop teams to next group get an even number
13
- next_key = group_keys[index + 1]
14
- groups[next_key] << group.pop if group.length.odd? && next_key
15
-
16
- yield group
17
- end
18
- end
19
-
20
- # Groups teams by the score given by the driver
21
- def group_teams_by_score(driver, teams)
22
- groups = teams.group_by { |team| driver.get_team_score team }
23
- group_keys = groups.keys.sort.reverse.to_a
24
-
25
- [groups, group_keys]
26
- end
27
-
28
- # Merges small groups to the right (if possible) such that all groups
29
- # are larger than min_size.
30
- # rubocop:disable Metrics/MethodLength, Metrics/AbcSize
31
- # :reek:TooManyStatements
32
- def merge_small_groups(groups, group_keys, min_size)
33
- new_keys = []
34
-
35
- group_keys.each_with_index do |key, index|
36
- group = groups[key]
37
-
38
- # Merge small groups into an adjacent group
39
- if group.length < min_size
40
- groups.delete(key)
41
-
42
- # When there is an adjacent lesser group, merge into that one
43
- new_key = group_keys[index + 1]
44
- if new_key
45
- groups[new_key] = group + groups[new_key]
46
- # If there isn't, merge into the adjacent greater group
47
- else
48
- new_key = new_keys[-1]
49
-
50
- if new_key
51
- groups[new_key] += group
52
- else
53
- # If there are no new keys just use the current key
54
- new_keys << key
55
- groups[key] = group
56
- end
57
- end
58
- # Leave larger groups the way they are
59
- else
60
- new_keys << key
61
- groups[key] = group
62
- end
63
- end
64
-
65
- new_keys
66
- end
67
- # rubocop:enable Metrics/MethodLength, Metrics/AbcSize
68
-
69
- # Get a set of already played matches. Matches are also sets
70
- def matches_set(driver)
71
- existing_matches = Hash.new(0)
72
- driver.matches.each do |match|
73
- match_teams = Set.new driver.get_match_teams match
74
- existing_matches[match_teams] += 1
75
- end
76
- existing_matches
77
- end
78
-
79
- # Check whether any match has already been played
80
- def any_match_exists?(matches, existing_matches)
81
- matches.any? { |match| existing_matches.include?(Set.new(match)) }
82
- end
83
-
84
- # Count the number of matches already played
85
- def count_existing_matches(matches, existing_matches)
86
- matches.map { |match| existing_matches[Set.new(match)] }.reduce(:+)
87
- end
88
-
89
- # Finds the first permutation of teams that has a unique pairing.
90
- # If none are found, the pairing that has the least duplicate matches
91
- # is returned.
92
- # rubocop:disable Metrics/MethodLength
93
- def first_permutation_pairing(teams, existing_matches)
94
- min_dups = Float::INFINITY
95
- best_matches = nil
96
-
97
- # Find the first permutation that has no duplicate matches
98
- # Or the permutation with the least duplicate matches
99
- teams.permutation.each do |variation|
100
- matches = (yield variation).to_a
101
- dup_count = count_existing_matches(matches, existing_matches)
102
-
103
- # Quick exit when there are no duplicates
104
- return matches if dup_count.zero?
105
-
106
- # Update best stats as we go along
107
- if dup_count < min_dups
108
- min_dups = dup_count
109
- best_matches = matches
110
- end
111
- end
112
-
113
- best_matches
114
- end
115
- # rubocop:enable Metrics/MethodLength
116
- end
117
- end
118
- end