basketball 0.0.1
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 +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
|