sashite-pin 3.1.0 → 3.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: e469a5469a29de49338f5066b5b5743860459709ab2e771209d1c55ab7e7ff0d
4
- data.tar.gz: 680f65379ee7ea0ead2238ed646a29ee54d673a80de7f0c54f1c56a11cf8f98f
3
+ metadata.gz: 5f278c657907ec277b42f6c5f7b22b5df56d6cb57bd6438eeda33c3c847294f9
4
+ data.tar.gz: 96e8c58b731ea334214aa5803e27a7fdd629f17df38b1eb55ddf8654397bd5f5
5
5
  SHA512:
6
- metadata.gz: 1a4b4ce38433cdf1cd9a26baaba13fa8e48ff21676976287422ac917d3a41bc67e7eb688bf347a94cefb294542e98fa7d2bd292f923b7a8c6b66d73edfaebdd2
7
- data.tar.gz: 5da68a02154b85ab6a9b1070954f14be0a4f79e11e07c54448c0aae40772e4664c2b3368067a44c8fdef5a43b122a3c868e2cc8c6df5ed52f46afa2a2b65f021
6
+ metadata.gz: d451a383989a72da646522113f4ece370b88d57edc940c932cc238311dde1d3f52e477858a1dabba6ff7ee2b9d1a2ee561c9827230ecc3986c421ded53814634
7
+ data.tar.gz: 74cf7b9dd82202b8999951d42ae44bac1cd84cc286819268732070cc8b0bde769984de64a374bcd72ab1e5eaa803dd84e452e85f657163d5bd8d8512c978318b
data/README.md CHANGED
@@ -32,19 +32,28 @@ gem install sashite-pin
32
32
  require "sashite/pin"
33
33
 
34
34
  # Parse PIN strings into identifier objects
35
- identifier = Sashite::Pin.parse("K") # => #<Pin::Identifier type=:K side=:first state=:normal>
35
+ identifier = Sashite::Pin.parse("K") # => #<Pin::Identifier type=:K side=:first state=:normal terminal=false>
36
36
  identifier.to_s # => "K"
37
37
  identifier.type # => :K
38
38
  identifier.side # => :first
39
39
  identifier.state # => :normal
40
+ identifier.terminal? # => false
41
+
42
+ # Parse terminal pieces (e.g., kings in chess)
43
+ terminal_king = Sashite::Pin.parse("K^") # => #<Pin::Identifier type=:K side=:first state=:normal terminal=true>
44
+ terminal_king.to_s # => "K^"
45
+ terminal_king.terminal? # => true
40
46
 
41
47
  # Create identifiers directly
42
- identifier = Sashite::Pin.identifier(:K, :first, :normal) # => #<Pin::Identifier type=:K side=:first state=:normal>
48
+ identifier = Sashite::Pin.identifier(:K, :first, :normal) # => #<Pin::Identifier type=:K side=:first state=:normal>
49
+ terminal_king = Sashite::Pin.identifier(:K, :first, :normal, terminal: true) # => #<Pin::Identifier type=:K side=:first state=:normal terminal=true>
43
50
  identifier = Sashite::Pin::Identifier.new(:R, :second, :enhanced) # => #<Pin::Identifier type=:R side=:second state=:enhanced>
44
51
 
45
52
  # Validate PIN strings
46
53
  Sashite::Pin.valid?("K") # => true
47
54
  Sashite::Pin.valid?("+R") # => true
55
+ Sashite::Pin.valid?("K^") # => true
56
+ Sashite::Pin.valid?("+K^") # => true
48
57
  Sashite::Pin.valid?("invalid") # => false
49
58
 
50
59
  # State manipulation (returns new immutable instances)
@@ -53,6 +62,11 @@ enhanced.to_s # => "+K"
53
62
  diminished = identifier.diminish # => #<Pin::Identifier type=:K side=:first state=:diminished>
54
63
  diminished.to_s # => "-K"
55
64
 
65
+ # Terminal marker manipulation
66
+ normal_king = Sashite::Pin.parse("K")
67
+ terminal_king = normal_king.mark_terminal # => "K^"
68
+ back_to_normal = terminal_king.unmark_terminal # => "K"
69
+
56
70
  # Side manipulation
57
71
  flipped = identifier.flip # => #<Pin::Identifier type=:K side=:second state=:normal>
58
72
  flipped.to_s # => "k"
@@ -74,6 +88,7 @@ flipped.second_player? # => true
74
88
  identifier.letter # => "K"
75
89
  enhanced.prefix # => "+"
76
90
  identifier.prefix # => ""
91
+ terminal_king.suffix # => "^"
77
92
 
78
93
  # Type and side comparison
79
94
  king1 = Sashite::Pin.parse("K")
@@ -87,6 +102,10 @@ king1.same_type?(queen) # => false (different types)
87
102
  # Functional transformations can be chained
88
103
  pawn = Sashite::Pin.parse("P")
89
104
  enemy_promoted = pawn.flip.enhance # => "+p" (second player promoted pawn)
105
+
106
+ # Transformations preserve terminal status
107
+ terminal_piece = Sashite::Pin.parse("K^")
108
+ enhanced_terminal = terminal_piece.enhance # => "+K^"
90
109
  ```
91
110
 
92
111
  ## Format Specification
@@ -94,7 +113,7 @@ enemy_promoted = pawn.flip.enhance # => "+p" (second player promoted
94
113
  ### Structure
95
114
 
96
115
  ```
97
- [<state>]<letter>
116
+ [<state-modifier>]<letter>[<terminal-marker>]
98
117
  ```
99
118
 
100
119
  ### Components
@@ -103,23 +122,30 @@ enemy_promoted = pawn.flip.enhance # => "+p" (second player promoted
103
122
 
104
123
  * Uppercase: First player pieces
105
124
  * Lowercase: Second player pieces
106
- * **State** (optional prefix):
125
+ * **State Modifier** (optional prefix):
107
126
 
108
127
  * `+`: Enhanced state (promoted, upgraded, empowered)
109
128
  * `-`: Diminished state (weakened, restricted, temporary)
110
129
  * No prefix: Normal state
130
+ * **Terminal Marker** (optional suffix):
131
+
132
+ * `^`: Terminal piece (critical to match continuation)
133
+ * No suffix: Non-terminal piece
111
134
 
112
135
  ### Regular Expression
113
136
 
114
137
  ```ruby
115
- /\A[-+]?[A-Za-z]\z/
138
+ /\A[-+]?[A-Za-z]\^?\z/
116
139
  ```
117
140
 
118
141
  ### Examples
119
142
 
120
143
  * `K` - First player king (normal state)
144
+ * `K^` - First player king (normal state, terminal)
121
145
  * `k` - Second player king (normal state)
146
+ * `k^` - Second player king (normal state, terminal)
122
147
  * `+R` - First player rook (enhanced state)
148
+ * `+R^` - First player rook (enhanced state, terminal)
123
149
  * `-p` - Second player pawn (diminished state)
124
150
 
125
151
  ## API Reference
@@ -128,13 +154,13 @@ enemy_promoted = pawn.flip.enhance # => "+p" (second player promoted
128
154
 
129
155
  * `Sashite::Pin.valid?(pin_string)` - Check if string is valid PIN notation
130
156
  * `Sashite::Pin.parse(pin_string)` - Parse PIN string into Identifier object
131
- * `Sashite::Pin.identifier(type, side, state = :normal)` - Create identifier instance directly
157
+ * `Sashite::Pin.identifier(type, side, state = :normal, terminal: false)` - Create identifier instance directly
132
158
 
133
159
  ### Identifier Class
134
160
 
135
161
  #### Creation and Parsing
136
162
 
137
- * `Sashite::Pin::Identifier.new(type, side, state = :normal)` - Create identifier instance
163
+ * `Sashite::Pin::Identifier.new(type, side, state = :normal, terminal: false)` - Create identifier instance
138
164
  * `Sashite::Pin::Identifier.parse(pin_string)` - Parse PIN string (same as module method)
139
165
  * `Sashite::Pin::Identifier.valid?(pin_string)` - Validate PIN string (class method)
140
166
 
@@ -143,8 +169,10 @@ enemy_promoted = pawn.flip.enhance # => "+p" (second player promoted
143
169
  * `#type` - Get piece type (symbol \:A to \:Z, always uppercase)
144
170
  * `#side` - Get player side (\:first or \:second)
145
171
  * `#state` - Get state (\:normal, \:enhanced, or \:diminished)
172
+ * `#terminal` - Get terminal status (Boolean)
146
173
  * `#letter` - Get letter representation (string, case determined by side)
147
174
  * `#prefix` - Get state prefix (string: "+", "-", or "")
175
+ * `#suffix` - Get terminal marker (string: "^" or "")
148
176
  * `#to_s` - Convert to PIN string representation
149
177
 
150
178
  #### Type and Case Handling
@@ -174,6 +202,10 @@ identifier2.letter # => "k" (lowercase display)
174
202
  * `#first_player?` - Check if first player identifier
175
203
  * `#second_player?` - Check if second player identifier
176
204
 
205
+ #### Terminal Queries
206
+
207
+ * `#terminal?` - Check if terminal piece
208
+
177
209
  #### State Transformations (immutable - return new instances)
178
210
 
179
211
  * `#enhance` - Create enhanced version
@@ -189,16 +221,24 @@ identifier2.letter # => "k" (lowercase display)
189
221
  * `#with_side(new_side)` - Create identifier with different side
190
222
  * `#with_state(new_state)` - Create identifier with different state
191
223
 
224
+ #### Terminal Transformations (immutable - return new instances)
225
+
226
+ * `#mark_terminal` - Create terminal version
227
+ * `#unmark_terminal` - Create non-terminal version
228
+ * `#with_terminal(boolean)` - Create identifier with specified terminal status
229
+
192
230
  #### Comparison Methods
193
231
 
194
232
  * `#same_type?(other)` - Check if same piece type
195
233
  * `#same_side?(other)` - Check if same side
196
234
  * `#same_state?(other)` - Check if same state
235
+ * `#same_terminal?(other)` - Check if same terminal status
197
236
  * `#==(other)` - Full equality comparison
198
237
 
199
238
  ### Constants
200
239
 
201
240
  * `Sashite::Pin::Identifier::PIN_PATTERN` - Regular expression for PIN validation (internal use)
241
+ * `Sashite::Pin::Identifier::TERMINAL_MARKER` - Terminal marker character (`"^"`)
202
242
 
203
243
  ## Advanced Usage
204
244
 
@@ -229,7 +269,7 @@ white_king.same_side?(black_king) # => false
229
269
  ### Immutable Transformations
230
270
  ```ruby
231
271
  # All transformations return new instances
232
- original = Sashite::Pin.piece(:K, :first, :normal)
272
+ original = Sashite::Pin.identifier(:K, :first, :normal)
233
273
  enhanced = original.enhance
234
274
  diminished = original.diminish
235
275
 
@@ -241,6 +281,11 @@ puts diminished.to_s # => "-K"
241
281
  # Transformations can be chained
242
282
  result = original.flip.enhance.with_type(:Q)
243
283
  puts result.to_s # => "+q"
284
+
285
+ # Terminal status is preserved through transformations
286
+ terminal_king = Sashite::Pin.parse("K^")
287
+ enhanced_terminal = terminal_king.enhance
288
+ puts enhanced_terminal.to_s # => "+K^"
244
289
  ```
245
290
 
246
291
  ### Game State Management
@@ -274,13 +319,17 @@ class GameBoard
274
319
  def promoted_pieces
275
320
  @pieces.select { |_, piece| piece.enhanced? }
276
321
  end
322
+
323
+ def terminal_pieces
324
+ @pieces.select { |_, piece| piece.terminal? }
325
+ end
277
326
  end
278
327
 
279
328
  # Usage
280
329
  board = GameBoard.new
281
- board.place("e1", Sashite::Pin.piece(:K, :first, :normal))
282
- board.place("e8", Sashite::Pin.piece(:K, :second, :normal))
283
- board.place("a7", Sashite::Pin.piece(:P, :first, :normal))
330
+ board.place("e1", Sashite::Pin.identifier(:K, :first, :normal, terminal: true))
331
+ board.place("e8", Sashite::Pin.identifier(:K, :second, :normal, terminal: true))
332
+ board.place("a7", Sashite::Pin.identifier(:P, :first, :normal))
284
333
 
285
334
  # Promote pawn
286
335
  board.promote("a7", :Q)
@@ -299,14 +348,16 @@ def analyze_pieces(pins)
299
348
  by_type: pieces.group_by(&:type),
300
349
  by_state: pieces.group_by(&:state),
301
350
  promoted: pieces.count(&:enhanced?),
302
- weakened: pieces.count(&:diminished?)
351
+ weakened: pieces.count(&:diminished?),
352
+ terminal: pieces.count(&:terminal?)
303
353
  }
304
354
  end
305
355
 
306
- pins = %w[K Q +R B N P k q r +b n -p]
356
+ pins = %w[K^ Q +R B N P k^ q r +b n -p]
307
357
  analysis = analyze_pieces(pins)
308
358
  puts analysis[:by_side][:first].size # => 6
309
359
  puts analysis[:promoted] # => 2
360
+ puts analysis[:terminal] # => 2
310
361
  ```
311
362
 
312
363
  ### Move Validation Example
@@ -325,7 +376,7 @@ def can_promote?(piece, target_rank)
325
376
  end
326
377
  end
327
378
 
328
- pawn = Sashite::Pin.piece(:P, :first, :normal)
379
+ pawn = Sashite::Pin.identifier(:P, :first, :normal)
329
380
  puts can_promote?(pawn, 8) # => true
330
381
 
331
382
  promoted_pawn = pawn.enhance
@@ -338,9 +389,10 @@ Following the [Game Protocol](https://sashite.dev/game-protocol/):
338
389
 
339
390
  | Protocol Attribute | PIN Encoding | Examples | Notes |
340
391
  |-------------------|--------------|----------|-------|
341
- | **Type** | ASCII letter choice | `K`/`k` = King, `P`/`p` = Pawn | Type is always stored as uppercase symbol (`:K`, `:P`) |
342
- | **Side** | Letter case in display | `K` = First player, `k` = Second player | Case is determined by side during rendering |
343
- | **State** | Optional prefix | `+K` = Enhanced, `-K` = Diminished, `K` = Normal | |
392
+ | **Piece Name** | ASCII letter choice | `K`/`k` = King, `P`/`p` = Pawn | Type is always stored as uppercase symbol (`:K`, `:P`) |
393
+ | **Piece Side** | Letter case in display | `K` = First player, `k` = Second player | Case is determined by side during rendering |
394
+ | **Piece State** | Optional prefix | `+K` = Enhanced, `-K` = Diminished, `K` = Normal | |
395
+ | **Terminal Status** | Optional suffix | `K^` = Terminal, `K` = Non-terminal | Identifies pieces critical to match continuation |
344
396
 
345
397
  **Type Convention**: All piece types are internally represented as uppercase symbols (`:A` to `:Z`). The display case is determined by the `side` attribute: first player pieces display as uppercase, second player pieces as lowercase.
346
398
 
@@ -352,9 +404,10 @@ Following the [Game Protocol](https://sashite.dev/game-protocol/):
352
404
 
353
405
  * **ASCII Compatible**: Maximum portability across systems
354
406
  * **Rule-Agnostic**: Independent of specific game mechanics
355
- * **Compact Format**: 1-2 characters per piece
407
+ * **Compact Format**: 1-3 characters per piece
356
408
  * **Visual Distinction**: Clear player differentiation through case
357
409
  * **Type Normalization**: Consistent uppercase type representation internally
410
+ * **Terminal Marker**: Explicit identification of pieces critical to match continuation
358
411
  * **Protocol Compliant**: Direct implementation of Sashité piece attributes
359
412
  * **Immutable**: All piece instances are frozen and transformations return new objects
360
413
  * **Functional**: Pure functions with no side effects
@@ -375,6 +428,15 @@ This design ensures:
375
428
  - Clear separation between piece identity (type) and ownership (side)
376
429
  - Predictable behavior when comparing pieces of the same type
377
430
 
431
+ ### Terminal Marker Convention
432
+
433
+ The terminal marker (`^`) identifies pieces whose presence, condition, or capacity for action determines whether the match can continue:
434
+
435
+ 1. **Suffix Position**: Always appears as the last character (`K^`, `+K^`, `-k^`)
436
+ 2. **Preservation**: Terminal status is preserved through all transformations
437
+ 3. **Equality**: Two pieces are equal only if they have the same terminal status
438
+ 4. **Independence**: Terminal status is independent of state (normal/enhanced/diminished)
439
+
378
440
  ### Example Flow
379
441
 
380
442
  ```ruby
@@ -394,6 +456,7 @@ This ensures that `parse(pin).to_s == pin` for all valid PIN strings while maint
394
456
  - **Maximum 26 piece types** per game system (one per ASCII letter)
395
457
  - **Exactly 2 players** (uppercase/lowercase distinction)
396
458
  - **3 state levels** (enhanced, normal, diminished)
459
+ - **2 terminal levels** (terminal, non-terminal)
397
460
 
398
461
  ## Related Specifications
399
462
 
@@ -17,13 +17,16 @@ module Sashite
17
17
  # This follows the Game Protocol's piece model with Type, Side, and State attributes.
18
18
  class Identifier
19
19
  # PIN validation pattern matching the specification
20
- PIN_PATTERN = /\A(?<prefix>[-+])?(?<letter>[a-zA-Z])\z/
20
+ PIN_PATTERN = /\A(?<prefix>[-+])?(?<letter>[a-zA-Z])(?<terminal>\^)?\z/
21
21
 
22
22
  # Valid state modifiers
23
23
  ENHANCED_PREFIX = "+"
24
24
  DIMINISHED_PREFIX = "-"
25
25
  NORMAL_PREFIX = ""
26
26
 
27
+ # Terminal marker
28
+ TERMINAL_MARKER = "^"
29
+
27
30
  # State constants
28
31
  ENHANCED_STATE = :enhanced
29
32
  DIMINISHED_STATE = :diminished
@@ -57,13 +60,17 @@ module Sashite
57
60
  # @return [Symbol] the piece state (:normal, :enhanced, or :diminished)
58
61
  attr_reader :state
59
62
 
63
+ # @return [Boolean] whether the piece is a terminal piece
64
+ attr_reader :terminal
65
+
60
66
  # Create a new identifier instance
61
67
  #
62
68
  # @param type [Symbol] piece type (:A to :Z)
63
69
  # @param side [Symbol] player side (:first or :second)
64
70
  # @param state [Symbol] piece state (:normal, :enhanced, or :diminished)
71
+ # @param terminal [Boolean] whether the piece is a terminal piece
65
72
  # @raise [ArgumentError] if parameters are invalid
66
- def initialize(type, side, state = NORMAL_STATE)
73
+ def initialize(type, side, state = NORMAL_STATE, terminal: false)
67
74
  self.class.validate_type(type)
68
75
  self.class.validate_side(side)
69
76
  self.class.validate_state(state)
@@ -71,6 +78,7 @@ module Sashite
71
78
  @type = type
72
79
  @side = side
73
80
  @state = state
81
+ @terminal = !!terminal
74
82
 
75
83
  freeze
76
84
  end
@@ -81,9 +89,11 @@ module Sashite
81
89
  # @return [Identifier] new identifier instance
82
90
  # @raise [ArgumentError] if the PIN string is invalid
83
91
  # @example
84
- # Pin::Identifier.parse("k") # => #<Pin::Identifier type=:K side=:second state=:normal>
85
- # Pin::Identifier.parse("+R") # => #<Pin::Identifier type=:R side=:first state=:enhanced>
86
- # Pin::Identifier.parse("-p") # => #<Pin::Identifier type=:P side=:second state=:diminished>
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>
87
97
  def self.parse(pin_string)
88
98
  string_value = String(pin_string)
89
99
  matches = match_pattern(string_value)
@@ -91,6 +101,7 @@ module Sashite
91
101
  letter = matches[:letter]
92
102
  enhanced = matches[:prefix] == ENHANCED_PREFIX
93
103
  diminished = matches[:prefix] == DIMINISHED_PREFIX
104
+ is_terminal = matches[:terminal] == TERMINAL_MARKER
94
105
 
95
106
  type = letter.upcase.to_sym
96
107
  side = letter == letter.upcase ? FIRST_PLAYER : SECOND_PLAYER
@@ -102,7 +113,7 @@ module Sashite
102
113
  NORMAL_STATE
103
114
  end
104
115
 
105
- new(type, side, state)
116
+ new(type, side, state, terminal: is_terminal)
106
117
  end
107
118
 
108
119
  # Check if a string is a valid PIN notation
@@ -127,8 +138,10 @@ module Sashite
127
138
  # @return [String] PIN notation string
128
139
  # @example
129
140
  # identifier.to_s # => "+R"
141
+ # terminal_king.to_s # => "K^"
142
+ # enhanced_terminal.to_s # => "+K^"
130
143
  def to_s
131
- "#{prefix}#{letter}"
144
+ "#{prefix}#{letter}#{suffix}"
132
145
  end
133
146
 
134
147
  # Get the letter representation
@@ -149,13 +162,20 @@ module Sashite
149
162
  end
150
163
  end
151
164
 
165
+ # Get the suffix representation
166
+ #
167
+ # @return [String] suffix representing terminal status
168
+ def suffix
169
+ terminal? ? TERMINAL_MARKER : ""
170
+ end
171
+
152
172
  # Create a new identifier with enhanced state
153
173
  #
154
174
  # @return [Identifier] new identifier instance with enhanced state
155
175
  def enhance
156
176
  return self if enhanced?
157
177
 
158
- self.class.new(type, side, ENHANCED_STATE)
178
+ self.class.new(type, side, ENHANCED_STATE, terminal: terminal)
159
179
  end
160
180
 
161
181
  # Create a new identifier without enhanced state
@@ -164,7 +184,7 @@ module Sashite
164
184
  def unenhance
165
185
  return self unless enhanced?
166
186
 
167
- self.class.new(type, side, NORMAL_STATE)
187
+ self.class.new(type, side, NORMAL_STATE, terminal: terminal)
168
188
  end
169
189
 
170
190
  # Create a new identifier with diminished state
@@ -173,7 +193,7 @@ module Sashite
173
193
  def diminish
174
194
  return self if diminished?
175
195
 
176
- self.class.new(type, side, DIMINISHED_STATE)
196
+ self.class.new(type, side, DIMINISHED_STATE, terminal: terminal)
177
197
  end
178
198
 
179
199
  # Create a new identifier without diminished state
@@ -182,7 +202,7 @@ module Sashite
182
202
  def undiminish
183
203
  return self unless diminished?
184
204
 
185
- self.class.new(type, side, NORMAL_STATE)
205
+ self.class.new(type, side, NORMAL_STATE, terminal: terminal)
186
206
  end
187
207
 
188
208
  # Create a new identifier with normal state (no modifiers)
@@ -191,14 +211,32 @@ module Sashite
191
211
  def normalize
192
212
  return self if normal?
193
213
 
194
- self.class.new(type, side, NORMAL_STATE)
214
+ self.class.new(type, side, NORMAL_STATE, terminal: terminal)
215
+ end
216
+
217
+ # Create a new identifier marked as terminal
218
+ #
219
+ # @return [Identifier] new identifier instance marked as terminal
220
+ def mark_terminal
221
+ return self if terminal?
222
+
223
+ self.class.new(type, side, state, terminal: true)
224
+ end
225
+
226
+ # Create a new identifier unmarked as terminal
227
+ #
228
+ # @return [Identifier] new identifier instance unmarked as terminal
229
+ def unmark_terminal
230
+ return self unless terminal?
231
+
232
+ self.class.new(type, side, state, terminal: false)
195
233
  end
196
234
 
197
235
  # Create a new identifier with opposite side
198
236
  #
199
237
  # @return [Identifier] new identifier instance with opposite side
200
238
  def flip
201
- self.class.new(type, opposite_side, state)
239
+ self.class.new(type, opposite_side, state, terminal: terminal)
202
240
  end
203
241
 
204
242
  # Create a new identifier with a different type
@@ -209,7 +247,7 @@ module Sashite
209
247
  self.class.validate_type(new_type)
210
248
  return self if type == new_type
211
249
 
212
- self.class.new(new_type, side, state)
250
+ self.class.new(new_type, side, state, terminal: terminal)
213
251
  end
214
252
 
215
253
  # Create a new identifier with a different side
@@ -220,7 +258,7 @@ module Sashite
220
258
  self.class.validate_side(new_side)
221
259
  return self if side == new_side
222
260
 
223
- self.class.new(type, new_side, state)
261
+ self.class.new(type, new_side, state, terminal: terminal)
224
262
  end
225
263
 
226
264
  # Create a new identifier with a different state
@@ -231,7 +269,18 @@ module Sashite
231
269
  self.class.validate_state(new_state)
232
270
  return self if state == new_state
233
271
 
234
- self.class.new(type, side, new_state)
272
+ self.class.new(type, side, new_state, terminal: terminal)
273
+ end
274
+
275
+ # Create a new identifier with a different terminal status
276
+ #
277
+ # @param new_terminal [Boolean] new terminal status
278
+ # @return [Identifier] new identifier instance with new terminal status
279
+ def with_terminal(new_terminal)
280
+ new_terminal_bool = !!new_terminal
281
+ return self if terminal? == new_terminal_bool
282
+
283
+ self.class.new(type, side, state, terminal: new_terminal_bool)
235
284
  end
236
285
 
237
286
  # Check if the identifier has enhanced state
@@ -269,6 +318,13 @@ module Sashite
269
318
  side == SECOND_PLAYER
270
319
  end
271
320
 
321
+ # Check if the identifier is a terminal piece
322
+ #
323
+ # @return [Boolean] true if terminal
324
+ def terminal?
325
+ terminal
326
+ end
327
+
272
328
  # Check if this identifier is the same type as another
273
329
  #
274
330
  # @param other [Identifier] identifier to compare with
@@ -299,6 +355,16 @@ module Sashite
299
355
  state == other.state
300
356
  end
301
357
 
358
+ # Check if this identifier has the same terminal status as another
359
+ #
360
+ # @param other [Identifier] identifier to compare with
361
+ # @return [Boolean] true if same terminal status
362
+ def same_terminal?(other)
363
+ return false unless other.is_a?(self.class)
364
+
365
+ terminal? == other.terminal?
366
+ end
367
+
302
368
  # Custom equality comparison
303
369
  #
304
370
  # @param other [Object] object to compare with
@@ -306,7 +372,7 @@ module Sashite
306
372
  def ==(other)
307
373
  return false unless other.is_a?(self.class)
308
374
 
309
- type == other.type && side == other.side && state == other.state
375
+ type == other.type && side == other.side && state == other.state && terminal? == other.terminal?
310
376
  end
311
377
 
312
378
  # Alias for == to ensure Set functionality works correctly
@@ -316,7 +382,7 @@ module Sashite
316
382
  #
317
383
  # @return [Integer] hash value
318
384
  def hash
319
- [self.class, type, side, state].hash
385
+ [self.class, type, side, state, terminal?].hash
320
386
  end
321
387
 
322
388
  # Validate that the type is a valid symbol
data/lib/sashite/pin.rb CHANGED
@@ -53,14 +53,16 @@ module Sashite
53
53
  # @param type [Symbol] piece type (:A to :Z)
54
54
  # @param side [Symbol] player side (:first or :second)
55
55
  # @param state [Symbol] piece state (:normal, :enhanced, or :diminished)
56
+ # @param terminal [Boolean] whether the piece is a terminal piece
56
57
  # @return [Pin::Identifier] new identifier instance
57
58
  # @raise [ArgumentError] if parameters are invalid
58
59
  # @example
59
- # Sashite::Pin.identifier(:K, :first, :normal) # => #<Pin::Identifier type=:K side=:first state=:normal>
60
- # Sashite::Pin.identifier(:R, :first, :enhanced) # => #<Pin::Identifier type=:R side=:first state=:enhanced>
61
- # Sashite::Pin.identifier(:P, :second, :diminished) # => #<Pin::Identifier type=:P side=:second state=:diminished>
62
- def self.identifier(type, side, state)
63
- Identifier.new(type, side, state)
60
+ # Sashite::Pin.identifier(:K, :first, :normal) # => #<Pin::Identifier type=:K side=:first state=:normal terminal=false>
61
+ # Sashite::Pin.identifier(:R, :first, :enhanced) # => #<Pin::Identifier type=:R side=:first state=:enhanced terminal=false>
62
+ # Sashite::Pin.identifier(:P, :second, :diminished) # => #<Pin::Identifier type=:P side=:second state=:diminished terminal=false>
63
+ # Sashite::Pin.identifier(:K, :first, :normal, terminal: true) # => #<Pin::Identifier type=:K side=:first state=:normal terminal=true>
64
+ def self.identifier(type, side, state, terminal: false)
65
+ Identifier.new(type, side, state, terminal: terminal)
64
66
  end
65
67
  end
66
68
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: sashite-pin
3
3
  version: !ruby/object:Gem::Version
4
- version: 3.1.0
4
+ version: 3.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Cyril Kato
@@ -13,10 +13,11 @@ description: |
13
13
  PIN (Piece Identifier Notation) provides a rule-agnostic format for identifying pieces
14
14
  in abstract strategy board games. This gem implements the PIN Specification v1.0.0 with
15
15
  a modern Ruby interface featuring immutable identifier objects and functional programming
16
- principles. PIN uses single ASCII letters with optional state modifiers and case-based
17
- side encoding (A-Z for first player, a-z for second player), enabling precise and portable
18
- identification of pieces across multiple games. Perfect for game engines, board game notation
19
- systems, and hybrid gaming platforms requiring compact, stateful piece representation.
16
+ principles. PIN uses single ASCII letters with optional state modifiers, terminal markers,
17
+ and case-based side encoding (A-Z for first player, a-z for second player), enabling
18
+ precise and portable identification of pieces across multiple games. Perfect for game
19
+ engines, board game notation systems, and hybrid gaming platforms requiring compact,
20
+ stateful piece representation.
20
21
  email: contact@cyril.email
21
22
  executables: []
22
23
  extensions: []
@@ -51,7 +52,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
51
52
  - !ruby/object:Gem::Version
52
53
  version: '0'
53
54
  requirements: []
54
- rubygems_version: 3.6.9
55
+ rubygems_version: 3.7.2
55
56
  specification_version: 4
56
57
  summary: PIN (Piece Identifier Notation) implementation for Ruby with immutable identifier
57
58
  objects