basketball 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.editorconfig +8 -0
- data/.gitignore +8 -0
- data/.rubocop.yml +41 -0
- data/.tool-versions +1 -0
- data/CHANGELOG.md +3 -0
- data/CODE_OF_CONDUCT.md +73 -0
- data/Gemfile +5 -0
- data/Guardfile +16 -0
- data/LICENSE +5 -0
- data/README.md +144 -0
- data/Rakefile +10 -0
- data/basketball.gemspec +47 -0
- data/exe/basketball-draft +7 -0
- data/lib/basketball/drafting/cli.rb +203 -0
- data/lib/basketball/drafting/engine.rb +186 -0
- data/lib/basketball/drafting/engine_serializer.rb +170 -0
- data/lib/basketball/drafting/entity.rb +33 -0
- data/lib/basketball/drafting/event.rb +24 -0
- data/lib/basketball/drafting/front_office.rb +93 -0
- data/lib/basketball/drafting/pick_event.rb +23 -0
- data/lib/basketball/drafting/player.rb +43 -0
- data/lib/basketball/drafting/player_search.rb +31 -0
- data/lib/basketball/drafting/position.rb +42 -0
- data/lib/basketball/drafting/roster.rb +37 -0
- data/lib/basketball/drafting/sim_event.rb +23 -0
- data/lib/basketball/drafting/team.rb +28 -0
- data/lib/basketball/drafting.rb +19 -0
- data/lib/basketball/version.rb +5 -0
- data/lib/basketball.rb +12 -0
- metadata +250 -0
@@ -0,0 +1,186 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Basketball
|
4
|
+
module Drafting
|
5
|
+
class Engine
|
6
|
+
class AlreadyPickedError < StandardError; end
|
7
|
+
class DupeEventError < StandardError; end
|
8
|
+
class EventOutOfOrderError < StandardError; end
|
9
|
+
class UnknownPlayerError < StandardError; end
|
10
|
+
class UnknownTeamError < StandardError; end
|
11
|
+
class EndOfDraftError < StandardError; end
|
12
|
+
|
13
|
+
DEFAULT_ROUNDS = 12
|
14
|
+
|
15
|
+
private_constant :DEFAULT_ROUNDS
|
16
|
+
|
17
|
+
attr_reader :events, :rounds
|
18
|
+
|
19
|
+
def initialize(players: [], teams: [], events: [], rounds: DEFAULT_ROUNDS)
|
20
|
+
@players_by_id = players.to_h { |p| [p.id, p] }
|
21
|
+
@teams_by_id = teams.to_h { |t| [t.id, t] }
|
22
|
+
@events = []
|
23
|
+
@rounds = rounds.to_i
|
24
|
+
|
25
|
+
# Each one will be validated for correctness.
|
26
|
+
events.each { |e| play!(e) }
|
27
|
+
|
28
|
+
freeze
|
29
|
+
end
|
30
|
+
|
31
|
+
def rosters
|
32
|
+
events_by_team = teams.to_h { |t| [t, []] }
|
33
|
+
|
34
|
+
events.each do |event|
|
35
|
+
events_by_team.fetch(event.team) << event
|
36
|
+
end
|
37
|
+
|
38
|
+
events_by_team.map do |team, events|
|
39
|
+
Roster.new(team:, events:)
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
def to_s
|
44
|
+
events.join("\n")
|
45
|
+
end
|
46
|
+
|
47
|
+
def teams
|
48
|
+
teams_by_id.values
|
49
|
+
end
|
50
|
+
|
51
|
+
def players
|
52
|
+
players_by_id.values
|
53
|
+
end
|
54
|
+
|
55
|
+
def total_picks
|
56
|
+
rounds * teams.length
|
57
|
+
end
|
58
|
+
|
59
|
+
def current_round
|
60
|
+
return if done?
|
61
|
+
|
62
|
+
(current_pick / teams.length.to_f).ceil
|
63
|
+
end
|
64
|
+
|
65
|
+
def current_round_pick
|
66
|
+
return if done?
|
67
|
+
|
68
|
+
mod = current_pick % teams.length
|
69
|
+
|
70
|
+
mod.positive? ? mod : teams.length
|
71
|
+
end
|
72
|
+
|
73
|
+
def current_team
|
74
|
+
return if done?
|
75
|
+
|
76
|
+
teams[current_round_pick - 1]
|
77
|
+
end
|
78
|
+
|
79
|
+
def current_pick
|
80
|
+
return if done?
|
81
|
+
|
82
|
+
internal_current_pick
|
83
|
+
end
|
84
|
+
|
85
|
+
def remaining_picks
|
86
|
+
total_picks - internal_current_pick + 1
|
87
|
+
end
|
88
|
+
|
89
|
+
def done?
|
90
|
+
internal_current_pick > total_picks
|
91
|
+
end
|
92
|
+
|
93
|
+
def not_done?
|
94
|
+
!done?
|
95
|
+
end
|
96
|
+
|
97
|
+
def sim!(times = nil)
|
98
|
+
counter = 0
|
99
|
+
|
100
|
+
until done? || (times && counter >= times)
|
101
|
+
team = current_team
|
102
|
+
|
103
|
+
player = team.pick(
|
104
|
+
undrafted_players:,
|
105
|
+
drafted_players: drafted_players(team),
|
106
|
+
round: current_round
|
107
|
+
)
|
108
|
+
|
109
|
+
event = SimEvent.new(
|
110
|
+
id: SecureRandom.uuid,
|
111
|
+
team:,
|
112
|
+
player:,
|
113
|
+
pick: current_pick,
|
114
|
+
round: current_round,
|
115
|
+
round_pick: current_round_pick
|
116
|
+
)
|
117
|
+
|
118
|
+
play!(event)
|
119
|
+
|
120
|
+
yield(event) if block_given?
|
121
|
+
|
122
|
+
counter += 1
|
123
|
+
end
|
124
|
+
|
125
|
+
self
|
126
|
+
end
|
127
|
+
|
128
|
+
def pick!(player)
|
129
|
+
return nil if done?
|
130
|
+
|
131
|
+
event = PickEvent.new(
|
132
|
+
id: SecureRandom.uuid,
|
133
|
+
team: current_team,
|
134
|
+
player:,
|
135
|
+
pick: current_pick,
|
136
|
+
round: current_round,
|
137
|
+
round_pick: current_round_pick
|
138
|
+
)
|
139
|
+
|
140
|
+
play!(event)
|
141
|
+
end
|
142
|
+
|
143
|
+
def undrafted_players
|
144
|
+
players - drafted_players
|
145
|
+
end
|
146
|
+
|
147
|
+
private
|
148
|
+
|
149
|
+
attr_reader :players_by_id, :teams_by_id
|
150
|
+
|
151
|
+
def internal_current_pick
|
152
|
+
events.length + 1
|
153
|
+
end
|
154
|
+
|
155
|
+
def drafted_players(team = nil)
|
156
|
+
events.each_with_object([]) do |e, memo|
|
157
|
+
next unless team.nil? || e.team == team
|
158
|
+
|
159
|
+
memo << e.player
|
160
|
+
end
|
161
|
+
end
|
162
|
+
|
163
|
+
# rubocop:disable Metrics/AbcSize
|
164
|
+
# rubocop:disable Metrics/CyclomaticComplexity
|
165
|
+
# rubocop:disable Metrics/PerceivedComplexity
|
166
|
+
def play!(event)
|
167
|
+
raise AlreadyPickedError, "#{player} was already picked" if drafted_players.include?(event.player)
|
168
|
+
raise DupeEventError, "#{event} is a dupe" if events.include?(event)
|
169
|
+
raise EventOutOfOrder, "#{event} team cant pick right now" if event.team != current_team
|
170
|
+
raise EventOutOfOrder, "#{event} has wrong pick" if event.pick != current_pick
|
171
|
+
raise EventOutOfOrder, "#{event} has wrong round" if event.round != current_round
|
172
|
+
raise EventOutOfOrder, "#{event} has wrong round_pick" if event.round_pick != current_round_pick
|
173
|
+
raise UnknownTeamError, "#{team} doesnt exist" unless teams.include?(event.team)
|
174
|
+
raise UnknownPlayerError, "#{player} doesnt exist" unless players.include?(event.player)
|
175
|
+
raise EndOfDraftError, "#{total_picks} pick limit reached" if events.length > total_picks + 1
|
176
|
+
|
177
|
+
events << event
|
178
|
+
|
179
|
+
event
|
180
|
+
end
|
181
|
+
# rubocop:enable Metrics/AbcSize
|
182
|
+
# rubocop:enable Metrics/CyclomaticComplexity
|
183
|
+
# rubocop:enable Metrics/PerceivedComplexity
|
184
|
+
end
|
185
|
+
end
|
186
|
+
end
|
@@ -0,0 +1,170 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Basketball
|
4
|
+
module Drafting
|
5
|
+
class EngineSerializer
|
6
|
+
EVENT_CLASSES = {
|
7
|
+
'PickEvent' => PickEvent,
|
8
|
+
'SimEvent' => SimEvent
|
9
|
+
}.freeze
|
10
|
+
|
11
|
+
private_constant :EVENT_CLASSES
|
12
|
+
|
13
|
+
def deserialize(string)
|
14
|
+
json = JSON.parse(string, symbolize_names: true)
|
15
|
+
teams = deserialize_teams(json)
|
16
|
+
players = deserialize_players(json)
|
17
|
+
events = deserialize_events(json, players, teams)
|
18
|
+
|
19
|
+
engine_opts = {
|
20
|
+
players:,
|
21
|
+
teams:,
|
22
|
+
events:
|
23
|
+
}
|
24
|
+
|
25
|
+
engine_opts[:rounds] = json.dig(:engine, :rounds) if json.dig(:engine, :rounds)
|
26
|
+
|
27
|
+
Engine.new(**engine_opts)
|
28
|
+
end
|
29
|
+
|
30
|
+
def serialize(engine)
|
31
|
+
{
|
32
|
+
info: serialize_info(engine),
|
33
|
+
engine: serialize_engine(engine),
|
34
|
+
rosters: serialize_rosters(engine)
|
35
|
+
}.to_json
|
36
|
+
end
|
37
|
+
|
38
|
+
private
|
39
|
+
|
40
|
+
def serialize_engine(engine)
|
41
|
+
{
|
42
|
+
rounds: engine.rounds,
|
43
|
+
teams: serialize_teams(engine),
|
44
|
+
players: serialize_players(engine),
|
45
|
+
events: serialize_events(engine.events)
|
46
|
+
}
|
47
|
+
end
|
48
|
+
|
49
|
+
def serialize_info(engine)
|
50
|
+
{
|
51
|
+
total_picks: engine.total_picks,
|
52
|
+
current_round: engine.current_round,
|
53
|
+
current_round_pick: engine.current_round_pick,
|
54
|
+
current_team: engine.current_team&.id,
|
55
|
+
current_pick: engine.current_pick,
|
56
|
+
remaining_picks: engine.remaining_picks,
|
57
|
+
done: engine.done?,
|
58
|
+
undrafted_players: engine.undrafted_players.map(&:id)
|
59
|
+
}
|
60
|
+
end
|
61
|
+
|
62
|
+
def serialize_rosters(engine)
|
63
|
+
engine.rosters.to_h do |roster|
|
64
|
+
[
|
65
|
+
roster.id,
|
66
|
+
{
|
67
|
+
events: roster.events.map(&:id),
|
68
|
+
players: roster.events.map { |event| event.player.id }
|
69
|
+
}
|
70
|
+
]
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
def serialize_teams(engine)
|
75
|
+
engine.teams.to_h do |team|
|
76
|
+
[
|
77
|
+
team.id,
|
78
|
+
{
|
79
|
+
name: team.name,
|
80
|
+
front_office: {
|
81
|
+
fuzz: team.front_office.fuzz,
|
82
|
+
depth: team.front_office.depth,
|
83
|
+
prioritized_positions: team.front_office.prioritized_positions
|
84
|
+
}
|
85
|
+
}
|
86
|
+
]
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
def serialize_players(engine)
|
91
|
+
engine.players.to_h do |player|
|
92
|
+
[
|
93
|
+
player.id,
|
94
|
+
{
|
95
|
+
first_name: player.first_name,
|
96
|
+
last_name: player.last_name,
|
97
|
+
overall: player.overall,
|
98
|
+
position: player.position.value
|
99
|
+
}
|
100
|
+
]
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
def serialize_events(events)
|
105
|
+
events.map do |event|
|
106
|
+
{
|
107
|
+
type: event.class.name.split('::').last,
|
108
|
+
id: event.id,
|
109
|
+
player: event.player.id,
|
110
|
+
team: event.team.id,
|
111
|
+
pick: event.pick,
|
112
|
+
round: event.round,
|
113
|
+
round_pick: event.round_pick
|
114
|
+
}
|
115
|
+
end
|
116
|
+
end
|
117
|
+
|
118
|
+
def deserialize_teams(json)
|
119
|
+
(json.dig(:engine, :teams) || []).map do |id, team_hash|
|
120
|
+
team_opts = {
|
121
|
+
id:,
|
122
|
+
name: team_hash[:name]
|
123
|
+
}
|
124
|
+
|
125
|
+
if team_hash.key?(:front_office)
|
126
|
+
front_office_hash = team_hash[:front_office] || {}
|
127
|
+
|
128
|
+
prioritized_positions = (front_office_hash[:prioritized_positions] || []).map do |v|
|
129
|
+
Position.new(v)
|
130
|
+
end
|
131
|
+
|
132
|
+
front_office_opts = {
|
133
|
+
prioritized_positions:,
|
134
|
+
fuzz: front_office_hash[:fuzz],
|
135
|
+
depth: front_office_hash[:depth]
|
136
|
+
}
|
137
|
+
|
138
|
+
team_opts[:front_office] = FrontOffice.new(**front_office_opts)
|
139
|
+
end
|
140
|
+
|
141
|
+
Team.new(**team_opts)
|
142
|
+
end
|
143
|
+
end
|
144
|
+
|
145
|
+
def deserialize_players(json)
|
146
|
+
(json.dig(:engine, :players) || []).map do |id, player_hash|
|
147
|
+
player_opts = player_hash.merge(
|
148
|
+
id:,
|
149
|
+
position: Position.new(player_hash[:position])
|
150
|
+
)
|
151
|
+
|
152
|
+
Player.new(**player_opts)
|
153
|
+
end
|
154
|
+
end
|
155
|
+
|
156
|
+
def deserialize_events(json, players, teams)
|
157
|
+
(json.dig(:engine, :events) || []).map do |event_hash|
|
158
|
+
event_opts = event_hash.slice(:id, :pick, :round, :round_pick).merge(
|
159
|
+
player: players.find { |p| p.id == event_hash[:player] },
|
160
|
+
team: teams.find { |t| t.id == event_hash[:team] }
|
161
|
+
)
|
162
|
+
|
163
|
+
class_constant = EVENT_CLASSES.fetch(event_hash[:type])
|
164
|
+
|
165
|
+
class_constant.new(**event_opts)
|
166
|
+
end
|
167
|
+
end
|
168
|
+
end
|
169
|
+
end
|
170
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Basketball
|
4
|
+
module Drafting
|
5
|
+
class Entity
|
6
|
+
extend Forwardable
|
7
|
+
include Comparable
|
8
|
+
|
9
|
+
attr_reader :id
|
10
|
+
|
11
|
+
def_delegators :id, :to_s
|
12
|
+
|
13
|
+
def initialize(id)
|
14
|
+
raise ArgumentError, 'id is required' if id.to_s.empty?
|
15
|
+
|
16
|
+
@id = id.to_s.upcase
|
17
|
+
end
|
18
|
+
|
19
|
+
def <=>(other)
|
20
|
+
id <=> other.id
|
21
|
+
end
|
22
|
+
|
23
|
+
def ==(other)
|
24
|
+
id == other.id
|
25
|
+
end
|
26
|
+
alias eql? ==
|
27
|
+
|
28
|
+
def hash
|
29
|
+
id.hash
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Basketball
|
4
|
+
module Drafting
|
5
|
+
class Event < Entity
|
6
|
+
attr_reader :pick, :round, :round_pick, :team
|
7
|
+
|
8
|
+
def initialize(id:, team:, pick:, round:, round_pick:)
|
9
|
+
super(id)
|
10
|
+
|
11
|
+
raise ArgumentError, 'team required' unless team
|
12
|
+
|
13
|
+
@team = team
|
14
|
+
@pick = pick.to_i
|
15
|
+
@round = round.to_i
|
16
|
+
@round_pick = round_pick.to_i
|
17
|
+
end
|
18
|
+
|
19
|
+
def to_s
|
20
|
+
"##{pick} overall (R#{round}:P#{round_pick}) by #{team}"
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,93 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Basketball
|
4
|
+
module Drafting
|
5
|
+
class FrontOffice
|
6
|
+
MAX_DEPTH = 3
|
7
|
+
MAX_FUZZ = 2
|
8
|
+
MAX_POSITIONS = 12
|
9
|
+
|
10
|
+
private_constant :MAX_DEPTH, :MAX_FUZZ, :MAX_POSITIONS
|
11
|
+
|
12
|
+
attr_reader :prioritized_positions, :fuzz, :depth
|
13
|
+
|
14
|
+
def initialize(prioritized_positions: [], fuzz: rand(0..MAX_FUZZ), depth: rand(0..MAX_DEPTH))
|
15
|
+
@fuzz = fuzz.to_i
|
16
|
+
@depth = depth.to_i
|
17
|
+
@prioritized_positions = prioritized_positions
|
18
|
+
|
19
|
+
# fill in the rest of the queue here
|
20
|
+
need_count = MAX_POSITIONS - @prioritized_positions.length
|
21
|
+
|
22
|
+
@prioritized_positions += random_positions_queue[0...need_count]
|
23
|
+
|
24
|
+
freeze
|
25
|
+
end
|
26
|
+
|
27
|
+
def to_s
|
28
|
+
"#{prioritized_positions.map(&:to_s).join(',')} (F:#{fuzz} D:#{depth})"
|
29
|
+
end
|
30
|
+
|
31
|
+
def pick(undrafted_players:, drafted_players:, round:)
|
32
|
+
players = []
|
33
|
+
|
34
|
+
players = adaptive_search(undrafted_players:, drafted_players:) if depth >= round
|
35
|
+
players = balanced_search(undrafted_players:, drafted_players:) if players.empty?
|
36
|
+
players = top_players(undrafted_players:) if players.empty?
|
37
|
+
|
38
|
+
players[0..fuzz].sample
|
39
|
+
end
|
40
|
+
|
41
|
+
private
|
42
|
+
|
43
|
+
def adaptive_search(undrafted_players:, drafted_players:)
|
44
|
+
search = PlayerSearch.new(undrafted_players)
|
45
|
+
|
46
|
+
drafted_positions = drafted_players.map(&:position)
|
47
|
+
|
48
|
+
search.query(exclude_positions: drafted_positions)
|
49
|
+
end
|
50
|
+
|
51
|
+
def balanced_search(undrafted_players:, drafted_players:)
|
52
|
+
search = PlayerSearch.new(undrafted_players)
|
53
|
+
|
54
|
+
players = []
|
55
|
+
|
56
|
+
# Try to find best pick for exact desired position.
|
57
|
+
# If you cant find one, then move to the next desired position until the end of the queue
|
58
|
+
available_prioritized_positions(drafted_players:).each do |position|
|
59
|
+
players = search.query(position:)
|
60
|
+
|
61
|
+
break if players.any?
|
62
|
+
end
|
63
|
+
|
64
|
+
players = players.any? ? players : search.query
|
65
|
+
end
|
66
|
+
|
67
|
+
def all_random_positions
|
68
|
+
Position::ALL_VALUES.to_a.shuffle.map { |v| Position.new(v) }
|
69
|
+
end
|
70
|
+
|
71
|
+
def random_positions_queue
|
72
|
+
all_random_positions + all_random_positions + [Position.random] + [Position.random]
|
73
|
+
end
|
74
|
+
|
75
|
+
def available_prioritized_positions(drafted_players:)
|
76
|
+
drafted_positions = drafted_players.map(&:position)
|
77
|
+
queue = prioritized_positions.dup
|
78
|
+
|
79
|
+
drafted_positions.each do |drafted_position|
|
80
|
+
index = queue.index(drafted_position)
|
81
|
+
|
82
|
+
next unless index
|
83
|
+
|
84
|
+
queue.delete_at(index)
|
85
|
+
|
86
|
+
queue << drafted_position
|
87
|
+
end
|
88
|
+
|
89
|
+
queue
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Basketball
|
4
|
+
module Drafting
|
5
|
+
class PickEvent < Event
|
6
|
+
attr_reader :player
|
7
|
+
|
8
|
+
def initialize(id:, team:, player:, pick:, round:, round_pick:)
|
9
|
+
super(id:, team:, pick:, round:, round_pick:)
|
10
|
+
|
11
|
+
raise ArgumentError, 'player required' unless player
|
12
|
+
|
13
|
+
@player = player
|
14
|
+
|
15
|
+
freeze
|
16
|
+
end
|
17
|
+
|
18
|
+
def to_s
|
19
|
+
"#{player} picked #{super}"
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,43 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Basketball
|
4
|
+
module Drafting
|
5
|
+
class Player < Entity
|
6
|
+
STAR_THRESHOLD = 75
|
7
|
+
OVERALL_STAR_INDICATOR = '⭐'
|
8
|
+
|
9
|
+
private_constant :STAR_THRESHOLD, :OVERALL_STAR_INDICATOR
|
10
|
+
|
11
|
+
attr_reader :first_name, :last_name, :position, :overall
|
12
|
+
|
13
|
+
def initialize(id:, position:, first_name: '', last_name: '', overall: 0)
|
14
|
+
super(id)
|
15
|
+
|
16
|
+
raise ArgumentError, 'position is required' unless position
|
17
|
+
|
18
|
+
@first_name = first_name.to_s
|
19
|
+
@last_name = last_name.to_s
|
20
|
+
@position = position
|
21
|
+
@overall = overall.to_i
|
22
|
+
|
23
|
+
freeze
|
24
|
+
end
|
25
|
+
|
26
|
+
def full_name
|
27
|
+
"#{first_name.strip} #{last_name.strip}".strip
|
28
|
+
end
|
29
|
+
|
30
|
+
def to_s
|
31
|
+
"[#{super}] #{full_name} (#{position}) #{overall} #{star_indicators.join(', ')}".strip
|
32
|
+
end
|
33
|
+
|
34
|
+
private
|
35
|
+
|
36
|
+
def star_indicators
|
37
|
+
[].tap do |indicators|
|
38
|
+
indicators << OVERALL_STAR_INDICATOR if overall >= STAR_THRESHOLD
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Basketball
|
4
|
+
module Drafting
|
5
|
+
class PlayerSearch
|
6
|
+
attr_reader :players
|
7
|
+
|
8
|
+
def initialize(players = [])
|
9
|
+
@players = players
|
10
|
+
end
|
11
|
+
|
12
|
+
def query(position: nil, exclude_positions: [])
|
13
|
+
filtered_players = players
|
14
|
+
|
15
|
+
if position
|
16
|
+
filtered_players = filtered_players.select do |player|
|
17
|
+
player.position == position
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
if exclude_positions.any?
|
22
|
+
filtered_players = filtered_players.reject do |player|
|
23
|
+
exclude_positions.include?(player.position)
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
filtered_players.sort_by(&:overall).reverse
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Basketball
|
4
|
+
module Drafting
|
5
|
+
class Position
|
6
|
+
extend Forwardable
|
7
|
+
|
8
|
+
class << self
|
9
|
+
def random
|
10
|
+
new(ALL_VALUES.to_a.sample)
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
class InvalidPositionError < StandardError; end
|
15
|
+
|
16
|
+
BACK_COURT_VALUES = %w[PG SG SF].to_set.freeze
|
17
|
+
FRONT_COURT_VALUES = %w[PF C].to_set.freeze
|
18
|
+
ALL_VALUES = (BACK_COURT_VALUES.to_a + FRONT_COURT_VALUES.to_a).to_set.freeze
|
19
|
+
|
20
|
+
attr_reader :value
|
21
|
+
|
22
|
+
def_delegators :value, :to_s
|
23
|
+
|
24
|
+
def initialize(value)
|
25
|
+
@value = value.to_s.upcase
|
26
|
+
|
27
|
+
raise InvalidPositionError, "Unknown position value: #{@value}" unless ALL_VALUES.include?(@value)
|
28
|
+
|
29
|
+
freeze
|
30
|
+
end
|
31
|
+
|
32
|
+
def ==(other)
|
33
|
+
value == other.value
|
34
|
+
end
|
35
|
+
alias eql? ==
|
36
|
+
|
37
|
+
def hash
|
38
|
+
value.hash
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Basketball
|
4
|
+
module Drafting
|
5
|
+
class Roster
|
6
|
+
extend Forwardable
|
7
|
+
|
8
|
+
class WrongTeamEventError < StandardError; end
|
9
|
+
|
10
|
+
attr_reader :team, :events
|
11
|
+
|
12
|
+
def_delegators :team, :id
|
13
|
+
|
14
|
+
def initialize(team:, events: [])
|
15
|
+
raise ArgumentError, 'team is required' unless team
|
16
|
+
|
17
|
+
other_teams_pick_event_ids = events.reject { |e| e.team == team }.map(&:id)
|
18
|
+
|
19
|
+
if other_teams_pick_event_ids.any?
|
20
|
+
raise WrongTeamEventError,
|
21
|
+
"Event(s): #{other_teams_pick_event_ids.join(',')} has wrong team"
|
22
|
+
end
|
23
|
+
|
24
|
+
@team = team
|
25
|
+
@events = events
|
26
|
+
end
|
27
|
+
|
28
|
+
def players
|
29
|
+
events.map(&:player)
|
30
|
+
end
|
31
|
+
|
32
|
+
def to_s
|
33
|
+
([team.to_s] + players.map(&:to_s)).join("\n")
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Basketball
|
4
|
+
module Drafting
|
5
|
+
class SimEvent < Event
|
6
|
+
attr_reader :player
|
7
|
+
|
8
|
+
def initialize(id:, team:, player:, pick:, round:, round_pick:)
|
9
|
+
super(id:, team:, pick:, round:, round_pick:)
|
10
|
+
|
11
|
+
raise ArgumentError, 'player required' unless player
|
12
|
+
|
13
|
+
@player = player
|
14
|
+
|
15
|
+
freeze
|
16
|
+
end
|
17
|
+
|
18
|
+
def to_s
|
19
|
+
"#{player} auto-picked #{super}"
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|