basketball 0.0.7 → 0.0.9

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 (64) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +9 -19
  3. data/CHANGELOG.md +1 -31
  4. data/README.md +72 -91
  5. data/basketball.gemspec +3 -6
  6. data/exe/{basketball-schedule → basketball-coordinator} +1 -1
  7. data/exe/{basketball-draft → basketball-room} +1 -1
  8. data/lib/basketball/app/coordinator_cli.rb +243 -0
  9. data/lib/basketball/app/coordinator_repository.rb +191 -0
  10. data/lib/basketball/app/file_store.rb +22 -0
  11. data/lib/basketball/app/room_cli.rb +212 -0
  12. data/lib/basketball/app/room_repository.rb +189 -0
  13. data/lib/basketball/app.rb +12 -0
  14. data/lib/basketball/draft/assessment.rb +31 -0
  15. data/lib/basketball/{drafting → draft}/event.rb +4 -3
  16. data/lib/basketball/draft/front_office.rb +99 -0
  17. data/lib/basketball/draft/pick.rb +32 -0
  18. data/lib/basketball/draft/room.rb +221 -0
  19. data/lib/basketball/{drafting/player_search.rb → draft/scout.rb} +5 -10
  20. data/lib/basketball/draft/skip.rb +12 -0
  21. data/lib/basketball/draft.rb +16 -0
  22. data/lib/basketball/entity.rb +10 -4
  23. data/lib/basketball/org/league.rb +73 -0
  24. data/lib/basketball/org/player.rb +26 -0
  25. data/lib/basketball/{drafting → org}/position.rb +3 -2
  26. data/lib/basketball/org/team.rb +38 -0
  27. data/lib/basketball/org.rb +12 -0
  28. data/lib/basketball/season/arena.rb +112 -0
  29. data/lib/basketball/season/calendar.rb +90 -0
  30. data/lib/basketball/season/coordinator.rb +239 -0
  31. data/lib/basketball/{scheduling/preseason_game.rb → season/exhibition.rb} +3 -2
  32. data/lib/basketball/season/game.rb +37 -0
  33. data/lib/basketball/season/matchup.rb +27 -0
  34. data/lib/basketball/season/opponent.rb +15 -0
  35. data/lib/basketball/season/regular.rb +9 -0
  36. data/lib/basketball/season/result.rb +37 -0
  37. data/lib/basketball/season.rb +16 -0
  38. data/lib/basketball/value_object.rb +4 -1
  39. data/lib/basketball/version.rb +1 -1
  40. data/lib/basketball.rb +11 -6
  41. metadata +40 -52
  42. data/lib/basketball/drafting/cli.rb +0 -235
  43. data/lib/basketball/drafting/engine.rb +0 -221
  44. data/lib/basketball/drafting/engine_serializer.rb +0 -186
  45. data/lib/basketball/drafting/front_office.rb +0 -92
  46. data/lib/basketball/drafting/league.rb +0 -70
  47. data/lib/basketball/drafting/pick_event.rb +0 -25
  48. data/lib/basketball/drafting/player.rb +0 -43
  49. data/lib/basketball/drafting/roster.rb +0 -37
  50. data/lib/basketball/drafting/sim_event.rb +0 -23
  51. data/lib/basketball/drafting/skip_event.rb +0 -13
  52. data/lib/basketball/drafting.rb +0 -9
  53. data/lib/basketball/scheduling/calendar.rb +0 -121
  54. data/lib/basketball/scheduling/calendar_serializer.rb +0 -94
  55. data/lib/basketball/scheduling/cli.rb +0 -198
  56. data/lib/basketball/scheduling/conference.rb +0 -57
  57. data/lib/basketball/scheduling/coordinator.rb +0 -180
  58. data/lib/basketball/scheduling/division.rb +0 -43
  59. data/lib/basketball/scheduling/game.rb +0 -32
  60. data/lib/basketball/scheduling/league.rb +0 -114
  61. data/lib/basketball/scheduling/league_serializer.rb +0 -99
  62. data/lib/basketball/scheduling/season_game.rb +0 -8
  63. data/lib/basketball/scheduling/team.rb +0 -21
  64. data/lib/basketball/scheduling.rb +0 -17
@@ -1,186 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require_relative 'front_office'
4
- require_relative 'player'
5
- require_relative 'pick_event'
6
- require_relative 'sim_event'
7
- require_relative 'skip_event'
8
-
9
- module Basketball
10
- module Drafting
11
- class EngineSerializer
12
- EVENT_CLASSES = {
13
- 'PickEvent' => PickEvent,
14
- 'SimEvent' => SimEvent,
15
- 'SkipEvent' => SkipEvent
16
- }.freeze
17
-
18
- private_constant :EVENT_CLASSES
19
-
20
- def to_hash(engine)
21
- {
22
- 'info' => serialize_info(engine),
23
- 'engine' => serialize_engine(engine),
24
- 'league' => serialize_league(engine)
25
- }
26
- end
27
-
28
- def from_hash(json)
29
- front_offices = deserialize_front_offices(json)
30
- players = deserialize_players(json)
31
- events = deserialize_events(json, players, front_offices)
32
-
33
- engine_opts = {
34
- players:,
35
- front_offices:,
36
- events:
37
- }
38
-
39
- engine_opts[:rounds] = json.dig('engine', 'rounds') if json.dig('engine', 'rounds')
40
-
41
- Engine.new(**engine_opts)
42
- end
43
-
44
- def deserialize(string)
45
- json = JSON.parse(string)
46
-
47
- from_hash(json)
48
- end
49
-
50
- def serialize(engine)
51
- to_hash(engine).to_json
52
- end
53
-
54
- private
55
-
56
- def serialize_engine(engine)
57
- {
58
- 'rounds' => engine.rounds,
59
- 'front_offices' => serialize_front_offices(engine),
60
- 'players' => serialize_players(engine),
61
- 'events' => serialize_events(engine.events)
62
- }
63
- end
64
-
65
- def serialize_info(engine)
66
- {
67
- 'total_picks' => engine.total_picks,
68
- 'current_round' => engine.current_round,
69
- 'current_round_pick' => engine.current_round_pick,
70
- 'current_front_office' => engine.current_front_office&.id,
71
- 'current_pick' => engine.current_pick,
72
- 'remaining_picks' => engine.remaining_picks,
73
- 'done' => engine.done?
74
- }
75
- end
76
-
77
- def serialize_league(engine)
78
- league = engine.to_league
79
-
80
- rosters = league.rosters.to_h do |roster|
81
- [
82
- roster.id,
83
- {
84
- 'players' => roster.players.map(&:id)
85
- }
86
- ]
87
- end
88
-
89
- {
90
- 'free_agents' => league.free_agents.map(&:id),
91
- 'rosters' => rosters
92
- }
93
- end
94
-
95
- def serialize_front_offices(engine)
96
- engine.front_offices.to_h do |front_office|
97
- [
98
- front_office.id,
99
- {
100
- 'name' => front_office.name,
101
- 'fuzz' => front_office.fuzz,
102
- 'depth' => front_office.depth,
103
- 'prioritized_positions' => front_office.prioritized_positions.map(&:code)
104
- }
105
- ]
106
- end
107
- end
108
-
109
- def serialize_players(engine)
110
- engine.players.to_h do |player|
111
- [
112
- player.id,
113
- {
114
- 'first_name' => player.first_name,
115
- 'last_name' => player.last_name,
116
- 'overall' => player.overall,
117
- 'position' => player.position.code
118
- }
119
- ]
120
- end
121
- end
122
-
123
- def serialize_events(events)
124
- events.map do |event|
125
- {
126
- 'type' => event.class.name.split('::').last,
127
- 'front_office' => event.front_office.id,
128
- 'pick' => event.pick,
129
- 'round' => event.round,
130
- 'round_pick' => event.round_pick
131
- }.tap do |hash|
132
- hash['player'] = event.player.id if event.respond_to?(:player)
133
- end
134
- end
135
- end
136
-
137
- def deserialize_front_offices(json)
138
- (json.dig('engine', 'front_offices') || []).map do |id, front_office_hash|
139
- prioritized_positions = (front_office_hash['prioritized_positions'] || []).map do |v|
140
- Position.new(v)
141
- end
142
-
143
- front_office_opts = {
144
- id:,
145
- name: front_office_hash['name'],
146
- prioritized_positions:,
147
- fuzz: front_office_hash['fuzz'],
148
- depth: front_office_hash['depth']
149
- }
150
-
151
- FrontOffice.new(**front_office_opts)
152
- end
153
- end
154
-
155
- def deserialize_players(json)
156
- (json.dig('engine', 'players') || []).map do |id, player_hash|
157
- player_opts = {
158
- id:,
159
- first_name: player_hash['first_name'],
160
- last_name: player_hash['last_name'],
161
- overall: player_hash['overall'],
162
- position: Position.new(player_hash['position'])
163
- }
164
-
165
- Player.new(**player_opts)
166
- end
167
- end
168
-
169
- def deserialize_events(json, players, front_offices)
170
- (json.dig('engine', 'events') || []).map do |event_hash|
171
- event_opts = event_hash.slice('pick', 'round', 'round_pick').merge(
172
- front_office: front_offices.find { |t| t.id == event_hash['front_office'] }
173
- )
174
-
175
- class_constant = EVENT_CLASSES.fetch(event_hash['type'])
176
-
177
- if [PickEvent, SimEvent].include?(class_constant)
178
- event_opts[:player] = players.find { |p| p.id == event_hash['player'] }
179
- end
180
-
181
- class_constant.new(**event_opts)
182
- end
183
- end
184
- end
185
- end
186
- end
@@ -1,92 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Basketball
4
- module Drafting
5
- class FrontOffice < Entity
6
- MAX_DEPTH = 3
7
- MAX_FUZZ = 2
8
- MAX_POSITIONS = 12
9
-
10
- private_constant :MAX_DEPTH, :MAX_FUZZ, :MAX_POSITIONS
11
-
12
- attr_reader :prioritized_positions, :fuzz, :depth, :name
13
-
14
- def initialize(id:, name: '', prioritized_positions: [], fuzz: rand(0..MAX_FUZZ), depth: rand(0..MAX_DEPTH))
15
- super(id)
16
-
17
- @name = name
18
- @fuzz = fuzz.to_i
19
- @depth = depth.to_i
20
- @prioritized_positions = prioritized_positions
21
-
22
- # fill in the rest of the queue here
23
- need_count = MAX_POSITIONS - @prioritized_positions.length
24
-
25
- @prioritized_positions += random_positions_queue[0...need_count]
26
-
27
- freeze
28
- end
29
-
30
- def pick(undrafted_player_search:, drafted_players:, round:)
31
- players = []
32
-
33
- players = adaptive_search(undrafted_player_search:, drafted_players:) if depth >= round
34
- players = balanced_search(undrafted_player_search:, drafted_players:) if players.empty?
35
- players = top_players(undrafted_player_search:) if players.empty?
36
-
37
- players[0..fuzz].sample
38
- end
39
-
40
- def to_s
41
- "[#{super}] #{name}"
42
- end
43
-
44
- private
45
-
46
- def adaptive_search(undrafted_player_search:, drafted_players:)
47
- drafted_positions = drafted_players.map(&:position)
48
-
49
- undrafted_player_search.query(exclude_positions: drafted_positions)
50
- end
51
-
52
- def balanced_search(undrafted_player_search:, drafted_players:)
53
- players = []
54
-
55
- # Try to find best pick for exact desired position.
56
- # If you cant find one, then move to the next desired position until the end of the queue
57
- available_prioritized_positions(drafted_players:).each do |position|
58
- players = undrafted_player_search.query(position:)
59
-
60
- break if players.any?
61
- end
62
-
63
- players = players.any? ? players : undrafted_player_search.query
64
- end
65
-
66
- def all_random_positions
67
- Position::ALL_VALUES.to_a.shuffle.map { |v| Position.new(v) }
68
- end
69
-
70
- def random_positions_queue
71
- all_random_positions + all_random_positions + [Position.random] + [Position.random]
72
- end
73
-
74
- def available_prioritized_positions(drafted_players:)
75
- drafted_positions = drafted_players.map(&:position)
76
- queue = prioritized_positions.dup
77
-
78
- drafted_positions.each do |drafted_position|
79
- index = queue.index(drafted_position)
80
-
81
- next unless index
82
-
83
- queue.delete_at(index)
84
-
85
- queue << drafted_position
86
- end
87
-
88
- queue
89
- end
90
- end
91
- end
92
- end
@@ -1,70 +0,0 @@
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
@@ -1,25 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require_relative 'event'
4
-
5
- module Basketball
6
- module Drafting
7
- class PickEvent < Event
8
- attr_reader_value :player
9
-
10
- def initialize(front_office:, player:, pick:, round:, round_pick:)
11
- super(front_office:, pick:, round:, round_pick:)
12
-
13
- raise ArgumentError, 'player required' unless player
14
-
15
- @player = player
16
-
17
- freeze
18
- end
19
-
20
- def to_s
21
- "#{player} picked #{super}"
22
- end
23
- end
24
- end
25
- end
@@ -1,43 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Basketball
4
- module Drafting
5
- class Player < Entity
6
- STAR_THRESHOLD = 75
7
- OVERALL_STAR_INDICATOR = '⭐'
8
-
9
- private_constant :STAR_THRESHOLD, :OVERALL_STAR_INDICATOR
10
-
11
- attr_reader :first_name, :last_name, :position, :overall
12
-
13
- def initialize(id:, position:, first_name: '', last_name: '', overall: 0)
14
- super(id)
15
-
16
- raise ArgumentError, 'position is required' unless position
17
-
18
- @first_name = first_name.to_s
19
- @last_name = last_name.to_s
20
- @position = position
21
- @overall = overall.to_i
22
-
23
- freeze
24
- end
25
-
26
- def full_name
27
- "#{first_name.strip} #{last_name.strip}".strip
28
- end
29
-
30
- def to_s
31
- "[#{super}] #{full_name} (#{position}) #{overall} #{star_indicators.join(', ')}".strip
32
- end
33
-
34
- private
35
-
36
- def star_indicators
37
- [].tap do |indicators|
38
- indicators << OVERALL_STAR_INDICATOR if overall >= STAR_THRESHOLD
39
- end
40
- end
41
- end
42
- end
43
- end
@@ -1,37 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Basketball
4
- module Drafting
5
- class Roster < Entity
6
- class PlayerRequiredError < StandardError; end
7
-
8
- attr_reader :name, :players
9
-
10
- def initialize(id:, name: '', players: [])
11
- super(id)
12
-
13
- @name = name.to_s
14
- @players = players.each { |p| register!(p) }
15
-
16
- freeze
17
- end
18
-
19
- def registered?(player)
20
- players.include?(player)
21
- end
22
-
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
30
- end
31
-
32
- def to_s
33
- (["[#{super}] #{name} Roster"] + players.map(&:to_s)).join("\n")
34
- end
35
- end
36
- end
37
- end
@@ -1,23 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Basketball
4
- module Drafting
5
- class SimEvent < Event
6
- attr_reader_value :player
7
-
8
- def initialize(front_office:, player:, pick:, round:, round_pick:)
9
- super(front_office:, pick:, round:, round_pick:)
10
-
11
- raise ArgumentError, 'player required' unless player
12
-
13
- @player = player
14
-
15
- freeze
16
- end
17
-
18
- def to_s
19
- "#{player} auto-picked #{super}"
20
- end
21
- end
22
- end
23
- end
@@ -1,13 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require_relative 'event'
4
-
5
- module Basketball
6
- module Drafting
7
- class SkipEvent < Event
8
- def to_s
9
- "skipped #{super}"
10
- end
11
- end
12
- end
13
- end
@@ -1,9 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require_relative 'drafting/cli'
4
-
5
- module Basketball
6
- module Drafting
7
- class PlayerAlreadyRegisteredError < StandardError; end
8
- end
9
- end
@@ -1,121 +0,0 @@
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