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 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