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.
- checksums.yaml +4 -4
- data/.rubocop.yml +9 -19
- data/CHANGELOG.md +1 -39
- data/README.md +72 -93
- data/basketball.gemspec +3 -6
- data/exe/{basketball-season-scheduling → 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/{draft/cli.rb → app/room_cli.rb} +53 -76
- 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/draft/event.rb +3 -2
- data/lib/basketball/draft/front_office.rb +35 -28
- data/lib/basketball/draft/{pick_event.rb → pick.rb} +13 -6
- data/lib/basketball/draft/room.rb +119 -119
- data/lib/basketball/draft/{player_search.rb → scout.rb} +4 -9
- data/lib/basketball/draft/skip.rb +12 -0
- data/lib/basketball/draft.rb +13 -6
- 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/{draft → 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 +41 -72
- data/lib/basketball/season/coordinator.rb +185 -126
- data/lib/basketball/season/{preseason_game.rb → exhibition.rb} +2 -1
- data/lib/basketball/season/game.rb +15 -10
- data/lib/basketball/season/matchup.rb +27 -0
- data/lib/basketball/season/opponent.rb +15 -0
- data/lib/basketball/season/{season_game.rb → regular.rb} +2 -1
- data/lib/basketball/season/result.rb +37 -0
- data/lib/basketball/season.rb +12 -13
- data/lib/basketball/value_object.rb +4 -1
- data/lib/basketball/version.rb +1 -1
- data/lib/basketball.rb +9 -4
- metadata +32 -44
- data/lib/basketball/draft/league.rb +0 -70
- data/lib/basketball/draft/player.rb +0 -43
- data/lib/basketball/draft/room_serializer.rb +0 -186
- data/lib/basketball/draft/roster.rb +0 -37
- data/lib/basketball/draft/sim_event.rb +0 -23
- data/lib/basketball/draft/skip_event.rb +0 -13
- data/lib/basketball/season/calendar_serializer.rb +0 -94
- data/lib/basketball/season/conference.rb +0 -57
- data/lib/basketball/season/division.rb +0 -43
- data/lib/basketball/season/league.rb +0 -114
- data/lib/basketball/season/league_serializer.rb +0 -99
- data/lib/basketball/season/scheduling_cli.rb +0 -198
- 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
|
-
|
7
|
-
|
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 :
|
17
|
+
private_constant :DEFAULT_MAX_STAR_LEVEL, :DEFAULT_MAX_RISK_LEVEL, :MAX_POSITIONS
|
11
18
|
|
12
|
-
attr_reader :prioritized_positions, :
|
19
|
+
attr_reader :prioritized_positions, :risk_level, :star_level, :scout
|
13
20
|
|
14
|
-
def initialize(
|
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
|
-
@
|
18
|
-
@
|
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(
|
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
|
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(
|
47
|
-
drafted_positions = drafted_players.map(&:position)
|
53
|
+
def adaptive_search(assessment)
|
54
|
+
drafted_positions = assessment.drafted_players.map(&:position)
|
48
55
|
|
49
|
-
|
56
|
+
scout.top_for(players: assessment.undrafted_players, exclude_positions: drafted_positions)
|
50
57
|
end
|
51
58
|
|
52
|
-
def balanced_search(
|
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
|
58
|
-
players =
|
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 :
|
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
|
-
|
8
|
-
|
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
|
-
"#{
|
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
|
8
|
+
class EndOfDraftError < StandardError; end
|
10
9
|
class EventOutOfOrderError < StandardError; end
|
11
|
-
class
|
10
|
+
class FrontOfficeAlreadyRegisteredError < StandardError; end
|
11
|
+
class PlayerAlreadyAddedError < StandardError; end
|
12
12
|
class UnknownFrontOfficeError < StandardError; end
|
13
|
-
class
|
14
|
-
|
15
|
-
DEFAULT_ROUNDS = 12
|
16
|
-
|
17
|
-
private_constant :DEFAULT_ROUNDS
|
13
|
+
class UnknownPlayerError < StandardError; end
|
18
14
|
|
19
|
-
attr_reader :events, :
|
15
|
+
attr_reader :rounds, :players, :front_offices, :events, :id
|
20
16
|
|
21
|
-
def initialize(
|
22
|
-
|
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
|
-
|
28
|
-
|
20
|
+
@rounds = rounds
|
21
|
+
@players = []
|
22
|
+
@front_offices = []
|
23
|
+
@events = []
|
29
24
|
|
30
|
-
|
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
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
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
|
-
|
40
|
-
|
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
|
-
|
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
|
54
|
-
|
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
|
61
|
+
def round
|
62
62
|
return if done?
|
63
63
|
|
64
|
-
(
|
64
|
+
(pick / front_offices.length.to_f).ceil
|
65
65
|
end
|
66
66
|
|
67
|
-
def
|
67
|
+
def round_pick
|
68
68
|
return if done?
|
69
69
|
|
70
|
-
mod =
|
70
|
+
mod = pick % front_offices.length
|
71
71
|
|
72
72
|
mod.positive? ? mod : front_offices.length
|
73
73
|
end
|
74
74
|
|
75
|
-
def
|
75
|
+
def front_office
|
76
76
|
return if done?
|
77
77
|
|
78
|
-
front_offices[
|
78
|
+
front_offices[round_pick - 1]
|
79
79
|
end
|
80
80
|
|
81
|
-
def
|
81
|
+
def pick
|
82
82
|
return if done?
|
83
83
|
|
84
|
-
|
84
|
+
internal_pick
|
85
85
|
end
|
86
86
|
|
87
87
|
def remaining_picks
|
88
|
-
total_picks -
|
88
|
+
total_picks - internal_pick + 1
|
89
89
|
end
|
90
90
|
|
91
91
|
def done?
|
92
|
-
|
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 =
|
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
|
-
|
120
|
+
add_event!(event)
|
110
121
|
|
111
122
|
event
|
112
123
|
end
|
113
124
|
|
114
|
-
def sim!
|
115
|
-
|
116
|
-
|
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
|
-
|
119
|
-
front_office = current_front_office
|
131
|
+
add_event!(event)
|
120
132
|
|
121
|
-
|
122
|
-
|
123
|
-
drafted_players: drafted_players(front_office),
|
124
|
-
round: current_round
|
125
|
-
)
|
133
|
+
event
|
134
|
+
end
|
126
135
|
|
127
|
-
|
128
|
-
|
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
|
-
|
139
|
+
while not_done?
|
140
|
+
event = sim!
|
136
141
|
|
137
|
-
yield
|
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 =
|
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
|
-
|
165
|
-
PlayerSearch.new(undrafted_players)
|
155
|
+
add_event!(event)
|
166
156
|
end
|
167
157
|
|
168
158
|
private
|
169
159
|
|
170
|
-
|
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
|
-
|
177
|
-
|
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
|
181
|
-
|
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
|
-
|
185
|
-
|
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
|
-
|
189
|
-
|
190
|
-
|
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
|
-
|
197
|
-
|
198
|
-
|
194
|
+
def registered?(front_office)
|
195
|
+
front_offices.include?(front_office)
|
196
|
+
end
|
199
197
|
|
200
|
-
|
201
|
-
|
202
|
-
|
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
|
-
|
202
|
+
front_offices << front_office
|
205
203
|
|
206
|
-
|
207
|
-
|
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
|
-
|
207
|
+
def player?(player)
|
208
|
+
players.include?(player)
|
209
|
+
end
|
213
210
|
|
214
|
-
|
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
|
-
|
6
|
-
|
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
|
22
|
+
filtered_players.sort_by { |p| [p.overall, p.id] }.reverse
|
28
23
|
end
|
29
24
|
end
|
30
25
|
end
|
data/lib/basketball/draft.rb
CHANGED
@@ -1,9 +1,16 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
|
3
|
+
# Services
|
4
|
+
require_relative 'draft/scout'
|
4
5
|
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
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'
|
data/lib/basketball/entity.rb
CHANGED
@@ -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
|
17
|
+
@id = id
|
16
18
|
end
|
17
19
|
|
18
20
|
def <=>(other)
|
19
|
-
|
21
|
+
comparable_id <=> other.comparable_id
|
20
22
|
end
|
21
23
|
|
22
24
|
def ==(other)
|
23
|
-
|
25
|
+
comparable_id == other.comparable_id
|
24
26
|
end
|
25
27
|
alias eql? ==
|
26
28
|
|
27
29
|
def hash
|
28
|
-
|
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
|