basketball 0.0.8 → 0.0.10

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