tournament-system 0.1.4 → 0.1.5
Sign up to get free protection for your applications and to get access to all the features.
- 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
|