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.
- checksums.yaml +7 -0
- data/.rspec +1 -0
- data/.rubocop.yml +32 -0
- data/CODE_OF_CONDUCT.md +132 -0
- data/CONTRIBUTING.md +72 -0
- data/MIT-LICENSE +20 -0
- data/README.md +419 -0
- data/Rakefile +4 -0
- data/lib/sports-manager.rb +59 -0
- data/lib/sports_manager/algorithms/filtering/no_overlap.rb +52 -0
- data/lib/sports_manager/algorithms/ordering/multiple_matches_participant.rb +78 -0
- data/lib/sports_manager/bye_match.rb +62 -0
- data/lib/sports_manager/constraint_builder.rb +30 -0
- data/lib/sports_manager/constraints/all_different_constraint.rb +24 -0
- data/lib/sports_manager/constraints/match_constraint.rb +37 -0
- data/lib/sports_manager/constraints/multi_category_constraint.rb +49 -0
- data/lib/sports_manager/constraints/next_round_constraint.rb +48 -0
- data/lib/sports_manager/constraints/no_overlapping_constraint.rb +55 -0
- data/lib/sports_manager/double_team.rb +7 -0
- data/lib/sports_manager/group.rb +42 -0
- data/lib/sports_manager/group_builder.rb +72 -0
- data/lib/sports_manager/helper.rb +228 -0
- data/lib/sports_manager/json_helper.rb +129 -0
- data/lib/sports_manager/match.rb +91 -0
- data/lib/sports_manager/match_builder.rb +112 -0
- data/lib/sports_manager/matches/algorithms/single_elimination_algorithm.rb +94 -0
- data/lib/sports_manager/matches/next_round.rb +38 -0
- data/lib/sports_manager/matches_generator.rb +33 -0
- data/lib/sports_manager/nil_team.rb +24 -0
- data/lib/sports_manager/participant.rb +23 -0
- data/lib/sports_manager/single_team.rb +7 -0
- data/lib/sports_manager/solution_drawer/cli/solution_table.rb +38 -0
- data/lib/sports_manager/solution_drawer/cli/table.rb +94 -0
- data/lib/sports_manager/solution_drawer/cli.rb +75 -0
- data/lib/sports_manager/solution_drawer/mermaid/bye_node.rb +39 -0
- data/lib/sports_manager/solution_drawer/mermaid/gantt.rb +126 -0
- data/lib/sports_manager/solution_drawer/mermaid/graph.rb +111 -0
- data/lib/sports_manager/solution_drawer/mermaid/node.rb +55 -0
- data/lib/sports_manager/solution_drawer/mermaid/node_style.rb +89 -0
- data/lib/sports_manager/solution_drawer/mermaid/solution_gantt.rb +57 -0
- data/lib/sports_manager/solution_drawer/mermaid/solution_graph.rb +76 -0
- data/lib/sports_manager/solution_drawer/mermaid.rb +65 -0
- data/lib/sports_manager/solution_drawer.rb +23 -0
- data/lib/sports_manager/team.rb +47 -0
- data/lib/sports_manager/team_builder.rb +31 -0
- data/lib/sports_manager/timeslot.rb +37 -0
- data/lib/sports_manager/timeslot_builder.rb +50 -0
- data/lib/sports_manager/tournament/setting.rb +45 -0
- data/lib/sports_manager/tournament.rb +69 -0
- data/lib/sports_manager/tournament_builder.rb +123 -0
- data/lib/sports_manager/tournament_day/validator.rb +69 -0
- data/lib/sports_manager/tournament_day.rb +50 -0
- data/lib/sports_manager/tournament_generator.rb +183 -0
- data/lib/sports_manager/tournament_problem_builder.rb +106 -0
- data/lib/sports_manager/tournament_solution/bye_fixture.rb +21 -0
- data/lib/sports_manager/tournament_solution/fixture.rb +39 -0
- data/lib/sports_manager/tournament_solution/serializer.rb +107 -0
- data/lib/sports_manager/tournament_solution/solution.rb +85 -0
- data/lib/sports_manager/tournament_solution.rb +34 -0
- data/lib/sports_manager/version.rb +5 -0
- data/sports-manager.gemspec +35 -0
- 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
|