basketball 0.0.9 → 0.0.11

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 (40) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +31 -21
  3. data/basketball.gemspec +14 -8
  4. data/exe/basketball +91 -0
  5. data/lib/basketball/app/coordinator_cli.rb +56 -72
  6. data/lib/basketball/app/coordinator_repository.rb +12 -88
  7. data/lib/basketball/app/document_repository.rb +67 -0
  8. data/lib/basketball/app/file_store.rb +16 -0
  9. data/lib/basketball/app/in_memory_store.rb +42 -0
  10. data/lib/basketball/app/league_repository.rb +20 -0
  11. data/lib/basketball/app/league_serializable.rb +99 -0
  12. data/lib/basketball/app/room_cli.rb +30 -26
  13. data/lib/basketball/app/room_repository.rb +1 -41
  14. data/lib/basketball/app.rb +10 -2
  15. data/lib/basketball/draft/pick.rb +0 -7
  16. data/lib/basketball/draft/room.rb +11 -13
  17. data/lib/basketball/entity.rb +9 -6
  18. data/lib/basketball/org/conference.rb +47 -0
  19. data/lib/basketball/org/division.rb +43 -0
  20. data/lib/basketball/org/has_divisions.rb +25 -0
  21. data/lib/basketball/org/has_players.rb +20 -0
  22. data/lib/basketball/org/has_teams.rb +24 -0
  23. data/lib/basketball/org/league.rb +59 -32
  24. data/lib/basketball/org.rb +12 -1
  25. data/lib/basketball/season/arena.rb +26 -25
  26. data/lib/basketball/season/calendar.rb +52 -22
  27. data/lib/basketball/season/coordinator.rb +25 -18
  28. data/lib/basketball/season/detail.rb +47 -0
  29. data/lib/basketball/season/exhibition.rb +1 -1
  30. data/lib/basketball/season/opponent.rb +6 -0
  31. data/lib/basketball/season/record.rb +92 -0
  32. data/lib/basketball/season/scheduler.rb +223 -0
  33. data/lib/basketball/season/standings.rb +56 -0
  34. data/lib/basketball/season.rb +6 -0
  35. data/lib/basketball/value_object.rb +6 -28
  36. data/lib/basketball/value_object_dsl.rb +30 -0
  37. data/lib/basketball/version.rb +1 -1
  38. metadata +22 -6
  39. /data/exe/{basketball-room → basketball-draft-room} +0 -0
  40. /data/exe/{basketball-coordinator → basketball-season-coordinator} +0 -0
@@ -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
@@ -4,7 +4,15 @@ module Basketball
4
4
  module App
5
5
  # Knows how to read and write documents to disk.
6
6
  class FileStore
7
+ class PathNotFoundError < StandardError; end
8
+
9
+ def exist?(path)
10
+ File.exist?(path)
11
+ end
12
+
7
13
  def read(path)
14
+ raise PathNotFoundError, "'#{path}' not found" unless exist?(path)
15
+
8
16
  File.read(path)
9
17
  end
10
18
 
@@ -17,6 +25,14 @@ module Basketball
17
25
 
18
26
  nil
19
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
20
36
  end
21
37
  end
22
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,99 @@
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
+ id: league.id,
12
+ conferences: serialize_conferences(league.conferences)
13
+ }
14
+ end
15
+
16
+ def serialize_conferences(conferences)
17
+ conferences.map do |conference|
18
+ {
19
+ id: conference.id,
20
+ divisions: serialize_divisions(conference.divisions)
21
+ }
22
+ end
23
+ end
24
+
25
+ def serialize_divisions(divisions)
26
+ divisions.map do |division|
27
+ {
28
+ id: division.id,
29
+ teams: serialize_teams(division.teams)
30
+ }
31
+ end
32
+ end
33
+
34
+ def serialize_teams(teams)
35
+ teams.map do |team|
36
+ {
37
+ id: team.id,
38
+ players: serialize_players(team.players)
39
+ }
40
+ end
41
+ end
42
+
43
+ def serialize_players(players)
44
+ players.map do |player|
45
+ {
46
+ id: player.id,
47
+ overall: player.overall,
48
+ position: player.position&.code
49
+ }
50
+ end
51
+ end
52
+
53
+ # Deserialization
54
+
55
+ def deserialize_league(league_hash)
56
+ Org::League.new(
57
+ conferences: deserialize_conferences(league_hash[:conferences])
58
+ )
59
+ end
60
+
61
+ def deserialize_conferences(conference_hashes)
62
+ (conference_hashes || []).map do |conference_hash|
63
+ Org::Conference.new(
64
+ id: conference_hash[:id],
65
+ divisions: deserialize_divisions(conference_hash[:divisions])
66
+ )
67
+ end
68
+ end
69
+
70
+ def deserialize_divisions(division_hashes)
71
+ (division_hashes || []).map do |division_hash|
72
+ Org::Division.new(
73
+ id: division_hash[:id],
74
+ teams: deserialize_teams(division_hash[:teams])
75
+ )
76
+ end
77
+ end
78
+
79
+ def deserialize_teams(team_hashes)
80
+ (team_hashes || []).map do |team_hash|
81
+ Org::Team.new(
82
+ id: team_hash[:id],
83
+ players: deserialize_players(team_hash[:players])
84
+ )
85
+ end
86
+ end
87
+
88
+ def deserialize_players(player_hashes)
89
+ (player_hashes || []).map do |player_hash|
90
+ Org::Player.new(
91
+ id: player_hash[:id],
92
+ overall: player_hash[:overall],
93
+ position: Org::Position.new(player_hash[:position])
94
+ )
95
+ end
96
+ end
97
+ end
98
+ end
99
+ end
@@ -3,23 +3,27 @@
3
3
  module Basketball
4
4
  module App
5
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
6
+ # exe/basketball-draft-room -o tmp/draft.json
7
+ # exe/basketball-draft-room -i tmp/draft.json -o tmp/draft-wip.json -s 26 -p P-5,P-10 -l 10
8
+ # exe/basketball-draft-room -i tmp/draft-wip.json -x 2
9
+ # exe/basketball-draft-room -i tmp/draft-wip.json -g -l 10
10
+ # exe/basketball-draft-room -i tmp/draft-wip.json -s 30 -l 10
11
+ # exe/basketball-draft-room -i tmp/draft-wip.json -ate
14
12
  class RoomCLI
15
13
  class PlayerNotFound < StandardError; end
16
14
 
17
- attr_reader :opts, :io, :repository
15
+ attr_reader :opts,
16
+ :io,
17
+ :room_repository
18
18
 
19
- def initialize(args:, io: $stdout)
20
- @io = io
21
- @opts = slop_parse(args)
22
- @repository = RoomRepository.new
19
+ def initialize(
20
+ args:,
21
+ io: $stdout,
22
+ room_repository: RoomRepository.new(FileStore.new)
23
+ )
24
+ @io = io
25
+ @opts = slop_parse(args)
26
+ @room_repository = room_repository
23
27
 
24
28
  if opts[:input].to_s.empty? && opts[:output].to_s.empty?
25
29
  io.puts('Input and/or output paths are required.')
@@ -37,7 +41,7 @@ module Basketball
37
41
  status(room)
38
42
  write(room)
39
43
  events(room)
40
- league(room)
44
+ teams(room)
41
45
  query(room)
42
46
 
43
47
  self
@@ -63,15 +67,15 @@ module Basketball
63
67
 
64
68
  def slop_parse(args)
65
69
  Slop.parse(args) do |o|
66
- o.banner = 'Usage: basketball-room [options] ...'
70
+ o.banner = 'Usage: basketball-draft-room [options] ...'
67
71
 
68
72
  o.string '-i', '--input', 'Path to load the Room from. If omitted then a new draft will be generated.'
69
73
  o.string '-o', '--output', 'Path to write the room to (if omitted then input path will be used)'
70
74
  o.integer '-s', '--simulate', 'Number of picks to simulate (default is 0).', default: 0
71
75
  o.bool '-a', '--simulate-all', 'Simulate the rest of the draft', default: false
72
76
  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
77
+ o.integer '-l', '--list', 'List the top rated available players (default is 0).', default: 0
78
+ o.bool '-t', '--teams', 'Output all teams and their picks', default: false
75
79
  o.integer '-x', '--skip', 'Number of picks to skip (default is 0).', default: 0
76
80
  o.bool '-e', '--events', 'Output event log.', default: false
77
81
 
@@ -112,11 +116,11 @@ module Basketball
112
116
  Draft::Room.new(rounds: 12, players:, front_offices:)
113
117
  end
114
118
 
115
- def league(room)
116
- return unless opts[:league]
119
+ def teams(room)
120
+ return unless opts[:teams]
117
121
 
118
122
  io.puts
119
- io.puts(room.league)
123
+ io.puts(room.teams)
120
124
  end
121
125
 
122
126
  def events(room)
@@ -129,19 +133,19 @@ module Basketball
129
133
  end
130
134
 
131
135
  def query(room)
132
- top = opts[:top]
136
+ list = opts[:list]
133
137
 
134
- return if top <= 0
138
+ return if list <= 0
135
139
 
136
- players = room.undrafted_players.sort_by(&:overall).reverse.take(opts[:top])
140
+ players = room.undrafted_players.sort_by(&:overall).reverse.take(opts[:list])
137
141
 
138
142
  io.puts
139
- io.puts("Top #{top} available players")
143
+ io.puts("Top #{list} available players")
140
144
  io.puts(players)
141
145
  end
142
146
 
143
147
  def read
144
- repository.load(opts[:input])
148
+ room_repository.load(opts[:input])
145
149
  end
146
150
 
147
151
  # rubocop:disable Metrics/AbcSize
@@ -200,7 +204,7 @@ module Basketball
200
204
  def write(room)
201
205
  output = output_default_to_input
202
206
 
203
- repository.save(output, room)
207
+ room_repository.save(output, room)
204
208
 
205
209
  io.puts
206
210
  io.puts("Draft written to: #{output}")
@@ -3,54 +3,14 @@
3
3
  module Basketball
4
4
  module App
5
5
  # Can load and save Room objects to JSON files.
6
- class RoomRepository
6
+ class RoomRepository < DocumentRepository
7
7
  PICK_EVENT = 'Pick'
8
8
  SKIP_EVENT = 'Skip'
9
9
 
10
10
  private_constant :PICK_EVENT, :SKIP_EVENT
11
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
12
  private
43
13
 
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
14
  def from_h(hash)
55
15
  front_offices = deserialize_front_offices(hash[:front_offices])
56
16
  players = deserialize_players(hash[:players])
@@ -1,10 +1,18 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- # Services
3
+ # Stores
4
4
  require_relative 'app/file_store'
5
+ require_relative 'app/in_memory_store'
5
6
 
6
- # Repositories
7
+ # Serialization
8
+ require_relative 'app/league_serializable'
9
+
10
+ # Repositories / Common
11
+ require_relative 'app/document_repository'
12
+
13
+ # Repositories / Implementations
7
14
  require_relative 'app/coordinator_repository'
15
+ require_relative 'app/league_repository'
8
16
  require_relative 'app/room_repository'
9
17
 
10
18
  # Controllers
@@ -20,13 +20,6 @@ module Basketball
20
20
  def to_s
21
21
  "#{super} #{auto ? 'auto-' : ''}picked #{player}"
22
22
  end
23
-
24
- def ==(other)
25
- super &&
26
- player == other.player &&
27
- auto == other.auto
28
- end
29
- alias eql? ==
30
23
  end
31
24
  end
32
25
  end
@@ -3,7 +3,7 @@
3
3
  module Basketball
4
4
  module Draft
5
5
  # Main pick-by-pick iterator object which will round-robin rotate team selections.
6
- class Room
6
+ class Room < Entity
7
7
  class AlreadyPickedError < StandardError; end
8
8
  class EndOfDraftError < StandardError; end
9
9
  class EventOutOfOrderError < StandardError; end
@@ -12,9 +12,11 @@ module Basketball
12
12
  class UnknownFrontOfficeError < StandardError; end
13
13
  class UnknownPlayerError < StandardError; end
14
14
 
15
- attr_reader :rounds, :players, :front_offices, :events, :id
15
+ attr_reader :rounds, :players, :front_offices, :events
16
16
 
17
17
  def initialize(front_offices:, rounds:, players: [], events: [])
18
+ super()
19
+
18
20
  raise InvalidRoundsError, "#{rounds} should be a positive number" unless rounds.positive?
19
21
 
20
22
  @rounds = rounds
@@ -28,17 +30,15 @@ module Basketball
28
30
  end
29
31
 
30
32
  # This method will return a materialized list of teams and their selections.
31
- def league
32
- Org::League.new.tap do |league|
33
- front_offices.each do |front_office|
34
- team = Org::Team.new(id: front_office.id)
35
-
36
- league.register!(team)
33
+ def teams
34
+ front_offices.each_with_object([]) do |front_office, memo|
35
+ team = Org::Team.new(id: front_office.id)
37
36
 
38
- drafted_players(front_office).each do |player|
39
- league.sign!(player:, team:)
40
- end
37
+ drafted_players(front_office).each do |player|
38
+ team.sign!(player)
41
39
  end
40
+
41
+ memo << team
42
42
  end
43
43
  end
44
44
 
@@ -157,8 +157,6 @@ module Basketball
157
157
 
158
158
  private
159
159
 
160
- attr_writer :id
161
-
162
160
  def player_events
163
161
  events.select { |e| e.respond_to?(:player) }
164
162
  end
@@ -4,16 +4,11 @@ module Basketball
4
4
  # Base class for uniquely identifiable classes. Subclasses are simply based on a string-based ID
5
5
  # and comparison/sorting/equality will be done in a case-insensitive manner.
6
6
  class Entity
7
- extend Forwardable
8
7
  include Comparable
9
8
 
10
9
  attr_reader :id
11
10
 
12
- def_delegators :id, :to_s
13
-
14
- def initialize(id)
15
- raise ArgumentError, 'id is required' if id.to_s.empty?
16
-
11
+ def initialize(id = nil)
17
12
  @id = id
18
13
  end
19
14
 
@@ -30,8 +25,16 @@ module Basketball
30
25
  comparable_id.hash
31
26
  end
32
27
 
28
+ def to_s
29
+ id.to_s
30
+ end
31
+
33
32
  def comparable_id
34
33
  id.to_s.upcase
35
34
  end
35
+
36
+ private
37
+
38
+ attr_writer :id
36
39
  end
37
40
  end
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Basketball
4
+ module Org
5
+ # A collection of divisions, teams, and players.
6
+ class Conference < Entity
7
+ include HasDivisions
8
+
9
+ attr_reader :divisions
10
+
11
+ def initialize(id:, divisions: [])
12
+ super(id)
13
+
14
+ @divisions = []
15
+
16
+ divisions.each { |d| register_division!(d) }
17
+
18
+ freeze
19
+ end
20
+
21
+ def to_s
22
+ ([super] + divisions.map(&:to_s)).join("\n")
23
+ end
24
+
25
+ def teams
26
+ divisions.flat_map(&:teams)
27
+ end
28
+
29
+ def players
30
+ divisions.flat_map(&:players)
31
+ end
32
+
33
+ private
34
+
35
+ def register_division!(division)
36
+ raise ArgumentError, 'division is required' unless division
37
+ raise DivisionAlreadyRegisteredError, "#{division} already registered" if division?(division)
38
+
39
+ assert_teams_are_not_already_registered(division.teams)
40
+
41
+ divisions << division
42
+
43
+ self
44
+ end
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Basketball
4
+ module Org
5
+ # A collection of teams and players.
6
+ class Division < Entity
7
+ include HasTeams
8
+
9
+ attr_reader :teams
10
+
11
+ def initialize(id:, teams: [])
12
+ super(id)
13
+
14
+ @teams = []
15
+
16
+ teams.each { |t| register_team!(t) }
17
+
18
+ freeze
19
+ end
20
+
21
+ def to_s
22
+ ([super] + teams.map(&:to_s)).join("\n")
23
+ end
24
+
25
+ def players
26
+ teams.flat_map(&:players)
27
+ end
28
+
29
+ private
30
+
31
+ def register_team!(team)
32
+ raise ArgumentError, 'team is required' unless team
33
+ raise TeamAlreadyRegisteredError, "#{team} already registered" if team?(team)
34
+
35
+ assert_players_are_not_already_signed(team.players)
36
+
37
+ teams << team
38
+
39
+ self
40
+ end
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Basketball
4
+ module Org
5
+ # Helper methods for objects that can be composed of divisions which are also composed of teams
6
+ # and players.
7
+ module HasDivisions
8
+ include HasTeams
9
+
10
+ def division?(division)
11
+ divisions.include?(division)
12
+ end
13
+
14
+ private
15
+
16
+ def assert_divisions_are_not_already_registered(divisions)
17
+ divisions.each do |division|
18
+ raise DivisionAlreadyRegisteredError, "#{division} already registered" if division?(division)
19
+
20
+ assert_teams_are_not_already_registered(division.teams)
21
+ end
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Basketball
4
+ module Org
5
+ # Helper methods for objects that can be composed of players.
6
+ module HasPlayers
7
+ def player?(player)
8
+ players.include?(player)
9
+ end
10
+
11
+ private
12
+
13
+ def assert_players_are_not_already_signed(players)
14
+ players.each do |player|
15
+ raise PlayerAlreadySignedError, "#{player} already registered" if player?(player)
16
+ end
17
+ end
18
+ end
19
+ end
20
+ end