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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: bf472f14954ad05551b972f6001a53dbfdbe9f0a
4
- data.tar.gz: 41769df3e3b025ff4f4b0986e33585e845b37fcd
3
+ metadata.gz: 8079e2152df4c6c3ba2f084b80ed9b0c25f24135
4
+ data.tar.gz: 80e2cbcaccdbf8f4a96543ea02327722ef0519b2
5
5
  SHA512:
6
- metadata.gz: b38f47ed72a2a1765f78b9d706702670cf4aa84457dc399c41fdce30f7e0f2a3dfc0b5ac8ea6a27fd95e93a7726c7629d9067b784f27458a09b37b1d624ba63c
7
- data.tar.gz: 658ec1108455ad0902c288c2d510588a91d07be6dc8200c759784d79b2e74eef3ecda3864334b53af8b6f563ffe1d7ed48c234d4505ab8c56bc9a90fc5848c9c
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,7 @@
1
+ module Tournament
2
+ module Algorithm
3
+ # This module provides multiple pairing algorithms
4
+ module Pairers
5
+ end
6
+ end
7
+ 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