basketball 0.0.8 → 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.
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