basketball 0.0.8 → 0.0.10

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 (59) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +9 -19
  3. data/CHANGELOG.md +1 -39
  4. data/README.md +75 -93
  5. data/basketball.gemspec +3 -6
  6. data/exe/{basketball-season-scheduling → basketball-coordinator} +1 -1
  7. data/exe/{basketball-draft → basketball-room} +1 -1
  8. data/lib/basketball/app/coordinator_cli.rb +250 -0
  9. data/lib/basketball/app/coordinator_repository.rb +114 -0
  10. data/lib/basketball/app/document_repository.rb +67 -0
  11. data/lib/basketball/app/file_store.rb +38 -0
  12. data/lib/basketball/app/in_memory_store.rb +42 -0
  13. data/lib/basketball/app/league_repository.rb +20 -0
  14. data/lib/basketball/app/league_serializable.rb +54 -0
  15. data/lib/basketball/{draft/cli.rb → app/room_cli.rb} +74 -80
  16. data/lib/basketball/app/room_repository.rb +149 -0
  17. data/lib/basketball/app.rb +20 -0
  18. data/lib/basketball/draft/assessment.rb +31 -0
  19. data/lib/basketball/draft/event.rb +3 -2
  20. data/lib/basketball/draft/front_office.rb +35 -28
  21. data/lib/basketball/draft/{pick_event.rb → pick.rb} +13 -6
  22. data/lib/basketball/draft/room.rb +119 -119
  23. data/lib/basketball/draft/{player_search.rb → scout.rb} +4 -9
  24. data/lib/basketball/draft/skip.rb +12 -0
  25. data/lib/basketball/draft.rb +13 -6
  26. data/lib/basketball/entity.rb +19 -10
  27. data/lib/basketball/org/league.rb +68 -0
  28. data/lib/basketball/org/player.rb +26 -0
  29. data/lib/basketball/{draft → org}/position.rb +3 -2
  30. data/lib/basketball/org/team.rb +38 -0
  31. data/lib/basketball/org.rb +12 -0
  32. data/lib/basketball/season/arena.rb +113 -0
  33. data/lib/basketball/season/calendar.rb +41 -72
  34. data/lib/basketball/season/coordinator.rb +186 -128
  35. data/lib/basketball/season/{preseason_game.rb → exhibition.rb} +2 -1
  36. data/lib/basketball/season/game.rb +15 -10
  37. data/lib/basketball/season/matchup.rb +27 -0
  38. data/lib/basketball/season/opponent.rb +15 -0
  39. data/lib/basketball/season/{season_game.rb → regular.rb} +2 -1
  40. data/lib/basketball/season/result.rb +37 -0
  41. data/lib/basketball/season.rb +12 -13
  42. data/lib/basketball/value_object.rb +8 -27
  43. data/lib/basketball/value_object_dsl.rb +30 -0
  44. data/lib/basketball/version.rb +1 -1
  45. data/lib/basketball.rb +9 -4
  46. metadata +37 -44
  47. data/lib/basketball/draft/league.rb +0 -70
  48. data/lib/basketball/draft/player.rb +0 -43
  49. data/lib/basketball/draft/room_serializer.rb +0 -186
  50. data/lib/basketball/draft/roster.rb +0 -37
  51. data/lib/basketball/draft/sim_event.rb +0 -23
  52. data/lib/basketball/draft/skip_event.rb +0 -13
  53. data/lib/basketball/season/calendar_serializer.rb +0 -94
  54. data/lib/basketball/season/conference.rb +0 -57
  55. data/lib/basketball/season/division.rb +0 -43
  56. data/lib/basketball/season/league.rb +0 -114
  57. data/lib/basketball/season/league_serializer.rb +0 -99
  58. data/lib/basketball/season/scheduling_cli.rb +0 -198
  59. data/lib/basketball/season/team.rb +0 -21
@@ -0,0 +1,113 @@
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
+ DEFAULT_MAX_HOME_ADVANTAGE = 5
13
+
14
+ DEFAULT_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
+ attr_reader :lotto, :max_home_advantage
23
+
24
+ def initialize(
25
+ strategy_frquencies: DEFAULT_STRATEGY_FREQUENCIES,
26
+ max_home_advantage: DEFAULT_MAX_HOME_ADVANTAGE
27
+ )
28
+ @max_home_advantage = max_home_advantage
29
+ @lotto = make_lotto(strategy_frquencies)
30
+
31
+ freeze
32
+ end
33
+
34
+ def play(matchup)
35
+ scores = generate_scores
36
+ winning_score = scores.max
37
+ losing_score = scores.min
38
+ strategy = pick_strategy
39
+
40
+ if home_wins?(matchup, strategy)
41
+ Result.new(
42
+ game: matchup.game,
43
+ home_score: winning_score,
44
+ away_score: losing_score
45
+ )
46
+ else
47
+ Result.new(
48
+ game: matchup.game,
49
+ home_score: losing_score,
50
+ away_score: winning_score
51
+ )
52
+ end
53
+ end
54
+
55
+ private
56
+
57
+ def make_lotto(strategy_frquencies)
58
+ strategy_frquencies.inject([]) do |memo, (name, frequency)|
59
+ memo + ([name] * frequency)
60
+ end.shuffle
61
+ end
62
+
63
+ def pick_strategy
64
+ lotto.sample
65
+ end
66
+
67
+ def home_wins?(game, strategy)
68
+ send("#{strategy}_strategy", game)
69
+ end
70
+
71
+ def top_player_sum(players, amount)
72
+ players.sort_by(&:overall).reverse.take(amount).sum(&:overall)
73
+ end
74
+
75
+ def generate_scores
76
+ scores = [
77
+ rand(70..120),
78
+ rand(70..120)
79
+ ]
80
+
81
+ # No ties
82
+ scores[0] += 1 if scores[0] == scores[1]
83
+
84
+ scores
85
+ end
86
+
87
+ def random_strategy(_game)
88
+ # 60% chance home wins
89
+ (([0] * 6) + ([1] * 4)).sample.zero?
90
+ end
91
+
92
+ def random_home_advantage
93
+ rand(0..max_home_advantage)
94
+ end
95
+
96
+ def top_one_strategy(matchup)
97
+ top_player_sum(matchup.home_players, 1) + random_home_advantage >= top_player_sum(matchup.away_players, 1)
98
+ end
99
+
100
+ def top_two_strategy(matchup)
101
+ top_player_sum(matchup.home_players, 2) + random_home_advantage >= top_player_sum(matchup.away_players, 2)
102
+ end
103
+
104
+ def top_three_strategy(matchup)
105
+ top_player_sum(matchup.home_players, 3) + random_home_advantage >= top_player_sum(matchup.away_players, 3)
106
+ end
107
+
108
+ def top_six_strategy(matchup)
109
+ top_player_sum(matchup.home_players, 6) + random_home_advantage >= top_player_sum(matchup.away_players, 6)
110
+ end
111
+ end
112
+ end
113
+ end
@@ -2,28 +2,34 @@
2
2
 
3
3
  module Basketball
4
4
  module Season
5
- class Calendar < ValueObject
6
- class TeamAlreadyBookedError < StandardError; end
7
- class InvalidGameOrderError < StandardError; end
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
8
  class OutOfBoundsError < StandardError; end
9
+ class TeamAlreadyBookedError < StandardError; end
9
10
 
10
11
  attr_reader :preseason_start_date,
11
12
  :preseason_end_date,
12
13
  :season_start_date,
13
- :season_end_date
14
-
15
- attr_reader_value :year, :games
16
-
17
- def initialize(year:, games: [])
18
- super()
19
-
20
- raise ArgumentError, 'year is required' unless year
21
-
22
- @year = year
23
- @preseason_start_date = Date.new(year, 9, 30)
24
- @preseason_end_date = Date.new(year, 10, 14)
25
- @season_start_date = Date.new(year, 10, 18)
26
- @season_end_date = Date.new(year + 1, 4, 29)
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
27
33
  @games = []
28
34
 
29
35
  games.each { |game| add!(game) }
@@ -40,78 +46,41 @@ module Basketball
40
46
  self
41
47
  end
42
48
 
43
- def preseason_games_for(date: nil, team: nil)
44
- games_for(date:, team:).select { |game| game.is_a?(PreseasonGame) }
49
+ def exhibitions_for(date: nil, opponent: nil)
50
+ games_for(date:, opponent:).select { |game| game.is_a?(Exhibition) }
45
51
  end
46
52
 
47
- def season_games_for(date: nil, team: nil)
48
- games_for(date:, team:).select { |game| game.is_a?(SeasonGame) }
53
+ def regulars_for(date: nil, opponent: nil)
54
+ games_for(date:, opponent:).select { |game| game.is_a?(Regular) }
49
55
  end
50
56
 
51
- def games_for(date: nil, team: nil)
57
+ def games_for(date: nil, opponent: nil)
52
58
  games.select do |game|
53
- (date.nil? || game.date == date) &&
54
- (team.nil? || (game.home_team == team || game.away_team == team))
59
+ (date.nil? || game.date == date) && (opponent.nil? || game.for?(opponent))
55
60
  end
56
61
  end
57
62
 
58
- def available_preseason_dates_for(team)
59
- all_preseason_dates - preseason_games_for(team:).map(&:date)
60
- end
61
-
62
- def available_season_dates_for(team)
63
- all_season_dates - season_games_for(team:).map(&:date)
64
- end
65
-
66
- def available_preseason_matchup_dates(team1, team2)
67
- available_team_dates = available_preseason_dates_for(team1)
68
- available_other_team_dates = available_preseason_dates_for(team2)
69
-
70
- available_team_dates & available_other_team_dates
71
- end
72
-
73
- def available_season_matchup_dates(team1, team2)
74
- available_team_dates = available_season_dates_for(team1)
75
- available_other_team_dates = available_season_dates_for(team2)
76
-
77
- available_team_dates & available_other_team_dates
78
- end
79
-
80
- def teams
81
- games.flat_map(&:teams)
82
- end
83
-
84
- def team(id)
85
- teams.find { |t| t == Team.new(id:) }
86
- end
87
-
88
63
  private
89
64
 
90
- def all_preseason_dates
91
- (preseason_start_date..preseason_end_date).to_a
92
- end
93
-
94
- def all_season_dates
95
- (season_start_date..season_end_date).to_a
96
- end
97
-
98
65
  def assert_free_date(game)
99
- if games_for(date: game.date, team: game.home_team).any?
100
- raise TeamAlreadyBookedError, "#{game.home_team} already playing on #{game.date}"
66
+ if games_for(date: game.date, opponent: game.home_opponent).any?
67
+ raise TeamAlreadyBookedError, "#{game.home_opponent} already playing on #{game.date}"
101
68
  end
102
69
 
103
- return unless games_for(date: game.date, team: game.away_team).any?
70
+ return unless games_for(date: game.date, opponent: game.away_opponent).any?
104
71
 
105
- raise TeamAlreadyBookedError, "#{game.away_team} already playing on #{game.date}"
72
+ raise TeamAlreadyBookedError, "#{game.away_opponent} already playing on #{game.date}"
106
73
  end
107
74
 
108
75
  def assert_in_bounds(game)
109
- if game.is_a?(PreseasonGame)
110
- raise OutOfBoundsError, "#{game.date} is before preseason begins" if game.date < preseason_start_date
111
- raise OutOfBoundsError, "#{game.date} is after preseason ends" if game.date > preseason_end_date
112
- elsif game.is_a?(SeasonGame)
113
- raise OutOfBoundsError, "#{game.date} is before season begins" if game.date < season_start_date
114
- raise OutOfBoundsError, "#{game.date} is after season ends" if game.date > season_end_date
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
115
84
  else
116
85
  raise ArgumentError, "Dont know what this game type is: #{game.class.name}"
117
86
  end
@@ -1,179 +1,237 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative 'calendar'
4
-
5
3
  module Basketball
6
4
  module Season
7
- # This is the service class responsible for actually picking out free dates ane pairing up teams to
8
- # play each other. This is a reasonable naive first pass at some underlying match-making algorithms
9
- # but could definitely use some help with the complexity/runtime/etc.
10
- class Coordinator
11
- MIN_PRESEASON_GAMES_PER_TEAM = 3
12
- MAX_PRESEASON_GAMES_PER_TEAM = 6
13
-
14
- private_constant :MIN_PRESEASON_GAMES_PER_TEAM,
15
- :MAX_PRESEASON_GAMES_PER_TEAM
16
-
17
- def schedule(year:, league:)
18
- Calendar.new(year:).tap do |calendar|
19
- schedule_preseason!(calendar:, league:)
20
- schedule_season!(calendar:, league:)
21
- end
5
+ # Main iterator-based object that knows how to manage a calendar and simulate games per day.
6
+ class Coordinator < Entity
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
+
23
+ def_delegators :calendar,
24
+ :preseason_start_date,
25
+ :preseason_end_date,
26
+ :season_start_date,
27
+ :season_end_date,
28
+ :games,
29
+ :exhibitions_for,
30
+ :regulars_for,
31
+ :games_for
32
+
33
+ def initialize(
34
+ calendar:,
35
+ current_date:,
36
+ results: [],
37
+ league: Org::League.new
38
+ )
39
+ super()
40
+
41
+ raise ArgumentError, 'calendar is required' unless calendar
42
+ raise ArgumentError, 'current_date is required' if current_date.to_s.empty?
43
+ raise ArgumentError, 'league is required' unless league
44
+
45
+ @calendar = calendar
46
+ @current_date = current_date
47
+ @arena = Arena.new
48
+ @results = []
49
+ @league = league
50
+
51
+ results.each { |result| replay!(result) }
52
+
53
+ assert_current_date
54
+ assert_all_past_dates_are_played
55
+ assert_all_future_dates_arent_played
56
+ assert_all_known_teams
22
57
  end
23
58
 
24
- private
59
+ def sim_rest!(&)
60
+ events = []
61
+
62
+ while not_done?
63
+ new_events = sim!(&)
25
64
 
26
- def base_matchup_count(league, team1, team2)
27
- # Same Conference, Same Division
28
- if league.division_for(team1) == league.division_for(team2)
29
- 4
30
- # Same Conference, Different Division and one of 4/10 that play 3 times
31
- elsif league.conference_for(team1) == league.conference_for(team2)
32
- 3
33
- # Different Conference
34
- else
35
- 2
65
+ events += new_events
36
66
  end
67
+
68
+ events
37
69
  end
38
70
 
39
- # rubocop:disable Metrics/AbcSize
40
- # This method derives the plan for which a schedule can be generated from.
41
- def matchup_plan(league)
42
- matchups = {}
43
- game_counts = league.teams.to_h { |t| [t, 0] }
44
- teams = game_counts.keys
45
-
46
- (0...teams.length).each do |i|
47
- team1 = teams[i]
48
-
49
- (i + 1...teams.length).each do |j|
50
- team2 = teams[j]
51
- key = [team1, team2].sort
52
- count = base_matchup_count(league, team1, team2)
53
- matchups[key] = count
54
- game_counts[team1] += count
55
- game_counts[team2] += count
56
- end
71
+ def assert_current_date
72
+ if current_date < preseason_start_date
73
+ raise OutOfBoundsError, "current date #{current_date} should be on or after #{preseason_start_date}"
57
74
  end
58
75
 
59
- # Each team will play 6 games against conference opponents in other divisions.
60
- # The fours hash will be that plan.
61
- find_fours(league).each do |team, opponents|
62
- next if game_counts[team] == 82
76
+ return unless current_date > season_end_date
77
+
78
+ raise OutOfBoundsError, "current date #{current_date} should be on or after #{preseason_start_date}"
79
+ end
63
80
 
64
- opponents.each do |opponent|
65
- next if game_counts[team] == 82
66
- next if game_counts[opponent] == 82
81
+ def sim!
82
+ return [] if done?
67
83
 
68
- game_counts[team] += 1
69
- game_counts[opponent] += 1
84
+ events = []
85
+ games = games_for(date: current_date)
70
86
 
71
- key = [team, opponent].sort
87
+ games.each do |game|
88
+ home_players = opponent_team(game.home_opponent).players
89
+ away_players = opponent_team(game.away_opponent).players
90
+ matchup = Matchup.new(game:, home_players:, away_players:)
91
+ event = arena.play(matchup)
72
92
 
73
- matchups[key] += 1
74
- end
93
+ play!(event)
94
+
95
+ yield(event) if block_given?
96
+
97
+ events << event
75
98
  end
76
99
 
77
- matchups
78
- end
79
- # rubocop:enable Metrics/AbcSize
100
+ increment_current_date!
80
101
 
81
- # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
82
- # I am not liking this algorithm implementation at all but it will seemingly produce a valid
83
- # result about 1 out of every 1000 cycles. I have yet to spot the assignment pattern to make
84
- # this way more deterministic.
85
- def find_fours(league)
86
- balanced = false
87
- count = 0
88
- four_tracker = {}
102
+ events
103
+ end
89
104
 
90
- until balanced
91
- # Let's not completely thrash our CPUs in case this algorithm hits an infinite loop.
92
- # Instead, lets hard-fail against a hard boundary.
93
- raise ArgumentError, 'we spent too much CPU time and didnt resolve fours' if count > 100_000
105
+ def total_days
106
+ (season_end_date - preseason_start_date).to_i
107
+ end
94
108
 
95
- four_tracker = league.teams.to_h { |team| [team, []] }
109
+ def days_left
110
+ (season_end_date - current_date).to_i
111
+ end
96
112
 
97
- league.teams.each do |team|
98
- opponents = league.cross_division_opponents_for(team).shuffle
113
+ def total_exhibitions
114
+ exhibitions_for.length
115
+ end
99
116
 
100
- opponents.each do |opponent|
101
- if four_tracker[team].length < 6 && four_tracker[opponent].length < 6
102
- four_tracker[opponent] << team
103
- four_tracker[team] << opponent
104
- end
105
- end
106
- end
117
+ def exhibitions_left
118
+ total_exhibitions - exhibition_result_events.length
119
+ end
107
120
 
108
- good = true
121
+ def total_regulars
122
+ regulars_for.length
123
+ end
109
124
 
110
- # trip-wire: if one team isnt balanced then we are not balanced
111
- four_tracker.each { |_k, v| good = false if v.length < 6 }
125
+ def regulars_left
126
+ total_regulars - regular_result_events.length
127
+ end
112
128
 
113
- balanced = good
129
+ def current_games
130
+ games_for(date: current_date) - results.map(&:game)
131
+ end
114
132
 
115
- count += 1
116
- end
133
+ def done?
134
+ current_date == season_end_date && games.length == results.length
135
+ end
117
136
 
118
- four_tracker
137
+ def not_done?
138
+ !done?
119
139
  end
120
- # rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
121
140
 
122
- def schedule_season!(calendar:, league:)
123
- matchups = matchup_plan(league)
141
+ def add!(game)
142
+ assert_today_or_in_future(game)
143
+ assert_known_teams(game)
124
144
 
125
- matchups.each do |(team1, team2), count|
126
- candidates = calendar.available_season_matchup_dates(team1, team2)
127
- dates = candidates.sample(count)
128
- games = balanced_games(dates, team1, team2)
145
+ calendar.add!(game)
129
146
 
130
- games.each { |game| calendar.add!(game) }
131
- end
147
+ self
132
148
  end
133
149
 
134
- def balanced_games(dates, team1, team2)
135
- dates.map.with_index(1) do |date, index|
136
- if index.even?
137
- SeasonGame.new(date:, home_team: team1, away_team: team2)
138
- else
139
- SeasonGame.new(date:, home_team: team2, away_team: team1)
140
- end
150
+ def result_for(game)
151
+ results.find do |result|
152
+ result.game == game
141
153
  end
142
154
  end
143
155
 
144
- def schedule_preseason!(calendar:, league:)
145
- league.teams.each do |team|
146
- current_games = calendar.preseason_games_for(team:)
147
- count = current_games.length
156
+ private
157
+
158
+ attr_writer :arena
159
+
160
+ def opponent_team(opponent)
161
+ league.teams.find { |t| t == opponent }
162
+ end
163
+
164
+ def exhibition_result_events
165
+ results.select { |e| e.game.is_a?(Exhibition) }
166
+ end
148
167
 
149
- next if count >= MIN_PRESEASON_GAMES_PER_TEAM
168
+ def regular_result_events
169
+ results.select { |e| e.game.is_a?(Regular) }
170
+ end
150
171
 
151
- other_teams = (league.teams - [team]).shuffle
172
+ def increment_current_date!
173
+ return self if current_date >= season_end_date
152
174
 
153
- other_teams.each do |other_team|
154
- break if count > MIN_PRESEASON_GAMES_PER_TEAM
155
- next if calendar.preseason_games_for(team: other_team).length >= MAX_PRESEASON_GAMES_PER_TEAM
175
+ @current_date = current_date + 1
156
176
 
157
- candidates = calendar.available_preseason_matchup_dates(team, other_team)
177
+ self
178
+ end
158
179
 
159
- next if candidates.empty?
180
+ def assert_today_or_in_future(game)
181
+ return unless game.date <= current_date
160
182
 
161
- date = candidates.sample
162
- game = random_preseason_game(date, team, other_team)
183
+ raise OutOfBoundsError, "#{game.date} is on or before the current date (#{current_date})"
184
+ end
163
185
 
164
- calendar.add!(game)
186
+ def assert_known_teams(game)
187
+ raise UnknownTeamError, "unknown opponent: #{game.home_opponent}" if league.not_registered?(game.home_opponent)
165
188
 
166
- count += 1
167
- end
168
- end
189
+ return unless league.not_registered?(game.away_opponent)
190
+
191
+ raise UnknownTeamError, "unknown opponent: #{game.away_opponent}"
169
192
  end
170
193
 
171
- def random_preseason_game(date, team1, team2)
172
- if rand(1..2) == 1
173
- PreseasonGame.new(date:, home_team: team1, away_team: team2)
174
- else
175
- PreseasonGame.new(date:, home_team: team2, away_team: team1)
194
+ def assert_all_past_dates_are_played
195
+ games_that_should_be_played = games.select { |game| game.date < current_date }
196
+
197
+ games_played = results.map(&:game)
198
+ unplayed_games = games_that_should_be_played - games_played
199
+
200
+ return if unplayed_games.empty?
201
+
202
+ raise UnplayedGamesError, "#{unplayed_games.length} game(s) not played before #{current_date}"
203
+ end
204
+
205
+ def assert_all_future_dates_arent_played
206
+ games_that_shouldnt_be_played = results.select do |result|
207
+ result.date > current_date
176
208
  end
209
+
210
+ count = games_that_shouldnt_be_played.length
211
+
212
+ return unless games_that_shouldnt_be_played.any?
213
+
214
+ raise PlayedGamesError, "#{count} game(s) played after #{current_date}"
215
+ end
216
+
217
+ def play!(result)
218
+ raise GameNotCurrentError, "#{result} is not for #{current_date}" if result.date != current_date
219
+
220
+ replay!(result)
221
+ end
222
+
223
+ def replay!(result)
224
+ raise AlreadyPlayedGameError, "#{result.game} already played!" if result_for(result.game)
225
+
226
+ raise UnknownGameError, "game not added: #{result.game}" unless games.include?(result.game)
227
+
228
+ results << result
229
+
230
+ result
231
+ end
232
+
233
+ def assert_all_known_teams
234
+ calendar.games.each { |game| assert_known_teams(game) }
177
235
  end
178
236
  end
179
237
  end
@@ -2,7 +2,8 @@
2
2
 
3
3
  module Basketball
4
4
  module Season
5
- class PreseasonGame < Game
5
+ # An exhibition game.
6
+ class Exhibition < Game
6
7
  def to_s
7
8
  "#{super} (preseason)"
8
9
  end