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,89 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module SportsManager
|
4
|
+
class SolutionDrawer
|
5
|
+
class Mermaid
|
6
|
+
class NodeStyle
|
7
|
+
# TODO: Add an algorithms to dynamically create the colors
|
8
|
+
COLORS = %w[
|
9
|
+
#A9F9A9 #4FF7DE #FF6E41
|
10
|
+
#5FAD4E #009BFF #00FF00
|
11
|
+
#FF0000 #01FFFE #FFA6FE
|
12
|
+
#FFDB66 #006401 #010067
|
13
|
+
#95003A #007DB5 #FF00F6
|
14
|
+
#FFEEE8 #774D00 #90FB92
|
15
|
+
#0076FF #D5FF00 #FF937E
|
16
|
+
#6A826C #FF029D #FE8900
|
17
|
+
#7A4782 #7E2DD2 #85A900
|
18
|
+
#FF0056 #A42400 #00AE7E
|
19
|
+
#683D3B #BDC6FF #263400
|
20
|
+
#BDD393 #00B917 #9E008E
|
21
|
+
#001544 #C28C9F #FF74A3
|
22
|
+
#01D0FF #004754 #E56FFE
|
23
|
+
#788231 #0E4CA1 #91D0CB
|
24
|
+
#BE9970 #968AE8 #BB8800
|
25
|
+
#43002C #DEFF74 #00FFC6
|
26
|
+
#FFE502 #620E00 #008F9C
|
27
|
+
#98FF52 #7544B1 #B500FF
|
28
|
+
#00FF78 #005F39 #6B6882
|
29
|
+
#A75740 #A5FFD2 #FFB167
|
30
|
+
].freeze
|
31
|
+
|
32
|
+
attr_reader :elements
|
33
|
+
|
34
|
+
def self.call(elements)
|
35
|
+
new(elements).to_params
|
36
|
+
end
|
37
|
+
|
38
|
+
def initialize(elements)
|
39
|
+
@elements = elements.map(&:to_s)
|
40
|
+
end
|
41
|
+
|
42
|
+
def to_params
|
43
|
+
{ class_definitions: class_definitions, subgraph_colorscheme: subgraph_colorscheme }
|
44
|
+
end
|
45
|
+
|
46
|
+
def subgraph_colorscheme
|
47
|
+
elements.map do |element|
|
48
|
+
node_name = element.upcase
|
49
|
+
style_class = element
|
50
|
+
|
51
|
+
"#{node_name}:::#{style_class}"
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
def class_definitions
|
56
|
+
colorscheme.map do |element, properties|
|
57
|
+
attributes = properties
|
58
|
+
.map { |property| property.join(':') }
|
59
|
+
.join(', ')
|
60
|
+
"#{element} #{attributes}"
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
def colorscheme
|
65
|
+
elements.each_with_index.with_object({}) do |(element, index), scheme|
|
66
|
+
node_color = COLORS[index]
|
67
|
+
text_color = text_color(node_color)
|
68
|
+
|
69
|
+
scheme[element] = { fill: node_color, color: text_color }
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
# Internal: ripoff from stackoverflow
|
74
|
+
def text_color(color)
|
75
|
+
red, green, blue = color
|
76
|
+
.gsub('#', '')
|
77
|
+
.chars
|
78
|
+
.each_slice(2)
|
79
|
+
.map(&:join)
|
80
|
+
.map(&:hex)
|
81
|
+
|
82
|
+
formula = (red * 0.299) + (green * 0.587) + (blue * 0.114)
|
83
|
+
|
84
|
+
formula > 186 ? '#000000' : '#FFFFFF'
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
@@ -0,0 +1,57 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module SportsManager
|
4
|
+
class SolutionDrawer
|
5
|
+
class Mermaid
|
6
|
+
class SolutionGantt
|
7
|
+
extend Forwardable
|
8
|
+
|
9
|
+
TIME_TEMPLATE = '%d/%m %H:%M'
|
10
|
+
TIME_INTERVAL = '1h' # TODO: make it dynamic
|
11
|
+
|
12
|
+
attr_reader :solution
|
13
|
+
|
14
|
+
def_delegators :solution, :fixtures, :acronyms
|
15
|
+
|
16
|
+
def self.draw(solution)
|
17
|
+
new(solution).draw
|
18
|
+
end
|
19
|
+
|
20
|
+
def initialize(solution)
|
21
|
+
@solution = solution
|
22
|
+
end
|
23
|
+
|
24
|
+
def draw
|
25
|
+
Gantt.draw(sections: sections)
|
26
|
+
end
|
27
|
+
|
28
|
+
def sections
|
29
|
+
fixtures
|
30
|
+
.group_by(&:court)
|
31
|
+
.yield_self { |sections_fixtures| build_sections(sections_fixtures) }
|
32
|
+
end
|
33
|
+
|
34
|
+
private
|
35
|
+
|
36
|
+
def build_sections(sections_fixtures)
|
37
|
+
sections_fixtures.transform_values do |fixtures|
|
38
|
+
fixtures.map(&method(:build_task))
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
def build_task(fixture)
|
43
|
+
category = fixture.category
|
44
|
+
match_id = fixture.match_id
|
45
|
+
slot = fixture.slot
|
46
|
+
|
47
|
+
category_acronym = acronyms[category].upcase
|
48
|
+
id = "#{category_acronym} M#{match_id}"
|
49
|
+
interval = TIME_INTERVAL
|
50
|
+
time = slot.strftime(TIME_TEMPLATE)
|
51
|
+
|
52
|
+
{ id: id, interval: interval, time: time }
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
@@ -0,0 +1,76 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module SportsManager
|
4
|
+
class SolutionDrawer
|
5
|
+
class Mermaid
|
6
|
+
class SolutionGraph
|
7
|
+
attr_reader :solution
|
8
|
+
|
9
|
+
STYLE_CLASS_DEFINITION = '%<name> fill:%<fill>, color:%<color>'
|
10
|
+
|
11
|
+
LinkNode = Struct.new(:node_hash) do
|
12
|
+
def definition
|
13
|
+
"#{node_hash[:previous_name]} --> #{node_hash[:next_name]}"
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
def self.draw(solution)
|
18
|
+
new(solution).draw
|
19
|
+
end
|
20
|
+
|
21
|
+
def initialize(solution)
|
22
|
+
@solution = solution
|
23
|
+
end
|
24
|
+
|
25
|
+
def draw
|
26
|
+
Graph.draw(subgraphs: subgraphs, **node_style)
|
27
|
+
end
|
28
|
+
|
29
|
+
private
|
30
|
+
|
31
|
+
def subgraphs
|
32
|
+
solution
|
33
|
+
.fixtures_dependencies_by_category
|
34
|
+
.yield_self(&method(:build_subgraphs))
|
35
|
+
.yield_self(&method(:serialize))
|
36
|
+
end
|
37
|
+
|
38
|
+
def build_subgraphs(categories_fixtures)
|
39
|
+
categories_fixtures
|
40
|
+
.transform_values { |fixtures| build_nodes(fixtures) }
|
41
|
+
.transform_values { |nodes| (nodes | build_links(nodes)) }
|
42
|
+
end
|
43
|
+
|
44
|
+
def build_nodes(fixtures)
|
45
|
+
fixtures
|
46
|
+
.sort_by(&:match_id)
|
47
|
+
.map { |fixture| Node.for(fixture) }
|
48
|
+
end
|
49
|
+
|
50
|
+
def build_links(nodes)
|
51
|
+
nodes
|
52
|
+
.select(&:links?)
|
53
|
+
.flat_map { |linked_node| build_link(linked_node, nodes) }
|
54
|
+
end
|
55
|
+
|
56
|
+
def build_link(linked_node, nodes)
|
57
|
+
new_nodes = nodes
|
58
|
+
.select { |node| linked_node.depends_on?(node) }
|
59
|
+
|
60
|
+
new_nodes.map do |previous_node|
|
61
|
+
LinkNode.new({ previous_name: previous_node.name, next_name: linked_node.name })
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
def serialize(graph)
|
66
|
+
graph.transform_values { |nodes| nodes.map(&:definition) }
|
67
|
+
end
|
68
|
+
|
69
|
+
def node_style
|
70
|
+
courts = solution.courts.map { |n| "court#{n}" }
|
71
|
+
NodeStyle.call(courts)
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
@@ -0,0 +1,65 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module SportsManager
|
4
|
+
class SolutionDrawer
|
5
|
+
# Public: draws the tournament solution to mermaid's charts
|
6
|
+
class Mermaid
|
7
|
+
extend Forwardable
|
8
|
+
|
9
|
+
attr_reader :tournament_solution
|
10
|
+
|
11
|
+
def_delegators :tournament_solution, :solutions,
|
12
|
+
:total_solutions, :solved?
|
13
|
+
|
14
|
+
NO_SOLUTION = 'No solution found'
|
15
|
+
|
16
|
+
def self.draw(tournament_solution)
|
17
|
+
new(tournament_solution).draw
|
18
|
+
end
|
19
|
+
|
20
|
+
def initialize(tournament_solution)
|
21
|
+
@tournament_solution = tournament_solution
|
22
|
+
end
|
23
|
+
|
24
|
+
def draw
|
25
|
+
no_solution || draw_solution
|
26
|
+
end
|
27
|
+
|
28
|
+
private
|
29
|
+
|
30
|
+
def no_solution
|
31
|
+
return false if solved?
|
32
|
+
|
33
|
+
puts NO_SOLUTION
|
34
|
+
|
35
|
+
true
|
36
|
+
end
|
37
|
+
|
38
|
+
def draw_solution
|
39
|
+
puts 'Solutions:'
|
40
|
+
solutions.map.with_index(1, &method(:draw_all))
|
41
|
+
puts "Total solutions: #{total_solutions}"
|
42
|
+
end
|
43
|
+
|
44
|
+
def draw_all(solution, index)
|
45
|
+
puts '-' * 80
|
46
|
+
puts "Solutions #{index}"
|
47
|
+
|
48
|
+
puts 'Gantt:'
|
49
|
+
puts gantt(solution)
|
50
|
+
|
51
|
+
puts 'Graph:'
|
52
|
+
puts graph(solution)
|
53
|
+
puts '-' * 80
|
54
|
+
end
|
55
|
+
|
56
|
+
def gantt(solution)
|
57
|
+
SolutionGantt.new(solution).draw
|
58
|
+
end
|
59
|
+
|
60
|
+
def graph(solution)
|
61
|
+
SolutionGraph.new(solution).draw
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module SportsManager
|
4
|
+
class SolutionDrawer
|
5
|
+
attr_reader :tournament_solution
|
6
|
+
|
7
|
+
def initialize(tournament_solution)
|
8
|
+
@tournament_solution = tournament_solution
|
9
|
+
end
|
10
|
+
|
11
|
+
def none; end
|
12
|
+
|
13
|
+
def mermaid
|
14
|
+
Mermaid.draw(tournament_solution)
|
15
|
+
end
|
16
|
+
|
17
|
+
def cli
|
18
|
+
CLI.draw(tournament_solution)
|
19
|
+
end
|
20
|
+
|
21
|
+
# TODO: create json method | create class | move serialize class too
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,47 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module SportsManager
|
4
|
+
# Public: Participants in a team for a tournament category
|
5
|
+
class Team
|
6
|
+
attr_reader :category, :participants
|
7
|
+
|
8
|
+
def self.for(participants:, category:)
|
9
|
+
klass = case participants.size
|
10
|
+
when 1 then SingleTeam
|
11
|
+
when 2 then DoubleTeam
|
12
|
+
else raise StandardError,
|
13
|
+
"Participants #{participants} is not " \
|
14
|
+
'between 1 and 2'
|
15
|
+
end
|
16
|
+
|
17
|
+
klass.new(participants: participants, category: category)
|
18
|
+
end
|
19
|
+
|
20
|
+
def initialize(participants:, category: nil)
|
21
|
+
@category = category
|
22
|
+
@participants = participants
|
23
|
+
end
|
24
|
+
|
25
|
+
def name
|
26
|
+
participants
|
27
|
+
.flat_map(&:name)
|
28
|
+
.join(' e ')
|
29
|
+
end
|
30
|
+
|
31
|
+
def find_participant(id)
|
32
|
+
participants.find { |participant| participant.id == id }
|
33
|
+
end
|
34
|
+
|
35
|
+
def find_participants(ids)
|
36
|
+
unique_ids = ids.uniq
|
37
|
+
found_participants = unique_ids.map(&method(:find_participant)).compact
|
38
|
+
found_participants if found_participants.size == unique_ids.size
|
39
|
+
end
|
40
|
+
|
41
|
+
def ==(other)
|
42
|
+
instance_of?(other.class) &&
|
43
|
+
category == other.category &&
|
44
|
+
participants == other.participants
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module SportsManager
|
4
|
+
class TeamBuilder
|
5
|
+
attr_reader :category, :subscriptions, :participants
|
6
|
+
|
7
|
+
def initialize(category:, subscriptions:)
|
8
|
+
@category = category
|
9
|
+
@subscriptions = subscriptions
|
10
|
+
@participants = subscriptions.flatten
|
11
|
+
end
|
12
|
+
|
13
|
+
def build
|
14
|
+
subscriptions.map(&method(:build_team))
|
15
|
+
end
|
16
|
+
|
17
|
+
private
|
18
|
+
|
19
|
+
def build_team(team)
|
20
|
+
team_participants = Utils::Array
|
21
|
+
.wrap(team)
|
22
|
+
.map { |participant| build_participant(participant) }
|
23
|
+
|
24
|
+
Team.for(category: category, participants: team_participants)
|
25
|
+
end
|
26
|
+
|
27
|
+
def build_participant(participant)
|
28
|
+
Participant.new(**participant)
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module SportsManager
|
4
|
+
# Public: A available time in a court that can be scheduled
|
5
|
+
class Timeslot
|
6
|
+
include Comparable
|
7
|
+
|
8
|
+
attr_reader :court, :slot, :date
|
9
|
+
|
10
|
+
alias hour slot
|
11
|
+
|
12
|
+
def initialize(court:, date:, slot:)
|
13
|
+
@court = court
|
14
|
+
@date = date
|
15
|
+
@slot = slot
|
16
|
+
end
|
17
|
+
|
18
|
+
# TODO: timezone
|
19
|
+
def datetime
|
20
|
+
return slot if slot.is_a? Time
|
21
|
+
|
22
|
+
@datetime ||= DateTime.parse("#{date} #{hour}:00:00")
|
23
|
+
end
|
24
|
+
|
25
|
+
def <=>(other)
|
26
|
+
return false unless instance_of?(other.class)
|
27
|
+
|
28
|
+
datetime <=> other.datetime
|
29
|
+
end
|
30
|
+
|
31
|
+
def ==(other)
|
32
|
+
instance_of?(other.class) &&
|
33
|
+
court == other.court &&
|
34
|
+
datetime == other.datetime
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
@@ -0,0 +1,50 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module SportsManager
|
4
|
+
# NOTE: maybe change the name since we are not creating a Timeslot object here
|
5
|
+
# Public: Builds the possible timeslots as Time objects
|
6
|
+
class TimeslotBuilder
|
7
|
+
require 'time'
|
8
|
+
require 'forwardable'
|
9
|
+
extend Forwardable
|
10
|
+
|
11
|
+
attr_reader :tournament_day
|
12
|
+
|
13
|
+
def_delegators :tournament_day, :start_hour, :end_hour, :date
|
14
|
+
|
15
|
+
def self.build(tournament_day:, interval:)
|
16
|
+
new(tournament_day: tournament_day, interval: interval).build
|
17
|
+
end
|
18
|
+
|
19
|
+
def initialize(tournament_day:, interval:)
|
20
|
+
@tournament_day = tournament_day
|
21
|
+
@interval = interval
|
22
|
+
end
|
23
|
+
|
24
|
+
def build
|
25
|
+
time_range.step(interval).map(&Time.method(:at))
|
26
|
+
end
|
27
|
+
|
28
|
+
private
|
29
|
+
|
30
|
+
def time_range
|
31
|
+
(start_time.to_i..end_time.to_i)
|
32
|
+
end
|
33
|
+
|
34
|
+
def interval
|
35
|
+
@interval * 60
|
36
|
+
end
|
37
|
+
|
38
|
+
def start_time
|
39
|
+
parse_time(start_hour)
|
40
|
+
end
|
41
|
+
|
42
|
+
def end_time
|
43
|
+
parse_time(end_hour)
|
44
|
+
end
|
45
|
+
|
46
|
+
def parse_time(time)
|
47
|
+
Time.parse("#{date}T#{time}:00:00")
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
@@ -0,0 +1,45 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module SportsManager
|
4
|
+
class Tournament
|
5
|
+
# Public: The tournament's configurations
|
6
|
+
class Setting
|
7
|
+
attr_reader :match_time, :break_time, :courts, :tournament_days, :single_day_matches
|
8
|
+
|
9
|
+
def initialize(match_time:, break_time:, courts:, tournament_days:, single_day_matches:)
|
10
|
+
@match_time = match_time
|
11
|
+
@break_time = break_time
|
12
|
+
@courts = courts
|
13
|
+
@tournament_days = tournament_days
|
14
|
+
@single_day_matches = single_day_matches
|
15
|
+
end
|
16
|
+
|
17
|
+
def ==(other)
|
18
|
+
return false unless instance_of?(other.class)
|
19
|
+
|
20
|
+
match_time == other.match_time &&
|
21
|
+
break_time == other.break_time &&
|
22
|
+
courts == other.courts &&
|
23
|
+
tournament_days == other.tournament_days &&
|
24
|
+
single_day_matches == other.single_day_matches
|
25
|
+
end
|
26
|
+
|
27
|
+
def timeslots
|
28
|
+
@timeslots ||= tournament_days.flat_map(&method(:build_day_slots))
|
29
|
+
end
|
30
|
+
|
31
|
+
def court_list
|
32
|
+
@court_list ||= courts.times.to_a
|
33
|
+
end
|
34
|
+
|
35
|
+
private
|
36
|
+
|
37
|
+
def build_day_slots(tournament_day)
|
38
|
+
tournament_day
|
39
|
+
.timeslots(interval: break_time)
|
40
|
+
.yield_self { |slots| court_list.product(slots) }
|
41
|
+
.map { |(court, slot)| Timeslot.new(court: court, date: tournament_day, slot: slot) }
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
@@ -0,0 +1,69 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module SportsManager
|
4
|
+
# Public: A tournament with different categories matches
|
5
|
+
class Tournament
|
6
|
+
extend Forwardable
|
7
|
+
|
8
|
+
attr_reader :settings, :groups
|
9
|
+
|
10
|
+
def_delegators :settings, :match_time, :break_time, :courts, :timeslots,
|
11
|
+
:single_day_matches, :tournament_days
|
12
|
+
|
13
|
+
def initialize(settings: nil, groups: nil)
|
14
|
+
@settings = settings
|
15
|
+
@groups = groups
|
16
|
+
end
|
17
|
+
|
18
|
+
def ==(other)
|
19
|
+
settings == other.settings && groups == other.groups
|
20
|
+
end
|
21
|
+
|
22
|
+
def categories
|
23
|
+
@categories ||= groups.map(&:category)
|
24
|
+
end
|
25
|
+
|
26
|
+
def matches
|
27
|
+
@matches ||= groups.each_with_object({}) do |group, category_matches|
|
28
|
+
category_matches[group.category] = group.matches
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
def first_round_matches
|
33
|
+
categories.each_with_object({}) do |category, first_rounds|
|
34
|
+
first_rounds[category] = find_matches(category: category, round: 0)
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
def find_matches(category:, round:)
|
39
|
+
category_matches = groups
|
40
|
+
.find { |group| group.category == category }
|
41
|
+
&.find_matches(round)
|
42
|
+
|
43
|
+
category_matches || []
|
44
|
+
end
|
45
|
+
|
46
|
+
def total_matches
|
47
|
+
matches.values.map(&:size).sum
|
48
|
+
end
|
49
|
+
|
50
|
+
def participants
|
51
|
+
@participants ||= all_participants.uniq(&:id)
|
52
|
+
end
|
53
|
+
|
54
|
+
def multi_tournament_participants
|
55
|
+
@multi_tournament_participants ||= participants
|
56
|
+
.select { |participant| all_participants.count(participant) > 1 }
|
57
|
+
end
|
58
|
+
|
59
|
+
def find_participant_matches(participant)
|
60
|
+
groups.flat_map { |group| group.find_participant_matches(participant) }
|
61
|
+
end
|
62
|
+
|
63
|
+
private
|
64
|
+
|
65
|
+
def all_participants
|
66
|
+
@all_participants ||= groups.map(&:participants).flatten
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|