basketball 0.0.4 → 0.0.6

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