basketball 0.0.7 → 0.0.9

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.
Files changed (64) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +9 -19
  3. data/CHANGELOG.md +1 -31
  4. data/README.md +72 -91
  5. data/basketball.gemspec +3 -6
  6. data/exe/{basketball-schedule → basketball-coordinator} +1 -1
  7. data/exe/{basketball-draft → basketball-room} +1 -1
  8. data/lib/basketball/app/coordinator_cli.rb +243 -0
  9. data/lib/basketball/app/coordinator_repository.rb +191 -0
  10. data/lib/basketball/app/file_store.rb +22 -0
  11. data/lib/basketball/app/room_cli.rb +212 -0
  12. data/lib/basketball/app/room_repository.rb +189 -0
  13. data/lib/basketball/app.rb +12 -0
  14. data/lib/basketball/draft/assessment.rb +31 -0
  15. data/lib/basketball/{drafting → draft}/event.rb +4 -3
  16. data/lib/basketball/draft/front_office.rb +99 -0
  17. data/lib/basketball/draft/pick.rb +32 -0
  18. data/lib/basketball/draft/room.rb +221 -0
  19. data/lib/basketball/{drafting/player_search.rb → draft/scout.rb} +5 -10
  20. data/lib/basketball/draft/skip.rb +12 -0
  21. data/lib/basketball/draft.rb +16 -0
  22. data/lib/basketball/entity.rb +10 -4
  23. data/lib/basketball/org/league.rb +73 -0
  24. data/lib/basketball/org/player.rb +26 -0
  25. data/lib/basketball/{drafting → org}/position.rb +3 -2
  26. data/lib/basketball/org/team.rb +38 -0
  27. data/lib/basketball/org.rb +12 -0
  28. data/lib/basketball/season/arena.rb +112 -0
  29. data/lib/basketball/season/calendar.rb +90 -0
  30. data/lib/basketball/season/coordinator.rb +239 -0
  31. data/lib/basketball/{scheduling/preseason_game.rb → season/exhibition.rb} +3 -2
  32. data/lib/basketball/season/game.rb +37 -0
  33. data/lib/basketball/season/matchup.rb +27 -0
  34. data/lib/basketball/season/opponent.rb +15 -0
  35. data/lib/basketball/season/regular.rb +9 -0
  36. data/lib/basketball/season/result.rb +37 -0
  37. data/lib/basketball/season.rb +16 -0
  38. data/lib/basketball/value_object.rb +4 -1
  39. data/lib/basketball/version.rb +1 -1
  40. data/lib/basketball.rb +11 -6
  41. metadata +40 -52
  42. data/lib/basketball/drafting/cli.rb +0 -235
  43. data/lib/basketball/drafting/engine.rb +0 -221
  44. data/lib/basketball/drafting/engine_serializer.rb +0 -186
  45. data/lib/basketball/drafting/front_office.rb +0 -92
  46. data/lib/basketball/drafting/league.rb +0 -70
  47. data/lib/basketball/drafting/pick_event.rb +0 -25
  48. data/lib/basketball/drafting/player.rb +0 -43
  49. data/lib/basketball/drafting/roster.rb +0 -37
  50. data/lib/basketball/drafting/sim_event.rb +0 -23
  51. data/lib/basketball/drafting/skip_event.rb +0 -13
  52. data/lib/basketball/drafting.rb +0 -9
  53. data/lib/basketball/scheduling/calendar.rb +0 -121
  54. data/lib/basketball/scheduling/calendar_serializer.rb +0 -94
  55. data/lib/basketball/scheduling/cli.rb +0 -198
  56. data/lib/basketball/scheduling/conference.rb +0 -57
  57. data/lib/basketball/scheduling/coordinator.rb +0 -180
  58. data/lib/basketball/scheduling/division.rb +0 -43
  59. data/lib/basketball/scheduling/game.rb +0 -32
  60. data/lib/basketball/scheduling/league.rb +0 -114
  61. data/lib/basketball/scheduling/league_serializer.rb +0 -99
  62. data/lib/basketball/scheduling/season_game.rb +0 -8
  63. data/lib/basketball/scheduling/team.rb +0 -21
  64. data/lib/basketball/scheduling.rb +0 -17
@@ -0,0 +1,112 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Basketball
4
+ module Season
5
+ # A very, very, very basic starting point for a "semi-randomized" game simulator.
6
+ class Arena
7
+ RANDOM = :random
8
+ TOP_ONE = :top_one
9
+ TOP_TWO = :top_two
10
+ TOP_THREE = :top_three
11
+ TOP_SIX = :top_six
12
+ MAX_HOME_ADVANTAGE = 5
13
+
14
+ STRATEGY_FREQUENCIES = {
15
+ RANDOM => 10,
16
+ TOP_ONE => 5,
17
+ TOP_TWO => 10,
18
+ TOP_THREE => 20,
19
+ TOP_SIX => 30
20
+ }.freeze
21
+
22
+ private_constant :STRATEGY_FREQUENCIES,
23
+ :RANDOM,
24
+ :TOP_ONE,
25
+ :TOP_TWO,
26
+ :TOP_SIX,
27
+ :MAX_HOME_ADVANTAGE
28
+
29
+ def initialize
30
+ @lotto = STRATEGY_FREQUENCIES.inject([]) do |memo, (name, frequency)|
31
+ memo + ([name] * frequency)
32
+ end.shuffle
33
+
34
+ freeze
35
+ end
36
+
37
+ def play(matchup)
38
+ scores = generate_scores
39
+ winning_score = scores.max
40
+ losing_score = scores.min
41
+ strategy = pick_strategy
42
+
43
+ if home_wins?(matchup, strategy)
44
+ Result.new(
45
+ game: matchup.game,
46
+ home_score: winning_score,
47
+ away_score: losing_score
48
+ )
49
+ else
50
+ Result.new(
51
+ game: matchup.game,
52
+ home_score: losing_score,
53
+ away_score: winning_score
54
+ )
55
+ end
56
+ end
57
+
58
+ private
59
+
60
+ attr_reader :lotto
61
+
62
+ def pick_strategy
63
+ lotto.sample
64
+ end
65
+
66
+ def home_wins?(game, strategy)
67
+ send("#{strategy}_strategy", game)
68
+ end
69
+
70
+ def top_player_sum(players, amount)
71
+ players.sort_by(&:overall).reverse.take(amount).sum(&:overall)
72
+ end
73
+
74
+ def generate_scores
75
+ scores = [
76
+ rand(70..120),
77
+ rand(70..120)
78
+ ]
79
+
80
+ # No ties
81
+ scores[0] += 1 if scores[0] == scores[1]
82
+
83
+ scores
84
+ end
85
+
86
+ def random_strategy(_game)
87
+ # 60% chance home wins
88
+ (([0] * 6) + ([1] * 4)).sample.zero?
89
+ end
90
+
91
+ def random_home_advantage
92
+ rand(0..MAX_HOME_ADVANTAGE)
93
+ end
94
+
95
+ def top_one_strategy(matchup)
96
+ top_player_sum(matchup.home_players, 1) + random_home_advantage >= top_player_sum(matchup.away_players, 1)
97
+ end
98
+
99
+ def top_two_strategy(matchup)
100
+ top_player_sum(matchup.home_players, 2) + random_home_advantage >= top_player_sum(matchup.away_players, 2)
101
+ end
102
+
103
+ def top_three_strategy(matchup)
104
+ top_player_sum(matchup.home_players, 3) + random_home_advantage >= top_player_sum(matchup.away_players, 3)
105
+ end
106
+
107
+ def top_six_strategy(matchup)
108
+ top_player_sum(matchup.home_players, 6) + random_home_advantage >= top_player_sum(matchup.away_players, 6)
109
+ end
110
+ end
111
+ end
112
+ end
@@ -0,0 +1,90 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Basketball
4
+ module Season
5
+ # Sets boundaries for preseason and regular season play. Add games as long as they are
6
+ # within the correct dated boundaries
7
+ class Calendar
8
+ class OutOfBoundsError < StandardError; end
9
+ class TeamAlreadyBookedError < StandardError; end
10
+
11
+ attr_reader :preseason_start_date,
12
+ :preseason_end_date,
13
+ :season_start_date,
14
+ :season_end_date,
15
+ :games
16
+
17
+ def initialize(
18
+ preseason_start_date:,
19
+ preseason_end_date:,
20
+ season_start_date:,
21
+ season_end_date:,
22
+ games: []
23
+ )
24
+ raise ArgumentError, 'preseason_start_date is required' if preseason_start_date.to_s.empty?
25
+ raise ArgumentError, 'preseason_end_date is required' if preseason_end_date.to_s.empty?
26
+ raise ArgumentError, 'season_start_date is required' if season_start_date.to_s.empty?
27
+ raise ArgumentError, 'season_end_date is required' if season_end_date.to_s.empty?
28
+
29
+ @preseason_start_date = preseason_start_date
30
+ @preseason_end_date = preseason_end_date
31
+ @season_start_date = season_start_date
32
+ @season_end_date = season_end_date
33
+ @games = []
34
+
35
+ games.each { |game| add!(game) }
36
+
37
+ freeze
38
+ end
39
+
40
+ def add!(game)
41
+ assert_in_bounds(game)
42
+ assert_free_date(game)
43
+
44
+ @games << game
45
+
46
+ self
47
+ end
48
+
49
+ def exhibitions_for(date: nil, opponent: nil)
50
+ games_for(date:, opponent:).select { |game| game.is_a?(Exhibition) }
51
+ end
52
+
53
+ def regulars_for(date: nil, opponent: nil)
54
+ games_for(date:, opponent:).select { |game| game.is_a?(Regular) }
55
+ end
56
+
57
+ def games_for(date: nil, opponent: nil)
58
+ games.select do |game|
59
+ (date.nil? || game.date == date) && (opponent.nil? || game.for?(opponent))
60
+ end
61
+ end
62
+
63
+ private
64
+
65
+ def assert_free_date(game)
66
+ if games_for(date: game.date, opponent: game.home_opponent).any?
67
+ raise TeamAlreadyBookedError, "#{game.home_opponent} already playing on #{game.date}"
68
+ end
69
+
70
+ return unless games_for(date: game.date, opponent: game.away_opponent).any?
71
+
72
+ raise TeamAlreadyBookedError, "#{game.away_opponent} already playing on #{game.date}"
73
+ end
74
+
75
+ def assert_in_bounds(game)
76
+ date = game.date
77
+
78
+ if game.is_a?(Exhibition)
79
+ raise OutOfBoundsError, "#{date} is before preseason begins" if date < preseason_start_date
80
+ raise OutOfBoundsError, "#{date} is after preseason ends" if date > preseason_end_date
81
+ elsif game.is_a?(Regular)
82
+ raise OutOfBoundsError, "#{date} is before season begins" if date < season_start_date
83
+ raise OutOfBoundsError, "#{date} is after season ends" if date > season_end_date
84
+ else
85
+ raise ArgumentError, "Dont know what this game type is: #{game.class.name}"
86
+ end
87
+ end
88
+ end
89
+ end
90
+ end
@@ -0,0 +1,239 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Basketball
4
+ module Season
5
+ # Main iterator-based object that knows how to manage a calendar and simulate games per day.
6
+ class Coordinator
7
+ extend Forwardable
8
+
9
+ class AlreadyPlayedGameError < StandardError; end
10
+ class GameNotCurrentError < StandardError; end
11
+ class OutOfBoundsError < StandardError; end
12
+ class PlayedGamesError < StandardError; end
13
+ class UnknownGameError < StandardError; end
14
+ class UnknownTeamError < StandardError; end
15
+ class UnplayedGamesError < StandardError; end
16
+
17
+ attr_reader :calendar,
18
+ :current_date,
19
+ :arena,
20
+ :results,
21
+ :league,
22
+ :id
23
+
24
+ def_delegators :calendar,
25
+ :preseason_start_date,
26
+ :preseason_end_date,
27
+ :season_start_date,
28
+ :season_end_date,
29
+ :games,
30
+ :exhibitions_for,
31
+ :regulars_for,
32
+ :games_for
33
+
34
+ def initialize(
35
+ calendar:,
36
+ current_date:,
37
+ results: [],
38
+ league: Org::League.new
39
+ )
40
+ super()
41
+
42
+ raise ArgumentError, 'calendar is required' unless calendar
43
+ raise ArgumentError, 'current_date is required' if current_date.to_s.empty?
44
+ raise ArgumentError, 'league is required' unless league
45
+
46
+ @calendar = calendar
47
+ @current_date = current_date
48
+ @arena = Arena.new
49
+ @results = []
50
+ @league = league
51
+
52
+ results.each { |result| replay!(result) }
53
+
54
+ assert_current_date
55
+ assert_all_past_dates_are_played
56
+ assert_all_future_dates_arent_played
57
+ assert_all_known_teams
58
+ end
59
+
60
+ def sim_rest!(&)
61
+ events = []
62
+
63
+ while not_done?
64
+ new_events = sim!(&)
65
+
66
+ events += new_events
67
+ end
68
+
69
+ events
70
+ end
71
+
72
+ def assert_current_date
73
+ if current_date < preseason_start_date
74
+ raise OutOfBoundsError, "current date #{current_date} should be on or after #{preseason_start_date}"
75
+ end
76
+
77
+ return unless current_date > season_end_date
78
+
79
+ raise OutOfBoundsError, "current date #{current_date} should be on or after #{preseason_start_date}"
80
+ end
81
+
82
+ def sim!
83
+ return [] if done?
84
+
85
+ events = []
86
+ games = games_for(date: current_date)
87
+
88
+ games.each do |game|
89
+ home_players = opponent_team(game.home_opponent).players
90
+ away_players = opponent_team(game.away_opponent).players
91
+ matchup = Matchup.new(game:, home_players:, away_players:)
92
+ event = arena.play(matchup)
93
+
94
+ play!(event)
95
+
96
+ yield(event) if block_given?
97
+
98
+ events << event
99
+ end
100
+
101
+ increment_current_date!
102
+
103
+ events
104
+ end
105
+
106
+ def total_days
107
+ (season_end_date - preseason_start_date).to_i
108
+ end
109
+
110
+ def days_left
111
+ (season_end_date - current_date).to_i
112
+ end
113
+
114
+ def total_exhibitions
115
+ exhibitions_for.length
116
+ end
117
+
118
+ def exhibitions_left
119
+ total_exhibitions - exhibition_result_events.length
120
+ end
121
+
122
+ def total_regulars
123
+ regulars_for.length
124
+ end
125
+
126
+ def regulars_left
127
+ total_regulars - regular_result_events.length
128
+ end
129
+
130
+ def current_games
131
+ games_for(date: current_date) - results.map(&:game)
132
+ end
133
+
134
+ def done?
135
+ current_date == season_end_date && games.length == results.length
136
+ end
137
+
138
+ def not_done?
139
+ !done?
140
+ end
141
+
142
+ def add!(game)
143
+ assert_today_or_in_future(game)
144
+ assert_known_teams(game)
145
+
146
+ calendar.add!(game)
147
+
148
+ self
149
+ end
150
+
151
+ def result_for(game)
152
+ results.find do |result|
153
+ result.game == game
154
+ end
155
+ end
156
+
157
+ private
158
+
159
+ attr_writer :id, :arena
160
+
161
+ def opponent_team(opponent)
162
+ league.teams.find { |t| t == opponent }
163
+ end
164
+
165
+ def exhibition_result_events
166
+ results.select { |e| e.game.is_a?(Exhibition) }
167
+ end
168
+
169
+ def regular_result_events
170
+ results.select { |e| e.game.is_a?(Regular) }
171
+ end
172
+
173
+ def increment_current_date!
174
+ return self if current_date >= season_end_date
175
+
176
+ @current_date = current_date + 1
177
+
178
+ self
179
+ end
180
+
181
+ def assert_today_or_in_future(game)
182
+ return unless game.date <= current_date
183
+
184
+ raise OutOfBoundsError, "#{game.date} is on or before the current date (#{current_date})"
185
+ end
186
+
187
+ def assert_known_teams(game)
188
+ raise UnknownTeamError, "unknown opponent: #{game.home_opponent}" if league.not_registered?(game.home_opponent)
189
+
190
+ return unless league.not_registered?(game.away_opponent)
191
+
192
+ raise UnknownTeamError, "unknown opponent: #{game.away_opponent}"
193
+ end
194
+
195
+ def assert_all_past_dates_are_played
196
+ games_that_should_be_played = games.select { |game| game.date < current_date }
197
+
198
+ games_played = results.map(&:game)
199
+ unplayed_games = games_that_should_be_played - games_played
200
+
201
+ return if unplayed_games.empty?
202
+
203
+ raise UnplayedGamesError, "#{unplayed_games.length} game(s) not played before #{current_date}"
204
+ end
205
+
206
+ def assert_all_future_dates_arent_played
207
+ games_that_shouldnt_be_played = results.select do |result|
208
+ result.date > current_date
209
+ end
210
+
211
+ count = games_that_shouldnt_be_played.length
212
+
213
+ return unless games_that_shouldnt_be_played.any?
214
+
215
+ raise PlayedGamesError, "#{count} game(s) played after #{current_date}"
216
+ end
217
+
218
+ def play!(result)
219
+ raise GameNotCurrentError, "#{result} is not for #{current_date}" if result.date != current_date
220
+
221
+ replay!(result)
222
+ end
223
+
224
+ def replay!(result)
225
+ raise AlreadyPlayedGameError, "#{result.game} already played!" if result_for(result.game)
226
+
227
+ raise UnknownGameError, "game not added: #{result.game}" unless games.include?(result.game)
228
+
229
+ results << result
230
+
231
+ result
232
+ end
233
+
234
+ def assert_all_known_teams
235
+ calendar.games.each { |game| assert_known_teams(game) }
236
+ end
237
+ end
238
+ end
239
+ end
@@ -1,8 +1,9 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Basketball
4
- module Scheduling
5
- class PreseasonGame < Game
4
+ module Season
5
+ # An exhibition game.
6
+ class Exhibition < Game
6
7
  def to_s
7
8
  "#{super} (preseason)"
8
9
  end
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Basketball
4
+ module Season
5
+ # Base class describing what all games have in common.
6
+ class Game < ValueObject
7
+ value_reader :date, :home_opponent, :away_opponent
8
+
9
+ def initialize(date:, home_opponent:, away_opponent:)
10
+ super()
11
+
12
+ raise ArgumentError, 'date is required' unless date
13
+ raise ArgumentError, 'home_opponent is required' unless home_opponent
14
+ raise ArgumentError, 'away_opponent is required' unless away_opponent
15
+ raise ArgumentError, 'teams cannot play themselves' if home_opponent == away_opponent
16
+
17
+ @date = date
18
+ @home_opponent = home_opponent
19
+ @away_opponent = away_opponent
20
+
21
+ freeze
22
+ end
23
+
24
+ def for?(team)
25
+ teams.include?(team)
26
+ end
27
+
28
+ def teams
29
+ [home_opponent, away_opponent]
30
+ end
31
+
32
+ def to_s
33
+ "#{date} - #{away_opponent} at #{home_opponent}"
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Basketball
4
+ module Season
5
+ # A Matchup is a late materialization of a game. While a game is a skeleton for a future
6
+ # matchup, it does not materialize until game time (rosters could change right up until game time).
7
+ class Matchup
8
+ class PlayersOnBothTeamsError < StandardError; end
9
+
10
+ attr_reader :game, :home_players, :away_players
11
+
12
+ def initialize(game:, home_players: [], away_players: [])
13
+ raise ArgumentError, 'game is required' unless game
14
+
15
+ @game = game
16
+ @home_players = home_players.uniq
17
+ @away_players = away_players.uniq
18
+
19
+ if home_players.intersect?(away_players)
20
+ raise PlayersOnBothTeamsError, 'players cannot be on both home and away team'
21
+ end
22
+
23
+ freeze
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Basketball
4
+ module Season
5
+ # Represents a team without a roster. Equal to a team by identity.
6
+ # A team's roster will not be known until the last minute (when it is game time).
7
+ class Opponent < Entity
8
+ def initialize(id:)
9
+ super(id)
10
+
11
+ freeze
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Basketball
4
+ module Season
5
+ # A regular season game.
6
+ class Regular < Game
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Basketball
4
+ module Season
5
+ # Base class describing the end result of a game. This should/could be sub-classed to include
6
+ # more sport-specific information.
7
+ class Result
8
+ extend Forwardable
9
+
10
+ class CannotTieError < StandardError; end
11
+
12
+ attr_reader :game, :home_score, :away_score
13
+
14
+ def_delegators :game, :date, :home_opponent, :away_opponent, :teams
15
+
16
+ def initialize(game:, home_score:, away_score:)
17
+ raise ArgumentError, 'game is required' unless game
18
+ raise CannotTieError, "#{game} ended in a tie" if home_score == away_score
19
+
20
+ @game = game
21
+ @home_score = home_score.to_i
22
+ @away_score = away_score.to_i
23
+ end
24
+
25
+ def to_s
26
+ "#{game.date} - #{away_opponent} (#{away_score}) at #{home_opponent} (#{home_score})"
27
+ end
28
+
29
+ def ==(other)
30
+ game == other.game &&
31
+ home_score == other.home_score &&
32
+ away_score == other.away_score
33
+ end
34
+ alias eql? ==
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Common
4
+ require_relative 'season/arena'
5
+ require_relative 'season/calendar'
6
+ require_relative 'season/result'
7
+ require_relative 'season/game'
8
+ require_relative 'season/matchup'
9
+ require_relative 'season/opponent'
10
+
11
+ # Game Subclasses
12
+ require_relative 'season/exhibition'
13
+ require_relative 'season/regular'
14
+
15
+ # Specific
16
+ require_relative 'season/coordinator'
@@ -1,6 +1,9 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Basketball
4
+ # A Value Object is something that has no specific identity, instead its identity is the sum of
5
+ # the attribute values. Changing one will change the entire object's identity.
6
+ # Comes with a very simple DSL for specifying properties along with base equality and sorting methods.
4
7
  class ValueObject
5
8
  include Comparable
6
9
 
@@ -23,7 +26,7 @@ module Basketball
23
26
  @value_keys ||= []
24
27
  end
25
28
 
26
- def attr_reader_value(*keys)
29
+ def value_reader(*keys)
27
30
  keys.each { |k| value_keys << k.to_sym }
28
31
 
29
32
  attr_reader(*keys)
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Basketball
4
- VERSION = '0.0.7'
4
+ VERSION = '0.0.9'
5
5
  end
data/lib/basketball.rb CHANGED
@@ -1,16 +1,21 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'faker'
3
+ require 'date'
4
4
  require 'fileutils'
5
5
  require 'forwardable'
6
6
  require 'json'
7
- require 'securerandom'
8
7
  require 'slop'
9
8
 
10
- # Top-level
9
+ # Generic
11
10
  require_relative 'basketball/entity'
12
11
  require_relative 'basketball/value_object'
13
12
 
14
- # Submodules
15
- require_relative 'basketball/drafting'
16
- require_relative 'basketball/scheduling'
13
+ # Dependent on Generic
14
+ require_relative 'basketball/org'
15
+
16
+ # Dependent on Org
17
+ require_relative 'basketball/draft'
18
+ require_relative 'basketball/season'
19
+
20
+ # Dependent on All
21
+ require_relative 'basketball/app'