sashite-pnn 2.0.0 → 3.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.
data/README.md CHANGED
@@ -9,512 +9,259 @@
9
9
 
10
10
  ## What is PNN?
11
11
 
12
- PNN (Piece Name Notation) extends [PIN (Piece Identifier Notation)](https://sashite.dev/specs/pin/1.0.0/) to provide style-aware piece representation in abstract strategy board games. PNN adds a derivation marker that distinguishes pieces by their style origin, enabling cross-style game scenarios and piece origin tracking.
12
+ PNN (Piece Name Notation) is a formal, rule-agnostic naming system for identifying **pieces** in abstract strategy board games such as chess, shōgi, xiangqi, and their many variants. Each piece is represented by a canonical, human-readable ASCII name with optional state modifiers and optional terminal markers (e.g., `"KING"`, `"queen"`, `"+ROOK"`, `"-pawn"`, `"KING^"`).
13
13
 
14
- This gem implements the [PNN Specification v1.0.0](https://sashite.dev/specs/pnn/1.0.0/), providing a modern Ruby interface with immutable piece objects and full backward compatibility with PIN while adding style differentiation capabilities.
14
+ This gem implements the [PNN Specification v1.0.0](https://sashite.dev/specs/pnn/1.0.0/), supporting validation, parsing, and comparison of piece names with integrated state management and terminal piece identification.
15
15
 
16
16
  ## Installation
17
-
18
17
  ```ruby
19
18
  # In your Gemfile
20
19
  gem "sashite-pnn"
21
20
  ```
22
21
 
23
22
  Or install manually:
24
-
25
23
  ```sh
26
24
  gem install sashite-pnn
27
25
  ```
28
26
 
29
27
  ## Usage
30
28
 
29
+ ### Basic Operations
31
30
  ```ruby
32
31
  require "sashite/pnn"
33
32
 
34
- # Parse PNN strings into piece objects
35
- piece = Sashite::Pnn.parse("K") # => #<Pnn::Piece type=:K side=:first state=:normal native=true>
36
- piece.to_s # => "K"
37
- piece.type # => :K
38
- piece.side # => :first
39
- piece.state # => :normal
40
- piece.native? # => true
33
+ # Parse PNN strings into piece name objects
34
+ name = Sashite::Pnn.parse("KING") # => #<Pnn::Name value="KING">
35
+ name.to_s # => "KING"
36
+ name.value # => "KING"
41
37
 
42
- # Create pieces directly
43
- piece = Sashite::Pnn.piece(:K, :first) # => #<Pnn::Piece type=:K side=:first state=:normal native=true>
44
- piece = Sashite::Pnn::Piece.new(:R, :second, :enhanced, false) # => #<Pnn::Piece type=:R side=:second state=:enhanced native=false>
38
+ # Create from string or symbol
39
+ name = Sashite::Pnn.name("queen") # => #<Pnn::Name value="queen">
40
+ name = Sashite::Pnn::Name.new(:ROOK) # => #<Pnn::Name value="ROOK">
45
41
 
46
42
  # Validate PNN strings
47
- Sashite::Pnn.valid?("K") # => true
48
- Sashite::Pnn.valid?("+R'") # => true
49
- Sashite::Pnn.valid?("invalid") # => false
50
-
51
- # Style derivation with apostrophe suffix
52
- native_king = Sashite::Pnn.parse("K") # => #<Pnn::Piece type=:K side=:first state=:normal native=true>
53
- foreign_king = Sashite::Pnn.parse("K'") # => #<Pnn::Piece type=:K side=:first state=:normal native=false>
54
-
55
- native_king.to_s # => "K"
56
- foreign_king.to_s # => "K'"
57
-
58
- # State manipulation (returns new immutable instances)
59
- enhanced = piece.enhance # => #<Pnn::Piece type=:K side=:first state=:enhanced native=true>
60
- enhanced.to_s # => "+K"
61
- diminished = piece.diminish # => #<Pnn::Piece type=:K side=:first state=:diminished native=true>
62
- diminished.to_s # => "-K"
63
-
64
- # Style derivation manipulation
65
- foreign_piece = piece.derive # => #<Pnn::Piece type=:K side=:first state=:normal native=false>
66
- foreign_piece.to_s # => "K'"
67
- back_to_native = foreign_piece.underive # => #<Pnn::Piece type=:K side=:first state=:normal native=true>
68
- back_to_native.to_s # => "K"
69
-
70
- # Side manipulation
71
- flipped = piece.flip # => #<Pnn::Piece type=:K side=:second state=:normal native=true>
72
- flipped.to_s # => "k"
73
-
74
- # Type manipulation
75
- queen = piece.with_type(:Q) # => #<Pnn::Piece type=:Q side=:first state=:normal native=true>
76
- queen.to_s # => "Q"
77
-
78
- # Style queries
79
- piece.native? # => true
80
- foreign_king.derived? # => true
81
-
82
- # State queries
83
- piece.normal? # => true
84
- enhanced.enhanced? # => true
85
- diminished.diminished? # => true
86
-
87
- # Side queries
88
- piece.first_player? # => true
89
- flipped.second_player? # => true
90
-
91
- # Attribute access
92
- piece.letter # => "K"
93
- enhanced.prefix # => "+"
94
- foreign_king.suffix # => "'"
95
- piece.suffix # => ""
96
-
97
- # Type and side comparison
98
- king1 = Sashite::Pnn.parse("K")
99
- king2 = Sashite::Pnn.parse("k")
100
- queen = Sashite::Pnn.parse("Q")
101
-
102
- king1.same_type?(king2) # => true (both kings)
103
- king1.same_side?(queen) # => true (both first player)
104
- king1.same_type?(queen) # => false (different types)
105
-
106
- # Style comparison
107
- native_king = Sashite::Pnn.parse("K")
108
- foreign_king = Sashite::Pnn.parse("K'")
109
-
110
- native_king.same_style?(foreign_king) # => false (different derivation)
111
-
112
- # Functional transformations can be chained
113
- pawn = Sashite::Pnn.parse("P")
114
- enemy_foreign_promoted = pawn.flip.derive.enhance # => "+p'" (second player foreign promoted pawn)
43
+ Sashite::Pnn.valid?("BISHOP") # => true
44
+ Sashite::Pnn.valid?("King") # => false (mixed case not allowed)
45
+ Sashite::Pnn.valid?("+ROOK") # => true (enhanced state)
46
+ Sashite::Pnn.valid?("-pawn") # => true (diminished state)
47
+ Sashite::Pnn.valid?("KING^") # => true (terminal piece)
48
+ Sashite::Pnn.valid?("+KING^") # => true (enhanced terminal piece)
115
49
  ```
116
50
 
117
- ## Format Specification
118
-
119
- ### Structure
120
- ```
121
- <pin>[<suffix>]
122
- ```
123
-
124
- ### Components
125
-
126
- - **PIN part** (`[<state>]<letter>`): Standard PIN notation
127
- - **Letter** (`A-Z`, `a-z`): Represents piece type and side
128
- - Uppercase: First player pieces
129
- - Lowercase: Second player pieces
130
- - **State** (optional prefix):
131
- - `+`: Enhanced state (promoted, upgraded, empowered)
132
- - `-`: Diminished state (weakened, restricted, temporary)
133
- - No prefix: Normal state
134
-
135
- - **Derivation suffix** (optional):
136
- - `'`: Foreign style (piece has opposite side's native style)
137
- - No suffix: Native style (piece has current side's native style)
138
-
139
- ### Regular Expression
51
+ ### State Modifiers
140
52
  ```ruby
141
- /\A[-+]?[A-Za-z]'?\z/
53
+ # Enhanced pieces (+ prefix)
54
+ enhanced = Sashite::Pnn.parse("+QUEEN")
55
+ enhanced.enhanced? # => true
56
+ enhanced.normal? # => false
57
+ enhanced.base_name # => "QUEEN"
58
+
59
+ # Diminished pieces (- prefix)
60
+ diminished = Sashite::Pnn.parse("-pawn")
61
+ diminished.diminished? # => true
62
+ diminished.base_name # => "pawn"
63
+
64
+ # Normal pieces (no prefix)
65
+ normal = Sashite::Pnn.parse("KNIGHT")
66
+ normal.normal? # => true
67
+ normal.enhanced? # => false
68
+ normal.diminished? # => false
142
69
  ```
143
70
 
144
- ### Examples
145
- - `K` - First player king (native style, normal state)
146
- - `k'` - Second player king (foreign style, normal state)
147
- - `+R'` - First player rook (foreign style, enhanced state)
148
- - `-p` - Second player pawn (native style, diminished state)
149
-
150
- ## Game Examples
151
-
152
- ### Cross-Style Chess vs. Shōgi
153
-
71
+ ### Terminal Markers
154
72
  ```ruby
155
- # Match setup: First player uses Chess, Second player uses Shōgi
156
- # Native styles: first=Chess, second=Shōgi
157
-
158
- # Native pieces (no derivation suffix)
159
- white_king = Sashite::Pnn.piece(:K, :first) # => "K" (Chess king)
160
- black_king = Sashite::Pnn.piece(:K, :second) # => "k" (Shōgi king)
161
-
162
- # Foreign pieces (with derivation suffix)
163
- white_shogi_king = Sashite::Pnn.piece(:K, :first, :normal, false) # => "K'" (Shōgi king for white)
164
- black_chess_king = Sashite::Pnn.piece(:K, :second, :normal, false) # => "k'" (Chess king for black)
165
-
166
- # Promoted pieces in cross-style context
167
- white_promoted_rook = Sashite::Pnn.parse("+R'") # White shōgi rook promoted to Dragon King
168
- black_promoted_pawn = Sashite::Pnn.parse("+p") # Black shōgi pawn promoted to Tokin
169
-
170
- white_promoted_rook.enhanced? # => true
171
- white_promoted_rook.derived? # => true
172
- black_promoted_pawn.enhanced? # => true
173
- black_promoted_pawn.native? # => true
73
+ # Terminal pieces (^ suffix)
74
+ terminal = Sashite::Pnn.parse("KING^")
75
+ terminal.terminal? # => true
76
+ terminal.base_name # => "KING"
77
+
78
+ # Non-terminal pieces (no suffix)
79
+ non_terminal = Sashite::Pnn.parse("PAWN")
80
+ non_terminal.terminal? # => false
81
+
82
+ # Combined state and terminal marker
83
+ enhanced_terminal = Sashite::Pnn.parse("+ROOK^")
84
+ enhanced_terminal.enhanced? # => true
85
+ enhanced_terminal.terminal? # => true
86
+ enhanced_terminal.base_name # => "ROOK"
87
+
88
+ diminished_terminal = Sashite::Pnn.parse("-king^")
89
+ diminished_terminal.diminished? # => true
90
+ diminished_terminal.terminal? # => true
91
+ diminished_terminal.base_name # => "king"
174
92
  ```
175
93
 
176
- ### Single-Style Games (PIN Compatibility)
177
-
94
+ ### Player Assignment
178
95
  ```ruby
179
- # Traditional Chess (both players use Chess style)
180
- # All pieces are native, so PNN behaves exactly like PIN
181
-
182
- white_pieces = %w[K Q +R B N P].map { |pin| Sashite::Pnn.parse(pin) }
183
- black_pieces = %w[k q +r b n p].map { |pin| Sashite::Pnn.parse(pin) }
184
-
185
- white_pieces.all?(&:native?) # => true
186
- black_pieces.all?(&:native?) # => true
187
-
188
- # PNN strings match PIN strings for native pieces
189
- white_pieces.map(&:to_s) # => ["K", "Q", "+R", "B", "N", "P"]
190
- black_pieces.map(&:to_s) # => ["k", "q", "+r", "b", "n", "p"]
96
+ # First player pieces (uppercase)
97
+ first_player = Sashite::Pnn.parse("KING")
98
+ first_player.first_player? # => true
99
+ first_player.second_player? # => false
100
+
101
+ first_player_terminal = Sashite::Pnn.parse("KING^")
102
+ first_player_terminal.first_player? # => true
103
+ first_player_terminal.terminal? # => true
104
+
105
+ # Second player pieces (lowercase)
106
+ second_player = Sashite::Pnn.parse("king")
107
+ second_player.first_player? # => false
108
+ second_player.second_player? # => true
109
+
110
+ second_player_terminal = Sashite::Pnn.parse("king^")
111
+ second_player_terminal.second_player? # => true
112
+ second_player_terminal.terminal? # => true
191
113
  ```
192
114
 
193
- ### Style Mutation During Gameplay
194
-
115
+ ### Normalization and Comparison
195
116
  ```ruby
196
- # Simulate capture with style change (Ōgi rules)
197
- chess_queen = Sashite::Pnn.parse("q'") # Black chess queen (foreign for shōgi player)
198
- captured = chess_queen.flip.with_type(:P).underive # Becomes white native pawn
199
-
200
- chess_queen.to_s # => "q'" (black foreign queen)
201
- captured.to_s # => "P" (white native pawn)
202
-
203
- # Style derivation changes during gameplay
204
- shogi_piece = Sashite::Pnn.parse("r") # Black shōgi rook (native)
205
- foreign_piece = shogi_piece.derive # Convert to foreign style
206
- foreign_piece.to_s # => "r'" (black foreign rook)
117
+ a = Sashite::Pnn.parse("ROOK")
118
+ b = Sashite::Pnn.parse("ROOK")
119
+
120
+ a == b # => true
121
+ a.same_base_name?(Sashite::Pnn.parse("rook")) # => true (same piece, different player)
122
+ a.same_base_name?(Sashite::Pnn.parse("ROOK^")) # => true (same piece, terminal marker)
123
+ a.same_base_name?(Sashite::Pnn.parse("+rook")) # => true (same piece, different state)
124
+ a.to_s # => "ROOK"
207
125
  ```
208
126
 
209
- ## API Reference
210
-
211
- ### Main Module Methods
212
-
213
- - `Sashite::Pnn.valid?(pnn_string)` - Check if string is valid PNN notation
214
- - `Sashite::Pnn.parse(pnn_string)` - Parse PNN string into Piece object
215
- - `Sashite::Pnn.piece(type, side, state = :normal, native = true)` - Create piece instance directly
216
-
217
- ### Piece Class
218
-
219
- #### Creation and Parsing
220
- - `Sashite::Pnn::Piece.new(type, side, state = :normal, native = true)` - Create piece instance
221
- - `Sashite::Pnn::Piece.parse(pnn_string)` - Parse PNN string (same as module method)
222
- - `Sashite::Pnn::Piece.valid?(pnn_string)` - Validate PNN string (class method)
223
-
224
- #### Attribute Access
225
- - `#type` - Get piece type (symbol :A to :Z, always uppercase)
226
- - `#side` - Get player side (:first or :second)
227
- - `#state` - Get state (:normal, :enhanced, or :diminished)
228
- - `#native` - Get style derivation (true for native, false for foreign)
229
- - `#letter` - Get letter representation (string, case determined by side)
230
- - `#prefix` - Get state prefix (string: "+", "-", or "")
231
- - `#suffix` - Get derivation suffix (string: "'" or "")
232
- - `#to_s` - Convert to PNN string representation
233
-
234
- #### Style Queries
235
- - `#native?` - Check if native style (current side's native style)
236
- - `#derived?` - Check if foreign style (opposite side's native style)
237
- - `#foreign?` - Alias for `#derived?`
238
-
239
- #### State Queries
240
- - `#normal?` - Check if normal state (no modifiers)
241
- - `#enhanced?` - Check if enhanced state
242
- - `#diminished?` - Check if diminished state
243
-
244
- #### Side Queries
245
- - `#first_player?` - Check if first player piece
246
- - `#second_player?` - Check if second player piece
247
-
248
- #### State Transformations (immutable - return new instances)
249
- - `#enhance` - Create enhanced version
250
- - `#unenhance` - Remove enhanced state
251
- - `#diminish` - Create diminished version
252
- - `#undiminish` - Remove diminished state
253
- - `#normalize` - Remove all state modifiers
254
-
255
- #### Style Transformations (immutable - return new instances)
256
- - `#derive` - Convert to foreign style (add derivation suffix)
257
- - `#underive` - Convert to native style (remove derivation suffix)
258
- - `#flip` - Switch player (change side)
259
-
260
- #### Attribute Transformations (immutable - return new instances)
261
- - `#with_type(new_type)` - Create piece with different type
262
- - `#with_side(new_side)` - Create piece with different side
263
- - `#with_state(new_state)` - Create piece with different state
264
- - `#with_derivation(native)` - Create piece with different derivation
265
-
266
- #### Comparison Methods
267
- - `#same_type?(other)` - Check if same piece type
268
- - `#same_side?(other)` - Check if same side
269
- - `#same_state?(other)` - Check if same state
270
- - `#same_style?(other)` - Check if same style derivation
271
- - `#==(other)` - Full equality comparison
272
-
273
- ### Constants
274
- - `Sashite::Pnn::Piece::NATIVE` - Constant for native style (`true`)
275
- - `Sashite::Pnn::Piece::FOREIGN` - Constant for foreign style (`false`)
276
- - `Sashite::Pnn::Piece::FOREIGN_SUFFIX` - Derivation suffix for foreign pieces (`"'"`)
277
- - `Sashite::Pnn::Piece::NATIVE_SUFFIX` - Derivation suffix for native pieces (`""`)
278
-
279
- **Note**: PNN validation leverages the existing `Sashite::Pin::Piece::PIN_PATTERN` for the PIN component, with additional logic for the optional derivation suffix.
280
-
281
- ## Advanced Usage
282
-
283
- ### Style Derivation Examples
284
-
127
+ ### Collections and Filtering
285
128
  ```ruby
286
- # Understanding native vs. foreign pieces
287
- # In a Chess vs. Shōgi match:
288
- # - First player native style: Chess
289
- # - Second player native style: Shōgi
290
-
291
- native_chess_king = Sashite::Pnn.parse("K") # First player native (Chess king)
292
- foreign_shogi_king = Sashite::Pnn.parse("K'") # First player foreign (Shōgi king)
293
-
294
- native_shogi_king = Sashite::Pnn.parse("k") # Second player native (Shōgi king)
295
- foreign_chess_king = Sashite::Pnn.parse("k'") # Second player foreign (Chess king)
296
-
297
- # Style queries
298
- native_chess_king.native? # => true
299
- foreign_shogi_king.derived? # => true
300
- native_shogi_king.native? # => true
301
- foreign_chess_king.derived? # => true
302
- ```
129
+ pieces = %w[KING^ queen +ROOK -pawn BISHOP knight^ GENERAL^].map { |n| Sashite::Pnn.parse(n) }
303
130
 
304
- ### Immutable Transformations
305
- ```ruby
306
- # All transformations return new instances
307
- original = Sashite::Pnn.piece(:K, :first)
308
- enhanced = original.enhance
309
- derived = original.derive
310
- flipped = original.flip
311
-
312
- # Original piece is never modified
313
- puts original # => "K"
314
- puts enhanced # => "+K"
315
- puts derived # => "K'"
316
- puts flipped # => "k"
317
-
318
- # Transformations can be chained
319
- result = original.flip.derive.enhance.with_type(:Q)
320
- puts result # => "+q'"
321
- ```
131
+ # Filter by player
132
+ first_player_pieces = pieces.select(&:first_player?).map(&:to_s)
133
+ # => ["KING^", "+ROOK", "BISHOP", "GENERAL^"]
322
134
 
323
- ### Cross-Style Game State Management
324
- ```ruby
325
- class CrossStyleGameBoard
326
- def initialize(first_style, second_style)
327
- @first_style = first_style
328
- @second_style = second_style
329
- @pieces = {}
330
- end
331
-
332
- def place(square, piece)
333
- @pieces[square] = piece
334
- end
335
-
336
- def capture_with_style_change(from_square, to_square, new_type = nil)
337
- captured = @pieces[to_square]
338
- capturing = @pieces.delete(from_square)
339
-
340
- return nil unless captured && capturing
341
-
342
- # Style mutation: captured piece becomes native to capturing side
343
- mutated = captured.flip.underive
344
- mutated = mutated.with_type(new_type) if new_type
345
-
346
- @pieces[to_square] = capturing
347
- mutated # Return mutated captured piece for hand
348
- end
349
-
350
- def pieces_by_style_derivation
351
- {
352
- native: @pieces.select { |_, piece| piece.native? },
353
- foreign: @pieces.select { |_, piece| piece.derived? }
354
- }
355
- end
356
- end
357
-
358
- # Usage
359
- board = CrossStyleGameBoard.new(:chess, :shogi)
360
- board.place("e1", Sashite::Pnn.piece(:K, :first)) # Chess king
361
- board.place("e8", Sashite::Pnn.piece(:K, :second)) # Shōgi king
362
- board.place("d4", Sashite::Pnn.piece(:Q, :first, :normal, false)) # Chess queen using Shōgi style
363
-
364
- analysis = board.pieces_by_style_derivation
365
- puts analysis[:native].size # => 2
366
- puts analysis[:foreign].size # => 1
367
- ```
135
+ # Filter by state
136
+ enhanced_pieces = pieces.select(&:enhanced?).map(&:to_s)
137
+ # => ["+ROOK"]
368
138
 
369
- ### PIN Compatibility Layer
370
- ```ruby
371
- # PNN is fully backward compatible with PIN
372
- def convert_pin_to_pnn(pin_string)
373
- # All PIN strings are valid PNN strings (native pieces)
374
- Sashite::Pnn.parse(pin_string)
375
- end
139
+ diminished_pieces = pieces.select(&:diminished?).map(&:to_s)
140
+ # => ["-pawn"]
376
141
 
377
- def convert_pnn_to_pin(pnn_piece)
378
- # Only native PNN pieces can be converted to PIN
379
- return nil unless pnn_piece.native?
142
+ # Filter by terminal status
143
+ terminal_pieces = pieces.select(&:terminal?).map(&:to_s)
144
+ # => ["KING^", "knight^", "GENERAL^"]
380
145
 
381
- "#{pnn_piece.prefix}#{pnn_piece.letter}"
382
- end
146
+ # Combine filters
147
+ first_player_terminals = pieces.select { |p| p.first_player? && p.terminal? }.map(&:to_s)
148
+ # => ["KING^", "GENERAL^"]
149
+ ```
383
150
 
384
- # Usage
385
- pin_pieces = %w[K Q +R -P k q r p]
386
- pnn_pieces = pin_pieces.map { |pin| convert_pin_to_pnn(pin) }
151
+ ## Format Specification
387
152
 
388
- pnn_pieces.all?(&:native?) # => true
389
- pnn_pieces.map { |p| convert_pnn_to_pin(p) } # => ["K", "Q", "+R", "-P", "k", "q", "r", "p"]
153
+ ### Structure
390
154
  ```
391
-
392
- ### Move Validation Example
393
- ```ruby
394
- def can_promote_in_style?(piece, target_rank, style_rules)
395
- return false unless piece.normal? # Already promoted pieces can't promote again
396
-
397
- case [piece.type, piece.native? ? style_rules[:native] : style_rules[:foreign]]
398
- when %i[P chess] # Chess pawn
399
- (piece.first_player? && target_rank == 8) ||
400
- (piece.second_player? && target_rank == 1)
401
- when %i[P shogi] # Shōgi pawn
402
- (piece.first_player? && target_rank >= 7) ||
403
- (piece.second_player? && target_rank <= 3)
404
- when %i[R shogi], %i[B shogi] # Shōgi major pieces
405
- true
406
- else
407
- false
408
- end
409
- end
410
-
411
- # Usage
412
- chess_pawn = Sashite::Pnn.piece(:P, :first)
413
- shogi_pawn = Sashite::Pnn.piece(:P, :first, :normal, false)
414
-
415
- style_rules = { native: :chess, foreign: :shogi }
416
-
417
- puts can_promote_in_style?(chess_pawn, 8, style_rules) # => true (chess pawn on 8th rank)
418
- puts can_promote_in_style?(shogi_pawn, 8, style_rules) # => true (shogi pawn on 8th rank)
155
+ [<state-modifier>]<piece-name>[<terminal-marker>]
419
156
  ```
420
157
 
421
- ## Implementation Architecture
422
-
423
- This gem uses **composition over inheritance** by building upon the proven [sashite-pin](https://github.com/sashite/pin.rb) gem:
158
+ Where:
159
+ - `<state-modifier>` is optional `+` (enhanced) or `-` (diminished)
160
+ - `<piece-name>` is case-consistent alphabetic characters
161
+ - `<terminal-marker>` is optional `^` (terminal piece)
424
162
 
425
- - **PIN Foundation**: All type, side, and state logic is handled by an internal `Pin::Piece` object
426
- - **PNN Extension**: Only the derivation (`native`) attribute and related methods are added
427
- - **Delegation Pattern**: Core PIN methods are delegated to the internal PIN piece
428
- - **Immutability**: All transformations return new instances, maintaining functional programming principles
163
+ ### Grammar (BNF)
164
+ ```bnf
165
+ <pnn> ::= <state-modifier> <name-body> <terminal-marker>
166
+ | <state-modifier> <name-body>
167
+ | <name-body> <terminal-marker>
168
+ | <name-body>
429
169
 
430
- This architecture ensures:
431
- - **Reliability**: Reuses battle-tested PIN logic
432
- - **Maintainability**: PIN updates automatically benefit PNN
433
- - **Consistency**: PIN and PNN pieces behave identically for shared attributes
434
- - **Performance**: Minimal overhead over pure PIN implementation
170
+ <state-modifier> ::= "+" | "-"
435
171
 
436
- ## Protocol Mapping
172
+ <name-body> ::= <uppercase-name> | <lowercase-name>
437
173
 
438
- Following the [Game Protocol](https://sashite.dev/game-protocol/):
174
+ <uppercase-name> ::= <uppercase-letter>+
175
+ <lowercase-name> ::= <lowercase-letter>+
439
176
 
440
- | Protocol Attribute | PNN Encoding | Examples | Notes |
441
- |-------------------|--------------|----------|-------|
442
- | **Type** | ASCII letter choice | `K`/`k` = King, `P`/`p` = Pawn | Type is always stored as uppercase symbol (`:K`, `:P`) |
443
- | **Side** | Letter case in display | `K` = First player, `k` = Second player | Case is determined by side during rendering |
444
- | **State** | Optional prefix | `+K` = Enhanced, `-K` = Diminished, `K` = Normal | |
445
- | **Style** | Derivation suffix | `K` = Native style, `K'` = Foreign style | |
177
+ <terminal-marker> ::= "^"
446
178
 
447
- **Style Derivation Logic**:
448
- - **No suffix**: Piece has the **native style** of its current side
449
- - **Apostrophe suffix (`'`)**: Piece has the **foreign style** (opposite side's native style)
450
-
451
- **Canonical principle**: Identical pieces must have identical PNN representations.
179
+ <uppercase-letter> ::= "A" | "B" | "C" | ... | "Z"
180
+ <lowercase-letter> ::= "a" | "b" | "c" | ... | "z"
181
+ ```
452
182
 
453
- ## Properties
183
+ ### Regular Expression
184
+ ```ruby
185
+ /\A[+-]?([A-Z]+|[a-z]+)\^?\z/
186
+ ```
454
187
 
455
- * **PIN Compatible**: All valid PIN strings are valid PNN strings
456
- * **Style Aware**: Distinguishes pieces by their style origin through derivation markers
457
- * **ASCII Compatible**: Maximum portability across systems
458
- * **Rule-Agnostic**: Independent of specific game mechanics
459
- * **Compact Format**: Minimal character usage (1-3 characters per piece)
460
- * **Visual Distinction**: Clear player and style differentiation
461
- * **Protocol Compliant**: Complete implementation of Sashité piece attributes
462
- * **Immutable**: All piece instances are frozen and transformations return new objects
463
- * **Functional**: Pure functions with no side effects
188
+ ## Design Principles
464
189
 
465
- ## Implementation Notes
190
+ * **Human-readable**: Names like `"KING"` or `"queen"` are intuitive and descriptive.
191
+ * **State-aware**: Integrated state management through `+` and `-` modifiers.
192
+ * **Terminal-aware**: Explicit identification of terminal pieces through `^` marker.
193
+ * **Rule-agnostic**: Independent of specific game mechanics.
194
+ * **Case-consistent**: Visual distinction between players through case.
195
+ * **Canonical**: One valid name per piece state within a given context.
196
+ * **ASCII-only**: Compatible with all systems.
466
197
 
467
- ### Style Derivation Convention
198
+ ## Integration with PIN
468
199
 
469
- PNN follows a strict style derivation convention:
200
+ PNN names serve as the formal source for PIN character identifiers. For example:
470
201
 
471
- 1. **Native pieces** (no suffix): Use the current side's native style
472
- 2. **Foreign pieces** (`'` suffix): Use the opposite side's native style
473
- 3. **Match context**: Each side has a defined native style for the entire match
474
- 4. **Style mutations**: Pieces can change derivation through gameplay mechanics
202
+ | PNN | PIN | Description |
203
+ | ---------- | -------- | ----------- |
204
+ | `KING` | `K` | First player king |
205
+ | `king` | `k` | Second player king |
206
+ | `KING^` | `K^` | Terminal first player king |
207
+ | `king^` | `k^` | Terminal second player king |
208
+ | `+ROOK` | `+R` | Enhanced first player rook |
209
+ | `+ROOK^` | `+R^` | Enhanced terminal first player rook |
210
+ | `-pawn` | `-p` | Diminished second player pawn |
211
+ | `-pawn^` | `-p^` | Diminished terminal second player pawn |
475
212
 
476
- ### Example Flow
213
+ Multiple PNN names may map to the same PIN character (e.g., `"KING"` and `"KHAN"` both → `K`), but PNN provides unambiguous naming within broader contexts.
477
214
 
215
+ ## Examples
478
216
  ```ruby
479
- # Match context: First player=Chess, Second player=Shōgi
480
- # Input: "K'" (first player foreign)
481
- # Parsing
482
- # type: :K, side: :first, state: :normal, native: false
483
- # Style resolution
484
- # Effective style: Shōgi (second player's native style)
485
- # Display
486
- # PNN: "K'" (first player king with foreign/Shōgi style)
217
+ # Traditional pieces
218
+ Sashite::Pnn.parse("KING") # => #<Pnn::Name value="KING">
219
+ Sashite::Pnn.parse("queen") # => #<Pnn::Name value="queen">
220
+
221
+ # Terminal pieces
222
+ Sashite::Pnn.parse("KING^") # => #<Pnn::Name value="KING^">
223
+ Sashite::Pnn.parse("general^") # => #<Pnn::Name value="general^">
224
+
225
+ # State modifiers
226
+ Sashite::Pnn.parse("+ROOK") # => #<Pnn::Name value="+ROOK">
227
+ Sashite::Pnn.parse("-pawn") # => #<Pnn::Name value="-pawn">
228
+
229
+ # Combined modifiers
230
+ Sashite::Pnn.parse("+KING^") # => #<Pnn::Name value="+KING^">
231
+ Sashite::Pnn.parse("-pawn^") # => #<Pnn::Name value="-pawn^">
232
+
233
+ # Validation
234
+ Sashite::Pnn.valid?("BISHOP") # => true
235
+ Sashite::Pnn.valid?("KING^") # => true
236
+ Sashite::Pnn.valid?("+ROOK^") # => true
237
+ Sashite::Pnn.valid?("Bishop") # => false (mixed case)
238
+ Sashite::Pnn.valid?("KING1") # => false (no digits allowed)
239
+ Sashite::Pnn.valid?("^KING") # => false (terminal marker must be suffix)
487
240
  ```
488
241
 
489
- This ensures that `parse(pnn).to_s == pnn` for all valid PNN strings while enabling cross-style gameplay.
490
-
491
- ## System Constraints
492
-
493
- - **Maximum 26 piece types** per game system (one per ASCII letter)
494
- - **Exactly 2 players** (uppercase/lowercase distinction)
495
- - **3 state levels** (enhanced, normal, diminished)
496
- - **2 style derivations** (native, foreign)
497
- - **Style context dependency**: Requires match-level side-style associations
242
+ ## API Reference
498
243
 
499
- ## Related Specifications
244
+ ### Main Module
500
245
 
501
- - [PIN](https://sashite.dev/specs/pin/1.0.0/) - Piece Identifier Notation (style-agnostic base)
502
- - [Game Protocol](https://sashite.dev/game-protocol/) - Conceptual foundation for abstract strategy board games
503
- - [SNN](https://sashite.dev/specs/snn/1.0.0/) - Style Name Notation
504
- - [GAN](https://sashite.dev/specs/gan/1.0.0/) - General Actor Notation (alternative style-aware format)
505
- - [CELL](https://sashite.dev/specs/cell/) - Board position coordinates
506
- - [HAND](https://sashite.dev/specs/hand/) - Reserve location notation
507
- - [PMN](https://sashite.dev/specs/pmn/) - Portable Move Notation
246
+ * `Sashite::Pnn.valid?(str)` Returns `true` if the string is valid PNN.
247
+ * `Sashite::Pnn.parse(str)` Returns a `Sashite::Pnn::Name` object.
248
+ * `Sashite::Pnn.name(sym_or_str)` Alias for constructing a name.
508
249
 
509
- ## Documentation
250
+ ### `Sashite::Pnn::Name`
510
251
 
511
- - [Official PNN Specification v1.0.0](https://sashite.dev/specs/pnn/1.0.0/)
512
- - [PNN Examples Documentation](https://sashite.dev/specs/pnn/1.0.0/examples/)
513
- - [Game Protocol Foundation](https://sashite.dev/game-protocol/)
514
- - [API Documentation](https://rubydoc.info/github/sashite/pnn.rb/main)
252
+ * `#value` Returns the canonical string value.
253
+ * `#to_s` Returns the string representation.
254
+ * `#base_name` Returns the name without state modifier or terminal marker.
255
+ * `#enhanced?` — Returns `true` if piece has enhanced state (`+` prefix).
256
+ * `#diminished?` — Returns `true` if piece has diminished state (`-` prefix).
257
+ * `#normal?` — Returns `true` if piece has normal state (no prefix).
258
+ * `#terminal?` — Returns `true` if piece is a terminal piece (`^` suffix).
259
+ * `#first_player?` — Returns `true` if piece belongs to first player (uppercase).
260
+ * `#second_player?` — Returns `true` if piece belongs to second player (lowercase).
261
+ * `#same_base_name?(other)` — Returns `true` if both pieces have same base name.
262
+ * `#==`, `#eql?`, `#hash` — Value-based equality.
515
263
 
516
264
  ## Development
517
-
518
265
  ```sh
519
266
  # Clone the repository
520
267
  git clone https://github.com/sashite/pnn.rb.git