sashite-epin 1.1.0 → 1.2.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: b308beb3213522cf87199c0b28e1abe7a9d767260ac6ec3ee5f9581ae1ef1bdb
4
- data.tar.gz: 3c965f92446dbcf1965844e7454c3bdf1152e2c3e7a74953e974e1239ae6137a
3
+ metadata.gz: 43dd91dce36507e42cf07b57202cff4d323fb889fde7d8daf62498b8bf588f04
4
+ data.tar.gz: b4237ab78e18f3e879893fd934f438aa3729d60508dbf8c5378727b84b2a9b7c
5
5
  SHA512:
6
- metadata.gz: 99a9d2ec0c5834a886517746e7d50206dbb1f331043133df7f1e919a47f506d71a7d5f58798680b3d005640983ab8c22ad7e892b57a0b7df04c8a6401ee28512
7
- data.tar.gz: 56d10546fb4068fc8e3ccdaf5f36de9d947b7f7cb2c3cc3a4fd8c9a9c01acfeb7258de0a833a85ddd64d4f889a49e274b045a07d73892e06d774812f0d849729
6
+ metadata.gz: 75122035fe9b0a3a0d4bda91518761b57a12a1551eb7743b27e31775c5007df75da1bd470f8b0cd69037d33bfafbc2c3c1773f7f7eb52fc083e6928989266c76
7
+ data.tar.gz: '087da6655e1528df88d86b276eff52fb1ae71e4d3b83295b105135b4ba22f5d8c75f2675fac9f3cbc61dcb94aeda9ab7c6de274dc4a8e614ca9549d4583df002'
@@ -6,37 +6,44 @@ module Sashite
6
6
  module Epin
7
7
  # Represents an identifier in EPIN (Extended Piece Identifier Notation) format.
8
8
  #
9
- # An identifier consists of a PIN component with an optional derivation marker:
10
- # - PIN component: [<state>]<letter> (from PIN specification)
11
- # - Derivation marker: "'" (foreign style) or none (native style)
9
+ # EPIN extends PIN by adding a derivation marker to encode Piece Style relative to Piece Side.
12
10
  #
13
- # The case of the letter determines ownership:
14
- # - Uppercase (A-Z): first player
15
- # - Lowercase (a-z): second player
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
16
  #
17
- # Style derivation logic:
18
- # - No suffix: piece has the native style of its current side
17
+ # Style Derivation Logic:
18
+ # - No apostrophe suffix: piece has the native style of its current side
19
19
  # - Apostrophe suffix: piece has the foreign style (opposite side's native style)
20
20
  #
21
- # All instances are immutable - state manipulation methods return new instances.
21
+ # All instances are immutable - transformations return new instances.
22
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/
23
25
  class Identifier
24
- # Valid derivation suffixes
25
- DERIVATION_SUFFIX = "'"
26
- NATIVE_SUFFIX = ""
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 = ""
27
34
 
28
- # Derivation constants
35
+ # Style constants
29
36
  NATIVE = true
30
37
  FOREIGN = false
31
38
 
32
- # Valid derivations
39
+ # Valid derivation values
33
40
  VALID_DERIVATIONS = [NATIVE, FOREIGN].freeze
34
41
 
35
42
  # Error messages
36
43
  ERROR_INVALID_EPIN = "Invalid EPIN string: %s"
37
44
  ERROR_INVALID_DERIVATION = "Derivation must be true (native) or false (foreign), got: %s"
38
45
 
39
- # @return [Symbol] the piece type (:A to :Z)
46
+ # @return [Symbol] the piece type (:A to :Z, always uppercase)
40
47
  def type
41
48
  @pin_identifier.type
42
49
  end
@@ -51,27 +58,36 @@ module Sashite
51
58
  @pin_identifier.state
52
59
  end
53
60
 
61
+ # @return [Boolean] whether the piece is a terminal piece
62
+ def terminal
63
+ @pin_identifier.terminal
64
+ end
65
+
54
66
  # @return [Boolean] the style derivation (true for native, false for foreign)
55
67
  attr_reader :native
56
68
 
57
- # Create a new identifier instance
69
+ # Create a new EPIN identifier instance
58
70
  #
59
71
  # @param type [Symbol] piece type (:A to :Z)
60
72
  # @param side [Symbol] player side (:first or :second)
61
73
  # @param state [Symbol] piece state (:normal, :enhanced, or :diminished)
62
74
  # @param native [Boolean] style derivation (true for native, false for foreign)
75
+ # @param terminal [Boolean] whether the piece is a terminal piece
63
76
  # @raise [ArgumentError] if parameters are invalid
77
+ #
64
78
  # @example
65
- # Identifier.new(:K, :first, :normal, true)
66
- # Identifier.new(:P, :second, :enhanced, false)
67
- def initialize(type, side, state = Pin::Identifier::NORMAL_STATE, native = NATIVE)
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)
68
84
  # Validate using PIN class methods for type, side, and state
69
85
  Pin::Identifier.validate_type(type)
70
86
  Pin::Identifier.validate_side(side)
71
87
  Pin::Identifier.validate_state(state)
72
88
  self.class.validate_derivation(native)
73
89
 
74
- @pin_identifier = Pin::Identifier.new(type, side, state)
90
+ @pin_identifier = Pin::Identifier.new(type, side, state, terminal: terminal)
75
91
  @native = native
76
92
 
77
93
  freeze
@@ -79,56 +95,72 @@ module Sashite
79
95
 
80
96
  # Parse an EPIN string into an Identifier object
81
97
  #
98
+ # EPIN format: [<state>]<letter>[<terminal>][<derivation>]
99
+ # where terminal marker (^) comes BEFORE derivation marker (')
100
+ #
82
101
  # @param epin_string [String] EPIN notation string
83
102
  # @return [Identifier] new identifier instance
84
103
  # @raise [ArgumentError] if the EPIN string is invalid
104
+ #
85
105
  # @example
86
- # Epin::Identifier.parse("k") # => #<Epin::Identifier type=:K side=:second state=:normal native=true>
87
- # Epin::Identifier.parse("+R'") # => #<Epin::Identifier type=:R side=:first state=:enhanced native=false>
88
- # Epin::Identifier.parse("-p") # => #<Epin::Identifier type=:P side=:second state=:diminished native=true>
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
89
111
  def self.parse(epin_string)
90
112
  string_value = String(epin_string)
91
113
 
92
- # Check for derivation suffix
93
- if string_value.end_with?(DERIVATION_SUFFIX)
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)
94
119
  pin_part = string_value[0...-1] # Remove the apostrophe
95
- foreign = true
120
+ derived = true
96
121
  else
97
122
  pin_part = string_value
98
- foreign = false
123
+ derived = false
99
124
  end
100
125
 
101
126
  # Validate and parse the PIN part using existing PIN logic
102
127
  raise ::ArgumentError, format(ERROR_INVALID_EPIN, string_value) unless Pin::Identifier.valid?(pin_part)
103
128
 
104
129
  pin_identifier = Pin::Identifier.parse(pin_part)
105
- identifier_native = !foreign
130
+ identifier_native = !derived
106
131
 
107
- new(pin_identifier.type, pin_identifier.side, pin_identifier.state, identifier_native)
132
+ new(pin_identifier.type, pin_identifier.side, pin_identifier.state, identifier_native, terminal: pin_identifier.terminal)
108
133
  end
109
134
 
110
135
  # Check if a string is a valid EPIN notation
111
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
+ #
112
143
  # @param epin_string [String] The string to validate
113
144
  # @return [Boolean] true if valid EPIN, false otherwise
114
145
  #
115
146
  # @example
116
- # Sashite::Epin::Identifier.valid?("K") # => true
117
- # Sashite::Epin::Identifier.valid?("+R'") # => true
118
- # Sashite::Epin::Identifier.valid?("-p") # => true
119
- # Sashite::Epin::Identifier.valid?("KK") # => false
120
- # Sashite::Epin::Identifier.valid?("++K") # => false
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)
121
154
  def self.valid?(epin_string)
122
155
  return false unless epin_string.is_a?(::String)
123
156
  return false if epin_string.empty?
124
157
 
125
- # Check for derivation suffix
126
- if epin_string.end_with?(DERIVATION_SUFFIX)
127
- pin_part = epin_string[0...-1] # Remove the apostrophe
128
- return false if pin_part.empty? # Can't have just an apostrophe
129
- else
130
- pin_part = epin_string
131
- end
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
132
164
 
133
165
  # Validate the PIN part using existing PIN validation
134
166
  Pin::Identifier.valid?(pin_part)
@@ -136,13 +168,17 @@ module Sashite
136
168
 
137
169
  # Convert the identifier to its EPIN string representation
138
170
  #
171
+ # Format: [<state>]<letter>[<terminal>][<derivation>]
172
+ #
139
173
  # @return [String] EPIN notation string
174
+ #
140
175
  # @example
141
176
  # identifier.to_s # => "+R'"
177
+ # identifier.to_s # => "K^"
178
+ # identifier.to_s # => "+K^'"
142
179
  # identifier.to_s # => "-p"
143
- # identifier.to_s # => "K"
144
180
  def to_s
145
- "#{prefix}#{letter}#{suffix}"
181
+ "#{prefix}#{letter}#{terminal_marker}#{derivation_marker}"
146
182
  end
147
183
 
148
184
  # Get the letter representation (inherited from PIN logic)
@@ -152,156 +188,237 @@ module Sashite
152
188
  @pin_identifier.letter
153
189
  end
154
190
 
155
- # Get the prefix representation (inherited from PIN logic)
191
+ # Get the state prefix (inherited from PIN logic)
156
192
  #
157
- # @return [String] prefix representing the state
193
+ # @return [String] prefix representing the state ("+" / "-" / "")
158
194
  def prefix
159
195
  @pin_identifier.prefix
160
196
  end
161
197
 
162
- # Get the suffix representation
198
+ # Get the terminal marker (inherited from PIN logic)
163
199
  #
164
- # @return [String] suffix representing the derivation
165
- def suffix
166
- native? ? NATIVE_SUFFIX : DERIVATION_SUFFIX
200
+ # @return [String] terminal marker ("^" or "")
201
+ def terminal_marker
202
+ @pin_identifier.suffix
167
203
  end
168
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
+
169
215
  # Create a new identifier with enhanced state
170
216
  #
217
+ # Preserves type, side, terminal status, and derivation.
218
+ #
171
219
  # @return [Identifier] new identifier instance with enhanced state
220
+ #
172
221
  # @example
173
222
  # identifier.enhance # (:K, :first, :normal, true) => (:K, :first, :enhanced, true)
174
223
  def enhance
175
224
  return self if enhanced?
176
225
 
177
- self.class.new(type, side, Pin::Identifier::ENHANCED_STATE, native)
226
+ self.class.new(type, side, Pin::Identifier::ENHANCED_STATE, native, terminal: terminal)
178
227
  end
179
228
 
180
229
  # Create a new identifier without enhanced state
181
230
  #
182
- # @return [Identifier] new identifier instance without enhanced state
231
+ # @return [Identifier] new identifier instance with normal state
232
+ #
183
233
  # @example
184
234
  # identifier.unenhance # (:K, :first, :enhanced, true) => (:K, :first, :normal, true)
185
235
  def unenhance
186
236
  return self unless enhanced?
187
237
 
188
- self.class.new(type, side, Pin::Identifier::NORMAL_STATE, native)
238
+ self.class.new(type, side, Pin::Identifier::NORMAL_STATE, native, terminal: terminal)
189
239
  end
190
240
 
191
241
  # Create a new identifier with diminished state
192
242
  #
193
243
  # @return [Identifier] new identifier instance with diminished state
244
+ #
194
245
  # @example
195
246
  # identifier.diminish # (:K, :first, :normal, true) => (:K, :first, :diminished, true)
196
247
  def diminish
197
248
  return self if diminished?
198
249
 
199
- self.class.new(type, side, Pin::Identifier::DIMINISHED_STATE, native)
250
+ self.class.new(type, side, Pin::Identifier::DIMINISHED_STATE, native, terminal: terminal)
200
251
  end
201
252
 
202
253
  # Create a new identifier without diminished state
203
254
  #
204
- # @return [Identifier] new identifier instance without diminished state
255
+ # @return [Identifier] new identifier instance with normal state
256
+ #
205
257
  # @example
206
258
  # identifier.undiminish # (:K, :first, :diminished, true) => (:K, :first, :normal, true)
207
259
  def undiminish
208
260
  return self unless diminished?
209
261
 
210
- self.class.new(type, side, Pin::Identifier::NORMAL_STATE, native)
262
+ self.class.new(type, side, Pin::Identifier::NORMAL_STATE, native, terminal: terminal)
211
263
  end
212
264
 
213
- # Create a new identifier with normal state (no modifiers)
265
+ # Create a new identifier with normal state (no state modifiers)
214
266
  #
215
267
  # @return [Identifier] new identifier instance with normal state
268
+ #
216
269
  # @example
217
270
  # identifier.normalize # (:K, :first, :enhanced, true) => (:K, :first, :normal, true)
218
271
  def normalize
219
272
  return self if normal?
220
273
 
221
- self.class.new(type, side, Pin::Identifier::NORMAL_STATE, native)
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)
222
301
  end
223
302
 
224
303
  # Create a new identifier with opposite side
225
304
  #
226
305
  # @return [Identifier] new identifier instance with opposite side
306
+ #
227
307
  # @example
228
308
  # identifier.flip # (:K, :first, :normal, true) => (:K, :second, :normal, true)
229
309
  def flip
230
- self.class.new(type, opposite_side, state, native)
310
+ self.class.new(type, opposite_side, state, native, terminal: terminal)
231
311
  end
232
312
 
233
- # Create a new identifier with foreign style (derivation marker)
313
+ # Create a new identifier with foreign/derived style
314
+ #
315
+ # Converts a native piece to foreign style (opposite side's native style).
234
316
  #
235
317
  # @return [Identifier] new identifier instance with foreign style
318
+ #
236
319
  # @example
237
320
  # identifier.derive # (:K, :first, :normal, true) => (:K, :first, :normal, false)
321
+ # # "K" => "K'"
238
322
  def derive
239
323
  return self if derived?
240
324
 
241
- self.class.new(type, side, state, FOREIGN)
325
+ self.class.new(type, side, state, FOREIGN, terminal: terminal)
242
326
  end
243
327
 
244
- # Create a new identifier with native style (no derivation marker)
328
+ # Create a new identifier with native style
329
+ #
330
+ # Converts a foreign piece to native style (current side's native style).
245
331
  #
246
332
  # @return [Identifier] new identifier instance with native style
333
+ #
247
334
  # @example
248
335
  # identifier.underive # (:K, :first, :normal, false) => (:K, :first, :normal, true)
336
+ # # "K'" => "K"
249
337
  def underive
250
338
  return self if native?
251
339
 
252
- self.class.new(type, side, state, NATIVE)
340
+ self.class.new(type, side, state, NATIVE, terminal: terminal)
253
341
  end
254
342
 
255
- # Create a new identifier with a different type (keeping same side, state, and derivation)
343
+ # Create a new identifier with a different type
344
+ #
345
+ # Preserves side, state, terminal status, and derivation.
256
346
  #
257
347
  # @param new_type [Symbol] new type (:A to :Z)
258
348
  # @return [Identifier] new identifier instance with different type
349
+ #
259
350
  # @example
260
351
  # identifier.with_type(:Q) # (:K, :first, :normal, true) => (:Q, :first, :normal, true)
261
352
  def with_type(new_type)
262
353
  Pin::Identifier.validate_type(new_type)
263
354
  return self if type == new_type
264
355
 
265
- self.class.new(new_type, side, state, native)
356
+ self.class.new(new_type, side, state, native, terminal: terminal)
266
357
  end
267
358
 
268
- # Create a new identifier with a different side (keeping same type, state, and derivation)
359
+ # Create a new identifier with a different side
360
+ #
361
+ # Preserves type, state, terminal status, and derivation.
269
362
  #
270
363
  # @param new_side [Symbol] :first or :second
271
364
  # @return [Identifier] new identifier instance with different side
365
+ #
272
366
  # @example
273
367
  # identifier.with_side(:second) # (:K, :first, :normal, true) => (:K, :second, :normal, true)
274
368
  def with_side(new_side)
275
369
  Pin::Identifier.validate_side(new_side)
276
370
  return self if side == new_side
277
371
 
278
- self.class.new(type, new_side, state, native)
372
+ self.class.new(type, new_side, state, native, terminal: terminal)
279
373
  end
280
374
 
281
- # Create a new identifier with a different state (keeping same type, side, and derivation)
375
+ # Create a new identifier with a different state
376
+ #
377
+ # Preserves type, side, terminal status, and derivation.
282
378
  #
283
379
  # @param new_state [Symbol] :normal, :enhanced, or :diminished
284
380
  # @return [Identifier] new identifier instance with different state
381
+ #
285
382
  # @example
286
383
  # identifier.with_state(:enhanced) # (:K, :first, :normal, true) => (:K, :first, :enhanced, true)
287
384
  def with_state(new_state)
288
385
  Pin::Identifier.validate_state(new_state)
289
386
  return self if state == new_state
290
387
 
291
- self.class.new(type, side, new_state, native)
388
+ self.class.new(type, side, new_state, native, terminal: terminal)
292
389
  end
293
390
 
294
- # Create a new identifier with a different derivation (keeping same type, side, and state)
391
+ # Create a new identifier with a different derivation
392
+ #
393
+ # Preserves type, side, state, and terminal status.
295
394
  #
296
395
  # @param new_native [Boolean] true for native, false for foreign
297
396
  # @return [Identifier] new identifier instance with different derivation
397
+ #
298
398
  # @example
299
399
  # identifier.with_derivation(false) # (:K, :first, :normal, true) => (:K, :first, :normal, false)
300
400
  def with_derivation(new_native)
301
401
  self.class.validate_derivation(new_native)
302
402
  return self if native == new_native
303
403
 
304
- self.class.new(type, side, state, new_native)
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)
305
422
  end
306
423
 
307
424
  # Check if the identifier has enhanced state
@@ -318,9 +435,9 @@ module Sashite
318
435
  @pin_identifier.diminished?
319
436
  end
320
437
 
321
- # Check if the identifier has normal state (no modifiers)
438
+ # Check if the identifier has normal state (no state modifiers)
322
439
  #
323
- # @return [Boolean] true if no modifiers are present
440
+ # @return [Boolean] true if normal
324
441
  def normal?
325
442
  @pin_identifier.normal?
326
443
  end
@@ -339,27 +456,44 @@ module Sashite
339
456
  @pin_identifier.second_player?
340
457
  end
341
458
 
342
- # Check if the identifier has native style (no derivation marker)
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.
343
472
  #
344
473
  # @return [Boolean] true if native style
345
474
  def native?
346
475
  native == NATIVE
347
476
  end
348
477
 
349
- # Check if the identifier has foreign style (derivation marker)
478
+ # Check if the identifier has foreign/derived style
479
+ #
480
+ # A derived piece has the foreign style (opposite side's native style).
350
481
  #
351
- # @return [Boolean] true if foreign style
482
+ # @return [Boolean] true if foreign/derived style
352
483
  def derived?
353
484
  native == FOREIGN
354
485
  end
355
486
 
356
- # Alias for derived? to match the specification terminology
487
+ # Alias for derived? to match specification terminology
357
488
  alias foreign? derived?
358
489
 
359
- # Check if this identifier is the same type as another (ignoring side, state, and derivation)
490
+ # Check if this identifier is the same type as another
491
+ #
492
+ # Ignores side, state, terminal status, and derivation.
360
493
  #
361
494
  # @param other [Identifier] identifier to compare with
362
495
  # @return [Boolean] true if same type
496
+ #
363
497
  # @example
364
498
  # king1.same_type?(king2) # (:K, :first, :normal, true) and (:K, :second, :enhanced, false) => true
365
499
  def same_type?(other)
@@ -388,6 +522,16 @@ module Sashite
388
522
  @pin_identifier.same_state?(other.instance_variable_get(:@pin_identifier))
389
523
  end
390
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
+
391
535
  # Check if this identifier has the same style derivation as another
392
536
  #
393
537
  # @param other [Identifier] identifier to compare with
@@ -400,6 +544,9 @@ module Sashite
400
544
 
401
545
  # Custom equality comparison
402
546
  #
547
+ # Two identifiers are equal if they have the same type, side, state,
548
+ # terminal status, and derivation.
549
+ #
403
550
  # @param other [Object] object to compare with
404
551
  # @return [Boolean] true if identifiers are equal
405
552
  def ==(other)
data/lib/sashite/epin.rb CHANGED
@@ -9,16 +9,20 @@ module Sashite
9
9
  # EPIN extends PIN by adding derivation markers that distinguish pieces by their style origin,
10
10
  # enabling cross-style game scenarios and piece origin tracking.
11
11
  #
12
- # Format: [<state>]<letter>[<derivation>]
12
+ # Format: [<state>]<letter>[<terminal>][<derivation>]
13
13
  # - State modifier: "+" (enhanced), "-" (diminished), or none (normal)
14
14
  # - Letter: A-Z (first player), a-z (second player)
15
+ # - Terminal marker: "^" (terminal piece)
15
16
  # - Derivation marker: "'" (foreign style), or none (native style)
16
17
  #
17
18
  # Examples:
18
- # "K" - First player king (native style, normal state)
19
- # "k'" - Second player king (foreign style, normal state)
20
- # "+R'" - First player rook (foreign style, enhanced state)
21
- # "-p" - Second player pawn (native style, diminished state)
19
+ # "K" - First player king (native style, normal state, non-terminal)
20
+ # "K^" - First player king (native style, normal state, terminal)
21
+ # "k'" - Second player king (foreign style, normal state, non-terminal)
22
+ # "k^'" - Second player king (foreign style, normal state, terminal)
23
+ # "+R'" - First player rook (foreign style, enhanced state, non-terminal)
24
+ # "+K^'" - First player king (foreign style, enhanced state, terminal)
25
+ # "-p" - Second player pawn (native style, diminished state, non-terminal)
22
26
  #
23
27
  # @see https://sashite.dev/specs/epin/1.0.0/
24
28
  module Epin
@@ -28,11 +32,14 @@ module Sashite
28
32
  # @return [Boolean] true if valid EPIN, false otherwise
29
33
  #
30
34
  # @example
31
- # Sashite::Epin.valid?("K") # => true
32
- # Sashite::Epin.valid?("+R'") # => true
33
- # Sashite::Epin.valid?("-p") # => true
34
- # Sashite::Epin.valid?("KK") # => false
35
- # Sashite::Epin.valid?("++K") # => false
35
+ # Sashite::Epin.valid?("K") # => true
36
+ # Sashite::Epin.valid?("K^") # => true
37
+ # Sashite::Epin.valid?("+R'") # => true
38
+ # Sashite::Epin.valid?("+K^'") # => true
39
+ # Sashite::Epin.valid?("-p") # => true
40
+ # Sashite::Epin.valid?("KK") # => false
41
+ # Sashite::Epin.valid?("++K") # => false
42
+ # Sashite::Epin.valid?("K'^") # => false (wrong order)
36
43
  def self.valid?(epin_string)
37
44
  Identifier.valid?(epin_string)
38
45
  end
@@ -42,10 +49,13 @@ module Sashite
42
49
  # @param epin_string [String] EPIN notation string
43
50
  # @return [Epin::Identifier] new identifier instance
44
51
  # @raise [ArgumentError] if the EPIN string is invalid
52
+ #
45
53
  # @example
46
- # Sashite::Epin.parse("K") # => #<Epin::Identifier type=:K side=:first state=:normal native=true>
47
- # Sashite::Epin.parse("+R'") # => #<Epin::Identifier type=:R side=:first state=:enhanced native=false>
48
- # Sashite::Epin.parse("-p") # => #<Epin::Identifier type=:P side=:second state=:diminished native=true>
54
+ # Sashite::Epin.parse("K") # => #<Epin::Identifier type=:K side=:first state=:normal native=true terminal=false>
55
+ # Sashite::Epin.parse("K^") # => #<Epin::Identifier type=:K side=:first state=:normal native=true terminal=true>
56
+ # Sashite::Epin.parse("+R'") # => #<Epin::Identifier type=:R side=:first state=:enhanced native=false terminal=false>
57
+ # Sashite::Epin.parse("+K^'") # => #<Epin::Identifier type=:K side=:first state=:enhanced native=false terminal=true>
58
+ # Sashite::Epin.parse("-p") # => #<Epin::Identifier type=:P side=:second state=:diminished native=true terminal=false>
49
59
  def self.parse(epin_string)
50
60
  Identifier.parse(epin_string)
51
61
  end
@@ -56,14 +66,18 @@ module Sashite
56
66
  # @param side [Symbol] player side (:first or :second)
57
67
  # @param state [Symbol] piece state (:normal, :enhanced, or :diminished)
58
68
  # @param native [Boolean] style derivation (true for native, false for foreign)
69
+ # @param terminal [Boolean] whether the piece is a terminal piece
59
70
  # @return [Epin::Identifier] new identifier instance
60
71
  # @raise [ArgumentError] if parameters are invalid
72
+ #
61
73
  # @example
62
- # Sashite::Epin.identifier(:K, :first, :normal, true) # => #<Epin::Identifier type=:K side=:first state=:normal native=true>
63
- # Sashite::Epin.identifier(:R, :first, :enhanced, false) # => #<Epin::Identifier type=:R side=:first state=:enhanced native=false>
64
- # Sashite::Epin.identifier(:P, :second, :diminished, true) # => #<Epin::Identifier type=:P side=:second state=:diminished native=true>
65
- def self.identifier(type, side, state, native)
66
- Identifier.new(type, side, state, native)
74
+ # Sashite::Epin.identifier(:K, :first, :normal, true) # => "K"
75
+ # Sashite::Epin.identifier(:K, :first, :normal, true, terminal: true) # => "K^"
76
+ # Sashite::Epin.identifier(:R, :first, :enhanced, false) # => "+R'"
77
+ # Sashite::Epin.identifier(:K, :first, :enhanced, false, terminal: true) # => "+K^'"
78
+ # Sashite::Epin.identifier(:P, :second, :diminished, true) # => "-p"
79
+ def self.identifier(type, side, state, native, terminal: false)
80
+ Identifier.new(type, side, state, native, terminal: terminal)
67
81
  end
68
82
  end
69
83
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: sashite-epin
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.1.0
4
+ version: 1.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Cyril Kato
@@ -15,14 +15,14 @@ dependencies:
15
15
  requirements:
16
16
  - - "~>"
17
17
  - !ruby/object:Gem::Version
18
- version: 3.1.0
18
+ version: 3.2.0
19
19
  type: :runtime
20
20
  prerelease: false
21
21
  version_requirements: !ruby/object:Gem::Requirement
22
22
  requirements:
23
23
  - - "~>"
24
24
  - !ruby/object:Gem::Version
25
- version: 3.1.0
25
+ version: 3.2.0
26
26
  description: |
27
27
  EPIN (Extended Piece Identifier Notation) extends PIN to provide style-aware piece representation
28
28
  in abstract strategy board games. This gem implements the EPIN Specification v1.0.0 with
@@ -65,7 +65,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
65
65
  - !ruby/object:Gem::Version
66
66
  version: '0'
67
67
  requirements: []
68
- rubygems_version: 3.6.9
68
+ rubygems_version: 3.7.2
69
69
  specification_version: 4
70
70
  summary: EPIN (Extended Piece Identifier Notation) implementation for Ruby extending
71
71
  PIN with style derivation markers.