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