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.
- checksums.yaml +4 -4
- data/.rubocop.yml +9 -19
- data/CHANGELOG.md +1 -39
- data/README.md +72 -93
- data/basketball.gemspec +3 -6
- data/exe/{basketball-season-scheduling → 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/{draft/cli.rb → app/room_cli.rb} +53 -76
- 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/draft/event.rb +3 -2
- data/lib/basketball/draft/front_office.rb +35 -28
- data/lib/basketball/draft/{pick_event.rb → pick.rb} +13 -6
- data/lib/basketball/draft/room.rb +119 -119
- data/lib/basketball/draft/{player_search.rb → scout.rb} +4 -9
- data/lib/basketball/draft/skip.rb +12 -0
- data/lib/basketball/draft.rb +13 -6
- 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/{draft → 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 +41 -72
- data/lib/basketball/season/coordinator.rb +185 -126
- data/lib/basketball/season/{preseason_game.rb → exhibition.rb} +2 -1
- data/lib/basketball/season/game.rb +15 -10
- data/lib/basketball/season/matchup.rb +27 -0
- data/lib/basketball/season/opponent.rb +15 -0
- data/lib/basketball/season/{season_game.rb → regular.rb} +2 -1
- data/lib/basketball/season/result.rb +37 -0
- data/lib/basketball/season.rb +12 -13
- data/lib/basketball/value_object.rb +4 -1
- data/lib/basketball/version.rb +1 -1
- data/lib/basketball.rb +9 -4
- metadata +32 -44
- data/lib/basketball/draft/league.rb +0 -70
- data/lib/basketball/draft/player.rb +0 -43
- data/lib/basketball/draft/room_serializer.rb +0 -186
- data/lib/basketball/draft/roster.rb +0 -37
- data/lib/basketball/draft/sim_event.rb +0 -23
- data/lib/basketball/draft/skip_event.rb +0 -13
- data/lib/basketball/season/calendar_serializer.rb +0 -94
- data/lib/basketball/season/conference.rb +0 -57
- data/lib/basketball/season/division.rb +0 -43
- data/lib/basketball/season/league.rb +0 -114
- data/lib/basketball/season/league_serializer.rb +0 -99
- data/lib/basketball/season/scheduling_cli.rb +0 -198
- 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
|
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
|
-
|
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
|
-
|
6
|
-
|
7
|
-
|
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
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
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
|
44
|
-
games_for(date:,
|
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
|
48
|
-
games_for(date:,
|
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,
|
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,
|
100
|
-
raise TeamAlreadyBookedError, "#{game.
|
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,
|
70
|
+
return unless games_for(date: game.date, opponent: game.away_opponent).any?
|
104
71
|
|
105
|
-
raise TeamAlreadyBookedError, "#{game.
|
72
|
+
raise TeamAlreadyBookedError, "#{game.away_opponent} already playing on #{game.date}"
|
106
73
|
end
|
107
74
|
|
108
75
|
def assert_in_bounds(game)
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
raise OutOfBoundsError, "#{
|
114
|
-
|
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
|