sashite-pin 3.2.0 → 4.0.0

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.
@@ -1,441 +1,464 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative "constants"
4
+ require_relative "errors"
5
+
3
6
  module Sashite
4
7
  module Pin
5
- # Represents an identifier in PIN (Piece Identifier Notation) format.
8
+ # Represents a parsed PIN (Piece Identifier Notation) identifier.
9
+ #
10
+ # An Identifier encodes four attributes of a piece:
11
+ # - Type: the piece type (A-Z as uppercase symbol)
12
+ # - Side: the player side (:first or :second)
13
+ # - State: the piece state (:normal, :enhanced, or :diminished)
14
+ # - Terminal: whether the piece is terminal (true or false)
6
15
  #
7
- # An identifier consists of a single ASCII letter with optional state modifiers:
8
- # - Enhanced state: prefix '+'
9
- # - Diminished state: prefix '-'
10
- # - Normal state: no modifier
16
+ # Instances are immutable (frozen after creation).
11
17
  #
12
- # The case of the letter determines ownership:
13
- # - Uppercase (A-Z): first player
14
- # - Lowercase (a-z): second player
18
+ # @example Creating identifiers
19
+ # pin = Identifier.new(:K, :first)
20
+ # pin = Identifier.new(:R, :second, :enhanced)
21
+ # pin = Identifier.new(:K, :first, :normal, terminal: true)
15
22
  #
16
- # All instances are immutable - state manipulation methods return new instances.
17
- # This follows the Game Protocol's piece model with Type, Side, and State attributes.
23
+ # @example String conversion
24
+ # Identifier.new(:K, :first).to_s # => "K"
25
+ # Identifier.new(:R, :second, :enhanced).to_s # => "+r"
26
+ # Identifier.new(:K, :first, :normal, terminal: true).to_s # => "K^"
27
+ #
28
+ # @see https://sashite.dev/specs/pin/1.0.0/
18
29
  class Identifier
19
- # PIN validation pattern matching the specification
20
- PIN_PATTERN = /\A(?<prefix>[-+])?(?<letter>[a-zA-Z])(?<terminal>\^)?\z/
21
-
22
- # Valid state modifiers
23
- ENHANCED_PREFIX = "+"
24
- DIMINISHED_PREFIX = "-"
25
- NORMAL_PREFIX = ""
26
-
27
- # Terminal marker
28
- TERMINAL_MARKER = "^"
29
-
30
- # State constants
31
- ENHANCED_STATE = :enhanced
32
- DIMINISHED_STATE = :diminished
33
- NORMAL_STATE = :normal
34
-
35
- # Player side constants
36
- FIRST_PLAYER = :first
37
- SECOND_PLAYER = :second
38
-
39
- # Valid types (A-Z)
40
- VALID_TYPES = (:A..:Z).to_a.freeze
41
-
42
- # Valid sides
43
- VALID_SIDES = [FIRST_PLAYER, SECOND_PLAYER].freeze
44
-
45
- # Valid states
46
- VALID_STATES = [NORMAL_STATE, ENHANCED_STATE, DIMINISHED_STATE].freeze
47
-
48
- # Error messages
49
- ERROR_INVALID_PIN = "Invalid PIN string: %s"
50
- ERROR_INVALID_TYPE = "Type must be a symbol from :A to :Z, got: %s"
51
- ERROR_INVALID_SIDE = "Side must be :first or :second, got: %s"
52
- ERROR_INVALID_STATE = "State must be :normal, :enhanced, or :diminished, got: %s"
53
-
54
- # @return [Symbol] the piece type (:A to :Z)
30
+ # @return [Symbol] Piece type (:A to :Z, always uppercase)
55
31
  attr_reader :type
56
32
 
57
- # @return [Symbol] the player side (:first or :second)
33
+ # @return [Symbol] Player side (:first or :second)
58
34
  attr_reader :side
59
35
 
60
- # @return [Symbol] the piece state (:normal, :enhanced, or :diminished)
36
+ # @return [Symbol] Piece state (:normal, :enhanced, or :diminished)
61
37
  attr_reader :state
62
38
 
63
- # @return [Boolean] whether the piece is a terminal piece
64
- attr_reader :terminal
65
-
66
- # Create a new identifier instance
39
+ # Creates a new Identifier instance.
40
+ #
41
+ # @param type [Symbol] Piece type (:A to :Z)
42
+ # @param side [Symbol] Player side (:first or :second)
43
+ # @param state [Symbol] Piece state (:normal, :enhanced, or :diminished)
44
+ # @param terminal [Boolean] Terminal status
45
+ # @return [Identifier] A new frozen Identifier instance
46
+ # @raise [Errors::Argument] If any attribute is invalid
67
47
  #
68
- # @param type [Symbol] piece type (:A to :Z)
69
- # @param side [Symbol] player side (:first or :second)
70
- # @param state [Symbol] piece state (:normal, :enhanced, or :diminished)
71
- # @param terminal [Boolean] whether the piece is a terminal piece
72
- # @raise [ArgumentError] if parameters are invalid
73
- def initialize(type, side, state = NORMAL_STATE, terminal: false)
74
- self.class.validate_type(type)
75
- self.class.validate_side(side)
76
- self.class.validate_state(state)
48
+ # @example
49
+ # Identifier.new(:K, :first)
50
+ # Identifier.new(:R, :second, :enhanced)
51
+ # Identifier.new(:K, :first, :normal, terminal: true)
52
+ def initialize(type, side, state = :normal, terminal: false)
53
+ validate_type!(type)
54
+ validate_side!(side)
55
+ validate_state!(state)
56
+ validate_terminal!(terminal)
77
57
 
78
58
  @type = type
79
59
  @side = side
80
60
  @state = state
81
- @terminal = !!terminal
61
+ @terminal = terminal
82
62
 
83
63
  freeze
84
64
  end
85
65
 
86
- # Parse a PIN string into an Identifier object
66
+ # Returns the terminal status.
87
67
  #
88
- # @param pin_string [String] PIN notation string
89
- # @return [Identifier] new identifier instance
90
- # @raise [ArgumentError] if the PIN string is invalid
91
- # @example
92
- # Pin::Identifier.parse("k") # => #<Pin::Identifier type=:K side=:second state=:normal terminal=false>
93
- # Pin::Identifier.parse("+R") # => #<Pin::Identifier type=:R side=:first state=:enhanced terminal=false>
94
- # Pin::Identifier.parse("-p") # => #<Pin::Identifier type=:P side=:second state=:diminished terminal=false>
95
- # Pin::Identifier.parse("K^") # => #<Pin::Identifier type=:K side=:first state=:normal terminal=true>
96
- # Pin::Identifier.parse("+K^") # => #<Pin::Identifier type=:K side=:first state=:enhanced terminal=true>
97
- def self.parse(pin_string)
98
- string_value = String(pin_string)
99
- matches = match_pattern(string_value)
100
-
101
- letter = matches[:letter]
102
- enhanced = matches[:prefix] == ENHANCED_PREFIX
103
- diminished = matches[:prefix] == DIMINISHED_PREFIX
104
- is_terminal = matches[:terminal] == TERMINAL_MARKER
105
-
106
- type = letter.upcase.to_sym
107
- side = letter == letter.upcase ? FIRST_PLAYER : SECOND_PLAYER
108
- state = if enhanced
109
- ENHANCED_STATE
110
- elsif diminished
111
- DIMINISHED_STATE
112
- else
113
- NORMAL_STATE
114
- end
115
-
116
- new(type, side, state, terminal: is_terminal)
117
- end
118
-
119
- # Check if a string is a valid PIN notation
120
- #
121
- # @param pin_string [String] The string to validate
122
- # @return [Boolean] true if valid PIN, false otherwise
68
+ # @return [Boolean] true if terminal piece, false otherwise
123
69
  #
124
70
  # @example
125
- # Sashite::Pin::Identifier.valid?("K") # => true
126
- # Sashite::Pin::Identifier.valid?("+R") # => true
127
- # Sashite::Pin::Identifier.valid?("-p") # => true
128
- # Sashite::Pin::Identifier.valid?("KK") # => false
129
- # Sashite::Pin::Identifier.valid?("++K") # => false
130
- def self.valid?(pin_string)
131
- return false unless pin_string.is_a?(::String)
132
-
133
- pin_string.match?(PIN_PATTERN)
71
+ # Identifier.new(:K, :first).terminal? # => false
72
+ # Identifier.new(:K, :first, :normal, terminal: true).terminal? # => true
73
+ def terminal?
74
+ @terminal
134
75
  end
135
76
 
136
- # Convert the identifier to its PIN string representation
77
+ # ========================================================================
78
+ # String Conversion
79
+ # ========================================================================
80
+
81
+ # Returns the PIN string representation.
82
+ #
83
+ # @return [String] The PIN string
137
84
  #
138
- # @return [String] PIN notation string
139
85
  # @example
140
- # identifier.to_s # => "+R"
141
- # terminal_king.to_s # => "K^"
142
- # enhanced_terminal.to_s # => "+K^"
86
+ # Identifier.new(:K, :first).to_s # => "K"
87
+ # Identifier.new(:R, :second, :enhanced).to_s # => "+r"
88
+ # Identifier.new(:K, :first, :normal, terminal: true).to_s # => "K^"
143
89
  def to_s
144
90
  "#{prefix}#{letter}#{suffix}"
145
91
  end
146
92
 
147
- # Get the letter representation
93
+ # Returns the letter component of the PIN.
94
+ #
95
+ # @return [String] Uppercase for first player, lowercase for second
148
96
  #
149
- # @return [String] letter representation combining type and side
97
+ # @example
98
+ # Identifier.new(:K, :first).letter # => "K"
99
+ # Identifier.new(:K, :second).letter # => "k"
150
100
  def letter
151
- first_player? ? type.to_s.upcase : type.to_s.downcase
101
+ case side
102
+ when :first then String(type.upcase)
103
+ when :second then String(type.downcase)
104
+ end
152
105
  end
153
106
 
154
- # Get the prefix representation
107
+ # Returns the state prefix of the PIN.
108
+ #
109
+ # @return [String] "+" for enhanced, "-" for diminished, "" for normal
155
110
  #
156
- # @return [String] prefix representing the state
111
+ # @example
112
+ # Identifier.new(:K, :first, :enhanced).prefix # => "+"
113
+ # Identifier.new(:K, :first, :diminished).prefix # => "-"
114
+ # Identifier.new(:K, :first, :normal).prefix # => ""
157
115
  def prefix
158
116
  case state
159
- when ENHANCED_STATE then ENHANCED_PREFIX
160
- when DIMINISHED_STATE then DIMINISHED_PREFIX
161
- else NORMAL_PREFIX
117
+ when :enhanced then Constants::ENHANCED_PREFIX
118
+ when :diminished then Constants::DIMINISHED_PREFIX
119
+ else Constants::EMPTY_STRING
162
120
  end
163
121
  end
164
122
 
165
- # Get the suffix representation
123
+ # Returns the terminal suffix of the PIN.
124
+ #
125
+ # @return [String] "^" if terminal, "" otherwise
166
126
  #
167
- # @return [String] suffix representing terminal status
127
+ # @example
128
+ # Identifier.new(:K, :first, :normal, terminal: true).suffix # => "^"
129
+ # Identifier.new(:K, :first).suffix # => ""
168
130
  def suffix
169
- terminal? ? TERMINAL_MARKER : ""
131
+ terminal? ? Constants::TERMINAL_SUFFIX : Constants::EMPTY_STRING
170
132
  end
171
133
 
172
- # Create a new identifier with enhanced state
134
+ # ========================================================================
135
+ # State Transformations
136
+ # ========================================================================
137
+
138
+ # Returns a new Identifier with enhanced state.
139
+ #
140
+ # @return [Identifier] A new Identifier with :enhanced state
173
141
  #
174
- # @return [Identifier] new identifier instance with enhanced state
142
+ # @example
143
+ # pin = Identifier.new(:K, :first)
144
+ # pin.enhance.to_s # => "+K"
175
145
  def enhance
176
146
  return self if enhanced?
177
147
 
178
- self.class.new(type, side, ENHANCED_STATE, terminal: terminal)
148
+ self.class.new(type, side, :enhanced, terminal: terminal?)
179
149
  end
180
150
 
181
- # Create a new identifier without enhanced state
151
+ # Returns a new Identifier with diminished state.
182
152
  #
183
- # @return [Identifier] new identifier instance with normal state
184
- def unenhance
185
- return self unless enhanced?
186
-
187
- self.class.new(type, side, NORMAL_STATE, terminal: terminal)
188
- end
189
-
190
- # Create a new identifier with diminished state
153
+ # @return [Identifier] A new Identifier with :diminished state
191
154
  #
192
- # @return [Identifier] new identifier instance with diminished state
155
+ # @example
156
+ # pin = Identifier.new(:K, :first)
157
+ # pin.diminish.to_s # => "-K"
193
158
  def diminish
194
159
  return self if diminished?
195
160
 
196
- self.class.new(type, side, DIMINISHED_STATE, terminal: terminal)
161
+ self.class.new(type, side, :diminished, terminal: terminal?)
197
162
  end
198
163
 
199
- # Create a new identifier without diminished state
164
+ # Returns a new Identifier with normal state.
200
165
  #
201
- # @return [Identifier] new identifier instance with normal state
202
- def undiminish
203
- return self unless diminished?
204
-
205
- self.class.new(type, side, NORMAL_STATE, terminal: terminal)
206
- end
207
-
208
- # Create a new identifier with normal state (no modifiers)
166
+ # @return [Identifier] A new Identifier with :normal state
209
167
  #
210
- # @return [Identifier] new identifier instance with normal state
168
+ # @example
169
+ # pin = Identifier.new(:K, :first, :enhanced)
170
+ # pin.normalize.to_s # => "K"
211
171
  def normalize
212
172
  return self if normal?
213
173
 
214
- self.class.new(type, side, NORMAL_STATE, terminal: terminal)
174
+ self.class.new(type, side, :normal, terminal: terminal?)
175
+ end
176
+
177
+ # ========================================================================
178
+ # Side Transformations
179
+ # ========================================================================
180
+
181
+ # Returns a new Identifier with the opposite side.
182
+ #
183
+ # @return [Identifier] A new Identifier with flipped side
184
+ #
185
+ # @example
186
+ # pin = Identifier.new(:K, :first)
187
+ # pin.flip.to_s # => "k"
188
+ def flip
189
+ new_side = first_player? ? :second : :first
190
+ self.class.new(type, new_side, state, terminal: terminal?)
215
191
  end
216
192
 
217
- # Create a new identifier marked as terminal
193
+ # ========================================================================
194
+ # Terminal Transformations
195
+ # ========================================================================
196
+
197
+ # Returns a new Identifier marked as terminal.
198
+ #
199
+ # @return [Identifier] A new Identifier with terminal: true
218
200
  #
219
- # @return [Identifier] new identifier instance marked as terminal
201
+ # @example
202
+ # pin = Identifier.new(:K, :first)
203
+ # pin.mark_terminal.to_s # => "K^"
220
204
  def mark_terminal
221
205
  return self if terminal?
222
206
 
223
207
  self.class.new(type, side, state, terminal: true)
224
208
  end
225
209
 
226
- # Create a new identifier unmarked as terminal
210
+ # Returns a new Identifier unmarked as terminal.
211
+ #
212
+ # @return [Identifier] A new Identifier with terminal: false
227
213
  #
228
- # @return [Identifier] new identifier instance unmarked as terminal
214
+ # @example
215
+ # pin = Identifier.new(:K, :first, :normal, terminal: true)
216
+ # pin.unmark_terminal.to_s # => "K"
229
217
  def unmark_terminal
230
218
  return self unless terminal?
231
219
 
232
220
  self.class.new(type, side, state, terminal: false)
233
221
  end
234
222
 
235
- # Create a new identifier with opposite side
236
- #
237
- # @return [Identifier] new identifier instance with opposite side
238
- def flip
239
- self.class.new(type, opposite_side, state, terminal: terminal)
240
- end
223
+ # ========================================================================
224
+ # Attribute Transformations
225
+ # ========================================================================
241
226
 
242
- # Create a new identifier with a different type
227
+ # Returns a new Identifier with a different type.
243
228
  #
244
- # @param new_type [Symbol] new type (:A to :Z)
245
- # @return [Identifier] new identifier instance with new type
229
+ # @param new_type [Symbol] The new piece type (:A to :Z)
230
+ # @return [Identifier] A new Identifier with the specified type
231
+ # @raise [Errors::Argument] If the type is invalid
232
+ #
233
+ # @example
234
+ # pin = Identifier.new(:K, :first)
235
+ # pin.with_type(:Q).to_s # => "Q"
246
236
  def with_type(new_type)
247
- self.class.validate_type(new_type)
248
- return self if type == new_type
237
+ return self if type.equal?(new_type)
249
238
 
250
- self.class.new(new_type, side, state, terminal: terminal)
239
+ self.class.new(new_type, side, state, terminal: terminal?)
251
240
  end
252
241
 
253
- # Create a new identifier with a different side
242
+ # Returns a new Identifier with a different side.
254
243
  #
255
- # @param new_side [Symbol] new side (:first or :second)
256
- # @return [Identifier] new identifier instance with new side
244
+ # @param new_side [Symbol] The new side (:first or :second)
245
+ # @return [Identifier] A new Identifier with the specified side
246
+ # @raise [Errors::Argument] If the side is invalid
247
+ #
248
+ # @example
249
+ # pin = Identifier.new(:K, :first)
250
+ # pin.with_side(:second).to_s # => "k"
257
251
  def with_side(new_side)
258
- self.class.validate_side(new_side)
259
- return self if side == new_side
252
+ return self if side.equal?(new_side)
260
253
 
261
- self.class.new(type, new_side, state, terminal: terminal)
254
+ self.class.new(type, new_side, state, terminal: terminal?)
262
255
  end
263
256
 
264
- # Create a new identifier with a different state
257
+ # Returns a new Identifier with a different state.
265
258
  #
266
- # @param new_state [Symbol] new state (:normal, :enhanced, or :diminished)
267
- # @return [Identifier] new identifier instance with new state
259
+ # @param new_state [Symbol] The new state (:normal, :enhanced, or :diminished)
260
+ # @return [Identifier] A new Identifier with the specified state
261
+ # @raise [Errors::Argument] If the state is invalid
262
+ #
263
+ # @example
264
+ # pin = Identifier.new(:K, :first)
265
+ # pin.with_state(:enhanced).to_s # => "+K"
268
266
  def with_state(new_state)
269
- self.class.validate_state(new_state)
270
- return self if state == new_state
267
+ return self if state.equal?(new_state)
271
268
 
272
- self.class.new(type, side, new_state, terminal: terminal)
269
+ self.class.new(type, side, new_state, terminal: terminal?)
273
270
  end
274
271
 
275
- # Create a new identifier with a different terminal status
272
+ # Returns a new Identifier with a different terminal status.
273
+ #
274
+ # @param new_terminal [Boolean] The new terminal status
275
+ # @return [Identifier] A new Identifier with the specified terminal status
276
+ # @raise [Errors::Argument] If the terminal is not a boolean
276
277
  #
277
- # @param new_terminal [Boolean] new terminal status
278
- # @return [Identifier] new identifier instance with new terminal status
278
+ # @example
279
+ # pin = Identifier.new(:K, :first)
280
+ # pin.with_terminal(true).to_s # => "K^"
279
281
  def with_terminal(new_terminal)
280
- new_terminal_bool = !!new_terminal
281
- return self if terminal? == new_terminal_bool
282
+ return self if terminal?.equal?(new_terminal)
282
283
 
283
- self.class.new(type, side, state, terminal: new_terminal_bool)
284
+ self.class.new(type, side, state, terminal: new_terminal)
284
285
  end
285
286
 
286
- # Check if the identifier has enhanced state
287
+ # ========================================================================
288
+ # State Queries
289
+ # ========================================================================
290
+
291
+ # Checks if the Identifier has normal state.
287
292
  #
288
- # @return [Boolean] true if enhanced
289
- def enhanced?
290
- state == ENHANCED_STATE
293
+ # @return [Boolean] true if normal state
294
+ #
295
+ # @example
296
+ # Identifier.new(:K, :first).normal? # => true
297
+ def normal?
298
+ state.equal?(:normal)
291
299
  end
292
300
 
293
- # Check if the identifier has diminished state
301
+ # Checks if the Identifier has enhanced state.
294
302
  #
295
- # @return [Boolean] true if diminished
296
- def diminished?
297
- state == DIMINISHED_STATE
303
+ # @return [Boolean] true if enhanced state
304
+ #
305
+ # @example
306
+ # Identifier.new(:K, :first, :enhanced).enhanced? # => true
307
+ def enhanced?
308
+ state.equal?(:enhanced)
298
309
  end
299
310
 
300
- # Check if the identifier has normal state
311
+ # Checks if the Identifier has diminished state.
301
312
  #
302
- # @return [Boolean] true if normal
303
- def normal?
304
- state == NORMAL_STATE
313
+ # @return [Boolean] true if diminished state
314
+ #
315
+ # @example
316
+ # Identifier.new(:K, :first, :diminished).diminished? # => true
317
+ def diminished?
318
+ state.equal?(:diminished)
305
319
  end
306
320
 
307
- # Check if the identifier belongs to the first player
321
+ # ========================================================================
322
+ # Side Queries
323
+ # ========================================================================
324
+
325
+ # Checks if the Identifier belongs to the first player.
308
326
  #
309
327
  # @return [Boolean] true if first player
328
+ #
329
+ # @example
330
+ # Identifier.new(:K, :first).first_player? # => true
310
331
  def first_player?
311
- side == FIRST_PLAYER
332
+ side.equal?(:first)
312
333
  end
313
334
 
314
- # Check if the identifier belongs to the second player
335
+ # Checks if the Identifier belongs to the second player.
315
336
  #
316
337
  # @return [Boolean] true if second player
338
+ #
339
+ # @example
340
+ # Identifier.new(:K, :second).second_player? # => true
317
341
  def second_player?
318
- side == SECOND_PLAYER
342
+ side.equal?(:second)
319
343
  end
320
344
 
321
- # Check if the identifier is a terminal piece
322
- #
323
- # @return [Boolean] true if terminal
324
- def terminal?
325
- terminal
326
- end
345
+ # ========================================================================
346
+ # Comparison Queries
347
+ # ========================================================================
327
348
 
328
- # Check if this identifier is the same type as another
349
+ # Checks if two Identifiers have the same type.
329
350
  #
330
- # @param other [Identifier] identifier to compare with
351
+ # @param other [Identifier] The other Identifier to compare
331
352
  # @return [Boolean] true if same type
353
+ #
354
+ # @example
355
+ # pin1 = Identifier.new(:K, :first)
356
+ # pin2 = Identifier.new(:K, :second)
357
+ # pin1.same_type?(pin2) # => true
332
358
  def same_type?(other)
333
- return false unless other.is_a?(self.class)
334
-
335
- type == other.type
359
+ type.equal?(other.type)
336
360
  end
337
361
 
338
- # Check if this identifier has the same side as another
362
+ # Checks if two Identifiers have the same side.
339
363
  #
340
- # @param other [Identifier] identifier to compare with
364
+ # @param other [Identifier] The other Identifier to compare
341
365
  # @return [Boolean] true if same side
366
+ #
367
+ # @example
368
+ # pin1 = Identifier.new(:K, :first)
369
+ # pin2 = Identifier.new(:Q, :first)
370
+ # pin1.same_side?(pin2) # => true
342
371
  def same_side?(other)
343
- return false unless other.is_a?(self.class)
344
-
345
- side == other.side
372
+ side.equal?(other.side)
346
373
  end
347
374
 
348
- # Check if this identifier has the same state as another
375
+ # Checks if two Identifiers have the same state.
349
376
  #
350
- # @param other [Identifier] identifier to compare with
377
+ # @param other [Identifier] The other Identifier to compare
351
378
  # @return [Boolean] true if same state
379
+ #
380
+ # @example
381
+ # pin1 = Identifier.new(:K, :first, :enhanced)
382
+ # pin2 = Identifier.new(:Q, :second, :enhanced)
383
+ # pin1.same_state?(pin2) # => true
352
384
  def same_state?(other)
353
- return false unless other.is_a?(self.class)
354
-
355
- state == other.state
385
+ state.equal?(other.state)
356
386
  end
357
387
 
358
- # Check if this identifier has the same terminal status as another
388
+ # Checks if two Identifiers have the same terminal status.
359
389
  #
360
- # @param other [Identifier] identifier to compare with
390
+ # @param other [Identifier] The other Identifier to compare
361
391
  # @return [Boolean] true if same terminal status
392
+ #
393
+ # @example
394
+ # pin1 = Identifier.new(:K, :first, :normal, terminal: true)
395
+ # pin2 = Identifier.new(:Q, :second, :normal, terminal: true)
396
+ # pin1.same_terminal?(pin2) # => true
362
397
  def same_terminal?(other)
363
- return false unless other.is_a?(self.class)
364
-
365
- terminal? == other.terminal?
398
+ terminal?.equal?(other.terminal?)
366
399
  end
367
400
 
368
- # Custom equality comparison
401
+ # ========================================================================
402
+ # Equality
403
+ # ========================================================================
404
+
405
+ # Checks equality with another Identifier.
369
406
  #
370
- # @param other [Object] object to compare with
371
- # @return [Boolean] true if identifiers are equal
407
+ # @param other [Object] The object to compare
408
+ # @return [Boolean] true if equal
372
409
  def ==(other)
373
- return false unless other.is_a?(self.class)
410
+ return false unless self.class === other
374
411
 
375
- type == other.type && side == other.side && state == other.state && terminal? == other.terminal?
412
+ type.equal?(other.type) &&
413
+ side.equal?(other.side) &&
414
+ state.equal?(other.state) &&
415
+ terminal?.equal?(other.terminal?)
376
416
  end
377
417
 
378
- # Alias for == to ensure Set functionality works correctly
379
418
  alias eql? ==
380
419
 
381
- # Custom hash implementation for use in collections
420
+ # Returns a hash code for the Identifier.
382
421
  #
383
- # @return [Integer] hash value
422
+ # @return [Integer] Hash code
384
423
  def hash
385
- [self.class, type, side, state, terminal?].hash
424
+ [type, side, state, terminal?].hash
386
425
  end
387
426
 
388
- # Validate that the type is a valid symbol
427
+ # Returns an inspect string for the Identifier.
389
428
  #
390
- # @param type [Symbol] the type to validate
391
- # @raise [ArgumentError] if invalid
392
- def self.validate_type(type)
393
- return if VALID_TYPES.include?(type)
394
-
395
- raise ::ArgumentError, format(ERROR_INVALID_TYPE, type.inspect)
429
+ # @return [String] Inspect representation
430
+ def inspect
431
+ "#<#{self.class} #{self}>"
396
432
  end
397
433
 
398
- # Validate that the side is a valid symbol
399
- #
400
- # @param side [Symbol] the side to validate
401
- # @raise [ArgumentError] if invalid
402
- def self.validate_side(side)
403
- return if VALID_SIDES.include?(side)
434
+ private
404
435
 
405
- raise ::ArgumentError, format(ERROR_INVALID_SIDE, side.inspect)
406
- end
436
+ # ========================================================================
437
+ # Private Validation
438
+ # ========================================================================
407
439
 
408
- # Validate that the state is a valid symbol
409
- #
410
- # @param state [Symbol] the state to validate
411
- # @raise [ArgumentError] if invalid
412
- def self.validate_state(state)
413
- return if VALID_STATES.include?(state)
440
+ def validate_type!(type)
441
+ return if Constants::VALID_TYPES.include?(type)
414
442
 
415
- raise ::ArgumentError, format(ERROR_INVALID_STATE, state.inspect)
443
+ raise Errors::Argument, Errors::Argument::Messages::INVALID_TYPE
416
444
  end
417
445
 
418
- # Match PIN pattern against string
419
- #
420
- # @param string [String] string to match
421
- # @return [MatchData] match data
422
- # @raise [ArgumentError] if string doesn't match
423
- def self.match_pattern(string)
424
- matches = PIN_PATTERN.match(string)
425
- return matches if matches
446
+ def validate_side!(side)
447
+ return if Constants::VALID_SIDES.include?(side)
426
448
 
427
- raise ::ArgumentError, format(ERROR_INVALID_PIN, string)
449
+ raise Errors::Argument, Errors::Argument::Messages::INVALID_SIDE
428
450
  end
429
451
 
430
- private_class_method :match_pattern
452
+ def validate_state!(state)
453
+ return if Constants::VALID_STATES.include?(state)
431
454
 
432
- private
455
+ raise Errors::Argument, Errors::Argument::Messages::INVALID_STATE
456
+ end
433
457
 
434
- # Get the opposite side of the current identifier
435
- #
436
- # @return [Symbol] :first if current side is :second, :second if current side is :first
437
- def opposite_side
438
- first_player? ? SECOND_PLAYER : FIRST_PLAYER
458
+ def validate_terminal!(terminal)
459
+ return if ::TrueClass === terminal || ::FalseClass === terminal
460
+
461
+ raise Errors::Argument, Errors::Argument::Messages::INVALID_TERMINAL
439
462
  end
440
463
  end
441
464
  end