basketball 0.0.8 → 0.0.10

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 (59) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +9 -19
  3. data/CHANGELOG.md +1 -39
  4. data/README.md +75 -93
  5. data/basketball.gemspec +3 -6
  6. data/exe/{basketball-season-scheduling → basketball-coordinator} +1 -1
  7. data/exe/{basketball-draft → basketball-room} +1 -1
  8. data/lib/basketball/app/coordinator_cli.rb +250 -0
  9. data/lib/basketball/app/coordinator_repository.rb +114 -0
  10. data/lib/basketball/app/document_repository.rb +67 -0
  11. data/lib/basketball/app/file_store.rb +38 -0
  12. data/lib/basketball/app/in_memory_store.rb +42 -0
  13. data/lib/basketball/app/league_repository.rb +20 -0
  14. data/lib/basketball/app/league_serializable.rb +54 -0
  15. data/lib/basketball/{draft/cli.rb → app/room_cli.rb} +74 -80
  16. data/lib/basketball/app/room_repository.rb +149 -0
  17. data/lib/basketball/app.rb +20 -0
  18. data/lib/basketball/draft/assessment.rb +31 -0
  19. data/lib/basketball/draft/event.rb +3 -2
  20. data/lib/basketball/draft/front_office.rb +35 -28
  21. data/lib/basketball/draft/{pick_event.rb → pick.rb} +13 -6
  22. data/lib/basketball/draft/room.rb +119 -119
  23. data/lib/basketball/draft/{player_search.rb → scout.rb} +4 -9
  24. data/lib/basketball/draft/skip.rb +12 -0
  25. data/lib/basketball/draft.rb +13 -6
  26. data/lib/basketball/entity.rb +19 -10
  27. data/lib/basketball/org/league.rb +68 -0
  28. data/lib/basketball/org/player.rb +26 -0
  29. data/lib/basketball/{draft → org}/position.rb +3 -2
  30. data/lib/basketball/org/team.rb +38 -0
  31. data/lib/basketball/org.rb +12 -0
  32. data/lib/basketball/season/arena.rb +113 -0
  33. data/lib/basketball/season/calendar.rb +41 -72
  34. data/lib/basketball/season/coordinator.rb +186 -128
  35. data/lib/basketball/season/{preseason_game.rb → exhibition.rb} +2 -1
  36. data/lib/basketball/season/game.rb +15 -10
  37. data/lib/basketball/season/matchup.rb +27 -0
  38. data/lib/basketball/season/opponent.rb +15 -0
  39. data/lib/basketball/season/{season_game.rb → regular.rb} +2 -1
  40. data/lib/basketball/season/result.rb +37 -0
  41. data/lib/basketball/season.rb +12 -13
  42. data/lib/basketball/value_object.rb +8 -27
  43. data/lib/basketball/value_object_dsl.rb +30 -0
  44. data/lib/basketball/version.rb +1 -1
  45. data/lib/basketball.rb +9 -4
  46. metadata +37 -44
  47. data/lib/basketball/draft/league.rb +0 -70
  48. data/lib/basketball/draft/player.rb +0 -43
  49. data/lib/basketball/draft/room_serializer.rb +0 -186
  50. data/lib/basketball/draft/roster.rb +0 -37
  51. data/lib/basketball/draft/sim_event.rb +0 -23
  52. data/lib/basketball/draft/skip_event.rb +0 -13
  53. data/lib/basketball/season/calendar_serializer.rb +0 -94
  54. data/lib/basketball/season/conference.rb +0 -57
  55. data/lib/basketball/season/division.rb +0 -43
  56. data/lib/basketball/season/league.rb +0 -114
  57. data/lib/basketball/season/league_serializer.rb +0 -99
  58. data/lib/basketball/season/scheduling_cli.rb +0 -198
  59. data/lib/basketball/season/team.rb +0 -21
@@ -0,0 +1,114 @@
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 < DocumentRepository
7
+ include LeagueSerializable
8
+
9
+ GAME_CLASSES = {
10
+ 'Exhibition' => Season::Exhibition,
11
+ 'Regular' => Season::Regular
12
+ }.freeze
13
+
14
+ private_constant :GAME_CLASSES
15
+
16
+ private
17
+
18
+ def from_h(hash)
19
+ Season::Coordinator.new(
20
+ calendar: deserialize_calendar(hash[:calendar]),
21
+ current_date: Date.parse(hash[:current_date]),
22
+ results: deserialize_results(hash[:results]),
23
+ league: deserialize_league(hash[:league])
24
+ )
25
+ end
26
+
27
+ def to_h(coordinator)
28
+ {
29
+ calendar: serialize_calendar(coordinator.calendar),
30
+ current_date: coordinator.current_date.to_s,
31
+ results: serialize_results(coordinator.results),
32
+ league: serialize_league(coordinator.league)
33
+ }
34
+ end
35
+
36
+ # Serialization
37
+
38
+ def serialize_games(games)
39
+ games.map { |game| serialize_game(game) }
40
+ end
41
+
42
+ def serialize_calendar(calendar)
43
+ {
44
+ preseason_start_date: calendar.preseason_start_date.to_s,
45
+ preseason_end_date: calendar.preseason_end_date.to_s,
46
+ season_start_date: calendar.season_start_date.to_s,
47
+ season_end_date: calendar.season_end_date.to_s,
48
+ games: serialize_games(calendar.games)
49
+ }
50
+ end
51
+
52
+ def serialize_game(game)
53
+ {
54
+ type: game.class.name.split('::').last,
55
+ date: game.date.to_s,
56
+ home_opponent: game.home_opponent.id,
57
+ away_opponent: game.away_opponent.id
58
+ }
59
+ end
60
+
61
+ def serialize_result(result)
62
+ {
63
+ game: serialize_game(result.game),
64
+ home_score: result.home_score,
65
+ away_score: result.away_score
66
+ }
67
+ end
68
+
69
+ def serialize_results(results)
70
+ results.map do |result|
71
+ serialize_result(result)
72
+ end
73
+ end
74
+
75
+ # Deserialization
76
+
77
+ def deserialize_calendar(calendar_hash)
78
+ Season::Calendar.new(
79
+ preseason_start_date: Date.parse(calendar_hash[:preseason_start_date]),
80
+ preseason_end_date: Date.parse(calendar_hash[:preseason_end_date]),
81
+ season_start_date: Date.parse(calendar_hash[:season_start_date]),
82
+ season_end_date: Date.parse(calendar_hash[:season_end_date]),
83
+ games: deserialize_games(calendar_hash[:games])
84
+ )
85
+ end
86
+
87
+ def deserialize_games(game_hashes)
88
+ (game_hashes || []).map { |game_hash| deserialize_game(game_hash) }
89
+ end
90
+
91
+ def deserialize_game(game_hash)
92
+ GAME_CLASSES.fetch(game_hash[:type]).new(
93
+ date: Date.parse(game_hash[:date]),
94
+ home_opponent: Season::Opponent.new(id: game_hash[:home_opponent]),
95
+ away_opponent: Season::Opponent.new(id: game_hash[:away_opponent])
96
+ )
97
+ end
98
+
99
+ def deserialize_results(result_hashes)
100
+ (result_hashes || []).map do |result_hash|
101
+ deserialize_result(result_hash)
102
+ end
103
+ end
104
+
105
+ def deserialize_result(result_hash)
106
+ Season::Result.new(
107
+ game: deserialize_game(result_hash[:game]),
108
+ home_score: result_hash[:home_score],
109
+ away_score: result_hash[:away_score]
110
+ )
111
+ end
112
+ end
113
+ end
114
+ end
@@ -0,0 +1,67 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Basketball
4
+ module App
5
+ # Base class for all repositories which are based on a flat document.
6
+ # At the very minimum sub-classes should implement #to_h(object) and #from_h(hash).
7
+ class DocumentRepository
8
+ attr_reader :store
9
+
10
+ def initialize(store = InMemoryStore.new)
11
+ super()
12
+
13
+ @store = store
14
+ end
15
+
16
+ def load(id)
17
+ contents = store.read(id)
18
+
19
+ deserialize(contents).tap do |object|
20
+ object.send('id=', id)
21
+ end
22
+ end
23
+
24
+ def save(id, object)
25
+ object.send('id=', id)
26
+
27
+ contents = serialize(object)
28
+
29
+ store.write(id, contents)
30
+
31
+ object
32
+ end
33
+
34
+ def delete(object)
35
+ return false unless object.id
36
+
37
+ store.delete(object.id)
38
+
39
+ object.send('id=', nil)
40
+
41
+ true
42
+ end
43
+
44
+ protected
45
+
46
+ def from_h(hash)
47
+ Entity.new(hash[:id])
48
+ end
49
+
50
+ def to_h(entity)
51
+ { id: entity.id }
52
+ end
53
+
54
+ private
55
+
56
+ def deserialize(string)
57
+ hash = JSON.parse(string, symbolize_names: true)
58
+
59
+ from_h(hash)
60
+ end
61
+
62
+ def serialize(object)
63
+ to_h(object).to_json
64
+ end
65
+ end
66
+ end
67
+ end
@@ -0,0 +1,38 @@
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
+ class PathNotFoundError < StandardError; end
8
+
9
+ def exist?(path)
10
+ File.exist?(path)
11
+ end
12
+
13
+ def read(path)
14
+ raise PathNotFoundError, "'#{path}' not found" unless exist?(path)
15
+
16
+ File.read(path)
17
+ end
18
+
19
+ def write(path, contents)
20
+ dir = File.dirname(path)
21
+
22
+ FileUtils.mkdir_p(dir)
23
+
24
+ File.write(path, contents)
25
+
26
+ nil
27
+ end
28
+
29
+ def delete(path)
30
+ raise PathNotFoundError, "'#{path}' not found" unless exist?(path)
31
+
32
+ File.delete(path)
33
+
34
+ nil
35
+ end
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Basketball
4
+ module App
5
+ # Knows how to read and write documents to a Ruby Hash.
6
+ class InMemoryStore
7
+ class KeyNotFoundError < StandardError; end
8
+
9
+ attr_reader :data
10
+
11
+ def initialize(data = {})
12
+ @data = data
13
+
14
+ freeze
15
+ end
16
+
17
+ def exist?(key)
18
+ data.key?(key.to_s)
19
+ end
20
+
21
+ def read(key)
22
+ raise KeyNotFoundError, "'#{key}' not found" unless exist?(key)
23
+
24
+ data[key.to_s]
25
+ end
26
+
27
+ def write(key, contents)
28
+ data[key.to_s] = contents
29
+
30
+ nil
31
+ end
32
+
33
+ def delete(key)
34
+ raise KeyNotFoundError, "'#{key}' not found" unless exist?(key)
35
+
36
+ data.delete(key)
37
+
38
+ nil
39
+ end
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Basketball
4
+ module App
5
+ # Knows how to flatten a League instance and rehydrate one from JSON and/or a Ruby hash.
6
+ class LeagueRepository < DocumentRepository
7
+ include LeagueSerializable
8
+
9
+ private
10
+
11
+ def from_h(hash)
12
+ deserialize_league(hash)
13
+ end
14
+
15
+ def to_h(league)
16
+ serialize_league(league)
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,54 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Basketball
4
+ module App
5
+ # Provides methods to serialize/deserialize a League object
6
+ module LeagueSerializable
7
+ # Serialization
8
+
9
+ def serialize_league(league)
10
+ {
11
+ teams: league.teams.map { |team| serialize_team(team) }
12
+ }
13
+ end
14
+
15
+ def serialize_team(team)
16
+ {
17
+ id: team.id,
18
+ players: team.players.map { |player| serialize_player(player) }
19
+ }
20
+ end
21
+
22
+ def serialize_player(player)
23
+ {
24
+ id: player.id,
25
+ overall: player.overall,
26
+ position: player.position&.code
27
+ }
28
+ end
29
+
30
+ # Deserialization
31
+
32
+ def deserialize_league(league_hash)
33
+ team_hashes = league_hash[:teams] || []
34
+ teams = team_hashes.map { |team_hash| deserialize_team(team_hash) }
35
+
36
+ Org::League.new(teams:)
37
+ end
38
+
39
+ def deserialize_team(team_hash)
40
+ players = (team_hash[:players] || []).map { |player_hash| deserialize_player(player_hash) }
41
+
42
+ Org::Team.new(id: team_hash[:id], players:)
43
+ end
44
+
45
+ def deserialize_player(player_hash)
46
+ Org::Player.new(
47
+ id: player_hash[:id],
48
+ overall: player_hash[:overall],
49
+ position: Org::Position.new(player_hash[:position])
50
+ )
51
+ end
52
+ end
53
+ end
54
+ end
@@ -1,30 +1,34 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative 'room'
4
- require_relative 'room_serializer'
5
- require_relative 'player_search'
6
- require_relative 'position'
7
-
8
3
  module Basketball
9
- module Draft
4
+ module App
10
5
  # Examples:
11
- # exe/basketball-draft -o tmp/draft.json
12
- # exe/basketball-draft -i tmp/draft.json -o tmp/draft-wip.json -s 26 -p P-5,P-10 -t 10 -q PG
13
- # exe/basketball-draft -i tmp/draft-wip.json -x 2
14
- # exe/basketball-draft -i tmp/draft-wip.json -r -t 10
15
- # exe/basketball-draft -i tmp/draft-wip.json -t 10 -q SG
16
- # exe/basketball-draft -i tmp/draft-wip.json -s 30 -t 10
17
- # exe/basketball-draft -i tmp/draft-wip.json -a -r
18
- # exe/basketball-draft -i tmp/draft-wip.json -l
19
- class CLI
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 -r tmp/draft-league.json
14
+ class RoomCLI
20
15
  class PlayerNotFound < StandardError; end
21
16
 
22
- attr_reader :opts, :serializer, :io
23
-
24
- def initialize(args:, io: $stdout)
25
- @io = io
26
- @serializer = RoomSerializer.new
27
- @opts = slop_parse(args)
17
+ attr_reader :opts,
18
+ :io,
19
+ :room_repository,
20
+ :league_repository
21
+
22
+ def initialize(
23
+ args:,
24
+ io: $stdout,
25
+ room_repository: RoomRepository.new(FileStore.new),
26
+ league_repository: LeagueRepository.new(FileStore.new)
27
+ )
28
+ @io = io
29
+ @opts = slop_parse(args)
30
+ @room_repository = room_repository
31
+ @league_repository = league_repository
28
32
 
29
33
  if opts[:input].to_s.empty? && opts[:output].to_s.empty?
30
34
  io.puts('Input and/or output paths are required.')
@@ -39,49 +43,54 @@ module Basketball
39
43
  room = load_room
40
44
 
41
45
  execute(room)
46
+ status(room)
47
+ write(room)
48
+ events(room)
49
+ league(room)
50
+ query(room)
51
+ rosters(room)
52
+
53
+ self
54
+ end
55
+
56
+ private
57
+
58
+ def rosters(room)
59
+ return if opts[:rosters].to_s.empty?
42
60
 
61
+ league_repository.save(opts[:rosters], room.league)
62
+ end
63
+
64
+ def status(room)
43
65
  io.puts
44
66
  io.puts('Status')
45
67
 
46
68
  if room.done?
47
69
  io.puts('Draft is complete!')
48
70
  else
49
- current_round = room.current_round
50
- current_round_pick = room.current_round_pick
51
- current_front_office = room.current_front_office
71
+ round = room.round
72
+ round_pick = room.round_pick
73
+ front_office = room.front_office
52
74
 
53
75
  io.puts("#{room.remaining_picks} Remaining pick(s)")
54
- io.puts("Up Next: Round #{current_round} pick #{current_round_pick} for #{current_front_office}")
76
+ io.puts("Up Next: Round #{round} pick #{round_pick} for #{front_office}")
55
77
  end
56
-
57
- write(room)
58
-
59
- log(room)
60
-
61
- league(room)
62
-
63
- query(room)
64
-
65
- self
66
78
  end
67
79
 
68
- private
69
-
70
80
  def slop_parse(args)
71
81
  Slop.parse(args) do |o|
72
- o.banner = 'Usage: basketball-draft [options] ...'
82
+ o.banner = 'Usage: basketball-room [options] ...'
73
83
 
74
- o.string '-i', '--input',
75
- 'Path to load the room from. If omitted then a new draft will be generated.'
84
+ o.string '-i', '--input', 'Path to load the Room from. If omitted then a new draft will be generated.'
76
85
  o.string '-o', '--output', 'Path to write the room to (if omitted then input path will be used)'
77
86
  o.integer '-s', '--simulate', 'Number of picks to simulate (default is 0).', default: 0
78
87
  o.bool '-a', '--simulate-all', 'Simulate the rest of the draft', default: false
79
88
  o.array '-p', '--picks', 'Comma-separated list of ordered player IDs to pick.', delimiter: ','
80
89
  o.integer '-t', '--top', 'Output the top rated available players (default is 0).', default: 0
81
- o.string '-q', '--query', "Filter TOP by position: #{Position::ALL_VALUES.join(', ')}."
82
- o.bool '-r', '--rosters', 'Output all front_office rosters.', default: false
90
+ o.bool '-l', '--league', 'Output all teams and their picks', default: false
83
91
  o.integer '-x', '--skip', 'Number of picks to skip (default is 0).', default: 0
84
- o.bool '-l', '--log', 'Output event log.', default: false
92
+ o.bool '-e', '--events', 'Output event log.', default: false
93
+ o.string '-r', '--rosters', 'Path to write the resulting rosters (league) to.'
85
94
 
86
95
  o.on '-h', '--help', 'Print out help, like this is doing right now.' do
87
96
  io.puts(o)
@@ -104,33 +113,31 @@ module Basketball
104
113
 
105
114
  def generate_draft
106
115
  front_offices = 30.times.map do |i|
107
- FrontOffice.new(
108
- id: "T-#{i + 1}", name: Faker::Team.name
116
+ Draft::FrontOffice.new(
117
+ id: "T-#{i + 1}"
109
118
  )
110
119
  end
111
120
 
112
121
  players = 450.times.map do |i|
113
- Player.new(
122
+ Org::Player.new(
114
123
  id: "P-#{i + 1}",
115
- first_name: Faker::Name.first_name,
116
- last_name: Faker::Name.last_name,
117
- position: Position.random,
118
- overall: (0..100).to_a.sample
124
+ overall: (20..100).to_a.sample,
125
+ position: Org::Position.random
119
126
  )
120
127
  end
121
128
 
122
- Room.new(players:, front_offices:)
129
+ Draft::Room.new(rounds: 12, players:, front_offices:)
123
130
  end
124
131
 
125
132
  def league(room)
126
- return unless opts[:rosters]
133
+ return unless opts[:league]
127
134
 
128
135
  io.puts
129
- io.puts(room.to_league)
136
+ io.puts(room.league)
130
137
  end
131
138
 
132
- def log(room)
133
- return unless opts[:log]
139
+ def events(room)
140
+ return unless opts[:events]
134
141
 
135
142
  io.puts
136
143
  io.puts('Event Log')
@@ -138,32 +145,20 @@ module Basketball
138
145
  puts room.events
139
146
  end
140
147
 
141
- # rubocop:disable Metrics/AbcSize
142
148
  def query(room)
143
149
  top = opts[:top]
144
150
 
145
151
  return if top <= 0
146
152
 
147
- search = PlayerSearch.new(room.undrafted_players)
148
- position = opts[:query].to_s.empty? ? nil : Position.new(opts[:query])
149
- players = search.query(position:).take(opts[:top])
153
+ players = room.undrafted_players.sort_by(&:overall).reverse.take(opts[:top])
150
154
 
151
155
  io.puts
152
- io.print("Top #{top} available players")
153
-
154
- if position
155
- io.puts(" for #{position} position:")
156
- else
157
- io.puts(' for all positions:')
158
- end
159
-
156
+ io.puts("Top #{top} available players")
160
157
  io.puts(players)
161
158
  end
162
- # rubocop:enable Metrics/AbcSize
163
159
 
164
160
  def read
165
- contents = File.read(opts[:input])
166
- serializer.deserialize(contents)
161
+ room_repository.load(opts[:input])
167
162
  end
168
163
 
169
164
  # rubocop:disable Metrics/AbcSize
@@ -195,14 +190,14 @@ module Basketball
195
190
  event_count += 1
196
191
  end
197
192
 
198
- room.sim!(opts[:simulate]) do |event|
199
- io.puts(event)
193
+ opts[:simulate].times do
194
+ room.sim!
200
195
 
201
196
  event_count += 1
202
197
  end
203
198
 
204
199
  if opts[:simulate_all]
205
- room.sim! do |event|
200
+ room.sim_rest! do |event|
206
201
  io.puts(event)
207
202
 
208
203
  event_count += 1
@@ -215,15 +210,14 @@ module Basketball
215
210
  end
216
211
  # rubocop:enable Metrics/AbcSize
217
212
 
218
- def write(room)
219
- output = opts[:output].to_s.empty? ? opts[:input] : opts[:output]
220
-
221
- contents = serializer.serialize(room)
222
- out_dir = File.dirname(output)
213
+ def output_default_to_input
214
+ opts[:output].to_s.empty? ? opts[:input] : opts[:output]
215
+ end
223
216
 
224
- FileUtils.mkdir_p(out_dir)
217
+ def write(room)
218
+ output = output_default_to_input
225
219
 
226
- File.write(output, contents)
220
+ room_repository.save(output, room)
227
221
 
228
222
  io.puts
229
223
  io.puts("Draft written to: #{output}")