basketball 0.0.8 → 0.0.10

Sign up to get free protection for your applications and to get access to all the features.
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