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,223 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Basketball
|
4
|
+
module Season
|
5
|
+
# This is the service class responsible for actually picking out free dates and pairing up teams to
|
6
|
+
# play each other. This is a reasonable naive first pass at some underlying match-making algorithms
|
7
|
+
# but could definitely use some help with the complexity/runtime/etc.
|
8
|
+
class Scheduler
|
9
|
+
class BadConferencesSizeError < StandardError; end
|
10
|
+
class BadDivisionsSizeError < StandardError; end
|
11
|
+
class BadTeamsSizeError < StandardError; end
|
12
|
+
|
13
|
+
MIN_PRESEASON_GAMES_PER_TEAM = 3
|
14
|
+
MAX_PRESEASON_GAMES_PER_TEAM = 6
|
15
|
+
CONFERENCES_SIZE = 2
|
16
|
+
DIVISIONS_SIZE = 3
|
17
|
+
TEAMS_SIZE = 5
|
18
|
+
|
19
|
+
private_constant :MIN_PRESEASON_GAMES_PER_TEAM,
|
20
|
+
:MAX_PRESEASON_GAMES_PER_TEAM,
|
21
|
+
:CONFERENCES_SIZE,
|
22
|
+
:DIVISIONS_SIZE,
|
23
|
+
:TEAMS_SIZE
|
24
|
+
|
25
|
+
def schedule(league:, year: Time.now.year)
|
26
|
+
assert_properly_sized_league(league)
|
27
|
+
|
28
|
+
Calendar.new(**make_dates(year)).tap do |calendar|
|
29
|
+
schedule_exhibition!(calendar:, league:)
|
30
|
+
schedule_season!(calendar:, league:)
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
private
|
35
|
+
|
36
|
+
def make_dates(year)
|
37
|
+
{
|
38
|
+
exhibition_start_date: Date.new(year, 9, 30),
|
39
|
+
exhibition_end_date: Date.new(year, 10, 14),
|
40
|
+
regular_start_date: Date.new(year, 10, 18),
|
41
|
+
regular_end_date: Date.new(year + 1, 4, 29)
|
42
|
+
}
|
43
|
+
end
|
44
|
+
|
45
|
+
def assert_properly_sized_league(league)
|
46
|
+
if league.conferences.length != CONFERENCES_SIZE
|
47
|
+
raise BadConferencesSizeError, "there has to be #{CONFERENCES_SIZE} conferences"
|
48
|
+
end
|
49
|
+
|
50
|
+
league.conferences.each do |conference|
|
51
|
+
if conference.divisions.length != DIVISIONS_SIZE
|
52
|
+
raise BadDivisionsSizeError, "#{conference.id} should have exactly #{DIVISIONS_SIZE} divisions"
|
53
|
+
end
|
54
|
+
|
55
|
+
conference.divisions.each do |division|
|
56
|
+
if division.teams.length != TEAMS_SIZE
|
57
|
+
raise BadTeamsSizeError, "#{division.id} should have exactly #{TEAMS_SIZE} teams"
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
def base_matchup_count(league, team1, team2)
|
64
|
+
# Same Conference, Same Division
|
65
|
+
if league.division_for(team1) == league.division_for(team2)
|
66
|
+
4
|
67
|
+
# Same Conference, Different Division and one of 4/10 that play 3 times
|
68
|
+
elsif league.conference_for(team1) == league.conference_for(team2)
|
69
|
+
3
|
70
|
+
# Different Conference
|
71
|
+
else
|
72
|
+
2
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
# rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
|
77
|
+
# This method derives the plan for which a schedule can be generated from.
|
78
|
+
def matchup_plan(league)
|
79
|
+
matchups = {}
|
80
|
+
game_counts = league.teams.to_h { |t| [t, 0] }
|
81
|
+
teams = game_counts.keys
|
82
|
+
|
83
|
+
(0...teams.length).each do |i|
|
84
|
+
team1 = teams[i]
|
85
|
+
|
86
|
+
(i + 1...teams.length).each do |j|
|
87
|
+
team2 = teams[j]
|
88
|
+
key = [team1, team2].sort
|
89
|
+
count = base_matchup_count(league, team1, team2)
|
90
|
+
matchups[key] = count
|
91
|
+
game_counts[team1] += count
|
92
|
+
game_counts[team2] += count
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
# Each team will play 6 games against conference opponents in other divisions.
|
97
|
+
# The fours hash will be that plan.
|
98
|
+
find_fours(league).each do |team, opponents|
|
99
|
+
next if game_counts[team] == 82
|
100
|
+
|
101
|
+
opponents.each do |opponent|
|
102
|
+
next if game_counts[team] == 82
|
103
|
+
next if game_counts[opponent] == 82
|
104
|
+
|
105
|
+
game_counts[team] += 1
|
106
|
+
game_counts[opponent] += 1
|
107
|
+
|
108
|
+
key = [team, opponent].sort
|
109
|
+
|
110
|
+
matchups[key] += 1
|
111
|
+
end
|
112
|
+
end
|
113
|
+
|
114
|
+
matchups
|
115
|
+
end
|
116
|
+
# rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
|
117
|
+
|
118
|
+
# rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
|
119
|
+
# I am not liking this algorithm implementation at all but it will seemingly produce a valid
|
120
|
+
# result about 1 out of every 1000 cycles. I have yet to spot the assignment pattern to make
|
121
|
+
# this way more deterministic.
|
122
|
+
def find_fours(league)
|
123
|
+
balanced = false
|
124
|
+
count = 0
|
125
|
+
four_tracker = {}
|
126
|
+
|
127
|
+
until balanced
|
128
|
+
# Let's not completely thrash our CPUs in case this algorithm hits an infinite loop.
|
129
|
+
# Instead, lets hard-fail against a hard boundary.
|
130
|
+
raise ArgumentError, 'we spent too much CPU time and didnt resolve fours' if count > 100_000
|
131
|
+
|
132
|
+
four_tracker = league.teams.to_h { |team| [team, []] }
|
133
|
+
|
134
|
+
league.teams.each do |team|
|
135
|
+
opponents = league.cross_division_opponents_for(team).shuffle
|
136
|
+
|
137
|
+
opponents.each do |opponent|
|
138
|
+
if four_tracker[team].length < 6 && four_tracker[opponent].length < 6
|
139
|
+
four_tracker[opponent] << team
|
140
|
+
four_tracker[team] << opponent
|
141
|
+
end
|
142
|
+
end
|
143
|
+
end
|
144
|
+
|
145
|
+
good = true
|
146
|
+
|
147
|
+
# trip-wire: if one team isnt balanced then we are not balanced
|
148
|
+
four_tracker.each { |_k, v| good = false if v.length < 6 }
|
149
|
+
|
150
|
+
balanced = good
|
151
|
+
|
152
|
+
count += 1
|
153
|
+
end
|
154
|
+
|
155
|
+
four_tracker
|
156
|
+
end
|
157
|
+
# rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
|
158
|
+
|
159
|
+
def schedule_season!(calendar:, league:)
|
160
|
+
matchups = matchup_plan(league)
|
161
|
+
|
162
|
+
matchups.each do |(team1, team2), count|
|
163
|
+
candidates = calendar.available_regular_matchup_dates(team1, team2)
|
164
|
+
dates = candidates.sample(count)
|
165
|
+
games = balanced_games(dates, team1, team2)
|
166
|
+
|
167
|
+
games.each { |game| calendar.add!(game) }
|
168
|
+
end
|
169
|
+
end
|
170
|
+
|
171
|
+
def balanced_games(dates, team1, team2)
|
172
|
+
dates.map.with_index(1) do |date, index|
|
173
|
+
home_opponent, away_opponent =
|
174
|
+
if index.even?
|
175
|
+
[Opponent.from(team1), Opponent.from(team2)]
|
176
|
+
else
|
177
|
+
[Opponent.from(team2), Opponent.from(team1)]
|
178
|
+
end
|
179
|
+
|
180
|
+
Regular.new(date:, home_opponent:, away_opponent:)
|
181
|
+
end
|
182
|
+
end
|
183
|
+
|
184
|
+
def schedule_exhibition!(calendar:, league:)
|
185
|
+
league.teams.each do |team|
|
186
|
+
current_games = calendar.exhibitions_for(opponent: team)
|
187
|
+
count = current_games.length
|
188
|
+
|
189
|
+
next if count >= MIN_PRESEASON_GAMES_PER_TEAM
|
190
|
+
|
191
|
+
other_teams = (league.teams - [team]).shuffle
|
192
|
+
|
193
|
+
other_teams.each do |other_team|
|
194
|
+
break if count > MIN_PRESEASON_GAMES_PER_TEAM
|
195
|
+
next if calendar.exhibitions_for(opponent: other_team).length >= MAX_PRESEASON_GAMES_PER_TEAM
|
196
|
+
|
197
|
+
candidates = calendar.available_exhibition_matchup_dates(team, other_team)
|
198
|
+
|
199
|
+
next if candidates.empty?
|
200
|
+
|
201
|
+
date = candidates.sample
|
202
|
+
game = random_exhibition_game(date, team, other_team)
|
203
|
+
|
204
|
+
calendar.add!(game)
|
205
|
+
|
206
|
+
count += 1
|
207
|
+
end
|
208
|
+
end
|
209
|
+
end
|
210
|
+
|
211
|
+
def random_exhibition_game(date, team1, team2)
|
212
|
+
home_opponent, away_opponent =
|
213
|
+
if rand(1..2) == 1
|
214
|
+
[Opponent.from(team1), Opponent.from(team2)]
|
215
|
+
else
|
216
|
+
[Opponent.from(team2), Opponent.from(team1)]
|
217
|
+
end
|
218
|
+
|
219
|
+
Exhibition.new(date:, home_opponent:, away_opponent:)
|
220
|
+
end
|
221
|
+
end
|
222
|
+
end
|
223
|
+
end
|
@@ -0,0 +1,56 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Basketball
|
4
|
+
module Season
|
5
|
+
# Represents a League with each team's win/loss details.
|
6
|
+
class Standings
|
7
|
+
class TeamAlreadyRegisteredError < StandardError; end
|
8
|
+
class TeamNotRegisteredError < StandardError; end
|
9
|
+
|
10
|
+
def initialize
|
11
|
+
@records_by_id = {}
|
12
|
+
end
|
13
|
+
|
14
|
+
def register!(team)
|
15
|
+
raise TeamAlreadyRegisteredError, "#{team} already registered!" if team?(team)
|
16
|
+
|
17
|
+
records_by_id[team.id] = Record.new(id: team.id)
|
18
|
+
|
19
|
+
self
|
20
|
+
end
|
21
|
+
|
22
|
+
def records
|
23
|
+
records_by_id.values
|
24
|
+
end
|
25
|
+
|
26
|
+
def record_for(team)
|
27
|
+
raise TeamNotRegisteredError, "#{team} not registered" unless team?(team)
|
28
|
+
|
29
|
+
records_by_id.fetch(team.id)
|
30
|
+
end
|
31
|
+
|
32
|
+
def accept!(result)
|
33
|
+
[
|
34
|
+
record_for(result.home_opponent),
|
35
|
+
record_for(result.away_opponent)
|
36
|
+
].each do |record|
|
37
|
+
record.accept!(result)
|
38
|
+
end
|
39
|
+
|
40
|
+
self
|
41
|
+
end
|
42
|
+
|
43
|
+
def team?(team)
|
44
|
+
records_by_id.key?(team.id)
|
45
|
+
end
|
46
|
+
|
47
|
+
def to_s
|
48
|
+
records.sort.reverse.map.with_index(1) { |r, i| "##{i} #{r}" }.join("\n")
|
49
|
+
end
|
50
|
+
|
51
|
+
private
|
52
|
+
|
53
|
+
attr_reader :records_by_id
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
data/lib/basketball/season.rb
CHANGED
@@ -12,5 +12,11 @@ require_relative 'season/opponent'
|
|
12
12
|
require_relative 'season/exhibition'
|
13
13
|
require_relative 'season/regular'
|
14
14
|
|
15
|
+
# Standings
|
16
|
+
require_relative 'season/detail'
|
17
|
+
require_relative 'season/record'
|
18
|
+
require_relative 'season/standings'
|
19
|
+
|
15
20
|
# Specific
|
16
21
|
require_relative 'season/coordinator'
|
22
|
+
require_relative 'season/scheduler'
|
@@ -1,37 +1,15 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
require_relative 'value_object_dsl'
|
4
|
+
|
3
5
|
module Basketball
|
4
6
|
# A Value Object is something that has no specific identity, instead its identity is the sum of
|
5
7
|
# the attribute values. Changing one will change the entire object's identity.
|
6
|
-
# Comes with a very simple DSL for specifying properties along with
|
8
|
+
# Comes with a very simple DSL provided by ValueObjectDSL for specifying properties along with
|
9
|
+
# base equality and sorting methods.
|
7
10
|
class ValueObject
|
8
11
|
include Comparable
|
9
|
-
|
10
|
-
class << self
|
11
|
-
def all_value_keys
|
12
|
-
@all_value_keys ||= ancestors.flat_map do |ancestor|
|
13
|
-
if ancestor < Basketball::ValueObject
|
14
|
-
ancestor.value_keys
|
15
|
-
else
|
16
|
-
[]
|
17
|
-
end
|
18
|
-
end.uniq.sort
|
19
|
-
end
|
20
|
-
|
21
|
-
def all_sorted_value_keys
|
22
|
-
all_value_keys.sort
|
23
|
-
end
|
24
|
-
|
25
|
-
def value_keys
|
26
|
-
@value_keys ||= []
|
27
|
-
end
|
28
|
-
|
29
|
-
def value_reader(*keys)
|
30
|
-
keys.each { |k| value_keys << k.to_sym }
|
31
|
-
|
32
|
-
attr_reader(*keys)
|
33
|
-
end
|
34
|
-
end
|
12
|
+
extend ValueObjectDSL
|
35
13
|
|
36
14
|
def to_s
|
37
15
|
to_h.map { |k, v| "#{k}: #{v}" }.join(', ')
|
@@ -55,7 +33,7 @@ module Basketball
|
|
55
33
|
alias eql? ==
|
56
34
|
|
57
35
|
def hash
|
58
|
-
all_sorted_values.
|
36
|
+
all_sorted_values.hash
|
59
37
|
end
|
60
38
|
|
61
39
|
def all_sorted_values
|
@@ -0,0 +1,30 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Basketball
|
4
|
+
# Class-level methods that extend core Ruby attr_* methods.
|
5
|
+
module ValueObjectDSL
|
6
|
+
def all_value_keys
|
7
|
+
@all_value_keys ||= ancestors.flat_map do |ancestor|
|
8
|
+
if ancestor < Basketball::ValueObject
|
9
|
+
ancestor.value_keys
|
10
|
+
else
|
11
|
+
[]
|
12
|
+
end
|
13
|
+
end.uniq.sort
|
14
|
+
end
|
15
|
+
|
16
|
+
def all_sorted_value_keys
|
17
|
+
all_value_keys.sort
|
18
|
+
end
|
19
|
+
|
20
|
+
def value_keys
|
21
|
+
@value_keys ||= []
|
22
|
+
end
|
23
|
+
|
24
|
+
def value_reader(*keys)
|
25
|
+
keys.each { |k| value_keys << k.to_sym }
|
26
|
+
|
27
|
+
attr_reader(*keys)
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
data/lib/basketball/version.rb
CHANGED
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: basketball
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0.
|
4
|
+
version: 0.0.11
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Matthew Ruggio
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2023-
|
11
|
+
date: 2023-06-07 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: slop
|
@@ -31,8 +31,9 @@ description: " This library is meant to serve as the domain for a basketball
|
|
31
31
|
email:
|
32
32
|
- mattruggio@icloud.com
|
33
33
|
executables:
|
34
|
-
- basketball
|
35
|
-
- basketball-
|
34
|
+
- basketball
|
35
|
+
- basketball-season-coordinator
|
36
|
+
- basketball-draft-room
|
36
37
|
extensions: []
|
37
38
|
extra_rdoc_files: []
|
38
39
|
files:
|
@@ -48,13 +49,18 @@ files:
|
|
48
49
|
- README.md
|
49
50
|
- Rakefile
|
50
51
|
- basketball.gemspec
|
51
|
-
- exe/basketball
|
52
|
-
- exe/basketball-room
|
52
|
+
- exe/basketball
|
53
|
+
- exe/basketball-draft-room
|
54
|
+
- exe/basketball-season-coordinator
|
53
55
|
- lib/basketball.rb
|
54
56
|
- lib/basketball/app.rb
|
55
57
|
- lib/basketball/app/coordinator_cli.rb
|
56
58
|
- lib/basketball/app/coordinator_repository.rb
|
59
|
+
- lib/basketball/app/document_repository.rb
|
57
60
|
- lib/basketball/app/file_store.rb
|
61
|
+
- lib/basketball/app/in_memory_store.rb
|
62
|
+
- lib/basketball/app/league_repository.rb
|
63
|
+
- lib/basketball/app/league_serializable.rb
|
58
64
|
- lib/basketball/app/room_cli.rb
|
59
65
|
- lib/basketball/app/room_repository.rb
|
60
66
|
- lib/basketball/draft.rb
|
@@ -67,6 +73,11 @@ files:
|
|
67
73
|
- lib/basketball/draft/skip.rb
|
68
74
|
- lib/basketball/entity.rb
|
69
75
|
- lib/basketball/org.rb
|
76
|
+
- lib/basketball/org/conference.rb
|
77
|
+
- lib/basketball/org/division.rb
|
78
|
+
- lib/basketball/org/has_divisions.rb
|
79
|
+
- lib/basketball/org/has_players.rb
|
80
|
+
- lib/basketball/org/has_teams.rb
|
70
81
|
- lib/basketball/org/league.rb
|
71
82
|
- lib/basketball/org/player.rb
|
72
83
|
- lib/basketball/org/position.rb
|
@@ -75,13 +86,18 @@ files:
|
|
75
86
|
- lib/basketball/season/arena.rb
|
76
87
|
- lib/basketball/season/calendar.rb
|
77
88
|
- lib/basketball/season/coordinator.rb
|
89
|
+
- lib/basketball/season/detail.rb
|
78
90
|
- lib/basketball/season/exhibition.rb
|
79
91
|
- lib/basketball/season/game.rb
|
80
92
|
- lib/basketball/season/matchup.rb
|
81
93
|
- lib/basketball/season/opponent.rb
|
94
|
+
- lib/basketball/season/record.rb
|
82
95
|
- lib/basketball/season/regular.rb
|
83
96
|
- lib/basketball/season/result.rb
|
97
|
+
- lib/basketball/season/scheduler.rb
|
98
|
+
- lib/basketball/season/standings.rb
|
84
99
|
- lib/basketball/value_object.rb
|
100
|
+
- lib/basketball/value_object_dsl.rb
|
85
101
|
- lib/basketball/version.rb
|
86
102
|
homepage: https://github.com/mattruggio/basketball
|
87
103
|
licenses:
|
File without changes
|
File without changes
|