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,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SportsManager
4
+ class MatchesGenerator
5
+ def self.call(subscriptions_ids)
6
+ new(subscriptions_ids).call
7
+ end
8
+
9
+ def initialize(subscriptions_ids)
10
+ @subscriptions_ids = subscriptions_ids
11
+ end
12
+
13
+ def call
14
+ generate_matches(@subscriptions_ids)
15
+ end
16
+
17
+ private
18
+
19
+ attr_reader :subscriptions
20
+
21
+ def generate_matches(subscriptions_ids)
22
+ list = subscriptions_ids.dup
23
+ size = subscriptions_ids.size
24
+
25
+ number_of_matches = size.even? ? (size / 2) : ((size / 2) + 1)
26
+
27
+ number_of_matches.times.map do
28
+ match = [list.shift, list.pop].compact
29
+ match unless match.empty?
30
+ end.compact
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SportsManager
4
+ # Public: A representation of no Team used by next round matches that don't
5
+ # have a winner set yet.
6
+ class NilTeam
7
+ attr_reader :category, :participants
8
+
9
+ def initialize(category:)
10
+ @category = category
11
+ @participants = []
12
+ end
13
+
14
+ def name
15
+ ''
16
+ end
17
+
18
+ def ==(other)
19
+ return false unless instance_of? other.class
20
+
21
+ category == other.category
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SportsManager
4
+ # Public: A team's player in a tournament
5
+ class Participant
6
+ include Comparable
7
+
8
+ attr_reader :id, :name
9
+
10
+ alias eql? <=>
11
+
12
+ def initialize(id:, name:)
13
+ @id = id
14
+ @name = name
15
+ end
16
+
17
+ def <=>(other)
18
+ return unless instance_of?(other.class)
19
+
20
+ id <=> other.id
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SportsManager
4
+ # TODO: keep only team
5
+ class SingleTeam < Team
6
+ end
7
+ end
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SportsManager
4
+ class SolutionDrawer
5
+ class CLI
6
+ class SolutionTable
7
+ TIME_TEMPLATE = '%d/%m at %H:%M'
8
+
9
+ attr_reader :fixtures
10
+
11
+ def initialize(solution)
12
+ @fixtures = solution.fixtures
13
+ end
14
+
15
+ def draw
16
+ Table.draw(formatted_fixtures)
17
+ end
18
+
19
+ private
20
+
21
+ def formatted_fixtures
22
+ fixtures.map(&method(:serialized_fixture))
23
+ end
24
+
25
+ def serialized_fixture(fixture)
26
+ {
27
+ category: fixture.category,
28
+ id: fixture.match_id,
29
+ round: fixture.round,
30
+ participants: fixture.title,
31
+ court: fixture.court,
32
+ time: fixture.slot.strftime(TIME_TEMPLATE)
33
+ }
34
+ end
35
+ end
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,94 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SportsManager
4
+ class SolutionDrawer
5
+ class CLI
6
+ # Public: creates a table based on a list of hashes containing the
7
+ # name of the columns and their values
8
+ #
9
+ # Example:
10
+ # data = [
11
+ # { a: [1, 2, 3], b: 10_000, c: 'ABCDEFGHIJKLMNOP' },
12
+ # { a: [4, 5, 6, 7], b: 20_000_000, c: 'QRSTUVWXYZ' }
13
+ # ]
14
+ #
15
+ # a | b | c
16
+ # -------------|----------|-----------------
17
+ # 1, 2, 3 | 10000 | ABCDEFGHIJKLMNOP
18
+ # 4, 5, 6, 7 | 20000000 | QRSTUVWXYZ
19
+ class Table
20
+ attr_reader :data
21
+
22
+ SECTION_SEPARATOR = '-|-'
23
+ COLUMN_SEPARATOR = ' | '
24
+ ROW_SEPARATOR = '-'
25
+
26
+ def self.draw(data)
27
+ new(data).draw
28
+ end
29
+
30
+ def initialize(data)
31
+ @data = data
32
+ end
33
+
34
+ def draw
35
+ [header_row, separator, *content_rows].join("\n")
36
+ end
37
+
38
+ private
39
+
40
+ def header
41
+ @header ||= data.flat_map(&:keys).uniq
42
+ end
43
+
44
+ def header_row
45
+ header
46
+ .map { |column| column.to_s.center(widths[column]) }
47
+ .join(COLUMN_SEPARATOR)
48
+ end
49
+
50
+ def separator
51
+ header
52
+ .map { |column| ROW_SEPARATOR * widths[column] }
53
+ .join(SECTION_SEPARATOR)
54
+ end
55
+
56
+ def content_rows
57
+ data
58
+ .map(&method(:row_content))
59
+ .map { |row| row.join(COLUMN_SEPARATOR) }
60
+ end
61
+
62
+ def row_content(row)
63
+ row.map do |(column, value)|
64
+ column_padding = widths[column]
65
+
66
+ Array(value)
67
+ .join(', ')
68
+ .ljust(column_padding)
69
+ end
70
+ end
71
+
72
+ # Internal: sets the longest width for each column
73
+ def widths
74
+ @widths ||= data
75
+ .flat_map(&:to_a)
76
+ .each_with_object(headers_width) do |(key, value), columns_width|
77
+ column_size = columns_width[key].to_i
78
+ value_size = value.to_s.size
79
+
80
+ columns_width[key] = [column_size, value_size].max
81
+ end
82
+ end
83
+
84
+ # Internal: sets the starting width for each column based on
85
+ # their names
86
+ def headers_width
87
+ header.each_with_object({}) do |column, width|
88
+ width[column.to_sym] = column.to_s.size
89
+ end
90
+ end
91
+ end
92
+ end
93
+ end
94
+ end
@@ -0,0 +1,75 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SportsManager
4
+ class SolutionDrawer
5
+ # Public: Outputs the tournament's timetable solutions to stdout
6
+ # TODO: allow to pass which stdout will be used. puts|Logger|other
7
+ class CLI
8
+ extend Forwardable
9
+
10
+ attr_reader :tournament_solution
11
+
12
+ def_delegators :tournament_solution, :solutions,
13
+ :total_solutions, :solved?
14
+
15
+ NO_SOLUTION = 'No solution found'
16
+ START_NUMBER = 1
17
+
18
+ def self.draw(tournament_solution)
19
+ new(tournament_solution).draw
20
+ end
21
+
22
+ def initialize(tournament_solution)
23
+ @tournament_solution = tournament_solution
24
+ end
25
+
26
+ def draw
27
+ no_solution || draw_solutions
28
+ end
29
+
30
+ private
31
+
32
+ def no_solution
33
+ return false if solved?
34
+
35
+ puts NO_SOLUTION
36
+
37
+ true
38
+ end
39
+
40
+ def draw_solutions
41
+ puts(
42
+ output_header,
43
+ output_solutions,
44
+ output_footer
45
+ )
46
+ end
47
+
48
+ def output_header
49
+ "Tournament Timetable:\n\n"
50
+ end
51
+
52
+ def output_solutions
53
+ solutions_tables.join("\n")
54
+ end
55
+
56
+ def output_footer
57
+ "Total solutions: #{total_solutions}"
58
+ end
59
+
60
+ def solutions_tables
61
+ solutions.map.with_index(START_NUMBER, &method(:draw_solution))
62
+ end
63
+
64
+ def draw_solution(solution, number)
65
+ table_data = SolutionTable.new(solution).draw
66
+
67
+ <<~MESSAGE
68
+ Solution #{number}
69
+ #{table_data}
70
+
71
+ MESSAGE
72
+ end
73
+ end
74
+ end
75
+ end
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SportsManager
4
+ class SolutionDrawer
5
+ class Mermaid
6
+ class ByeNode
7
+ extend Forwardable
8
+
9
+ attr_reader :fixture
10
+
11
+ def_delegators :fixture, :match_id, :category, :title
12
+
13
+ def initialize(fixture)
14
+ @fixture = fixture
15
+ end
16
+
17
+ def name
18
+ "#{category}_#{match_id}"
19
+ end
20
+
21
+ def definition
22
+ "#{name}[\"#{description}\"]:::#{style_class}"
23
+ end
24
+
25
+ def style_class
26
+ 'court'
27
+ end
28
+
29
+ def description
30
+ "#{match_id}\\n#{title}"
31
+ end
32
+
33
+ def links?
34
+ false
35
+ end
36
+ end
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,126 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SportsManager
4
+ class SolutionDrawer
5
+ class Mermaid
6
+ class Gantt
7
+ extend Forwardable
8
+
9
+ attr_reader :configurations
10
+
11
+ DISPLAY_MODE = {
12
+ compact: 'compact',
13
+ empty: nil
14
+ }.freeze
15
+
16
+ FIELDS = %i[
17
+ display_mode
18
+ title
19
+ date_format
20
+ axis_format
21
+ tick_interval
22
+ sections
23
+ ].freeze
24
+
25
+ DEFAULT_CONFIGURATIONS = {
26
+ display_mode: :compact,
27
+ title: 'Tournament Schedule',
28
+ date_format: 'DD/MM HH:mm',
29
+ axis_format: '%H:%M',
30
+ tick_interval: '1hour',
31
+ sections: ''
32
+ }.freeze
33
+
34
+ TEMPLATE = <<~GANTT.chomp
35
+ ---
36
+ displayMode: %<display_mode>s
37
+ ---
38
+ gantt
39
+ title %<title>s
40
+ dateFormat %<date_format>s
41
+ axisFormat %<axis_format>s
42
+ tickInterval %<tick_interval>s
43
+
44
+ %<sections>s
45
+ GANTT
46
+
47
+ TASK_TEMPLATE = ' %<id>s: %<time>s, %<interval>s'
48
+
49
+ def self.draw(configurations = {})
50
+ new
51
+ .add_configurations(configurations)
52
+ .draw
53
+ end
54
+
55
+ def initialize
56
+ @configurations = {}
57
+ end
58
+
59
+ def draw
60
+ format(TEMPLATE, gantt_configurations)
61
+ end
62
+
63
+ def add_display_mode(mode = :compact)
64
+ configurations[:display_mode] = DISPLAY_MODE[mode.to_sym]
65
+
66
+ self
67
+ end
68
+
69
+ def add_title(title)
70
+ configurations[:title] = title
71
+
72
+ self
73
+ end
74
+
75
+ def add_date_format(format)
76
+ configurations[:date_format] = format
77
+
78
+ self
79
+ end
80
+
81
+ def add_axis_format(format)
82
+ configurations[:axis_format] = format
83
+
84
+ self
85
+ end
86
+
87
+ def add_tick_interval(format)
88
+ configurations[:tick_interval] = format
89
+
90
+ self
91
+ end
92
+
93
+ def add_sections(sections_tasks)
94
+ configurations[:sections] = sections_tasks
95
+ .map { |(section, tasks)| build_section(section, tasks) }
96
+ .join("\n")
97
+
98
+ self
99
+ end
100
+
101
+ def add_configurations(configurations)
102
+ configurations.slice(*FIELDS).each do |field, value|
103
+ public_send("add_#{field}", value)
104
+ end
105
+
106
+ self
107
+ end
108
+
109
+ private
110
+
111
+ def gantt_configurations
112
+ DEFAULT_CONFIGURATIONS
113
+ .merge(configurations)
114
+ .slice(*FIELDS)
115
+ end
116
+
117
+ def build_section(section, tasks)
118
+ section_name = " section #{section}"
119
+ section_tasks = tasks.map { |fields| format(TASK_TEMPLATE, fields) }
120
+
121
+ [section_name, section_tasks].join("\n")
122
+ end
123
+ end
124
+ end
125
+ end
126
+ end
@@ -0,0 +1,111 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SportsManager
4
+ class SolutionDrawer
5
+ class Mermaid
6
+ class Graph
7
+ attr_reader :configurations
8
+
9
+ FIELDS = %i[
10
+ subgraphs
11
+ class_definitions
12
+ subgraph_colorscheme
13
+ ].freeze
14
+
15
+ TEMPLATE = <<~GRAPH.chomp
16
+ graph LR
17
+ %<class_definitions>s
18
+ %<subgraph_colorscheme>s
19
+ %<subgraphs>s
20
+ GRAPH
21
+
22
+ SUBGRAPH = <<~GRAPH.chomp
23
+ subgraph %<name>s
24
+ direction LR
25
+
26
+ %<nodes>s
27
+ end
28
+ GRAPH
29
+
30
+ STYLE = <<~GRAPH.chomp
31
+ classDef %<class_definition>s
32
+ GRAPH
33
+
34
+ # TODO: move to class and make dynamic based on input
35
+ DEFAULT_STYLE_CONFIGURATIONS = {
36
+ class_definitions: [
37
+ 'court0 fill:#a9f9a9',
38
+ 'court1 fill:#4ff7de',
39
+ 'court_default fill:#aff7de'
40
+ ],
41
+ subgraph_colorscheme: [
42
+ 'COURT_0:::court0',
43
+ 'COURT_1:::court1',
44
+ 'COURT:::court_default',
45
+ 'COURT_0 --- COURT_1 --- COURT'
46
+ ]
47
+ }.freeze
48
+
49
+ def self.draw(configurations = {})
50
+ new
51
+ .add_configurations(configurations)
52
+ .draw
53
+ end
54
+
55
+ def initialize
56
+ @configurations = {}
57
+ end
58
+
59
+ def draw
60
+ format(TEMPLATE, graph_configurations)
61
+ end
62
+
63
+ def add_configurations(configurations)
64
+ DEFAULT_STYLE_CONFIGURATIONS
65
+ .merge(configurations)
66
+ .slice(*FIELDS)
67
+ .each { |field, value| public_send("add_#{field}", value) }
68
+
69
+ self
70
+ end
71
+
72
+ def add_subgraphs(subgraphs)
73
+ configurations[:subgraphs] = subgraphs
74
+ .map { |(name, nodes)| build_subgraph(name, nodes) }
75
+ .join("\n")
76
+
77
+ self
78
+ end
79
+
80
+ def add_class_definitions(class_definitions)
81
+ configurations[:class_definitions] = class_definitions
82
+ .map { |class_definition| format(STYLE, class_definition: class_definition) }
83
+ .join("\n")
84
+
85
+ self
86
+ end
87
+
88
+ def add_subgraph_colorscheme(subgraph_colorscheme)
89
+ nodes = subgraph_colorscheme.join("\n ")
90
+ colorscheme = format(SUBGRAPH, name: 'colorscheme', nodes: nodes)
91
+
92
+ configurations[:subgraph_colorscheme] = colorscheme
93
+
94
+ self
95
+ end
96
+
97
+ private
98
+
99
+ def graph_configurations
100
+ configurations.slice(*FIELDS)
101
+ end
102
+
103
+ def build_subgraph(name, nodes)
104
+ subgraph_nodes = nodes.join("\n ")
105
+
106
+ format(SUBGRAPH, name: name, nodes: subgraph_nodes)
107
+ end
108
+ end
109
+ end
110
+ end
111
+ end
@@ -0,0 +1,55 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SportsManager
4
+ class SolutionDrawer
5
+ class Mermaid
6
+ class Node
7
+ extend Forwardable
8
+ TIME_TEMPLATE = '%d/%m %H:%M'
9
+
10
+ attr_reader :fixture
11
+
12
+ def_delegators :fixture, :match_id, :category, :title, :court
13
+
14
+ def self.for(fixture)
15
+ node_class = fixture.playable? ? self : ByeNode
16
+
17
+ node_class.new(fixture)
18
+ end
19
+
20
+ def initialize(fixture)
21
+ @fixture = fixture
22
+ end
23
+
24
+ def name
25
+ "#{category}_#{match_id}"
26
+ end
27
+
28
+ # Internal: my_node[This is a node]:::awesome_style
29
+ def definition
30
+ "#{name}[#{description}]:::#{style_class}"
31
+ end
32
+
33
+ def style_class
34
+ "court#{court}"
35
+ end
36
+
37
+ def description
38
+ "#{match_id}\\n#{title}\\n#{slot}"
39
+ end
40
+
41
+ def slot
42
+ fixture.slot.strftime(TIME_TEMPLATE)
43
+ end
44
+
45
+ def links?
46
+ fixture.dependencies?
47
+ end
48
+
49
+ def depends_on?(node)
50
+ fixture.depends_on?(node.fixture)
51
+ end
52
+ end
53
+ end
54
+ end
55
+ end