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,24 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Basketball
|
4
|
+
module Org
|
5
|
+
# Helper methods for objects that can be composed of teams which are also made up of players.
|
6
|
+
module HasTeams
|
7
|
+
include HasPlayers
|
8
|
+
|
9
|
+
def team?(team)
|
10
|
+
teams.include?(team)
|
11
|
+
end
|
12
|
+
|
13
|
+
private
|
14
|
+
|
15
|
+
def assert_teams_are_not_already_registered(teams)
|
16
|
+
teams.each do |team|
|
17
|
+
raise TeamAlreadyRegisteredError, "#{team} already registered" if team?(team)
|
18
|
+
|
19
|
+
assert_players_are_not_already_signed(team.players)
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -2,72 +2,99 @@
|
|
2
2
|
|
3
3
|
module Basketball
|
4
4
|
module Org
|
5
|
-
# Describes a collection of teams and players.
|
6
|
-
# adding teams and players to ensure the all the
|
5
|
+
# Describes a collection of conferences, divisions, teams, and players.
|
6
|
+
# Holds the rules which support adding teams and players to ensure the all the
|
7
|
+
# teams are cohesive, such as:
|
8
|
+
# - preventing duplicate conferences
|
9
|
+
# - preventing duplicate divisions
|
7
10
|
# - preventing duplicate teams
|
8
11
|
# - preventing double-signing players across teams
|
9
|
-
class League
|
10
|
-
|
11
|
-
class UnregisteredTeamError < StandardError; end
|
12
|
+
class League < Entity
|
13
|
+
include HasDivisions
|
12
14
|
|
13
|
-
|
15
|
+
class ConferenceAlreadyRegisteredError < StandardError; end
|
14
16
|
|
15
|
-
|
16
|
-
@teams = []
|
17
|
+
alias signed? player?
|
17
18
|
|
18
|
-
|
19
|
+
attr_reader :conferences
|
19
20
|
|
20
|
-
|
21
|
+
def initialize(conferences: [])
|
22
|
+
super()
|
23
|
+
|
24
|
+
@conferences = []
|
25
|
+
|
26
|
+
conferences.each { |c| register!(c) }
|
21
27
|
end
|
22
28
|
|
23
29
|
def to_s
|
24
|
-
|
30
|
+
conferences.map(&:to_s).join("\n")
|
25
31
|
end
|
26
32
|
|
27
33
|
def sign!(player:, team:)
|
28
34
|
raise ArgumentError, 'player is required' unless player
|
29
35
|
raise ArgumentError, 'team is required' unless team
|
30
|
-
raise UnregisteredTeamError, "#{team}
|
31
|
-
raise PlayerAlreadySignedError, "#{player}
|
36
|
+
raise UnregisteredTeamError, "#{team} not registered" unless team?(team)
|
37
|
+
raise PlayerAlreadySignedError, "#{player} already registered" if player?(player)
|
38
|
+
|
39
|
+
# It is OK to pass in a detached team as long as its equivalent resides in this
|
40
|
+
# League's object graph.
|
41
|
+
team_for(team.id).sign!(player)
|
42
|
+
|
43
|
+
self
|
44
|
+
end
|
45
|
+
|
46
|
+
def register!(conference)
|
47
|
+
raise ArgumentError, 'conference is required' unless conference
|
48
|
+
raise ConferenceAlreadyRegisteredError, "#{conference} already registered" if conference?(conference)
|
49
|
+
|
50
|
+
assert_divisions_are_not_already_registered(conference.divisions)
|
32
51
|
|
33
|
-
|
52
|
+
conferences << conference
|
34
53
|
|
35
54
|
self
|
36
55
|
end
|
37
56
|
|
38
|
-
def
|
39
|
-
|
57
|
+
def conference?(conference)
|
58
|
+
conferences.include?(conference)
|
59
|
+
end
|
60
|
+
|
61
|
+
def divisions
|
62
|
+
conferences.flat_map(&:divisions)
|
63
|
+
end
|
64
|
+
|
65
|
+
def teams
|
66
|
+
conferences.flat_map(&:teams)
|
40
67
|
end
|
41
68
|
|
42
69
|
def players
|
43
|
-
|
70
|
+
conferences.flat_map(&:players)
|
44
71
|
end
|
45
72
|
|
46
|
-
def
|
47
|
-
|
73
|
+
def conference_for(team)
|
74
|
+
conferences.find { |c| c.divisions.find { |d| d.teams.include?(team) } }
|
48
75
|
end
|
49
76
|
|
50
|
-
def
|
51
|
-
teams.include?(team)
|
77
|
+
def division_for(team)
|
78
|
+
conference_for(team)&.divisions&.find { |d| d.teams.include?(team) }
|
52
79
|
end
|
53
80
|
|
54
|
-
|
55
|
-
|
56
|
-
|
81
|
+
# Same conference, different division
|
82
|
+
def cross_division_opponents_for(team)
|
83
|
+
conference = conference_for(team)
|
84
|
+
division = division_for(team)
|
57
85
|
|
58
|
-
|
59
|
-
raise PlayerAlreadySignedError, "#{player} already signed" if signed?(player)
|
60
|
-
end
|
86
|
+
return nil unless conference && division
|
61
87
|
|
62
|
-
|
88
|
+
other_divisions = conference.divisions - [division]
|
63
89
|
|
64
|
-
|
90
|
+
other_divisions.flat_map(&:teams)
|
65
91
|
end
|
66
92
|
|
67
|
-
|
68
|
-
|
93
|
+
private
|
94
|
+
|
95
|
+
def team_for(id)
|
96
|
+
teams.find { |team| team.id == id }
|
69
97
|
end
|
70
|
-
alias eql? ==
|
71
98
|
end
|
72
99
|
end
|
73
100
|
end
|
data/lib/basketball/org.rb
CHANGED
@@ -1,12 +1,23 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
# Cross-cutting Concerns
|
4
|
+
require_relative 'org/has_players'
|
5
|
+
require_relative 'org/has_teams'
|
6
|
+
require_relative 'org/has_divisions'
|
7
|
+
|
8
|
+
# Domain Models
|
9
|
+
require_relative 'org/conference'
|
10
|
+
require_relative 'org/division'
|
11
|
+
require_relative 'org/league'
|
3
12
|
require_relative 'org/player'
|
4
13
|
require_relative 'org/position'
|
5
14
|
require_relative 'org/team'
|
6
|
-
require_relative 'org/league'
|
7
15
|
|
8
16
|
module Basketball
|
9
17
|
module Org
|
18
|
+
class DivisionAlreadyRegisteredError < StandardError; end
|
10
19
|
class PlayerAlreadySignedError < StandardError; end
|
20
|
+
class TeamAlreadyRegisteredError < StandardError; end
|
21
|
+
class UnregisteredTeamError < StandardError; end
|
11
22
|
end
|
12
23
|
end
|
@@ -4,32 +4,29 @@ module Basketball
|
|
4
4
|
module Season
|
5
5
|
# A very, very, very basic starting point for a "semi-randomized" game simulator.
|
6
6
|
class Arena
|
7
|
-
RANDOM
|
8
|
-
TOP_ONE
|
9
|
-
TOP_TWO
|
10
|
-
TOP_THREE
|
11
|
-
TOP_SIX
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
RANDOM =>
|
16
|
-
TOP_ONE =>
|
17
|
-
TOP_TWO =>
|
18
|
-
TOP_THREE =>
|
19
|
-
TOP_SIX =>
|
7
|
+
RANDOM = :random
|
8
|
+
TOP_ONE = :top_one
|
9
|
+
TOP_TWO = :top_two
|
10
|
+
TOP_THREE = :top_three
|
11
|
+
TOP_SIX = :top_six
|
12
|
+
DEFAULT_MAX_HOME_ADVANTAGE = 5
|
13
|
+
|
14
|
+
DEFAULT_STRATEGY_FREQUENCIES = {
|
15
|
+
RANDOM => 3,
|
16
|
+
TOP_ONE => 1,
|
17
|
+
TOP_TWO => 1,
|
18
|
+
TOP_THREE => 1,
|
19
|
+
TOP_SIX => 1
|
20
20
|
}.freeze
|
21
21
|
|
22
|
-
|
23
|
-
:RANDOM,
|
24
|
-
:TOP_ONE,
|
25
|
-
:TOP_TWO,
|
26
|
-
:TOP_SIX,
|
27
|
-
:MAX_HOME_ADVANTAGE
|
22
|
+
attr_reader :lotto, :max_home_advantage
|
28
23
|
|
29
|
-
def initialize
|
30
|
-
|
31
|
-
|
32
|
-
|
24
|
+
def initialize(
|
25
|
+
strategy_frquencies: DEFAULT_STRATEGY_FREQUENCIES,
|
26
|
+
max_home_advantage: DEFAULT_MAX_HOME_ADVANTAGE
|
27
|
+
)
|
28
|
+
@max_home_advantage = max_home_advantage
|
29
|
+
@lotto = make_lotto(strategy_frquencies)
|
33
30
|
|
34
31
|
freeze
|
35
32
|
end
|
@@ -57,7 +54,11 @@ module Basketball
|
|
57
54
|
|
58
55
|
private
|
59
56
|
|
60
|
-
|
57
|
+
def make_lotto(strategy_frquencies)
|
58
|
+
strategy_frquencies.inject([]) do |memo, (name, frequency)|
|
59
|
+
memo + ([name] * frequency)
|
60
|
+
end.shuffle
|
61
|
+
end
|
61
62
|
|
62
63
|
def pick_strategy
|
63
64
|
lotto.sample
|
@@ -89,7 +90,7 @@ module Basketball
|
|
89
90
|
end
|
90
91
|
|
91
92
|
def random_home_advantage
|
92
|
-
rand(0..
|
93
|
+
rand(0..max_home_advantage)
|
93
94
|
end
|
94
95
|
|
95
96
|
def top_one_strategy(matchup)
|
@@ -2,35 +2,35 @@
|
|
2
2
|
|
3
3
|
module Basketball
|
4
4
|
module Season
|
5
|
-
# Sets boundaries for
|
5
|
+
# Sets boundaries for exhibition and regular season play. Add games as long as they are
|
6
6
|
# within the correct dated boundaries
|
7
7
|
class Calendar
|
8
8
|
class OutOfBoundsError < StandardError; end
|
9
9
|
class TeamAlreadyBookedError < StandardError; end
|
10
10
|
|
11
|
-
attr_reader :
|
12
|
-
:
|
13
|
-
:
|
14
|
-
:
|
11
|
+
attr_reader :exhibition_start_date,
|
12
|
+
:exhibition_end_date,
|
13
|
+
:regular_start_date,
|
14
|
+
:regular_end_date,
|
15
15
|
:games
|
16
16
|
|
17
17
|
def initialize(
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
18
|
+
exhibition_start_date:,
|
19
|
+
exhibition_end_date:,
|
20
|
+
regular_start_date:,
|
21
|
+
regular_end_date:,
|
22
22
|
games: []
|
23
23
|
)
|
24
|
-
raise ArgumentError, '
|
25
|
-
raise ArgumentError, '
|
26
|
-
raise ArgumentError, '
|
27
|
-
raise ArgumentError, '
|
24
|
+
raise ArgumentError, 'exhibition_start_date is required' if exhibition_start_date.to_s.empty?
|
25
|
+
raise ArgumentError, 'exhibition_end_date is required' if exhibition_end_date.to_s.empty?
|
26
|
+
raise ArgumentError, 'regular_start_date is required' if regular_start_date.to_s.empty?
|
27
|
+
raise ArgumentError, 'regular_end_date is required' if regular_end_date.to_s.empty?
|
28
28
|
|
29
|
-
@
|
30
|
-
@
|
31
|
-
@
|
32
|
-
@
|
33
|
-
@games
|
29
|
+
@exhibition_start_date = exhibition_start_date
|
30
|
+
@exhibition_end_date = exhibition_end_date
|
31
|
+
@regular_start_date = regular_start_date
|
32
|
+
@regular_end_date = regular_end_date
|
33
|
+
@games = []
|
34
34
|
|
35
35
|
games.each { |game| add!(game) }
|
36
36
|
|
@@ -60,8 +60,38 @@ module Basketball
|
|
60
60
|
end
|
61
61
|
end
|
62
62
|
|
63
|
+
def available_exhibition_dates_for(opponent)
|
64
|
+
all_exhibition_dates - exhibitions_for(opponent:).map(&:date)
|
65
|
+
end
|
66
|
+
|
67
|
+
def available_regular_dates_for(opponent)
|
68
|
+
all_season_dates - regulars_for(opponent:).map(&:date)
|
69
|
+
end
|
70
|
+
|
71
|
+
def available_exhibition_matchup_dates(opponent1, opponent2)
|
72
|
+
available_opponent_dates = available_exhibition_dates_for(opponent1)
|
73
|
+
available_other_opponent_dates = available_exhibition_dates_for(opponent2)
|
74
|
+
|
75
|
+
available_opponent_dates & available_other_opponent_dates
|
76
|
+
end
|
77
|
+
|
78
|
+
def available_regular_matchup_dates(opponent1, opponent2)
|
79
|
+
available_opponent_dates = available_regular_dates_for(opponent1)
|
80
|
+
available_other_opponent_dates = available_regular_dates_for(opponent2)
|
81
|
+
|
82
|
+
available_opponent_dates & available_other_opponent_dates
|
83
|
+
end
|
84
|
+
|
63
85
|
private
|
64
86
|
|
87
|
+
def all_exhibition_dates
|
88
|
+
(exhibition_start_date..exhibition_end_date).to_a
|
89
|
+
end
|
90
|
+
|
91
|
+
def all_season_dates
|
92
|
+
(regular_start_date..regular_end_date).to_a
|
93
|
+
end
|
94
|
+
|
65
95
|
def assert_free_date(game)
|
66
96
|
if games_for(date: game.date, opponent: game.home_opponent).any?
|
67
97
|
raise TeamAlreadyBookedError, "#{game.home_opponent} already playing on #{game.date}"
|
@@ -76,11 +106,11 @@ module Basketball
|
|
76
106
|
date = game.date
|
77
107
|
|
78
108
|
if game.is_a?(Exhibition)
|
79
|
-
raise OutOfBoundsError, "#{date} is before
|
80
|
-
raise OutOfBoundsError, "#{date} is after
|
109
|
+
raise OutOfBoundsError, "#{date} is before exhibition begins" if date < exhibition_start_date
|
110
|
+
raise OutOfBoundsError, "#{date} is after exhibition ends" if date > exhibition_end_date
|
81
111
|
elsif game.is_a?(Regular)
|
82
|
-
raise OutOfBoundsError, "#{date} is before season begins" if date <
|
83
|
-
raise OutOfBoundsError, "#{date} is after season ends" if date >
|
112
|
+
raise OutOfBoundsError, "#{date} is before season begins" if date < regular_start_date
|
113
|
+
raise OutOfBoundsError, "#{date} is after season ends" if date > regular_end_date
|
84
114
|
else
|
85
115
|
raise ArgumentError, "Dont know what this game type is: #{game.class.name}"
|
86
116
|
end
|
@@ -3,7 +3,7 @@
|
|
3
3
|
module Basketball
|
4
4
|
module Season
|
5
5
|
# Main iterator-based object that knows how to manage a calendar and simulate games per day.
|
6
|
-
class Coordinator
|
6
|
+
class Coordinator < Entity
|
7
7
|
extend Forwardable
|
8
8
|
|
9
9
|
class AlreadyPlayedGameError < StandardError; end
|
@@ -18,14 +18,13 @@ module Basketball
|
|
18
18
|
:current_date,
|
19
19
|
:arena,
|
20
20
|
:results,
|
21
|
-
:league
|
22
|
-
:id
|
21
|
+
:league
|
23
22
|
|
24
23
|
def_delegators :calendar,
|
25
|
-
:
|
26
|
-
:
|
27
|
-
:
|
28
|
-
:
|
24
|
+
:exhibition_start_date,
|
25
|
+
:exhibition_end_date,
|
26
|
+
:regular_start_date,
|
27
|
+
:regular_end_date,
|
29
28
|
:games,
|
30
29
|
:exhibitions_for,
|
31
30
|
:regulars_for,
|
@@ -70,13 +69,13 @@ module Basketball
|
|
70
69
|
end
|
71
70
|
|
72
71
|
def assert_current_date
|
73
|
-
if current_date <
|
74
|
-
raise OutOfBoundsError, "current date #{current_date} should be on or after #{
|
72
|
+
if current_date < exhibition_start_date
|
73
|
+
raise OutOfBoundsError, "current date #{current_date} should be on or after #{exhibition_start_date}"
|
75
74
|
end
|
76
75
|
|
77
|
-
return unless current_date >
|
76
|
+
return unless current_date > regular_end_date
|
78
77
|
|
79
|
-
raise OutOfBoundsError, "current date #{current_date} should be on or after #{
|
78
|
+
raise OutOfBoundsError, "current date #{current_date} should be on or after #{exhibition_start_date}"
|
80
79
|
end
|
81
80
|
|
82
81
|
def sim!
|
@@ -104,11 +103,11 @@ module Basketball
|
|
104
103
|
end
|
105
104
|
|
106
105
|
def total_days
|
107
|
-
(
|
106
|
+
(regular_end_date - exhibition_start_date).to_i
|
108
107
|
end
|
109
108
|
|
110
109
|
def days_left
|
111
|
-
(
|
110
|
+
(regular_end_date - current_date).to_i
|
112
111
|
end
|
113
112
|
|
114
113
|
def total_exhibitions
|
@@ -132,7 +131,7 @@ module Basketball
|
|
132
131
|
end
|
133
132
|
|
134
133
|
def done?
|
135
|
-
current_date ==
|
134
|
+
current_date == regular_end_date && games.length == results.length
|
136
135
|
end
|
137
136
|
|
138
137
|
def not_done?
|
@@ -154,9 +153,17 @@ module Basketball
|
|
154
153
|
end
|
155
154
|
end
|
156
155
|
|
156
|
+
def regular_results
|
157
|
+
results.select { |result| result.game.is_a?(Regular) }
|
158
|
+
end
|
159
|
+
|
160
|
+
def exhibition_results
|
161
|
+
results.select { |result| result.game.is_a?(Exhibition) }
|
162
|
+
end
|
163
|
+
|
157
164
|
private
|
158
165
|
|
159
|
-
attr_writer :
|
166
|
+
attr_writer :arena
|
160
167
|
|
161
168
|
def opponent_team(opponent)
|
162
169
|
league.teams.find { |t| t == opponent }
|
@@ -171,7 +178,7 @@ module Basketball
|
|
171
178
|
end
|
172
179
|
|
173
180
|
def increment_current_date!
|
174
|
-
return self if current_date >=
|
181
|
+
return self if current_date >= regular_end_date
|
175
182
|
|
176
183
|
@current_date = current_date + 1
|
177
184
|
|
@@ -185,9 +192,9 @@ module Basketball
|
|
185
192
|
end
|
186
193
|
|
187
194
|
def assert_known_teams(game)
|
188
|
-
raise UnknownTeamError, "unknown opponent: #{game.home_opponent}"
|
195
|
+
raise UnknownTeamError, "unknown opponent: #{game.home_opponent}" unless league.team?(game.home_opponent)
|
189
196
|
|
190
|
-
return
|
197
|
+
return if league.team?(game.away_opponent)
|
191
198
|
|
192
199
|
raise UnknownTeamError, "unknown opponent: #{game.away_opponent}"
|
193
200
|
end
|
@@ -0,0 +1,47 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Basketball
|
4
|
+
module Season
|
5
|
+
# Describes a result from the perspective of a team.
|
6
|
+
class Detail < ValueObject
|
7
|
+
value_reader :date,
|
8
|
+
:home,
|
9
|
+
:opponent_score,
|
10
|
+
:opponent,
|
11
|
+
:score
|
12
|
+
|
13
|
+
alias home? home
|
14
|
+
|
15
|
+
def initialize(date:, home:, opponent:, opponent_score:, score:)
|
16
|
+
super()
|
17
|
+
|
18
|
+
raise ArgumentError, 'date is required' unless date
|
19
|
+
raise ArgumentError, 'opponent is required' unless opponent
|
20
|
+
raise ArgumentError, 'score is required' unless score
|
21
|
+
raise ArgumentError, 'opponent_score is required' unless opponent_score
|
22
|
+
raise ArgumentError, 'home is required' if home.nil?
|
23
|
+
raise CannotTieError, 'scores cannot be equal' if score == opponent_score
|
24
|
+
|
25
|
+
@date = date
|
26
|
+
@opponent = opponent
|
27
|
+
@score = score
|
28
|
+
@opponent_score = opponent_score
|
29
|
+
@home = home
|
30
|
+
|
31
|
+
freeze
|
32
|
+
end
|
33
|
+
|
34
|
+
def win?
|
35
|
+
score > opponent_score
|
36
|
+
end
|
37
|
+
|
38
|
+
def loss?
|
39
|
+
score < opponent_score
|
40
|
+
end
|
41
|
+
|
42
|
+
def to_s
|
43
|
+
"[#{date}] #{win? ? 'Win' : 'Loss'} #{home? ? 'vs' : 'at'} #{opponent} (#{score}-#{opponent_score})"
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
@@ -5,6 +5,12 @@ module Basketball
|
|
5
5
|
# Represents a team without a roster. Equal to a team by identity.
|
6
6
|
# A team's roster will not be known until the last minute (when it is game time).
|
7
7
|
class Opponent < Entity
|
8
|
+
class << self
|
9
|
+
def from(team)
|
10
|
+
new(id: team.id)
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
8
14
|
def initialize(id:)
|
9
15
|
super(id)
|
10
16
|
|
@@ -0,0 +1,92 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Basketball
|
4
|
+
module Season
|
5
|
+
# Represents a team within a Standings object. Each Record is comprised of Detail instances
|
6
|
+
# which are the game results in the perspective of a single Team.
|
7
|
+
class Record < Entity
|
8
|
+
class DetailAlreadyAddedError < StandardError; end
|
9
|
+
class OpponentNotFoundError < StandardError; end
|
10
|
+
|
11
|
+
def initialize(id:)
|
12
|
+
super(id)
|
13
|
+
|
14
|
+
@details_by_date = {}
|
15
|
+
|
16
|
+
details.each { |detail| add!(detail) }
|
17
|
+
|
18
|
+
freeze
|
19
|
+
end
|
20
|
+
|
21
|
+
def accept!(result)
|
22
|
+
if result.home_opponent == self
|
23
|
+
detail = Detail.new(
|
24
|
+
date: result.date,
|
25
|
+
opponent: result.away_opponent,
|
26
|
+
score: result.home_score,
|
27
|
+
opponent_score: result.away_score,
|
28
|
+
home: true
|
29
|
+
)
|
30
|
+
|
31
|
+
add!(detail)
|
32
|
+
elsif result.away_opponent == self
|
33
|
+
detail = Detail.new(
|
34
|
+
date: result.date,
|
35
|
+
opponent: result.home_opponent,
|
36
|
+
score: result.away_score,
|
37
|
+
opponent_score: result.home_score,
|
38
|
+
home: false
|
39
|
+
)
|
40
|
+
|
41
|
+
add!(detail)
|
42
|
+
else
|
43
|
+
raise OpponentNotFoundError, "#{result} has no opponent for #{self}"
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
def detail_for(date)
|
48
|
+
details_by_date[date]
|
49
|
+
end
|
50
|
+
|
51
|
+
def details
|
52
|
+
details_by_date.values
|
53
|
+
end
|
54
|
+
|
55
|
+
def win_percentage
|
56
|
+
(win_count.to_f / game_count).round(3)
|
57
|
+
end
|
58
|
+
|
59
|
+
def game_count
|
60
|
+
details.length
|
61
|
+
end
|
62
|
+
|
63
|
+
def win_count
|
64
|
+
details.count(&:win?)
|
65
|
+
end
|
66
|
+
|
67
|
+
def loss_count
|
68
|
+
details.count(&:loss?)
|
69
|
+
end
|
70
|
+
|
71
|
+
def to_s
|
72
|
+
"[#{super}] #{win_count}-#{loss_count} (#{win_percentage})"
|
73
|
+
end
|
74
|
+
|
75
|
+
def <=>(other)
|
76
|
+
[win_count, win_percentage] <=> [other.win_count, other.win_percentage]
|
77
|
+
end
|
78
|
+
|
79
|
+
private
|
80
|
+
|
81
|
+
attr_reader :details_by_date
|
82
|
+
|
83
|
+
def add!(detail)
|
84
|
+
raise DetailAlreadyAddedError, "#{detail} already added for date" if detail_for(detail.date)
|
85
|
+
|
86
|
+
details_by_date[detail.date] = detail
|
87
|
+
|
88
|
+
self
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|