basketball 0.0.8 → 0.0.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (59) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +9 -19
  3. data/CHANGELOG.md +1 -39
  4. data/README.md +75 -93
  5. data/basketball.gemspec +3 -6
  6. data/exe/{basketball-season-scheduling → basketball-coordinator} +1 -1
  7. data/exe/{basketball-draft → basketball-room} +1 -1
  8. data/lib/basketball/app/coordinator_cli.rb +250 -0
  9. data/lib/basketball/app/coordinator_repository.rb +114 -0
  10. data/lib/basketball/app/document_repository.rb +67 -0
  11. data/lib/basketball/app/file_store.rb +38 -0
  12. data/lib/basketball/app/in_memory_store.rb +42 -0
  13. data/lib/basketball/app/league_repository.rb +20 -0
  14. data/lib/basketball/app/league_serializable.rb +54 -0
  15. data/lib/basketball/{draft/cli.rb → app/room_cli.rb} +74 -80
  16. data/lib/basketball/app/room_repository.rb +149 -0
  17. data/lib/basketball/app.rb +20 -0
  18. data/lib/basketball/draft/assessment.rb +31 -0
  19. data/lib/basketball/draft/event.rb +3 -2
  20. data/lib/basketball/draft/front_office.rb +35 -28
  21. data/lib/basketball/draft/{pick_event.rb → pick.rb} +13 -6
  22. data/lib/basketball/draft/room.rb +119 -119
  23. data/lib/basketball/draft/{player_search.rb → scout.rb} +4 -9
  24. data/lib/basketball/draft/skip.rb +12 -0
  25. data/lib/basketball/draft.rb +13 -6
  26. data/lib/basketball/entity.rb +19 -10
  27. data/lib/basketball/org/league.rb +68 -0
  28. data/lib/basketball/org/player.rb +26 -0
  29. data/lib/basketball/{draft → org}/position.rb +3 -2
  30. data/lib/basketball/org/team.rb +38 -0
  31. data/lib/basketball/org.rb +12 -0
  32. data/lib/basketball/season/arena.rb +113 -0
  33. data/lib/basketball/season/calendar.rb +41 -72
  34. data/lib/basketball/season/coordinator.rb +186 -128
  35. data/lib/basketball/season/{preseason_game.rb → exhibition.rb} +2 -1
  36. data/lib/basketball/season/game.rb +15 -10
  37. data/lib/basketball/season/matchup.rb +27 -0
  38. data/lib/basketball/season/opponent.rb +15 -0
  39. data/lib/basketball/season/{season_game.rb → regular.rb} +2 -1
  40. data/lib/basketball/season/result.rb +37 -0
  41. data/lib/basketball/season.rb +12 -13
  42. data/lib/basketball/value_object.rb +8 -27
  43. data/lib/basketball/value_object_dsl.rb +30 -0
  44. data/lib/basketball/version.rb +1 -1
  45. data/lib/basketball.rb +9 -4
  46. metadata +37 -44
  47. data/lib/basketball/draft/league.rb +0 -70
  48. data/lib/basketball/draft/player.rb +0 -43
  49. data/lib/basketball/draft/room_serializer.rb +0 -186
  50. data/lib/basketball/draft/roster.rb +0 -37
  51. data/lib/basketball/draft/sim_event.rb +0 -23
  52. data/lib/basketball/draft/skip_event.rb +0 -13
  53. data/lib/basketball/season/calendar_serializer.rb +0 -94
  54. data/lib/basketball/season/conference.rb +0 -57
  55. data/lib/basketball/season/division.rb +0 -43
  56. data/lib/basketball/season/league.rb +0 -114
  57. data/lib/basketball/season/league_serializer.rb +0 -99
  58. data/lib/basketball/season/scheduling_cli.rb +0 -198
  59. data/lib/basketball/season/team.rb +0 -21
@@ -1,142 +1,148 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative 'league'
4
-
5
3
  module Basketball
6
4
  module Draft
7
- class Room
5
+ # Main pick-by-pick iterator object which will round-robin rotate team selections.
6
+ class Room < Entity
8
7
  class AlreadyPickedError < StandardError; end
9
- class DupeEventError < StandardError; end
8
+ class EndOfDraftError < StandardError; end
10
9
  class EventOutOfOrderError < StandardError; end
11
- class UnknownPlayerError < StandardError; end
10
+ class FrontOfficeAlreadyRegisteredError < StandardError; end
11
+ class PlayerAlreadyAddedError < StandardError; end
12
12
  class UnknownFrontOfficeError < StandardError; end
13
- class EndOfDraftError < StandardError; end
14
-
15
- DEFAULT_ROUNDS = 12
13
+ class UnknownPlayerError < StandardError; end
16
14
 
17
- private_constant :DEFAULT_ROUNDS
15
+ attr_reader :rounds, :players, :front_offices, :events
18
16
 
19
- attr_reader :events, :rounds
17
+ def initialize(front_offices:, rounds:, players: [], events: [])
18
+ super()
20
19
 
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
20
+ raise InvalidRoundsError, "#{rounds} should be a positive number" unless rounds.positive?
26
21
 
27
- # Each one will be validated for correctness.
28
- events.each { |e| play!(e) }
22
+ @rounds = rounds
23
+ @players = []
24
+ @front_offices = []
25
+ @events = []
29
26
 
30
- freeze
27
+ front_offices.each { |front_office| register!(front_office) }
28
+ players.each { |player| add_player!(player) }
29
+ events.each { |event| add_event!(event) }
31
30
  end
32
31
 
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
32
+ # This method will return a materialized list of teams and their selections.
33
+ def league
34
+ Org::League.new.tap do |league|
35
+ front_offices.each do |front_office|
36
+ team = Org::Team.new(id: front_office.id)
38
37
 
39
- undrafted_players.each do |player|
40
- league.register!(player:)
38
+ league.register!(team)
39
+
40
+ drafted_players(front_office).each do |player|
41
+ league.sign!(player:, team:)
42
+ end
41
43
  end
42
44
  end
43
45
  end
44
46
 
45
- def to_s
46
- events.join("\n")
47
- end
47
+ ### Peek Methods
48
48
 
49
- def front_offices
50
- front_offices_by_id.values
51
- end
52
-
53
- def players
54
- players_by_id.values
49
+ def assessment
50
+ Assessment.new(
51
+ drafted_players: drafted_players(front_office),
52
+ undrafted_players:,
53
+ pick:,
54
+ round:,
55
+ round_pick:
56
+ )
55
57
  end
56
58
 
57
59
  def total_picks
58
60
  rounds * front_offices.length
59
61
  end
60
62
 
61
- def current_round
63
+ def round
62
64
  return if done?
63
65
 
64
- (current_pick / front_offices.length.to_f).ceil
66
+ (pick / front_offices.length.to_f).ceil
65
67
  end
66
68
 
67
- def current_round_pick
69
+ def round_pick
68
70
  return if done?
69
71
 
70
- mod = current_pick % front_offices.length
72
+ mod = pick % front_offices.length
71
73
 
72
74
  mod.positive? ? mod : front_offices.length
73
75
  end
74
76
 
75
- def current_front_office
77
+ def front_office
76
78
  return if done?
77
79
 
78
- front_offices[current_round_pick - 1]
80
+ front_offices[round_pick - 1]
79
81
  end
80
82
 
81
- def current_pick
83
+ def pick
82
84
  return if done?
83
85
 
84
- internal_current_pick
86
+ internal_pick
85
87
  end
86
88
 
87
89
  def remaining_picks
88
- total_picks - internal_current_pick + 1
90
+ total_picks - internal_pick + 1
89
91
  end
90
92
 
91
93
  def done?
92
- internal_current_pick > total_picks
94
+ internal_pick > total_picks
93
95
  end
94
96
 
95
97
  def not_done?
96
98
  !done?
97
99
  end
98
100
 
101
+ def drafted_players(front_office = nil)
102
+ raise UnknownFrontOfficeError, "#{front_office} doesnt exist" if front_office && !registered?(front_office)
103
+
104
+ player_events.each_with_object([]) do |e, memo|
105
+ next unless front_office.nil? || e.front_office == front_office
106
+
107
+ memo << e.player
108
+ end
109
+ end
110
+
111
+ def undrafted_players
112
+ players - drafted_players
113
+ end
114
+
115
+ ### Event Methods
116
+
99
117
  def skip!
100
118
  return if done?
101
119
 
102
- event = SkipEvent.new(
103
- front_office: current_front_office,
104
- pick: current_pick,
105
- round: current_round,
106
- round_pick: current_round_pick
107
- )
120
+ event = Skip.new(front_office:, pick:, round:, round_pick:)
108
121
 
109
- play!(event)
122
+ add_event!(event)
110
123
 
111
124
  event
112
125
  end
113
126
 
114
- def sim!(times = nil)
115
- counter = 0
116
- events = []
127
+ def sim!
128
+ return if done?
129
+
130
+ player = front_office.pick(assessment)
131
+ event = Pick.new(front_office:, pick:, round:, round_pick:, player:, auto: true)
117
132
 
118
- until done? || (times && counter >= times)
119
- front_office = current_front_office
133
+ add_event!(event)
120
134
 
121
- player = front_office.pick(
122
- undrafted_player_search:,
123
- drafted_players: drafted_players(front_office),
124
- round: current_round
125
- )
135
+ event
136
+ end
126
137
 
127
- event = SimEvent.new(
128
- front_office:,
129
- player:,
130
- pick: current_pick,
131
- round: current_round,
132
- round_pick: current_round_pick
133
- )
138
+ def sim_rest!
139
+ events = []
134
140
 
135
- play!(event)
141
+ while not_done?
142
+ event = sim!
136
143
 
137
- yield(event) if block_given?
144
+ yield event if block_given?
138
145
 
139
- counter += 1
140
146
  events << event
141
147
  end
142
148
 
@@ -146,76 +152,70 @@ module Basketball
146
152
  def pick!(player)
147
153
  return nil if done?
148
154
 
149
- event = PickEvent.new(
150
- front_office: current_front_office,
151
- player:,
152
- pick: current_pick,
153
- round: current_round,
154
- round_pick: current_round_pick
155
- )
155
+ event = Pick.new(front_office:, pick:, round:, round_pick:, player:)
156
156
 
157
- play!(event)
157
+ add_event!(event)
158
158
  end
159
159
 
160
- def undrafted_players
161
- players - drafted_players
162
- end
160
+ private
163
161
 
164
- def undrafted_player_search
165
- PlayerSearch.new(undrafted_players)
162
+ def player_events
163
+ events.select { |e| e.respond_to?(:player) }
166
164
  end
167
165
 
168
- private
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
169
174
 
170
- attr_reader :players_by_id, :front_offices_by_id
175
+ assert_player(event)
171
176
 
172
- def player_events
173
- events.select { |e| e.respond_to?(:player) }
177
+ events << event
178
+
179
+ event
174
180
  end
181
+ # rubocop:enable Metrics/AbcSize
175
182
 
176
- def internal_current_pick
177
- events.length + 1
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)
178
188
  end
179
189
 
180
- def drafted_players(front_office = nil)
181
- player_events.each_with_object([]) do |e, memo|
182
- next unless front_office.nil? || e.front_office == front_office
190
+ def internal_pick
191
+ events.length + 1
192
+ end
183
193
 
184
- memo << e.player
185
- end
194
+ def registered?(front_office)
195
+ front_offices.include?(front_office)
186
196
  end
187
197
 
188
- # rubocop:disable Metrics/AbcSize
189
- # rubocop:disable Metrics/CyclomaticComplexity
190
- # rubocop:disable Metrics/PerceivedComplexity
191
- def play!(event)
192
- if event.respond_to?(:player) && drafted_players.include?(event.player)
193
- raise AlreadyPickedError, "#{player} was already picked"
194
- end
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)
195
201
 
196
- if event.respond_to?(:player) && !players.include?(event.player)
197
- raise UnknownPlayerError, "#{event.player} doesnt exist"
198
- end
202
+ front_offices << front_office
199
203
 
200
- if event.front_office != current_front_office
201
- raise EventOutOfOrder, "#{event} #{event.front_office} cant pick right now"
202
- end
204
+ self
205
+ end
203
206
 
204
- raise UnknownFrontOfficeError, "#{front_office} doesnt exist" unless front_offices.include?(event.front_office)
207
+ def player?(player)
208
+ players.include?(player)
209
+ end
205
210
 
206
- raise DupeEventError, "#{event} is a dupe" if events.include?(event)
207
- raise EventOutOfOrder, "#{event} has wrong pick" if event.pick != current_pick
208
- raise EventOutOfOrder, "#{event} has wrong round" if event.round != current_round
209
- raise EventOutOfOrder, "#{event} has wrong round_pick" if event.round_pick != current_round_pick
210
- raise EndOfDraftError, "#{total_picks} pick limit reached" if events.length > total_picks + 1
211
+ def add_player!(player)
212
+ raise ArgumentError, 'player required' unless player
213
+ raise PlayerAlreadyAddedError, "#{player} already added" if player?(player)
211
214
 
212
- events << event
215
+ players << player
213
216
 
214
- event
217
+ self
215
218
  end
216
- # rubocop:enable Metrics/AbcSize
217
- # rubocop:enable Metrics/CyclomaticComplexity
218
- # rubocop:enable Metrics/PerceivedComplexity
219
219
  end
220
220
  end
221
221
  end
@@ -2,14 +2,9 @@
2
2
 
3
3
  module Basketball
4
4
  module Draft
5
- class PlayerSearch
6
- attr_reader :players
7
-
8
- def initialize(players = [])
9
- @players = players
10
- end
11
-
12
- def query(position: nil, exclude_positions: [])
5
+ # A Scout knows how to process a set of players and figure out who the top prospects are.
6
+ class Scout
7
+ def top_for(players: [], position: nil, exclude_positions: [])
13
8
  filtered_players = players
14
9
 
15
10
  if position
@@ -24,7 +19,7 @@ module Basketball
24
19
  end
25
20
  end
26
21
 
27
- filtered_players.sort_by(&:overall).reverse
22
+ filtered_players.sort_by { |p| [p.overall, p.id] }.reverse
28
23
  end
29
24
  end
30
25
  end
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Basketball
4
+ module Draft
5
+ # Room event which indicates a front office has intentionally skipped a selection.
6
+ class Skip < Event
7
+ def to_s
8
+ "#{super} skipped"
9
+ end
10
+ end
11
+ end
12
+ end
@@ -1,9 +1,16 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative 'draft/cli'
3
+ # Services
4
+ require_relative 'draft/scout'
4
5
 
5
- module Basketball
6
- module Draft
7
- class PlayerAlreadyRegisteredError < StandardError; end
8
- end
9
- end
6
+ # Common
7
+ require_relative 'draft/assessment'
8
+ require_relative 'draft/event'
9
+ require_relative 'draft/front_office'
10
+
11
+ # Event Subclasses
12
+ require_relative 'draft/pick'
13
+ require_relative 'draft/skip'
14
+
15
+ # Specific
16
+ require_relative 'draft/room'
@@ -1,31 +1,40 @@
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
- extend Forwardable
6
7
  include Comparable
7
8
 
8
9
  attr_reader :id
9
10
 
10
- def_delegators :id, :to_s
11
-
12
- def initialize(id)
13
- raise ArgumentError, 'id is required' if id.to_s.empty?
14
-
15
- @id = id.to_s.upcase
11
+ def initialize(id = nil)
12
+ @id = id
16
13
  end
17
14
 
18
15
  def <=>(other)
19
- id <=> other.id
16
+ comparable_id <=> other.comparable_id
20
17
  end
21
18
 
22
19
  def ==(other)
23
- id == other.id
20
+ comparable_id == other.comparable_id
24
21
  end
25
22
  alias eql? ==
26
23
 
27
24
  def hash
28
- id.hash
25
+ comparable_id.hash
26
+ end
27
+
28
+ def to_s
29
+ id.to_s
29
30
  end
31
+
32
+ def comparable_id
33
+ id.to_s.upcase
34
+ end
35
+
36
+ private
37
+
38
+ attr_writer :id
30
39
  end
31
40
  end
@@ -0,0 +1,68 @@
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 < Entity
10
+ class TeamAlreadyRegisteredError < StandardError; end
11
+ class UnregisteredTeamError < StandardError; end
12
+
13
+ attr_reader :teams
14
+
15
+ def initialize(teams: [])
16
+ super()
17
+
18
+ @teams = []
19
+
20
+ teams.each { |team| register!(team) }
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
+ end
67
+ end
68
+ 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 Draft
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
- attr_reader_value :code
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