basketball 0.0.10 → 0.0.11

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.
@@ -3,32 +3,27 @@
3
3
  module Basketball
4
4
  module App
5
5
  # Examples:
6
- # exe/basketball-room -o tmp/draft.json
7
- # exe/basketball-room -i tmp/draft.json -o tmp/draft-wip.json -s 26 -p P-5,P-10 -t 10
8
- # exe/basketball-room -i tmp/draft-wip.json -x 2
9
- # exe/basketball-room -i tmp/draft-wip.json -g -t 10
10
- # exe/basketball-room -i tmp/draft-wip.json -s 30 -t 10
11
- # exe/basketball-room -i tmp/draft-wip.json -ale
12
- #
13
- # exe/basketball-room -o tmp/draft-wip.json -ale -r tmp/draft-league.json
6
+ # exe/basketball-draft-room -o tmp/draft.json
7
+ # exe/basketball-draft-room -i tmp/draft.json -o tmp/draft-wip.json -s 26 -p P-5,P-10 -l 10
8
+ # exe/basketball-draft-room -i tmp/draft-wip.json -x 2
9
+ # exe/basketball-draft-room -i tmp/draft-wip.json -g -l 10
10
+ # exe/basketball-draft-room -i tmp/draft-wip.json -s 30 -l 10
11
+ # exe/basketball-draft-room -i tmp/draft-wip.json -ate
14
12
  class RoomCLI
15
13
  class PlayerNotFound < StandardError; end
16
14
 
17
15
  attr_reader :opts,
18
16
  :io,
19
- :room_repository,
20
- :league_repository
17
+ :room_repository
21
18
 
22
19
  def initialize(
23
20
  args:,
24
21
  io: $stdout,
25
- room_repository: RoomRepository.new(FileStore.new),
26
- league_repository: LeagueRepository.new(FileStore.new)
22
+ room_repository: RoomRepository.new(FileStore.new)
27
23
  )
28
24
  @io = io
29
25
  @opts = slop_parse(args)
30
26
  @room_repository = room_repository
31
- @league_repository = league_repository
32
27
 
33
28
  if opts[:input].to_s.empty? && opts[:output].to_s.empty?
34
29
  io.puts('Input and/or output paths are required.')
@@ -46,21 +41,14 @@ module Basketball
46
41
  status(room)
47
42
  write(room)
48
43
  events(room)
49
- league(room)
44
+ teams(room)
50
45
  query(room)
51
- rosters(room)
52
46
 
53
47
  self
54
48
  end
55
49
 
56
50
  private
57
51
 
58
- def rosters(room)
59
- return if opts[:rosters].to_s.empty?
60
-
61
- league_repository.save(opts[:rosters], room.league)
62
- end
63
-
64
52
  def status(room)
65
53
  io.puts
66
54
  io.puts('Status')
@@ -79,18 +67,17 @@ module Basketball
79
67
 
80
68
  def slop_parse(args)
81
69
  Slop.parse(args) do |o|
82
- o.banner = 'Usage: basketball-room [options] ...'
70
+ o.banner = 'Usage: basketball-draft-room [options] ...'
83
71
 
84
72
  o.string '-i', '--input', 'Path to load the Room from. If omitted then a new draft will be generated.'
85
73
  o.string '-o', '--output', 'Path to write the room to (if omitted then input path will be used)'
86
74
  o.integer '-s', '--simulate', 'Number of picks to simulate (default is 0).', default: 0
87
75
  o.bool '-a', '--simulate-all', 'Simulate the rest of the draft', default: false
88
76
  o.array '-p', '--picks', 'Comma-separated list of ordered player IDs to pick.', delimiter: ','
89
- o.integer '-t', '--top', 'Output the top rated available players (default is 0).', default: 0
90
- o.bool '-l', '--league', 'Output all teams and their picks', default: false
77
+ o.integer '-l', '--list', 'List the top rated available players (default is 0).', default: 0
78
+ o.bool '-t', '--teams', 'Output all teams and their picks', default: false
91
79
  o.integer '-x', '--skip', 'Number of picks to skip (default is 0).', default: 0
92
80
  o.bool '-e', '--events', 'Output event log.', default: false
93
- o.string '-r', '--rosters', 'Path to write the resulting rosters (league) to.'
94
81
 
95
82
  o.on '-h', '--help', 'Print out help, like this is doing right now.' do
96
83
  io.puts(o)
@@ -129,11 +116,11 @@ module Basketball
129
116
  Draft::Room.new(rounds: 12, players:, front_offices:)
130
117
  end
131
118
 
132
- def league(room)
133
- return unless opts[:league]
119
+ def teams(room)
120
+ return unless opts[:teams]
134
121
 
135
122
  io.puts
136
- io.puts(room.league)
123
+ io.puts(room.teams)
137
124
  end
138
125
 
139
126
  def events(room)
@@ -146,14 +133,14 @@ module Basketball
146
133
  end
147
134
 
148
135
  def query(room)
149
- top = opts[:top]
136
+ list = opts[:list]
150
137
 
151
- return if top <= 0
138
+ return if list <= 0
152
139
 
153
- players = room.undrafted_players.sort_by(&:overall).reverse.take(opts[:top])
140
+ players = room.undrafted_players.sort_by(&:overall).reverse.take(opts[:list])
154
141
 
155
142
  io.puts
156
- io.puts("Top #{top} available players")
143
+ io.puts("Top #{list} available players")
157
144
  io.puts(players)
158
145
  end
159
146
 
@@ -20,13 +20,6 @@ module Basketball
20
20
  def to_s
21
21
  "#{super} #{auto ? 'auto-' : ''}picked #{player}"
22
22
  end
23
-
24
- def ==(other)
25
- super &&
26
- player == other.player &&
27
- auto == other.auto
28
- end
29
- alias eql? ==
30
23
  end
31
24
  end
32
25
  end
@@ -30,17 +30,15 @@ module Basketball
30
30
  end
31
31
 
32
32
  # This method will return a materialized list of teams and their selections.
33
- def league
34
- Org::League.new.tap do |league|
35
- front_offices.each do |front_office|
36
- team = Org::Team.new(id: front_office.id)
33
+ def teams
34
+ front_offices.each_with_object([]) do |front_office, memo|
35
+ team = Org::Team.new(id: front_office.id)
37
36
 
38
- league.register!(team)
39
-
40
- drafted_players(front_office).each do |player|
41
- league.sign!(player:, team:)
42
- end
37
+ drafted_players(front_office).each do |player|
38
+ team.sign!(player)
43
39
  end
40
+
41
+ memo << team
44
42
  end
45
43
  end
46
44
 
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Basketball
4
+ module Org
5
+ # A collection of divisions, teams, and players.
6
+ class Conference < Entity
7
+ include HasDivisions
8
+
9
+ attr_reader :divisions
10
+
11
+ def initialize(id:, divisions: [])
12
+ super(id)
13
+
14
+ @divisions = []
15
+
16
+ divisions.each { |d| register_division!(d) }
17
+
18
+ freeze
19
+ end
20
+
21
+ def to_s
22
+ ([super] + divisions.map(&:to_s)).join("\n")
23
+ end
24
+
25
+ def teams
26
+ divisions.flat_map(&:teams)
27
+ end
28
+
29
+ def players
30
+ divisions.flat_map(&:players)
31
+ end
32
+
33
+ private
34
+
35
+ def register_division!(division)
36
+ raise ArgumentError, 'division is required' unless division
37
+ raise DivisionAlreadyRegisteredError, "#{division} already registered" if division?(division)
38
+
39
+ assert_teams_are_not_already_registered(division.teams)
40
+
41
+ divisions << division
42
+
43
+ self
44
+ end
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Basketball
4
+ module Org
5
+ # A collection of teams and players.
6
+ class Division < Entity
7
+ include HasTeams
8
+
9
+ attr_reader :teams
10
+
11
+ def initialize(id:, teams: [])
12
+ super(id)
13
+
14
+ @teams = []
15
+
16
+ teams.each { |t| register_team!(t) }
17
+
18
+ freeze
19
+ end
20
+
21
+ def to_s
22
+ ([super] + teams.map(&:to_s)).join("\n")
23
+ end
24
+
25
+ def players
26
+ teams.flat_map(&:players)
27
+ end
28
+
29
+ private
30
+
31
+ def register_team!(team)
32
+ raise ArgumentError, 'team is required' unless team
33
+ raise TeamAlreadyRegisteredError, "#{team} already registered" if team?(team)
34
+
35
+ assert_players_are_not_already_signed(team.players)
36
+
37
+ teams << team
38
+
39
+ self
40
+ end
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Basketball
4
+ module Org
5
+ # Helper methods for objects that can be composed of divisions which are also composed of teams
6
+ # and players.
7
+ module HasDivisions
8
+ include HasTeams
9
+
10
+ def division?(division)
11
+ divisions.include?(division)
12
+ end
13
+
14
+ private
15
+
16
+ def assert_divisions_are_not_already_registered(divisions)
17
+ divisions.each do |division|
18
+ raise DivisionAlreadyRegisteredError, "#{division} already registered" if division?(division)
19
+
20
+ assert_teams_are_not_already_registered(division.teams)
21
+ end
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Basketball
4
+ module Org
5
+ # Helper methods for objects that can be composed of players.
6
+ module HasPlayers
7
+ def player?(player)
8
+ players.include?(player)
9
+ end
10
+
11
+ private
12
+
13
+ def assert_players_are_not_already_signed(players)
14
+ players.each do |player|
15
+ raise PlayerAlreadySignedError, "#{player} already registered" if player?(player)
16
+ end
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Basketball
4
+ module Org
5
+ # Helper methods for objects that can be composed of teams which are also made up of players.
6
+ module HasTeams
7
+ include HasPlayers
8
+
9
+ def team?(team)
10
+ teams.include?(team)
11
+ end
12
+
13
+ private
14
+
15
+ def assert_teams_are_not_already_registered(teams)
16
+ teams.each do |team|
17
+ raise TeamAlreadyRegisteredError, "#{team} already registered" if team?(team)
18
+
19
+ assert_players_are_not_already_signed(team.players)
20
+ end
21
+ end
22
+ end
23
+ end
24
+ end
@@ -2,66 +2,98 @@
2
2
 
3
3
  module Basketball
4
4
  module Org
5
- # Describes a collection of teams and players. Holds the rules which support
6
- # adding teams and players to ensure the all the teams are cohesive, such as:
5
+ # Describes a collection of conferences, divisions, teams, and players.
6
+ # Holds the rules which support adding teams and players to ensure the all the
7
+ # teams are cohesive, such as:
8
+ # - preventing duplicate conferences
9
+ # - preventing duplicate divisions
7
10
  # - preventing duplicate teams
8
11
  # - preventing double-signing players across teams
9
12
  class League < Entity
10
- class TeamAlreadyRegisteredError < StandardError; end
11
- class UnregisteredTeamError < StandardError; end
13
+ include HasDivisions
12
14
 
13
- attr_reader :teams
15
+ class ConferenceAlreadyRegisteredError < StandardError; end
14
16
 
15
- def initialize(teams: [])
17
+ alias signed? player?
18
+
19
+ attr_reader :conferences
20
+
21
+ def initialize(conferences: [])
16
22
  super()
17
23
 
18
- @teams = []
24
+ @conferences = []
19
25
 
20
- teams.each { |team| register!(team) }
26
+ conferences.each { |c| register!(c) }
21
27
  end
22
28
 
23
29
  def to_s
24
- teams.map(&:to_s).join("\n")
30
+ conferences.map(&:to_s).join("\n")
25
31
  end
26
32
 
27
33
  def sign!(player:, team:)
28
34
  raise ArgumentError, 'player is required' unless player
29
35
  raise ArgumentError, 'team is required' unless team
30
- raise UnregisteredTeamError, "#{team} is not registered" unless registered?(team)
31
- raise PlayerAlreadySignedError, "#{player} is already signed" if signed?(player)
36
+ raise UnregisteredTeamError, "#{team} not registered" unless team?(team)
37
+ raise PlayerAlreadySignedError, "#{player} already registered" if player?(player)
32
38
 
33
- team.sign!(player)
39
+ # It is OK to pass in a detached team as long as its equivalent resides in this
40
+ # League's object graph.
41
+ team_for(team.id).sign!(player)
34
42
 
35
43
  self
36
44
  end
37
45
 
38
- def signed?(player)
39
- players.include?(player)
46
+ def register!(conference)
47
+ raise ArgumentError, 'conference is required' unless conference
48
+ raise ConferenceAlreadyRegisteredError, "#{conference} already registered" if conference?(conference)
49
+
50
+ assert_divisions_are_not_already_registered(conference.divisions)
51
+
52
+ conferences << conference
53
+
54
+ self
55
+ end
56
+
57
+ def conference?(conference)
58
+ conferences.include?(conference)
59
+ end
60
+
61
+ def divisions
62
+ conferences.flat_map(&:divisions)
63
+ end
64
+
65
+ def teams
66
+ conferences.flat_map(&:teams)
40
67
  end
41
68
 
42
69
  def players
43
- teams.flat_map(&:players)
70
+ conferences.flat_map(&:players)
44
71
  end
45
72
 
46
- def not_registered?(team)
47
- !registered?(team)
73
+ def conference_for(team)
74
+ conferences.find { |c| c.divisions.find { |d| d.teams.include?(team) } }
48
75
  end
49
76
 
50
- def registered?(team)
51
- teams.include?(team)
77
+ def division_for(team)
78
+ conference_for(team)&.divisions&.find { |d| d.teams.include?(team) }
52
79
  end
53
80
 
54
- def register!(team)
55
- raise ArgumentError, 'team is required' unless team
56
- raise TeamAlreadyRegisteredError, "#{team} already registered" if registered?(team)
81
+ # Same conference, different division
82
+ def cross_division_opponents_for(team)
83
+ conference = conference_for(team)
84
+ division = division_for(team)
57
85
 
58
- team.players.each do |player|
59
- raise PlayerAlreadySignedError, "#{player} already signed" if signed?(player)
60
- end
86
+ return nil unless conference && division
61
87
 
62
- teams << team
88
+ other_divisions = conference.divisions - [division]
63
89
 
64
- self
90
+ other_divisions.flat_map(&:teams)
91
+ end
92
+
93
+ private
94
+
95
+ def team_for(id)
96
+ teams.find { |team| team.id == id }
65
97
  end
66
98
  end
67
99
  end
@@ -1,12 +1,23 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ # Cross-cutting Concerns
4
+ require_relative 'org/has_players'
5
+ require_relative 'org/has_teams'
6
+ require_relative 'org/has_divisions'
7
+
8
+ # Domain Models
9
+ require_relative 'org/conference'
10
+ require_relative 'org/division'
11
+ require_relative 'org/league'
3
12
  require_relative 'org/player'
4
13
  require_relative 'org/position'
5
14
  require_relative 'org/team'
6
- require_relative 'org/league'
7
15
 
8
16
  module Basketball
9
17
  module Org
18
+ class DivisionAlreadyRegisteredError < StandardError; end
10
19
  class PlayerAlreadySignedError < StandardError; end
20
+ class TeamAlreadyRegisteredError < StandardError; end
21
+ class UnregisteredTeamError < StandardError; end
11
22
  end
12
23
  end
@@ -12,11 +12,11 @@ module Basketball
12
12
  DEFAULT_MAX_HOME_ADVANTAGE = 5
13
13
 
14
14
  DEFAULT_STRATEGY_FREQUENCIES = {
15
- RANDOM => 10,
16
- TOP_ONE => 5,
17
- TOP_TWO => 10,
18
- TOP_THREE => 20,
19
- TOP_SIX => 30
15
+ RANDOM => 3,
16
+ TOP_ONE => 1,
17
+ TOP_TWO => 1,
18
+ TOP_THREE => 1,
19
+ TOP_SIX => 1
20
20
  }.freeze
21
21
 
22
22
  attr_reader :lotto, :max_home_advantage
@@ -2,35 +2,35 @@
2
2
 
3
3
  module Basketball
4
4
  module Season
5
- # Sets boundaries for preseason and regular season play. Add games as long as they are
5
+ # Sets boundaries for exhibition and regular season play. Add games as long as they are
6
6
  # within the correct dated boundaries
7
7
  class Calendar
8
8
  class OutOfBoundsError < StandardError; end
9
9
  class TeamAlreadyBookedError < StandardError; end
10
10
 
11
- attr_reader :preseason_start_date,
12
- :preseason_end_date,
13
- :season_start_date,
14
- :season_end_date,
11
+ attr_reader :exhibition_start_date,
12
+ :exhibition_end_date,
13
+ :regular_start_date,
14
+ :regular_end_date,
15
15
  :games
16
16
 
17
17
  def initialize(
18
- preseason_start_date:,
19
- preseason_end_date:,
20
- season_start_date:,
21
- season_end_date:,
18
+ exhibition_start_date:,
19
+ exhibition_end_date:,
20
+ regular_start_date:,
21
+ regular_end_date:,
22
22
  games: []
23
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?
24
+ raise ArgumentError, 'exhibition_start_date is required' if exhibition_start_date.to_s.empty?
25
+ raise ArgumentError, 'exhibition_end_date is required' if exhibition_end_date.to_s.empty?
26
+ raise ArgumentError, 'regular_start_date is required' if regular_start_date.to_s.empty?
27
+ raise ArgumentError, 'regular_end_date is required' if regular_end_date.to_s.empty?
28
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
33
- @games = []
29
+ @exhibition_start_date = exhibition_start_date
30
+ @exhibition_end_date = exhibition_end_date
31
+ @regular_start_date = regular_start_date
32
+ @regular_end_date = regular_end_date
33
+ @games = []
34
34
 
35
35
  games.each { |game| add!(game) }
36
36
 
@@ -60,8 +60,38 @@ module Basketball
60
60
  end
61
61
  end
62
62
 
63
+ def available_exhibition_dates_for(opponent)
64
+ all_exhibition_dates - exhibitions_for(opponent:).map(&:date)
65
+ end
66
+
67
+ def available_regular_dates_for(opponent)
68
+ all_season_dates - regulars_for(opponent:).map(&:date)
69
+ end
70
+
71
+ def available_exhibition_matchup_dates(opponent1, opponent2)
72
+ available_opponent_dates = available_exhibition_dates_for(opponent1)
73
+ available_other_opponent_dates = available_exhibition_dates_for(opponent2)
74
+
75
+ available_opponent_dates & available_other_opponent_dates
76
+ end
77
+
78
+ def available_regular_matchup_dates(opponent1, opponent2)
79
+ available_opponent_dates = available_regular_dates_for(opponent1)
80
+ available_other_opponent_dates = available_regular_dates_for(opponent2)
81
+
82
+ available_opponent_dates & available_other_opponent_dates
83
+ end
84
+
63
85
  private
64
86
 
87
+ def all_exhibition_dates
88
+ (exhibition_start_date..exhibition_end_date).to_a
89
+ end
90
+
91
+ def all_season_dates
92
+ (regular_start_date..regular_end_date).to_a
93
+ end
94
+
65
95
  def assert_free_date(game)
66
96
  if games_for(date: game.date, opponent: game.home_opponent).any?
67
97
  raise TeamAlreadyBookedError, "#{game.home_opponent} already playing on #{game.date}"
@@ -76,11 +106,11 @@ module Basketball
76
106
  date = game.date
77
107
 
78
108
  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
109
+ raise OutOfBoundsError, "#{date} is before exhibition begins" if date < exhibition_start_date
110
+ raise OutOfBoundsError, "#{date} is after exhibition ends" if date > exhibition_end_date
81
111
  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
112
+ raise OutOfBoundsError, "#{date} is before season begins" if date < regular_start_date
113
+ raise OutOfBoundsError, "#{date} is after season ends" if date > regular_end_date
84
114
  else
85
115
  raise ArgumentError, "Dont know what this game type is: #{game.class.name}"
86
116
  end