sports-manager 0.0.1

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.
Files changed (62) hide show
  1. checksums.yaml +7 -0
  2. data/.rspec +1 -0
  3. data/.rubocop.yml +32 -0
  4. data/CODE_OF_CONDUCT.md +132 -0
  5. data/CONTRIBUTING.md +72 -0
  6. data/MIT-LICENSE +20 -0
  7. data/README.md +419 -0
  8. data/Rakefile +4 -0
  9. data/lib/sports-manager.rb +59 -0
  10. data/lib/sports_manager/algorithms/filtering/no_overlap.rb +52 -0
  11. data/lib/sports_manager/algorithms/ordering/multiple_matches_participant.rb +78 -0
  12. data/lib/sports_manager/bye_match.rb +62 -0
  13. data/lib/sports_manager/constraint_builder.rb +30 -0
  14. data/lib/sports_manager/constraints/all_different_constraint.rb +24 -0
  15. data/lib/sports_manager/constraints/match_constraint.rb +37 -0
  16. data/lib/sports_manager/constraints/multi_category_constraint.rb +49 -0
  17. data/lib/sports_manager/constraints/next_round_constraint.rb +48 -0
  18. data/lib/sports_manager/constraints/no_overlapping_constraint.rb +55 -0
  19. data/lib/sports_manager/double_team.rb +7 -0
  20. data/lib/sports_manager/group.rb +42 -0
  21. data/lib/sports_manager/group_builder.rb +72 -0
  22. data/lib/sports_manager/helper.rb +228 -0
  23. data/lib/sports_manager/json_helper.rb +129 -0
  24. data/lib/sports_manager/match.rb +91 -0
  25. data/lib/sports_manager/match_builder.rb +112 -0
  26. data/lib/sports_manager/matches/algorithms/single_elimination_algorithm.rb +94 -0
  27. data/lib/sports_manager/matches/next_round.rb +38 -0
  28. data/lib/sports_manager/matches_generator.rb +33 -0
  29. data/lib/sports_manager/nil_team.rb +24 -0
  30. data/lib/sports_manager/participant.rb +23 -0
  31. data/lib/sports_manager/single_team.rb +7 -0
  32. data/lib/sports_manager/solution_drawer/cli/solution_table.rb +38 -0
  33. data/lib/sports_manager/solution_drawer/cli/table.rb +94 -0
  34. data/lib/sports_manager/solution_drawer/cli.rb +75 -0
  35. data/lib/sports_manager/solution_drawer/mermaid/bye_node.rb +39 -0
  36. data/lib/sports_manager/solution_drawer/mermaid/gantt.rb +126 -0
  37. data/lib/sports_manager/solution_drawer/mermaid/graph.rb +111 -0
  38. data/lib/sports_manager/solution_drawer/mermaid/node.rb +55 -0
  39. data/lib/sports_manager/solution_drawer/mermaid/node_style.rb +89 -0
  40. data/lib/sports_manager/solution_drawer/mermaid/solution_gantt.rb +57 -0
  41. data/lib/sports_manager/solution_drawer/mermaid/solution_graph.rb +76 -0
  42. data/lib/sports_manager/solution_drawer/mermaid.rb +65 -0
  43. data/lib/sports_manager/solution_drawer.rb +23 -0
  44. data/lib/sports_manager/team.rb +47 -0
  45. data/lib/sports_manager/team_builder.rb +31 -0
  46. data/lib/sports_manager/timeslot.rb +37 -0
  47. data/lib/sports_manager/timeslot_builder.rb +50 -0
  48. data/lib/sports_manager/tournament/setting.rb +45 -0
  49. data/lib/sports_manager/tournament.rb +69 -0
  50. data/lib/sports_manager/tournament_builder.rb +123 -0
  51. data/lib/sports_manager/tournament_day/validator.rb +69 -0
  52. data/lib/sports_manager/tournament_day.rb +50 -0
  53. data/lib/sports_manager/tournament_generator.rb +183 -0
  54. data/lib/sports_manager/tournament_problem_builder.rb +106 -0
  55. data/lib/sports_manager/tournament_solution/bye_fixture.rb +21 -0
  56. data/lib/sports_manager/tournament_solution/fixture.rb +39 -0
  57. data/lib/sports_manager/tournament_solution/serializer.rb +107 -0
  58. data/lib/sports_manager/tournament_solution/solution.rb +85 -0
  59. data/lib/sports_manager/tournament_solution.rb +34 -0
  60. data/lib/sports_manager/version.rb +5 -0
  61. data/sports-manager.gemspec +35 -0
  62. metadata +120 -0
@@ -0,0 +1,228 @@
1
+ # frozen_string_literal: true
2
+
3
+ # rubocop:disable Metrics/MethodLength, Metrics/ModuleLength
4
+ module SportsManager
5
+ # Public: Predefined payloads for validation
6
+ module Helper
7
+ module_function
8
+
9
+ def minimal
10
+ {
11
+ when: {
12
+ '2023-09-09': { start: 9, end: 20 }
13
+ },
14
+ courts: 1,
15
+ game_length: 60,
16
+ rest_break: 30,
17
+ single_day_matches: false,
18
+ subscriptions: {
19
+ mixed_single: [{ id: 1, name: 'João' }, { id: 34, name: 'Cleber' }]
20
+ },
21
+ matches: {
22
+ mixed_single: [[1, 34]]
23
+ }
24
+ }
25
+ end
26
+
27
+ def simple
28
+ {
29
+ when: {
30
+ '2023-09-09': { start: 9, end: 20 }
31
+ },
32
+ courts: 1,
33
+ game_length: 60,
34
+ rest_break: 30,
35
+ single_day_matches: false,
36
+ subscriptions: {
37
+ mixed_single: [
38
+ { id: 1, name: 'João' }, { id: 5, name: 'Carlos' },
39
+ { id: 10, name: 'Daniel' }, { id: 17, name: 'Laura' },
40
+ { id: 25, name: 'Joana' }, { id: 29, name: 'Carolina' },
41
+ { id: 33, name: 'Erica' }, { id: 34, name: 'Cleber' }
42
+ ]
43
+ },
44
+ matches: {
45
+ mixed_single: [[1, 34], [5, 33], [10, 29], [17, 25]]
46
+ }
47
+ }
48
+ end
49
+
50
+ def simple_multi_court
51
+ simple.merge(courts: 2)
52
+ end
53
+
54
+ def simple_odd_matches
55
+ {
56
+ when: {
57
+ '2023-09-09': { start: 9, end: 20 }
58
+ },
59
+ courts: 2,
60
+ game_length: 60,
61
+ rest_break: 30,
62
+ single_day_matches: false,
63
+ subscriptions: {
64
+ mixed_single: [
65
+ { id: 1, name: 'João' }, { id: 5, name: 'Carlos' },
66
+ { id: 10, name: 'Daniel' }, { id: 29, name: 'Carolina' },
67
+ { id: 33, name: 'Erica' }, { id: 34, name: 'Cleber' }
68
+ ]
69
+ },
70
+ matches: {
71
+ mixed_single: [[1], [5, 34], [10, 29], [33]]
72
+ }
73
+ }
74
+ end
75
+
76
+ def complete
77
+ {
78
+ when: {
79
+ '2023-09-09': { start: 9, end: 20 },
80
+ '2023-09-10': { start: 9, end: 13 }
81
+ },
82
+ courts: 2,
83
+ game_length: 60,
84
+ rest_break: 30,
85
+ single_day_matches: false,
86
+ subscriptions: {
87
+ mens_single: [
88
+ { id: 1, name: 'João' }, { id: 2, name: 'Marcelo' },
89
+ { id: 3, name: 'José' }, { id: 4, name: 'Pedro' },
90
+ { id: 5, name: 'Carlos' }, { id: 6, name: 'Leandro' },
91
+ { id: 7, name: 'Leonardo' }, { id: 8, name: 'Cláudio' },
92
+ { id: 9, name: 'Alexandre' }, { id: 10, name: 'Daniel' },
93
+ { id: 11, name: 'Marcos' }, { id: 12, name: 'Henrique' },
94
+ { id: 13, name: 'Joaquim' }, { id: 14, name: 'Alex' },
95
+ { id: 15, name: 'Bruno' }, { id: 16, name: 'Fábio' }
96
+ ]
97
+ },
98
+ matches: {
99
+ mens_single: [
100
+ [1, 16],
101
+ [2, 15],
102
+ [3, 14],
103
+ [4, 13],
104
+ [5, 12],
105
+ [6, 11],
106
+ [7, 10],
107
+ [8, 9]
108
+ ]
109
+ }
110
+ }
111
+ end
112
+
113
+ def complex
114
+ {
115
+ when: {
116
+ '2023-09-09': { start: 9, end: 20 },
117
+ '2023-09-10': { start: 9, end: 13 }
118
+ },
119
+ courts: 2,
120
+ game_length: 60,
121
+ rest_break: 30,
122
+ single_day_matches: false,
123
+ subscriptions: {
124
+ mens_single: [
125
+ { id: 1, name: 'João' }, { id: 2, name: 'Marcelo' },
126
+ { id: 3, name: 'José' }, { id: 4, name: 'Pedro' },
127
+ { id: 5, name: 'Carlos' }, { id: 6, name: 'Leandro' },
128
+ { id: 7, name: 'Leonardo' }, { id: 8, name: 'Cláudio' },
129
+ { id: 9, name: 'Alexandre' }, { id: 10, name: 'Daniel' },
130
+ { id: 11, name: 'Marcos' }, { id: 12, name: 'Henrique' },
131
+ { id: 13, name: 'Joaquim' }, { id: 14, name: 'Alex' },
132
+ { id: 15, name: 'Bruno' }, { id: 16, name: 'Fábio' }
133
+ ],
134
+ womens_double: [
135
+ [{ id: 17, name: 'Laura' }, { id: 18, name: 'Karina' }],
136
+ [{ id: 19, name: 'Camila' }, { id: 20, name: 'Bruna' }],
137
+ [{ id: 21, name: 'Aline' }, { id: 22, name: 'Cintia' }],
138
+ [{ id: 23, name: 'Maria' }, { id: 24, name: 'Elis' }],
139
+ [{ id: 25, name: 'Joana' }, { id: 26, name: 'Izadora' }],
140
+ [{ id: 27, name: 'Claudia' }, { id: 28, name: 'Marina' }],
141
+ [{ id: 29, name: 'Carolina' }, { id: 30, name: 'Patricia' }],
142
+ [{ id: 31, name: 'Jéssica' }, { id: 32, name: 'Daniela' }]
143
+ ],
144
+ mixed_single: [
145
+ { id: 1, name: 'João' }, { id: 5, name: 'Carlos' },
146
+ { id: 10, name: 'Daniel' }, { id: 17, name: 'Laura' },
147
+ { id: 25, name: 'Joana' }, { id: 29, name: 'Carolina' },
148
+ { id: 33, name: 'Erica' }, { id: 34, name: 'Cleber' }
149
+ ]
150
+ },
151
+ matches: {
152
+ mens_single: [
153
+ [1, 16],
154
+ [2, 15],
155
+ [3, 14],
156
+ [4, 13],
157
+ [5, 12],
158
+ [6, 11],
159
+ [7, 10],
160
+ [8, 9]
161
+ ],
162
+ womens_double: [
163
+ [[17, 18], [31, 32]],
164
+ [[19, 20], [29, 30]],
165
+ [[21, 22], [27, 28]],
166
+ [[23, 24], [25, 26]]
167
+ ],
168
+ mixed_single: [
169
+ [1, 34],
170
+ [5, 33],
171
+ [10, 29],
172
+ [17, 25]
173
+ ]
174
+ }
175
+ }
176
+ end
177
+
178
+ def duplicate
179
+ {
180
+ when: { '2023-09-09': { start: 9, end: 20 } },
181
+ courts: 1,
182
+ game_length: 60,
183
+ rest_break: 30,
184
+ single_day_matches: false,
185
+ subscriptions: {
186
+ mens_single: [
187
+ { id: 1, name: 'João' },
188
+ { id: 2, name: 'Marcelo' }
189
+ ],
190
+ mixed_single: [
191
+ { id: 1, name: 'João' },
192
+ { id: 5, name: 'Carlos' },
193
+ { id: 10, name: 'Daniel' },
194
+ { id: 17, name: 'Laura' }
195
+ ]
196
+ },
197
+ matches: {
198
+ mens_single: [[1, 2]],
199
+ mixed_single: [[1, 10], [5, 17]]
200
+ }
201
+ }
202
+ end
203
+
204
+ def no_solution
205
+ {
206
+ when: {
207
+ '2023-09-09': { start: 9, end: 10 }
208
+ },
209
+ courts: 1,
210
+ game_length: 60,
211
+ rest_break: 30,
212
+ single_day_matches: false,
213
+ subscriptions: {
214
+ mixed_single: [
215
+ { id: 1, name: 'João' }, { id: 5, name: 'Carlos' },
216
+ { id: 10, name: 'Daniel' }, { id: 17, name: 'Laura' },
217
+ { id: 25, name: 'Joana' }, { id: 29, name: 'Carolina' },
218
+ { id: 33, name: 'Erica' }, { id: 34, name: 'Cleber' }
219
+ ]
220
+ },
221
+ matches: {
222
+ mixed_single: [[1, 34], [5, 33], [10, 29], [17, 25]]
223
+ }
224
+ }
225
+ end
226
+ end
227
+ end
228
+ # rubocop:enable Metrics/MethodLength, Metrics/ModuleLength
@@ -0,0 +1,129 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SportsManager
4
+ module JsonHelper
5
+ def as_json(_options = nil)
6
+ instance_variables.each_with_object({}) do |var, hash|
7
+ key = var.to_s.delete('@').to_sym
8
+ value = instance_variable_get(var)
9
+ hash[key] = JsonHelper.convert_value(value)
10
+ end
11
+ end
12
+
13
+ def self.deep_symbolize_keys(object)
14
+ case object
15
+ when Hash
16
+ object.each_with_object({}) do |(k, v), result|
17
+ key = begin
18
+ k.to_sym
19
+ rescue StandardError
20
+ k
21
+ end
22
+ result[key] = deep_symbolize_keys(v)
23
+ end
24
+ when Array
25
+ object.map { |v| deep_symbolize_keys(v) }
26
+ else
27
+ object
28
+ end
29
+ end
30
+
31
+ def self.convert_value(value)
32
+ case value
33
+ when Hash
34
+ value.transform_keys(&:to_sym).transform_values { |v| convert_value(v) }
35
+ when Array
36
+ value.map { |v| convert_value(v) }
37
+ else
38
+ value.respond_to?(:as_json) ? value.as_json : value
39
+ end
40
+ end
41
+
42
+ def self.convert_custom_object(object)
43
+ object.instance_variables.each_with_object({}) do |var, hash|
44
+ key = var.to_s.delete('@').to_sym
45
+ hash[key] = convert_value(object.instance_variable_get(var))
46
+ end
47
+ end
48
+ end
49
+ end
50
+
51
+ class Hash
52
+ def as_json(_options = nil)
53
+ transform_keys(&:to_s).transform_values do |value|
54
+ value.respond_to?(:as_json) ? value.as_json : value
55
+ end
56
+ end
57
+
58
+ def deep_symbolize_keys
59
+ SportsManager::JsonHelper.deep_symbolize_keys(self)
60
+ end
61
+ end
62
+
63
+ class Array
64
+ def as_json(_options = nil)
65
+ map do |value|
66
+ value.respond_to?(:as_json) ? value.as_json : value
67
+ end
68
+ end
69
+
70
+ def deep_symbolize_keys
71
+ SportsManager::JsonHelper.deep_symbolize_keys(self)
72
+ end
73
+ end
74
+
75
+ class Symbol
76
+ def as_json(_options = nil)
77
+ to_s
78
+ end
79
+ end
80
+
81
+ class Numeric
82
+ def as_json(_options = nil)
83
+ self
84
+ end
85
+ end
86
+
87
+ class String
88
+ def as_json(_options = nil)
89
+ self
90
+ end
91
+ end
92
+
93
+ class TrueClass
94
+ def as_json(_options = nil)
95
+ self
96
+ end
97
+ end
98
+
99
+ class FalseClass
100
+ def as_json(_options = nil)
101
+ self
102
+ end
103
+ end
104
+
105
+ class NilClass
106
+ def as_json(_options = nil)
107
+ self
108
+ end
109
+ end
110
+
111
+ class Time
112
+ def as_json(_options = nil)
113
+ xmlschema(3)
114
+ end
115
+ end
116
+
117
+ class Object
118
+ def as_json(options = nil)
119
+ if respond_to?(:attributes)
120
+ attributes.as_json(options)
121
+ else
122
+ instance_variables.each_with_object({}) do |var, hash|
123
+ key = var.to_s.delete('@').to_sym
124
+ value = instance_variable_get(var)
125
+ hash[key] = SportsManager::JsonHelper.convert_value(value)
126
+ end
127
+ end
128
+ end
129
+ end
@@ -0,0 +1,91 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SportsManager
4
+ # Public: A category's match with teams, which rounds is ocurring and which
5
+ # matches its depends on before happening.
6
+ class Match
7
+ attr_reader :id, :category, :team1, :team2, :round, :teams, :depends_on
8
+
9
+ def self.build_next_match(category:, depends_on:, id: nil, round: 0)
10
+ new(
11
+ id: id,
12
+ category: category,
13
+ round: round,
14
+ depends_on: depends_on,
15
+ team1: NilTeam.new(category: category),
16
+ team2: NilTeam.new(category: category)
17
+ )
18
+ end
19
+
20
+ def initialize(category:, id: nil, team1: nil, team2: nil, round: 0, depends_on: []) # rubocop:disable Metrics/ParameterLists
21
+ @id = id
22
+ @category = category
23
+ @team1 = team1
24
+ @team2 = team2
25
+ @round = round
26
+ @teams = [team1, team2].compact
27
+ @depends_on = depends_on
28
+ end
29
+
30
+ def playable?
31
+ true
32
+ end
33
+
34
+ def participants
35
+ @participants ||= teams.map(&:participants).flatten
36
+ end
37
+
38
+ def dependencies?
39
+ depends_on && !depends_on.empty?
40
+ end
41
+
42
+ def dependencies
43
+ @dependencies ||= depends_on
44
+ .flat_map { |match| [match, *match.depends_on] }
45
+ end
46
+
47
+ def playable_dependencies
48
+ depends_on.select(&:playable?)
49
+ end
50
+
51
+ def previous_matches?
52
+ previous_matches && !previous_matches.empty?
53
+ end
54
+
55
+ def previous_matches
56
+ playable_dependencies.flat_map { |match| [match, *match.previous_matches] }
57
+ end
58
+
59
+ def title(title_format: 'M%<id>s')
60
+ match_participants = if previous_matches?
61
+ depends_on_names(title_format)
62
+ else
63
+ teams_names
64
+ end
65
+
66
+ match_participants.join(' vs. ')
67
+ end
68
+
69
+ def teams_names
70
+ teams.map(&:name)
71
+ end
72
+
73
+ def ==(other)
74
+ return false unless instance_of?(other.class)
75
+
76
+ id == other.id && category == other.category && round == other.round
77
+ end
78
+
79
+ private
80
+
81
+ def depends_on_names(title_format)
82
+ depends_on.map do |match|
83
+ if match.playable?
84
+ format(title_format, id: match.id)
85
+ else
86
+ match.teams_names.join(' vs.')
87
+ end
88
+ end
89
+ end
90
+ end
91
+ end
@@ -0,0 +1,112 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SportsManager
4
+ # Public: Build matches objects from category, teams, and list of matches.
5
+ class MatchBuilder
6
+ attr_reader :category, :matches, :teams, :builded_matches, :tournament_type
7
+
8
+ INITIAL_ID = 1
9
+ DEFAULT_MATCH_CLASS = Match
10
+ BYE_MATCH_CLASS = ByeMatch
11
+ NIL_TEAM = NilTeam
12
+
13
+ def initialize(category:, matches:, teams:, tournament_type:)
14
+ @category = category
15
+ @teams = teams
16
+ @matches = matches_completer(matches)
17
+ @builded_matches = []
18
+ @tournament_type = tournament_type
19
+ end
20
+
21
+ def build
22
+ return build_already_generated_matches if generated_matches_structure?
23
+
24
+ build_matches
25
+ end
26
+
27
+ private
28
+
29
+ def participant_ids
30
+ return matches unless generated_matches_structure?
31
+
32
+ matches.map do |match|
33
+ match[:participants]
34
+ end
35
+ end
36
+
37
+ def generated_matches_structure?
38
+ matches&.first.is_a?(Hash)
39
+ end
40
+
41
+ def build_already_generated_matches
42
+ matches.each do |match|
43
+ builded_matches << build_match(match_id: match[:id],
44
+ participant_ids: match[:participants] || [],
45
+ round: match[:round] || 0,
46
+ depends_on: match[:depends_on] || [])
47
+ end
48
+ builded_matches
49
+ end
50
+
51
+ def build_matches
52
+ initial_matches | future_matches
53
+ end
54
+
55
+ def initial_matches
56
+ @initial_matches ||= participant_ids.map.with_index(INITIAL_ID) do |participant_ids, match_id|
57
+ build_match(match_id: match_id, participant_ids: participant_ids)
58
+ end
59
+ end
60
+
61
+ def build_match(match_id:, participant_ids:, round: 0, depends_on: [])
62
+ participant_ids
63
+ .map { |id| Array(id) }
64
+ .map { |id_array| find_team(id_array) }
65
+ .yield_self do |team1, team2|
66
+ initialize_match(match_id: match_id, teams: [team1, team2], round: round,
67
+ depends_on: depends_on, participant_ids: participant_ids)
68
+ end
69
+ end
70
+
71
+ def find_team(participant_ids)
72
+ teams.find do |team|
73
+ team.find_participants(participant_ids)
74
+ end
75
+ end
76
+
77
+ def initialize_match(match_id:, teams:, round:, depends_on: [], participant_ids: [])
78
+ klass = match_class(teams: teams, participant_ids: participant_ids)
79
+ team1, team2 = teams.map { |team| team || NIL_TEAM.new(category: category) }
80
+
81
+ depends_on_matches = builded_matches.map do |match|
82
+ depends_on.include?(match.id) ? match : nil
83
+ end.compact
84
+
85
+ klass.new(category: category, team1: team1, team2: team2, id: match_id, round: round,
86
+ depends_on: depends_on_matches)
87
+ end
88
+
89
+ def match_class(teams:, participant_ids:)
90
+ return BYE_MATCH_CLASS if teams.any?(&:nil?) && participant_ids.size == 1
91
+
92
+ DEFAULT_MATCH_CLASS
93
+ end
94
+
95
+ def future_matches
96
+ Matches::NextRound
97
+ .new(category: category, base_matches: initial_matches, algorithm: tournament_type)
98
+ .next_matches
99
+ end
100
+
101
+ def subscriptions_ids
102
+ teams.select { |team| team.category == category }
103
+ .map { |team| team.participants.map(&:id) }
104
+ end
105
+
106
+ def matches_completer(matches)
107
+ return MatchesGenerator.call(subscriptions_ids) if matches.nil? || matches.empty?
108
+
109
+ matches
110
+ end
111
+ end
112
+ end
@@ -0,0 +1,94 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SportsManager
4
+ module Matches
5
+ module Algorithms
6
+ # TODO: implement
7
+ # Public: Algorithm for building the rounds and matches in a
8
+ # Single Elimination Tournament. This format is also known as Knockout
9
+ class SingleEliminationAlgorithm
10
+ attr_reader :category, :opening_round_matches, :opening_round_size
11
+
12
+ def initialize(category:, matches:)
13
+ @category = category
14
+ @opening_round_matches = matches
15
+ @opening_round_size = matches.size
16
+ end
17
+
18
+ # TODO: BYE, odd matches
19
+ def next_matches
20
+ return @next_matches if defined? @next_matches
21
+
22
+ matches = opening_round_matches.dup
23
+
24
+ remaining_matches.times.reduce(0) do |count, _|
25
+ team1 = count
26
+ team2 = team1 + 1
27
+
28
+ matches << build_next_match(matches[team1], matches[team2], matches.size + 1)
29
+
30
+ team2 + 1
31
+ end
32
+
33
+ @next_matches = matches - opening_round_matches
34
+ end
35
+
36
+ # TODO: make use of it later
37
+ def needs_bye?
38
+ !power_of_two?
39
+ end
40
+
41
+ # Internal: The number of matches required to find a winner, without a
42
+ # third place match, is the number of players/teams minus one.
43
+ def total_matches
44
+ return 0 if teams_size.zero?
45
+
46
+ teams_size - 1
47
+ end
48
+
49
+ # Internal: The number of rounds is the closest Log2N for N players.
50
+ def total_rounds
51
+ return 0 if teams_size.zero?
52
+
53
+ Math.log2(teams_size).ceil
54
+ end
55
+
56
+ def round_for_match(match_number)
57
+ return 0 if match_number.zero? || opening_round_size.zero?
58
+
59
+ rounds = total_rounds
60
+
61
+ (1..rounds).each do |round|
62
+ matches_in_round = 2**(rounds - round)
63
+
64
+ return round if match_number <= matches_in_round
65
+
66
+ match_number -= matches_in_round
67
+ end
68
+ end
69
+
70
+ private
71
+
72
+ def teams_size
73
+ @teams_size ||= opening_round_size * 2
74
+ end
75
+
76
+ def power_of_two?
77
+ opening_round_size.to_s(2).count('1') == 1
78
+ end
79
+
80
+ def remaining_matches
81
+ total_matches - opening_round_size
82
+ end
83
+
84
+ def build_next_match(match1, match2, id)
85
+ depends_on = [match1, match2]
86
+ dependencies = depends_on.map(&:depends_on).flatten
87
+ round = dependencies && !dependencies.empty? ? (dependencies.size / 2) : 1
88
+
89
+ Match.build_next_match(category: category, depends_on: depends_on, round: round, id: id)
90
+ end
91
+ end
92
+ end
93
+ end
94
+ end
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SportsManager
4
+ module Matches
5
+ # Public: Determinates the next rounds and their dependencies
6
+ class NextRound
7
+ attr_reader :category, :base_matches, :match_maker
8
+
9
+ # NOTE: maybe move logic to class method and just return the algorithm
10
+ # NOTE: maybe keep it if mixing multiple formats and put decision logic in here
11
+ # TODO: implement round-robin
12
+ # TODO: implement to consider: start w round-robin followed by knockout
13
+ DEFAULT_ALGORITHM = Algorithms::SingleEliminationAlgorithm
14
+
15
+ def initialize(category:, base_matches:, algorithm: DEFAULT_ALGORITHM)
16
+ @category = category
17
+ @base_matches = base_matches
18
+ @match_maker = algorithm.new(category: category, matches: base_matches)
19
+ end
20
+
21
+ def next_matches
22
+ match_maker.next_matches
23
+ end
24
+
25
+ def total_matches
26
+ match_maker.total_matches
27
+ end
28
+
29
+ def total_rounds
30
+ match_maker.total_rounds
31
+ end
32
+
33
+ def round_for_match(match_number)
34
+ match_maker.round_for_match(match_number)
35
+ end
36
+ end
37
+ end
38
+ end