basketball 0.0.2 → 0.0.4

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: eade2dd71d031c05eaf7e5fa792056d9ee6621f6168c6cbd288e5106f0fe4c3a
4
- data.tar.gz: 66b92eb67dd64d84288dc5869b2076753d043311e22d903d4d8e5468ea90f7f6
3
+ metadata.gz: 71ab0cd4aa4e2bb10d0a9640e18a756938fbe749842abdbbda9be9f2023be9cf
4
+ data.tar.gz: 5d216901cadb3c92d41a1e445956aef4a11fc1d3e418430003d050693c43d3be
5
5
  SHA512:
6
- metadata.gz: 72a89afec17d978e79dd979a50188c62c19532a77143312a79f387a182cab5988a7e90399be60f019ce3a5d6639f5eb0ddc3f14ca896e5f3c3496488be3427de
7
- data.tar.gz: 4f04e99ddd1e64ad893395264c5753b975f4c46b741324d53bd125c1f2a8daa2079c44e2a85fd52bf6dd24d7b09fcf29c5f9266db4c67da19e8f952884de8a72
6
+ metadata.gz: a3812684e58e97711745e31a8696c12c32c79e542661302db86eb7b0fecc3784ede1650ec2d0079b66a641f5e09da657f22f596583630d932e03e55ba7ed3514
7
+ data.tar.gz: f1edd69f31a2ff8ca4d5e53bab9bd59c74a091ab276db1a53901aa1e7de17202898ba01a917304f1e7018da1b3ad129fb012a3ac12516e832f4b8fa6cb8cd232
data/CHANGELOG.md CHANGED
@@ -1,4 +1,15 @@
1
- #### 0.0.1 - May 4th, 2023
1
+ #### 0.0.4 - May 5th, 2023
2
+
3
+ * Add ability to skip draft picks using `Basketball::Drafting::Engine#skip!`
4
+ * Add ability to output event full drafting event log using CLI: `exe/basketball-draft -i tmp/draft-wip.json -l`
5
+ * Add ability to skip draft picks using CLI: `exe/basketball-draft -i tmp/draft-wip.json -x 1`
6
+
7
+ #### 0.0.3 - May 5th, 2023
8
+
9
+ * `Drafting::Engine#sim!` should return events
10
+ * Added `Drafting::Engine#undrafted_player_search`
11
+
12
+ #### 0.0.2 - May 4th, 2023
2
13
 
3
14
  * Remove autoloading in favor of require statements.
4
15
 
@@ -9,12 +9,16 @@ module Basketball
9
9
  module Drafting
10
10
  # Example:
11
11
  # exe/basketball-draft -o tmp/draft.json
12
- # exe/basketball-draft -i tmp/draft.json -o tmp/draft-wip.json -s 28 -p ONEALSH01,ONEALJE01 -t 10 -q PG
12
+ # exe/basketball-draft -i tmp/draft.json -o tmp/draft-wip.json -s 26 -p P-5,P-10 -t 10 -q PG
13
+ # exe/basketball-draft -i tmp/draft-wip.json -x 2
13
14
  # exe/basketball-draft -i tmp/draft-wip.json -r -t 10
14
15
  # exe/basketball-draft -i tmp/draft-wip.json -t 10 -q SG
15
16
  # exe/basketball-draft -i tmp/draft-wip.json -s 30 -t 10
16
17
  # exe/basketball-draft -i tmp/draft-wip.json -a -r
18
+ # exe/basketball-draft -i tmp/draft-wip.json -l
17
19
  class CLI
20
+ class PlayerNotFound < StandardError; end
21
+
18
22
  attr_reader :opts, :serializer, :io
19
23
 
20
24
  def initialize(args:, io: $stdout)
@@ -43,11 +47,13 @@ module Basketball
43
47
  io.puts('Draft is complete!')
44
48
  else
45
49
  io.puts("#{engine.remaining_picks} Remaining pick(s)")
46
- io.puts("Round #{engine.current_round} pick #{engine.current_round_pick} for #{engine.current_team}")
50
+ io.puts("Up Next: Round #{engine.current_round} pick #{engine.current_round_pick} for #{engine.current_team}")
47
51
  end
48
52
 
49
53
  write(engine)
50
54
 
55
+ log(engine)
56
+
51
57
  rosters(engine)
52
58
 
53
59
  query(engine)
@@ -70,6 +76,8 @@ module Basketball
70
76
  o.integer '-t', '--top', 'Output the top rated available players (default is 0).', default: 0
71
77
  o.string '-q', '--query', "Filter TOP by position: #{Position::ALL_VALUES.join(', ')}."
72
78
  o.bool '-r', '--rosters', 'Output all team rosters.', default: false
79
+ o.integer '-x', '--skip', 'Number of picks to skip (default is 0).', default: 0
80
+ o.bool '-l', '--log', 'Output event log.', default: false
73
81
 
74
82
  o.on '-h', '--help', 'Print out help, like this is doing right now.' do
75
83
  io.puts(o)
@@ -121,6 +129,15 @@ module Basketball
121
129
  end
122
130
  end
123
131
 
132
+ def log(engine)
133
+ return unless opts[:log]
134
+
135
+ io.puts
136
+ io.puts('Event Log')
137
+
138
+ puts engine.events
139
+ end
140
+
124
141
  # rubocop:disable Metrics/AbcSize
125
142
  def query(engine)
126
143
  top = opts[:top]
@@ -161,6 +178,8 @@ module Basketball
161
178
 
162
179
  player = engine.players.find { |p| p.id == id.to_s.upcase }
163
180
 
181
+ raise PlayerNotFound, "player not found by id: #{id}" unless player
182
+
164
183
  event = engine.pick!(player)
165
184
 
166
185
  io.puts(event)
@@ -168,6 +187,14 @@ module Basketball
168
187
  event_count += 1
169
188
  end
170
189
 
190
+ opts[:skip].times do
191
+ event = engine.skip!
192
+
193
+ io.puts(event)
194
+
195
+ event_count += 1
196
+ end
197
+
171
198
  engine.sim!(opts[:simulate]) do |event|
172
199
  io.puts(event)
173
200
 
@@ -96,14 +96,31 @@ module Basketball
96
96
  !done?
97
97
  end
98
98
 
99
+ def skip!
100
+ return if done?
101
+
102
+ event = SkipEvent.new(
103
+ id: SecureRandom.uuid,
104
+ team: current_team,
105
+ pick: current_pick,
106
+ round: current_round,
107
+ round_pick: current_round_pick
108
+ )
109
+
110
+ play!(event)
111
+
112
+ event
113
+ end
114
+
99
115
  def sim!(times = nil)
100
116
  counter = 0
117
+ events = []
101
118
 
102
119
  until done? || (times && counter >= times)
103
120
  team = current_team
104
121
 
105
122
  player = team.pick(
106
- undrafted_players:,
123
+ undrafted_player_search:,
107
124
  drafted_players: drafted_players(team),
108
125
  round: current_round
109
126
  )
@@ -122,9 +139,10 @@ module Basketball
122
139
  yield(event) if block_given?
123
140
 
124
141
  counter += 1
142
+ events << event
125
143
  end
126
144
 
127
- self
145
+ events
128
146
  end
129
147
 
130
148
  def pick!(player)
@@ -146,16 +164,24 @@ module Basketball
146
164
  players - drafted_players
147
165
  end
148
166
 
167
+ def undrafted_player_search
168
+ PlayerSearch.new(undrafted_players)
169
+ end
170
+
149
171
  private
150
172
 
151
173
  attr_reader :players_by_id, :teams_by_id
152
174
 
175
+ def player_events
176
+ events.select { |e| e.respond_to?(:player) }
177
+ end
178
+
153
179
  def internal_current_pick
154
180
  events.length + 1
155
181
  end
156
182
 
157
183
  def drafted_players(team = nil)
158
- events.each_with_object([]) do |e, memo|
184
+ player_events.each_with_object([]) do |e, memo|
159
185
  next unless team.nil? || e.team == team
160
186
 
161
187
  memo << e.player
@@ -166,15 +192,21 @@ module Basketball
166
192
  # rubocop:disable Metrics/CyclomaticComplexity
167
193
  # rubocop:disable Metrics/PerceivedComplexity
168
194
  def play!(event)
169
- raise AlreadyPickedError, "#{player} was already picked" if drafted_players.include?(event.player)
170
- raise DupeEventError, "#{event} is a dupe" if events.include?(event)
171
- raise EventOutOfOrder, "#{event} team cant pick right now" if event.team != current_team
172
- raise EventOutOfOrder, "#{event} has wrong pick" if event.pick != current_pick
173
- raise EventOutOfOrder, "#{event} has wrong round" if event.round != current_round
174
- raise EventOutOfOrder, "#{event} has wrong round_pick" if event.round_pick != current_round_pick
175
- raise UnknownTeamError, "#{team} doesnt exist" unless teams.include?(event.team)
176
- raise UnknownPlayerError, "#{player} doesnt exist" unless players.include?(event.player)
177
- raise EndOfDraftError, "#{total_picks} pick limit reached" if events.length > total_picks + 1
195
+ if event.respond_to?(:player) && drafted_players.include?(event.player)
196
+ raise AlreadyPickedError, "#{player} was already picked"
197
+ end
198
+
199
+ if event.respond_to?(:player) && !players.include?(event.player)
200
+ raise UnknownPlayerError, "#{event.player} doesnt exist"
201
+ end
202
+
203
+ raise DupeEventError, "#{event} is a dupe" if events.include?(event)
204
+ raise EventOutOfOrder, "#{event} team cant pick right now" if event.team != current_team
205
+ raise EventOutOfOrder, "#{event} has wrong pick" if event.pick != current_pick
206
+ raise EventOutOfOrder, "#{event} has wrong round" if event.round != current_round
207
+ raise EventOutOfOrder, "#{event} has wrong round_pick" if event.round_pick != current_round_pick
208
+ raise UnknownTeamError, "#{team} doesnt exist" unless teams.include?(event.team)
209
+ raise EndOfDraftError, "#{total_picks} pick limit reached" if events.length > total_picks + 1
178
210
 
179
211
  events << event
180
212
 
@@ -5,13 +5,15 @@ require_relative 'player'
5
5
  require_relative 'team'
6
6
  require_relative 'pick_event'
7
7
  require_relative 'sim_event'
8
+ require_relative 'skip_event'
8
9
 
9
10
  module Basketball
10
11
  module Drafting
11
12
  class EngineSerializer
12
13
  EVENT_CLASSES = {
13
14
  'PickEvent' => PickEvent,
14
- 'SimEvent' => SimEvent
15
+ 'SimEvent' => SimEvent,
16
+ 'SkipEvent' => SkipEvent
15
17
  }.freeze
16
18
 
17
19
  private_constant :EVENT_CLASSES
@@ -71,7 +73,7 @@ module Basketball
71
73
  roster.id,
72
74
  {
73
75
  events: roster.events.map(&:id),
74
- players: roster.events.map { |event| event.player.id }
76
+ players: roster.players.map(&:id)
75
77
  }
76
78
  ]
77
79
  end
@@ -101,7 +103,7 @@ module Basketball
101
103
  first_name: player.first_name,
102
104
  last_name: player.last_name,
103
105
  overall: player.overall,
104
- position: player.position.value
106
+ position: player.position.code
105
107
  }
106
108
  ]
107
109
  end
@@ -112,12 +114,13 @@ module Basketball
112
114
  {
113
115
  type: event.class.name.split('::').last,
114
116
  id: event.id,
115
- player: event.player.id,
116
117
  team: event.team.id,
117
118
  pick: event.pick,
118
119
  round: event.round,
119
120
  round_pick: event.round_pick
120
- }
121
+ }.tap do |hash|
122
+ hash[:player] = event.player.id if event.respond_to?(:player)
123
+ end
121
124
  end
122
125
  end
123
126
 
@@ -162,12 +165,15 @@ module Basketball
162
165
  def deserialize_events(json, players, teams)
163
166
  (json.dig(:engine, :events) || []).map do |event_hash|
164
167
  event_opts = event_hash.slice(:id, :pick, :round, :round_pick).merge(
165
- player: players.find { |p| p.id == event_hash[:player] },
166
168
  team: teams.find { |t| t.id == event_hash[:team] }
167
169
  )
168
170
 
169
171
  class_constant = EVENT_CLASSES.fetch(event_hash[:type])
170
172
 
173
+ if [PickEvent, SimEvent].include?(class_constant)
174
+ event_opts[:player] = players.find { |p| p.id == event_hash[:player] }
175
+ end
176
+
171
177
  class_constant.new(**event_opts)
172
178
  end
173
179
  end
@@ -1,7 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative 'entity'
4
-
5
3
  module Basketball
6
4
  module Drafting
7
5
  class Event < Entity
@@ -2,16 +2,18 @@
2
2
 
3
3
  module Basketball
4
4
  module Drafting
5
- class FrontOffice
5
+ class FrontOffice < ValueObject
6
6
  MAX_DEPTH = 3
7
7
  MAX_FUZZ = 2
8
8
  MAX_POSITIONS = 12
9
9
 
10
10
  private_constant :MAX_DEPTH, :MAX_FUZZ, :MAX_POSITIONS
11
11
 
12
- attr_reader :prioritized_positions, :fuzz, :depth
12
+ attr_reader_value :prioritized_positions, :fuzz, :depth
13
13
 
14
14
  def initialize(prioritized_positions: [], fuzz: rand(0..MAX_FUZZ), depth: rand(0..MAX_DEPTH))
15
+ super()
16
+
15
17
  @fuzz = fuzz.to_i
16
18
  @depth = depth.to_i
17
19
  @prioritized_positions = prioritized_positions
@@ -24,44 +26,36 @@ module Basketball
24
26
  freeze
25
27
  end
26
28
 
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:)
29
+ def pick(undrafted_player_search:, drafted_players:, round:)
32
30
  players = []
33
31
 
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?
32
+ players = adaptive_search(undrafted_player_search:, drafted_players:) if depth >= round
33
+ players = balanced_search(undrafted_player_search:, drafted_players:) if players.empty?
34
+ players = top_players(undrafted_player_search:) if players.empty?
37
35
 
38
36
  players[0..fuzz].sample
39
37
  end
40
38
 
41
39
  private
42
40
 
43
- def adaptive_search(undrafted_players:, drafted_players:)
44
- search = PlayerSearch.new(undrafted_players)
45
-
41
+ def adaptive_search(undrafted_player_search:, drafted_players:)
46
42
  drafted_positions = drafted_players.map(&:position)
47
43
 
48
- search.query(exclude_positions: drafted_positions)
44
+ undrafted_player_search.query(exclude_positions: drafted_positions)
49
45
  end
50
46
 
51
- def balanced_search(undrafted_players:, drafted_players:)
52
- search = PlayerSearch.new(undrafted_players)
53
-
47
+ def balanced_search(undrafted_player_search:, drafted_players:)
54
48
  players = []
55
49
 
56
50
  # Try to find best pick for exact desired position.
57
51
  # If you cant find one, then move to the next desired position until the end of the queue
58
52
  available_prioritized_positions(drafted_players:).each do |position|
59
- players = search.query(position:)
53
+ players = undrafted_player_search.query(position:)
60
54
 
61
55
  break if players.any?
62
56
  end
63
57
 
64
- players = players.any? ? players : search.query
58
+ players = players.any? ? players : undrafted_player_search.query
65
59
  end
66
60
 
67
61
  def all_random_positions
@@ -1,7 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative 'entity'
4
-
5
3
  module Basketball
6
4
  module Drafting
7
5
  class Player < Entity
@@ -2,7 +2,7 @@
2
2
 
3
3
  module Basketball
4
4
  module Drafting
5
- class Position
5
+ class Position < ValueObject
6
6
  extend Forwardable
7
7
 
8
8
  class << self
@@ -17,25 +17,18 @@ module Basketball
17
17
  FRONT_COURT_VALUES = %w[PF C].to_set.freeze
18
18
  ALL_VALUES = (BACK_COURT_VALUES.to_a + FRONT_COURT_VALUES.to_a).to_set.freeze
19
19
 
20
- attr_reader :value
20
+ attr_reader_value :code
21
21
 
22
- def_delegators :value, :to_s
22
+ def_delegators :code, :to_s
23
23
 
24
- def initialize(value)
25
- @value = value.to_s.upcase
24
+ def initialize(code)
25
+ super()
26
26
 
27
- raise InvalidPositionError, "Unknown position value: #{@value}" unless ALL_VALUES.include?(@value)
27
+ @code = code.to_s.upcase
28
28
 
29
- freeze
30
- end
29
+ raise InvalidPositionError, "Unknown position code: #{@code}" unless ALL_VALUES.include?(@code)
31
30
 
32
- def ==(other)
33
- value == other.value
34
- end
35
- alias eql? ==
36
-
37
- def hash
38
- value.hash
31
+ freeze
39
32
  end
40
33
  end
41
34
  end
@@ -2,16 +2,18 @@
2
2
 
3
3
  module Basketball
4
4
  module Drafting
5
- class Roster
5
+ class Roster < ValueObject
6
6
  extend Forwardable
7
7
 
8
8
  class WrongTeamEventError < StandardError; end
9
9
 
10
- attr_reader :team, :events
10
+ attr_reader_value :team, :events
11
11
 
12
12
  def_delegators :team, :id
13
13
 
14
14
  def initialize(team:, events: [])
15
+ super()
16
+
15
17
  raise ArgumentError, 'team is required' unless team
16
18
 
17
19
  other_teams_pick_event_ids = events.reject { |e| e.team == team }.map(&:id)
@@ -25,8 +27,12 @@ module Basketball
25
27
  @events = events
26
28
  end
27
29
 
30
+ def player_events
31
+ events.select { |e| e.respond_to?(:player) }
32
+ end
33
+
28
34
  def players
29
- events.map(&:player)
35
+ player_events.map(&:player)
30
36
  end
31
37
 
32
38
  def to_s
@@ -1,7 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative 'event'
4
-
5
3
  module Basketball
6
4
  module Drafting
7
5
  class SimEvent < Event
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'event'
4
+
5
+ module Basketball
6
+ module Drafting
7
+ class SkipEvent < Event
8
+ def to_s
9
+ "skipped #{super}"
10
+ end
11
+ end
12
+ end
13
+ end
@@ -1,12 +1,14 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative 'entity'
4
-
5
3
  module Basketball
6
4
  module Drafting
7
5
  class Team < Entity
6
+ extend Forwardable
7
+
8
8
  attr_reader :name, :front_office
9
9
 
10
+ def_delegators :front_office, :pick
11
+
10
12
  def initialize(id:, name: '', front_office: FrontOffice.new)
11
13
  super(id)
12
14
 
@@ -21,10 +23,6 @@ module Basketball
21
23
  def to_s
22
24
  "[#{super}] #{name}"
23
25
  end
24
-
25
- def pick(undrafted_players:, drafted_players:, round:)
26
- front_office.pick(undrafted_players:, drafted_players:, round:)
27
- end
28
26
  end
29
27
  end
30
28
  end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Basketball
4
+ class Entity
5
+ extend Forwardable
6
+ include Comparable
7
+
8
+ attr_reader :id
9
+
10
+ def_delegators :id, :to_s
11
+
12
+ def initialize(id)
13
+ raise ArgumentError, 'id is required' if id.to_s.empty?
14
+
15
+ @id = id.to_s.upcase
16
+ end
17
+
18
+ def <=>(other)
19
+ id <=> other.id
20
+ end
21
+
22
+ def ==(other)
23
+ id == other.id
24
+ end
25
+ alias eql? ==
26
+
27
+ def hash
28
+ id.hash
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,53 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Basketball
4
+ class ValueObject
5
+ include Comparable
6
+
7
+ # NOTE: This current implementation most likely does not work for deep inheritance trees.
8
+ class << self
9
+ def value_keys
10
+ @value_keys ||= []
11
+ end
12
+
13
+ def sorted_value_keys
14
+ value_keys.sort
15
+ end
16
+
17
+ def attr_reader_value(*keys)
18
+ keys.each { |k| value_keys << k.to_sym }
19
+
20
+ attr_reader(*keys)
21
+ end
22
+ end
23
+
24
+ def to_s
25
+ to_h.map { |k, v| "#{k}: #{v}" }.join(', ')
26
+ end
27
+
28
+ def to_h
29
+ self.class.sorted_value_keys.to_h { |k| [k, send(k)] }
30
+ end
31
+
32
+ def [](key)
33
+ to_h[key]
34
+ end
35
+
36
+ def <=>(other)
37
+ all_sorted_values <=> other.all_sorted_values
38
+ end
39
+
40
+ def ==(other)
41
+ to_h == other.to_h
42
+ end
43
+ alias eql? ==
44
+
45
+ def hash
46
+ all_sorted_values.map(&:hash).hash
47
+ end
48
+
49
+ def all_sorted_values
50
+ self.class.sorted_value_keys.map { |k| self[k] }
51
+ end
52
+ end
53
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Basketball
4
- VERSION = '0.0.2'
4
+ VERSION = '0.0.4'
5
5
  end
data/lib/basketball.rb CHANGED
@@ -7,4 +7,9 @@ require 'json'
7
7
  require 'securerandom'
8
8
  require 'slop'
9
9
 
10
+ # Top-level
11
+ require_relative 'basketball/entity'
12
+ require_relative 'basketball/value_object'
13
+
14
+ # Submodules
10
15
  require_relative 'basketball/drafting'
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: basketball
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.2
4
+ version: 0.0.4
5
5
  platform: ruby
6
6
  authors:
7
7
  - Matthew Ruggio
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2023-05-04 00:00:00.000000000 Z
11
+ date: 2023-05-05 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: faker
@@ -207,7 +207,6 @@ files:
207
207
  - lib/basketball/drafting/cli.rb
208
208
  - lib/basketball/drafting/engine.rb
209
209
  - lib/basketball/drafting/engine_serializer.rb
210
- - lib/basketball/drafting/entity.rb
211
210
  - lib/basketball/drafting/event.rb
212
211
  - lib/basketball/drafting/front_office.rb
213
212
  - lib/basketball/drafting/pick_event.rb
@@ -216,7 +215,10 @@ files:
216
215
  - lib/basketball/drafting/position.rb
217
216
  - lib/basketball/drafting/roster.rb
218
217
  - lib/basketball/drafting/sim_event.rb
218
+ - lib/basketball/drafting/skip_event.rb
219
219
  - lib/basketball/drafting/team.rb
220
+ - lib/basketball/entity.rb
221
+ - lib/basketball/value_object.rb
220
222
  - lib/basketball/version.rb
221
223
  homepage: https://github.com/mattruggio/basketball
222
224
  licenses:
@@ -1,33 +0,0 @@
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