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.
- checksums.yaml +7 -0
- data/.bundle/config +2 -0
- data/.editorconfig +8 -0
- data/.github/workflows/ci.yaml +29 -0
- data/.gitignore +8 -0
- data/.rubocop.yml +34 -0
- data/.tool-versions +1 -0
- data/.vscode/settings.json +5 -0
- data/CHANGELOG.md +3 -0
- data/CODE_OF_CONDUCT.md +73 -0
- data/Gemfile +5 -0
- data/Guardfile +17 -0
- data/LICENSE +5 -0
- data/README.md +245 -0
- data/Rakefile +11 -0
- data/bin/_guard-core +27 -0
- data/bin/bundle +109 -0
- data/bin/bundle-audit +27 -0
- data/bin/bundler-audit +27 -0
- data/bin/coderay +27 -0
- data/bin/console +11 -0
- data/bin/guard +27 -0
- data/bin/htmldiff +27 -0
- data/bin/ldiff +27 -0
- data/bin/listen +27 -0
- data/bin/playoffs +27 -0
- data/bin/pry +27 -0
- data/bin/racc +27 -0
- data/bin/rake +27 -0
- data/bin/rspec +27 -0
- data/bin/rubocop +27 -0
- data/bin/ruby-parse +27 -0
- data/bin/ruby-rewrite +27 -0
- data/bin/spoom +27 -0
- data/bin/srb +27 -0
- data/bin/srb-rbi +27 -0
- data/bin/tapioca +27 -0
- data/bin/thor +27 -0
- data/bin/yard +27 -0
- data/bin/yardoc +27 -0
- data/bin/yri +27 -0
- data/exe/playoffs +7 -0
- data/lib/playoffs/basketball.rb +120 -0
- data/lib/playoffs/best_of.rb +38 -0
- data/lib/playoffs/cli.rb +201 -0
- data/lib/playoffs/contestant.rb +8 -0
- data/lib/playoffs/round.rb +42 -0
- data/lib/playoffs/series.rb +167 -0
- data/lib/playoffs/team.rb +7 -0
- data/lib/playoffs/tournament/bracketable.rb +93 -0
- data/lib/playoffs/tournament/roundable.rb +53 -0
- data/lib/playoffs/tournament.rb +61 -0
- data/lib/playoffs/version.rb +6 -0
- data/lib/playoffs.rb +26 -0
- data/playoffs.gemspec +49 -0
- data/sorbet/config +4 -0
- data/sorbet/rbi/annotations/.gitattributes +1 -0
- data/sorbet/rbi/annotations/rainbow.rbi +269 -0
- data/sorbet/rbi/gems/.gitattributes +1 -0
- data/sorbet/rbi/gems/ansi@1.5.0.rbi +688 -0
- data/sorbet/rbi/gems/ast@2.4.2.rbi +585 -0
- data/sorbet/rbi/gems/bundler-audit@0.9.1.rbi +309 -0
- data/sorbet/rbi/gems/coderay@1.1.3.rbi +3426 -0
- data/sorbet/rbi/gems/diff-lcs@1.5.1.rbi +1131 -0
- data/sorbet/rbi/gems/docile@1.4.0.rbi +377 -0
- data/sorbet/rbi/gems/erubi@1.12.0.rbi +145 -0
- data/sorbet/rbi/gems/ffi@1.16.3.rbi +9 -0
- data/sorbet/rbi/gems/formatador@1.1.0.rbi +9 -0
- data/sorbet/rbi/gems/guard-compat@1.2.1.rbi +67 -0
- data/sorbet/rbi/gems/guard-rspec@4.7.3.rbi +563 -0
- data/sorbet/rbi/gems/guard@2.18.1.rbi +9 -0
- data/sorbet/rbi/gems/json@2.7.2.rbi +1562 -0
- data/sorbet/rbi/gems/language_server-protocol@3.17.0.3.rbi +14238 -0
- data/sorbet/rbi/gems/listen@3.9.0.rbi +9 -0
- data/sorbet/rbi/gems/lumberjack@1.2.10.rbi +9 -0
- data/sorbet/rbi/gems/method_source@1.1.0.rbi +304 -0
- data/sorbet/rbi/gems/nenv@0.3.0.rbi +9 -0
- data/sorbet/rbi/gems/netrc@0.11.0.rbi +158 -0
- data/sorbet/rbi/gems/notiffany@0.1.3.rbi +9 -0
- data/sorbet/rbi/gems/parallel@1.24.0.rbi +280 -0
- data/sorbet/rbi/gems/parser@3.3.1.0.rbi +7238 -0
- data/sorbet/rbi/gems/primitive@1.0.0.rbi +58 -0
- data/sorbet/rbi/gems/prism@0.29.0.rbi +37987 -0
- data/sorbet/rbi/gems/pry@0.14.2.rbi +10069 -0
- data/sorbet/rbi/gems/racc@1.7.3.rbi +162 -0
- data/sorbet/rbi/gems/rainbow@3.1.1.rbi +403 -0
- data/sorbet/rbi/gems/rake@13.2.1.rbi +3028 -0
- data/sorbet/rbi/gems/rb-fsevent@0.11.2.rbi +9 -0
- data/sorbet/rbi/gems/rb-inotify@0.10.1.rbi +9 -0
- data/sorbet/rbi/gems/rbi@0.1.13.rbi +3078 -0
- data/sorbet/rbi/gems/regexp_parser@2.9.2.rbi +3772 -0
- data/sorbet/rbi/gems/rexml@3.2.8.rbi +4794 -0
- data/sorbet/rbi/gems/rspec-core@3.13.0.rbi +10874 -0
- data/sorbet/rbi/gems/rspec-expectations@3.13.0.rbi +8154 -0
- data/sorbet/rbi/gems/rspec-mocks@3.13.1.rbi +5341 -0
- data/sorbet/rbi/gems/rspec-support@3.13.1.rbi +1630 -0
- data/sorbet/rbi/gems/rspec@3.13.0.rbi +83 -0
- data/sorbet/rbi/gems/rubocop-ast@1.31.3.rbi +7159 -0
- data/sorbet/rbi/gems/rubocop-capybara@2.20.0.rbi +1208 -0
- data/sorbet/rbi/gems/rubocop-factory_bot@2.25.1.rbi +928 -0
- data/sorbet/rbi/gems/rubocop-rake@0.6.0.rbi +329 -0
- data/sorbet/rbi/gems/rubocop-rspec@2.29.2.rbi +8247 -0
- data/sorbet/rbi/gems/rubocop-rspec_rails@2.28.3.rbi +911 -0
- data/sorbet/rbi/gems/rubocop-sorbet@0.8.3.rbi +1607 -0
- data/sorbet/rbi/gems/rubocop@1.63.5.rbi +57788 -0
- data/sorbet/rbi/gems/ruby-progressbar@1.13.0.rbi +1318 -0
- data/sorbet/rbi/gems/shellany@0.0.1.rbi +9 -0
- data/sorbet/rbi/gems/simplecov-console@0.9.1.rbi +103 -0
- data/sorbet/rbi/gems/simplecov-html@0.12.3.rbi +217 -0
- data/sorbet/rbi/gems/simplecov@0.22.0.rbi +2149 -0
- data/sorbet/rbi/gems/simplecov_json_formatter@0.1.4.rbi +9 -0
- data/sorbet/rbi/gems/sorbet-runtime-stub@0.2.0.rbi +8 -0
- data/sorbet/rbi/gems/spoom@1.3.2.rbi +4420 -0
- data/sorbet/rbi/gems/strscan@3.1.0.rbi +9 -0
- data/sorbet/rbi/gems/tapioca@0.14.2.rbi +3539 -0
- data/sorbet/rbi/gems/terminal-table@3.0.2.rbi +9 -0
- data/sorbet/rbi/gems/thor@1.3.1.rbi +4318 -0
- data/sorbet/rbi/gems/unicode-display_width@2.5.0.rbi +66 -0
- data/sorbet/rbi/gems/yard-sorbet@0.8.1.rbi +428 -0
- data/sorbet/rbi/gems/yard@0.9.36.rbi +18085 -0
- data/sorbet/tapioca/config.yml +13 -0
- data/sorbet/tapioca/require.rb +4 -0
- 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
|
data/lib/playoffs/cli.rb
ADDED
|
@@ -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,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,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
|