basketball 0.0.7 → 0.0.9
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.rubocop.yml +9 -19
- data/CHANGELOG.md +1 -31
- data/README.md +72 -91
- data/basketball.gemspec +3 -6
- data/exe/{basketball-schedule → 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/app/room_cli.rb +212 -0
- 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/{drafting → draft}/event.rb +4 -3
- data/lib/basketball/draft/front_office.rb +99 -0
- data/lib/basketball/draft/pick.rb +32 -0
- data/lib/basketball/draft/room.rb +221 -0
- data/lib/basketball/{drafting/player_search.rb → draft/scout.rb} +5 -10
- data/lib/basketball/draft/skip.rb +12 -0
- data/lib/basketball/draft.rb +16 -0
- 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/{drafting → 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 +90 -0
- data/lib/basketball/season/coordinator.rb +239 -0
- data/lib/basketball/{scheduling/preseason_game.rb → season/exhibition.rb} +3 -2
- data/lib/basketball/season/game.rb +37 -0
- data/lib/basketball/season/matchup.rb +27 -0
- data/lib/basketball/season/opponent.rb +15 -0
- data/lib/basketball/season/regular.rb +9 -0
- data/lib/basketball/season/result.rb +37 -0
- data/lib/basketball/season.rb +16 -0
- data/lib/basketball/value_object.rb +4 -1
- data/lib/basketball/version.rb +1 -1
- data/lib/basketball.rb +11 -6
- metadata +40 -52
- data/lib/basketball/drafting/cli.rb +0 -235
- data/lib/basketball/drafting/engine.rb +0 -221
- data/lib/basketball/drafting/engine_serializer.rb +0 -186
- data/lib/basketball/drafting/front_office.rb +0 -92
- data/lib/basketball/drafting/league.rb +0 -70
- data/lib/basketball/drafting/pick_event.rb +0 -25
- data/lib/basketball/drafting/player.rb +0 -43
- data/lib/basketball/drafting/roster.rb +0 -37
- data/lib/basketball/drafting/sim_event.rb +0 -23
- data/lib/basketball/drafting/skip_event.rb +0 -13
- data/lib/basketball/drafting.rb +0 -9
- data/lib/basketball/scheduling/calendar.rb +0 -121
- data/lib/basketball/scheduling/calendar_serializer.rb +0 -94
- data/lib/basketball/scheduling/cli.rb +0 -198
- data/lib/basketball/scheduling/conference.rb +0 -57
- data/lib/basketball/scheduling/coordinator.rb +0 -180
- data/lib/basketball/scheduling/division.rb +0 -43
- data/lib/basketball/scheduling/game.rb +0 -32
- data/lib/basketball/scheduling/league.rb +0 -114
- data/lib/basketball/scheduling/league_serializer.rb +0 -99
- data/lib/basketball/scheduling/season_game.rb +0 -8
- data/lib/basketball/scheduling/team.rb +0 -21
- data/lib/basketball/scheduling.rb +0 -17
@@ -0,0 +1,99 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Basketball
|
4
|
+
module Draft
|
5
|
+
# A team will send their front office to a draft room to make draft selections.
|
6
|
+
class FrontOffice < Entity
|
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
|
+
|
15
|
+
MAX_POSITIONS = 12
|
16
|
+
|
17
|
+
private_constant :DEFAULT_MAX_STAR_LEVEL, :DEFAULT_MAX_RISK_LEVEL, :MAX_POSITIONS
|
18
|
+
|
19
|
+
attr_reader :prioritized_positions, :risk_level, :star_level, :scout
|
20
|
+
|
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
|
+
)
|
27
|
+
super(id)
|
28
|
+
|
29
|
+
@risk_level = risk_level.to_i
|
30
|
+
@star_level = star_level.to_i
|
31
|
+
@prioritized_positions = prioritized_positions
|
32
|
+
@scout = Scout.new
|
33
|
+
|
34
|
+
# fill in the rest of the queue here
|
35
|
+
need_count = MAX_POSITIONS - @prioritized_positions.length
|
36
|
+
|
37
|
+
@prioritized_positions += random_positions_queue[0...need_count]
|
38
|
+
|
39
|
+
freeze
|
40
|
+
end
|
41
|
+
|
42
|
+
def pick(assessment)
|
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?
|
47
|
+
|
48
|
+
players[0..risk_level].sample
|
49
|
+
end
|
50
|
+
|
51
|
+
private
|
52
|
+
|
53
|
+
def adaptive_search(assessment)
|
54
|
+
drafted_positions = assessment.drafted_players.map(&:position)
|
55
|
+
|
56
|
+
scout.top_for(players: assessment.undrafted_players, exclude_positions: drafted_positions)
|
57
|
+
end
|
58
|
+
|
59
|
+
def balanced_search(assessment)
|
60
|
+
players = []
|
61
|
+
|
62
|
+
# Try to find best pick for exact desired position.
|
63
|
+
# If you cant find one, then move to the next desired position until the end of the queue
|
64
|
+
available_prioritized_positions(assessment.drafted_players).each do |position|
|
65
|
+
players = scout.top_for(players: assessment.undrafted_players, position:)
|
66
|
+
|
67
|
+
break if players.any?
|
68
|
+
end
|
69
|
+
|
70
|
+
players = players.any? ? players : scout.top_for
|
71
|
+
end
|
72
|
+
|
73
|
+
def all_random_positions
|
74
|
+
Org::Position::ALL_VALUES.to_a.shuffle.map { |v| Org::Position.new(v) }
|
75
|
+
end
|
76
|
+
|
77
|
+
def random_positions_queue
|
78
|
+
all_random_positions + all_random_positions + [Org::Position.random] + [Org::Position.random]
|
79
|
+
end
|
80
|
+
|
81
|
+
def available_prioritized_positions(drafted_players)
|
82
|
+
drafted_positions = drafted_players.map(&:position)
|
83
|
+
queue = prioritized_positions.dup
|
84
|
+
|
85
|
+
drafted_positions.each do |drafted_position|
|
86
|
+
index = queue.index(drafted_position)
|
87
|
+
|
88
|
+
next unless index
|
89
|
+
|
90
|
+
queue.delete_at(index)
|
91
|
+
|
92
|
+
queue << drafted_position
|
93
|
+
end
|
94
|
+
|
95
|
+
queue
|
96
|
+
end
|
97
|
+
end
|
98
|
+
end
|
99
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Basketball
|
4
|
+
module Draft
|
5
|
+
# Room event where a player is selected.
|
6
|
+
class Pick < Event
|
7
|
+
value_reader :player, :auto
|
8
|
+
|
9
|
+
def initialize(front_office:, player:, pick:, round:, round_pick:, auto: false)
|
10
|
+
super(front_office:, pick:, round:, round_pick:)
|
11
|
+
|
12
|
+
raise ArgumentError, 'player required' unless player
|
13
|
+
|
14
|
+
@player = player
|
15
|
+
@auto = auto
|
16
|
+
|
17
|
+
freeze
|
18
|
+
end
|
19
|
+
|
20
|
+
def to_s
|
21
|
+
"#{super} #{auto ? 'auto-' : ''}picked #{player}"
|
22
|
+
end
|
23
|
+
|
24
|
+
def ==(other)
|
25
|
+
super &&
|
26
|
+
player == other.player &&
|
27
|
+
auto == other.auto
|
28
|
+
end
|
29
|
+
alias eql? ==
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,221 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Basketball
|
4
|
+
module Draft
|
5
|
+
# Main pick-by-pick iterator object which will round-robin rotate team selections.
|
6
|
+
class Room
|
7
|
+
class AlreadyPickedError < StandardError; end
|
8
|
+
class EndOfDraftError < StandardError; end
|
9
|
+
class EventOutOfOrderError < StandardError; end
|
10
|
+
class FrontOfficeAlreadyRegisteredError < StandardError; end
|
11
|
+
class PlayerAlreadyAddedError < StandardError; end
|
12
|
+
class UnknownFrontOfficeError < StandardError; end
|
13
|
+
class UnknownPlayerError < StandardError; end
|
14
|
+
|
15
|
+
attr_reader :rounds, :players, :front_offices, :events, :id
|
16
|
+
|
17
|
+
def initialize(front_offices:, rounds:, players: [], events: [])
|
18
|
+
raise InvalidRoundsError, "#{rounds} should be a positive number" unless rounds.positive?
|
19
|
+
|
20
|
+
@rounds = rounds
|
21
|
+
@players = []
|
22
|
+
@front_offices = []
|
23
|
+
@events = []
|
24
|
+
|
25
|
+
front_offices.each { |front_office| register!(front_office) }
|
26
|
+
players.each { |player| add_player!(player) }
|
27
|
+
events.each { |event| add_event!(event) }
|
28
|
+
end
|
29
|
+
|
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)
|
37
|
+
|
38
|
+
drafted_players(front_office).each do |player|
|
39
|
+
league.sign!(player:, team:)
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
### Peek Methods
|
46
|
+
|
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
|
+
end
|
56
|
+
|
57
|
+
def total_picks
|
58
|
+
rounds * front_offices.length
|
59
|
+
end
|
60
|
+
|
61
|
+
def round
|
62
|
+
return if done?
|
63
|
+
|
64
|
+
(pick / front_offices.length.to_f).ceil
|
65
|
+
end
|
66
|
+
|
67
|
+
def round_pick
|
68
|
+
return if done?
|
69
|
+
|
70
|
+
mod = pick % front_offices.length
|
71
|
+
|
72
|
+
mod.positive? ? mod : front_offices.length
|
73
|
+
end
|
74
|
+
|
75
|
+
def front_office
|
76
|
+
return if done?
|
77
|
+
|
78
|
+
front_offices[round_pick - 1]
|
79
|
+
end
|
80
|
+
|
81
|
+
def pick
|
82
|
+
return if done?
|
83
|
+
|
84
|
+
internal_pick
|
85
|
+
end
|
86
|
+
|
87
|
+
def remaining_picks
|
88
|
+
total_picks - internal_pick + 1
|
89
|
+
end
|
90
|
+
|
91
|
+
def done?
|
92
|
+
internal_pick > total_picks
|
93
|
+
end
|
94
|
+
|
95
|
+
def not_done?
|
96
|
+
!done?
|
97
|
+
end
|
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
|
+
|
115
|
+
def skip!
|
116
|
+
return if done?
|
117
|
+
|
118
|
+
event = Skip.new(front_office:, pick:, round:, round_pick:)
|
119
|
+
|
120
|
+
add_event!(event)
|
121
|
+
|
122
|
+
event
|
123
|
+
end
|
124
|
+
|
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)
|
130
|
+
|
131
|
+
add_event!(event)
|
132
|
+
|
133
|
+
event
|
134
|
+
end
|
135
|
+
|
136
|
+
def sim_rest!
|
137
|
+
events = []
|
138
|
+
|
139
|
+
while not_done?
|
140
|
+
event = sim!
|
141
|
+
|
142
|
+
yield event if block_given?
|
143
|
+
|
144
|
+
events << event
|
145
|
+
end
|
146
|
+
|
147
|
+
events
|
148
|
+
end
|
149
|
+
|
150
|
+
def pick!(player)
|
151
|
+
return nil if done?
|
152
|
+
|
153
|
+
event = Pick.new(front_office:, pick:, round:, round_pick:, player:)
|
154
|
+
|
155
|
+
add_event!(event)
|
156
|
+
end
|
157
|
+
|
158
|
+
private
|
159
|
+
|
160
|
+
attr_writer :id
|
161
|
+
|
162
|
+
def player_events
|
163
|
+
events.select { |e| e.respond_to?(:player) }
|
164
|
+
end
|
165
|
+
|
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
|
180
|
+
end
|
181
|
+
# rubocop:enable Metrics/AbcSize
|
182
|
+
|
183
|
+
def assert_player(event)
|
184
|
+
return unless event.respond_to?(:player)
|
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)
|
188
|
+
end
|
189
|
+
|
190
|
+
def internal_pick
|
191
|
+
events.length + 1
|
192
|
+
end
|
193
|
+
|
194
|
+
def registered?(front_office)
|
195
|
+
front_offices.include?(front_office)
|
196
|
+
end
|
197
|
+
|
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)
|
201
|
+
|
202
|
+
front_offices << front_office
|
203
|
+
|
204
|
+
self
|
205
|
+
end
|
206
|
+
|
207
|
+
def player?(player)
|
208
|
+
players.include?(player)
|
209
|
+
end
|
210
|
+
|
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
|
218
|
+
end
|
219
|
+
end
|
220
|
+
end
|
221
|
+
end
|
@@ -1,15 +1,10 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
module Basketball
|
4
|
-
module
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
def initialize(players = [])
|
9
|
-
@players = players
|
10
|
-
end
|
11
|
-
|
12
|
-
def query(position: nil, exclude_positions: [])
|
4
|
+
module Draft
|
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
|
@@ -0,0 +1,16 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# Services
|
4
|
+
require_relative 'draft/scout'
|
5
|
+
|
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
|
@@ -0,0 +1,26 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Basketball
|
4
|
+
module Org
|
5
|
+
# Base class describing a player.
|
6
|
+
# A consumer application should extend these specific to their specific sports traits.
|
7
|
+
class Player < Entity
|
8
|
+
attr_reader :overall, :position
|
9
|
+
|
10
|
+
def initialize(id:, overall: 0, position: nil)
|
11
|
+
super(id)
|
12
|
+
|
13
|
+
raise ArgumentError, 'position is required' unless position
|
14
|
+
|
15
|
+
@overall = overall
|
16
|
+
@position = position
|
17
|
+
|
18
|
+
freeze
|
19
|
+
end
|
20
|
+
|
21
|
+
def to_s
|
22
|
+
"[#{super}] (#{position}) #{overall}".strip
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
@@ -1,7 +1,8 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
module Basketball
|
4
|
-
module
|
4
|
+
module Org
|
5
|
+
# Describes a player position.
|
5
6
|
class Position < ValueObject
|
6
7
|
extend Forwardable
|
7
8
|
|
@@ -17,7 +18,7 @@ module Basketball
|
|
17
18
|
FRONT_COURT_VALUES = %w[PF C].to_set.freeze
|
18
19
|
ALL_VALUES = (BACK_COURT_VALUES.to_a + FRONT_COURT_VALUES.to_a).to_set.freeze
|
19
20
|
|
20
|
-
|
21
|
+
value_reader :code
|
21
22
|
|
22
23
|
def_delegators :code, :to_s
|
23
24
|
|
@@ -0,0 +1,38 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Basketball
|
4
|
+
module Org
|
5
|
+
# Base class describing a team. A team here is bare metal and is just described by an ID
|
6
|
+
# and a collection of Player objects.
|
7
|
+
class Team < Entity
|
8
|
+
attr_reader :players
|
9
|
+
|
10
|
+
def initialize(id:, players: [])
|
11
|
+
super(id)
|
12
|
+
|
13
|
+
@players = []
|
14
|
+
|
15
|
+
players.each { |p| sign!(p) }
|
16
|
+
|
17
|
+
freeze
|
18
|
+
end
|
19
|
+
|
20
|
+
def to_s
|
21
|
+
([super.to_s] + players.map(&:to_s)).join("\n")
|
22
|
+
end
|
23
|
+
|
24
|
+
def signed?(player)
|
25
|
+
players.include?(player)
|
26
|
+
end
|
27
|
+
|
28
|
+
def sign!(player)
|
29
|
+
raise ArgumentError, 'player is required' unless player
|
30
|
+
raise PlayerAlreadySignedError, "#{player} already signed by #{self}" if signed?(player)
|
31
|
+
|
32
|
+
players << player
|
33
|
+
|
34
|
+
self
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
@@ -0,0 +1,12 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'org/player'
|
4
|
+
require_relative 'org/position'
|
5
|
+
require_relative 'org/team'
|
6
|
+
require_relative 'org/league'
|
7
|
+
|
8
|
+
module Basketball
|
9
|
+
module Org
|
10
|
+
class PlayerAlreadySignedError < StandardError; end
|
11
|
+
end
|
12
|
+
end
|