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,123 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SportsManager
4
+ # Public: A step-by-step tournament constructor (Builder Pattern)
5
+ class TournamentBuilder
6
+ attr_reader :matches, :subscriptions,
7
+ :categories, :configurations,
8
+ :tournament_days, :tournament
9
+
10
+ DEFAULT_CONFIGURATIONS = {
11
+ courts: 1,
12
+ match_time: 60,
13
+ break_time: 10,
14
+ single_day_matches: false,
15
+ tournament_type: Matches::Algorithms::SingleEliminationAlgorithm
16
+ }.freeze
17
+
18
+ def initialize
19
+ @matches = {}
20
+ @subscriptions = {}
21
+ @categories = []
22
+ @tournament_days = []
23
+ @configurations = DEFAULT_CONFIGURATIONS
24
+ end
25
+
26
+ def build
27
+ Tournament.new(settings: settings, groups: groups)
28
+ end
29
+
30
+ def add_matches(matches)
31
+ matches.each do |category, category_matches|
32
+ add_match(category: category, category_matches: category_matches)
33
+ end
34
+
35
+ self
36
+ end
37
+
38
+ def add_match(category:, category_matches:)
39
+ return self if [category, category_matches].any?(nil)
40
+
41
+ @matches.merge!(category => category_matches)
42
+
43
+ self
44
+ end
45
+
46
+ def add_subscription(category:, participants:)
47
+ @subscriptions.merge!(category => participants)
48
+
49
+ self
50
+ end
51
+
52
+ def add_subscriptions(subscriptions)
53
+ subscriptions.each do |category, participants|
54
+ add_subscription(category: category, participants: participants)
55
+ end
56
+
57
+ self
58
+ end
59
+
60
+ def add_configuration(key:, value:)
61
+ return self if [key, value].any?(nil)
62
+
63
+ @configurations = @configurations.merge(key.to_sym => value)
64
+
65
+ self
66
+ end
67
+
68
+ def add_configurations(params)
69
+ params.each { |key, value| add_configuration(key: key, value: value) }
70
+
71
+ self
72
+ end
73
+
74
+ def add_schedule(tournament_days)
75
+ tournament_days.each do |date, period|
76
+ add_date(
77
+ date: date,
78
+ start_hour: period[:start],
79
+ end_hour: period[:end]
80
+ )
81
+ end
82
+ self
83
+ end
84
+
85
+ def add_date(date:, start_hour:, end_hour:)
86
+ @tournament_days << TournamentDay.for(
87
+ date: date,
88
+ start_hour: start_hour,
89
+ end_hour: end_hour
90
+ )
91
+
92
+ self
93
+ end
94
+
95
+ private
96
+
97
+ def groups
98
+ categories = subscriptions.keys
99
+
100
+ categories.map do |category|
101
+ category_subscriptions = subscriptions[category]
102
+ category_matches = matches[category]
103
+
104
+ Group.for(
105
+ category: category,
106
+ subscriptions: category_subscriptions,
107
+ matches: category_matches,
108
+ tournament_type: configurations[:tournament_type]
109
+ )
110
+ end
111
+ end
112
+
113
+ def settings
114
+ Tournament::Setting.new(
115
+ tournament_days: tournament_days,
116
+ match_time: configurations[:match_time],
117
+ break_time: configurations[:break_time],
118
+ courts: configurations[:courts],
119
+ single_day_matches: configurations[:single_day_matches]
120
+ )
121
+ end
122
+ end
123
+ end
@@ -0,0 +1,69 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SportsManager
4
+ class TournamentDay
5
+ # Public: Validates tournament day's date and time range
6
+ class Validator
7
+ extend Forwardable
8
+
9
+ DateParsingError = Class.new(StandardError) do
10
+ def initialize(date)
11
+ message = "It is not possible to parse Date: #{date}"
12
+ super(message)
13
+ end
14
+ end
15
+
16
+ InvalidHour = Class.new(StandardError) do
17
+ def initialize(start_hour, end_hour)
18
+ message = 'start_hour and end_hour must be between 0 and 23. ' \
19
+ "start_hour: #{start_hour}, end_hour: #{end_hour}"
20
+ super(message)
21
+ end
22
+ end
23
+
24
+ MIN_HOUR = 0
25
+ MAX_HOUR = 23
26
+
27
+ attr_reader :tournament_day
28
+
29
+ def_delegators :tournament_day, :start_hour, :end_hour, :date
30
+
31
+ def initialize(tournament_day)
32
+ @tournament_day = tournament_day
33
+ end
34
+
35
+ def validate!
36
+ return true if valid?
37
+ raise DateParsingError, date unless parseable_date?
38
+
39
+ raise InvalidHour.new(start_hour, end_hour)
40
+ end
41
+
42
+ def valid?
43
+ parseable_date? && in_day_range?
44
+ end
45
+
46
+ private
47
+
48
+ def parseable_date?
49
+ !!parse_date
50
+ end
51
+
52
+ def parse_date
53
+ @parse_date ||= begin
54
+ Date.strptime(date, '%Y-%m-%d')
55
+ rescue StandardError
56
+ nil
57
+ end
58
+ end
59
+
60
+ def in_day_range?
61
+ [start_hour, end_hour].all?(&method(:day_range?))
62
+ end
63
+
64
+ def day_range?(hour)
65
+ hour.between?(MIN_HOUR, MAX_HOUR)
66
+ end
67
+ end
68
+ end
69
+ end
@@ -0,0 +1,50 @@
1
+ # frozen_string_literal: true
2
+
3
+ # TODO: revisit this logic
4
+ module SportsManager
5
+ # Public: One of the tournament's day with their specific date, duration,
6
+ # and timeslots available
7
+ class TournamentDay
8
+ include Comparable
9
+
10
+ MAX_HOUR = 24
11
+ ONE_HOUR = 60
12
+
13
+ attr_reader :start_hour, :end_hour, :date
14
+
15
+ def self.for(date:, start_hour:, end_hour:)
16
+ new(date: date, start_hour: start_hour, end_hour: end_hour)
17
+ end
18
+
19
+ def initialize(date:, start_hour:, end_hour:)
20
+ @date = date.to_s
21
+ @start_hour = start_hour.to_i
22
+ @end_hour = end_hour.to_i
23
+ Validator.new(self).validate!
24
+ end
25
+
26
+ def timeslots(interval: ONE_HOUR)
27
+ TimeslotBuilder.build(tournament_day: self, interval: interval)
28
+ end
29
+
30
+ def total_time
31
+ @total_time ||= end_hour - start_hour if start_hour <= end_hour
32
+ end
33
+
34
+ def <=>(other)
35
+ return unless instance_of?(other.class)
36
+
37
+ equality(other).find(-> { 0 }, &:nonzero?)
38
+ end
39
+
40
+ private
41
+
42
+ def equality(other)
43
+ [
44
+ date <=> other.date,
45
+ start_hour <=> other.start_hour,
46
+ end_hour <=> other.end_hour
47
+ ]
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,183 @@
1
+ # frozen_string_literal: true
2
+
3
+ # rubocop:disable Metrics/AbcSize
4
+ module SportsManager
5
+ # Public:Tournament Solver
6
+ # _TEAM A______
7
+ # MATCH 1 \_TEAM B______
8
+ # _TEAM B______/ \
9
+ # MATCH 5 \_TEAM B___
10
+ # _TEAM C______ / \
11
+ # MATCH 2 \_TEAM C______/ \
12
+ # _TEAM D______/ \
13
+ # MATCH 7 \__TEAM E__
14
+ # _TEAM E______ /
15
+ # MATCH 3 \_TEAM E______ /
16
+ # _TEAM F______/ \ /
17
+ # MATCH 6 \_TEAM E___/
18
+ # _TEAM G______ /
19
+ # MATCH 4 \_TEAM G______/
20
+ # _TEAM H______/
21
+ class TournamentGenerator
22
+ extend Forwardable
23
+
24
+ attr_reader :format, :tournament, :variables, :domains,
25
+ :ordering, :filtering, :csp
26
+
27
+ attr_accessor :days, :subscriptions, :matches, :courts, :game_length, :rest_break, :single_day_matches,
28
+ :tournament_type
29
+
30
+ def_delegators :tournament, :match_time, :timeslots
31
+
32
+ def self.example(type = :simple, format: :cli)
33
+ params = Helper.public_send(type)
34
+
35
+ new(format: format)
36
+ .add_days(params[:when])
37
+ .add_subscriptions(params[:subscriptions])
38
+ .add_matches(params[:matches])
39
+ .add_courts(params[:courts])
40
+ .add_game_length(params[:game_length])
41
+ .add_rest_break(params[:rest_break])
42
+ .enable_single_day_matches(params[:single_day_matches])
43
+ .call
44
+ end
45
+
46
+ def initialize(format: :cli)
47
+ @format = format
48
+ @days = {}
49
+ @subscriptions = {}
50
+ @matches = {}
51
+ @ordering = Algorithms::Ordering::MultipleMatchesParticipant
52
+ @filtering = Algorithms::Filtering::NoOverlap
53
+ end
54
+
55
+ def add_day(day, start, finish)
56
+ days[day.to_sym] = { start: start, end: finish }
57
+ self
58
+ end
59
+
60
+ def add_days(days)
61
+ days.each do |day, time_hash|
62
+ add_day(day, time_hash[:start], time_hash[:end])
63
+ end
64
+ self
65
+ end
66
+
67
+ def add_subscription(category, subscription)
68
+ subscriptions[category] ||= []
69
+ subscriptions[category] << subscription
70
+ self
71
+ end
72
+
73
+ def add_subscriptions_per_category(category, subscriptions)
74
+ subscriptions.each { |subscription| add_subscription(category, subscription) }
75
+ self
76
+ end
77
+
78
+ def add_subscriptions(subscriptions)
79
+ subscriptions.each do |category, subscriptions_per_category|
80
+ add_subscriptions_per_category(category, subscriptions_per_category)
81
+ end
82
+ self
83
+ end
84
+
85
+ def add_match(category, match)
86
+ matches[category] ||= []
87
+ matches[category] << match
88
+ self
89
+ end
90
+
91
+ def add_matches_per_category(category, matches)
92
+ matches.each { |match| add_match(category, match) }
93
+ self
94
+ end
95
+
96
+ def add_matches(matches)
97
+ matches.each do |category, matches_per_category|
98
+ add_matches_per_category(category, matches_per_category)
99
+ end
100
+ self
101
+ end
102
+
103
+ def add_courts(number_of_courts)
104
+ @courts = number_of_courts
105
+ self
106
+ end
107
+
108
+ def add_game_length(game_length)
109
+ @game_length = game_length
110
+ self
111
+ end
112
+
113
+ def add_rest_break(rest_break)
114
+ @rest_break = rest_break
115
+ self
116
+ end
117
+
118
+ def enable_single_day_matches(single_day_matches)
119
+ @single_day_matches = single_day_matches
120
+ self
121
+ end
122
+
123
+ def single_elimination_algorithm
124
+ @tournament_type = Matches::Algorithms::SingleEliminationAlgorithm
125
+ self
126
+ end
127
+
128
+ def call
129
+ setup
130
+
131
+ solutions = csp.solve
132
+
133
+ TournamentSolution
134
+ .new(tournament: tournament, solutions: solutions)
135
+ .tap(&method(:print_solution))
136
+ end
137
+
138
+ private
139
+
140
+ def setup
141
+ @tournament = build_tournament
142
+
143
+ @variables = @tournament.matches.values.flat_map(&:itself)
144
+
145
+ @domains = @variables.each_with_object({}) do |variable, domains|
146
+ domains[variable] = timeslots.sort_by(&:slot)
147
+ end
148
+
149
+ # TODO: Add a ordering specific for domains
150
+ @csp = TournamentProblemBuilder
151
+ .new(tournament)
152
+ .add_variables(variables)
153
+ .add_domains(domains)
154
+ .add_ordering(ordering)
155
+ .add_filtering(filtering)
156
+ .add_constraint(Constraints::AllDifferentConstraint)
157
+ .add_constraint(Constraints::NoOverlappingConstraint)
158
+ .add_constraint(Constraints::MatchConstraint)
159
+ .add_constraint(Constraints::MultiCategoryConstraint)
160
+ .add_constraint(Constraints::NextRoundConstraint)
161
+ .build
162
+ end
163
+
164
+ def build_tournament
165
+ TournamentBuilder
166
+ .new
167
+ .add_matches(matches)
168
+ .add_subscriptions(subscriptions)
169
+ .add_schedule(days)
170
+ .add_configuration(key: :courts, value: courts)
171
+ .add_configuration(key: :match_time, value: game_length)
172
+ .add_configuration(key: :break_time, value: rest_break)
173
+ .add_configuration(key: :single_day_matches, value: single_day_matches)
174
+ .add_configuration(key: :tournament_type, value: tournament_type)
175
+ .build
176
+ end
177
+
178
+ def print_solution(tournament_solution)
179
+ SolutionDrawer.new(tournament_solution).public_send(format)
180
+ end
181
+ end
182
+ end
183
+ # rubocop:enable Metrics/AbcSize
@@ -0,0 +1,106 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SportsManager
4
+ # TODO: better name this
5
+ # Public: Builds a CSP for Tournament Scheduling.
6
+ #
7
+ # Usage example:
8
+ # builder = ProblemBuilder.new(tournament)
9
+ #
10
+ # builder
11
+ # .add_variables(variables)
12
+ # .add_domains(domains)
13
+ # .add_ordering(NoOrder)
14
+ # .add_filtering(NoFilter)
15
+ #
16
+ # builder
17
+ # .add_constraint(AllDifferentConstraint)
18
+ # .add_constraint(NoOverlapping)
19
+ #
20
+ # csp = builder.build
21
+ class TournamentProblemBuilder
22
+ attr_reader :variables, :domains, :constraints, :max_solutions,
23
+ :filtering_algorithm, :ordering_algorithm, :tournament
24
+
25
+ MINIMUM_SOLUTIONS = 1
26
+
27
+ def initialize(tournament)
28
+ @variables = []
29
+ @domains = {}
30
+ @constraints = []
31
+
32
+ @filtering_algorithm = nil
33
+ @ordering_algorithm = nil
34
+
35
+ @max_solutions = MINIMUM_SOLUTIONS
36
+
37
+ @tournament = tournament
38
+ end
39
+
40
+ def add_variables(variables)
41
+ @variables = variables
42
+
43
+ self
44
+ end
45
+
46
+ def add_domains(domains)
47
+ @domains = domains
48
+
49
+ self
50
+ end
51
+
52
+ def add_ordering(ordering_algorithm_class)
53
+ @ordering_algorithm = ordering_algorithm_class
54
+
55
+ self
56
+ end
57
+
58
+ def add_filtering(filtering_algorithm_class)
59
+ @filtering_algorithm = filtering_algorithm_class
60
+
61
+ self
62
+ end
63
+
64
+ # TODO: change to pass symbol instead of class?
65
+ def add_constraint(constraint_class)
66
+ @constraints |= Utils::Array.wrap(constraint_class)
67
+
68
+ self
69
+ end
70
+
71
+ def add_max_solutions(max_solutions)
72
+ @max_solutions = [max_solutions, MINIMUM_SOLUTIONS].find(&:positive?)
73
+
74
+ self
75
+ end
76
+
77
+ def build
78
+ csp.tap do |problem|
79
+ variables.each do |variable|
80
+ problem.add_variable(variable, domains: domains[variable])
81
+ end
82
+ problem.add_ordering ordering(problem)
83
+ problem.add_filtering filtering(problem)
84
+ build_constraint(problem)
85
+ end
86
+ end
87
+
88
+ private
89
+
90
+ def csp
91
+ CSP::Problem.new(max_solutions: max_solutions)
92
+ end
93
+
94
+ def ordering(problem)
95
+ ordering_algorithm.for(problem: problem, dependency: tournament)
96
+ end
97
+
98
+ def filtering(problem)
99
+ filtering_algorithm.for(problem: problem, dependency: tournament)
100
+ end
101
+
102
+ def build_constraint(csp)
103
+ ConstraintBuilder.build(tournament: tournament, csp: csp, constraints: constraints)
104
+ end
105
+ end
106
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SportsManager
4
+ class TournamentSolution
5
+ # Public: A placeholder fixture for byes.
6
+ class ByeFixture
7
+ extend Forwardable
8
+
9
+ attr_reader :match, :category
10
+
11
+ def_delegator :match, :id, :match_id
12
+ def_delegators :match, :title, :depends_on, :playable?,
13
+ :dependencies, :dependencies?
14
+
15
+ def initialize(match)
16
+ @match = match
17
+ @category = match.category
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SportsManager
4
+ class TournamentSolution
5
+ # Public: A schedule for the participating teams.
6
+ class Fixture
7
+ extend Forwardable
8
+
9
+ attr_reader :match, :timeslot, :category
10
+
11
+ def_delegator :match, :id, :match_id
12
+ def_delegators :match, :title, :depends_on, :playable?,
13
+ :dependencies, :dependencies?, :round
14
+ def_delegators :timeslot, :court, :slot
15
+
16
+ def self.build_fixtures(fixtures)
17
+ fixtures.map { |(match, timeslot)| new(match: match, timeslot: timeslot) }
18
+ end
19
+
20
+ def initialize(match:, timeslot:)
21
+ @match = match
22
+ @timeslot = timeslot
23
+ @category = match.category
24
+ end
25
+
26
+ def depends_on?(fixture)
27
+ depends_on
28
+ .map(&:id)
29
+ .include?(fixture.match_id)
30
+ end
31
+
32
+ def bye_dependencies
33
+ depends_on
34
+ .reject(&:playable?)
35
+ .map { |bye| ByeFixture.new(bye) }
36
+ end
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,107 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SportsManager
4
+ class TournamentSolution
5
+ # NOTE: Maybe temporary? should each object override as_json?
6
+ class Serializer
7
+ include JsonHelper
8
+ extend Forwardable
9
+
10
+ attr_reader :tournament_solution
11
+
12
+ def initialize(tournament_solution)
13
+ @tournament_solution = tournament_solution
14
+ @tournament = tournament_solution.tournament
15
+ @solutions = tournament_solution.solutions
16
+ end
17
+
18
+ def as_json(*)
19
+ {
20
+ tournament: tournament,
21
+ solutions: solutions
22
+ }.as_json
23
+ end
24
+
25
+ private
26
+
27
+ def tournament
28
+ {
29
+ settings: settings,
30
+ groups: groups,
31
+ participants: participants
32
+ }.as_json
33
+ end
34
+
35
+ def solutions
36
+ @solutions
37
+ .map(&method(:build_solution))
38
+ .as_json
39
+ end
40
+
41
+ def settings
42
+ setting = @tournament.settings
43
+
44
+ params = {
45
+ match_time: setting.match_time,
46
+ break_time: setting.break_time,
47
+ courts: setting.court_list,
48
+ single_day_matches: setting.single_day_matches
49
+ }
50
+
51
+ params.as_json
52
+ end
53
+
54
+ def groups
55
+ @tournament.groups.map do |group|
56
+ {
57
+ category: group.category,
58
+ matches: build_matches(group)
59
+ }
60
+ end
61
+ end
62
+
63
+ def participants
64
+ @tournament
65
+ .participants
66
+ .sort_by(&:id)
67
+ .as_json
68
+ end
69
+
70
+ def build_solution(solution)
71
+ fixtures = solution
72
+ .fixtures
73
+ .map(&method(:build_fixture))
74
+
75
+ { fixtures: fixtures }
76
+ end
77
+
78
+ def build_fixture(fixture)
79
+ {
80
+ match: {
81
+ id: fixture.match_id,
82
+ title: fixture.title,
83
+ category: fixture.category
84
+ },
85
+ timeslot: {
86
+ court: fixture.court,
87
+ slot: fixture.slot
88
+ }
89
+ }
90
+ end
91
+
92
+ def build_matches(group)
93
+ matches = group
94
+ .all_matches
95
+ .sort_by { |match| [match.round, match.id] }
96
+
97
+ matches.map do |match|
98
+ {
99
+ id: match.id,
100
+ round: match.round,
101
+ playing: match.title
102
+ }
103
+ end
104
+ end
105
+ end
106
+ end
107
+ end