basketball 0.0.8 → 0.0.10

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