basketball 0.0.8 → 0.0.9

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