basketball 0.0.4 → 0.0.6

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 (34) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +10 -2
  3. data/CODE_OF_CONDUCT.md +1 -1
  4. data/README.md +120 -18
  5. data/basketball.gemspec +1 -1
  6. data/exe/basketball-schedule +7 -0
  7. data/lib/basketball/drafting/cli.rb +15 -15
  8. data/lib/basketball/drafting/engine.rb +43 -42
  9. data/lib/basketball/drafting/engine_serializer.rb +41 -49
  10. data/lib/basketball/drafting/event.rb +10 -10
  11. data/lib/basketball/drafting/front_office.rb +9 -4
  12. data/lib/basketball/drafting/league.rb +70 -0
  13. data/lib/basketball/drafting/pick_event.rb +3 -3
  14. data/lib/basketball/drafting/roster.rb +18 -24
  15. data/lib/basketball/drafting/sim_event.rb +3 -3
  16. data/lib/basketball/drafting.rb +6 -0
  17. data/lib/basketball/scheduling/calendar.rb +121 -0
  18. data/lib/basketball/scheduling/calendar_serializer.rb +84 -0
  19. data/lib/basketball/scheduling/cli.rb +198 -0
  20. data/lib/basketball/scheduling/conference.rb +57 -0
  21. data/lib/basketball/scheduling/coordinator.rb +180 -0
  22. data/lib/basketball/scheduling/division.rb +43 -0
  23. data/lib/basketball/scheduling/game.rb +32 -0
  24. data/lib/basketball/scheduling/league.rb +114 -0
  25. data/lib/basketball/scheduling/league_serializer.rb +90 -0
  26. data/lib/basketball/scheduling/preseason_game.rb +11 -0
  27. data/lib/basketball/scheduling/season_game.rb +8 -0
  28. data/lib/basketball/scheduling/team.rb +21 -0
  29. data/lib/basketball/scheduling.rb +17 -0
  30. data/lib/basketball/value_object.rb +16 -7
  31. data/lib/basketball/version.rb +1 -1
  32. data/lib/basketball.rb +1 -0
  33. metadata +18 -3
  34. data/lib/basketball/drafting/team.rb +0 -28
@@ -2,7 +2,6 @@
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'
8
7
  require_relative 'skip_event'
@@ -19,14 +18,14 @@ module Basketball
19
18
  private_constant :EVENT_CLASSES
20
19
 
21
20
  def deserialize(string)
22
- json = JSON.parse(string, symbolize_names: true)
23
- teams = deserialize_teams(json)
24
- players = deserialize_players(json)
25
- 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)
26
25
 
27
26
  engine_opts = {
28
27
  players:,
29
- teams:,
28
+ front_offices:,
30
29
  events:
31
30
  }
32
31
 
@@ -39,7 +38,7 @@ module Basketball
39
38
  {
40
39
  info: serialize_info(engine),
41
40
  engine: serialize_engine(engine),
42
- rosters: serialize_rosters(engine)
41
+ league: serialize_league(engine)
43
42
  }.to_json
44
43
  end
45
44
 
@@ -48,7 +47,7 @@ module Basketball
48
47
  def serialize_engine(engine)
49
48
  {
50
49
  rounds: engine.rounds,
51
- teams: serialize_teams(engine),
50
+ front_offices: serialize_front_offices(engine),
52
51
  players: serialize_players(engine),
53
52
  events: serialize_events(engine.events)
54
53
  }
@@ -59,37 +58,40 @@ module Basketball
59
58
  total_picks: engine.total_picks,
60
59
  current_round: engine.current_round,
61
60
  current_round_pick: engine.current_round_pick,
62
- current_team: engine.current_team&.id,
61
+ current_front_office: engine.current_front_office&.id,
63
62
  current_pick: engine.current_pick,
64
63
  remaining_picks: engine.remaining_picks,
65
- done: engine.done?,
66
- undrafted_players: engine.undrafted_players.map(&:id)
64
+ done: engine.done?
67
65
  }
68
66
  end
69
67
 
70
- def serialize_rosters(engine)
71
- 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|
72
72
  [
73
73
  roster.id,
74
74
  {
75
- events: roster.events.map(&:id),
76
75
  players: roster.players.map(&:id)
77
76
  }
78
77
  ]
79
78
  end
79
+
80
+ {
81
+ free_agents: league.free_agents.map(&:id),
82
+ rosters:
83
+ }
80
84
  end
81
85
 
82
- def serialize_teams(engine)
83
- engine.teams.to_h do |team|
86
+ def serialize_front_offices(engine)
87
+ engine.front_offices.to_h do |front_office|
84
88
  [
85
- team.id,
89
+ front_office.id,
86
90
  {
87
- name: team.name,
88
- front_office: {
89
- fuzz: team.front_office.fuzz,
90
- depth: team.front_office.depth,
91
- prioritized_positions: team.front_office.prioritized_positions
92
- }
91
+ name: front_office.name,
92
+ fuzz: front_office.fuzz,
93
+ depth: front_office.depth,
94
+ prioritized_positions: front_office.prioritized_positions.map(&:code)
93
95
  }
94
96
  ]
95
97
  end
@@ -113,8 +115,7 @@ module Basketball
113
115
  events.map do |event|
114
116
  {
115
117
  type: event.class.name.split('::').last,
116
- id: event.id,
117
- team: event.team.id,
118
+ front_office: event.front_office.id,
118
119
  pick: event.pick,
119
120
  round: event.round,
120
121
  round_pick: event.round_pick
@@ -124,30 +125,21 @@ module Basketball
124
125
  end
125
126
  end
126
127
 
127
- def deserialize_teams(json)
128
- (json.dig(:engine, :teams) || []).map do |id, team_hash|
129
- team_opts = {
128
+ def deserialize_front_offices(json)
129
+ (json.dig(:engine, :front_offices) || []).map do |id, front_office_hash|
130
+ prioritized_positions = (front_office_hash[:prioritized_positions] || []).map do |v|
131
+ Position.new(v)
132
+ end
133
+
134
+ front_office_opts = {
130
135
  id:,
131
- name: team_hash[:name]
136
+ name: front_office_hash[:name],
137
+ prioritized_positions:,
138
+ fuzz: front_office_hash[:fuzz],
139
+ depth: front_office_hash[:depth]
132
140
  }
133
141
 
134
- if team_hash.key?(:front_office)
135
- front_office_hash = team_hash[:front_office] || {}
136
-
137
- prioritized_positions = (front_office_hash[:prioritized_positions] || []).map do |v|
138
- Position.new(v)
139
- end
140
-
141
- front_office_opts = {
142
- prioritized_positions:,
143
- fuzz: front_office_hash[:fuzz],
144
- depth: front_office_hash[:depth]
145
- }
146
-
147
- team_opts[:front_office] = FrontOffice.new(**front_office_opts)
148
- end
149
-
150
- Team.new(**team_opts)
142
+ FrontOffice.new(**front_office_opts)
151
143
  end
152
144
  end
153
145
 
@@ -162,10 +154,10 @@ module Basketball
162
154
  end
163
155
  end
164
156
 
165
- def deserialize_events(json, players, teams)
157
+ def deserialize_events(json, players, front_offices)
166
158
  (json.dig(:engine, :events) || []).map do |event_hash|
167
- event_opts = event_hash.slice(:id, :pick, :round, :round_pick).merge(
168
- team: teams.find { |t| t.id == event_hash[:team] }
159
+ event_opts = event_hash.slice(:pick, :round, :round_pick).merge(
160
+ front_office: front_offices.find { |t| t.id == event_hash[:front_office] }
169
161
  )
170
162
 
171
163
  class_constant = EVENT_CLASSES.fetch(event_hash[:type])
@@ -2,22 +2,22 @@
2
2
 
3
3
  module Basketball
4
4
  module Drafting
5
- class Event < Entity
6
- attr_reader :pick, :round, :round_pick, :team
5
+ class Event < ValueObject
6
+ attr_reader_value :pick, :round, :round_pick, :front_office
7
7
 
8
- def initialize(id:, team:, pick:, round:, round_pick:)
9
- super(id)
8
+ def initialize(front_office:, pick:, round:, round_pick:)
9
+ super()
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,70 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'roster'
4
+
5
+ module Basketball
6
+ module Drafting
7
+ class League
8
+ class RosterNotFoundError < StandardError; end
9
+ class RosterAlreadyAddedError < StandardError; end
10
+
11
+ attr_reader :free_agents, :rosters
12
+
13
+ def initialize(free_agents: [], front_offices: [])
14
+ @rosters = []
15
+ @free_agents = []
16
+
17
+ front_offices.each { |front_office| add_roster(front_office) }
18
+ free_agents.each { |p| register!(player: p) }
19
+
20
+ freeze
21
+ end
22
+
23
+ def roster(front_office)
24
+ rosters.find { |r| r == front_office }
25
+ end
26
+
27
+ def register!(player:, front_office: nil)
28
+ raise PlayerRequiredError, 'player is required' unless player
29
+
30
+ rosters.each do |roster|
31
+ if roster.registered?(player)
32
+ raise PlayerAlreadyRegisteredError,
33
+ "#{player} already registered to: #{roster.id}"
34
+ end
35
+ end
36
+
37
+ if free_agents.include?(player)
38
+ raise PlayerAlreadyRegisteredError,
39
+ "#{player} already registered as a free agent"
40
+ end
41
+
42
+ if front_office
43
+ roster = roster(front_office)
44
+
45
+ raise RosterNotFoundError, "Roster not found for: #{front_office}" unless roster
46
+
47
+ roster.sign!(player)
48
+ else
49
+ free_agents << player
50
+ end
51
+
52
+ self
53
+ end
54
+
55
+ def to_s
56
+ (['League'] + rosters.map(&:to_s)).join("\n")
57
+ end
58
+
59
+ private
60
+
61
+ def add_roster(front_office)
62
+ raise RosterAlreadyAddedError, "#{front_office} already added" if rosters.include?(front_office)
63
+
64
+ rosters << Roster.new(id: front_office.id, name: front_office.name)
65
+
66
+ self
67
+ end
68
+ end
69
+ end
70
+ end
@@ -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:, team:, player:, pick:, round:, round_pick:)
11
- super(id:, team:, 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
 
@@ -2,41 +2,35 @@
2
2
 
3
3
  module Basketball
4
4
  module Drafting
5
- class Roster < ValueObject
6
- extend Forwardable
5
+ class Roster < Entity
6
+ class PlayerRequiredError < StandardError; end
7
7
 
8
- class WrongTeamEventError < StandardError; end
8
+ attr_reader :name, :players
9
9
 
10
- attr_reader_value :team, :events
10
+ def initialize(id:, name: '', players: [])
11
+ super(id)
11
12
 
12
- def_delegators :team, :id
13
+ @name = name.to_s
14
+ @players = players.each { |p| register!(p) }
13
15
 
14
- def initialize(team:, events: [])
15
- super()
16
-
17
- raise ArgumentError, 'team is required' unless team
18
-
19
- other_teams_pick_event_ids = events.reject { |e| e.team == team }.map(&:id)
20
-
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
25
-
26
- @team = team
27
- @events = events
16
+ freeze
28
17
  end
29
18
 
30
- def player_events
31
- events.select { |e| e.respond_to?(:player) }
19
+ def registered?(player)
20
+ players.include?(player)
32
21
  end
33
22
 
34
- def players
35
- player_events.map(&:player)
23
+ def sign!(player)
24
+ raise PlayerRequiredError, 'player is required' unless player
25
+ raise PlayerAlreadyRegisteredError, "#{player} already registered for #{id}" if registered?(player)
26
+
27
+ players << player
28
+
29
+ self
36
30
  end
37
31
 
38
32
  def to_s
39
- ([team.to_s] + players.map(&:to_s)).join("\n")
33
+ (["[#{super}] #{name} Roster"] + players.map(&:to_s)).join("\n")
40
34
  end
41
35
  end
42
36
  end
@@ -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:, team:, player:, pick:, round:, round_pick:)
9
- super(id:, team:, 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