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.
- checksums.yaml +4 -4
- data/lib/tournament/algorithm/page_playoff.rb +34 -0
- data/lib/tournament/algorithm/pairers/adjacent.rb +21 -0
- data/lib/tournament/algorithm/pairers/best_min_duplicates.rb +55 -0
- data/lib/tournament/algorithm/pairers/halves.rb +32 -0
- data/lib/tournament/algorithm/pairers/multi.rb +33 -0
- data/lib/tournament/algorithm/pairers.rb +7 -0
- data/lib/tournament/algorithm/round_robin.rb +60 -0
- data/lib/tournament/algorithm/single_bracket.rb +104 -0
- data/lib/tournament/algorithm/swiss.rb +83 -0
- data/lib/tournament/algorithm/util.rb +100 -0
- data/lib/tournament/algorithm.rb +6 -0
- data/lib/tournament/driver.rb +100 -9
- data/lib/tournament/page_playoff.rb +21 -18
- data/lib/tournament/round_robin.rb +27 -33
- data/lib/tournament/single_elimination.rb +23 -44
- data/lib/tournament/swiss/dutch.rb +39 -37
- data/lib/tournament/swiss.rb +16 -21
- data/lib/tournament/version.rb +2 -1
- data/lib/tournament-system.rb +0 -2
- data/lib/tournament.rb +6 -0
- metadata +14 -7
- data/lib/tournament/seeder/none.rb +0 -12
- data/lib/tournament/seeder/random.rb +0 -14
- data/lib/tournament/seeder/single_bracket.rb +0 -39
- data/lib/tournament/seeder.rb +0 -10
- data/lib/tournament/swiss/common.rb +0 -118
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 8079e2152df4c6c3ba2f084b80ed9b0c25f24135
|
4
|
+
data.tar.gz: 80e2cbcaccdbf8f4a96543ea02327722ef0519b2
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 98ca2b3ca59a77928148846a64a0020a511a34456694e15b9c2b1c60b8dfba60957c1814dec414e6aa3b14dbfd5ced19568dae3353cd06346297f6d23cae9035
|
7
|
+
data.tar.gz: bb76ea854b4f13efc63205c4c2e0555a92b99b2a2659449ad41e561ce66cc4aa9f00b6e61d41640586cd96bebf349f1e0e2833ecb788a4d577c4ba8fa87fc222
|
@@ -0,0 +1,34 @@
|
|
1
|
+
module Tournament
|
2
|
+
module Algorithm
|
3
|
+
# This module provides algorithms for dealing with the page playoff system.
|
4
|
+
module PagePlayoff
|
5
|
+
extend self
|
6
|
+
|
7
|
+
# The total number of rounds needed for all page playoff tournaments.
|
8
|
+
TOTAL_ROUNDS = 3
|
9
|
+
|
10
|
+
# :reek:ControlParameter
|
11
|
+
|
12
|
+
# Guess the next round (starting at 0) for page playoff.
|
13
|
+
#
|
14
|
+
# @param matches_count [Integer] the number of existing matches
|
15
|
+
# @return [Integer]
|
16
|
+
# @raise [ArgumentError] when the number of matches doesn't add up
|
17
|
+
def guess_round(matches_count)
|
18
|
+
round = MATCH_ROUND_MAP[matches_count]
|
19
|
+
|
20
|
+
raise ArgumentError, 'Invalid number of matches' unless round
|
21
|
+
|
22
|
+
round
|
23
|
+
end
|
24
|
+
|
25
|
+
private
|
26
|
+
|
27
|
+
MATCH_ROUND_MAP = {
|
28
|
+
0 => 0,
|
29
|
+
2 => 1,
|
30
|
+
3 => 2,
|
31
|
+
}.freeze
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,21 @@
|
|
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
|
@@ -0,0 +1,55 @@
|
|
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
|
@@ -0,0 +1,32 @@
|
|
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
|
@@ -0,0 +1,33 @@
|
|
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
|
@@ -0,0 +1,60 @@
|
|
1
|
+
require 'tournament/algorithm/util'
|
2
|
+
require 'tournament/algorithm/pairers/halves'
|
3
|
+
|
4
|
+
module Tournament
|
5
|
+
module Algorithm
|
6
|
+
# This module provides algorithms for dealing with round robin tournament
|
7
|
+
# systems.
|
8
|
+
module RoundRobin
|
9
|
+
extend self
|
10
|
+
|
11
|
+
# Calculates the total number of rounds needed for round robin with a
|
12
|
+
# certain amount of teams.
|
13
|
+
#
|
14
|
+
# @param teams_count [Integer] the number of teams
|
15
|
+
# @return [Integer] number of rounds needed for round robin
|
16
|
+
def total_rounds(teams_count)
|
17
|
+
Util.padded_teams_count(teams_count) - 1
|
18
|
+
end
|
19
|
+
|
20
|
+
# Guess the next round (starting at 0) for round robin.
|
21
|
+
#
|
22
|
+
# @param teams_count [Integer] the number of teams
|
23
|
+
# @param matches_count [Integer] the number of existing matches
|
24
|
+
# @return [Integer] next round number
|
25
|
+
def guess_round(teams_count, matches_count)
|
26
|
+
matches_count / (Util.padded_teams_count(teams_count) / 2)
|
27
|
+
end
|
28
|
+
|
29
|
+
# Rotate array using round robin.
|
30
|
+
#
|
31
|
+
# @param array [Array<>] array to rotate
|
32
|
+
# @param round [Integer] the round number, ie. amount to rotate by
|
33
|
+
def round_robin(array, round)
|
34
|
+
rotateable = array[1..-1]
|
35
|
+
|
36
|
+
[array[0]] + rotateable.rotate(-round)
|
37
|
+
end
|
38
|
+
|
39
|
+
# Enumerate all round robin rotations.
|
40
|
+
def round_robin_enum(array)
|
41
|
+
Array.new(total_rounds(array.length)) do |index|
|
42
|
+
round_robin(array, index)
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
# Rotates teams and pairs them for a round of round robin.
|
47
|
+
#
|
48
|
+
# Uses {Pairers::Halves} for pairing after rotating.
|
49
|
+
#
|
50
|
+
# @param teams [Array<team>] teams playing
|
51
|
+
# @param round [Integer] the round number
|
52
|
+
# @return [Array<Array(team, team)>] the paired teams
|
53
|
+
def round_robin_pairing(teams, round)
|
54
|
+
rotated = round_robin(teams, round)
|
55
|
+
|
56
|
+
Pairers::Halves.pair(rotated, bottom_reversed: true)
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
@@ -0,0 +1,104 @@
|
|
1
|
+
require 'ostruct'
|
2
|
+
|
3
|
+
module Tournament
|
4
|
+
module Algorithm
|
5
|
+
# This module provides algorithms for dealing with single bracket
|
6
|
+
# elimination tournament systems.
|
7
|
+
module SingleBracket
|
8
|
+
extend self
|
9
|
+
|
10
|
+
# Calculates the total number of rounds needed for a single bracket
|
11
|
+
# tournament with a certain number of teams.
|
12
|
+
#
|
13
|
+
# @param teams_count [Integer] the number of teams
|
14
|
+
# @return [Integer] number of rounds needed for round robin
|
15
|
+
def total_rounds(teams_count)
|
16
|
+
Math.log2(teams_count).ceil
|
17
|
+
end
|
18
|
+
|
19
|
+
# Calculates the maximum number of teams that can play in a single bracket
|
20
|
+
# tournament with a given number of rounds.
|
21
|
+
#
|
22
|
+
# @param rounds [Integer] the number of rounds
|
23
|
+
# @return [Integer] number of teams that could play
|
24
|
+
def max_teams(rounds)
|
25
|
+
2**rounds
|
26
|
+
end
|
27
|
+
|
28
|
+
# Guess the next round (starting at 0) for a single bracket tournament.
|
29
|
+
#
|
30
|
+
# @param teams_count [Integer] the number of teams
|
31
|
+
# @param matches_count [Integer] the number of existing matches
|
32
|
+
# @return [Integer] next round number
|
33
|
+
# @raise [ArgumentError] when the number of matches does not add up
|
34
|
+
def guess_round(teams_count, matches_count)
|
35
|
+
rounds = total_rounds(teams_count)
|
36
|
+
total_teams = max_teams(rounds)
|
37
|
+
|
38
|
+
# Make sure we don't have too many matches
|
39
|
+
unless total_teams >= matches_count
|
40
|
+
raise ArgumentError, 'Too many matches'
|
41
|
+
end
|
42
|
+
|
43
|
+
round = rounds - Math.log2(total_teams - matches_count)
|
44
|
+
unless (round % 1).zero?
|
45
|
+
# Make sure we don't have some weird number of matches
|
46
|
+
raise ArgumentError, 'Invalid number of matches'
|
47
|
+
end
|
48
|
+
|
49
|
+
round.to_i
|
50
|
+
end
|
51
|
+
|
52
|
+
# Padd an array of teams to the next highest power of 2.
|
53
|
+
#
|
54
|
+
# @param teams [Array<team>]
|
55
|
+
# @return [Array<team, nil>]
|
56
|
+
def padd_teams(teams)
|
57
|
+
required = max_teams(total_rounds(teams.length))
|
58
|
+
|
59
|
+
# Insert the padding at the bottom to give top teams byes first
|
60
|
+
Array.new(required) { |index| teams[index] }
|
61
|
+
end
|
62
|
+
|
63
|
+
# Seed teams for a single bracket tournament.
|
64
|
+
#
|
65
|
+
# Seed in a way that teams earlier in +teams+ always win against later
|
66
|
+
# ones, the first team plays the second in the finals, the 3rd and 4th get
|
67
|
+
# nocked out in the semi-finals, etc.
|
68
|
+
#
|
69
|
+
# Designed to be used with {Pairers::Adjacent}.
|
70
|
+
#
|
71
|
+
# @param teams [Array<Team>]
|
72
|
+
# @return [Array<team>]
|
73
|
+
# @raise [ArgumentError] when the number of teams is not a power of 2
|
74
|
+
def seed(teams)
|
75
|
+
unless (Math.log2(teams.length) % 1).zero?
|
76
|
+
raise ArgumentError, 'Need power-of-2 teams'
|
77
|
+
end
|
78
|
+
|
79
|
+
teams = teams.map.with_index do |team, index|
|
80
|
+
OpenStruct.new(team: team, index: index)
|
81
|
+
end
|
82
|
+
seed_bracket(teams).map(&:team)
|
83
|
+
end
|
84
|
+
|
85
|
+
private
|
86
|
+
|
87
|
+
# Recursively seed the top half of the teams and match teams reversed by
|
88
|
+
# index to the bottom half.
|
89
|
+
def seed_bracket(teams)
|
90
|
+
return teams if teams.length <= 2
|
91
|
+
|
92
|
+
top_half, bottom_half = teams.each_slice(teams.length / 2).to_a
|
93
|
+
top_half = seed_bracket top_half
|
94
|
+
|
95
|
+
top_half.map do |team|
|
96
|
+
# match with the team appropriate team in the bottom half
|
97
|
+
match = bottom_half[-team.index - 1]
|
98
|
+
|
99
|
+
[team, match]
|
100
|
+
end.flatten
|
101
|
+
end
|
102
|
+
end
|
103
|
+
end
|
104
|
+
end
|
@@ -0,0 +1,83 @@
|
|
1
|
+
module Tournament
|
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.
|
7
|
+
module Swiss
|
8
|
+
extend self
|
9
|
+
|
10
|
+
# Calculates the minimum number of rounds needed to properly order teams
|
11
|
+
# using the swiss tournament system.
|
12
|
+
#
|
13
|
+
# @param teams_count [Integer] the number of teams
|
14
|
+
# @return [Integer]
|
15
|
+
def minimum_rounds(teams_count)
|
16
|
+
Math.log2(teams_count).ceil
|
17
|
+
end
|
18
|
+
|
19
|
+
# Groups teams by their score.
|
20
|
+
#
|
21
|
+
# @param teams [Array<team>] the teams to group
|
22
|
+
# @param scores [Hash{team => Number}] the scores of each team
|
23
|
+
# @return [Array<Array<team>>] groups of teams sorted
|
24
|
+
# with the highest score at the front
|
25
|
+
def group_teams_by_score(teams, scores)
|
26
|
+
groups = teams.group_by { |team| scores[team] || 0 }
|
27
|
+
sorted_keys = groups.keys.sort.reverse
|
28
|
+
|
29
|
+
sorted_keys.map { |key| groups[key] }
|
30
|
+
end
|
31
|
+
|
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
|
+
# Rollover the last person in each group if its odd.
|
65
|
+
# This assumes that the total number of players in all groups is even.
|
66
|
+
# Rollover is performed in-place.
|
67
|
+
#
|
68
|
+
# @param groups [Array<Array<team>>] groups of teams
|
69
|
+
# @return [void]
|
70
|
+
def rollover_groups(groups)
|
71
|
+
groups.each_with_index do |group, index|
|
72
|
+
# Move last from the current group to the front of the next group
|
73
|
+
groups[index + 1].unshift group.pop if group.length.odd?
|
74
|
+
end
|
75
|
+
|
76
|
+
# Remove any empty groups
|
77
|
+
groups.reject!(&:empty?)
|
78
|
+
|
79
|
+
nil
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|