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