playoffs 1.0.0

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 (123) hide show
  1. checksums.yaml +7 -0
  2. data/.bundle/config +2 -0
  3. data/.editorconfig +8 -0
  4. data/.github/workflows/ci.yaml +29 -0
  5. data/.gitignore +8 -0
  6. data/.rubocop.yml +34 -0
  7. data/.tool-versions +1 -0
  8. data/.vscode/settings.json +5 -0
  9. data/CHANGELOG.md +3 -0
  10. data/CODE_OF_CONDUCT.md +73 -0
  11. data/Gemfile +5 -0
  12. data/Guardfile +17 -0
  13. data/LICENSE +5 -0
  14. data/README.md +245 -0
  15. data/Rakefile +11 -0
  16. data/bin/_guard-core +27 -0
  17. data/bin/bundle +109 -0
  18. data/bin/bundle-audit +27 -0
  19. data/bin/bundler-audit +27 -0
  20. data/bin/coderay +27 -0
  21. data/bin/console +11 -0
  22. data/bin/guard +27 -0
  23. data/bin/htmldiff +27 -0
  24. data/bin/ldiff +27 -0
  25. data/bin/listen +27 -0
  26. data/bin/playoffs +27 -0
  27. data/bin/pry +27 -0
  28. data/bin/racc +27 -0
  29. data/bin/rake +27 -0
  30. data/bin/rspec +27 -0
  31. data/bin/rubocop +27 -0
  32. data/bin/ruby-parse +27 -0
  33. data/bin/ruby-rewrite +27 -0
  34. data/bin/spoom +27 -0
  35. data/bin/srb +27 -0
  36. data/bin/srb-rbi +27 -0
  37. data/bin/tapioca +27 -0
  38. data/bin/thor +27 -0
  39. data/bin/yard +27 -0
  40. data/bin/yardoc +27 -0
  41. data/bin/yri +27 -0
  42. data/exe/playoffs +7 -0
  43. data/lib/playoffs/basketball.rb +120 -0
  44. data/lib/playoffs/best_of.rb +38 -0
  45. data/lib/playoffs/cli.rb +201 -0
  46. data/lib/playoffs/contestant.rb +8 -0
  47. data/lib/playoffs/round.rb +42 -0
  48. data/lib/playoffs/series.rb +167 -0
  49. data/lib/playoffs/team.rb +7 -0
  50. data/lib/playoffs/tournament/bracketable.rb +93 -0
  51. data/lib/playoffs/tournament/roundable.rb +53 -0
  52. data/lib/playoffs/tournament.rb +61 -0
  53. data/lib/playoffs/version.rb +6 -0
  54. data/lib/playoffs.rb +26 -0
  55. data/playoffs.gemspec +49 -0
  56. data/sorbet/config +4 -0
  57. data/sorbet/rbi/annotations/.gitattributes +1 -0
  58. data/sorbet/rbi/annotations/rainbow.rbi +269 -0
  59. data/sorbet/rbi/gems/.gitattributes +1 -0
  60. data/sorbet/rbi/gems/ansi@1.5.0.rbi +688 -0
  61. data/sorbet/rbi/gems/ast@2.4.2.rbi +585 -0
  62. data/sorbet/rbi/gems/bundler-audit@0.9.1.rbi +309 -0
  63. data/sorbet/rbi/gems/coderay@1.1.3.rbi +3426 -0
  64. data/sorbet/rbi/gems/diff-lcs@1.5.1.rbi +1131 -0
  65. data/sorbet/rbi/gems/docile@1.4.0.rbi +377 -0
  66. data/sorbet/rbi/gems/erubi@1.12.0.rbi +145 -0
  67. data/sorbet/rbi/gems/ffi@1.16.3.rbi +9 -0
  68. data/sorbet/rbi/gems/formatador@1.1.0.rbi +9 -0
  69. data/sorbet/rbi/gems/guard-compat@1.2.1.rbi +67 -0
  70. data/sorbet/rbi/gems/guard-rspec@4.7.3.rbi +563 -0
  71. data/sorbet/rbi/gems/guard@2.18.1.rbi +9 -0
  72. data/sorbet/rbi/gems/json@2.7.2.rbi +1562 -0
  73. data/sorbet/rbi/gems/language_server-protocol@3.17.0.3.rbi +14238 -0
  74. data/sorbet/rbi/gems/listen@3.9.0.rbi +9 -0
  75. data/sorbet/rbi/gems/lumberjack@1.2.10.rbi +9 -0
  76. data/sorbet/rbi/gems/method_source@1.1.0.rbi +304 -0
  77. data/sorbet/rbi/gems/nenv@0.3.0.rbi +9 -0
  78. data/sorbet/rbi/gems/netrc@0.11.0.rbi +158 -0
  79. data/sorbet/rbi/gems/notiffany@0.1.3.rbi +9 -0
  80. data/sorbet/rbi/gems/parallel@1.24.0.rbi +280 -0
  81. data/sorbet/rbi/gems/parser@3.3.1.0.rbi +7238 -0
  82. data/sorbet/rbi/gems/primitive@1.0.0.rbi +58 -0
  83. data/sorbet/rbi/gems/prism@0.29.0.rbi +37987 -0
  84. data/sorbet/rbi/gems/pry@0.14.2.rbi +10069 -0
  85. data/sorbet/rbi/gems/racc@1.7.3.rbi +162 -0
  86. data/sorbet/rbi/gems/rainbow@3.1.1.rbi +403 -0
  87. data/sorbet/rbi/gems/rake@13.2.1.rbi +3028 -0
  88. data/sorbet/rbi/gems/rb-fsevent@0.11.2.rbi +9 -0
  89. data/sorbet/rbi/gems/rb-inotify@0.10.1.rbi +9 -0
  90. data/sorbet/rbi/gems/rbi@0.1.13.rbi +3078 -0
  91. data/sorbet/rbi/gems/regexp_parser@2.9.2.rbi +3772 -0
  92. data/sorbet/rbi/gems/rexml@3.2.8.rbi +4794 -0
  93. data/sorbet/rbi/gems/rspec-core@3.13.0.rbi +10874 -0
  94. data/sorbet/rbi/gems/rspec-expectations@3.13.0.rbi +8154 -0
  95. data/sorbet/rbi/gems/rspec-mocks@3.13.1.rbi +5341 -0
  96. data/sorbet/rbi/gems/rspec-support@3.13.1.rbi +1630 -0
  97. data/sorbet/rbi/gems/rspec@3.13.0.rbi +83 -0
  98. data/sorbet/rbi/gems/rubocop-ast@1.31.3.rbi +7159 -0
  99. data/sorbet/rbi/gems/rubocop-capybara@2.20.0.rbi +1208 -0
  100. data/sorbet/rbi/gems/rubocop-factory_bot@2.25.1.rbi +928 -0
  101. data/sorbet/rbi/gems/rubocop-rake@0.6.0.rbi +329 -0
  102. data/sorbet/rbi/gems/rubocop-rspec@2.29.2.rbi +8247 -0
  103. data/sorbet/rbi/gems/rubocop-rspec_rails@2.28.3.rbi +911 -0
  104. data/sorbet/rbi/gems/rubocop-sorbet@0.8.3.rbi +1607 -0
  105. data/sorbet/rbi/gems/rubocop@1.63.5.rbi +57788 -0
  106. data/sorbet/rbi/gems/ruby-progressbar@1.13.0.rbi +1318 -0
  107. data/sorbet/rbi/gems/shellany@0.0.1.rbi +9 -0
  108. data/sorbet/rbi/gems/simplecov-console@0.9.1.rbi +103 -0
  109. data/sorbet/rbi/gems/simplecov-html@0.12.3.rbi +217 -0
  110. data/sorbet/rbi/gems/simplecov@0.22.0.rbi +2149 -0
  111. data/sorbet/rbi/gems/simplecov_json_formatter@0.1.4.rbi +9 -0
  112. data/sorbet/rbi/gems/sorbet-runtime-stub@0.2.0.rbi +8 -0
  113. data/sorbet/rbi/gems/spoom@1.3.2.rbi +4420 -0
  114. data/sorbet/rbi/gems/strscan@3.1.0.rbi +9 -0
  115. data/sorbet/rbi/gems/tapioca@0.14.2.rbi +3539 -0
  116. data/sorbet/rbi/gems/terminal-table@3.0.2.rbi +9 -0
  117. data/sorbet/rbi/gems/thor@1.3.1.rbi +4318 -0
  118. data/sorbet/rbi/gems/unicode-display_width@2.5.0.rbi +66 -0
  119. data/sorbet/rbi/gems/yard-sorbet@0.8.1.rbi +428 -0
  120. data/sorbet/rbi/gems/yard@0.9.36.rbi +18085 -0
  121. data/sorbet/tapioca/config.yml +13 -0
  122. data/sorbet/tapioca/require.rb +4 -0
  123. metadata +383 -0
@@ -0,0 +1,120 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ module Playoffs
5
+ # Knows how to create a tournament specific to professional basketball.
6
+ class Basketball
7
+ extend T::Sig
8
+
9
+ # rubocop:disable Metrics/MethodLength, Metrics/AbcSize
10
+ sig do
11
+ params(
12
+ eastern_teams: T::Array[Team],
13
+ western_teams: T::Array[Team]
14
+ ).returns(Tournament)
15
+ end
16
+ def self.tournament_for(eastern_teams:, western_teams:)
17
+ eastern_teams = eastern_teams.uniq
18
+ western_teams = western_teams.uniq
19
+
20
+ raise IncorrectTeamCount unless eastern_teams.size == 10
21
+ raise IncorrectTeamCount unless western_teams.size == 10
22
+
23
+ play_in_best_of = BestOf.new(1)
24
+
25
+ eastern_play_in78 = Series.new(best_of: play_in_best_of)
26
+ .register(T.must(eastern_teams[6]))
27
+ .register(T.must(eastern_teams[7]))
28
+
29
+ eastern_play_in910 = Series
30
+ .new(best_of: play_in_best_of)
31
+ .register(T.must(eastern_teams[8]))
32
+ .register(T.must(eastern_teams[9]))
33
+
34
+ eastern_play_in_consolation = Series
35
+ .new(best_of: play_in_best_of)
36
+ .loser_of(eastern_play_in78)
37
+ .winner_of(eastern_play_in910)
38
+
39
+ western_play_in78 = Series
40
+ .new(best_of: play_in_best_of)
41
+ .register(T.must(western_teams[6]))
42
+ .register(T.must(western_teams[7]))
43
+
44
+ western_play_in910 = Series
45
+ .new(best_of: play_in_best_of)
46
+ .register(T.must(western_teams[8]))
47
+ .register(T.must(western_teams[9]))
48
+
49
+ western_play_in_consolation = Series
50
+ .new(best_of: play_in_best_of)
51
+ .loser_of(western_play_in78)
52
+ .loser_of(western_play_in910)
53
+
54
+ championship =
55
+ Series
56
+ .new
57
+ .winner_of(
58
+ Series.new
59
+ .winner_of(
60
+ Series.new
61
+ .winner_of(
62
+ Series.new
63
+ .register(T.must(eastern_teams[0]))
64
+ .winner_of(eastern_play_in_consolation)
65
+ )
66
+ .winner_of(
67
+ Series.new
68
+ .register(T.must(eastern_teams[3]))
69
+ .register(T.must(eastern_teams[4]))
70
+ )
71
+ )
72
+ .winner_of(
73
+ Series.new
74
+ .winner_of(
75
+ Series.new
76
+ .register(T.must(eastern_teams[2]))
77
+ .register(T.must(eastern_teams[5]))
78
+ )
79
+ .winner_of(
80
+ Series.new
81
+ .register(T.must(eastern_teams[1]))
82
+ .winner_of(eastern_play_in78)
83
+ )
84
+ )
85
+ )
86
+ .winner_of(
87
+ Series.new
88
+ .winner_of(
89
+ Series.new
90
+ .winner_of(
91
+ Series.new
92
+ .register(T.must(western_teams[0]))
93
+ .winner_of(western_play_in_consolation)
94
+ )
95
+ .winner_of(
96
+ Series.new
97
+ .register(T.must(western_teams[3]))
98
+ .register(T.must(western_teams[4]))
99
+ )
100
+ )
101
+ .winner_of(
102
+ Series.new
103
+ .winner_of(
104
+ Series.new
105
+ .register(T.must(western_teams[2]))
106
+ .register(T.must(western_teams[5]))
107
+ )
108
+ .winner_of(
109
+ Series.new
110
+ .register(T.must(western_teams[1]))
111
+ .winner_of(western_play_in78)
112
+ )
113
+ )
114
+ )
115
+
116
+ Tournament.new(championship)
117
+ end
118
+ # rubocop:enable Metrics/MethodLength, Metrics/AbcSize
119
+ end
120
+ end
@@ -0,0 +1,38 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ module Playoffs
5
+ # Contains rules for how many total a series can be.
6
+ class BestOf
7
+ extend T::Sig
8
+
9
+ sig { returns(Integer) }
10
+ attr_reader :total
11
+
12
+ sig { params(total: Integer).void }
13
+ def initialize(total = 7)
14
+ raise ArgumentError, 'total has to be positive' unless total.positive?
15
+ raise ArgumentError, 'total has to be odd' unless total.odd?
16
+
17
+ @total = total
18
+
19
+ freeze
20
+ end
21
+
22
+ sig { returns(Integer) }
23
+ def to_win
24
+ (total / 2.0).ceil
25
+ end
26
+
27
+ sig { returns(String) }
28
+ def to_s
29
+ "#{self.class.to_s.split('::').last}::#{total}"
30
+ end
31
+
32
+ sig { params(other: BestOf).returns(T::Boolean) }
33
+ def ==(other)
34
+ total == other.total
35
+ end
36
+ alias eql? ==
37
+ end
38
+ end
@@ -0,0 +1,201 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ module Playoffs
5
+ # A very simple list of commands matching ARGV as input. Allows for capturing the output
6
+ # via the passed in IO object used for construction. This class is an example of how the
7
+ # underlying data structures, such as: BestOf, Round, Series, Team, and Tournament,
8
+ # in this library can be used.
9
+ class Cli
10
+ extend T::Sig
11
+
12
+ sig { returns(T.any(IO, StringIO)) }
13
+ attr_reader :io
14
+
15
+ sig { returns(String) }
16
+ attr_reader :script
17
+
18
+ sig { params(io: T.any(IO, StringIO), script: String).void }
19
+ def initialize(io, script: 'bin/playoffs')
20
+ @io = io
21
+ @script = script
22
+ end
23
+
24
+ sig { params(args: T::Array[String]).returns(Cli) }
25
+ def run(args)
26
+ path = args[0].to_s
27
+ action = args[1].to_s
28
+
29
+ if path.empty? && action.empty?
30
+ run_help(path, action, args)
31
+
32
+ return self
33
+ end
34
+
35
+ action = 'bracket' if action.empty?
36
+
37
+ method_name = "run_#{action}"
38
+
39
+ if respond_to?(method_name, true)
40
+ send(method_name, path, action, args)
41
+ else
42
+ io.puts("Unknown: #{action}")
43
+ end
44
+
45
+ self
46
+ end
47
+
48
+ protected
49
+
50
+ sig { overridable.params(path: String, tournament: Tournament).returns(Cli) }
51
+ def save(path, tournament)
52
+ FileUtils.mkdir_p(File.dirname(path))
53
+
54
+ File.open(path, 'w') { |out| YAML.dump(tournament, out) }
55
+
56
+ self
57
+ end
58
+
59
+ sig { overridable.params(path: String).returns(Tournament) }
60
+ def load(path)
61
+ YAML.load_file(
62
+ path,
63
+ permitted_classes: [
64
+ BestOf,
65
+ Round,
66
+ Series,
67
+ Team,
68
+ Tournament
69
+ ],
70
+ aliases: true
71
+ )
72
+ end
73
+
74
+ sig { overridable.params(series: Series).returns(Team) }
75
+ def pick(series)
76
+ index = rand(0..1)
77
+
78
+ T.must(series.teams[index])
79
+ end
80
+
81
+ private
82
+
83
+ # rubocop:disable Metrics/AbcSize
84
+ sig { params(_path: String, _action: String, _args: T::Array[String]).returns(Cli) }
85
+ def run_help(_path, _action, _args)
86
+ io.puts('Usage:')
87
+ io.puts(" Help: #{script}")
88
+ io.puts(" Generate New Playoff: #{script} <path> new <team_ids>")
89
+ io.puts(" View Bracket: #{script} <path>")
90
+ io.puts(" View All Rounds: #{script} <path> rounds")
91
+ io.puts(" View Next Round/Series: #{script} <path> up")
92
+ io.puts(" Log Next Game Winner: #{script} <path> win <team_id>")
93
+ io.puts(" Random Simulation: #{script} <path> sim")
94
+ io.puts(" Winner: #{script} <path> winner")
95
+ io.puts
96
+ io.puts('Where:')
97
+ io.puts(' <path> is a path to a YAML file.')
98
+ io.puts(' <team_ids> is a list of 20 team IDs separated by a comma.')
99
+ io.puts
100
+ io.puts('Example(s):')
101
+ io.puts(" #{script} 2024.yaml new BOS,NYK,MIL,CLE,ORL,IND,PHI,MIA,CHI,ATL,OKC,DEN,MIN,LAC,DAL,PHX,LAL,NO,SAC,GS")
102
+ io.puts(" #{script} 2024.yaml")
103
+ io.puts(" #{script} 2024.yaml rounds")
104
+ io.puts(" #{script} 2024.yaml up")
105
+ io.puts(" #{script} 2024.yaml win MIA")
106
+ io.puts(" #{script} 2024.yaml sim")
107
+ io.puts(" #{script} 2024.yaml winner")
108
+
109
+ self
110
+ end
111
+ # rubocop:enable Metrics/AbcSize
112
+
113
+ sig { params(path: String, _action: String, args: T::Array[String]).returns(Cli) }
114
+ def run_new(path, _action, args)
115
+ ids = args[2].to_s.split(',')
116
+ teams = ids.map { |id| Team.new(id) }
117
+ eastern_teams = teams[0..9] || []
118
+ western_teams = teams[10..19] || []
119
+
120
+ tournament = Basketball.tournament_for(eastern_teams:, western_teams:)
121
+
122
+ save(path, tournament)
123
+
124
+ self
125
+ end
126
+
127
+ sig { params(path: String, _action: String, _args: T::Array[String]).returns(Cli) }
128
+ def run_bracket(path, _action, _args)
129
+ io.puts(load(path).print_bracket)
130
+
131
+ self
132
+ end
133
+
134
+ sig { params(path: String, _action: String, _args: T::Array[String]).returns(Cli) }
135
+ def run_rounds(path, _action, _args)
136
+ io.puts(load(path).print_rounds)
137
+
138
+ self
139
+ end
140
+
141
+ sig { params(path: String, _action: String, _args: T::Array[String]).returns(Cli) }
142
+ def run_up(path, _action, _args)
143
+ tournament = load(path)
144
+
145
+ series = tournament.up_next
146
+
147
+ io.puts(tournament.up_next) if series
148
+
149
+ self
150
+ end
151
+
152
+ sig { params(path: String, _action: String, args: T::Array[String]).returns(Cli) }
153
+ def run_win(path, _action, args)
154
+ id = args[2].to_s
155
+
156
+ team = Team.new(id)
157
+ tournament = load(path)
158
+ series = tournament.up_next
159
+
160
+ T.must(series).win(team)
161
+
162
+ io.puts(series)
163
+
164
+ save(path, tournament)
165
+
166
+ self
167
+ end
168
+
169
+ sig { params(path: String, _action: String, _args: T::Array[String]).returns(Cli) }
170
+ def run_sim(path, _action, _args)
171
+ tournament = load(path)
172
+
173
+ total = 0
174
+ # while not nil (and assign series)
175
+ while (series = tournament.up_next)
176
+ team = pick(series)
177
+
178
+ series.win(team)
179
+
180
+ total += 1
181
+ end
182
+
183
+ io.puts(total.to_s)
184
+
185
+ io.puts(tournament.winner.to_s)
186
+
187
+ save(path, tournament)
188
+
189
+ self
190
+ end
191
+
192
+ sig { params(path: String, _action: String, _args: T::Array[String]).returns(Cli) }
193
+ def run_winner(path, _action, _args)
194
+ tournament = load(path)
195
+
196
+ io.puts(tournament.winner) if tournament.over?
197
+
198
+ self
199
+ end
200
+ end
201
+ end
@@ -0,0 +1,8 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ module Playoffs
5
+ # A contestant is an object that feeds into a series which can either be a winner/loser
6
+ # of another series or a team.
7
+ Contestant = T.type_alias { T.any(Series, Team) }
8
+ end
@@ -0,0 +1,42 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ module Playoffs
5
+ # Represents a group of series that can be played at the same time.
6
+ class Round
7
+ extend T::Sig
8
+
9
+ sig { returns(T::Array[Series]) }
10
+ attr_reader :series
11
+
12
+ sig { params(series: T::Array[Series]).void }
13
+ def initialize(series)
14
+ @series = series
15
+ end
16
+
17
+ sig { returns(String) }
18
+ def to_s
19
+ series.map(&:to_s).join("\n")
20
+ end
21
+
22
+ sig { returns(T.nilable(Series)) }
23
+ def up_next
24
+ not_over.first
25
+ end
26
+
27
+ sig { returns(T::Boolean) }
28
+ def over?
29
+ not_over.empty?
30
+ end
31
+
32
+ sig { returns(T::Boolean) }
33
+ def not_over?
34
+ !over?
35
+ end
36
+
37
+ sig { returns(T::Array[Series]) }
38
+ def not_over
39
+ series.reject(&:over?).sort_by(&:games_played)
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,167 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ module Playoffs
5
+ # A node in a directed acyclic graph which represents a current or future series between two teams.
6
+ # The teams are able to be resolved later in the graph by connecting series together:
7
+ # a contestestant does not have to be a team, it can be the promise of a team via
8
+ # another series (as the winner or loser).
9
+ class Series
10
+ extend T::Sig
11
+
12
+ sig { returns(T::Array[Contestant]) }
13
+ attr_reader :contestants
14
+
15
+ sig { returns(T.nilable(Series)) }
16
+ attr_accessor :winner_advances_to
17
+
18
+ sig { returns(T.nilable(Series)) }
19
+ attr_accessor :loser_advances_to
20
+
21
+ sig { returns(BestOf) }
22
+ attr_reader :best_of
23
+
24
+ sig { returns(T::Array[Team]) }
25
+ attr_reader :winners_by_game
26
+
27
+ sig { returns(T.nilable(Team)) }
28
+ attr_reader :winner
29
+
30
+ sig { params(best_of: BestOf).void }
31
+ def initialize(best_of: BestOf.new)
32
+ @contestants = T.let([], T::Array[Contestant])
33
+ @best_of = best_of
34
+ @winners_by_game = T.let([], T::Array[Team])
35
+ end
36
+
37
+ sig { returns(T::Array[Team]) }
38
+ def teams
39
+ contestants.map do |contestant|
40
+ case contestant
41
+ when Team
42
+ contestant
43
+ when Series
44
+ contestant.winner_advances_to == self ? contestant.winner : contestant.loser
45
+ else
46
+ T.absurd(contestant)
47
+ end
48
+ end.compact
49
+ end
50
+
51
+ sig { returns(T.nilable(Team)) }
52
+ def loser
53
+ return unless winner
54
+
55
+ (teams - [winner]).first
56
+ end
57
+
58
+ sig { params(team: Team).returns(Series) }
59
+ def win(team)
60
+ raise NotEnoughTeamsError unless teams.length == 2
61
+
62
+ saved_team = teams.find { |t| t == team }
63
+
64
+ raise InvalidTeamError unless saved_team
65
+ raise SeriesOverError if over?
66
+
67
+ winners_by_game << saved_team
68
+
69
+ @winner = T.let(saved_team, T.nilable(Team)) if winner?(saved_team)
70
+
71
+ self
72
+ end
73
+
74
+ sig { returns(Integer) }
75
+ def games_played
76
+ winners_by_game.length
77
+ end
78
+
79
+ sig { returns(T::Boolean) }
80
+ def over?
81
+ !not_over?
82
+ end
83
+
84
+ sig { returns(T::Boolean) }
85
+ def not_over?
86
+ winner.nil?
87
+ end
88
+
89
+ sig { returns(T::Boolean) }
90
+ def valid?
91
+ return false if contestants.length != 2
92
+
93
+ contestants.each do |contestant|
94
+ case contestant
95
+ when Team
96
+ # Teams are always valid
97
+ when Series
98
+ # short-circuit because we cant be valid if the previous series (children) aren't.
99
+ return false unless contestant.valid?
100
+ else
101
+ T.absurd(contestant)
102
+ end
103
+ end
104
+
105
+ true
106
+ end
107
+
108
+ sig { params(team: Team).returns(Integer) }
109
+ def win_count_for(team)
110
+ winners_by_game.select { |winner| winner == team }.length
111
+ end
112
+
113
+ # rubocop:disable Metrics/AbcSize
114
+ sig { returns(String) }
115
+ def to_s
116
+ team_lines = teams.map do |contestant|
117
+ [contestant.to_s, win_count_for(contestant)]
118
+ end
119
+
120
+ team_lines += [['TBD', 0]] * (2 - team_lines.length)
121
+
122
+ [
123
+ valid? ? nil : '!INVALID!',
124
+ best_of.to_s,
125
+ self.class.to_s.split('::').last,
126
+ team_lines,
127
+ over? ? 'Done' : nil
128
+ ].compact.flatten.join('::')
129
+ end
130
+ # rubocop:enable Metrics/AbcSize
131
+
132
+ sig { params(series: Series).returns(Series) }
133
+ def winner_of(series)
134
+ series.winner_advances_to = self
135
+
136
+ add(series)
137
+ end
138
+
139
+ sig { params(series: Series).returns(Series) }
140
+ def loser_of(series)
141
+ series.loser_advances_to = self
142
+
143
+ add(series)
144
+ end
145
+
146
+ sig { params(team: Team).returns(Series) }
147
+ def register(team)
148
+ add(team)
149
+ end
150
+
151
+ private
152
+
153
+ sig { params(team: Team).returns(T::Boolean) }
154
+ def winner?(team)
155
+ winners_by_game.select { |w| w == team }.length == best_of.to_win
156
+ end
157
+
158
+ sig { params(contestant: Contestant).returns(Series) }
159
+ def add(contestant)
160
+ raise ArgumentError, 'No more than two contestants allowed.' if contestants.length >= 2
161
+
162
+ contestants << contestant
163
+
164
+ self
165
+ end
166
+ end
167
+ end
@@ -0,0 +1,7 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ module Playoffs
5
+ # Represents a team in the context of playoffs.
6
+ class Team < Primitive::Entity; end
7
+ end
@@ -0,0 +1,93 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ module Playoffs
5
+ # Print out a basic, tree-based, text hierarchy of all series.
6
+ module Bracketable
7
+ extend T::Helpers
8
+ extend T::Sig
9
+
10
+ abstract!
11
+
12
+ sig { abstract.returns(Series) }
13
+ def championship; end
14
+
15
+ sig { returns(String) }
16
+ def print_bracket
17
+ top_line =
18
+ if championship.valid? && championship.over?
19
+ championship.winner.to_s
20
+ elsif championship.valid?
21
+ 'TBD'
22
+ else
23
+ '!INVALID!'
24
+ end
25
+
26
+ ([top_line] + traverse_to_s(championship)).join("\n")
27
+ end
28
+
29
+ private
30
+
31
+ # rubocop:disable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
32
+ sig do
33
+ params(
34
+ series: Series,
35
+ next_series: T.nilable(Series),
36
+ depth: Integer,
37
+ draw_sibling_depths: T::Array[Integer]
38
+ ).returns(String)
39
+ end
40
+ def series_line(series, next_series = nil, depth = 0, draw_sibling_depths = [])
41
+ type =
42
+ if next_series && next_series == series.loser_advances_to
43
+ '::LoserAdvances'
44
+ elsif next_series && series.loser_advances_to
45
+ '::WinnerAdvances'
46
+ end
47
+
48
+ line = ''
49
+
50
+ (0...depth).each do |d|
51
+ line += (draw_sibling_depths.include?(d) ? '│ ' : ' ')
52
+ end
53
+
54
+ current_char =
55
+ if depth.positive? && draw_sibling_depths.include?(depth)
56
+ '├ '
57
+ else
58
+ '└ '
59
+ end
60
+
61
+ line + "#{current_char}#{series}#{type}"
62
+ end
63
+ # rubocop:enable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
64
+
65
+ # rubocop:disable Metrics/ParameterLists
66
+ sig do
67
+ params(
68
+ series: Series,
69
+ next_series: T.nilable(Series),
70
+ depth: Integer,
71
+ lines: T::Array[String],
72
+ draw_sibling_depths: T::Array[Integer]
73
+ ).returns(T::Array[String])
74
+ end
75
+ def traverse_to_s(series, next_series = nil, depth = 0, lines = [], draw_sibling_depths = [])
76
+ lines << series_line(series, next_series, depth, draw_sibling_depths)
77
+
78
+ series.contestants.each_with_index do |contestant, index|
79
+ new_siblings_depths =
80
+ if index < series.contestants.length - 1
81
+ draw_sibling_depths + [depth + 1]
82
+ else
83
+ draw_sibling_depths
84
+ end
85
+
86
+ traverse_to_s(contestant, series, depth + 1, lines, new_siblings_depths) if contestant.is_a?(Series)
87
+ end
88
+
89
+ lines
90
+ end
91
+ # rubocop:enable Metrics/ParameterLists
92
+ end
93
+ end