sashite-epin 1.2.0 → 2.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.
@@ -6,553 +6,261 @@ module Sashite
6
6
  module Epin
7
7
  # Represents an identifier in EPIN (Extended Piece Identifier Notation) format.
8
8
  #
9
- # EPIN extends PIN by adding a derivation marker to encode Piece Style relative to Piece Side.
9
+ # EPIN extends PIN by adding a derivation marker to track piece style in cross-style games.
10
+ # An EPIN identifier is simply a PIN identifier plus a boolean flag indicating whether
11
+ # the piece uses its own side's native style (native) or the opponent's style (derived).
10
12
  #
11
- # Format: [<state>]<letter>[<terminal>][<derivation>]
12
- # - State modifier: "+" (enhanced), "-" (diminished), or none (normal)
13
- # - Letter: A-Z (first player), a-z (second player)
14
- # - Terminal marker: "^" (terminal piece)
15
- # - Derivation marker: "'" (foreign/derived style) or none (native style)
13
+ # ## Pure Composition Design
16
14
  #
17
- # Style Derivation Logic:
18
- # - No apostrophe suffix: piece has the native style of its current side
19
- # - Apostrophe suffix: piece has the foreign style (opposite side's native style)
15
+ # EPIN doesn't reimplement PIN features - it's pure composition:
16
+ # - All piece attributes (name, side, state, terminal) come from the PIN component
17
+ # - EPIN adds only style derivation tracking (native vs derived)
18
+ # - Zero code duplication
20
19
  #
21
- # All instances are immutable - transformations return new instances.
22
- # This extends the Game Protocol's piece model with Style support through derivation.
20
+ # ## Minimal API
23
21
  #
24
- # @see https://sashite.dev/specs/epin/1.0.0/
22
+ # Core methods (6 total):
23
+ # 1. new(pin, derived: false) - create from PIN component
24
+ # 2. pin - get PIN component
25
+ # 3. derived? - check derivation status
26
+ # 4. to_s - serialize
27
+ # 5. with_pin(new_pin) - replace PIN component
28
+ # 6. with_derived(boolean) - change derivation status
29
+ #
30
+ # Everything else uses the PIN component API directly.
31
+ #
32
+ # All instances are immutable - transformation methods return new instances.
33
+ #
34
+ # @example Basic usage
35
+ # # Create from PIN component
36
+ # pin = Sashite::Pin.parse("K^")
37
+ # epin = Sashite::Epin::Identifier.new(pin, derived: false)
38
+ # epin.to_s # => "K^" (native)
39
+ #
40
+ # # Mark as derived
41
+ # derived = epin.mark_derived
42
+ # derived.to_s # => "K^'" (derived from opponent's style)
43
+ #
44
+ # @example Accessing attributes via PIN component
45
+ # epin = Sashite::Epin.parse("+R^'")
46
+ # epin.pin.type # => :R (Piece Name)
47
+ # epin.pin.side # => :first (Piece Side)
48
+ # epin.pin.state # => :enhanced (Piece State)
49
+ # epin.pin.terminal? # => true (Terminal Status)
50
+ # epin.derived? # => true (Piece Style)
51
+ #
52
+ # @example Transformations
53
+ # epin = Sashite::Epin.parse("K^")
54
+ #
55
+ # # Transform PIN component
56
+ # epin.with_pin(epin.pin.with_type(:Q)) # => "Q^"
57
+ #
58
+ # # Transform derivation
59
+ # epin.mark_derived # => "K^'"
60
+ # epin.with_derived(true) # => "K^'"
61
+ #
62
+ # @see https://sashite.dev/specs/epin/1.0.0/ EPIN Specification v1.0.0
25
63
  class Identifier
26
- # EPIN validation pattern matching the specification: /\A[-+]?[A-Za-z]\^?'?\z/
64
+ # EPIN validation pattern matching the specification
65
+ # Grammar: <epin> ::= <pin> | <pin> "'"
27
66
  EPIN_PATTERN = /\A[-+]?[A-Za-z]\^?'?\z/
28
67
 
29
- # Derivation marker for foreign/derived style
68
+ # Derivation marker character
30
69
  DERIVATION_MARKER = "'"
31
70
 
32
- # No derivation marker (native style)
33
- NATIVE_MARKER = ""
34
-
35
- # Style constants
36
- NATIVE = true
37
- FOREIGN = false
38
-
39
- # Valid derivation values
40
- VALID_DERIVATIONS = [NATIVE, FOREIGN].freeze
41
-
42
71
  # Error messages
43
72
  ERROR_INVALID_EPIN = "Invalid EPIN string: %s"
44
- ERROR_INVALID_DERIVATION = "Derivation must be true (native) or false (foreign), got: %s"
45
-
46
- # @return [Symbol] the piece type (:A to :Z, always uppercase)
47
- def type
48
- @pin_identifier.type
49
- end
50
-
51
- # @return [Symbol] the player side (:first or :second)
52
- def side
53
- @pin_identifier.side
54
- end
73
+ ERROR_INVALID_PIN = "PIN component must be a Pin::Identifier, got: %s"
74
+ ERROR_MULTIPLE_MARKERS = "EPIN string cannot have multiple derivation markers: %s"
55
75
 
56
- # @return [Symbol] the piece state (:normal, :enhanced, or :diminished)
57
- def state
58
- @pin_identifier.state
59
- end
76
+ # @return [Pin::Identifier] the PIN component
77
+ attr_reader :pin
60
78
 
61
- # @return [Boolean] whether the piece is a terminal piece
62
- def terminal
63
- @pin_identifier.terminal
79
+ # @return [Boolean] whether the piece uses derived style (opponent's native style)
80
+ def derived?
81
+ @derived
64
82
  end
65
83
 
66
- # @return [Boolean] the style derivation (true for native, false for foreign)
67
- attr_reader :native
68
-
69
- # Create a new EPIN identifier instance
84
+ # Create a new EPIN identifier from PIN component and derivation flag
70
85
  #
71
- # @param type [Symbol] piece type (:A to :Z)
72
- # @param side [Symbol] player side (:first or :second)
73
- # @param state [Symbol] piece state (:normal, :enhanced, or :diminished)
74
- # @param native [Boolean] style derivation (true for native, false for foreign)
75
- # @param terminal [Boolean] whether the piece is a terminal piece
76
- # @raise [ArgumentError] if parameters are invalid
86
+ # @param pin [Pin::Identifier] the PIN component
87
+ # @param derived [Boolean] whether the piece uses derived style (default: false)
88
+ # @raise [ArgumentError] if pin is not a Pin::Identifier
77
89
  #
78
- # @example
79
- # Identifier.new(:K, :first, :normal, true) # => "K"
80
- # Identifier.new(:K, :first, :normal, true, terminal: true) # => "K^"
81
- # Identifier.new(:K, :first, :normal, false, terminal: true) # => "K^'"
82
- # Identifier.new(:P, :second, :enhanced, false) # => "+p'"
83
- def initialize(type, side, state = Pin::Identifier::NORMAL_STATE, native = NATIVE, terminal: false)
84
- # Validate using PIN class methods for type, side, and state
85
- Pin::Identifier.validate_type(type)
86
- Pin::Identifier.validate_side(side)
87
- Pin::Identifier.validate_state(state)
88
- self.class.validate_derivation(native)
89
-
90
- @pin_identifier = Pin::Identifier.new(type, side, state, terminal: terminal)
91
- @native = native
90
+ # @example Create EPIN identifiers
91
+ # pin = Sashite::Pin.parse("K^")
92
+ # native = Sashite::Epin::Identifier.new(pin, derived: false) # => "K^"
93
+ # derived = Sashite::Epin::Identifier.new(pin, derived: true) # => "K^'"
94
+ def initialize(pin, derived: false)
95
+ raise ::ArgumentError, format(ERROR_INVALID_PIN, pin.class) unless pin.is_a?(Pin::Identifier)
96
+
97
+ @pin = pin
98
+ @derived = !!derived
92
99
 
93
100
  freeze
94
101
  end
95
102
 
96
103
  # Parse an EPIN string into an Identifier object
97
104
  #
98
- # EPIN format: [<state>]<letter>[<terminal>][<derivation>]
99
- # where terminal marker (^) comes BEFORE derivation marker (')
100
- #
101
105
  # @param epin_string [String] EPIN notation string
102
106
  # @return [Identifier] new identifier instance
103
107
  # @raise [ArgumentError] if the EPIN string is invalid
104
108
  #
105
- # @example
106
- # Epin::Identifier.parse("k") # => native second player king
107
- # Epin::Identifier.parse("K^") # => native terminal first player king
108
- # Epin::Identifier.parse("+R'") # => foreign enhanced first player rook
109
- # Epin::Identifier.parse("+K^'") # => foreign enhanced terminal first player king
110
- # Epin::Identifier.parse("-p") # => native diminished second player pawn
109
+ # @example Parse EPIN strings
110
+ # Sashite::Epin::Identifier.parse("K^") # => Native king
111
+ # Sashite::Epin::Identifier.parse("K^'") # => Derived king
112
+ # Sashite::Epin::Identifier.parse("+R'") # => Derived enhanced rook
111
113
  def self.parse(epin_string)
112
114
  string_value = String(epin_string)
115
+ validate_epin_string(string_value)
113
116
 
114
- # Validate EPIN pattern first
115
- raise ::ArgumentError, format(ERROR_INVALID_EPIN, string_value) unless string_value.match?(EPIN_PATTERN)
116
-
117
- # Check for derivation marker (must be at the end)
118
- if string_value.end_with?(DERIVATION_MARKER)
119
- pin_part = string_value[0...-1] # Remove the apostrophe
120
- derived = true
121
- else
122
- pin_part = string_value
123
- derived = false
124
- end
117
+ # Check for derivation marker
118
+ has_marker = string_value.end_with?(DERIVATION_MARKER)
125
119
 
126
- # Validate and parse the PIN part using existing PIN logic
127
- raise ::ArgumentError, format(ERROR_INVALID_EPIN, string_value) unless Pin::Identifier.valid?(pin_part)
120
+ # Extract PIN part (remove derivation marker if present)
121
+ pin_part = has_marker ? string_value[0...-1] : string_value
128
122
 
123
+ # Parse PIN component
129
124
  pin_identifier = Pin::Identifier.parse(pin_part)
130
- identifier_native = !derived
131
125
 
132
- new(pin_identifier.type, pin_identifier.side, pin_identifier.state, identifier_native, terminal: pin_identifier.terminal)
126
+ new(pin_identifier, derived: has_marker)
133
127
  end
134
128
 
135
129
  # Check if a string is a valid EPIN notation
136
130
  #
137
- # Valid EPIN format: [<state>]<letter>[<terminal>][<derivation>]
138
- # - State: + or - (optional)
139
- # - Letter: A-Z or a-z (required)
140
- # - Terminal: ^ (optional)
141
- # - Derivation: ' (optional)
142
- #
143
- # @param epin_string [String] The string to validate
131
+ # @param epin_string [String] the string to validate
144
132
  # @return [Boolean] true if valid EPIN, false otherwise
145
133
  #
146
- # @example
147
- # Sashite::Epin::Identifier.valid?("K") # => true
134
+ # @example Validate EPIN strings
148
135
  # Sashite::Epin::Identifier.valid?("K^") # => true
136
+ # Sashite::Epin::Identifier.valid?("K^'") # => true
149
137
  # Sashite::Epin::Identifier.valid?("+R'") # => true
150
- # Sashite::Epin::Identifier.valid?("+K^'") # => true
151
- # Sashite::Epin::Identifier.valid?("KK") # => false
152
- # Sashite::Epin::Identifier.valid?("++K") # => false
153
- # Sashite::Epin::Identifier.valid?("K'^") # => false (wrong order)
138
+ # Sashite::Epin::Identifier.valid?("K^''") # => false (multiple markers)
139
+ # Sashite::Epin::Identifier.valid?("KK'") # => false (invalid PIN)
154
140
  def self.valid?(epin_string)
155
141
  return false unless epin_string.is_a?(::String)
156
- return false if epin_string.empty?
157
-
158
- # Check EPIN pattern
159
142
  return false unless epin_string.match?(EPIN_PATTERN)
160
143
 
161
- # Extract PIN part (remove derivation marker if present)
162
- pin_part = epin_string.end_with?(DERIVATION_MARKER) ? epin_string[0...-1] : epin_string
163
- return false if pin_part.empty? # Can't have just an apostrophe
144
+ # Check for multiple derivation markers
145
+ marker_count = epin_string.count(DERIVATION_MARKER)
146
+ return false if marker_count > 1
147
+
148
+ # Validate PIN part
149
+ has_marker = epin_string.end_with?(DERIVATION_MARKER)
150
+ pin_part = has_marker ? epin_string[0...-1] : epin_string
164
151
 
165
- # Validate the PIN part using existing PIN validation
166
152
  Pin::Identifier.valid?(pin_part)
167
153
  end
168
154
 
169
155
  # Convert the identifier to its EPIN string representation
170
156
  #
171
- # Format: [<state>]<letter>[<terminal>][<derivation>]
172
- #
173
157
  # @return [String] EPIN notation string
174
158
  #
175
- # @example
176
- # identifier.to_s # => "+R'"
177
- # identifier.to_s # => "K^"
178
- # identifier.to_s # => "+K^'"
179
- # identifier.to_s # => "-p"
159
+ # @example Serialize identifiers
160
+ # native.to_s # => "K^"
161
+ # derived.to_s # => "K^'"
180
162
  def to_s
181
- "#{prefix}#{letter}#{terminal_marker}#{derivation_marker}"
182
- end
183
-
184
- # Get the letter representation (inherited from PIN logic)
185
- #
186
- # @return [String] letter representation combining type and side
187
- def letter
188
- @pin_identifier.letter
189
- end
190
-
191
- # Get the state prefix (inherited from PIN logic)
192
- #
193
- # @return [String] prefix representing the state ("+" / "-" / "")
194
- def prefix
195
- @pin_identifier.prefix
196
- end
197
-
198
- # Get the terminal marker (inherited from PIN logic)
199
- #
200
- # @return [String] terminal marker ("^" or "")
201
- def terminal_marker
202
- @pin_identifier.suffix
163
+ pin.to_s + suffix
203
164
  end
204
165
 
205
- # Get the derivation marker (EPIN-specific)
166
+ # Get the derivation marker suffix
206
167
  #
207
- # @return [String] derivation marker ("'" for foreign, "" for native)
208
- def derivation_marker
209
- native? ? NATIVE_MARKER : DERIVATION_MARKER
168
+ # @return [String] derivation marker if derived, empty string if native
169
+ def suffix
170
+ derived? ? DERIVATION_MARKER : ""
210
171
  end
211
172
 
212
- # Alias for backward compatibility
213
- alias suffix derivation_marker
214
-
215
- # Create a new identifier with enhanced state
173
+ # Create a new identifier with a different PIN component
216
174
  #
217
- # Preserves type, side, terminal status, and derivation.
175
+ # @param new_pin [Pin::Identifier] new PIN component
176
+ # @return [Identifier] new identifier with different PIN
177
+ # @raise [ArgumentError] if new_pin is not a Pin::Identifier
218
178
  #
219
- # @return [Identifier] new identifier instance with enhanced state
220
- #
221
- # @example
222
- # identifier.enhance # (:K, :first, :normal, true) => (:K, :first, :enhanced, true)
223
- def enhance
224
- return self if enhanced?
179
+ # @example Replace PIN component
180
+ # epin = Sashite::Epin.parse("K^'")
181
+ # new_pin = epin.pin.with_type(:Q)
182
+ # epin.with_pin(new_pin).to_s # => "Q^'"
183
+ def with_pin(new_pin)
184
+ raise ::ArgumentError, format(ERROR_INVALID_PIN, new_pin.class) unless new_pin.is_a?(Pin::Identifier)
185
+ return self if pin == new_pin
225
186
 
226
- self.class.new(type, side, Pin::Identifier::ENHANCED_STATE, native, terminal: terminal)
187
+ self.class.new(new_pin, derived: derived?)
227
188
  end
228
189
 
229
- # Create a new identifier without enhanced state
190
+ # Create a new identifier with different derivation status
230
191
  #
231
- # @return [Identifier] new identifier instance with normal state
192
+ # @param new_derived [Boolean] new derivation status
193
+ # @return [Identifier] new identifier with different derivation
232
194
  #
233
- # @example
234
- # identifier.unenhance # (:K, :first, :enhanced, true) => (:K, :first, :normal, true)
235
- def unenhance
236
- return self unless enhanced?
195
+ # @example Change derivation status
196
+ # native = Sashite::Epin.parse("K^")
197
+ # derived = native.with_derived(true)
198
+ # derived.to_s # => "K^'"
199
+ def with_derived(new_derived)
200
+ new_derived_bool = !!new_derived
201
+ return self if derived? == new_derived_bool
237
202
 
238
- self.class.new(type, side, Pin::Identifier::NORMAL_STATE, native, terminal: terminal)
203
+ self.class.new(pin, derived: new_derived_bool)
239
204
  end
240
205
 
241
- # Create a new identifier with diminished state
206
+ # Create a new identifier marked as derived (opponent's native style)
242
207
  #
243
- # @return [Identifier] new identifier instance with diminished state
208
+ # @return [Identifier] new identifier marked as derived
244
209
  #
245
- # @example
246
- # identifier.diminish # (:K, :first, :normal, true) => (:K, :first, :diminished, true)
247
- def diminish
248
- return self if diminished?
249
-
250
- self.class.new(type, side, Pin::Identifier::DIMINISHED_STATE, native, terminal: terminal)
251
- end
252
-
253
- # Create a new identifier without diminished state
254
- #
255
- # @return [Identifier] new identifier instance with normal state
256
- #
257
- # @example
258
- # identifier.undiminish # (:K, :first, :diminished, true) => (:K, :first, :normal, true)
259
- def undiminish
260
- return self unless diminished?
261
-
262
- self.class.new(type, side, Pin::Identifier::NORMAL_STATE, native, terminal: terminal)
263
- end
264
-
265
- # Create a new identifier with normal state (no state modifiers)
266
- #
267
- # @return [Identifier] new identifier instance with normal state
268
- #
269
- # @example
270
- # identifier.normalize # (:K, :first, :enhanced, true) => (:K, :first, :normal, true)
271
- def normalize
272
- return self if normal?
273
-
274
- self.class.new(type, side, Pin::Identifier::NORMAL_STATE, native, terminal: terminal)
275
- end
276
-
277
- # Create a new identifier marked as terminal
278
- #
279
- # @return [Identifier] new identifier instance marked as terminal
280
- #
281
- # @example
282
- # identifier.mark_terminal # "K" => "K^"
283
- # identifier.mark_terminal # "K'" => "K^'"
284
- def mark_terminal
285
- return self if terminal?
286
-
287
- self.class.new(type, side, state, native, terminal: true)
288
- end
289
-
290
- # Create a new identifier unmarked as terminal
291
- #
292
- # @return [Identifier] new identifier instance unmarked as terminal
293
- #
294
- # @example
295
- # identifier.unmark_terminal # "K^" => "K"
296
- # identifier.unmark_terminal # "K^'" => "K'"
297
- def unmark_terminal
298
- return self unless terminal?
299
-
300
- self.class.new(type, side, state, native, terminal: false)
301
- end
302
-
303
- # Create a new identifier with opposite side
304
- #
305
- # @return [Identifier] new identifier instance with opposite side
306
- #
307
- # @example
308
- # identifier.flip # (:K, :first, :normal, true) => (:K, :second, :normal, true)
309
- def flip
310
- self.class.new(type, opposite_side, state, native, terminal: terminal)
311
- end
312
-
313
- # Create a new identifier with foreign/derived style
314
- #
315
- # Converts a native piece to foreign style (opposite side's native style).
316
- #
317
- # @return [Identifier] new identifier instance with foreign style
318
- #
319
- # @example
320
- # identifier.derive # (:K, :first, :normal, true) => (:K, :first, :normal, false)
321
- # # "K" => "K'"
322
- def derive
210
+ # @example Mark as derived
211
+ # native = Sashite::Epin.parse("K^")
212
+ # derived = native.mark_derived
213
+ # derived.to_s # => "K^'"
214
+ def mark_derived
323
215
  return self if derived?
324
216
 
325
- self.class.new(type, side, state, FOREIGN, terminal: terminal)
217
+ self.class.new(pin, derived: true)
326
218
  end
327
219
 
328
- # Create a new identifier with native style
329
- #
330
- # Converts a foreign piece to native style (current side's native style).
220
+ # Create a new identifier marked as native (own side's native style)
331
221
  #
332
- # @return [Identifier] new identifier instance with native style
222
+ # @return [Identifier] new identifier marked as native
333
223
  #
334
- # @example
335
- # identifier.underive # (:K, :first, :normal, false) => (:K, :first, :normal, true)
336
- # # "K'" => "K"
337
- def underive
338
- return self if native?
224
+ # @example Mark as native
225
+ # derived = Sashite::Epin.parse("K^'")
226
+ # native = derived.unmark_native
227
+ # native.to_s # => "K^"
228
+ def unmark_native
229
+ return self unless derived?
339
230
 
340
- self.class.new(type, side, state, NATIVE, terminal: terminal)
231
+ self.class.new(pin, derived: false)
341
232
  end
342
233
 
343
- # Create a new identifier with a different type
234
+ # Check if the identifier uses native style (own side's native style)
344
235
  #
345
- # Preserves side, state, terminal status, and derivation.
346
- #
347
- # @param new_type [Symbol] new type (:A to :Z)
348
- # @return [Identifier] new identifier instance with different type
349
- #
350
- # @example
351
- # identifier.with_type(:Q) # (:K, :first, :normal, true) => (:Q, :first, :normal, true)
352
- def with_type(new_type)
353
- Pin::Identifier.validate_type(new_type)
354
- return self if type == new_type
355
-
356
- self.class.new(new_type, side, state, native, terminal: terminal)
357
- end
358
-
359
- # Create a new identifier with a different side
360
- #
361
- # Preserves type, state, terminal status, and derivation.
362
- #
363
- # @param new_side [Symbol] :first or :second
364
- # @return [Identifier] new identifier instance with different side
365
- #
366
- # @example
367
- # identifier.with_side(:second) # (:K, :first, :normal, true) => (:K, :second, :normal, true)
368
- def with_side(new_side)
369
- Pin::Identifier.validate_side(new_side)
370
- return self if side == new_side
371
-
372
- self.class.new(type, new_side, state, native, terminal: terminal)
373
- end
374
-
375
- # Create a new identifier with a different state
376
- #
377
- # Preserves type, side, terminal status, and derivation.
378
- #
379
- # @param new_state [Symbol] :normal, :enhanced, or :diminished
380
- # @return [Identifier] new identifier instance with different state
381
- #
382
- # @example
383
- # identifier.with_state(:enhanced) # (:K, :first, :normal, true) => (:K, :first, :enhanced, true)
384
- def with_state(new_state)
385
- Pin::Identifier.validate_state(new_state)
386
- return self if state == new_state
387
-
388
- self.class.new(type, side, new_state, native, terminal: terminal)
389
- end
390
-
391
- # Create a new identifier with a different derivation
392
- #
393
- # Preserves type, side, state, and terminal status.
394
- #
395
- # @param new_native [Boolean] true for native, false for foreign
396
- # @return [Identifier] new identifier instance with different derivation
397
- #
398
- # @example
399
- # identifier.with_derivation(false) # (:K, :first, :normal, true) => (:K, :first, :normal, false)
400
- def with_derivation(new_native)
401
- self.class.validate_derivation(new_native)
402
- return self if native == new_native
403
-
404
- self.class.new(type, side, state, new_native, terminal: terminal)
405
- end
406
-
407
- # Create a new identifier with a different terminal status
408
- #
409
- # Preserves type, side, state, and derivation.
410
- #
411
- # @param new_terminal_bool [Boolean] terminal status
412
- # @return [Identifier] new identifier instance with different terminal status
413
- #
414
- # @example
415
- # identifier.with_terminal(true) # "K" => "K^"
416
- def with_terminal(new_terminal_bool)
417
- raise ::TypeError unless [true, false].include?(new_terminal_bool)
418
-
419
- return self if terminal? == new_terminal_bool
420
-
421
- self.class.new(type, side, state, native, terminal: new_terminal_bool)
422
- end
423
-
424
- # Check if the identifier has enhanced state
425
- #
426
- # @return [Boolean] true if enhanced
427
- def enhanced?
428
- @pin_identifier.enhanced?
429
- end
430
-
431
- # Check if the identifier has diminished state
432
- #
433
- # @return [Boolean] true if diminished
434
- def diminished?
435
- @pin_identifier.diminished?
436
- end
437
-
438
- # Check if the identifier has normal state (no state modifiers)
439
- #
440
- # @return [Boolean] true if normal
441
- def normal?
442
- @pin_identifier.normal?
443
- end
444
-
445
- # Check if the identifier belongs to the first player
446
- #
447
- # @return [Boolean] true if first player
448
- def first_player?
449
- @pin_identifier.first_player?
450
- end
451
-
452
- # Check if the identifier belongs to the second player
453
- #
454
- # @return [Boolean] true if second player
455
- def second_player?
456
- @pin_identifier.second_player?
457
- end
458
-
459
- # Check if the identifier is a terminal piece
460
- #
461
- # A terminal piece is one whose presence, condition, or capacity for action
462
- # determines whether the match can continue.
463
- #
464
- # @return [Boolean] true if terminal
465
- def terminal?
466
- @pin_identifier.terminal?
467
- end
468
-
469
- # Check if the identifier has native style
470
- #
471
- # A native piece has the native style of its current side.
472
- #
473
- # @return [Boolean] true if native style
236
+ # @return [Boolean] true if native (not derived)
474
237
  def native?
475
- native == NATIVE
476
- end
477
-
478
- # Check if the identifier has foreign/derived style
479
- #
480
- # A derived piece has the foreign style (opposite side's native style).
481
- #
482
- # @return [Boolean] true if foreign/derived style
483
- def derived?
484
- native == FOREIGN
238
+ !derived?
485
239
  end
486
240
 
487
- # Alias for derived? to match specification terminology
488
- alias foreign? derived?
489
-
490
- # Check if this identifier is the same type as another
491
- #
492
- # Ignores side, state, terminal status, and derivation.
241
+ # Check if this identifier has the same derivation status as another
493
242
  #
494
243
  # @param other [Identifier] identifier to compare with
495
- # @return [Boolean] true if same type
244
+ # @return [Boolean] true if same derivation status
496
245
  #
497
- # @example
498
- # king1.same_type?(king2) # (:K, :first, :normal, true) and (:K, :second, :enhanced, false) => true
499
- def same_type?(other)
246
+ # @example Compare derivation status
247
+ # native = Sashite::Epin.parse("K^")
248
+ # derived = Sashite::Epin.parse("K^'")
249
+ # native.same_derivation?(derived) # => false
250
+ def same_derivation?(other)
500
251
  return false unless other.is_a?(self.class)
501
252
 
502
- @pin_identifier.same_type?(other.instance_variable_get(:@pin_identifier))
503
- end
504
-
505
- # Check if this identifier belongs to the same side as another
506
- #
507
- # @param other [Identifier] identifier to compare with
508
- # @return [Boolean] true if same side
509
- def same_side?(other)
510
- return false unless other.is_a?(self.class)
511
-
512
- @pin_identifier.same_side?(other.instance_variable_get(:@pin_identifier))
513
- end
514
-
515
- # Check if this identifier has the same state as another
516
- #
517
- # @param other [Identifier] identifier to compare with
518
- # @return [Boolean] true if same state
519
- def same_state?(other)
520
- return false unless other.is_a?(self.class)
521
-
522
- @pin_identifier.same_state?(other.instance_variable_get(:@pin_identifier))
523
- end
524
-
525
- # Check if this identifier has the same terminal status as another
526
- #
527
- # @param other [Identifier] identifier to compare with
528
- # @return [Boolean] true if same terminal status
529
- def same_terminal?(other)
530
- return false unless other.is_a?(self.class)
531
-
532
- @pin_identifier.same_terminal?(other.instance_variable_get(:@pin_identifier))
533
- end
534
-
535
- # Check if this identifier has the same style derivation as another
536
- #
537
- # @param other [Identifier] identifier to compare with
538
- # @return [Boolean] true if same style derivation
539
- def same_style?(other)
540
- return false unless other.is_a?(self.class)
541
-
542
- native == other.native
253
+ derived? == other.derived?
543
254
  end
544
255
 
545
256
  # Custom equality comparison
546
257
  #
547
- # Two identifiers are equal if they have the same type, side, state,
548
- # terminal status, and derivation.
549
- #
550
258
  # @param other [Object] object to compare with
551
259
  # @return [Boolean] true if identifiers are equal
552
260
  def ==(other)
553
261
  return false unless other.is_a?(self.class)
554
262
 
555
- @pin_identifier == other.instance_variable_get(:@pin_identifier) && native == other.native
263
+ pin == other.pin && derived? == other.derived?
556
264
  end
557
265
 
558
266
  # Alias for == to ensure Set functionality works correctly
@@ -562,27 +270,24 @@ module Sashite
562
270
  #
563
271
  # @return [Integer] hash value
564
272
  def hash
565
- [self.class, @pin_identifier, native].hash
273
+ [self.class, pin, derived?].hash
566
274
  end
567
275
 
568
- # Validate that the derivation is a valid boolean
276
+ # Validate EPIN string format
569
277
  #
570
- # @param derivation [Boolean] the derivation to validate
571
- # @raise [ArgumentError] if invalid
572
- def self.validate_derivation(derivation)
573
- return if VALID_DERIVATIONS.include?(derivation)
278
+ # @param string [String] string to validate
279
+ # @raise [ArgumentError] if string doesn't match EPIN pattern or has multiple markers
280
+ def self.validate_epin_string(string)
281
+ raise ::ArgumentError, format(ERROR_INVALID_EPIN, string) unless string.match?(EPIN_PATTERN)
574
282
 
575
- raise ::ArgumentError, format(ERROR_INVALID_DERIVATION, derivation.inspect)
576
- end
577
-
578
- private
283
+ # Check for multiple derivation markers
284
+ marker_count = string.count(DERIVATION_MARKER)
285
+ return unless marker_count > 1
579
286
 
580
- # Get the opposite side of the current identifier
581
- #
582
- # @return [Symbol] :first if current side is :second, :second if current side is :first
583
- def opposite_side
584
- @pin_identifier.send(:opposite_side)
287
+ raise ::ArgumentError, format(ERROR_MULTIPLE_MARKERS, string)
585
288
  end
289
+
290
+ private_class_method :validate_epin_string
586
291
  end
587
292
  end
588
293
  end