basketball 0.0.5 → 0.0.6

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: 793bfe7afef52aa72d3f89c94541fc7d8f24b350d63cb7c75d7215c285695b74
4
- data.tar.gz: 46a3ac7199fb534ff4827c8fe48bbe371c7f87bc623e579ffdce2310132a1263
3
+ metadata.gz: 262751c76037e2859937529aa23bdd456fd77779a090ed4e844830c428b09e75
4
+ data.tar.gz: 1a904f317fc3c1becfc381155a089d2ed0b041b60d358c1325459a363e6ce0ec
5
5
  SHA512:
6
- metadata.gz: cf8c46719f3669a31c932d7dc2742c287666fcb305c90c596c87b79c37d4fb95aafa446c70575ed4d8d3750a3f3672646e9ee49c6a0cf95e4dd7eb092831670e
7
- data.tar.gz: db01d3bfc4a8c1448875c5a099d0a33fd60ad60471ae5cda5f9eeeaf8a1fb2daa3e52d5ca2d8138867e42aaf6f54d259cdad7d3628cfbaa2d7bcf134fe5dc4b9
6
+ metadata.gz: 913504954c8c053fac3147468433d12b02ad5d6a7badc563587668c4d3943b898c49c951d69c393affc5f5e07d54257a5a004184e55a73f435af01344afa6baf
7
+ data.tar.gz: 791e48aa9711e33e29b994d0e811a735def017de757bc02c8f348b57f2ce0a42c1e5fbbd6737ed2525ddd9fbf3698c9fb77be991208a1b927e4b27063a2b9f04
data/CHANGELOG.md CHANGED
@@ -1,3 +1,8 @@
1
+ #### 0.0.6 - May 11th, 2023
2
+
3
+ * Added Scheduling module that can generate full schedules for entire league.
4
+ * Drafting::Event does not have identity anymore (no current tangible benefit).
5
+
1
6
  #### 0.0.5 - May 5th, 2023
2
7
 
3
8
  * Remove the notion of Team in favor of a flat front office.
data/README.md CHANGED
@@ -30,13 +30,13 @@ bundle binstubs basketball
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
 
33
- ![Basketball Architecture - Overview.png](/docs/architecture/overview.png)
33
+ ![Basketball Architecture - Overview](/docs/architecture/overview.png)
34
34
 
35
35
  ## Drafting Module
36
36
 
37
37
  The drafting module is responsible for providing a turn-based iterator allowing the consumer to either manually pick or simulate picks. Here is a cartoon showing the major components:
38
38
 
39
- ![Basketball Architecture - Drafting.png](/docs/architecture/drafting.png)
39
+ ![Basketball Architecture - Drafting](/docs/architecture/drafting.png)
40
40
 
41
41
  Element | Description
42
42
  :------------ | :-----------
@@ -47,7 +47,7 @@ Element | Description
47
47
  **Front Office** | Identifiable as a team, contains configuration for how to auto-pick draft selections.
48
48
  **League** | Set of rosters that together form a cohesive league.
49
49
  **Pick Event** | Result event emitted when a player is manually selected.
50
- **Playe ** | Identitiable as a person able to be drafted.
50
+ **Player** | Identitiable as a person able to be drafted.
51
51
  **Position** | Value object based on position code: PG, SG, SF, PF, and C.
52
52
  **Roster** | Identifiable as a team, set of players that make up a single team.
53
53
  **Sim Event** | Result event emitted when a player is automatically selected by a front office.
@@ -55,62 +55,137 @@ Element | Description
55
55
 
56
56
  ### The Drafting CLI
57
57
 
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:
58
+ The drafting module is meant to be interfaced with 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:
59
59
 
60
- #### Generate a Fresh Draft
60
+ ##### Generate a Fresh Draft
61
61
 
62
62
  ```zsh
63
63
  basketball-draft -o tmp/draft.json
64
64
  ```
65
65
 
66
- #### N Top Available Players
66
+ ##### N Top Available Players
67
67
 
68
68
  ```zsh
69
69
  basketball-draft -i tmp/draft.json -t 10
70
70
  ```
71
71
 
72
- #### N Top Available Players for a Position
72
+ ##### N Top Available Players for a Position
73
73
 
74
74
  ```zsh
75
75
  basketball-draft -i tmp/draft.json -t 10 -q PG
76
76
  ```
77
77
 
78
- #### Output Current Rosters
78
+ ##### Output Current Rosters
79
79
 
80
80
  ```zsh
81
81
  basketball-draft -i tmp/draft.json -r
82
82
  ```
83
83
 
84
- #### Output Event Log
84
+ ##### Output Event Log
85
85
 
86
86
  ```zsh
87
87
  basketball-draft -i tmp/draft.json -l
88
88
  ```
89
89
 
90
- #### Simulate N picks
90
+ ##### Simulate N Picks
91
91
 
92
92
  ```zsh
93
93
  basketball-draft -i tmp/draft.json -s 10
94
94
  ```
95
95
 
96
- #### Skip N picks
96
+ ##### Skip N Picks
97
97
 
98
98
  ```zsh
99
99
  basketball-draft -i tmp/draft.json -x 10
100
100
  ```
101
101
 
102
- #### Pick Players
102
+ ##### Pick Players
103
103
 
104
104
  ```zsh
105
105
  basketball-draft -i tmp/draft.json -p P-100,P-200,P-300
106
106
  ```
107
107
 
108
- #### Simulate the Rest of the Draft
108
+ ##### Simulate the Rest of the Draft
109
109
 
110
110
  ```zsh
111
111
  basketball-draft -i tmp/draft.json -a
112
112
  ```
113
113
 
114
+ ##### Help Menu
115
+
116
+ ```zsh
117
+ basketball-draft -h
118
+ ```
119
+
120
+ ## Scheduling Module
121
+
122
+ The Scheduling module is meant to take a League (conferences/divisions/teams) and turn it into a Calendar. This Calendar creation is atomic - the full calendar will be generated completely all in one call. Here is a cartoon showing the major components:
123
+
124
+ ![Basketball Architecture - Scheduling](/docs/architecture/scheduling.png)
125
+
126
+ Element | Description
127
+ :------------ | :-----------
128
+ **Away Team** | Team object designated as the away team for a Game.
129
+ **Calendar Serializer** | Understands how to serialize and deserialize a Calendar object.
130
+ **Calendar** | Hold a calendar for a year season. Pass in a year and the Calendar will know how to mark important boundary dates (preseason start, preseason end, season start, and season end) and it knows how to ensure Calendar correctness regarding dates.
131
+ **Conference** | Describes a conference in terms of structure; composed of an array of divisions (there can only 3).
132
+ **Coordinator** | Service which can generate a Calendar from a League.
133
+ **Division** | Describes a division in terms of structure; composed of an array of teams (there can only 5).
134
+ **Game** | Matches up a date with two teams (home and away) to represent a scheduled matchup.
135
+ **Home Team** | Team object designated as the home team for a Game.
136
+ **League Serializer** | Understands how to serialize and deserialize a League object.
137
+ **League** | Describes a league in terms of structure; composed of an array conferences (there can only be 2).
138
+ **Scheduling** | Bounded context (sub-module) dealing with matchup and calendar generation.
139
+ **Team** | Identified by an ID and described by a name: represents a basketball team that can be scheduled.
140
+
141
+ ##### Generate League
142
+
143
+ ```zsh
144
+ basketball-schedule -o tmp/league.json
145
+ ```
146
+
147
+ ##### Generate Calendar From League
148
+
149
+ ```zsh
150
+ basketball-schedule -i tmp/league.json -o tmp/calendar.json
151
+ ```
152
+
153
+ ##### Generate Calendar From League For a Specific Year
154
+
155
+ ```zsh
156
+ basketball-schedule -i tmp/league.json -o tmp/calendar.json -y 2005
157
+ ```
158
+
159
+ ##### Output a Generated Calendar's Matchups
160
+
161
+ ```zsh
162
+ basketball-schedule -c tmp/calendar.json
163
+ ```
164
+
165
+ ##### Output a Generated Calendar's Matchups For a Specific Team
166
+
167
+ ```zsh
168
+ basketball-schedule -c tmp/calendar.json -t C0-D0-T0
169
+ ```
170
+
171
+ ##### Output a Generated Calendar's Matchups For a Specific Date
172
+
173
+ ```zsh
174
+ basketball-schedule -c tmp/calendar.json -d 2005-02-03
175
+ ```
176
+
177
+ ##### Output a Generated Calendar's Matchups For a Specific Team and Date
178
+
179
+ ```zsh
180
+ basketball-schedule -c tmp/calendar.json -d 2005-02-03 -t C0-D0-T0
181
+ ```
182
+
183
+ ##### Help Menu
184
+
185
+ ```zsh
186
+ basketball-schedule -h
187
+ ```
188
+
114
189
  ## Contributing
115
190
 
116
191
  ### Development Environment Configuration
data/basketball.gemspec CHANGED
@@ -17,7 +17,7 @@ Gem::Specification.new do |s|
17
17
  s.email = ['mattruggio@icloud.com']
18
18
  s.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(.github|bin|docs|spec)/}) }
19
19
  s.bindir = 'exe'
20
- s.executables = %w[basketball-draft]
20
+ s.executables = %w[basketball-draft basketball-schedule]
21
21
  s.homepage = 'https://github.com/mattruggio/basketball'
22
22
  s.license = 'MIT'
23
23
  s.metadata = {
@@ -0,0 +1,7 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require 'bundler/setup'
5
+ require 'basketball'
6
+
7
+ Basketball::Scheduling::CLI.new(args: ARGV).invoke!
@@ -7,7 +7,7 @@ require_relative 'position'
7
7
 
8
8
  module Basketball
9
9
  module Drafting
10
- # Example:
10
+ # Examples:
11
11
  # exe/basketball-draft -o tmp/draft.json
12
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
13
  # exe/basketball-draft -i tmp/draft-wip.json -x 2
@@ -69,7 +69,7 @@ module Basketball
69
69
 
70
70
  def slop_parse(args)
71
71
  Slop.parse(args) do |o|
72
- o.banner = 'Usage: draft [options] ...'
72
+ o.banner = 'Usage: basketball-draft [options] ...'
73
73
 
74
74
  o.string '-i', '--input',
75
75
  'Path to load the engine from. If omitted then a new draft will be generated.'
@@ -100,7 +100,6 @@ module Basketball
100
100
  return if done?
101
101
 
102
102
  event = SkipEvent.new(
103
- id: SecureRandom.uuid,
104
103
  front_office: current_front_office,
105
104
  pick: current_pick,
106
105
  round: current_round,
@@ -126,7 +125,6 @@ module Basketball
126
125
  )
127
126
 
128
127
  event = SimEvent.new(
129
- id: SecureRandom.uuid,
130
128
  front_office:,
131
129
  player:,
132
130
  pick: current_pick,
@@ -149,7 +147,6 @@ module Basketball
149
147
  return nil if done?
150
148
 
151
149
  event = PickEvent.new(
152
- id: SecureRandom.uuid,
153
150
  front_office: current_front_office,
154
151
  player:,
155
152
  pick: current_pick,
@@ -206,11 +203,11 @@ module Basketball
206
203
 
207
204
  raise UnknownFrontOfficeError, "#{front_office} doesnt exist" unless front_offices.include?(event.front_office)
208
205
 
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
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
214
211
 
215
212
  events << event
216
213
 
@@ -115,7 +115,6 @@ module Basketball
115
115
  events.map do |event|
116
116
  {
117
117
  type: event.class.name.split('::').last,
118
- id: event.id,
119
118
  front_office: event.front_office.id,
120
119
  pick: event.pick,
121
120
  round: event.round,
@@ -157,7 +156,7 @@ module Basketball
157
156
 
158
157
  def deserialize_events(json, players, front_offices)
159
158
  (json.dig(:engine, :events) || []).map do |event_hash|
160
- event_opts = event_hash.slice(:id, :pick, :round, :round_pick).merge(
159
+ event_opts = event_hash.slice(:pick, :round, :round_pick).merge(
161
160
  front_office: front_offices.find { |t| t.id == event_hash[:front_office] }
162
161
  )
163
162
 
@@ -2,11 +2,11 @@
2
2
 
3
3
  module Basketball
4
4
  module Drafting
5
- class Event < Entity
6
- attr_reader :pick, :round, :round_pick, :front_office
5
+ class Event < ValueObject
6
+ attr_reader_value :pick, :round, :round_pick, :front_office
7
7
 
8
- def initialize(id:, front_office:, pick:, round:, round_pick:)
9
- super(id)
8
+ def initialize(front_office:, pick:, round:, round_pick:)
9
+ super()
10
10
 
11
11
  raise ArgumentError, 'front_office required' unless front_office
12
12
 
@@ -5,7 +5,6 @@ require_relative 'roster'
5
5
  module Basketball
6
6
  module Drafting
7
7
  class League
8
- class PlayerAlreadyRegisteredError < StandardError; end
9
8
  class RosterNotFoundError < StandardError; end
10
9
  class RosterAlreadyAddedError < StandardError; end
11
10
 
@@ -5,10 +5,10 @@ require_relative 'event'
5
5
  module Basketball
6
6
  module Drafting
7
7
  class PickEvent < Event
8
- attr_reader :player
8
+ attr_reader_value :player
9
9
 
10
- def initialize(id:, front_office:, player:, pick:, round:, round_pick:)
11
- super(id:, front_office:, pick:, round:, round_pick:)
10
+ def initialize(front_office:, player:, pick:, round:, round_pick:)
11
+ super(front_office:, pick:, round:, round_pick:)
12
12
 
13
13
  raise ArgumentError, 'player required' unless player
14
14
 
@@ -3,7 +3,6 @@
3
3
  module Basketball
4
4
  module Drafting
5
5
  class Roster < Entity
6
- class PlayerAlreadyRegisteredError < StandardError; end
7
6
  class PlayerRequiredError < StandardError; end
8
7
 
9
8
  attr_reader :name, :players
@@ -3,10 +3,10 @@
3
3
  module Basketball
4
4
  module Drafting
5
5
  class SimEvent < Event
6
- attr_reader :player
6
+ attr_reader_value :player
7
7
 
8
- def initialize(id:, front_office:, player:, pick:, round:, round_pick:)
9
- super(id:, front_office:, pick:, round:, round_pick:)
8
+ def initialize(front_office:, player:, pick:, round:, round_pick:)
9
+ super(front_office:, pick:, round:, round_pick:)
10
10
 
11
11
  raise ArgumentError, 'player required' unless player
12
12
 
@@ -1,3 +1,9 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative 'drafting/cli'
4
+
5
+ module Basketball
6
+ module Drafting
7
+ class PlayerAlreadyRegisteredError < StandardError; end
8
+ end
9
+ end
@@ -0,0 +1,121 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Basketball
4
+ module Scheduling
5
+ class Calendar < ValueObject
6
+ class TeamAlreadyBookedError < StandardError; end
7
+ class InvalidGameOrderError < StandardError; end
8
+ class OutOfBoundsError < StandardError; end
9
+
10
+ attr_reader :preseason_start_date,
11
+ :preseason_end_date,
12
+ :season_start_date,
13
+ :season_end_date
14
+
15
+ attr_reader_value :year, :games
16
+
17
+ def initialize(year:, games: [])
18
+ super()
19
+
20
+ raise ArgumentError, 'year is required' unless year
21
+
22
+ @year = year
23
+ @preseason_start_date = Date.new(year, 9, 30)
24
+ @preseason_end_date = Date.new(year, 10, 14)
25
+ @season_start_date = Date.new(year, 10, 18)
26
+ @season_end_date = Date.new(year + 1, 4, 29)
27
+ @games = []
28
+
29
+ games.each { |game| add!(game) }
30
+
31
+ freeze
32
+ end
33
+
34
+ def add!(game)
35
+ assert_in_bounds(game)
36
+ assert_free_date(game)
37
+
38
+ @games << game
39
+
40
+ self
41
+ end
42
+
43
+ def preseason_games_for(date: nil, team: nil)
44
+ games_for(date:, team:).select { |game| game.is_a?(PreseasonGame) }
45
+ end
46
+
47
+ def season_games_for(date: nil, team: nil)
48
+ games_for(date:, team:).select { |game| game.is_a?(SeasonGame) }
49
+ end
50
+
51
+ def games_for(date: nil, team: nil)
52
+ games.select do |game|
53
+ (date.nil? || game.date == date) &&
54
+ (team.nil? || (game.home_team == team || game.away_team == team))
55
+ end
56
+ end
57
+
58
+ def available_preseason_dates_for(team)
59
+ all_preseason_dates - preseason_games_for(team:).map(&:date)
60
+ end
61
+
62
+ def available_season_dates_for(team)
63
+ all_season_dates - season_games_for(team:).map(&:date)
64
+ end
65
+
66
+ def available_preseason_matchup_dates(team1, team2)
67
+ available_team_dates = available_preseason_dates_for(team1)
68
+ available_other_team_dates = available_preseason_dates_for(team2)
69
+
70
+ available_team_dates & available_other_team_dates
71
+ end
72
+
73
+ def available_season_matchup_dates(team1, team2)
74
+ available_team_dates = available_season_dates_for(team1)
75
+ available_other_team_dates = available_season_dates_for(team2)
76
+
77
+ available_team_dates & available_other_team_dates
78
+ end
79
+
80
+ def teams
81
+ games.flat_map(&:teams)
82
+ end
83
+
84
+ def team(id)
85
+ teams.find { |t| t == Team.new(id:) }
86
+ end
87
+
88
+ private
89
+
90
+ def all_preseason_dates
91
+ (preseason_start_date..preseason_end_date).to_a
92
+ end
93
+
94
+ def all_season_dates
95
+ (season_start_date..season_end_date).to_a
96
+ end
97
+
98
+ def assert_free_date(game)
99
+ if games_for(date: game.date, team: game.home_team).any?
100
+ raise TeamAlreadyBookedError, "#{game.home_team} already playing on #{game.date}"
101
+ end
102
+
103
+ return unless games_for(date: game.date, team: game.away_team).any?
104
+
105
+ raise TeamAlreadyBookedError, "#{game.away_team} already playing on #{game.date}"
106
+ end
107
+
108
+ def assert_in_bounds(game)
109
+ if game.is_a?(PreseasonGame)
110
+ raise OutOfBoundsError, "#{game.date} is before preseason begins" if game.date < preseason_start_date
111
+ raise OutOfBoundsError, "#{game.date} is after preseason ends" if game.date > preseason_end_date
112
+ elsif game.is_a?(SeasonGame)
113
+ raise OutOfBoundsError, "#{game.date} is before season begins" if game.date < season_start_date
114
+ raise OutOfBoundsError, "#{game.date} is after season ends" if game.date > season_end_date
115
+ else
116
+ raise ArgumentError, "Dont know what this game type is: #{game.class.name}"
117
+ end
118
+ end
119
+ end
120
+ end
121
+ end
@@ -0,0 +1,84 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'game'
4
+ require_relative 'preseason_game'
5
+ require_relative 'season_game'
6
+
7
+ module Basketball
8
+ module Scheduling
9
+ class CalendarSerializer
10
+ GAME_CLASSES = {
11
+ 'PreseasonGame' => PreseasonGame,
12
+ 'SeasonGame' => SeasonGame
13
+ }.freeze
14
+
15
+ def deserialize(string)
16
+ json = JSON.parse(string, symbolize_names: true)
17
+
18
+ Calendar.new(
19
+ year: json[:year].to_i,
20
+ games: deserialize_games(json)
21
+ )
22
+ end
23
+
24
+ def serialize(calendar)
25
+ {
26
+ year: calendar.preseason_start_date.year,
27
+ teams: serialize_teams(calendar.games.flat_map(&:teams).uniq),
28
+ games: serialize_games(calendar.games)
29
+ }.to_json
30
+ end
31
+
32
+ private
33
+
34
+ ## Deserialization
35
+
36
+ def deserialize_games(json)
37
+ teams = deserialize_teams(json[:teams])
38
+
39
+ (json[:games] || []).map do |game_hash|
40
+ GAME_CLASSES.fetch(game_hash[:type]).new(
41
+ date: Date.parse(game_hash[:date]),
42
+ home_team: teams.fetch(game_hash[:home_team]),
43
+ away_team: teams.fetch(game_hash[:away_team])
44
+ )
45
+ end
46
+ end
47
+
48
+ def deserialize_teams(teams)
49
+ (teams || []).to_h do |id, team_hash|
50
+ team = Team.new(id:, name: team_hash[:name])
51
+
52
+ [
53
+ team.id,
54
+ team
55
+ ]
56
+ end
57
+ end
58
+
59
+ ## Serialization
60
+
61
+ def serialize_teams(teams)
62
+ teams.to_h do |team|
63
+ [
64
+ team.id,
65
+ {
66
+ name: team.name
67
+ }
68
+ ]
69
+ end
70
+ end
71
+
72
+ def serialize_games(games)
73
+ games.sort_by(&:date).map do |game|
74
+ {
75
+ type: game.class.name.split('::').last,
76
+ date: game.date,
77
+ home_team: game.home_team.id,
78
+ away_team: game.away_team.id
79
+ }
80
+ end
81
+ end
82
+ end
83
+ end
84
+ end