basketball 0.0.8 → 0.0.9

Sign up to get free protection for your applications and to get access to all the features.
Files changed (54) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +9 -19
  3. data/CHANGELOG.md +1 -39
  4. data/README.md +72 -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 +243 -0
  9. data/lib/basketball/app/coordinator_repository.rb +191 -0
  10. data/lib/basketball/app/file_store.rb +22 -0
  11. data/lib/basketball/{draft/cli.rb → app/room_cli.rb} +53 -76
  12. data/lib/basketball/app/room_repository.rb +189 -0
  13. data/lib/basketball/app.rb +12 -0
  14. data/lib/basketball/draft/assessment.rb +31 -0
  15. data/lib/basketball/draft/event.rb +3 -2
  16. data/lib/basketball/draft/front_office.rb +35 -28
  17. data/lib/basketball/draft/{pick_event.rb → pick.rb} +13 -6
  18. data/lib/basketball/draft/room.rb +119 -119
  19. data/lib/basketball/draft/{player_search.rb → scout.rb} +4 -9
  20. data/lib/basketball/draft/skip.rb +12 -0
  21. data/lib/basketball/draft.rb +13 -6
  22. data/lib/basketball/entity.rb +10 -4
  23. data/lib/basketball/org/league.rb +73 -0
  24. data/lib/basketball/org/player.rb +26 -0
  25. data/lib/basketball/{draft → org}/position.rb +3 -2
  26. data/lib/basketball/org/team.rb +38 -0
  27. data/lib/basketball/org.rb +12 -0
  28. data/lib/basketball/season/arena.rb +112 -0
  29. data/lib/basketball/season/calendar.rb +41 -72
  30. data/lib/basketball/season/coordinator.rb +185 -126
  31. data/lib/basketball/season/{preseason_game.rb → exhibition.rb} +2 -1
  32. data/lib/basketball/season/game.rb +15 -10
  33. data/lib/basketball/season/matchup.rb +27 -0
  34. data/lib/basketball/season/opponent.rb +15 -0
  35. data/lib/basketball/season/{season_game.rb → regular.rb} +2 -1
  36. data/lib/basketball/season/result.rb +37 -0
  37. data/lib/basketball/season.rb +12 -13
  38. data/lib/basketball/value_object.rb +4 -1
  39. data/lib/basketball/version.rb +1 -1
  40. data/lib/basketball.rb +9 -4
  41. metadata +32 -44
  42. data/lib/basketball/draft/league.rb +0 -70
  43. data/lib/basketball/draft/player.rb +0 -43
  44. data/lib/basketball/draft/room_serializer.rb +0 -186
  45. data/lib/basketball/draft/roster.rb +0 -37
  46. data/lib/basketball/draft/sim_event.rb +0 -23
  47. data/lib/basketball/draft/skip_event.rb +0 -13
  48. data/lib/basketball/season/calendar_serializer.rb +0 -94
  49. data/lib/basketball/season/conference.rb +0 -57
  50. data/lib/basketball/season/division.rb +0 -43
  51. data/lib/basketball/season/league.rb +0 -114
  52. data/lib/basketball/season/league_serializer.rb +0 -99
  53. data/lib/basketball/season/scheduling_cli.rb +0 -198
  54. data/lib/basketball/season/team.rb +0 -21
@@ -2,22 +2,34 @@
2
2
 
3
3
  module Basketball
4
4
  module Draft
5
+ # A team will send their front office to a draft room to make draft selections.
5
6
  class FrontOffice < Entity
6
- MAX_DEPTH = 3
7
- MAX_FUZZ = 2
7
+ # The higher the number the more the front office will make more top-player-based decisions
8
+ # over position-based decisions.
9
+ DEFAULT_MAX_STAR_LEVEL = 5
10
+
11
+ # Riskier front offices may choose to not draft the top player. The higher the number the more
12
+ # they will not select the top player available.
13
+ DEFAULT_MAX_RISK_LEVEL = 5
14
+
8
15
  MAX_POSITIONS = 12
9
16
 
10
- private_constant :MAX_DEPTH, :MAX_FUZZ, :MAX_POSITIONS
17
+ private_constant :DEFAULT_MAX_STAR_LEVEL, :DEFAULT_MAX_RISK_LEVEL, :MAX_POSITIONS
11
18
 
12
- attr_reader :prioritized_positions, :fuzz, :depth, :name
19
+ attr_reader :prioritized_positions, :risk_level, :star_level, :scout
13
20
 
14
- def initialize(id:, name: '', prioritized_positions: [], fuzz: rand(0..MAX_FUZZ), depth: rand(0..MAX_DEPTH))
21
+ def initialize(
22
+ id:,
23
+ prioritized_positions: [],
24
+ risk_level: rand(0..DEFAULT_MAX_RISK_LEVEL),
25
+ star_level: rand(0..DEFAULT_MAX_STAR_LEVEL)
26
+ )
15
27
  super(id)
16
28
 
17
- @name = name
18
- @fuzz = fuzz.to_i
19
- @depth = depth.to_i
29
+ @risk_level = risk_level.to_i
30
+ @star_level = star_level.to_i
20
31
  @prioritized_positions = prioritized_positions
32
+ @scout = Scout.new
21
33
 
22
34
  # fill in the rest of the queue here
23
35
  need_count = MAX_POSITIONS - @prioritized_positions.length
@@ -27,51 +39,46 @@ module Basketball
27
39
  freeze
28
40
  end
29
41
 
30
- def pick(undrafted_player_search:, drafted_players:, round:)
42
+ def pick(assessment)
31
43
  players = []
44
+ players = adaptive_search(assessment) if star_level >= assessment.round
45
+ players = balanced_search(assessment) if players.empty?
46
+ players = top_players(assessment) if players.empty?
32
47
 
33
- players = adaptive_search(undrafted_player_search:, drafted_players:) if depth >= round
34
- players = balanced_search(undrafted_player_search:, drafted_players:) if players.empty?
35
- players = top_players(undrafted_player_search:) if players.empty?
36
-
37
- players[0..fuzz].sample
38
- end
39
-
40
- def to_s
41
- "[#{super}] #{name}"
48
+ players[0..risk_level].sample
42
49
  end
43
50
 
44
51
  private
45
52
 
46
- def adaptive_search(undrafted_player_search:, drafted_players:)
47
- drafted_positions = drafted_players.map(&:position)
53
+ def adaptive_search(assessment)
54
+ drafted_positions = assessment.drafted_players.map(&:position)
48
55
 
49
- undrafted_player_search.query(exclude_positions: drafted_positions)
56
+ scout.top_for(players: assessment.undrafted_players, exclude_positions: drafted_positions)
50
57
  end
51
58
 
52
- def balanced_search(undrafted_player_search:, drafted_players:)
59
+ def balanced_search(assessment)
53
60
  players = []
54
61
 
55
62
  # Try to find best pick for exact desired position.
56
63
  # If you cant find one, then move to the next desired position until the end of the queue
57
- available_prioritized_positions(drafted_players:).each do |position|
58
- players = undrafted_player_search.query(position:)
64
+ available_prioritized_positions(assessment.drafted_players).each do |position|
65
+ players = scout.top_for(players: assessment.undrafted_players, position:)
59
66
 
60
67
  break if players.any?
61
68
  end
62
69
 
63
- players = players.any? ? players : undrafted_player_search.query
70
+ players = players.any? ? players : scout.top_for
64
71
  end
65
72
 
66
73
  def all_random_positions
67
- Position::ALL_VALUES.to_a.shuffle.map { |v| Position.new(v) }
74
+ Org::Position::ALL_VALUES.to_a.shuffle.map { |v| Org::Position.new(v) }
68
75
  end
69
76
 
70
77
  def random_positions_queue
71
- all_random_positions + all_random_positions + [Position.random] + [Position.random]
78
+ all_random_positions + all_random_positions + [Org::Position.random] + [Org::Position.random]
72
79
  end
73
80
 
74
- def available_prioritized_positions(drafted_players:)
81
+ def available_prioritized_positions(drafted_players)
75
82
  drafted_positions = drafted_players.map(&:position)
76
83
  queue = prioritized_positions.dup
77
84
 
@@ -1,25 +1,32 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative 'event'
4
-
5
3
  module Basketball
6
4
  module Draft
7
- class PickEvent < Event
8
- attr_reader_value :player
5
+ # Room event where a player is selected.
6
+ class Pick < Event
7
+ value_reader :player, :auto
9
8
 
10
- def initialize(front_office:, player:, pick:, round:, round_pick:)
9
+ def initialize(front_office:, player:, pick:, round:, round_pick:, auto: false)
11
10
  super(front_office:, pick:, round:, round_pick:)
12
11
 
13
12
  raise ArgumentError, 'player required' unless player
14
13
 
15
14
  @player = player
15
+ @auto = auto
16
16
 
17
17
  freeze
18
18
  end
19
19
 
20
20
  def to_s
21
- "#{player} picked #{super}"
21
+ "#{super} #{auto ? 'auto-' : ''}picked #{player}"
22
+ end
23
+
24
+ def ==(other)
25
+ super &&
26
+ player == other.player &&
27
+ auto == other.auto
22
28
  end
29
+ alias eql? ==
23
30
  end
24
31
  end
25
32
  end
@@ -1,142 +1,146 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative 'league'
4
-
5
3
  module Basketball
6
4
  module Draft
5
+ # Main pick-by-pick iterator object which will round-robin rotate team selections.
7
6
  class Room
8
7
  class AlreadyPickedError < StandardError; end
9
- class DupeEventError < StandardError; end
8
+ class EndOfDraftError < StandardError; end
10
9
  class EventOutOfOrderError < StandardError; end
11
- class UnknownPlayerError < StandardError; end
10
+ class FrontOfficeAlreadyRegisteredError < StandardError; end
11
+ class PlayerAlreadyAddedError < StandardError; end
12
12
  class UnknownFrontOfficeError < StandardError; end
13
- class EndOfDraftError < StandardError; end
14
-
15
- DEFAULT_ROUNDS = 12
16
-
17
- private_constant :DEFAULT_ROUNDS
13
+ class UnknownPlayerError < StandardError; end
18
14
 
19
- attr_reader :events, :rounds
15
+ attr_reader :rounds, :players, :front_offices, :events, :id
20
16
 
21
- def initialize(players: [], front_offices: [], events: [], rounds: DEFAULT_ROUNDS)
22
- @players_by_id = players.to_h { |p| [p.id, p] }
23
- @front_offices_by_id = front_offices.to_h { |fo| [fo.id, fo] }
24
- @events = []
25
- @rounds = rounds.to_i
17
+ def initialize(front_offices:, rounds:, players: [], events: [])
18
+ raise InvalidRoundsError, "#{rounds} should be a positive number" unless rounds.positive?
26
19
 
27
- # Each one will be validated for correctness.
28
- events.each { |e| play!(e) }
20
+ @rounds = rounds
21
+ @players = []
22
+ @front_offices = []
23
+ @events = []
29
24
 
30
- freeze
25
+ front_offices.each { |front_office| register!(front_office) }
26
+ players.each { |player| add_player!(player) }
27
+ events.each { |event| add_event!(event) }
31
28
  end
32
29
 
33
- def to_league
34
- League.new(front_offices:).tap do |league|
35
- player_events.each do |event|
36
- league.register!(player: event.player, front_office: event.front_office)
37
- end
30
+ # This method will return a materialized list of teams and their selections.
31
+ def league
32
+ Org::League.new.tap do |league|
33
+ front_offices.each do |front_office|
34
+ team = Org::Team.new(id: front_office.id)
35
+
36
+ league.register!(team)
38
37
 
39
- undrafted_players.each do |player|
40
- league.register!(player:)
38
+ drafted_players(front_office).each do |player|
39
+ league.sign!(player:, team:)
40
+ end
41
41
  end
42
42
  end
43
43
  end
44
44
 
45
- def to_s
46
- events.join("\n")
47
- end
48
-
49
- def front_offices
50
- front_offices_by_id.values
51
- end
45
+ ### Peek Methods
52
46
 
53
- def players
54
- players_by_id.values
47
+ def assessment
48
+ Assessment.new(
49
+ drafted_players: drafted_players(front_office),
50
+ undrafted_players:,
51
+ pick:,
52
+ round:,
53
+ round_pick:
54
+ )
55
55
  end
56
56
 
57
57
  def total_picks
58
58
  rounds * front_offices.length
59
59
  end
60
60
 
61
- def current_round
61
+ def round
62
62
  return if done?
63
63
 
64
- (current_pick / front_offices.length.to_f).ceil
64
+ (pick / front_offices.length.to_f).ceil
65
65
  end
66
66
 
67
- def current_round_pick
67
+ def round_pick
68
68
  return if done?
69
69
 
70
- mod = current_pick % front_offices.length
70
+ mod = pick % front_offices.length
71
71
 
72
72
  mod.positive? ? mod : front_offices.length
73
73
  end
74
74
 
75
- def current_front_office
75
+ def front_office
76
76
  return if done?
77
77
 
78
- front_offices[current_round_pick - 1]
78
+ front_offices[round_pick - 1]
79
79
  end
80
80
 
81
- def current_pick
81
+ def pick
82
82
  return if done?
83
83
 
84
- internal_current_pick
84
+ internal_pick
85
85
  end
86
86
 
87
87
  def remaining_picks
88
- total_picks - internal_current_pick + 1
88
+ total_picks - internal_pick + 1
89
89
  end
90
90
 
91
91
  def done?
92
- internal_current_pick > total_picks
92
+ internal_pick > total_picks
93
93
  end
94
94
 
95
95
  def not_done?
96
96
  !done?
97
97
  end
98
98
 
99
+ def drafted_players(front_office = nil)
100
+ raise UnknownFrontOfficeError, "#{front_office} doesnt exist" if front_office && !registered?(front_office)
101
+
102
+ player_events.each_with_object([]) do |e, memo|
103
+ next unless front_office.nil? || e.front_office == front_office
104
+
105
+ memo << e.player
106
+ end
107
+ end
108
+
109
+ def undrafted_players
110
+ players - drafted_players
111
+ end
112
+
113
+ ### Event Methods
114
+
99
115
  def skip!
100
116
  return if done?
101
117
 
102
- event = SkipEvent.new(
103
- front_office: current_front_office,
104
- pick: current_pick,
105
- round: current_round,
106
- round_pick: current_round_pick
107
- )
118
+ event = Skip.new(front_office:, pick:, round:, round_pick:)
108
119
 
109
- play!(event)
120
+ add_event!(event)
110
121
 
111
122
  event
112
123
  end
113
124
 
114
- def sim!(times = nil)
115
- counter = 0
116
- events = []
125
+ def sim!
126
+ return if done?
127
+
128
+ player = front_office.pick(assessment)
129
+ event = Pick.new(front_office:, pick:, round:, round_pick:, player:, auto: true)
117
130
 
118
- until done? || (times && counter >= times)
119
- front_office = current_front_office
131
+ add_event!(event)
120
132
 
121
- player = front_office.pick(
122
- undrafted_player_search:,
123
- drafted_players: drafted_players(front_office),
124
- round: current_round
125
- )
133
+ event
134
+ end
126
135
 
127
- event = SimEvent.new(
128
- front_office:,
129
- player:,
130
- pick: current_pick,
131
- round: current_round,
132
- round_pick: current_round_pick
133
- )
136
+ def sim_rest!
137
+ events = []
134
138
 
135
- play!(event)
139
+ while not_done?
140
+ event = sim!
136
141
 
137
- yield(event) if block_given?
142
+ yield event if block_given?
138
143
 
139
- counter += 1
140
144
  events << event
141
145
  end
142
146
 
@@ -146,76 +150,72 @@ module Basketball
146
150
  def pick!(player)
147
151
  return nil if done?
148
152
 
149
- event = PickEvent.new(
150
- front_office: current_front_office,
151
- player:,
152
- pick: current_pick,
153
- round: current_round,
154
- round_pick: current_round_pick
155
- )
156
-
157
- play!(event)
158
- end
159
-
160
- def undrafted_players
161
- players - drafted_players
162
- end
153
+ event = Pick.new(front_office:, pick:, round:, round_pick:, player:)
163
154
 
164
- def undrafted_player_search
165
- PlayerSearch.new(undrafted_players)
155
+ add_event!(event)
166
156
  end
167
157
 
168
158
  private
169
159
 
170
- attr_reader :players_by_id, :front_offices_by_id
160
+ attr_writer :id
171
161
 
172
162
  def player_events
173
163
  events.select { |e| e.respond_to?(:player) }
174
164
  end
175
165
 
176
- def internal_current_pick
177
- events.length + 1
166
+ # rubocop:disable Metrics/AbcSize
167
+ def add_event!(event)
168
+ raise EndOfDraftError, "#{total_picks} pick limit reached" if done?
169
+ raise UnknownFrontOfficeError, "#{front_office} doesnt exist" unless front_offices.include?(event.front_office)
170
+ raise EventOutOfOrderError, "#{event.front_office} cant pick right now" if event.front_office != front_office
171
+ raise EventOutOfOrderError, "#{event} has wrong pick" if event.pick != pick
172
+ raise EventOutOfOrderError, "#{event} has wrong round" if event.round != round
173
+ raise EventOutOfOrderError, "#{event} has wrong round_pick" if event.round_pick != round_pick
174
+
175
+ assert_player(event)
176
+
177
+ events << event
178
+
179
+ event
178
180
  end
181
+ # rubocop:enable Metrics/AbcSize
179
182
 
180
- def drafted_players(front_office = nil)
181
- player_events.each_with_object([]) do |e, memo|
182
- next unless front_office.nil? || e.front_office == front_office
183
+ def assert_player(event)
184
+ return unless event.respond_to?(:player)
183
185
 
184
- memo << e.player
185
- end
186
+ raise AlreadyPickedError, "#{event.player} was already picked" if drafted_players.include?(event.player)
187
+ raise UnknownPlayerError, "#{event.player} doesnt exist" unless players.include?(event.player)
186
188
  end
187
189
 
188
- # rubocop:disable Metrics/AbcSize
189
- # rubocop:disable Metrics/CyclomaticComplexity
190
- # rubocop:disable Metrics/PerceivedComplexity
191
- def play!(event)
192
- if event.respond_to?(:player) && drafted_players.include?(event.player)
193
- raise AlreadyPickedError, "#{player} was already picked"
194
- end
190
+ def internal_pick
191
+ events.length + 1
192
+ end
195
193
 
196
- if event.respond_to?(:player) && !players.include?(event.player)
197
- raise UnknownPlayerError, "#{event.player} doesnt exist"
198
- end
194
+ def registered?(front_office)
195
+ front_offices.include?(front_office)
196
+ end
199
197
 
200
- if event.front_office != current_front_office
201
- raise EventOutOfOrder, "#{event} #{event.front_office} cant pick right now"
202
- end
198
+ def register!(front_office)
199
+ raise ArgumentError, 'front_office required' unless front_office
200
+ raise FrontOfficeAlreadyRegisteredError, "#{front_office} already registered" if registered?(front_office)
203
201
 
204
- raise UnknownFrontOfficeError, "#{front_office} doesnt exist" unless front_offices.include?(event.front_office)
202
+ front_offices << front_office
205
203
 
206
- raise DupeEventError, "#{event} is a dupe" if events.include?(event)
207
- raise EventOutOfOrder, "#{event} has wrong pick" if event.pick != current_pick
208
- raise EventOutOfOrder, "#{event} has wrong round" if event.round != current_round
209
- raise EventOutOfOrder, "#{event} has wrong round_pick" if event.round_pick != current_round_pick
210
- raise EndOfDraftError, "#{total_picks} pick limit reached" if events.length > total_picks + 1
204
+ self
205
+ end
211
206
 
212
- events << event
207
+ def player?(player)
208
+ players.include?(player)
209
+ end
213
210
 
214
- event
211
+ def add_player!(player)
212
+ raise ArgumentError, 'player required' unless player
213
+ raise PlayerAlreadyAddedError, "#{player} already added" if player?(player)
214
+
215
+ players << player
216
+
217
+ self
215
218
  end
216
- # rubocop:enable Metrics/AbcSize
217
- # rubocop:enable Metrics/CyclomaticComplexity
218
- # rubocop:enable Metrics/PerceivedComplexity
219
219
  end
220
220
  end
221
221
  end
@@ -2,14 +2,9 @@
2
2
 
3
3
  module Basketball
4
4
  module Draft
5
- class PlayerSearch
6
- attr_reader :players
7
-
8
- def initialize(players = [])
9
- @players = players
10
- end
11
-
12
- def query(position: nil, exclude_positions: [])
5
+ # A Scout knows how to process a set of players and figure out who the top prospects are.
6
+ class Scout
7
+ def top_for(players: [], position: nil, exclude_positions: [])
13
8
  filtered_players = players
14
9
 
15
10
  if position
@@ -24,7 +19,7 @@ module Basketball
24
19
  end
25
20
  end
26
21
 
27
- filtered_players.sort_by(&:overall).reverse
22
+ filtered_players.sort_by { |p| [p.overall, p.id] }.reverse
28
23
  end
29
24
  end
30
25
  end
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Basketball
4
+ module Draft
5
+ # Room event which indicates a front office has intentionally skipped a selection.
6
+ class Skip < Event
7
+ def to_s
8
+ "#{super} skipped"
9
+ end
10
+ end
11
+ end
12
+ end
@@ -1,9 +1,16 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative 'draft/cli'
3
+ # Services
4
+ require_relative 'draft/scout'
4
5
 
5
- module Basketball
6
- module Draft
7
- class PlayerAlreadyRegisteredError < StandardError; end
8
- end
9
- end
6
+ # Common
7
+ require_relative 'draft/assessment'
8
+ require_relative 'draft/event'
9
+ require_relative 'draft/front_office'
10
+
11
+ # Event Subclasses
12
+ require_relative 'draft/pick'
13
+ require_relative 'draft/skip'
14
+
15
+ # Specific
16
+ require_relative 'draft/room'
@@ -1,6 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Basketball
4
+ # Base class for uniquely identifiable classes. Subclasses are simply based on a string-based ID
5
+ # and comparison/sorting/equality will be done in a case-insensitive manner.
4
6
  class Entity
5
7
  extend Forwardable
6
8
  include Comparable
@@ -12,20 +14,24 @@ module Basketball
12
14
  def initialize(id)
13
15
  raise ArgumentError, 'id is required' if id.to_s.empty?
14
16
 
15
- @id = id.to_s.upcase
17
+ @id = id
16
18
  end
17
19
 
18
20
  def <=>(other)
19
- id <=> other.id
21
+ comparable_id <=> other.comparable_id
20
22
  end
21
23
 
22
24
  def ==(other)
23
- id == other.id
25
+ comparable_id == other.comparable_id
24
26
  end
25
27
  alias eql? ==
26
28
 
27
29
  def hash
28
- id.hash
30
+ comparable_id.hash
31
+ end
32
+
33
+ def comparable_id
34
+ id.to_s.upcase
29
35
  end
30
36
  end
31
37
  end
@@ -0,0 +1,73 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Basketball
4
+ module Org
5
+ # Describes a collection of teams and players. Holds the rules which support
6
+ # adding teams and players to ensure the all the teams are cohesive, such as:
7
+ # - preventing duplicate teams
8
+ # - preventing double-signing players across teams
9
+ class League
10
+ class TeamAlreadyRegisteredError < StandardError; end
11
+ class UnregisteredTeamError < StandardError; end
12
+
13
+ attr_reader :teams
14
+
15
+ def initialize(teams: [])
16
+ @teams = []
17
+
18
+ teams.each { |team| register!(team) }
19
+
20
+ freeze
21
+ end
22
+
23
+ def to_s
24
+ teams.map(&:to_s).join("\n")
25
+ end
26
+
27
+ def sign!(player:, team:)
28
+ raise ArgumentError, 'player is required' unless player
29
+ raise ArgumentError, 'team is required' unless team
30
+ raise UnregisteredTeamError, "#{team} is not registered" unless registered?(team)
31
+ raise PlayerAlreadySignedError, "#{player} is already signed" if signed?(player)
32
+
33
+ team.sign!(player)
34
+
35
+ self
36
+ end
37
+
38
+ def signed?(player)
39
+ players.include?(player)
40
+ end
41
+
42
+ def players
43
+ teams.flat_map(&:players)
44
+ end
45
+
46
+ def not_registered?(team)
47
+ !registered?(team)
48
+ end
49
+
50
+ def registered?(team)
51
+ teams.include?(team)
52
+ end
53
+
54
+ def register!(team)
55
+ raise ArgumentError, 'team is required' unless team
56
+ raise TeamAlreadyRegisteredError, "#{team} already registered" if registered?(team)
57
+
58
+ team.players.each do |player|
59
+ raise PlayerAlreadySignedError, "#{player} already signed" if signed?(player)
60
+ end
61
+
62
+ teams << team
63
+
64
+ self
65
+ end
66
+
67
+ def ==(other)
68
+ teams == other.teams
69
+ end
70
+ alias eql? ==
71
+ end
72
+ end
73
+ end