basketball 0.0.7 → 0.0.9

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