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,59 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'csp-resolver'
|
4
|
+
require 'forwardable'
|
5
|
+
require 'ostruct'
|
6
|
+
require_relative 'sports_manager/version'
|
7
|
+
require_relative 'sports_manager/helper'
|
8
|
+
require_relative 'sports_manager/team'
|
9
|
+
require_relative 'sports_manager/single_team'
|
10
|
+
require_relative 'sports_manager/double_team'
|
11
|
+
require_relative 'sports_manager/json_helper'
|
12
|
+
require_relative 'sports_manager/matches/algorithms/single_elimination_algorithm'
|
13
|
+
require_relative 'sports_manager/algorithms/ordering/multiple_matches_participant'
|
14
|
+
require_relative 'sports_manager/algorithms/filtering/no_overlap'
|
15
|
+
require_relative 'sports_manager/constraints/all_different_constraint'
|
16
|
+
require_relative 'sports_manager/constraints/no_overlapping_constraint'
|
17
|
+
require_relative 'sports_manager/constraints/match_constraint'
|
18
|
+
require_relative 'sports_manager/constraints/multi_category_constraint'
|
19
|
+
require_relative 'sports_manager/constraints/next_round_constraint'
|
20
|
+
require_relative 'sports_manager/tournament_problem_builder'
|
21
|
+
require_relative 'sports_manager/tournament_solution'
|
22
|
+
require_relative 'sports_manager/participant'
|
23
|
+
require_relative 'sports_manager/solution_drawer'
|
24
|
+
require_relative 'sports_manager/matches_generator'
|
25
|
+
require_relative 'sports_manager/tournament_builder'
|
26
|
+
require_relative 'sports_manager/constraint_builder'
|
27
|
+
require_relative 'sports_manager/tournament_solution/bye_fixture'
|
28
|
+
require_relative 'sports_manager/tournament_solution/fixture'
|
29
|
+
require_relative 'sports_manager/tournament_solution/solution'
|
30
|
+
require_relative 'sports_manager/tournament_solution/serializer'
|
31
|
+
require_relative 'sports_manager/solution_drawer/cli'
|
32
|
+
require_relative 'sports_manager/tournament'
|
33
|
+
require_relative 'sports_manager/tournament_day'
|
34
|
+
require_relative 'sports_manager/tournament/setting'
|
35
|
+
require_relative 'sports_manager/group_builder'
|
36
|
+
require_relative 'sports_manager/match'
|
37
|
+
require_relative 'sports_manager/matches/next_round'
|
38
|
+
require_relative 'sports_manager/group'
|
39
|
+
require_relative 'sports_manager/solution_drawer/cli/solution_table'
|
40
|
+
require_relative 'sports_manager/tournament_day/validator'
|
41
|
+
require_relative 'sports_manager/timeslot_builder'
|
42
|
+
require_relative 'sports_manager/timeslot'
|
43
|
+
require_relative 'sports_manager/bye_match'
|
44
|
+
require_relative 'sports_manager/nil_team'
|
45
|
+
require_relative 'sports_manager/match_builder'
|
46
|
+
require_relative 'sports_manager/team_builder'
|
47
|
+
require_relative 'sports_manager/solution_drawer/cli/table'
|
48
|
+
require_relative 'sports_manager/tournament_generator'
|
49
|
+
require_relative 'sports_manager/solution_drawer/mermaid/gantt'
|
50
|
+
require_relative 'sports_manager/solution_drawer/mermaid/graph'
|
51
|
+
require_relative 'sports_manager/solution_drawer/mermaid/node'
|
52
|
+
require_relative 'sports_manager/solution_drawer/mermaid/node_style'
|
53
|
+
require_relative 'sports_manager/solution_drawer/mermaid/bye_node'
|
54
|
+
require_relative 'sports_manager/solution_drawer/mermaid/solution_graph'
|
55
|
+
require_relative 'sports_manager/solution_drawer/mermaid/solution_gantt'
|
56
|
+
require_relative 'sports_manager/solution_drawer/mermaid'
|
57
|
+
|
58
|
+
module SportsManager
|
59
|
+
end
|
@@ -0,0 +1,52 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module SportsManager
|
4
|
+
module Algorithms
|
5
|
+
module Filtering
|
6
|
+
class NoOverlap
|
7
|
+
extend Forwardable
|
8
|
+
|
9
|
+
attr_reader :tournament
|
10
|
+
|
11
|
+
def_delegators :tournament, :match_time
|
12
|
+
|
13
|
+
def self.for(dependency:, problem: nil) # rubocop:disable Lint/UnusedMethodArgument
|
14
|
+
new(dependency)
|
15
|
+
end
|
16
|
+
|
17
|
+
def initialize(tournament)
|
18
|
+
@tournament = tournament
|
19
|
+
end
|
20
|
+
|
21
|
+
def call(values:, assignment_values: [])
|
22
|
+
unassigned_values = values - assignment_values
|
23
|
+
|
24
|
+
unassigned_values.reject do |timeslot|
|
25
|
+
timeslot_range = to_range(timeslot.slot)
|
26
|
+
|
27
|
+
assignment_values.any? do |assignment_value|
|
28
|
+
next if timeslot.court != assignment_value.court
|
29
|
+
|
30
|
+
assigned_range = to_range(assignment_value.slot)
|
31
|
+
|
32
|
+
overlap?(timeslot_range, assigned_range)
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
private
|
38
|
+
|
39
|
+
def overlap?(time_range1, time_range2)
|
40
|
+
time_range1.cover?(time_range2.first) ||
|
41
|
+
time_range2.cover?(time_range1.first)
|
42
|
+
end
|
43
|
+
|
44
|
+
def to_range(value)
|
45
|
+
end_value = value + (match_time * 60)
|
46
|
+
|
47
|
+
(value...end_value)
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
@@ -0,0 +1,78 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module SportsManager
|
4
|
+
module Algorithms
|
5
|
+
module Ordering
|
6
|
+
class MultipleMatchesParticipant
|
7
|
+
attr_reader :tournament
|
8
|
+
|
9
|
+
def self.for(dependency:, problem: nil) # rubocop:disable Lint/UnusedMethodArgument
|
10
|
+
new(dependency)
|
11
|
+
end
|
12
|
+
|
13
|
+
def initialize(tournament)
|
14
|
+
@tournament = tournament
|
15
|
+
end
|
16
|
+
|
17
|
+
def call(variables)
|
18
|
+
variables.sort_by(&method(:sort))
|
19
|
+
end
|
20
|
+
|
21
|
+
private
|
22
|
+
|
23
|
+
def sort(variable)
|
24
|
+
participants = variable.participants
|
25
|
+
|
26
|
+
multi_matches = boolean_to_integer multi_matches?(participants)
|
27
|
+
|
28
|
+
duplicate_ids = sorted_duplicates(participants)
|
29
|
+
duplicate_ids_size = minimum(duplicate_ids.size)
|
30
|
+
number_of_participants = minimum(participants.size)
|
31
|
+
|
32
|
+
[
|
33
|
+
multi_matches,
|
34
|
+
duplicate_ids_size,
|
35
|
+
duplicate_ids,
|
36
|
+
number_of_participants,
|
37
|
+
variable.id
|
38
|
+
]
|
39
|
+
end
|
40
|
+
|
41
|
+
def multi_matches?(participants)
|
42
|
+
!(multiple_participants & participants).empty?
|
43
|
+
end
|
44
|
+
|
45
|
+
def sorted_duplicates(participants)
|
46
|
+
participants
|
47
|
+
.select { |participant| multiple_participants.include? participant }
|
48
|
+
.map(&:id)
|
49
|
+
.sort
|
50
|
+
end
|
51
|
+
|
52
|
+
def multiple_participants
|
53
|
+
tournament.multi_tournament_participants
|
54
|
+
end
|
55
|
+
|
56
|
+
# Internal: Gives preference for positive finite values over zero
|
57
|
+
# or infinity values when ordering.
|
58
|
+
def minimum(number)
|
59
|
+
values = [number, Float::INFINITY]
|
60
|
+
|
61
|
+
values.min_by do |value|
|
62
|
+
[
|
63
|
+
boolean_to_integer(value.positive?),
|
64
|
+
boolean_to_integer(value.finite?)
|
65
|
+
]
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
# TODO: permit set as ascending or descending and swap -1 and 1
|
70
|
+
# Internal: Truthy values should have a precedence over falsey in
|
71
|
+
# ascending order
|
72
|
+
def boolean_to_integer(truthyness)
|
73
|
+
truthyness ? -1 : 1
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
@@ -0,0 +1,62 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module SportsManager
|
4
|
+
# Public: a Match where it has only one team.
|
5
|
+
# This match is a placeholder used to create the next rounds.
|
6
|
+
# The team in this match will automatically play on the next round.
|
7
|
+
class ByeMatch
|
8
|
+
attr_reader :id, :category, :team1, :team2, :round, :teams, :depends_on
|
9
|
+
|
10
|
+
def initialize(category:, id: nil, team1: nil, team2: nil, round: 0, depends_on: []) # rubocop:disable Metrics/ParameterLists, Lint/UnusedMethodArgument
|
11
|
+
@id = id
|
12
|
+
@category = category
|
13
|
+
@team1 = team1
|
14
|
+
@team2 = team2
|
15
|
+
@round = round
|
16
|
+
@teams = [team1, team2].compact
|
17
|
+
@depends_on = []
|
18
|
+
end
|
19
|
+
|
20
|
+
def playable?
|
21
|
+
false
|
22
|
+
end
|
23
|
+
|
24
|
+
def participants
|
25
|
+
@participants ||= teams.map(&:participants).flatten
|
26
|
+
end
|
27
|
+
|
28
|
+
def dependencies?
|
29
|
+
false
|
30
|
+
end
|
31
|
+
|
32
|
+
def dependencies
|
33
|
+
[]
|
34
|
+
end
|
35
|
+
|
36
|
+
def playable_dependencies
|
37
|
+
[]
|
38
|
+
end
|
39
|
+
|
40
|
+
def previous_matches?
|
41
|
+
false
|
42
|
+
end
|
43
|
+
|
44
|
+
def previous_matches
|
45
|
+
[]
|
46
|
+
end
|
47
|
+
|
48
|
+
def title
|
49
|
+
teams_names.join.concat(' | BYE')
|
50
|
+
end
|
51
|
+
|
52
|
+
def teams_names
|
53
|
+
teams.map(&:name).reject(&:empty?)
|
54
|
+
end
|
55
|
+
|
56
|
+
def ==(other)
|
57
|
+
return false unless instance_of?(other.class)
|
58
|
+
|
59
|
+
id == other.id && category == other.category && round == other.round
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module SportsManager
|
4
|
+
class ConstraintBuilder
|
5
|
+
attr_reader :tournament, :constraints
|
6
|
+
|
7
|
+
DEFAULT_CONSTRAINTS = [
|
8
|
+
Constraints::AllDifferentConstraint,
|
9
|
+
Constraints::NoOverlappingConstraint,
|
10
|
+
Constraints::MatchConstraint,
|
11
|
+
Constraints::MultiCategoryConstraint,
|
12
|
+
Constraints::NextRoundConstraint
|
13
|
+
].freeze
|
14
|
+
|
15
|
+
def self.build(tournament:, csp:, constraints: nil)
|
16
|
+
new(tournament: tournament, constraints: constraints).build(csp)
|
17
|
+
end
|
18
|
+
|
19
|
+
def initialize(tournament:, constraints: nil)
|
20
|
+
@tournament = tournament
|
21
|
+
@constraints = constraints || DEFAULT_CONSTRAINTS
|
22
|
+
end
|
23
|
+
|
24
|
+
def build(csp)
|
25
|
+
constraints.map do |constraint|
|
26
|
+
constraint.for_tournament(tournament: tournament, csp: csp)
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module SportsManager
|
4
|
+
module Constraints
|
5
|
+
# Public: Constraint to all timeslot assignments be different for
|
6
|
+
# each match
|
7
|
+
class AllDifferentConstraint < ::CSP::Constraint
|
8
|
+
attr_reader :matches
|
9
|
+
|
10
|
+
def self.for_tournament(tournament:, csp:)
|
11
|
+
csp.add_constraint(new(tournament.matches.values.flatten))
|
12
|
+
end
|
13
|
+
|
14
|
+
def initialize(matches)
|
15
|
+
super
|
16
|
+
@matches = matches
|
17
|
+
end
|
18
|
+
|
19
|
+
def satisfies?(assignment)
|
20
|
+
assignment.values == assignment.values.uniq
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module SportsManager
|
4
|
+
module Constraints
|
5
|
+
# Public: Restrict match to be set only after the previous matches
|
6
|
+
# it depends on are scheduled.
|
7
|
+
class MatchConstraint < ::CSP::Constraint
|
8
|
+
attr_reader :target_match, :matches
|
9
|
+
|
10
|
+
def self.for_tournament(tournament:, csp:)
|
11
|
+
tournament.matches.each do |(_category, matches)|
|
12
|
+
matches.select(&:previous_matches?).each do |match|
|
13
|
+
csp.add_constraint(
|
14
|
+
new(target_match: match, matches: match.previous_matches)
|
15
|
+
)
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
def initialize(target_match:, matches:)
|
21
|
+
super([target_match] + matches)
|
22
|
+
@target_match = target_match
|
23
|
+
@matches = matches
|
24
|
+
end
|
25
|
+
|
26
|
+
def satisfies?(assignment)
|
27
|
+
return true unless variables.all? { |variable| assignment.key?(variable) }
|
28
|
+
|
29
|
+
target_time = assignment[target_match]
|
30
|
+
|
31
|
+
matches
|
32
|
+
.map { |match| assignment[match] }
|
33
|
+
.all? { |time| time < target_time }
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
@@ -0,0 +1,49 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module SportsManager
|
4
|
+
module Constraints
|
5
|
+
class MultiCategoryConstraint < ::CSP::Constraint
|
6
|
+
attr_reader :target_participant, :matches,
|
7
|
+
:match_time, :break_time, :minimum_match_gap
|
8
|
+
|
9
|
+
MINUTE = 60
|
10
|
+
|
11
|
+
def self.for_tournament(tournament:, csp:)
|
12
|
+
tournament.multi_tournament_participants.each do |participant|
|
13
|
+
matches = tournament.find_participant_matches(participant)
|
14
|
+
|
15
|
+
constraint = new(
|
16
|
+
target_participant: participant,
|
17
|
+
matches: matches,
|
18
|
+
match_time: tournament.match_time,
|
19
|
+
break_time: tournament.break_time
|
20
|
+
)
|
21
|
+
|
22
|
+
csp.add_constraint(constraint)
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
def initialize(target_participant:, matches:, match_time:, break_time:)
|
27
|
+
super(matches)
|
28
|
+
@target_participant = target_participant
|
29
|
+
@matches = matches
|
30
|
+
@match_time = match_time
|
31
|
+
@break_time = break_time
|
32
|
+
@minimum_match_gap = match_time + break_time
|
33
|
+
end
|
34
|
+
|
35
|
+
# TODO: See if needs review in case of solution of split timeslots under match_time
|
36
|
+
def satisfies?(assignment)
|
37
|
+
return true unless variables.all? { |variable| assignment.key?(variable) }
|
38
|
+
|
39
|
+
timeslots = assignment.slice(*variables).values.map(&:slot)
|
40
|
+
|
41
|
+
timeslots.combination(2).all? do |timeslot1, timeslot2|
|
42
|
+
diff_in_minutes = (timeslot1 - timeslot2).abs / MINUTE
|
43
|
+
|
44
|
+
diff_in_minutes >= minimum_match_gap
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
@@ -0,0 +1,48 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module SportsManager
|
4
|
+
module Constraints
|
5
|
+
class NextRoundConstraint < ::CSP::Constraint
|
6
|
+
attr_reader :target_match, :matches,
|
7
|
+
:match_time, :break_time, :minimum_match_gap
|
8
|
+
|
9
|
+
MINUTE = 60
|
10
|
+
|
11
|
+
def self.for_tournament(tournament:, csp:)
|
12
|
+
tournament
|
13
|
+
.matches.values.flatten
|
14
|
+
.select(&:previous_matches?)
|
15
|
+
.each do |match|
|
16
|
+
csp.add_constraint new(
|
17
|
+
target_match: match,
|
18
|
+
matches: match.previous_matches,
|
19
|
+
match_time: tournament.match_time,
|
20
|
+
break_time: tournament.break_time
|
21
|
+
)
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
def initialize(target_match:, matches:, match_time:, break_time:)
|
26
|
+
super([target_match] + matches)
|
27
|
+
@target_match = target_match
|
28
|
+
@matches = matches
|
29
|
+
@match_time = match_time
|
30
|
+
@break_time = break_time
|
31
|
+
@minimum_match_gap = match_time + break_time
|
32
|
+
end
|
33
|
+
|
34
|
+
def satisfies?(assignment)
|
35
|
+
return true unless variables.all? { |variable| assignment.key?(variable) }
|
36
|
+
|
37
|
+
match_time = assignment[target_match].slot
|
38
|
+
matches_timeslots = assignment.slice(*matches).values.map(&:slot)
|
39
|
+
|
40
|
+
matches_timeslots.all? do |timeslot|
|
41
|
+
diff_in_minutes = (timeslot - match_time).abs / MINUTE
|
42
|
+
|
43
|
+
diff_in_minutes >= minimum_match_gap
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
@@ -0,0 +1,55 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module SportsManager
|
4
|
+
module Constraints
|
5
|
+
class NoOverlappingConstraint < ::CSP::Constraint
|
6
|
+
attr_reader :matches, :match_time
|
7
|
+
|
8
|
+
def self.for_tournament(tournament:, csp:)
|
9
|
+
csp.add_constraint(
|
10
|
+
new(
|
11
|
+
matches: tournament.matches.values.flatten,
|
12
|
+
match_time: tournament.match_time
|
13
|
+
)
|
14
|
+
)
|
15
|
+
end
|
16
|
+
|
17
|
+
def initialize(matches:, match_time:)
|
18
|
+
super(matches)
|
19
|
+
@matches = matches
|
20
|
+
@match_time = match_time
|
21
|
+
end
|
22
|
+
|
23
|
+
def satisfies?(assignment)
|
24
|
+
return true unless variables.all? { |variable| assignment.key?(variable) }
|
25
|
+
|
26
|
+
variables
|
27
|
+
.map { |variable| assignment[variable] }
|
28
|
+
.group_by(&:court)
|
29
|
+
.none? { |_, timeslots| timeslots_overlap?(timeslots: timeslots) }
|
30
|
+
end
|
31
|
+
|
32
|
+
private
|
33
|
+
|
34
|
+
def timeslots_overlap?(timeslots:)
|
35
|
+
timeslots
|
36
|
+
.map(&:slot)
|
37
|
+
.map(&method(:to_range))
|
38
|
+
.combination(2)
|
39
|
+
.any?(&method(:overlap?))
|
40
|
+
end
|
41
|
+
|
42
|
+
def overlap?(*ranges)
|
43
|
+
time_range1, time_range2 = ranges.flatten
|
44
|
+
time_range1.cover?(time_range2.first) ||
|
45
|
+
time_range2.cover?(time_range1.first)
|
46
|
+
end
|
47
|
+
|
48
|
+
def to_range(value)
|
49
|
+
end_value = value + (match_time * 60)
|
50
|
+
|
51
|
+
(value...end_value)
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module SportsManager
|
4
|
+
# TODO: change the name Group. In terms of sports tournament it means something else.
|
5
|
+
# Public: A category with teams and matches between them
|
6
|
+
class Group
|
7
|
+
attr_reader :category, :all_matches, :teams
|
8
|
+
|
9
|
+
def self.for(category:, subscriptions:, matches:, tournament_type:)
|
10
|
+
GroupBuilder.new(category: category, subscriptions: subscriptions, matches: matches,
|
11
|
+
tournament_type: tournament_type).build
|
12
|
+
end
|
13
|
+
|
14
|
+
def initialize(category: nil, matches: nil, teams: nil)
|
15
|
+
@category = category
|
16
|
+
@all_matches = matches
|
17
|
+
@teams = teams
|
18
|
+
end
|
19
|
+
|
20
|
+
def participants
|
21
|
+
@participants ||= teams.map(&:participants).flatten
|
22
|
+
end
|
23
|
+
|
24
|
+
def matches
|
25
|
+
@matches ||= all_matches.select(&:playable?)
|
26
|
+
end
|
27
|
+
|
28
|
+
def first_round_matches
|
29
|
+
find_matches(0)
|
30
|
+
end
|
31
|
+
|
32
|
+
def find_matches(round_number)
|
33
|
+
matches.select { |match| match.round == round_number }
|
34
|
+
end
|
35
|
+
|
36
|
+
def find_participant_matches(participant)
|
37
|
+
matches.select do |match|
|
38
|
+
match.participants.include? participant
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
@@ -0,0 +1,72 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module SportsManager
|
4
|
+
# Public: Builds a group with matches, teams, and participants from a payload
|
5
|
+
#
|
6
|
+
# category - The matches' category.
|
7
|
+
# subscriptions - The players subscribed. Doubles are represented as a nested list.
|
8
|
+
# matches - A list of matches. Each match is represented by a list of
|
9
|
+
# players that will be participaing in it.
|
10
|
+
#
|
11
|
+
# Example 1: A Team of one player
|
12
|
+
# Mixed single category with subscribed players: João, Marcelo, Bruno, and Fábio.
|
13
|
+
# The initial matches will be:
|
14
|
+
# * João vs Fábio
|
15
|
+
# * Marcelo vs Bruno
|
16
|
+
#
|
17
|
+
# category: :mixed_single
|
18
|
+
# subscriptions: [
|
19
|
+
# { id: 1, name: "João"},
|
20
|
+
# { id: 2, name: "Marcelo" },
|
21
|
+
# { id: 15, name: 'Bruno' },
|
22
|
+
# { id: 16, name: 'Fábio' }
|
23
|
+
# ]
|
24
|
+
# matches: [
|
25
|
+
# [1,16],
|
26
|
+
# [2,15]
|
27
|
+
# ]
|
28
|
+
#
|
29
|
+
# Example 2: A team of two players
|
30
|
+
# Women's double category with subscribed players: Laura, Karina, Camila,
|
31
|
+
# Bruna, Carolina, Patricia, Jéssica, and Daniela.
|
32
|
+
#
|
33
|
+
# The initial matches will be:
|
34
|
+
# * Laura and Karina vs Jéssica and Daniela
|
35
|
+
# * Camila and Bruna vs Carolina and Patricia
|
36
|
+
#
|
37
|
+
# category: :womens_double
|
38
|
+
# subscriptions: [
|
39
|
+
# [{ id: 17, name: 'Laura' }, { id: 18, name: 'Karina' }],
|
40
|
+
# [{ id: 19, name: 'Camila' }, { id: 20, name: 'Bruna' }],
|
41
|
+
# [{ id: 29, name: 'Carolina' }, { id: 30, name: 'Patricia' }],
|
42
|
+
# [{ id: 31, name: 'Jéssica' }, { id: 32, name: 'Daniela' }]
|
43
|
+
# ]
|
44
|
+
# matches: [
|
45
|
+
# [[17, 18], [31, 32]],
|
46
|
+
# [[19, 20], [29, 30]]
|
47
|
+
# ]
|
48
|
+
class GroupBuilder
|
49
|
+
attr_reader :category, :subscriptions, :matches, :tournament_type
|
50
|
+
|
51
|
+
def initialize(category:, subscriptions:, matches:, tournament_type:)
|
52
|
+
@category = category
|
53
|
+
@subscriptions = subscriptions
|
54
|
+
@matches = matches
|
55
|
+
@tournament_type = tournament_type
|
56
|
+
end
|
57
|
+
|
58
|
+
def build
|
59
|
+
Group.new(category: category, matches: builded_matches, teams: teams)
|
60
|
+
end
|
61
|
+
|
62
|
+
private
|
63
|
+
|
64
|
+
def builded_matches
|
65
|
+
MatchBuilder.new(category: category, matches: matches, teams: teams, tournament_type: tournament_type).build
|
66
|
+
end
|
67
|
+
|
68
|
+
def teams
|
69
|
+
TeamBuilder.new(category: category, subscriptions: subscriptions).build
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|