basketball 0.0.7 → 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 -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
|