sashite-epin 1.2.0 → 2.1.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,588 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "sashite/pin"
4
-
5
- module Sashite
6
- module Epin
7
- # Represents an identifier in EPIN (Extended Piece Identifier Notation) format.
8
- #
9
- # EPIN extends PIN by adding a derivation marker to encode Piece Style relative to Piece Side.
10
- #
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)
16
- #
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)
20
- #
21
- # All instances are immutable - transformations return new instances.
22
- # This extends the Game Protocol's piece model with Style support through derivation.
23
- #
24
- # @see https://sashite.dev/specs/epin/1.0.0/
25
- class Identifier
26
- # EPIN validation pattern matching the specification: /\A[-+]?[A-Za-z]\^?'?\z/
27
- EPIN_PATTERN = /\A[-+]?[A-Za-z]\^?'?\z/
28
-
29
- # Derivation marker for foreign/derived style
30
- DERIVATION_MARKER = "'"
31
-
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
- # Error messages
43
- 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
55
-
56
- # @return [Symbol] the piece state (:normal, :enhanced, or :diminished)
57
- def state
58
- @pin_identifier.state
59
- end
60
-
61
- # @return [Boolean] whether the piece is a terminal piece
62
- def terminal
63
- @pin_identifier.terminal
64
- end
65
-
66
- # @return [Boolean] the style derivation (true for native, false for foreign)
67
- attr_reader :native
68
-
69
- # Create a new EPIN identifier instance
70
- #
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
77
- #
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
92
-
93
- freeze
94
- end
95
-
96
- # Parse an EPIN string into an Identifier object
97
- #
98
- # EPIN format: [<state>]<letter>[<terminal>][<derivation>]
99
- # where terminal marker (^) comes BEFORE derivation marker (')
100
- #
101
- # @param epin_string [String] EPIN notation string
102
- # @return [Identifier] new identifier instance
103
- # @raise [ArgumentError] if the EPIN string is invalid
104
- #
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
111
- def self.parse(epin_string)
112
- string_value = String(epin_string)
113
-
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
125
-
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)
128
-
129
- pin_identifier = Pin::Identifier.parse(pin_part)
130
- identifier_native = !derived
131
-
132
- new(pin_identifier.type, pin_identifier.side, pin_identifier.state, identifier_native, terminal: pin_identifier.terminal)
133
- end
134
-
135
- # Check if a string is a valid EPIN notation
136
- #
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
144
- # @return [Boolean] true if valid EPIN, false otherwise
145
- #
146
- # @example
147
- # Sashite::Epin::Identifier.valid?("K") # => true
148
- # Sashite::Epin::Identifier.valid?("K^") # => true
149
- # 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)
154
- def self.valid?(epin_string)
155
- return false unless epin_string.is_a?(::String)
156
- return false if epin_string.empty?
157
-
158
- # Check EPIN pattern
159
- return false unless epin_string.match?(EPIN_PATTERN)
160
-
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
164
-
165
- # Validate the PIN part using existing PIN validation
166
- Pin::Identifier.valid?(pin_part)
167
- end
168
-
169
- # Convert the identifier to its EPIN string representation
170
- #
171
- # Format: [<state>]<letter>[<terminal>][<derivation>]
172
- #
173
- # @return [String] EPIN notation string
174
- #
175
- # @example
176
- # identifier.to_s # => "+R'"
177
- # identifier.to_s # => "K^"
178
- # identifier.to_s # => "+K^'"
179
- # identifier.to_s # => "-p"
180
- 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
203
- end
204
-
205
- # Get the derivation marker (EPIN-specific)
206
- #
207
- # @return [String] derivation marker ("'" for foreign, "" for native)
208
- def derivation_marker
209
- native? ? NATIVE_MARKER : DERIVATION_MARKER
210
- end
211
-
212
- # Alias for backward compatibility
213
- alias suffix derivation_marker
214
-
215
- # Create a new identifier with enhanced state
216
- #
217
- # Preserves type, side, terminal status, and derivation.
218
- #
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?
225
-
226
- self.class.new(type, side, Pin::Identifier::ENHANCED_STATE, native, terminal: terminal)
227
- end
228
-
229
- # Create a new identifier without enhanced state
230
- #
231
- # @return [Identifier] new identifier instance with normal state
232
- #
233
- # @example
234
- # identifier.unenhance # (:K, :first, :enhanced, true) => (:K, :first, :normal, true)
235
- def unenhance
236
- return self unless enhanced?
237
-
238
- self.class.new(type, side, Pin::Identifier::NORMAL_STATE, native, terminal: terminal)
239
- end
240
-
241
- # Create a new identifier with diminished state
242
- #
243
- # @return [Identifier] new identifier instance with diminished state
244
- #
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
323
- return self if derived?
324
-
325
- self.class.new(type, side, state, FOREIGN, terminal: terminal)
326
- end
327
-
328
- # Create a new identifier with native style
329
- #
330
- # Converts a foreign piece to native style (current side's native style).
331
- #
332
- # @return [Identifier] new identifier instance with native style
333
- #
334
- # @example
335
- # identifier.underive # (:K, :first, :normal, false) => (:K, :first, :normal, true)
336
- # # "K'" => "K"
337
- def underive
338
- return self if native?
339
-
340
- self.class.new(type, side, state, NATIVE, terminal: terminal)
341
- end
342
-
343
- # Create a new identifier with a different type
344
- #
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
474
- 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
485
- end
486
-
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.
493
- #
494
- # @param other [Identifier] identifier to compare with
495
- # @return [Boolean] true if same type
496
- #
497
- # @example
498
- # king1.same_type?(king2) # (:K, :first, :normal, true) and (:K, :second, :enhanced, false) => true
499
- def same_type?(other)
500
- return false unless other.is_a?(self.class)
501
-
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
543
- end
544
-
545
- # Custom equality comparison
546
- #
547
- # Two identifiers are equal if they have the same type, side, state,
548
- # terminal status, and derivation.
549
- #
550
- # @param other [Object] object to compare with
551
- # @return [Boolean] true if identifiers are equal
552
- def ==(other)
553
- return false unless other.is_a?(self.class)
554
-
555
- @pin_identifier == other.instance_variable_get(:@pin_identifier) && native == other.native
556
- end
557
-
558
- # Alias for == to ensure Set functionality works correctly
559
- alias eql? ==
560
-
561
- # Custom hash implementation for use in collections
562
- #
563
- # @return [Integer] hash value
564
- def hash
565
- [self.class, @pin_identifier, native].hash
566
- end
567
-
568
- # Validate that the derivation is a valid boolean
569
- #
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)
574
-
575
- raise ::ArgumentError, format(ERROR_INVALID_DERIVATION, derivation.inspect)
576
- end
577
-
578
- private
579
-
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)
585
- end
586
- end
587
- end
588
- end