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.
- checksums.yaml +4 -4
- data/README.md +31 -21
- data/basketball.gemspec +14 -8
- data/exe/basketball +91 -0
- data/lib/basketball/app/coordinator_cli.rb +56 -72
- data/lib/basketball/app/coordinator_repository.rb +12 -88
- data/lib/basketball/app/document_repository.rb +67 -0
- data/lib/basketball/app/file_store.rb +16 -0
- data/lib/basketball/app/in_memory_store.rb +42 -0
- data/lib/basketball/app/league_repository.rb +20 -0
- data/lib/basketball/app/league_serializable.rb +99 -0
- data/lib/basketball/app/room_cli.rb +30 -26
- data/lib/basketball/app/room_repository.rb +1 -41
- data/lib/basketball/app.rb +10 -2
- data/lib/basketball/draft/pick.rb +0 -7
- data/lib/basketball/draft/room.rb +11 -13
- data/lib/basketball/entity.rb +9 -6
- data/lib/basketball/org/conference.rb +47 -0
- data/lib/basketball/org/division.rb +43 -0
- data/lib/basketball/org/has_divisions.rb +25 -0
- data/lib/basketball/org/has_players.rb +20 -0
- data/lib/basketball/org/has_teams.rb +24 -0
- data/lib/basketball/org/league.rb +59 -32
- data/lib/basketball/org.rb +12 -1
- data/lib/basketball/season/arena.rb +26 -25
- data/lib/basketball/season/calendar.rb +52 -22
- data/lib/basketball/season/coordinator.rb +25 -18
- data/lib/basketball/season/detail.rb +47 -0
- data/lib/basketball/season/exhibition.rb +1 -1
- data/lib/basketball/season/opponent.rb +6 -0
- data/lib/basketball/season/record.rb +92 -0
- data/lib/basketball/season/scheduler.rb +223 -0
- data/lib/basketball/season/standings.rb +56 -0
- data/lib/basketball/season.rb +6 -0
- data/lib/basketball/value_object.rb +6 -28
- data/lib/basketball/value_object_dsl.rb +30 -0
- data/lib/basketball/version.rb +1 -1
- metadata +22 -6
- /data/exe/{basketball-room → basketball-draft-room} +0 -0
- /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 -
|
8
|
-
# exe/basketball-room -i tmp/draft-wip.json -x 2
|
9
|
-
# exe/basketball-room -i tmp/draft-wip.json -g -
|
10
|
-
# exe/basketball-room -i tmp/draft-wip.json -s 30 -
|
11
|
-
# exe/basketball-room -i tmp/draft-wip.json -
|
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,
|
15
|
+
attr_reader :opts,
|
16
|
+
:io,
|
17
|
+
:room_repository
|
18
18
|
|
19
|
-
def initialize(
|
20
|
-
|
21
|
-
|
22
|
-
|
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
|
-
|
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 '-
|
74
|
-
o.bool '-
|
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
|
116
|
-
return unless opts[:
|
119
|
+
def teams(room)
|
120
|
+
return unless opts[:teams]
|
117
121
|
|
118
122
|
io.puts
|
119
|
-
io.puts(room.
|
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
|
-
|
136
|
+
list = opts[:list]
|
133
137
|
|
134
|
-
return if
|
138
|
+
return if list <= 0
|
135
139
|
|
136
|
-
players = room.undrafted_players.sort_by(&:overall).reverse.take(opts[:
|
140
|
+
players = room.undrafted_players.sort_by(&:overall).reverse.take(opts[:list])
|
137
141
|
|
138
142
|
io.puts
|
139
|
-
io.puts("Top #{
|
143
|
+
io.puts("Top #{list} available players")
|
140
144
|
io.puts(players)
|
141
145
|
end
|
142
146
|
|
143
147
|
def read
|
144
|
-
|
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
|
-
|
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])
|
data/lib/basketball/app.rb
CHANGED
@@ -1,10 +1,18 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
#
|
3
|
+
# Stores
|
4
4
|
require_relative 'app/file_store'
|
5
|
+
require_relative 'app/in_memory_store'
|
5
6
|
|
6
|
-
#
|
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
|
@@ -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
|
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
|
32
|
-
|
33
|
-
|
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
|
-
|
39
|
-
|
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
|
data/lib/basketball/entity.rb
CHANGED
@@ -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
|
-
|
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
|