basketball 0.0.8 → 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 (54) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +9 -19
  3. data/CHANGELOG.md +1 -39
  4. data/README.md +72 -93
  5. data/basketball.gemspec +3 -6
  6. data/exe/{basketball-season-scheduling → 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/{draft/cli.rb → app/room_cli.rb} +53 -76
  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/draft/event.rb +3 -2
  16. data/lib/basketball/draft/front_office.rb +35 -28
  17. data/lib/basketball/draft/{pick_event.rb → pick.rb} +13 -6
  18. data/lib/basketball/draft/room.rb +119 -119
  19. data/lib/basketball/draft/{player_search.rb → scout.rb} +4 -9
  20. data/lib/basketball/draft/skip.rb +12 -0
  21. data/lib/basketball/draft.rb +13 -6
  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/{draft → 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 +41 -72
  30. data/lib/basketball/season/coordinator.rb +185 -126
  31. data/lib/basketball/season/{preseason_game.rb → exhibition.rb} +2 -1
  32. data/lib/basketball/season/game.rb +15 -10
  33. data/lib/basketball/season/matchup.rb +27 -0
  34. data/lib/basketball/season/opponent.rb +15 -0
  35. data/lib/basketball/season/{season_game.rb → regular.rb} +2 -1
  36. data/lib/basketball/season/result.rb +37 -0
  37. data/lib/basketball/season.rb +12 -13
  38. data/lib/basketball/value_object.rb +4 -1
  39. data/lib/basketball/version.rb +1 -1
  40. data/lib/basketball.rb +9 -4
  41. metadata +32 -44
  42. data/lib/basketball/draft/league.rb +0 -70
  43. data/lib/basketball/draft/player.rb +0 -43
  44. data/lib/basketball/draft/room_serializer.rb +0 -186
  45. data/lib/basketball/draft/roster.rb +0 -37
  46. data/lib/basketball/draft/sim_event.rb +0 -23
  47. data/lib/basketball/draft/skip_event.rb +0 -13
  48. data/lib/basketball/season/calendar_serializer.rb +0 -94
  49. data/lib/basketball/season/conference.rb +0 -57
  50. data/lib/basketball/season/division.rb +0 -43
  51. data/lib/basketball/season/league.rb +0 -114
  52. data/lib/basketball/season/league_serializer.rb +0 -99
  53. data/lib/basketball/season/scheduling_cli.rb +0 -198
  54. data/lib/basketball/season/team.rb +0 -21
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Basketball
4
+ module Org
5
+ # Base class describing a player.
6
+ # A consumer application should extend these specific to their specific sports traits.
7
+ class Player < Entity
8
+ attr_reader :overall, :position
9
+
10
+ def initialize(id:, overall: 0, position: nil)
11
+ super(id)
12
+
13
+ raise ArgumentError, 'position is required' unless position
14
+
15
+ @overall = overall
16
+ @position = position
17
+
18
+ freeze
19
+ end
20
+
21
+ def to_s
22
+ "[#{super}] (#{position}) #{overall}".strip
23
+ end
24
+ end
25
+ end
26
+ end
@@ -1,7 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Basketball
4
- module Draft
4
+ module Org
5
+ # Describes a player position.
5
6
  class Position < ValueObject
6
7
  extend Forwardable
7
8
 
@@ -17,7 +18,7 @@ module Basketball
17
18
  FRONT_COURT_VALUES = %w[PF C].to_set.freeze
18
19
  ALL_VALUES = (BACK_COURT_VALUES.to_a + FRONT_COURT_VALUES.to_a).to_set.freeze
19
20
 
20
- attr_reader_value :code
21
+ value_reader :code
21
22
 
22
23
  def_delegators :code, :to_s
23
24
 
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Basketball
4
+ module Org
5
+ # Base class describing a team. A team here is bare metal and is just described by an ID
6
+ # and a collection of Player objects.
7
+ class Team < Entity
8
+ attr_reader :players
9
+
10
+ def initialize(id:, players: [])
11
+ super(id)
12
+
13
+ @players = []
14
+
15
+ players.each { |p| sign!(p) }
16
+
17
+ freeze
18
+ end
19
+
20
+ def to_s
21
+ ([super.to_s] + players.map(&:to_s)).join("\n")
22
+ end
23
+
24
+ def signed?(player)
25
+ players.include?(player)
26
+ end
27
+
28
+ def sign!(player)
29
+ raise ArgumentError, 'player is required' unless player
30
+ raise PlayerAlreadySignedError, "#{player} already signed by #{self}" if signed?(player)
31
+
32
+ players << player
33
+
34
+ self
35
+ end
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'org/player'
4
+ require_relative 'org/position'
5
+ require_relative 'org/team'
6
+ require_relative 'org/league'
7
+
8
+ module Basketball
9
+ module Org
10
+ class PlayerAlreadySignedError < StandardError; end
11
+ end
12
+ end
@@ -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
@@ -2,28 +2,34 @@
2
2
 
3
3
  module Basketball
4
4
  module Season
5
- class Calendar < ValueObject
6
- class TeamAlreadyBookedError < StandardError; end
7
- class InvalidGameOrderError < StandardError; end
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
8
  class OutOfBoundsError < StandardError; end
9
+ class TeamAlreadyBookedError < StandardError; end
9
10
 
10
11
  attr_reader :preseason_start_date,
11
12
  :preseason_end_date,
12
13
  :season_start_date,
13
- :season_end_date
14
-
15
- attr_reader_value :year, :games
16
-
17
- def initialize(year:, games: [])
18
- super()
19
-
20
- raise ArgumentError, 'year is required' unless year
21
-
22
- @year = year
23
- @preseason_start_date = Date.new(year, 9, 30)
24
- @preseason_end_date = Date.new(year, 10, 14)
25
- @season_start_date = Date.new(year, 10, 18)
26
- @season_end_date = Date.new(year + 1, 4, 29)
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
27
33
  @games = []
28
34
 
29
35
  games.each { |game| add!(game) }
@@ -40,78 +46,41 @@ module Basketball
40
46
  self
41
47
  end
42
48
 
43
- def preseason_games_for(date: nil, team: nil)
44
- games_for(date:, team:).select { |game| game.is_a?(PreseasonGame) }
49
+ def exhibitions_for(date: nil, opponent: nil)
50
+ games_for(date:, opponent:).select { |game| game.is_a?(Exhibition) }
45
51
  end
46
52
 
47
- def season_games_for(date: nil, team: nil)
48
- games_for(date:, team:).select { |game| game.is_a?(SeasonGame) }
53
+ def regulars_for(date: nil, opponent: nil)
54
+ games_for(date:, opponent:).select { |game| game.is_a?(Regular) }
49
55
  end
50
56
 
51
- def games_for(date: nil, team: nil)
57
+ def games_for(date: nil, opponent: nil)
52
58
  games.select do |game|
53
- (date.nil? || game.date == date) &&
54
- (team.nil? || (game.home_team == team || game.away_team == team))
59
+ (date.nil? || game.date == date) && (opponent.nil? || game.for?(opponent))
55
60
  end
56
61
  end
57
62
 
58
- def available_preseason_dates_for(team)
59
- all_preseason_dates - preseason_games_for(team:).map(&:date)
60
- end
61
-
62
- def available_season_dates_for(team)
63
- all_season_dates - season_games_for(team:).map(&:date)
64
- end
65
-
66
- def available_preseason_matchup_dates(team1, team2)
67
- available_team_dates = available_preseason_dates_for(team1)
68
- available_other_team_dates = available_preseason_dates_for(team2)
69
-
70
- available_team_dates & available_other_team_dates
71
- end
72
-
73
- def available_season_matchup_dates(team1, team2)
74
- available_team_dates = available_season_dates_for(team1)
75
- available_other_team_dates = available_season_dates_for(team2)
76
-
77
- available_team_dates & available_other_team_dates
78
- end
79
-
80
- def teams
81
- games.flat_map(&:teams)
82
- end
83
-
84
- def team(id)
85
- teams.find { |t| t == Team.new(id:) }
86
- end
87
-
88
63
  private
89
64
 
90
- def all_preseason_dates
91
- (preseason_start_date..preseason_end_date).to_a
92
- end
93
-
94
- def all_season_dates
95
- (season_start_date..season_end_date).to_a
96
- end
97
-
98
65
  def assert_free_date(game)
99
- if games_for(date: game.date, team: game.home_team).any?
100
- raise TeamAlreadyBookedError, "#{game.home_team} already playing on #{game.date}"
66
+ if games_for(date: game.date, opponent: game.home_opponent).any?
67
+ raise TeamAlreadyBookedError, "#{game.home_opponent} already playing on #{game.date}"
101
68
  end
102
69
 
103
- return unless games_for(date: game.date, team: game.away_team).any?
70
+ return unless games_for(date: game.date, opponent: game.away_opponent).any?
104
71
 
105
- raise TeamAlreadyBookedError, "#{game.away_team} already playing on #{game.date}"
72
+ raise TeamAlreadyBookedError, "#{game.away_opponent} already playing on #{game.date}"
106
73
  end
107
74
 
108
75
  def assert_in_bounds(game)
109
- if game.is_a?(PreseasonGame)
110
- raise OutOfBoundsError, "#{game.date} is before preseason begins" if game.date < preseason_start_date
111
- raise OutOfBoundsError, "#{game.date} is after preseason ends" if game.date > preseason_end_date
112
- elsif game.is_a?(SeasonGame)
113
- raise OutOfBoundsError, "#{game.date} is before season begins" if game.date < season_start_date
114
- raise OutOfBoundsError, "#{game.date} is after season ends" if game.date > season_end_date
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
115
84
  else
116
85
  raise ArgumentError, "Dont know what this game type is: #{game.class.name}"
117
86
  end