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