basketball 0.0.3 → 0.0.5

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 29805ef216312c25dee77d7f7c2a82bf220287404e9e241d24e16dddfb895fcc
4
- data.tar.gz: 3d8a0038acf62e4e1aa230299cc1b2d7d777596bc133b159b1e661639c75c76d
3
+ metadata.gz: 793bfe7afef52aa72d3f89c94541fc7d8f24b350d63cb7c75d7215c285695b74
4
+ data.tar.gz: 46a3ac7199fb534ff4827c8fe48bbe371c7f87bc623e579ffdce2310132a1263
5
5
  SHA512:
6
- metadata.gz: ae3baac2215dbfee6b9cf70a44b2bdf9257d7dade811c6a30bf37b22f840bd85621f6441d137e07f38b615210092d0cede011a2e9f4655aa1e05c6281411a856
7
- data.tar.gz: 4bcb67de94178f2945376faa2cb57c5f5884d60b24479d73e12d1145c7c20c1b9f3dd547f321c578f39d43868fb2f8d158859020b14d7e436fbface8a89b2809
6
+ metadata.gz: cf8c46719f3669a31c932d7dc2742c287666fcb305c90c596c87b79c37d4fb95aafa446c70575ed4d8d3750a3f3672646e9ee49c6a0cf95e4dd7eb092831670e
7
+ data.tar.gz: db01d3bfc4a8c1448875c5a099d0a33fd60ad60471ae5cda5f9eeeaf8a1fb2daa3e52d5ca2d8138867e42aaf6f54d259cdad7d3628cfbaa2d7bcf134fe5dc4b9
data/CHANGELOG.md CHANGED
@@ -1,7 +1,16 @@
1
+ #### 0.0.5 - May 5th, 2023
2
+
3
+ * Remove the notion of Team in favor of a flat front office.
4
+ #### 0.0.4 - May 5th, 2023
5
+
6
+ * Add ability to skip draft picks using `Basketball::Drafting::Engine#skip!`
7
+ * Add ability to output event full drafting event log using CLI: `basketball-draft -i tmp/draft-wip.json -l`
8
+ * Add ability to skip draft picks using CLI: `basketball-draft -i tmp/draft-wip.json -x 1`
9
+
1
10
  #### 0.0.3 - May 5th, 2023
2
11
 
3
- * Drafting::Engine#sim! should return events
4
- * Added Drafting::Engine#undrafted_player_search
12
+ * `Drafting::Engine#sim!` should return events
13
+ * Added `Drafting::Engine#undrafted_player_search`
5
14
 
6
15
  #### 0.0.2 - May 4th, 2023
7
16
 
data/CODE_OF_CONDUCT.md CHANGED
@@ -56,7 +56,7 @@ further defined and clarified by project maintainers.
56
56
 
57
57
  Instances of abusive, harassing, or otherwise unacceptable behavior may be
58
58
  reported. All complaints will be reviewed and investigated and will result in a response that
59
- is deemed necessary and appropriate to the circumstances. The project team is
59
+ is deemed necessary and appropriate to the circumstances. The project front_office is
60
60
  obligated to maintain confidentiality with regard to the reporter of an incident.
61
61
  Further details of specific enforcement policies may be posted separately.
62
62
 
data/README.md CHANGED
@@ -26,7 +26,7 @@ Install executable scripts:
26
26
  bundle binstubs basketball
27
27
  ````
28
28
 
29
- ## Main Modules
29
+ ## Sub-Modules
30
30
 
31
31
  This library is broken down into several bounded contexts that can be consumed either via its Ruby API's or CLI through provided executable scripts:
32
32
 
@@ -38,6 +38,21 @@ The drafting module is responsible for providing a turn-based iterator allowing
38
38
 
39
39
  ![Basketball Architecture - Drafting.png](/docs/architecture/drafting.png)
40
40
 
41
+ Element | Description
42
+ :------------ | :-----------
43
+ **Drafting** | Bounded context (sub-module) dealing with executing an asynchronous draft.
44
+ **Engine** | Aggregate root responsible for providing an iterable interface capable of executing a draft, pick by pick.
45
+ **Event** | Represents one cycle execution result from the Engine.
46
+ **External Ruby App** | An example consumer for the Drafting context.
47
+ **Front Office** | Identifiable as a team, contains configuration for how to auto-pick draft selections.
48
+ **League** | Set of rosters that together form a cohesive league.
49
+ **Pick Event** | Result event emitted when a player is manually selected.
50
+ **Playe ** | Identitiable as a person able to be drafted.
51
+ **Position** | Value object based on position code: PG, SG, SF, PF, and C.
52
+ **Roster** | Identifiable as a team, set of players that make up a single team.
53
+ **Sim Event** | Result event emitted when a player is automatically selected by a front office.
54
+ **Skip Event** | Result event emitted when a front office decides to skip a round.
55
+
41
56
  ### The Drafting CLI
42
57
 
43
58
  The drafting module is meant to be interfaces using its Ruby API by consuming applications. It also ships with a CLI which a user can interact with to emulate "the draft process". Technically speaking, the CLI provides an example application built on top of the Drafting module. Each time a CLI command is executed, its results will be resaved, so the output file can then be used as the next command's input file to string together commands. The following sections are example CLI interactions:
@@ -45,43 +60,55 @@ The drafting module is meant to be interfaces using its Ruby API by consuming ap
45
60
  #### Generate a Fresh Draft
46
61
 
47
62
  ```zsh
48
- exe/basketball-draft -o tmp/draft.json
63
+ basketball-draft -o tmp/draft.json
49
64
  ```
50
65
 
51
66
  #### N Top Available Players
52
67
 
53
68
  ```zsh
54
- exe/basketball-draft -i tmp/draft.json -t 10
69
+ basketball-draft -i tmp/draft.json -t 10
55
70
  ```
56
71
 
57
72
  #### N Top Available Players for a Position
58
73
 
59
74
  ```zsh
60
- exe/basketball-draft -i tmp/draft.json -t 10 -q PG
75
+ basketball-draft -i tmp/draft.json -t 10 -q PG
61
76
  ```
62
77
 
63
78
  #### Output Current Rosters
64
79
 
65
80
  ```zsh
66
- exe/basketball-draft -i tmp/draft.json -r
81
+ basketball-draft -i tmp/draft.json -r
82
+ ```
83
+
84
+ #### Output Event Log
85
+
86
+ ```zsh
87
+ basketball-draft -i tmp/draft.json -l
67
88
  ```
68
89
 
69
90
  #### Simulate N picks
70
91
 
71
92
  ```zsh
72
- exe/basketball-draft -i tmp/draft.json -s 10
93
+ basketball-draft -i tmp/draft.json -s 10
94
+ ```
95
+
96
+ #### Skip N picks
97
+
98
+ ```zsh
99
+ basketball-draft -i tmp/draft.json -x 10
73
100
  ```
74
101
 
75
102
  #### Pick Players
76
103
 
77
104
  ```zsh
78
- exe/basketball-draft -i tmp/draft.json -p P-100,P-200,P-300
105
+ basketball-draft -i tmp/draft.json -p P-100,P-200,P-300
79
106
  ```
80
107
 
81
108
  #### Simulate the Rest of the Draft
82
109
 
83
110
  ```zsh
84
- exe/basketball-draft -i tmp/draft.json -a
111
+ basketball-draft -i tmp/draft.json -a
85
112
  ```
86
113
 
87
114
  ## Contributing
@@ -9,12 +9,16 @@ module Basketball
9
9
  module Drafting
10
10
  # Example:
11
11
  # exe/basketball-draft -o tmp/draft.json
12
- # exe/basketball-draft -i tmp/draft.json -o tmp/draft-wip.json -s 28 -p ONEALSH01,ONEALJE01 -t 10 -q PG
12
+ # exe/basketball-draft -i tmp/draft.json -o tmp/draft-wip.json -s 26 -p P-5,P-10 -t 10 -q PG
13
+ # exe/basketball-draft -i tmp/draft-wip.json -x 2
13
14
  # exe/basketball-draft -i tmp/draft-wip.json -r -t 10
14
15
  # exe/basketball-draft -i tmp/draft-wip.json -t 10 -q SG
15
16
  # exe/basketball-draft -i tmp/draft-wip.json -s 30 -t 10
16
17
  # exe/basketball-draft -i tmp/draft-wip.json -a -r
18
+ # exe/basketball-draft -i tmp/draft-wip.json -l
17
19
  class CLI
20
+ class PlayerNotFound < StandardError; end
21
+
18
22
  attr_reader :opts, :serializer, :io
19
23
 
20
24
  def initialize(args:, io: $stdout)
@@ -42,13 +46,19 @@ module Basketball
42
46
  if engine.done?
43
47
  io.puts('Draft is complete!')
44
48
  else
49
+ current_round = engine.current_round
50
+ current_round_pick = engine.current_round_pick
51
+ current_front_office = engine.current_front_office
52
+
45
53
  io.puts("#{engine.remaining_picks} Remaining pick(s)")
46
- io.puts("Round #{engine.current_round} pick #{engine.current_round_pick} for #{engine.current_team}")
54
+ io.puts("Up Next: Round #{current_round} pick #{current_round_pick} for #{current_front_office}")
47
55
  end
48
56
 
49
57
  write(engine)
50
58
 
51
- rosters(engine)
59
+ log(engine)
60
+
61
+ league(engine)
52
62
 
53
63
  query(engine)
54
64
 
@@ -69,7 +79,9 @@ module Basketball
69
79
  o.array '-p', '--picks', 'Comma-separated list of ordered player IDs to pick.', delimiter: ','
70
80
  o.integer '-t', '--top', 'Output the top rated available players (default is 0).', default: 0
71
81
  o.string '-q', '--query', "Filter TOP by position: #{Position::ALL_VALUES.join(', ')}."
72
- o.bool '-r', '--rosters', 'Output all team rosters.', default: false
82
+ o.bool '-r', '--rosters', 'Output all front_office rosters.', default: false
83
+ o.integer '-x', '--skip', 'Number of picks to skip (default is 0).', default: 0
84
+ o.bool '-l', '--log', 'Output event log.', default: false
73
85
 
74
86
  o.on '-h', '--help', 'Print out help, like this is doing right now.' do
75
87
  io.puts(o)
@@ -80,7 +92,7 @@ module Basketball
80
92
 
81
93
  def load_engine
82
94
  if opts[:input].to_s.empty?
83
- io.puts('Input path was not provided, generating fresh teams and players')
95
+ io.puts('Input path was not provided, generating fresh front_offices and players')
84
96
 
85
97
  generate_draft
86
98
  else
@@ -91,8 +103,8 @@ module Basketball
91
103
  end
92
104
 
93
105
  def generate_draft
94
- teams = 30.times.map do |i|
95
- Team.new(
106
+ front_offices = 30.times.map do |i|
107
+ FrontOffice.new(
96
108
  id: "T-#{i + 1}", name: Faker::Team.name
97
109
  )
98
110
  end
@@ -107,18 +119,23 @@ module Basketball
107
119
  )
108
120
  end
109
121
 
110
- Engine.new(players:, teams:)
122
+ Engine.new(players:, front_offices:)
111
123
  end
112
124
 
113
- def rosters(engine)
125
+ def league(engine)
114
126
  return unless opts[:rosters]
115
127
 
116
128
  io.puts
117
- io.puts('Rosters')
129
+ io.puts(engine.to_league)
130
+ end
131
+
132
+ def log(engine)
133
+ return unless opts[:log]
134
+
135
+ io.puts
136
+ io.puts('Event Log')
118
137
 
119
- engine.rosters.each do |roster|
120
- io.puts(roster)
121
- end
138
+ puts engine.events
122
139
  end
123
140
 
124
141
  # rubocop:disable Metrics/AbcSize
@@ -161,6 +178,8 @@ module Basketball
161
178
 
162
179
  player = engine.players.find { |p| p.id == id.to_s.upcase }
163
180
 
181
+ raise PlayerNotFound, "player not found by id: #{id}" unless player
182
+
164
183
  event = engine.pick!(player)
165
184
 
166
185
  io.puts(event)
@@ -168,6 +187,14 @@ module Basketball
168
187
  event_count += 1
169
188
  end
170
189
 
190
+ opts[:skip].times do
191
+ event = engine.skip!
192
+
193
+ io.puts(event)
194
+
195
+ event_count += 1
196
+ end
197
+
171
198
  engine.sim!(opts[:simulate]) do |event|
172
199
  io.puts(event)
173
200
 
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative 'roster'
3
+ require_relative 'league'
4
4
 
5
5
  module Basketball
6
6
  module Drafting
@@ -9,7 +9,7 @@ module Basketball
9
9
  class DupeEventError < StandardError; end
10
10
  class EventOutOfOrderError < StandardError; end
11
11
  class UnknownPlayerError < StandardError; end
12
- class UnknownTeamError < StandardError; end
12
+ class UnknownFrontOfficeError < StandardError; end
13
13
  class EndOfDraftError < StandardError; end
14
14
 
15
15
  DEFAULT_ROUNDS = 12
@@ -18,11 +18,11 @@ module Basketball
18
18
 
19
19
  attr_reader :events, :rounds
20
20
 
21
- def initialize(players: [], teams: [], events: [], rounds: DEFAULT_ROUNDS)
22
- @players_by_id = players.to_h { |p| [p.id, p] }
23
- @teams_by_id = teams.to_h { |t| [t.id, t] }
24
- @events = []
25
- @rounds = rounds.to_i
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
26
26
 
27
27
  # Each one will be validated for correctness.
28
28
  events.each { |e| play!(e) }
@@ -30,15 +30,15 @@ module Basketball
30
30
  freeze
31
31
  end
32
32
 
33
- def rosters
34
- events_by_team = teams.to_h { |t| [t, []] }
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
35
38
 
36
- events.each do |event|
37
- events_by_team.fetch(event.team) << event
38
- end
39
-
40
- events_by_team.map do |team, events|
41
- Roster.new(team:, events:)
39
+ undrafted_players.each do |player|
40
+ league.register!(player:)
41
+ end
42
42
  end
43
43
  end
44
44
 
@@ -46,8 +46,8 @@ module Basketball
46
46
  events.join("\n")
47
47
  end
48
48
 
49
- def teams
50
- teams_by_id.values
49
+ def front_offices
50
+ front_offices_by_id.values
51
51
  end
52
52
 
53
53
  def players
@@ -55,27 +55,27 @@ module Basketball
55
55
  end
56
56
 
57
57
  def total_picks
58
- rounds * teams.length
58
+ rounds * front_offices.length
59
59
  end
60
60
 
61
61
  def current_round
62
62
  return if done?
63
63
 
64
- (current_pick / teams.length.to_f).ceil
64
+ (current_pick / front_offices.length.to_f).ceil
65
65
  end
66
66
 
67
67
  def current_round_pick
68
68
  return if done?
69
69
 
70
- mod = current_pick % teams.length
70
+ mod = current_pick % front_offices.length
71
71
 
72
- mod.positive? ? mod : teams.length
72
+ mod.positive? ? mod : front_offices.length
73
73
  end
74
74
 
75
- def current_team
75
+ def current_front_office
76
76
  return if done?
77
77
 
78
- teams[current_round_pick - 1]
78
+ front_offices[current_round_pick - 1]
79
79
  end
80
80
 
81
81
  def current_pick
@@ -96,22 +96,38 @@ module Basketball
96
96
  !done?
97
97
  end
98
98
 
99
+ def skip!
100
+ return if done?
101
+
102
+ event = SkipEvent.new(
103
+ id: SecureRandom.uuid,
104
+ front_office: current_front_office,
105
+ pick: current_pick,
106
+ round: current_round,
107
+ round_pick: current_round_pick
108
+ )
109
+
110
+ play!(event)
111
+
112
+ event
113
+ end
114
+
99
115
  def sim!(times = nil)
100
116
  counter = 0
101
117
  events = []
102
118
 
103
119
  until done? || (times && counter >= times)
104
- team = current_team
120
+ front_office = current_front_office
105
121
 
106
- player = team.pick(
122
+ player = front_office.pick(
107
123
  undrafted_player_search:,
108
- drafted_players: drafted_players(team),
124
+ drafted_players: drafted_players(front_office),
109
125
  round: current_round
110
126
  )
111
127
 
112
128
  event = SimEvent.new(
113
129
  id: SecureRandom.uuid,
114
- team:,
130
+ front_office:,
115
131
  player:,
116
132
  pick: current_pick,
117
133
  round: current_round,
@@ -134,7 +150,7 @@ module Basketball
134
150
 
135
151
  event = PickEvent.new(
136
152
  id: SecureRandom.uuid,
137
- team: current_team,
153
+ front_office: current_front_office,
138
154
  player:,
139
155
  pick: current_pick,
140
156
  round: current_round,
@@ -154,15 +170,19 @@ module Basketball
154
170
 
155
171
  private
156
172
 
157
- attr_reader :players_by_id, :teams_by_id
173
+ attr_reader :players_by_id, :front_offices_by_id
174
+
175
+ def player_events
176
+ events.select { |e| e.respond_to?(:player) }
177
+ end
158
178
 
159
179
  def internal_current_pick
160
180
  events.length + 1
161
181
  end
162
182
 
163
- def drafted_players(team = nil)
164
- events.each_with_object([]) do |e, memo|
165
- next unless team.nil? || e.team == team
183
+ def drafted_players(front_office = nil)
184
+ player_events.each_with_object([]) do |e, memo|
185
+ next unless front_office.nil? || e.front_office == front_office
166
186
 
167
187
  memo << e.player
168
188
  end
@@ -172,15 +192,25 @@ module Basketball
172
192
  # rubocop:disable Metrics/CyclomaticComplexity
173
193
  # rubocop:disable Metrics/PerceivedComplexity
174
194
  def play!(event)
175
- raise AlreadyPickedError, "#{player} was already picked" if drafted_players.include?(event.player)
176
- raise DupeEventError, "#{event} is a dupe" if events.include?(event)
177
- raise EventOutOfOrder, "#{event} team cant pick right now" if event.team != current_team
178
- raise EventOutOfOrder, "#{event} has wrong pick" if event.pick != current_pick
179
- raise EventOutOfOrder, "#{event} has wrong round" if event.round != current_round
180
- raise EventOutOfOrder, "#{event} has wrong round_pick" if event.round_pick != current_round_pick
181
- raise UnknownTeamError, "#{team} doesnt exist" unless teams.include?(event.team)
182
- raise UnknownPlayerError, "#{player} doesnt exist" unless players.include?(event.player)
183
- raise EndOfDraftError, "#{total_picks} pick limit reached" if events.length > total_picks + 1
195
+ if event.respond_to?(:player) && drafted_players.include?(event.player)
196
+ raise AlreadyPickedError, "#{player} was already picked"
197
+ end
198
+
199
+ if event.respond_to?(:player) && !players.include?(event.player)
200
+ raise UnknownPlayerError, "#{event.player} doesnt exist"
201
+ end
202
+
203
+ if event.front_office != current_front_office
204
+ raise EventOutOfOrder, "#{event} #{event.front_office} cant pick right now"
205
+ end
206
+
207
+ raise UnknownFrontOfficeError, "#{front_office} doesnt exist" unless front_offices.include?(event.front_office)
208
+
209
+ raise DupeEventError, "#{event} is a dupe" if events.include?(event)
210
+ raise EventOutOfOrder, "#{event} has wrong pick" if event.pick != current_pick
211
+ raise EventOutOfOrder, "#{event} has wrong round" if event.round != current_round
212
+ raise EventOutOfOrder, "#{event} has wrong round_pick" if event.round_pick != current_round_pick
213
+ raise EndOfDraftError, "#{total_picks} pick limit reached" if events.length > total_picks + 1
184
214
 
185
215
  events << event
186
216
 
@@ -2,29 +2,30 @@
2
2
 
3
3
  require_relative 'front_office'
4
4
  require_relative 'player'
5
- require_relative 'team'
6
5
  require_relative 'pick_event'
7
6
  require_relative 'sim_event'
7
+ require_relative 'skip_event'
8
8
 
9
9
  module Basketball
10
10
  module Drafting
11
11
  class EngineSerializer
12
12
  EVENT_CLASSES = {
13
13
  'PickEvent' => PickEvent,
14
- 'SimEvent' => SimEvent
14
+ 'SimEvent' => SimEvent,
15
+ 'SkipEvent' => SkipEvent
15
16
  }.freeze
16
17
 
17
18
  private_constant :EVENT_CLASSES
18
19
 
19
20
  def deserialize(string)
20
- json = JSON.parse(string, symbolize_names: true)
21
- teams = deserialize_teams(json)
22
- players = deserialize_players(json)
23
- events = deserialize_events(json, players, teams)
21
+ json = JSON.parse(string, symbolize_names: true)
22
+ front_offices = deserialize_front_offices(json)
23
+ players = deserialize_players(json)
24
+ events = deserialize_events(json, players, front_offices)
24
25
 
25
26
  engine_opts = {
26
27
  players:,
27
- teams:,
28
+ front_offices:,
28
29
  events:
29
30
  }
30
31
 
@@ -37,7 +38,7 @@ module Basketball
37
38
  {
38
39
  info: serialize_info(engine),
39
40
  engine: serialize_engine(engine),
40
- rosters: serialize_rosters(engine)
41
+ league: serialize_league(engine)
41
42
  }.to_json
42
43
  end
43
44
 
@@ -46,7 +47,7 @@ module Basketball
46
47
  def serialize_engine(engine)
47
48
  {
48
49
  rounds: engine.rounds,
49
- teams: serialize_teams(engine),
50
+ front_offices: serialize_front_offices(engine),
50
51
  players: serialize_players(engine),
51
52
  events: serialize_events(engine.events)
52
53
  }
@@ -57,37 +58,40 @@ module Basketball
57
58
  total_picks: engine.total_picks,
58
59
  current_round: engine.current_round,
59
60
  current_round_pick: engine.current_round_pick,
60
- current_team: engine.current_team&.id,
61
+ current_front_office: engine.current_front_office&.id,
61
62
  current_pick: engine.current_pick,
62
63
  remaining_picks: engine.remaining_picks,
63
- done: engine.done?,
64
- undrafted_players: engine.undrafted_players.map(&:id)
64
+ done: engine.done?
65
65
  }
66
66
  end
67
67
 
68
- def serialize_rosters(engine)
69
- engine.rosters.to_h do |roster|
68
+ def serialize_league(engine)
69
+ league = engine.to_league
70
+
71
+ rosters = league.rosters.to_h do |roster|
70
72
  [
71
73
  roster.id,
72
74
  {
73
- events: roster.events.map(&:id),
74
- players: roster.events.map { |event| event.player.id }
75
+ players: roster.players.map(&:id)
75
76
  }
76
77
  ]
77
78
  end
79
+
80
+ {
81
+ free_agents: league.free_agents.map(&:id),
82
+ rosters:
83
+ }
78
84
  end
79
85
 
80
- def serialize_teams(engine)
81
- engine.teams.to_h do |team|
86
+ def serialize_front_offices(engine)
87
+ engine.front_offices.to_h do |front_office|
82
88
  [
83
- team.id,
89
+ front_office.id,
84
90
  {
85
- name: team.name,
86
- front_office: {
87
- fuzz: team.front_office.fuzz,
88
- depth: team.front_office.depth,
89
- prioritized_positions: team.front_office.prioritized_positions
90
- }
91
+ name: front_office.name,
92
+ fuzz: front_office.fuzz,
93
+ depth: front_office.depth,
94
+ prioritized_positions: front_office.prioritized_positions.map(&:code)
91
95
  }
92
96
  ]
93
97
  end
@@ -112,39 +116,31 @@ module Basketball
112
116
  {
113
117
  type: event.class.name.split('::').last,
114
118
  id: event.id,
115
- player: event.player.id,
116
- team: event.team.id,
119
+ front_office: event.front_office.id,
117
120
  pick: event.pick,
118
121
  round: event.round,
119
122
  round_pick: event.round_pick
120
- }
123
+ }.tap do |hash|
124
+ hash[:player] = event.player.id if event.respond_to?(:player)
125
+ end
121
126
  end
122
127
  end
123
128
 
124
- def deserialize_teams(json)
125
- (json.dig(:engine, :teams) || []).map do |id, team_hash|
126
- team_opts = {
129
+ def deserialize_front_offices(json)
130
+ (json.dig(:engine, :front_offices) || []).map do |id, front_office_hash|
131
+ prioritized_positions = (front_office_hash[:prioritized_positions] || []).map do |v|
132
+ Position.new(v)
133
+ end
134
+
135
+ front_office_opts = {
127
136
  id:,
128
- name: team_hash[:name]
137
+ name: front_office_hash[:name],
138
+ prioritized_positions:,
139
+ fuzz: front_office_hash[:fuzz],
140
+ depth: front_office_hash[:depth]
129
141
  }
130
142
 
131
- if team_hash.key?(:front_office)
132
- front_office_hash = team_hash[:front_office] || {}
133
-
134
- prioritized_positions = (front_office_hash[:prioritized_positions] || []).map do |v|
135
- Position.new(v)
136
- end
137
-
138
- front_office_opts = {
139
- prioritized_positions:,
140
- fuzz: front_office_hash[:fuzz],
141
- depth: front_office_hash[:depth]
142
- }
143
-
144
- team_opts[:front_office] = FrontOffice.new(**front_office_opts)
145
- end
146
-
147
- Team.new(**team_opts)
143
+ FrontOffice.new(**front_office_opts)
148
144
  end
149
145
  end
150
146
 
@@ -159,15 +155,18 @@ module Basketball
159
155
  end
160
156
  end
161
157
 
162
- def deserialize_events(json, players, teams)
158
+ def deserialize_events(json, players, front_offices)
163
159
  (json.dig(:engine, :events) || []).map do |event_hash|
164
160
  event_opts = event_hash.slice(:id, :pick, :round, :round_pick).merge(
165
- player: players.find { |p| p.id == event_hash[:player] },
166
- team: teams.find { |t| t.id == event_hash[:team] }
161
+ front_office: front_offices.find { |t| t.id == event_hash[:front_office] }
167
162
  )
168
163
 
169
164
  class_constant = EVENT_CLASSES.fetch(event_hash[:type])
170
165
 
166
+ if [PickEvent, SimEvent].include?(class_constant)
167
+ event_opts[:player] = players.find { |p| p.id == event_hash[:player] }
168
+ end
169
+
171
170
  class_constant.new(**event_opts)
172
171
  end
173
172
  end
@@ -3,21 +3,21 @@
3
3
  module Basketball
4
4
  module Drafting
5
5
  class Event < Entity
6
- attr_reader :pick, :round, :round_pick, :team
6
+ attr_reader :pick, :round, :round_pick, :front_office
7
7
 
8
- def initialize(id:, team:, pick:, round:, round_pick:)
8
+ def initialize(id:, front_office:, pick:, round:, round_pick:)
9
9
  super(id)
10
10
 
11
- raise ArgumentError, 'team required' unless team
11
+ raise ArgumentError, 'front_office required' unless front_office
12
12
 
13
- @team = team
14
- @pick = pick.to_i
15
- @round = round.to_i
16
- @round_pick = round_pick.to_i
13
+ @front_office = front_office
14
+ @pick = pick.to_i
15
+ @round = round.to_i
16
+ @round_pick = round_pick.to_i
17
17
  end
18
18
 
19
19
  def to_s
20
- "##{pick} overall (R#{round}:P#{round_pick}) by #{team}"
20
+ "##{pick} overall (R#{round}:P#{round_pick}) by #{front_office}"
21
21
  end
22
22
  end
23
23
  end
@@ -2,18 +2,19 @@
2
2
 
3
3
  module Basketball
4
4
  module Drafting
5
- class FrontOffice < ValueObject
5
+ class FrontOffice < Entity
6
6
  MAX_DEPTH = 3
7
7
  MAX_FUZZ = 2
8
8
  MAX_POSITIONS = 12
9
9
 
10
10
  private_constant :MAX_DEPTH, :MAX_FUZZ, :MAX_POSITIONS
11
11
 
12
- attr_reader_value :prioritized_positions, :fuzz, :depth
12
+ attr_reader :prioritized_positions, :fuzz, :depth, :name
13
13
 
14
- def initialize(prioritized_positions: [], fuzz: rand(0..MAX_FUZZ), depth: rand(0..MAX_DEPTH))
15
- super()
14
+ def initialize(id:, name: '', prioritized_positions: [], fuzz: rand(0..MAX_FUZZ), depth: rand(0..MAX_DEPTH))
15
+ super(id)
16
16
 
17
+ @name = name
17
18
  @fuzz = fuzz.to_i
18
19
  @depth = depth.to_i
19
20
  @prioritized_positions = prioritized_positions
@@ -36,6 +37,10 @@ module Basketball
36
37
  players[0..fuzz].sample
37
38
  end
38
39
 
40
+ def to_s
41
+ "[#{super}] #{name}"
42
+ end
43
+
39
44
  private
40
45
 
41
46
  def adaptive_search(undrafted_player_search:, drafted_players:)
@@ -0,0 +1,71 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'roster'
4
+
5
+ module Basketball
6
+ module Drafting
7
+ class League
8
+ class PlayerAlreadyRegisteredError < StandardError; end
9
+ class RosterNotFoundError < StandardError; end
10
+ class RosterAlreadyAddedError < StandardError; end
11
+
12
+ attr_reader :free_agents, :rosters
13
+
14
+ def initialize(free_agents: [], front_offices: [])
15
+ @rosters = []
16
+ @free_agents = []
17
+
18
+ front_offices.each { |front_office| add_roster(front_office) }
19
+ free_agents.each { |p| register!(player: p) }
20
+
21
+ freeze
22
+ end
23
+
24
+ def roster(front_office)
25
+ rosters.find { |r| r == front_office }
26
+ end
27
+
28
+ def register!(player:, front_office: nil)
29
+ raise PlayerRequiredError, 'player is required' unless player
30
+
31
+ rosters.each do |roster|
32
+ if roster.registered?(player)
33
+ raise PlayerAlreadyRegisteredError,
34
+ "#{player} already registered to: #{roster.id}"
35
+ end
36
+ end
37
+
38
+ if free_agents.include?(player)
39
+ raise PlayerAlreadyRegisteredError,
40
+ "#{player} already registered as a free agent"
41
+ end
42
+
43
+ if front_office
44
+ roster = roster(front_office)
45
+
46
+ raise RosterNotFoundError, "Roster not found for: #{front_office}" unless roster
47
+
48
+ roster.sign!(player)
49
+ else
50
+ free_agents << player
51
+ end
52
+
53
+ self
54
+ end
55
+
56
+ def to_s
57
+ (['League'] + rosters.map(&:to_s)).join("\n")
58
+ end
59
+
60
+ private
61
+
62
+ def add_roster(front_office)
63
+ raise RosterAlreadyAddedError, "#{front_office} already added" if rosters.include?(front_office)
64
+
65
+ rosters << Roster.new(id: front_office.id, name: front_office.name)
66
+
67
+ self
68
+ end
69
+ end
70
+ end
71
+ end
@@ -7,8 +7,8 @@ module Basketball
7
7
  class PickEvent < Event
8
8
  attr_reader :player
9
9
 
10
- def initialize(id:, team:, player:, pick:, round:, round_pick:)
11
- super(id:, team:, pick:, round:, round_pick:)
10
+ def initialize(id:, front_office:, player:, pick:, round:, round_pick:)
11
+ super(id:, front_office:, pick:, round:, round_pick:)
12
12
 
13
13
  raise ArgumentError, 'player required' unless player
14
14
 
@@ -2,37 +2,36 @@
2
2
 
3
3
  module Basketball
4
4
  module Drafting
5
- class Roster < ValueObject
6
- extend Forwardable
5
+ class Roster < Entity
6
+ class PlayerAlreadyRegisteredError < StandardError; end
7
+ class PlayerRequiredError < StandardError; end
7
8
 
8
- class WrongTeamEventError < StandardError; end
9
+ attr_reader :name, :players
9
10
 
10
- attr_reader_value :team, :events
11
+ def initialize(id:, name: '', players: [])
12
+ super(id)
11
13
 
12
- def_delegators :team, :id
14
+ @name = name.to_s
15
+ @players = players.each { |p| register!(p) }
13
16
 
14
- def initialize(team:, events: [])
15
- super()
16
-
17
- raise ArgumentError, 'team is required' unless team
17
+ freeze
18
+ end
18
19
 
19
- other_teams_pick_event_ids = events.reject { |e| e.team == team }.map(&:id)
20
+ def registered?(player)
21
+ players.include?(player)
22
+ end
20
23
 
21
- if other_teams_pick_event_ids.any?
22
- raise WrongTeamEventError,
23
- "Event(s): #{other_teams_pick_event_ids.join(',')} has wrong team"
24
- end
24
+ def sign!(player)
25
+ raise PlayerRequiredError, 'player is required' unless player
26
+ raise PlayerAlreadyRegisteredError, "#{player} already registered for #{id}" if registered?(player)
25
27
 
26
- @team = team
27
- @events = events
28
- end
28
+ players << player
29
29
 
30
- def players
31
- events.map(&:player)
30
+ self
32
31
  end
33
32
 
34
33
  def to_s
35
- ([team.to_s] + players.map(&:to_s)).join("\n")
34
+ (["[#{super}] #{name} Roster"] + players.map(&:to_s)).join("\n")
36
35
  end
37
36
  end
38
37
  end
@@ -5,8 +5,8 @@ module Basketball
5
5
  class SimEvent < Event
6
6
  attr_reader :player
7
7
 
8
- def initialize(id:, team:, player:, pick:, round:, round_pick:)
9
- super(id:, team:, pick:, round:, round_pick:)
8
+ def initialize(id:, front_office:, player:, pick:, round:, round_pick:)
9
+ super(id:, front_office:, pick:, round:, round_pick:)
10
10
 
11
11
  raise ArgumentError, 'player required' unless player
12
12
 
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'event'
4
+
5
+ module Basketball
6
+ module Drafting
7
+ class SkipEvent < Event
8
+ def to_s
9
+ "skipped #{super}"
10
+ end
11
+ end
12
+ end
13
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Basketball
4
- VERSION = '0.0.3'
4
+ VERSION = '0.0.5'
5
5
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: basketball
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.3
4
+ version: 0.0.5
5
5
  platform: ruby
6
6
  authors:
7
7
  - Matthew Ruggio
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2023-05-05 00:00:00.000000000 Z
11
+ date: 2023-05-06 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: faker
@@ -209,13 +209,14 @@ files:
209
209
  - lib/basketball/drafting/engine_serializer.rb
210
210
  - lib/basketball/drafting/event.rb
211
211
  - lib/basketball/drafting/front_office.rb
212
+ - lib/basketball/drafting/league.rb
212
213
  - lib/basketball/drafting/pick_event.rb
213
214
  - lib/basketball/drafting/player.rb
214
215
  - lib/basketball/drafting/player_search.rb
215
216
  - lib/basketball/drafting/position.rb
216
217
  - lib/basketball/drafting/roster.rb
217
218
  - lib/basketball/drafting/sim_event.rb
218
- - lib/basketball/drafting/team.rb
219
+ - lib/basketball/drafting/skip_event.rb
219
220
  - lib/basketball/entity.rb
220
221
  - lib/basketball/value_object.rb
221
222
  - lib/basketball/version.rb
@@ -1,28 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Basketball
4
- module Drafting
5
- class Team < Entity
6
- extend Forwardable
7
-
8
- attr_reader :name, :front_office
9
-
10
- def_delegators :front_office, :pick
11
-
12
- def initialize(id:, name: '', front_office: FrontOffice.new)
13
- super(id)
14
-
15
- raise ArgumentError, 'front_office is required' unless front_office
16
-
17
- @name = name.to_s
18
- @front_office = front_office
19
-
20
- freeze
21
- end
22
-
23
- def to_s
24
- "[#{super}] #{name}"
25
- end
26
- end
27
- end
28
- end