basketball 0.0.8 → 0.0.10

Sign up to get free protection for your applications and to get access to all the features.
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