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
@@ -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