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