basketball 0.0.8 → 0.0.10

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.
Files changed (59) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +9 -19
  3. data/CHANGELOG.md +1 -39
  4. data/README.md +75 -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 +250 -0
  9. data/lib/basketball/app/coordinator_repository.rb +114 -0
  10. data/lib/basketball/app/document_repository.rb +67 -0
  11. data/lib/basketball/app/file_store.rb +38 -0
  12. data/lib/basketball/app/in_memory_store.rb +42 -0
  13. data/lib/basketball/app/league_repository.rb +20 -0
  14. data/lib/basketball/app/league_serializable.rb +54 -0
  15. data/lib/basketball/{draft/cli.rb → app/room_cli.rb} +74 -80
  16. data/lib/basketball/app/room_repository.rb +149 -0
  17. data/lib/basketball/app.rb +20 -0
  18. data/lib/basketball/draft/assessment.rb +31 -0
  19. data/lib/basketball/draft/event.rb +3 -2
  20. data/lib/basketball/draft/front_office.rb +35 -28
  21. data/lib/basketball/draft/{pick_event.rb → pick.rb} +13 -6
  22. data/lib/basketball/draft/room.rb +119 -119
  23. data/lib/basketball/draft/{player_search.rb → scout.rb} +4 -9
  24. data/lib/basketball/draft/skip.rb +12 -0
  25. data/lib/basketball/draft.rb +13 -6
  26. data/lib/basketball/entity.rb +19 -10
  27. data/lib/basketball/org/league.rb +68 -0
  28. data/lib/basketball/org/player.rb +26 -0
  29. data/lib/basketball/{draft → org}/position.rb +3 -2
  30. data/lib/basketball/org/team.rb +38 -0
  31. data/lib/basketball/org.rb +12 -0
  32. data/lib/basketball/season/arena.rb +113 -0
  33. data/lib/basketball/season/calendar.rb +41 -72
  34. data/lib/basketball/season/coordinator.rb +186 -128
  35. data/lib/basketball/season/{preseason_game.rb → exhibition.rb} +2 -1
  36. data/lib/basketball/season/game.rb +15 -10
  37. data/lib/basketball/season/matchup.rb +27 -0
  38. data/lib/basketball/season/opponent.rb +15 -0
  39. data/lib/basketball/season/{season_game.rb → regular.rb} +2 -1
  40. data/lib/basketball/season/result.rb +37 -0
  41. data/lib/basketball/season.rb +12 -13
  42. data/lib/basketball/value_object.rb +8 -27
  43. data/lib/basketball/value_object_dsl.rb +30 -0
  44. data/lib/basketball/version.rb +1 -1
  45. data/lib/basketball.rb +9 -4
  46. metadata +37 -44
  47. data/lib/basketball/draft/league.rb +0 -70
  48. data/lib/basketball/draft/player.rb +0 -43
  49. data/lib/basketball/draft/room_serializer.rb +0 -186
  50. data/lib/basketball/draft/roster.rb +0 -37
  51. data/lib/basketball/draft/sim_event.rb +0 -23
  52. data/lib/basketball/draft/skip_event.rb +0 -13
  53. data/lib/basketball/season/calendar_serializer.rb +0 -94
  54. data/lib/basketball/season/conference.rb +0 -57
  55. data/lib/basketball/season/division.rb +0 -43
  56. data/lib/basketball/season/league.rb +0 -114
  57. data/lib/basketball/season/league_serializer.rb +0 -99
  58. data/lib/basketball/season/scheduling_cli.rb +0 -198
  59. data/lib/basketball/season/team.rb +0 -21
@@ -0,0 +1,149 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Basketball
4
+ module App
5
+ # Can load and save Room objects to JSON files.
6
+ class RoomRepository < DocumentRepository
7
+ PICK_EVENT = 'Pick'
8
+ SKIP_EVENT = 'Skip'
9
+
10
+ private_constant :PICK_EVENT, :SKIP_EVENT
11
+
12
+ private
13
+
14
+ def from_h(hash)
15
+ front_offices = deserialize_front_offices(hash[:front_offices])
16
+ players = deserialize_players(hash[:players])
17
+ events = deserialize_events(hash[:events], players:, front_offices:)
18
+
19
+ Draft::Room.new(
20
+ rounds: hash[:rounds],
21
+ front_offices:,
22
+ players:,
23
+ events:
24
+ )
25
+ end
26
+
27
+ def to_h(room)
28
+ {
29
+ rounds: room.rounds,
30
+ front_offices: room.front_offices.map { |fo| serialize_front_office(fo) },
31
+ players: room.players.map { |p| serialize_player(p) },
32
+ events: serialize_events(room.events)
33
+ }
34
+ end
35
+
36
+ # Serialization
37
+
38
+ def serialize_player(player)
39
+ {
40
+ id: player.id,
41
+ overall: player.overall,
42
+ position: player.position&.code
43
+ }
44
+ end
45
+
46
+ def serialize_front_office(front_office)
47
+ {
48
+ id: front_office.id,
49
+ risk_level: front_office.risk_level
50
+ }
51
+ end
52
+
53
+ def serialize_events(events)
54
+ events.map do |event|
55
+ case event
56
+ when Draft::Pick
57
+ serialize_pick(event)
58
+ when Draft::Skip
59
+ serialize_skip(event)
60
+ end
61
+ end
62
+ end
63
+
64
+ def serialize_pick(event)
65
+ {
66
+ type: PICK_EVENT,
67
+ front_office: event.front_office.id,
68
+ pick: event.pick,
69
+ round: event.round,
70
+ round_pick: event.round_pick,
71
+ auto: event.auto,
72
+ player: event.player.id
73
+ }
74
+ end
75
+
76
+ def serialize_skip(event)
77
+ {
78
+ type: SKIP_EVENT,
79
+ front_office: event.front_office.id,
80
+ pick: event.pick,
81
+ round: event.round,
82
+ round_pick: event.round_pick
83
+ }
84
+ end
85
+
86
+ # Deserialization
87
+
88
+ def deserialize_player(player_hash)
89
+ Org::Player.new(
90
+ id: player_hash[:id],
91
+ overall: player_hash[:overall],
92
+ position: Org::Position.new(player_hash[:position])
93
+ )
94
+ end
95
+
96
+ def deserialize_front_office(hash)
97
+ Draft::FrontOffice.new(
98
+ id: hash[:id],
99
+ risk_level: hash[:risk_level]
100
+ )
101
+ end
102
+
103
+ def deserialize_front_offices(hashes)
104
+ (hashes || []).map { |fo| deserialize_front_office(fo) }
105
+ end
106
+
107
+ def deserialize_players(hashes)
108
+ (hashes || []).map { |hash| deserialize_player(hash) }
109
+ end
110
+
111
+ def deserialize_pick(hash, players:, front_office:)
112
+ player_id = hash[:player]
113
+ player = players.find { |p| p.id == player_id }
114
+
115
+ Draft::Pick.new(
116
+ front_office:,
117
+ pick: hash[:pick],
118
+ round: hash[:round],
119
+ round_pick: hash[:round_pick],
120
+ player:,
121
+ auto: hash[:auto]
122
+ )
123
+ end
124
+
125
+ def deserialize_skip(hash, front_office:)
126
+ Draft::Skip.new(
127
+ front_office:,
128
+ pick: hash[:pick],
129
+ round: hash[:round],
130
+ round_pick: hash[:round_pick]
131
+ )
132
+ end
133
+
134
+ def deserialize_events(hashes, players:, front_offices:)
135
+ (hashes || []).map do |hash|
136
+ front_office_id = hash[:front_office]
137
+ front_office = front_offices.find { |fo| fo.id == front_office_id }
138
+
139
+ case hash[:type]
140
+ when PICK_EVENT
141
+ deserialize_pick(hash, players:, front_office:)
142
+ when SKIP_EVENT
143
+ deserialize_skip(hash, front_office:)
144
+ end
145
+ end
146
+ end
147
+ end
148
+ end
149
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Stores
4
+ require_relative 'app/file_store'
5
+ require_relative 'app/in_memory_store'
6
+
7
+ # Serialization
8
+ require_relative 'app/league_serializable'
9
+
10
+ # Repositories / Common
11
+ require_relative 'app/document_repository'
12
+
13
+ # Repositories / Implementations
14
+ require_relative 'app/coordinator_repository'
15
+ require_relative 'app/league_repository'
16
+ require_relative 'app/room_repository'
17
+
18
+ # Controllers
19
+ require_relative 'app/coordinator_cli'
20
+ require_relative 'app/room_cli'
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Basketball
4
+ module Draft
5
+ # An assessment is given to a front office when it needs the front office to make a pick.
6
+ # This is essentially just a data-transfer object to get information to a front office to make a pick.
7
+ class Assessment
8
+ attr_reader :drafted_players,
9
+ :pick,
10
+ :round_pick,
11
+ :round,
12
+ :undrafted_players
13
+
14
+ def initialize(
15
+ drafted_players:,
16
+ pick:,
17
+ round_pick:,
18
+ round:,
19
+ undrafted_players:
20
+ )
21
+ @drafted_players = drafted_players
22
+ @pick = pick
23
+ @round = round
24
+ @round_pick = round_pick
25
+ @undrafted_players = undrafted_players
26
+
27
+ freeze
28
+ end
29
+ end
30
+ end
31
+ end
@@ -2,8 +2,9 @@
2
2
 
3
3
  module Basketball
4
4
  module Draft
5
+ # Describes what all Room events have to have to be considered an "event".
5
6
  class Event < ValueObject
6
- attr_reader_value :pick, :round, :round_pick, :front_office
7
+ value_reader :pick, :round, :round_pick, :front_office
7
8
 
8
9
  def initialize(front_office:, pick:, round:, round_pick:)
9
10
  super()
@@ -17,7 +18,7 @@ module Basketball
17
18
  end
18
19
 
19
20
  def to_s
20
- "##{pick} overall (R#{round}:P#{round_pick}) by #{front_office}"
21
+ "[##{pick} R:#{round} P:#{round_pick}] #{front_office}"
21
22
  end
22
23
  end
23
24
  end
@@ -2,22 +2,34 @@
2
2
 
3
3
  module Basketball
4
4
  module Draft
5
+ # A team will send their front office to a draft room to make draft selections.
5
6
  class FrontOffice < Entity
6
- MAX_DEPTH = 3
7
- MAX_FUZZ = 2
7
+ # The higher the number the more the front office will make more top-player-based decisions
8
+ # over position-based decisions.
9
+ DEFAULT_MAX_STAR_LEVEL = 5
10
+
11
+ # Riskier front offices may choose to not draft the top player. The higher the number the more
12
+ # they will not select the top player available.
13
+ DEFAULT_MAX_RISK_LEVEL = 5
14
+
8
15
  MAX_POSITIONS = 12
9
16
 
10
- private_constant :MAX_DEPTH, :MAX_FUZZ, :MAX_POSITIONS
17
+ private_constant :DEFAULT_MAX_STAR_LEVEL, :DEFAULT_MAX_RISK_LEVEL, :MAX_POSITIONS
11
18
 
12
- attr_reader :prioritized_positions, :fuzz, :depth, :name
19
+ attr_reader :prioritized_positions, :risk_level, :star_level, :scout
13
20
 
14
- def initialize(id:, name: '', prioritized_positions: [], fuzz: rand(0..MAX_FUZZ), depth: rand(0..MAX_DEPTH))
21
+ def initialize(
22
+ id:,
23
+ prioritized_positions: [],
24
+ risk_level: rand(0..DEFAULT_MAX_RISK_LEVEL),
25
+ star_level: rand(0..DEFAULT_MAX_STAR_LEVEL)
26
+ )
15
27
  super(id)
16
28
 
17
- @name = name
18
- @fuzz = fuzz.to_i
19
- @depth = depth.to_i
29
+ @risk_level = risk_level.to_i
30
+ @star_level = star_level.to_i
20
31
  @prioritized_positions = prioritized_positions
32
+ @scout = Scout.new
21
33
 
22
34
  # fill in the rest of the queue here
23
35
  need_count = MAX_POSITIONS - @prioritized_positions.length
@@ -27,51 +39,46 @@ module Basketball
27
39
  freeze
28
40
  end
29
41
 
30
- def pick(undrafted_player_search:, drafted_players:, round:)
42
+ def pick(assessment)
31
43
  players = []
44
+ players = adaptive_search(assessment) if star_level >= assessment.round
45
+ players = balanced_search(assessment) if players.empty?
46
+ players = top_players(assessment) if players.empty?
32
47
 
33
- players = adaptive_search(undrafted_player_search:, drafted_players:) if depth >= round
34
- players = balanced_search(undrafted_player_search:, drafted_players:) if players.empty?
35
- players = top_players(undrafted_player_search:) if players.empty?
36
-
37
- players[0..fuzz].sample
38
- end
39
-
40
- def to_s
41
- "[#{super}] #{name}"
48
+ players[0..risk_level].sample
42
49
  end
43
50
 
44
51
  private
45
52
 
46
- def adaptive_search(undrafted_player_search:, drafted_players:)
47
- drafted_positions = drafted_players.map(&:position)
53
+ def adaptive_search(assessment)
54
+ drafted_positions = assessment.drafted_players.map(&:position)
48
55
 
49
- undrafted_player_search.query(exclude_positions: drafted_positions)
56
+ scout.top_for(players: assessment.undrafted_players, exclude_positions: drafted_positions)
50
57
  end
51
58
 
52
- def balanced_search(undrafted_player_search:, drafted_players:)
59
+ def balanced_search(assessment)
53
60
  players = []
54
61
 
55
62
  # Try to find best pick for exact desired position.
56
63
  # If you cant find one, then move to the next desired position until the end of the queue
57
- available_prioritized_positions(drafted_players:).each do |position|
58
- players = undrafted_player_search.query(position:)
64
+ available_prioritized_positions(assessment.drafted_players).each do |position|
65
+ players = scout.top_for(players: assessment.undrafted_players, position:)
59
66
 
60
67
  break if players.any?
61
68
  end
62
69
 
63
- players = players.any? ? players : undrafted_player_search.query
70
+ players = players.any? ? players : scout.top_for
64
71
  end
65
72
 
66
73
  def all_random_positions
67
- Position::ALL_VALUES.to_a.shuffle.map { |v| Position.new(v) }
74
+ Org::Position::ALL_VALUES.to_a.shuffle.map { |v| Org::Position.new(v) }
68
75
  end
69
76
 
70
77
  def random_positions_queue
71
- all_random_positions + all_random_positions + [Position.random] + [Position.random]
78
+ all_random_positions + all_random_positions + [Org::Position.random] + [Org::Position.random]
72
79
  end
73
80
 
74
- def available_prioritized_positions(drafted_players:)
81
+ def available_prioritized_positions(drafted_players)
75
82
  drafted_positions = drafted_players.map(&:position)
76
83
  queue = prioritized_positions.dup
77
84
 
@@ -1,25 +1,32 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative 'event'
4
-
5
3
  module Basketball
6
4
  module Draft
7
- class PickEvent < Event
8
- attr_reader_value :player
5
+ # Room event where a player is selected.
6
+ class Pick < Event
7
+ value_reader :player, :auto
9
8
 
10
- def initialize(front_office:, player:, pick:, round:, round_pick:)
9
+ def initialize(front_office:, player:, pick:, round:, round_pick:, auto: false)
11
10
  super(front_office:, pick:, round:, round_pick:)
12
11
 
13
12
  raise ArgumentError, 'player required' unless player
14
13
 
15
14
  @player = player
15
+ @auto = auto
16
16
 
17
17
  freeze
18
18
  end
19
19
 
20
20
  def to_s
21
- "#{player} picked #{super}"
21
+ "#{super} #{auto ? 'auto-' : ''}picked #{player}"
22
+ end
23
+
24
+ def ==(other)
25
+ super &&
26
+ player == other.player &&
27
+ auto == other.auto
22
28
  end
29
+ alias eql? ==
23
30
  end
24
31
  end
25
32
  end