basketball 0.0.10 → 0.0.11

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