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
@@ -0,0 +1,191 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Basketball
4
+ module App
5
+ # Knows how to flatten a Coordinator instance and rehydrate one from JSON and/or a Ruby hash.
6
+ class CoordinatorRepository
7
+ GAME_CLASSES = {
8
+ 'Exhibition' => Season::Exhibition,
9
+ 'Regular' => Season::Regular
10
+ }.freeze
11
+
12
+ private_constant :GAME_CLASSES
13
+
14
+ attr_reader :store
15
+
16
+ def initialize(store: FileStore.new)
17
+ super()
18
+
19
+ @store = store
20
+
21
+ freeze
22
+ end
23
+
24
+ def load(path)
25
+ contents = store.read(path)
26
+
27
+ deserialize(contents).tap do |coordinator|
28
+ coordinator.send('id=', path)
29
+ end
30
+ end
31
+
32
+ def save(path, coordinator)
33
+ contents = serialize(coordinator)
34
+
35
+ store.write(path, contents)
36
+
37
+ coordinator.send('id=', path)
38
+
39
+ coordinator
40
+ end
41
+
42
+ private
43
+
44
+ def deserialize(string)
45
+ hash = JSON.parse(string, symbolize_names: true)
46
+
47
+ from_h(hash)
48
+ end
49
+
50
+ def serialize(object)
51
+ to_h(object).to_json
52
+ end
53
+
54
+ def from_h(hash)
55
+ Season::Coordinator.new(
56
+ calendar: deserialize_calendar(hash[:calendar]),
57
+ current_date: Date.parse(hash[:current_date]),
58
+ results: deserialize_results(hash[:results]),
59
+ league: deserialize_league(hash[:league])
60
+ )
61
+ end
62
+
63
+ def to_h(coordinator)
64
+ {
65
+ calendar: serialize_calendar(coordinator.calendar),
66
+ current_date: coordinator.current_date.to_s,
67
+ results: serialize_results(coordinator.results),
68
+ league: serialize_league(coordinator.league)
69
+ }
70
+ end
71
+
72
+ # Serialization
73
+
74
+ def serialize_player(player)
75
+ {
76
+ id: player.id,
77
+ overall: player.overall,
78
+ position: player.position&.code
79
+ }
80
+ end
81
+
82
+ def serialize_games(games)
83
+ games.map { |game| serialize_game(game) }
84
+ end
85
+
86
+ def serialize_calendar(calendar)
87
+ {
88
+ preseason_start_date: calendar.preseason_start_date.to_s,
89
+ preseason_end_date: calendar.preseason_end_date.to_s,
90
+ season_start_date: calendar.season_start_date.to_s,
91
+ season_end_date: calendar.season_end_date.to_s,
92
+ games: serialize_games(calendar.games)
93
+ }
94
+ end
95
+
96
+ def serialize_game(game)
97
+ {
98
+ type: game.class.name.split('::').last,
99
+ date: game.date.to_s,
100
+ home_opponent: game.home_opponent.id,
101
+ away_opponent: game.away_opponent.id
102
+ }
103
+ end
104
+
105
+ def serialize_result(result)
106
+ {
107
+ game: serialize_game(result.game),
108
+ home_score: result.home_score,
109
+ away_score: result.away_score
110
+ }
111
+ end
112
+
113
+ def serialize_league(league)
114
+ {
115
+ teams: league.teams.map { |team| serialize_team(team) }
116
+ }
117
+ end
118
+
119
+ def serialize_team(team)
120
+ {
121
+ id: team.id,
122
+ players: team.players.map { |player| serialize_player(player) }
123
+ }
124
+ end
125
+
126
+ def serialize_results(results)
127
+ results.map do |result|
128
+ serialize_result(result)
129
+ end
130
+ end
131
+
132
+ # Deserialization
133
+
134
+ def deserialize_player(player_hash)
135
+ Org::Player.new(
136
+ id: player_hash[:id],
137
+ overall: player_hash[:overall],
138
+ position: Org::Position.new(player_hash[:position])
139
+ )
140
+ end
141
+
142
+ def deserialize_league(league_hash)
143
+ team_hashes = league_hash[:teams] || []
144
+
145
+ teams = team_hashes.map do |team_hash|
146
+ players = (team_hash[:players] || []).map { |player_hash| deserialize_player(player_hash) }
147
+
148
+ Org::Team.new(id: team_hash[:id], players:)
149
+ end
150
+
151
+ Org::League.new(teams:)
152
+ end
153
+
154
+ def deserialize_calendar(calendar_hash)
155
+ Season::Calendar.new(
156
+ preseason_start_date: Date.parse(calendar_hash[:preseason_start_date]),
157
+ preseason_end_date: Date.parse(calendar_hash[:preseason_end_date]),
158
+ season_start_date: Date.parse(calendar_hash[:season_start_date]),
159
+ season_end_date: Date.parse(calendar_hash[:season_end_date]),
160
+ games: deserialize_games(calendar_hash[:games])
161
+ )
162
+ end
163
+
164
+ def deserialize_games(game_hashes)
165
+ (game_hashes || []).map { |game_hash| deserialize_game(game_hash) }
166
+ end
167
+
168
+ def deserialize_game(game_hash)
169
+ GAME_CLASSES.fetch(game_hash[:type]).new(
170
+ date: Date.parse(game_hash[:date]),
171
+ home_opponent: Season::Opponent.new(id: game_hash[:home_opponent]),
172
+ away_opponent: Season::Opponent.new(id: game_hash[:away_opponent])
173
+ )
174
+ end
175
+
176
+ def deserialize_results(result_hashes)
177
+ (result_hashes || []).map do |result_hash|
178
+ deserialize_result(result_hash)
179
+ end
180
+ end
181
+
182
+ def deserialize_result(result_hash)
183
+ Season::Result.new(
184
+ game: deserialize_game(result_hash[:game]),
185
+ home_score: result_hash[:home_score],
186
+ away_score: result_hash[:away_score]
187
+ )
188
+ end
189
+ end
190
+ end
191
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Basketball
4
+ module App
5
+ # Knows how to read and write documents to disk.
6
+ class FileStore
7
+ def read(path)
8
+ File.read(path)
9
+ end
10
+
11
+ def write(path, contents)
12
+ dir = File.dirname(path)
13
+
14
+ FileUtils.mkdir_p(dir)
15
+
16
+ File.write(path, contents)
17
+
18
+ nil
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,212 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Basketball
4
+ module App
5
+ # Examples:
6
+ # exe/basketball-room -o tmp/draft.json
7
+ # exe/basketball-room -i tmp/draft.json -o tmp/draft-wip.json -s 26 -p P-5,P-10 -t 10
8
+ # exe/basketball-room -i tmp/draft-wip.json -x 2
9
+ # exe/basketball-room -i tmp/draft-wip.json -g -t 10
10
+ # exe/basketball-room -i tmp/draft-wip.json -s 30 -t 10
11
+ # exe/basketball-room -i tmp/draft-wip.json -ale
12
+ #
13
+ # exe/basketball-room -o tmp/draft-wip.json -ale
14
+ class RoomCLI
15
+ class PlayerNotFound < StandardError; end
16
+
17
+ attr_reader :opts, :io, :repository
18
+
19
+ def initialize(args:, io: $stdout)
20
+ @io = io
21
+ @opts = slop_parse(args)
22
+ @repository = RoomRepository.new
23
+
24
+ if opts[:input].to_s.empty? && opts[:output].to_s.empty?
25
+ io.puts('Input and/or output paths are required.')
26
+
27
+ exit
28
+ end
29
+
30
+ freeze
31
+ end
32
+
33
+ def invoke!
34
+ room = load_room
35
+
36
+ execute(room)
37
+ status(room)
38
+ write(room)
39
+ events(room)
40
+ league(room)
41
+ query(room)
42
+
43
+ self
44
+ end
45
+
46
+ private
47
+
48
+ def status(room)
49
+ io.puts
50
+ io.puts('Status')
51
+
52
+ if room.done?
53
+ io.puts('Draft is complete!')
54
+ else
55
+ round = room.round
56
+ round_pick = room.round_pick
57
+ front_office = room.front_office
58
+
59
+ io.puts("#{room.remaining_picks} Remaining pick(s)")
60
+ io.puts("Up Next: Round #{round} pick #{round_pick} for #{front_office}")
61
+ end
62
+ end
63
+
64
+ def slop_parse(args)
65
+ Slop.parse(args) do |o|
66
+ o.banner = 'Usage: basketball-room [options] ...'
67
+
68
+ o.string '-i', '--input', 'Path to load the Room from. If omitted then a new draft will be generated.'
69
+ o.string '-o', '--output', 'Path to write the room to (if omitted then input path will be used)'
70
+ o.integer '-s', '--simulate', 'Number of picks to simulate (default is 0).', default: 0
71
+ o.bool '-a', '--simulate-all', 'Simulate the rest of the draft', default: false
72
+ o.array '-p', '--picks', 'Comma-separated list of ordered player IDs to pick.', delimiter: ','
73
+ o.integer '-t', '--top', 'Output the top rated available players (default is 0).', default: 0
74
+ o.bool '-l', '--league', 'Output all teams and their picks', default: false
75
+ o.integer '-x', '--skip', 'Number of picks to skip (default is 0).', default: 0
76
+ o.bool '-e', '--events', 'Output event log.', default: false
77
+
78
+ o.on '-h', '--help', 'Print out help, like this is doing right now.' do
79
+ io.puts(o)
80
+ exit
81
+ end
82
+ end.to_h
83
+ end
84
+
85
+ def load_room
86
+ if opts[:input].to_s.empty?
87
+ io.puts('Input path was not provided, generating fresh front_offices and players')
88
+
89
+ generate_draft
90
+ else
91
+ io.puts("Draft loaded from: #{opts[:input]}")
92
+
93
+ read
94
+ end
95
+ end
96
+
97
+ def generate_draft
98
+ front_offices = 30.times.map do |i|
99
+ Draft::FrontOffice.new(
100
+ id: "T-#{i + 1}"
101
+ )
102
+ end
103
+
104
+ players = 450.times.map do |i|
105
+ Org::Player.new(
106
+ id: "P-#{i + 1}",
107
+ overall: (20..100).to_a.sample,
108
+ position: Org::Position.random
109
+ )
110
+ end
111
+
112
+ Draft::Room.new(rounds: 12, players:, front_offices:)
113
+ end
114
+
115
+ def league(room)
116
+ return unless opts[:league]
117
+
118
+ io.puts
119
+ io.puts(room.league)
120
+ end
121
+
122
+ def events(room)
123
+ return unless opts[:events]
124
+
125
+ io.puts
126
+ io.puts('Event Log')
127
+
128
+ puts room.events
129
+ end
130
+
131
+ def query(room)
132
+ top = opts[:top]
133
+
134
+ return if top <= 0
135
+
136
+ players = room.undrafted_players.sort_by(&:overall).reverse.take(opts[:top])
137
+
138
+ io.puts
139
+ io.puts("Top #{top} available players")
140
+ io.puts(players)
141
+ end
142
+
143
+ def read
144
+ repository.load(opts[:input])
145
+ end
146
+
147
+ # rubocop:disable Metrics/AbcSize
148
+ def execute(room)
149
+ event_count = 0
150
+
151
+ io.puts
152
+ io.puts('New Events')
153
+
154
+ (opts[:picks] || []).each do |id|
155
+ break if room.done?
156
+
157
+ player = room.players.find { |p| p.id == id.to_s.upcase }
158
+
159
+ raise PlayerNotFound, "player not found by id: #{id}" unless player
160
+
161
+ event = room.pick!(player)
162
+
163
+ io.puts(event)
164
+
165
+ event_count += 1
166
+ end
167
+
168
+ opts[:skip].times do
169
+ event = room.skip!
170
+
171
+ io.puts(event)
172
+
173
+ event_count += 1
174
+ end
175
+
176
+ opts[:simulate].times do
177
+ room.sim!
178
+
179
+ event_count += 1
180
+ end
181
+
182
+ if opts[:simulate_all]
183
+ room.sim_rest! do |event|
184
+ io.puts(event)
185
+
186
+ event_count += 1
187
+ end
188
+ end
189
+
190
+ io.puts("Generated #{event_count} new event(s)")
191
+
192
+ nil
193
+ end
194
+ # rubocop:enable Metrics/AbcSize
195
+
196
+ def output_default_to_input
197
+ opts[:output].to_s.empty? ? opts[:input] : opts[:output]
198
+ end
199
+
200
+ def write(room)
201
+ output = output_default_to_input
202
+
203
+ repository.save(output, room)
204
+
205
+ io.puts
206
+ io.puts("Draft written to: #{output}")
207
+
208
+ nil
209
+ end
210
+ end
211
+ end
212
+ end
@@ -0,0 +1,189 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Basketball
4
+ module App
5
+ # Can load and save Room objects to JSON files.
6
+ class RoomRepository
7
+ PICK_EVENT = 'Pick'
8
+ SKIP_EVENT = 'Skip'
9
+
10
+ private_constant :PICK_EVENT, :SKIP_EVENT
11
+
12
+ attr_reader :store
13
+
14
+ def initialize(store: FileStore.new)
15
+ super()
16
+
17
+ @store = store
18
+
19
+ freeze
20
+ end
21
+
22
+ def load(path)
23
+ contents = store.read(path)
24
+
25
+ room = deserialize(contents)
26
+
27
+ room.send('id=', path)
28
+
29
+ room
30
+ end
31
+
32
+ def save(path, room)
33
+ contents = serialize(room)
34
+
35
+ store.write(path, contents)
36
+
37
+ room.send('id=', path)
38
+
39
+ room
40
+ end
41
+
42
+ private
43
+
44
+ def deserialize(string)
45
+ hash = JSON.parse(string, symbolize_names: true)
46
+
47
+ from_h(hash)
48
+ end
49
+
50
+ def serialize(object)
51
+ to_h(object).to_json
52
+ end
53
+
54
+ def from_h(hash)
55
+ front_offices = deserialize_front_offices(hash[:front_offices])
56
+ players = deserialize_players(hash[:players])
57
+ events = deserialize_events(hash[:events], players:, front_offices:)
58
+
59
+ Draft::Room.new(
60
+ rounds: hash[:rounds],
61
+ front_offices:,
62
+ players:,
63
+ events:
64
+ )
65
+ end
66
+
67
+ def to_h(room)
68
+ {
69
+ rounds: room.rounds,
70
+ front_offices: room.front_offices.map { |fo| serialize_front_office(fo) },
71
+ players: room.players.map { |p| serialize_player(p) },
72
+ events: serialize_events(room.events)
73
+ }
74
+ end
75
+
76
+ # Serialization
77
+
78
+ def serialize_player(player)
79
+ {
80
+ id: player.id,
81
+ overall: player.overall,
82
+ position: player.position&.code
83
+ }
84
+ end
85
+
86
+ def serialize_front_office(front_office)
87
+ {
88
+ id: front_office.id,
89
+ risk_level: front_office.risk_level
90
+ }
91
+ end
92
+
93
+ def serialize_events(events)
94
+ events.map do |event|
95
+ case event
96
+ when Draft::Pick
97
+ serialize_pick(event)
98
+ when Draft::Skip
99
+ serialize_skip(event)
100
+ end
101
+ end
102
+ end
103
+
104
+ def serialize_pick(event)
105
+ {
106
+ type: PICK_EVENT,
107
+ front_office: event.front_office.id,
108
+ pick: event.pick,
109
+ round: event.round,
110
+ round_pick: event.round_pick,
111
+ auto: event.auto,
112
+ player: event.player.id
113
+ }
114
+ end
115
+
116
+ def serialize_skip(event)
117
+ {
118
+ type: SKIP_EVENT,
119
+ front_office: event.front_office.id,
120
+ pick: event.pick,
121
+ round: event.round,
122
+ round_pick: event.round_pick
123
+ }
124
+ end
125
+
126
+ # Deserialization
127
+
128
+ def deserialize_player(player_hash)
129
+ Org::Player.new(
130
+ id: player_hash[:id],
131
+ overall: player_hash[:overall],
132
+ position: Org::Position.new(player_hash[:position])
133
+ )
134
+ end
135
+
136
+ def deserialize_front_office(hash)
137
+ Draft::FrontOffice.new(
138
+ id: hash[:id],
139
+ risk_level: hash[:risk_level]
140
+ )
141
+ end
142
+
143
+ def deserialize_front_offices(hashes)
144
+ (hashes || []).map { |fo| deserialize_front_office(fo) }
145
+ end
146
+
147
+ def deserialize_players(hashes)
148
+ (hashes || []).map { |hash| deserialize_player(hash) }
149
+ end
150
+
151
+ def deserialize_pick(hash, players:, front_office:)
152
+ player_id = hash[:player]
153
+ player = players.find { |p| p.id == player_id }
154
+
155
+ Draft::Pick.new(
156
+ front_office:,
157
+ pick: hash[:pick],
158
+ round: hash[:round],
159
+ round_pick: hash[:round_pick],
160
+ player:,
161
+ auto: hash[:auto]
162
+ )
163
+ end
164
+
165
+ def deserialize_skip(hash, front_office:)
166
+ Draft::Skip.new(
167
+ front_office:,
168
+ pick: hash[:pick],
169
+ round: hash[:round],
170
+ round_pick: hash[:round_pick]
171
+ )
172
+ end
173
+
174
+ def deserialize_events(hashes, players:, front_offices:)
175
+ (hashes || []).map do |hash|
176
+ front_office_id = hash[:front_office]
177
+ front_office = front_offices.find { |fo| fo.id == front_office_id }
178
+
179
+ case hash[:type]
180
+ when PICK_EVENT
181
+ deserialize_pick(hash, players:, front_office:)
182
+ when SKIP_EVENT
183
+ deserialize_skip(hash, front_office:)
184
+ end
185
+ end
186
+ end
187
+ end
188
+ end
189
+ end
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Services
4
+ require_relative 'app/file_store'
5
+
6
+ # Repositories
7
+ require_relative 'app/coordinator_repository'
8
+ require_relative 'app/room_repository'
9
+
10
+ # Controllers
11
+ require_relative 'app/coordinator_cli'
12
+ require_relative 'app/room_cli'
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Basketball
4
+ module Draft
5
+ # An assessment is given to a front office when it needs the front office to make a pick.
6
+ # This is essentially just a data-transfer object to get information to a front office to make a pick.
7
+ class Assessment
8
+ attr_reader :drafted_players,
9
+ :pick,
10
+ :round_pick,
11
+ :round,
12
+ :undrafted_players
13
+
14
+ def initialize(
15
+ drafted_players:,
16
+ pick:,
17
+ round_pick:,
18
+ round:,
19
+ undrafted_players:
20
+ )
21
+ @drafted_players = drafted_players
22
+ @pick = pick
23
+ @round = round
24
+ @round_pick = round_pick
25
+ @undrafted_players = undrafted_players
26
+
27
+ freeze
28
+ end
29
+ end
30
+ end
31
+ end
@@ -1,9 +1,10 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Basketball
4
- module Drafting
4
+ module Draft
5
+ # Describes what all Room events have to have to be considered an "event".
5
6
  class Event < ValueObject
6
- attr_reader_value :pick, :round, :round_pick, :front_office
7
+ value_reader :pick, :round, :round_pick, :front_office
7
8
 
8
9
  def initialize(front_office:, pick:, round:, round_pick:)
9
10
  super()
@@ -17,7 +18,7 @@ module Basketball
17
18
  end
18
19
 
19
20
  def to_s
20
- "##{pick} overall (R#{round}:P#{round_pick}) by #{front_office}"
21
+ "[##{pick} R:#{round} P:#{round_pick}] #{front_office}"
21
22
  end
22
23
  end
23
24
  end