basketball 0.0.9 → 0.0.11

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