basketball 0.0.7 → 0.0.9
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.rubocop.yml +9 -19
- data/CHANGELOG.md +1 -31
- data/README.md +72 -91
- data/basketball.gemspec +3 -6
- data/exe/{basketball-schedule → basketball-coordinator} +1 -1
- data/exe/{basketball-draft → basketball-room} +1 -1
- data/lib/basketball/app/coordinator_cli.rb +243 -0
- data/lib/basketball/app/coordinator_repository.rb +191 -0
- data/lib/basketball/app/file_store.rb +22 -0
- data/lib/basketball/app/room_cli.rb +212 -0
- data/lib/basketball/app/room_repository.rb +189 -0
- data/lib/basketball/app.rb +12 -0
- data/lib/basketball/draft/assessment.rb +31 -0
- data/lib/basketball/{drafting → draft}/event.rb +4 -3
- data/lib/basketball/draft/front_office.rb +99 -0
- data/lib/basketball/draft/pick.rb +32 -0
- data/lib/basketball/draft/room.rb +221 -0
- data/lib/basketball/{drafting/player_search.rb → draft/scout.rb} +5 -10
- data/lib/basketball/draft/skip.rb +12 -0
- data/lib/basketball/draft.rb +16 -0
- data/lib/basketball/entity.rb +10 -4
- data/lib/basketball/org/league.rb +73 -0
- data/lib/basketball/org/player.rb +26 -0
- data/lib/basketball/{drafting → org}/position.rb +3 -2
- data/lib/basketball/org/team.rb +38 -0
- data/lib/basketball/org.rb +12 -0
- data/lib/basketball/season/arena.rb +112 -0
- data/lib/basketball/season/calendar.rb +90 -0
- data/lib/basketball/season/coordinator.rb +239 -0
- data/lib/basketball/{scheduling/preseason_game.rb → season/exhibition.rb} +3 -2
- data/lib/basketball/season/game.rb +37 -0
- data/lib/basketball/season/matchup.rb +27 -0
- data/lib/basketball/season/opponent.rb +15 -0
- data/lib/basketball/season/regular.rb +9 -0
- data/lib/basketball/season/result.rb +37 -0
- data/lib/basketball/season.rb +16 -0
- data/lib/basketball/value_object.rb +4 -1
- data/lib/basketball/version.rb +1 -1
- data/lib/basketball.rb +11 -6
- metadata +40 -52
- data/lib/basketball/drafting/cli.rb +0 -235
- data/lib/basketball/drafting/engine.rb +0 -221
- data/lib/basketball/drafting/engine_serializer.rb +0 -186
- data/lib/basketball/drafting/front_office.rb +0 -92
- data/lib/basketball/drafting/league.rb +0 -70
- data/lib/basketball/drafting/pick_event.rb +0 -25
- data/lib/basketball/drafting/player.rb +0 -43
- data/lib/basketball/drafting/roster.rb +0 -37
- data/lib/basketball/drafting/sim_event.rb +0 -23
- data/lib/basketball/drafting/skip_event.rb +0 -13
- data/lib/basketball/drafting.rb +0 -9
- data/lib/basketball/scheduling/calendar.rb +0 -121
- data/lib/basketball/scheduling/calendar_serializer.rb +0 -94
- data/lib/basketball/scheduling/cli.rb +0 -198
- data/lib/basketball/scheduling/conference.rb +0 -57
- data/lib/basketball/scheduling/coordinator.rb +0 -180
- data/lib/basketball/scheduling/division.rb +0 -43
- data/lib/basketball/scheduling/game.rb +0 -32
- data/lib/basketball/scheduling/league.rb +0 -114
- data/lib/basketball/scheduling/league_serializer.rb +0 -99
- data/lib/basketball/scheduling/season_game.rb +0 -8
- data/lib/basketball/scheduling/team.rb +0 -21
- data/lib/basketball/scheduling.rb +0 -17
@@ -1,94 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
require_relative 'game'
|
4
|
-
require_relative 'preseason_game'
|
5
|
-
require_relative 'season_game'
|
6
|
-
|
7
|
-
module Basketball
|
8
|
-
module Scheduling
|
9
|
-
class CalendarSerializer
|
10
|
-
GAME_CLASSES = {
|
11
|
-
'PreseasonGame' => PreseasonGame,
|
12
|
-
'SeasonGame' => SeasonGame
|
13
|
-
}.freeze
|
14
|
-
|
15
|
-
def to_hash(calendar)
|
16
|
-
teams = calendar.games.flat_map(&:teams).uniq
|
17
|
-
|
18
|
-
{
|
19
|
-
'year' => calendar.preseason_start_date.year,
|
20
|
-
'teams' => serialize_teams(teams),
|
21
|
-
'games' => serialize_games(calendar.games)
|
22
|
-
}
|
23
|
-
end
|
24
|
-
|
25
|
-
def from_hash(json)
|
26
|
-
Calendar.new(
|
27
|
-
year: json['year'].to_i,
|
28
|
-
games: deserialize_games(json)
|
29
|
-
)
|
30
|
-
end
|
31
|
-
|
32
|
-
def deserialize(string)
|
33
|
-
json = JSON.parse(string)
|
34
|
-
|
35
|
-
from_hash(json)
|
36
|
-
end
|
37
|
-
|
38
|
-
def serialize(calendar)
|
39
|
-
to_hash(calendar).to_json
|
40
|
-
end
|
41
|
-
|
42
|
-
private
|
43
|
-
|
44
|
-
## Deserialization
|
45
|
-
|
46
|
-
def deserialize_games(json)
|
47
|
-
teams = deserialize_teams(json['teams'])
|
48
|
-
|
49
|
-
(json['games'] || []).map do |game_hash|
|
50
|
-
GAME_CLASSES.fetch(game_hash['type']).new(
|
51
|
-
date: Date.parse(game_hash['date']),
|
52
|
-
home_team: teams.fetch(game_hash['home_team']),
|
53
|
-
away_team: teams.fetch(game_hash['away_team'])
|
54
|
-
)
|
55
|
-
end
|
56
|
-
end
|
57
|
-
|
58
|
-
def deserialize_teams(teams)
|
59
|
-
(teams || []).to_h do |id, team_hash|
|
60
|
-
team = Team.new(id:, name: team_hash['name'])
|
61
|
-
|
62
|
-
[
|
63
|
-
team.id,
|
64
|
-
team
|
65
|
-
]
|
66
|
-
end
|
67
|
-
end
|
68
|
-
|
69
|
-
## Serialization
|
70
|
-
|
71
|
-
def serialize_teams(teams)
|
72
|
-
teams.to_h do |team|
|
73
|
-
[
|
74
|
-
team.id,
|
75
|
-
{
|
76
|
-
'name' => team.name
|
77
|
-
}
|
78
|
-
]
|
79
|
-
end
|
80
|
-
end
|
81
|
-
|
82
|
-
def serialize_games(games)
|
83
|
-
games.sort_by(&:date).map do |game|
|
84
|
-
{
|
85
|
-
'type' => game.class.name.split('::').last,
|
86
|
-
'date' => game.date.to_s,
|
87
|
-
'home_team' => game.home_team.id,
|
88
|
-
'away_team' => game.away_team.id
|
89
|
-
}
|
90
|
-
end
|
91
|
-
end
|
92
|
-
end
|
93
|
-
end
|
94
|
-
end
|
@@ -1,198 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
require_relative 'calendar_serializer'
|
4
|
-
require_relative 'conference'
|
5
|
-
require_relative 'coordinator'
|
6
|
-
require_relative 'division'
|
7
|
-
require_relative 'league'
|
8
|
-
require_relative 'league_serializer'
|
9
|
-
require_relative 'team'
|
10
|
-
|
11
|
-
module Basketball
|
12
|
-
module Scheduling
|
13
|
-
# Examples:
|
14
|
-
# exe/basketball-schedule -o tmp/league.json
|
15
|
-
# exe/basketball-schedule -i tmp/league.json -o tmp/calendar.json
|
16
|
-
# exe/basketball-schedule -i tmp/league.json -o tmp/calendar.json -y 2005
|
17
|
-
# exe/basketball-schedule -c tmp/calendar.json
|
18
|
-
# exe/basketball-schedule -c tmp/calendar.json -t C0-D0-T0
|
19
|
-
# exe/basketball-schedule -c tmp/calendar.json -d 2005-02-03
|
20
|
-
# exe/basketball-schedule -c tmp/calendar.json -d 2005-02-03 -t C0-D0-T0
|
21
|
-
class CLI
|
22
|
-
attr_reader :opts,
|
23
|
-
:league_serializer,
|
24
|
-
:calendar_serializer,
|
25
|
-
:io,
|
26
|
-
:coordinator
|
27
|
-
|
28
|
-
def initialize(args:, io: $stdout)
|
29
|
-
@io = io
|
30
|
-
@opts = slop_parse(args)
|
31
|
-
@league_serializer = LeagueSerializer.new
|
32
|
-
@calendar_serializer = CalendarSerializer.new
|
33
|
-
@coordinator = Coordinator.new
|
34
|
-
|
35
|
-
freeze
|
36
|
-
end
|
37
|
-
|
38
|
-
def invoke!
|
39
|
-
if output?
|
40
|
-
out_dir = File.dirname(output)
|
41
|
-
FileUtils.mkdir_p(out_dir)
|
42
|
-
end
|
43
|
-
|
44
|
-
if output? && no_input?
|
45
|
-
execute_with_no_input
|
46
|
-
elsif output?
|
47
|
-
execute_with_input
|
48
|
-
end
|
49
|
-
|
50
|
-
output_cal_query if cal
|
51
|
-
|
52
|
-
self
|
53
|
-
end
|
54
|
-
|
55
|
-
private
|
56
|
-
|
57
|
-
def output_cal_query
|
58
|
-
contents = File.read(cal)
|
59
|
-
calendar = calendar_serializer.deserialize(contents)
|
60
|
-
team_instance = team ? calendar.team(team) : nil
|
61
|
-
games = calendar.games_for(date:, team: team_instance).sort_by(&:date)
|
62
|
-
pre_counter = 1
|
63
|
-
counter = 1
|
64
|
-
|
65
|
-
io.puts("Games for [team: #{team}, date: #{date}]")
|
66
|
-
games.each do |game|
|
67
|
-
if game.is_a?(PreseasonGame)
|
68
|
-
io.puts("##{pre_counter} - #{game}")
|
69
|
-
pre_counter += 1
|
70
|
-
else
|
71
|
-
io.puts("##{counter} - #{game}")
|
72
|
-
counter += 1
|
73
|
-
end
|
74
|
-
end
|
75
|
-
end
|
76
|
-
|
77
|
-
def execute_with_input
|
78
|
-
io.puts("Loading league from: #{input}")
|
79
|
-
|
80
|
-
contents = File.read(input)
|
81
|
-
league = league_serializer.deserialize(contents)
|
82
|
-
|
83
|
-
io.puts("Generating calendar for the year #{year}...")
|
84
|
-
|
85
|
-
calendar = coordinator.schedule(league:, year:)
|
86
|
-
contents = calendar_serializer.serialize(calendar)
|
87
|
-
|
88
|
-
File.write(output, contents)
|
89
|
-
|
90
|
-
io.puts("Calendar written to: #{output}")
|
91
|
-
end
|
92
|
-
|
93
|
-
def execute_with_no_input
|
94
|
-
league = generate_league
|
95
|
-
contents = league_serializer.serialize(league)
|
96
|
-
|
97
|
-
File.write(output, contents)
|
98
|
-
|
99
|
-
io.puts("League written to: #{output}")
|
100
|
-
end
|
101
|
-
|
102
|
-
def cal
|
103
|
-
opts[:cal].to_s.empty? ? nil : opts[:cal]
|
104
|
-
end
|
105
|
-
|
106
|
-
def team
|
107
|
-
opts[:team].to_s.empty? ? nil : opts[:team]
|
108
|
-
end
|
109
|
-
|
110
|
-
def date
|
111
|
-
opts[:date].to_s.empty? ? nil : Date.parse(opts[:date])
|
112
|
-
end
|
113
|
-
|
114
|
-
def year
|
115
|
-
opts[:year].to_s.empty? ? Date.today.year : opts[:year]
|
116
|
-
end
|
117
|
-
|
118
|
-
def no_input?
|
119
|
-
input.to_s.empty?
|
120
|
-
end
|
121
|
-
|
122
|
-
def input
|
123
|
-
opts[:input]
|
124
|
-
end
|
125
|
-
|
126
|
-
def output?
|
127
|
-
!output.to_s.empty?
|
128
|
-
end
|
129
|
-
|
130
|
-
def output
|
131
|
-
opts[:output]
|
132
|
-
end
|
133
|
-
|
134
|
-
def generate_conferences
|
135
|
-
2.times.map do |i|
|
136
|
-
id = "C#{i}"
|
137
|
-
|
138
|
-
Conference.new(
|
139
|
-
id:,
|
140
|
-
name: Faker::Esport.league,
|
141
|
-
divisions: generate_divisions("#{id}-")
|
142
|
-
)
|
143
|
-
end
|
144
|
-
end
|
145
|
-
|
146
|
-
def generate_divisions(id_prefix)
|
147
|
-
3.times.map do |j|
|
148
|
-
id = "#{id_prefix}D#{j}"
|
149
|
-
|
150
|
-
Division.new(
|
151
|
-
id:,
|
152
|
-
name: Faker::Address.community,
|
153
|
-
teams: generate_teams("#{id}-")
|
154
|
-
)
|
155
|
-
end
|
156
|
-
end
|
157
|
-
|
158
|
-
def generate_teams(id_prefix)
|
159
|
-
5.times.map do |k|
|
160
|
-
Team.new(
|
161
|
-
id: "#{id_prefix}T#{k}",
|
162
|
-
name: Faker::Team.name
|
163
|
-
)
|
164
|
-
end
|
165
|
-
end
|
166
|
-
|
167
|
-
def generate_league
|
168
|
-
League.new(conferences: generate_conferences)
|
169
|
-
end
|
170
|
-
|
171
|
-
def slop_parse(args)
|
172
|
-
Slop.parse(args) do |o|
|
173
|
-
o.banner = 'Usage: basketball-schedule [options] ...'
|
174
|
-
|
175
|
-
output_description = <<~DESC
|
176
|
-
If input path is omitted then a new league will be written to this path.
|
177
|
-
If an input path is specified then a Calendar will be written to the output path.
|
178
|
-
DESC
|
179
|
-
|
180
|
-
# League and Calendar Generation Interface
|
181
|
-
o.string '-i', '--input', 'Path to load the League from. If omitted then a new league will be generated.'
|
182
|
-
o.string '-o', '--output', output_description
|
183
|
-
o.integer '-y', '--year', 'Year to use to generate a calendar for (defaults to current year).'
|
184
|
-
|
185
|
-
# Calendar Query Interface
|
186
|
-
o.string '-c', '--cal', 'Path to load a Calendar from. If omitted then no matchups will be outputted.'
|
187
|
-
o.string '-d', '--date', 'Filter matchups to just the date specified (requires --cal option).'
|
188
|
-
o.string '-t', '--team', 'Filter matchups to just the team ID specified (requires --cal option).'
|
189
|
-
|
190
|
-
o.on '-h', '--help', 'Print out help, like this is doing right now.' do
|
191
|
-
io.puts(o)
|
192
|
-
exit
|
193
|
-
end
|
194
|
-
end.to_h
|
195
|
-
end
|
196
|
-
end
|
197
|
-
end
|
198
|
-
end
|
@@ -1,57 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
module Basketball
|
4
|
-
module Scheduling
|
5
|
-
class Conference < Entity
|
6
|
-
DIVISIONS_SIZE = 3
|
7
|
-
|
8
|
-
attr_reader :name, :divisions
|
9
|
-
|
10
|
-
def initialize(id:, name: '', divisions: [])
|
11
|
-
super(id)
|
12
|
-
|
13
|
-
@name = name.to_s
|
14
|
-
@divisions = []
|
15
|
-
|
16
|
-
divisions.each { |d| register_division!(d) }
|
17
|
-
|
18
|
-
if divisions.length != DIVISIONS_SIZE
|
19
|
-
raise BadDivisionsSizeError, "#{id} should have exactly #{DIVISIONS_SIZE} divisions"
|
20
|
-
end
|
21
|
-
|
22
|
-
freeze
|
23
|
-
end
|
24
|
-
|
25
|
-
def to_s
|
26
|
-
(["[#{super}] #{name}"] + divisions.map(&:to_s)).join("\n")
|
27
|
-
end
|
28
|
-
|
29
|
-
def division?(division)
|
30
|
-
divisions.include?(division)
|
31
|
-
end
|
32
|
-
|
33
|
-
def teams
|
34
|
-
divisions.flat_map(&:teams)
|
35
|
-
end
|
36
|
-
|
37
|
-
def team?(team)
|
38
|
-
teams.include?(team)
|
39
|
-
end
|
40
|
-
|
41
|
-
private
|
42
|
-
|
43
|
-
def register_division!(division)
|
44
|
-
raise ArgumentError, 'division is required' unless division
|
45
|
-
raise DivisionAlreadyRegisteredError, "#{division} already registered" if division?(division)
|
46
|
-
|
47
|
-
division.teams.each do |team|
|
48
|
-
raise TeamAlreadyRegisteredError, "#{team} already registered" if team?(team)
|
49
|
-
end
|
50
|
-
|
51
|
-
divisions << division
|
52
|
-
|
53
|
-
self
|
54
|
-
end
|
55
|
-
end
|
56
|
-
end
|
57
|
-
end
|
@@ -1,180 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
require_relative 'calendar'
|
4
|
-
|
5
|
-
module Basketball
|
6
|
-
module Scheduling
|
7
|
-
# This is the service class responsible for actually picking out free dates ane pairing up teams to
|
8
|
-
# play each other. This is a reasonable naive first pass at some underlying match-making algorithms
|
9
|
-
# but could definitely use some help with the complexity/runtime/etc.
|
10
|
-
class Coordinator
|
11
|
-
MIN_PRESEASON_GAMES_PER_TEAM = 4
|
12
|
-
MAX_PRESEASON_GAMES_PER_TEAM = 6
|
13
|
-
|
14
|
-
private_constant :MIN_PRESEASON_GAMES_PER_TEAM,
|
15
|
-
:MAX_PRESEASON_GAMES_PER_TEAM
|
16
|
-
|
17
|
-
def schedule(year:, league:)
|
18
|
-
Calendar.new(year:).tap do |calendar|
|
19
|
-
schedule_preseason!(calendar:, league:)
|
20
|
-
schedule_season!(calendar:, league:)
|
21
|
-
end
|
22
|
-
end
|
23
|
-
|
24
|
-
private
|
25
|
-
|
26
|
-
def base_matchup_count(league, team1, team2)
|
27
|
-
# Same Conference, Same Division
|
28
|
-
if league.division_for(team1) == league.division_for(team2)
|
29
|
-
4
|
30
|
-
# Same Conference, Different Division and one of 4/10 that play 3 times
|
31
|
-
elsif league.conference_for(team1) == league.conference_for(team2)
|
32
|
-
3
|
33
|
-
# Different Conference
|
34
|
-
else
|
35
|
-
2
|
36
|
-
end
|
37
|
-
end
|
38
|
-
|
39
|
-
# rubocop:disable Metrics/AbcSize
|
40
|
-
# This method derives the plan for which a schedule can be generated from.
|
41
|
-
def matchup_plan(league)
|
42
|
-
matchups = {}
|
43
|
-
game_counts = league.teams.to_h { |t| [t, 0] }
|
44
|
-
teams = game_counts.keys
|
45
|
-
|
46
|
-
(0...teams.length).each do |i|
|
47
|
-
team1 = teams[i]
|
48
|
-
|
49
|
-
(i + 1...teams.length).each do |j|
|
50
|
-
team2 = teams[j]
|
51
|
-
key = [team1, team2].sort
|
52
|
-
count = base_matchup_count(league, team1, team2)
|
53
|
-
matchups[key] = count
|
54
|
-
game_counts[team1] += count
|
55
|
-
game_counts[team2] += count
|
56
|
-
end
|
57
|
-
end
|
58
|
-
|
59
|
-
# Each team will play 6 games against conference opponents in other divisions.
|
60
|
-
# The fours hash will be that plan.
|
61
|
-
find_fours(league).each do |team, opponents|
|
62
|
-
next if game_counts[team] == 82
|
63
|
-
|
64
|
-
opponents.each do |opponent|
|
65
|
-
next if game_counts[team] == 82
|
66
|
-
next if game_counts[opponent] == 82
|
67
|
-
|
68
|
-
game_counts[team] += 1
|
69
|
-
game_counts[opponent] += 1
|
70
|
-
|
71
|
-
key = [team, opponent].sort
|
72
|
-
|
73
|
-
matchups[key] += 1
|
74
|
-
end
|
75
|
-
end
|
76
|
-
|
77
|
-
matchups
|
78
|
-
end
|
79
|
-
# rubocop:enable Metrics/AbcSize
|
80
|
-
|
81
|
-
# rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
|
82
|
-
# I am not liking this algorithm implementation at all but it will seemingly produce a valid
|
83
|
-
# result about 1 out of every 1000 cycles. I have yet to spot the assignment pattern to make
|
84
|
-
# this way more deterministic.
|
85
|
-
def find_fours(league)
|
86
|
-
balanced = false
|
87
|
-
count = 0
|
88
|
-
four_tracker = {}
|
89
|
-
|
90
|
-
until balanced
|
91
|
-
# Let's not completely thrash our CPUs in case this algorithm hits an infinite loop.
|
92
|
-
# Instead, lets hard-fail against a hard boundary.
|
93
|
-
raise ArgumentError, 'we spent too much CPU time and didnt resolve fours' if count > 100_000
|
94
|
-
|
95
|
-
four_tracker = league.teams.to_h { |team| [team, []] }
|
96
|
-
|
97
|
-
league.teams.each do |team|
|
98
|
-
opponents = league.cross_division_opponents_for(team).shuffle
|
99
|
-
|
100
|
-
opponents.each do |opponent|
|
101
|
-
if four_tracker[team].length < 6 && four_tracker[opponent].length < 6
|
102
|
-
four_tracker[opponent] << team
|
103
|
-
four_tracker[team] << opponent
|
104
|
-
end
|
105
|
-
end
|
106
|
-
end
|
107
|
-
|
108
|
-
good = true
|
109
|
-
|
110
|
-
# trip-wire: if one team isnt balanced then we are not balanced
|
111
|
-
four_tracker.each { |_k, v| good = false if v.length < 6 }
|
112
|
-
|
113
|
-
balanced = good
|
114
|
-
|
115
|
-
count += 1
|
116
|
-
end
|
117
|
-
|
118
|
-
four_tracker
|
119
|
-
end
|
120
|
-
# rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
|
121
|
-
|
122
|
-
def schedule_season!(calendar:, league:)
|
123
|
-
matchups = matchup_plan(league)
|
124
|
-
|
125
|
-
matchups.each do |(team1, team2), count|
|
126
|
-
candidates = calendar.available_season_matchup_dates(team1, team2)
|
127
|
-
dates = candidates.sample(count)
|
128
|
-
games = balanced_games(dates, team1, team2)
|
129
|
-
|
130
|
-
games.each { |game| calendar.add!(game) }
|
131
|
-
end
|
132
|
-
end
|
133
|
-
|
134
|
-
def balanced_games(dates, team1, team2)
|
135
|
-
dates.map.with_index(1) do |date, index|
|
136
|
-
if index.even?
|
137
|
-
SeasonGame.new(date:, home_team: team1, away_team: team2)
|
138
|
-
else
|
139
|
-
SeasonGame.new(date:, home_team: team2, away_team: team1)
|
140
|
-
end
|
141
|
-
end
|
142
|
-
end
|
143
|
-
|
144
|
-
def schedule_preseason!(calendar:, league:)
|
145
|
-
league.teams.each do |team|
|
146
|
-
current_games = calendar.preseason_games_for(team:)
|
147
|
-
count = current_games.length
|
148
|
-
|
149
|
-
next if count >= MIN_PRESEASON_GAMES_PER_TEAM
|
150
|
-
|
151
|
-
other_teams = (league.teams - [team]).shuffle
|
152
|
-
|
153
|
-
other_teams.each do |other_team|
|
154
|
-
break if count > MIN_PRESEASON_GAMES_PER_TEAM
|
155
|
-
next if calendar.preseason_games_for(team: other_team).length >= MAX_PRESEASON_GAMES_PER_TEAM
|
156
|
-
|
157
|
-
candidates = calendar.available_preseason_matchup_dates(team, other_team)
|
158
|
-
|
159
|
-
next if candidates.empty?
|
160
|
-
|
161
|
-
date = candidates.sample
|
162
|
-
game = random_preseason_game(date, team, other_team)
|
163
|
-
|
164
|
-
calendar.add!(game)
|
165
|
-
|
166
|
-
count += 1
|
167
|
-
end
|
168
|
-
end
|
169
|
-
end
|
170
|
-
|
171
|
-
def random_preseason_game(date, team1, team2)
|
172
|
-
if rand(1..2) == 1
|
173
|
-
PreseasonGame.new(date:, home_team: team1, away_team: team2)
|
174
|
-
else
|
175
|
-
PreseasonGame.new(date:, home_team: team2, away_team: team1)
|
176
|
-
end
|
177
|
-
end
|
178
|
-
end
|
179
|
-
end
|
180
|
-
end
|
@@ -1,43 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
module Basketball
|
4
|
-
module Scheduling
|
5
|
-
class Division < Entity
|
6
|
-
TEAMS_SIZE = 5
|
7
|
-
|
8
|
-
attr_reader :name, :teams
|
9
|
-
|
10
|
-
def initialize(id:, name: '', teams: [])
|
11
|
-
super(id)
|
12
|
-
|
13
|
-
@name = name.to_s
|
14
|
-
@teams = []
|
15
|
-
|
16
|
-
teams.each { |t| register_team!(t) }
|
17
|
-
|
18
|
-
raise BadTeamsSizeError, "#{id} should have exactly #{TEAMS_SIZE} teams" if teams.length != TEAMS_SIZE
|
19
|
-
|
20
|
-
freeze
|
21
|
-
end
|
22
|
-
|
23
|
-
def to_s
|
24
|
-
(["[#{super}] #{name}"] + teams.map(&:to_s)).join("\n")
|
25
|
-
end
|
26
|
-
|
27
|
-
def team?(team)
|
28
|
-
teams.include?(team)
|
29
|
-
end
|
30
|
-
|
31
|
-
private
|
32
|
-
|
33
|
-
def register_team!(team)
|
34
|
-
raise ArgumentError, 'team is required' unless team
|
35
|
-
raise TeamAlreadyRegisteredError, "#{team} already registered" if team?(team)
|
36
|
-
|
37
|
-
teams << team
|
38
|
-
|
39
|
-
self
|
40
|
-
end
|
41
|
-
end
|
42
|
-
end
|
43
|
-
end
|
@@ -1,32 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
module Basketball
|
4
|
-
module Scheduling
|
5
|
-
class Game < ValueObject
|
6
|
-
attr_reader_value :date, :home_team, :away_team
|
7
|
-
|
8
|
-
def initialize(date:, home_team:, away_team:)
|
9
|
-
super()
|
10
|
-
|
11
|
-
raise ArgumentError, 'date is required' unless date
|
12
|
-
raise ArgumentError, 'home_team is required' unless home_team
|
13
|
-
raise ArgumentError, 'away_team is required' unless away_team
|
14
|
-
raise ArgumentError, 'teams cannot play themselves' if home_team == away_team
|
15
|
-
|
16
|
-
@date = date
|
17
|
-
@home_team = home_team
|
18
|
-
@away_team = away_team
|
19
|
-
|
20
|
-
freeze
|
21
|
-
end
|
22
|
-
|
23
|
-
def teams
|
24
|
-
[home_team, away_team]
|
25
|
-
end
|
26
|
-
|
27
|
-
def to_s
|
28
|
-
"#{date} - #{away_team} at #{home_team}"
|
29
|
-
end
|
30
|
-
end
|
31
|
-
end
|
32
|
-
end
|