basketball 0.0.7 → 0.0.9
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 +4 -4
- data/.rubocop.yml +9 -19
- data/CHANGELOG.md +1 -31
- data/README.md +72 -91
- data/basketball.gemspec +3 -6
- data/exe/{basketball-schedule → basketball-coordinator} +1 -1
- data/exe/{basketball-draft → basketball-room} +1 -1
- data/lib/basketball/app/coordinator_cli.rb +243 -0
- data/lib/basketball/app/coordinator_repository.rb +191 -0
- data/lib/basketball/app/file_store.rb +22 -0
- data/lib/basketball/app/room_cli.rb +212 -0
- data/lib/basketball/app/room_repository.rb +189 -0
- data/lib/basketball/app.rb +12 -0
- data/lib/basketball/draft/assessment.rb +31 -0
- data/lib/basketball/{drafting → draft}/event.rb +4 -3
- data/lib/basketball/draft/front_office.rb +99 -0
- data/lib/basketball/draft/pick.rb +32 -0
- data/lib/basketball/draft/room.rb +221 -0
- data/lib/basketball/{drafting/player_search.rb → draft/scout.rb} +5 -10
- data/lib/basketball/draft/skip.rb +12 -0
- data/lib/basketball/draft.rb +16 -0
- data/lib/basketball/entity.rb +10 -4
- data/lib/basketball/org/league.rb +73 -0
- data/lib/basketball/org/player.rb +26 -0
- data/lib/basketball/{drafting → org}/position.rb +3 -2
- data/lib/basketball/org/team.rb +38 -0
- data/lib/basketball/org.rb +12 -0
- data/lib/basketball/season/arena.rb +112 -0
- data/lib/basketball/season/calendar.rb +90 -0
- data/lib/basketball/season/coordinator.rb +239 -0
- data/lib/basketball/{scheduling/preseason_game.rb → season/exhibition.rb} +3 -2
- data/lib/basketball/season/game.rb +37 -0
- data/lib/basketball/season/matchup.rb +27 -0
- data/lib/basketball/season/opponent.rb +15 -0
- data/lib/basketball/season/regular.rb +9 -0
- data/lib/basketball/season/result.rb +37 -0
- data/lib/basketball/season.rb +16 -0
- data/lib/basketball/value_object.rb +4 -1
- data/lib/basketball/version.rb +1 -1
- data/lib/basketball.rb +11 -6
- metadata +40 -52
- data/lib/basketball/drafting/cli.rb +0 -235
- data/lib/basketball/drafting/engine.rb +0 -221
- data/lib/basketball/drafting/engine_serializer.rb +0 -186
- data/lib/basketball/drafting/front_office.rb +0 -92
- data/lib/basketball/drafting/league.rb +0 -70
- data/lib/basketball/drafting/pick_event.rb +0 -25
- data/lib/basketball/drafting/player.rb +0 -43
- data/lib/basketball/drafting/roster.rb +0 -37
- data/lib/basketball/drafting/sim_event.rb +0 -23
- data/lib/basketball/drafting/skip_event.rb +0 -13
- data/lib/basketball/drafting.rb +0 -9
- data/lib/basketball/scheduling/calendar.rb +0 -121
- data/lib/basketball/scheduling/calendar_serializer.rb +0 -94
- data/lib/basketball/scheduling/cli.rb +0 -198
- data/lib/basketball/scheduling/conference.rb +0 -57
- data/lib/basketball/scheduling/coordinator.rb +0 -180
- data/lib/basketball/scheduling/division.rb +0 -43
- data/lib/basketball/scheduling/game.rb +0 -32
- data/lib/basketball/scheduling/league.rb +0 -114
- data/lib/basketball/scheduling/league_serializer.rb +0 -99
- data/lib/basketball/scheduling/season_game.rb +0 -8
- data/lib/basketball/scheduling/team.rb +0 -21
- data/lib/basketball/scheduling.rb +0 -17
@@ -0,0 +1,112 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Basketball
|
4
|
+
module Season
|
5
|
+
# A very, very, very basic starting point for a "semi-randomized" game simulator.
|
6
|
+
class Arena
|
7
|
+
RANDOM = :random
|
8
|
+
TOP_ONE = :top_one
|
9
|
+
TOP_TWO = :top_two
|
10
|
+
TOP_THREE = :top_three
|
11
|
+
TOP_SIX = :top_six
|
12
|
+
MAX_HOME_ADVANTAGE = 5
|
13
|
+
|
14
|
+
STRATEGY_FREQUENCIES = {
|
15
|
+
RANDOM => 10,
|
16
|
+
TOP_ONE => 5,
|
17
|
+
TOP_TWO => 10,
|
18
|
+
TOP_THREE => 20,
|
19
|
+
TOP_SIX => 30
|
20
|
+
}.freeze
|
21
|
+
|
22
|
+
private_constant :STRATEGY_FREQUENCIES,
|
23
|
+
:RANDOM,
|
24
|
+
:TOP_ONE,
|
25
|
+
:TOP_TWO,
|
26
|
+
:TOP_SIX,
|
27
|
+
:MAX_HOME_ADVANTAGE
|
28
|
+
|
29
|
+
def initialize
|
30
|
+
@lotto = STRATEGY_FREQUENCIES.inject([]) do |memo, (name, frequency)|
|
31
|
+
memo + ([name] * frequency)
|
32
|
+
end.shuffle
|
33
|
+
|
34
|
+
freeze
|
35
|
+
end
|
36
|
+
|
37
|
+
def play(matchup)
|
38
|
+
scores = generate_scores
|
39
|
+
winning_score = scores.max
|
40
|
+
losing_score = scores.min
|
41
|
+
strategy = pick_strategy
|
42
|
+
|
43
|
+
if home_wins?(matchup, strategy)
|
44
|
+
Result.new(
|
45
|
+
game: matchup.game,
|
46
|
+
home_score: winning_score,
|
47
|
+
away_score: losing_score
|
48
|
+
)
|
49
|
+
else
|
50
|
+
Result.new(
|
51
|
+
game: matchup.game,
|
52
|
+
home_score: losing_score,
|
53
|
+
away_score: winning_score
|
54
|
+
)
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
private
|
59
|
+
|
60
|
+
attr_reader :lotto
|
61
|
+
|
62
|
+
def pick_strategy
|
63
|
+
lotto.sample
|
64
|
+
end
|
65
|
+
|
66
|
+
def home_wins?(game, strategy)
|
67
|
+
send("#{strategy}_strategy", game)
|
68
|
+
end
|
69
|
+
|
70
|
+
def top_player_sum(players, amount)
|
71
|
+
players.sort_by(&:overall).reverse.take(amount).sum(&:overall)
|
72
|
+
end
|
73
|
+
|
74
|
+
def generate_scores
|
75
|
+
scores = [
|
76
|
+
rand(70..120),
|
77
|
+
rand(70..120)
|
78
|
+
]
|
79
|
+
|
80
|
+
# No ties
|
81
|
+
scores[0] += 1 if scores[0] == scores[1]
|
82
|
+
|
83
|
+
scores
|
84
|
+
end
|
85
|
+
|
86
|
+
def random_strategy(_game)
|
87
|
+
# 60% chance home wins
|
88
|
+
(([0] * 6) + ([1] * 4)).sample.zero?
|
89
|
+
end
|
90
|
+
|
91
|
+
def random_home_advantage
|
92
|
+
rand(0..MAX_HOME_ADVANTAGE)
|
93
|
+
end
|
94
|
+
|
95
|
+
def top_one_strategy(matchup)
|
96
|
+
top_player_sum(matchup.home_players, 1) + random_home_advantage >= top_player_sum(matchup.away_players, 1)
|
97
|
+
end
|
98
|
+
|
99
|
+
def top_two_strategy(matchup)
|
100
|
+
top_player_sum(matchup.home_players, 2) + random_home_advantage >= top_player_sum(matchup.away_players, 2)
|
101
|
+
end
|
102
|
+
|
103
|
+
def top_three_strategy(matchup)
|
104
|
+
top_player_sum(matchup.home_players, 3) + random_home_advantage >= top_player_sum(matchup.away_players, 3)
|
105
|
+
end
|
106
|
+
|
107
|
+
def top_six_strategy(matchup)
|
108
|
+
top_player_sum(matchup.home_players, 6) + random_home_advantage >= top_player_sum(matchup.away_players, 6)
|
109
|
+
end
|
110
|
+
end
|
111
|
+
end
|
112
|
+
end
|
@@ -0,0 +1,90 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Basketball
|
4
|
+
module Season
|
5
|
+
# Sets boundaries for preseason and regular season play. Add games as long as they are
|
6
|
+
# within the correct dated boundaries
|
7
|
+
class Calendar
|
8
|
+
class OutOfBoundsError < StandardError; end
|
9
|
+
class TeamAlreadyBookedError < StandardError; end
|
10
|
+
|
11
|
+
attr_reader :preseason_start_date,
|
12
|
+
:preseason_end_date,
|
13
|
+
:season_start_date,
|
14
|
+
:season_end_date,
|
15
|
+
:games
|
16
|
+
|
17
|
+
def initialize(
|
18
|
+
preseason_start_date:,
|
19
|
+
preseason_end_date:,
|
20
|
+
season_start_date:,
|
21
|
+
season_end_date:,
|
22
|
+
games: []
|
23
|
+
)
|
24
|
+
raise ArgumentError, 'preseason_start_date is required' if preseason_start_date.to_s.empty?
|
25
|
+
raise ArgumentError, 'preseason_end_date is required' if preseason_end_date.to_s.empty?
|
26
|
+
raise ArgumentError, 'season_start_date is required' if season_start_date.to_s.empty?
|
27
|
+
raise ArgumentError, 'season_end_date is required' if season_end_date.to_s.empty?
|
28
|
+
|
29
|
+
@preseason_start_date = preseason_start_date
|
30
|
+
@preseason_end_date = preseason_end_date
|
31
|
+
@season_start_date = season_start_date
|
32
|
+
@season_end_date = season_end_date
|
33
|
+
@games = []
|
34
|
+
|
35
|
+
games.each { |game| add!(game) }
|
36
|
+
|
37
|
+
freeze
|
38
|
+
end
|
39
|
+
|
40
|
+
def add!(game)
|
41
|
+
assert_in_bounds(game)
|
42
|
+
assert_free_date(game)
|
43
|
+
|
44
|
+
@games << game
|
45
|
+
|
46
|
+
self
|
47
|
+
end
|
48
|
+
|
49
|
+
def exhibitions_for(date: nil, opponent: nil)
|
50
|
+
games_for(date:, opponent:).select { |game| game.is_a?(Exhibition) }
|
51
|
+
end
|
52
|
+
|
53
|
+
def regulars_for(date: nil, opponent: nil)
|
54
|
+
games_for(date:, opponent:).select { |game| game.is_a?(Regular) }
|
55
|
+
end
|
56
|
+
|
57
|
+
def games_for(date: nil, opponent: nil)
|
58
|
+
games.select do |game|
|
59
|
+
(date.nil? || game.date == date) && (opponent.nil? || game.for?(opponent))
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
private
|
64
|
+
|
65
|
+
def assert_free_date(game)
|
66
|
+
if games_for(date: game.date, opponent: game.home_opponent).any?
|
67
|
+
raise TeamAlreadyBookedError, "#{game.home_opponent} already playing on #{game.date}"
|
68
|
+
end
|
69
|
+
|
70
|
+
return unless games_for(date: game.date, opponent: game.away_opponent).any?
|
71
|
+
|
72
|
+
raise TeamAlreadyBookedError, "#{game.away_opponent} already playing on #{game.date}"
|
73
|
+
end
|
74
|
+
|
75
|
+
def assert_in_bounds(game)
|
76
|
+
date = game.date
|
77
|
+
|
78
|
+
if game.is_a?(Exhibition)
|
79
|
+
raise OutOfBoundsError, "#{date} is before preseason begins" if date < preseason_start_date
|
80
|
+
raise OutOfBoundsError, "#{date} is after preseason ends" if date > preseason_end_date
|
81
|
+
elsif game.is_a?(Regular)
|
82
|
+
raise OutOfBoundsError, "#{date} is before season begins" if date < season_start_date
|
83
|
+
raise OutOfBoundsError, "#{date} is after season ends" if date > season_end_date
|
84
|
+
else
|
85
|
+
raise ArgumentError, "Dont know what this game type is: #{game.class.name}"
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
@@ -0,0 +1,239 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Basketball
|
4
|
+
module Season
|
5
|
+
# Main iterator-based object that knows how to manage a calendar and simulate games per day.
|
6
|
+
class Coordinator
|
7
|
+
extend Forwardable
|
8
|
+
|
9
|
+
class AlreadyPlayedGameError < StandardError; end
|
10
|
+
class GameNotCurrentError < StandardError; end
|
11
|
+
class OutOfBoundsError < StandardError; end
|
12
|
+
class PlayedGamesError < StandardError; end
|
13
|
+
class UnknownGameError < StandardError; end
|
14
|
+
class UnknownTeamError < StandardError; end
|
15
|
+
class UnplayedGamesError < StandardError; end
|
16
|
+
|
17
|
+
attr_reader :calendar,
|
18
|
+
:current_date,
|
19
|
+
:arena,
|
20
|
+
:results,
|
21
|
+
:league,
|
22
|
+
:id
|
23
|
+
|
24
|
+
def_delegators :calendar,
|
25
|
+
:preseason_start_date,
|
26
|
+
:preseason_end_date,
|
27
|
+
:season_start_date,
|
28
|
+
:season_end_date,
|
29
|
+
:games,
|
30
|
+
:exhibitions_for,
|
31
|
+
:regulars_for,
|
32
|
+
:games_for
|
33
|
+
|
34
|
+
def initialize(
|
35
|
+
calendar:,
|
36
|
+
current_date:,
|
37
|
+
results: [],
|
38
|
+
league: Org::League.new
|
39
|
+
)
|
40
|
+
super()
|
41
|
+
|
42
|
+
raise ArgumentError, 'calendar is required' unless calendar
|
43
|
+
raise ArgumentError, 'current_date is required' if current_date.to_s.empty?
|
44
|
+
raise ArgumentError, 'league is required' unless league
|
45
|
+
|
46
|
+
@calendar = calendar
|
47
|
+
@current_date = current_date
|
48
|
+
@arena = Arena.new
|
49
|
+
@results = []
|
50
|
+
@league = league
|
51
|
+
|
52
|
+
results.each { |result| replay!(result) }
|
53
|
+
|
54
|
+
assert_current_date
|
55
|
+
assert_all_past_dates_are_played
|
56
|
+
assert_all_future_dates_arent_played
|
57
|
+
assert_all_known_teams
|
58
|
+
end
|
59
|
+
|
60
|
+
def sim_rest!(&)
|
61
|
+
events = []
|
62
|
+
|
63
|
+
while not_done?
|
64
|
+
new_events = sim!(&)
|
65
|
+
|
66
|
+
events += new_events
|
67
|
+
end
|
68
|
+
|
69
|
+
events
|
70
|
+
end
|
71
|
+
|
72
|
+
def assert_current_date
|
73
|
+
if current_date < preseason_start_date
|
74
|
+
raise OutOfBoundsError, "current date #{current_date} should be on or after #{preseason_start_date}"
|
75
|
+
end
|
76
|
+
|
77
|
+
return unless current_date > season_end_date
|
78
|
+
|
79
|
+
raise OutOfBoundsError, "current date #{current_date} should be on or after #{preseason_start_date}"
|
80
|
+
end
|
81
|
+
|
82
|
+
def sim!
|
83
|
+
return [] if done?
|
84
|
+
|
85
|
+
events = []
|
86
|
+
games = games_for(date: current_date)
|
87
|
+
|
88
|
+
games.each do |game|
|
89
|
+
home_players = opponent_team(game.home_opponent).players
|
90
|
+
away_players = opponent_team(game.away_opponent).players
|
91
|
+
matchup = Matchup.new(game:, home_players:, away_players:)
|
92
|
+
event = arena.play(matchup)
|
93
|
+
|
94
|
+
play!(event)
|
95
|
+
|
96
|
+
yield(event) if block_given?
|
97
|
+
|
98
|
+
events << event
|
99
|
+
end
|
100
|
+
|
101
|
+
increment_current_date!
|
102
|
+
|
103
|
+
events
|
104
|
+
end
|
105
|
+
|
106
|
+
def total_days
|
107
|
+
(season_end_date - preseason_start_date).to_i
|
108
|
+
end
|
109
|
+
|
110
|
+
def days_left
|
111
|
+
(season_end_date - current_date).to_i
|
112
|
+
end
|
113
|
+
|
114
|
+
def total_exhibitions
|
115
|
+
exhibitions_for.length
|
116
|
+
end
|
117
|
+
|
118
|
+
def exhibitions_left
|
119
|
+
total_exhibitions - exhibition_result_events.length
|
120
|
+
end
|
121
|
+
|
122
|
+
def total_regulars
|
123
|
+
regulars_for.length
|
124
|
+
end
|
125
|
+
|
126
|
+
def regulars_left
|
127
|
+
total_regulars - regular_result_events.length
|
128
|
+
end
|
129
|
+
|
130
|
+
def current_games
|
131
|
+
games_for(date: current_date) - results.map(&:game)
|
132
|
+
end
|
133
|
+
|
134
|
+
def done?
|
135
|
+
current_date == season_end_date && games.length == results.length
|
136
|
+
end
|
137
|
+
|
138
|
+
def not_done?
|
139
|
+
!done?
|
140
|
+
end
|
141
|
+
|
142
|
+
def add!(game)
|
143
|
+
assert_today_or_in_future(game)
|
144
|
+
assert_known_teams(game)
|
145
|
+
|
146
|
+
calendar.add!(game)
|
147
|
+
|
148
|
+
self
|
149
|
+
end
|
150
|
+
|
151
|
+
def result_for(game)
|
152
|
+
results.find do |result|
|
153
|
+
result.game == game
|
154
|
+
end
|
155
|
+
end
|
156
|
+
|
157
|
+
private
|
158
|
+
|
159
|
+
attr_writer :id, :arena
|
160
|
+
|
161
|
+
def opponent_team(opponent)
|
162
|
+
league.teams.find { |t| t == opponent }
|
163
|
+
end
|
164
|
+
|
165
|
+
def exhibition_result_events
|
166
|
+
results.select { |e| e.game.is_a?(Exhibition) }
|
167
|
+
end
|
168
|
+
|
169
|
+
def regular_result_events
|
170
|
+
results.select { |e| e.game.is_a?(Regular) }
|
171
|
+
end
|
172
|
+
|
173
|
+
def increment_current_date!
|
174
|
+
return self if current_date >= season_end_date
|
175
|
+
|
176
|
+
@current_date = current_date + 1
|
177
|
+
|
178
|
+
self
|
179
|
+
end
|
180
|
+
|
181
|
+
def assert_today_or_in_future(game)
|
182
|
+
return unless game.date <= current_date
|
183
|
+
|
184
|
+
raise OutOfBoundsError, "#{game.date} is on or before the current date (#{current_date})"
|
185
|
+
end
|
186
|
+
|
187
|
+
def assert_known_teams(game)
|
188
|
+
raise UnknownTeamError, "unknown opponent: #{game.home_opponent}" if league.not_registered?(game.home_opponent)
|
189
|
+
|
190
|
+
return unless league.not_registered?(game.away_opponent)
|
191
|
+
|
192
|
+
raise UnknownTeamError, "unknown opponent: #{game.away_opponent}"
|
193
|
+
end
|
194
|
+
|
195
|
+
def assert_all_past_dates_are_played
|
196
|
+
games_that_should_be_played = games.select { |game| game.date < current_date }
|
197
|
+
|
198
|
+
games_played = results.map(&:game)
|
199
|
+
unplayed_games = games_that_should_be_played - games_played
|
200
|
+
|
201
|
+
return if unplayed_games.empty?
|
202
|
+
|
203
|
+
raise UnplayedGamesError, "#{unplayed_games.length} game(s) not played before #{current_date}"
|
204
|
+
end
|
205
|
+
|
206
|
+
def assert_all_future_dates_arent_played
|
207
|
+
games_that_shouldnt_be_played = results.select do |result|
|
208
|
+
result.date > current_date
|
209
|
+
end
|
210
|
+
|
211
|
+
count = games_that_shouldnt_be_played.length
|
212
|
+
|
213
|
+
return unless games_that_shouldnt_be_played.any?
|
214
|
+
|
215
|
+
raise PlayedGamesError, "#{count} game(s) played after #{current_date}"
|
216
|
+
end
|
217
|
+
|
218
|
+
def play!(result)
|
219
|
+
raise GameNotCurrentError, "#{result} is not for #{current_date}" if result.date != current_date
|
220
|
+
|
221
|
+
replay!(result)
|
222
|
+
end
|
223
|
+
|
224
|
+
def replay!(result)
|
225
|
+
raise AlreadyPlayedGameError, "#{result.game} already played!" if result_for(result.game)
|
226
|
+
|
227
|
+
raise UnknownGameError, "game not added: #{result.game}" unless games.include?(result.game)
|
228
|
+
|
229
|
+
results << result
|
230
|
+
|
231
|
+
result
|
232
|
+
end
|
233
|
+
|
234
|
+
def assert_all_known_teams
|
235
|
+
calendar.games.each { |game| assert_known_teams(game) }
|
236
|
+
end
|
237
|
+
end
|
238
|
+
end
|
239
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Basketball
|
4
|
+
module Season
|
5
|
+
# Base class describing what all games have in common.
|
6
|
+
class Game < ValueObject
|
7
|
+
value_reader :date, :home_opponent, :away_opponent
|
8
|
+
|
9
|
+
def initialize(date:, home_opponent:, away_opponent:)
|
10
|
+
super()
|
11
|
+
|
12
|
+
raise ArgumentError, 'date is required' unless date
|
13
|
+
raise ArgumentError, 'home_opponent is required' unless home_opponent
|
14
|
+
raise ArgumentError, 'away_opponent is required' unless away_opponent
|
15
|
+
raise ArgumentError, 'teams cannot play themselves' if home_opponent == away_opponent
|
16
|
+
|
17
|
+
@date = date
|
18
|
+
@home_opponent = home_opponent
|
19
|
+
@away_opponent = away_opponent
|
20
|
+
|
21
|
+
freeze
|
22
|
+
end
|
23
|
+
|
24
|
+
def for?(team)
|
25
|
+
teams.include?(team)
|
26
|
+
end
|
27
|
+
|
28
|
+
def teams
|
29
|
+
[home_opponent, away_opponent]
|
30
|
+
end
|
31
|
+
|
32
|
+
def to_s
|
33
|
+
"#{date} - #{away_opponent} at #{home_opponent}"
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Basketball
|
4
|
+
module Season
|
5
|
+
# A Matchup is a late materialization of a game. While a game is a skeleton for a future
|
6
|
+
# matchup, it does not materialize until game time (rosters could change right up until game time).
|
7
|
+
class Matchup
|
8
|
+
class PlayersOnBothTeamsError < StandardError; end
|
9
|
+
|
10
|
+
attr_reader :game, :home_players, :away_players
|
11
|
+
|
12
|
+
def initialize(game:, home_players: [], away_players: [])
|
13
|
+
raise ArgumentError, 'game is required' unless game
|
14
|
+
|
15
|
+
@game = game
|
16
|
+
@home_players = home_players.uniq
|
17
|
+
@away_players = away_players.uniq
|
18
|
+
|
19
|
+
if home_players.intersect?(away_players)
|
20
|
+
raise PlayersOnBothTeamsError, 'players cannot be on both home and away team'
|
21
|
+
end
|
22
|
+
|
23
|
+
freeze
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Basketball
|
4
|
+
module Season
|
5
|
+
# Represents a team without a roster. Equal to a team by identity.
|
6
|
+
# A team's roster will not be known until the last minute (when it is game time).
|
7
|
+
class Opponent < Entity
|
8
|
+
def initialize(id:)
|
9
|
+
super(id)
|
10
|
+
|
11
|
+
freeze
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Basketball
|
4
|
+
module Season
|
5
|
+
# Base class describing the end result of a game. This should/could be sub-classed to include
|
6
|
+
# more sport-specific information.
|
7
|
+
class Result
|
8
|
+
extend Forwardable
|
9
|
+
|
10
|
+
class CannotTieError < StandardError; end
|
11
|
+
|
12
|
+
attr_reader :game, :home_score, :away_score
|
13
|
+
|
14
|
+
def_delegators :game, :date, :home_opponent, :away_opponent, :teams
|
15
|
+
|
16
|
+
def initialize(game:, home_score:, away_score:)
|
17
|
+
raise ArgumentError, 'game is required' unless game
|
18
|
+
raise CannotTieError, "#{game} ended in a tie" if home_score == away_score
|
19
|
+
|
20
|
+
@game = game
|
21
|
+
@home_score = home_score.to_i
|
22
|
+
@away_score = away_score.to_i
|
23
|
+
end
|
24
|
+
|
25
|
+
def to_s
|
26
|
+
"#{game.date} - #{away_opponent} (#{away_score}) at #{home_opponent} (#{home_score})"
|
27
|
+
end
|
28
|
+
|
29
|
+
def ==(other)
|
30
|
+
game == other.game &&
|
31
|
+
home_score == other.home_score &&
|
32
|
+
away_score == other.away_score
|
33
|
+
end
|
34
|
+
alias eql? ==
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# Common
|
4
|
+
require_relative 'season/arena'
|
5
|
+
require_relative 'season/calendar'
|
6
|
+
require_relative 'season/result'
|
7
|
+
require_relative 'season/game'
|
8
|
+
require_relative 'season/matchup'
|
9
|
+
require_relative 'season/opponent'
|
10
|
+
|
11
|
+
# Game Subclasses
|
12
|
+
require_relative 'season/exhibition'
|
13
|
+
require_relative 'season/regular'
|
14
|
+
|
15
|
+
# Specific
|
16
|
+
require_relative 'season/coordinator'
|
@@ -1,6 +1,9 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
module Basketball
|
4
|
+
# A Value Object is something that has no specific identity, instead its identity is the sum of
|
5
|
+
# the attribute values. Changing one will change the entire object's identity.
|
6
|
+
# Comes with a very simple DSL for specifying properties along with base equality and sorting methods.
|
4
7
|
class ValueObject
|
5
8
|
include Comparable
|
6
9
|
|
@@ -23,7 +26,7 @@ module Basketball
|
|
23
26
|
@value_keys ||= []
|
24
27
|
end
|
25
28
|
|
26
|
-
def
|
29
|
+
def value_reader(*keys)
|
27
30
|
keys.each { |k| value_keys << k.to_sym }
|
28
31
|
|
29
32
|
attr_reader(*keys)
|
data/lib/basketball/version.rb
CHANGED
data/lib/basketball.rb
CHANGED
@@ -1,16 +1,21 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require '
|
3
|
+
require 'date'
|
4
4
|
require 'fileutils'
|
5
5
|
require 'forwardable'
|
6
6
|
require 'json'
|
7
|
-
require 'securerandom'
|
8
7
|
require 'slop'
|
9
8
|
|
10
|
-
#
|
9
|
+
# Generic
|
11
10
|
require_relative 'basketball/entity'
|
12
11
|
require_relative 'basketball/value_object'
|
13
12
|
|
14
|
-
#
|
15
|
-
require_relative 'basketball/
|
16
|
-
|
13
|
+
# Dependent on Generic
|
14
|
+
require_relative 'basketball/org'
|
15
|
+
|
16
|
+
# Dependent on Org
|
17
|
+
require_relative 'basketball/draft'
|
18
|
+
require_relative 'basketball/season'
|
19
|
+
|
20
|
+
# Dependent on All
|
21
|
+
require_relative 'basketball/app'
|