sashite-pnn 1.0.0 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
data/README.md CHANGED
@@ -5,13 +5,13 @@
5
5
  ![Ruby](https://github.com/sashite/pnn.rb/actions/workflows/main.yml/badge.svg?branch=main)
6
6
  [![License](https://img.shields.io/github/license/sashite/pnn.rb?label=License&logo=github)](https://github.com/sashite/pnn.rb/raw/main/LICENSE.md)
7
7
 
8
- > **PNN** (Piece Name Notation) support for the Ruby language.
8
+ > **PNN** (Piece Name Notation) implementation for the Ruby language.
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. It 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) 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.
13
13
 
14
- This gem implements the [PNN Specification v1.0.0](https://sashite.dev/specs/pnn/1.0.0/), providing a Ruby interface for working with style-aware piece representations through an intuitive object-oriented API.
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.
15
15
 
16
16
  ## Installation
17
17
 
@@ -26,320 +26,519 @@ Or install manually:
26
26
  gem install sashite-pnn
27
27
  ```
28
28
 
29
- ## PNN Format
29
+ ## Usage
30
30
 
31
- A PNN record consists of a [PIN](https://sashite.dev/specs/pin/1.0.0/) string with an optional derivation suffix:
31
+ ```ruby
32
+ require "sashite/pnn"
32
33
 
33
- ```
34
- <pin>[<suffix>]
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
41
+
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>
45
+
46
+ # 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)
35
115
  ```
36
116
 
37
- Where:
38
- - `<pin>` is a valid PIN string (`[<state>]<letter>`)
39
- - `<suffix>` is an optional derivation marker (`'` for foreign style)
117
+ ## Format Specification
40
118
 
41
- Examples:
42
- - `K` - First player king with native style
43
- - `K'` - First player king with foreign style
44
- - `+R` - First player rook with enhanced state and native style
45
- - `+R'` - First player rook with enhanced state and foreign style
119
+ ### Structure
120
+ ```
121
+ <pin>[<suffix>]
122
+ ```
46
123
 
47
- ## Basic Usage
124
+ ### Components
48
125
 
49
- ### Creating Piece Objects
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
50
134
 
51
- The primary interface is the `Sashite::Pnn::Piece` class, which represents a single piece in PNN format:
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)
52
138
 
139
+ ### Regular Expression
53
140
  ```ruby
54
- require "sashite/pnn"
55
-
56
- # Parse a PNN string into a piece object
57
- piece = Sashite::Pnn::Piece.parse("k")
58
- # => #<Sashite::Pnn::Piece letter="k" native=true>
141
+ /\A[-+]?[A-Za-z]'?\z/
142
+ ```
59
143
 
60
- # With derivation marker
61
- foreign_piece = Sashite::Pnn::Piece.parse("k'")
62
- # => #<Sashite::Pnn::Piece letter="k" native=false>
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)
63
149
 
64
- # With state modifiers and derivation
65
- enhanced_foreign = Sashite::Pnn::Piece.parse("+k'")
66
- # => #<Sashite::Pnn::Piece letter="k" enhanced=true native=false>
150
+ ## Game Examples
67
151
 
68
- # Create directly with constructor
69
- piece = Sashite::Pnn::Piece.new("k")
70
- foreign_piece = Sashite::Pnn::Piece.new("k", native: false)
71
- enhanced_piece = Sashite::Pnn::Piece.new("k", enhanced: true, native: false)
72
- ```
152
+ ### Cross-Style Chess vs. Shōgi
73
153
 
74
- ### Converting to PNN String
154
+ ```ruby
155
+ # Match setup: First player uses Chess, Second player uses Shōgi
156
+ # Native styles: first=Chess, second=Shōgi
75
157
 
76
- Convert a piece object back to its PNN string representation:
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)
77
161
 
78
- ```ruby
79
- piece = Sashite::Pnn::Piece.parse("k")
80
- piece.to_s
81
- # => "k"
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)
82
165
 
83
- foreign_piece = Sashite::Pnn::Piece.parse("k'")
84
- foreign_piece.to_s
85
- # => "k'"
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
86
169
 
87
- enhanced_foreign = Sashite::Pnn::Piece.parse("+k'")
88
- enhanced_foreign.to_s
89
- # => "+k'"
170
+ white_promoted_rook.enhanced? # => true
171
+ white_promoted_rook.derived? # => true
172
+ black_promoted_pawn.enhanced? # => true
173
+ black_promoted_pawn.native? # => true
90
174
  ```
91
175
 
92
- ### Style Manipulation
93
-
94
- Create new piece instances with different styles:
176
+ ### Single-Style Games (PIN Compatibility)
95
177
 
96
178
  ```ruby
97
- piece = Sashite::Pnn::Piece.parse("k")
179
+ # Traditional Chess (both players use Chess style)
180
+ # All pieces are native, so PNN behaves exactly like PIN
98
181
 
99
- # Convert to foreign style
100
- foreign = piece.foreignize
101
- foreign.to_s # => "k'"
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) }
102
184
 
103
- # Convert to native style
104
- native = foreign.nativize
105
- native.to_s # => "k"
185
+ white_pieces.all?(&:native?) # => true
186
+ black_pieces.all?(&:native?) # => true
106
187
 
107
- # Toggle style
108
- toggled = piece.toggle_style
109
- toggled.to_s # => "k'"
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"]
110
191
  ```
111
192
 
112
- ### State Manipulation (inherited from PIN)
113
-
114
- All PIN state manipulation methods are available:
193
+ ### Style Mutation During Gameplay
115
194
 
116
195
  ```ruby
117
- piece = Sashite::Pnn::Piece.parse("k")
118
-
119
- # Enhanced state (+ prefix)
120
- enhanced = piece.enhance
121
- enhanced.to_s # => "+k"
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
122
199
 
123
- # Diminished state (- prefix)
124
- diminished = piece.diminish
125
- diminished.to_s # => "-k"
200
+ chess_queen.to_s # => "q'" (black foreign queen)
201
+ captured.to_s # => "P" (white native pawn)
126
202
 
127
- # Combine with style changes
128
- enhanced_foreign = piece.enhance.foreignize
129
- enhanced_foreign.to_s # => "+k'"
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)
130
207
  ```
131
208
 
132
- ### Ownership Changes
209
+ ## API Reference
133
210
 
134
- Change piece ownership (case conversion):
211
+ ### Main Module Methods
135
212
 
136
- ```ruby
137
- white_king = Sashite::Pnn::Piece.parse("K")
138
- black_king = white_king.flip
139
- black_king.to_s # => "k"
140
-
141
- # Works with foreign pieces too
142
- foreign_white = Sashite::Pnn::Piece.parse("K'")
143
- foreign_black = foreign_white.flip
144
- foreign_black.to_s # => "k'"
145
- ```
146
-
147
- ## Cross-Style Game Examples
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
148
216
 
149
- ### Chess vs. Shōgi Match
217
+ ### Piece Class
150
218
 
151
- In a hybrid game where the first player uses Chess pieces and the second player uses Shōgi pieces:
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)
152
223
 
153
- ```ruby
154
- # First player (Chess style is native)
155
- white_pawn = Sashite::Pnn::Piece.parse("P") # Chess pawn (native)
156
- white_shogi_pawn = Sashite::Pnn::Piece.parse("P'") # Shōgi pawn (foreign)
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
157
233
 
158
- # Second player (Shōgi style is native)
159
- black_pawn = Sashite::Pnn::Piece.parse("p") # Shōgi pawn (native)
160
- black_chess_pawn = Sashite::Pnn::Piece.parse("p'") # Chess pawn (foreign)
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.
161
280
 
162
- # Promoted pieces with style
163
- promoted_shogi = Sashite::Pnn::Piece.parse("+P'") # Promoted Shōgi pawn (foreign to first player)
164
- ```
281
+ ## Advanced Usage
165
282
 
166
- ### Style Conversions During Gameplay
283
+ ### Style Derivation Examples
167
284
 
168
285
  ```ruby
169
- # Capture and style conversion
170
- enemy_piece = Sashite::Pnn::Piece.parse("p'") # Enemy's foreign piece
171
- captured = enemy_piece.flip.nativize # Convert to our side with native style
172
- captured.to_s # => "P"
173
-
174
- # Promotion with style preservation
175
- pawn = Sashite::Pnn::Piece.parse("p'") # Foreign pawn
176
- promoted = pawn.enhance # Promote while keeping foreign style
177
- promoted.to_s # => "+p'"
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
178
302
  ```
179
303
 
180
- ## Style Logic
181
-
182
- ### Native vs Foreign Style
183
-
184
- - **No suffix**: Piece has the **native style** of its current side
185
- - **Apostrophe suffix (`'`)**: Piece has the **foreign style** (opposite side's native style)
186
-
187
- ### Style Assignment
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
+ ```
188
322
 
323
+ ### Cross-Style Game State Management
189
324
  ```ruby
190
- piece = Sashite::Pnn::Piece.parse("K")
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
+ ```
191
368
 
192
- # Check style
193
- piece.native? # => true
194
- piece.foreign? # => false
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
195
376
 
196
- # Convert styles
197
- foreign = piece.foreignize
198
- foreign.native? # => false
199
- foreign.foreign? # => true
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?
200
380
 
201
- native = foreign.nativize
202
- native.native? # => true
203
- native.foreign? # => false
204
- ```
381
+ "#{pnn_piece.prefix}#{pnn_piece.letter}"
382
+ end
205
383
 
206
- ## State Modifier Methods
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) }
207
387
 
208
- The `Sashite::Pnn::Piece` class inherits all PIN functionality and adds style methods:
388
+ pnn_pieces.all?(&:native?) # => true
389
+ pnn_pieces.map { |p| convert_pnn_to_pin(p) } # => ["K", "Q", "+R", "-P", "k", "q", "r", "p"]
390
+ ```
209
391
 
210
- ### Inherited from PIN
211
- | Method | Description | Example |
212
- |--------|-------------|---------|
213
- | `enhance` | Add Enhanced state (`+` prefix) | `k` → `+k` |
214
- | `unenhance` | Remove Enhanced state | `+k` → `k` |
215
- | `diminish` | Add Diminished state (`-` prefix) | `k` → `-k` |
216
- | `undiminish` | Remove Diminished state | `-k` → `k` |
217
- | `normalize` | Remove all state modifiers | `+k` → `k` |
218
- | `flip` | Change ownership (case) | `K` → `k`, `k` → `K` |
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)
419
+ ```
219
420
 
220
- ### PNN-Specific Style Methods
221
- | Method | Description | Example |
222
- |--------|-------------|---------|
223
- | `foreignize` | Convert to foreign style | `k` → `k'` |
224
- | `nativize` | Convert to native style | `k'` → `k` |
225
- | `toggle_style` | Toggle between native/foreign | `k` → `k'`, `k'` → `k` |
421
+ ## Implementation Architecture
226
422
 
227
- All methods return new `Sashite::Pnn::Piece` instances, leaving the original unchanged (immutable design).
423
+ This gem uses **composition over inheritance** by building upon the proven [sashite-pin](https://github.com/sashite/pin.rb) gem:
228
424
 
229
- ## API Reference
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
230
429
 
231
- ### Module Methods
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
232
435
 
233
- - `Sashite::Pnn.valid?(pnn_string)` - Check if a string is valid PNN notation
234
- - `Sashite::Pnn.parse(pnn_string)` - Parse a PNN string into a piece object
436
+ ## Protocol Mapping
235
437
 
236
- ### Sashite::Pnn::Piece Class Methods
438
+ Following the [Game Protocol](https://sashite.dev/game-protocol/):
237
439
 
238
- - `Sashite::Pnn::Piece.parse(pnn_string)` - Parse a PNN string into a piece object
239
- - `Sashite::Pnn::Piece.new(letter, **options)` - Create a new piece instance
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 | |
240
446
 
241
- ### Instance Methods
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)
242
450
 
243
- #### Style Queries
244
- - `#native?` - Check if piece has native style
245
- - `#foreign?` - Check if piece has foreign style
451
+ **Canonical principle**: Identical pieces must have identical PNN representations.
246
452
 
247
- #### Style Manipulation
248
- - `#foreignize` - Convert to foreign style
249
- - `#nativize` - Convert to native style
250
- - `#toggle_style` - Toggle between native/foreign style
453
+ ## Properties
251
454
 
252
- #### Inherited PIN Methods
253
- All methods from `Sashite::Pin::Piece` are available, including state queries, state manipulation, and conversion methods.
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
254
464
 
255
- #### Conversion
256
- - `#to_s` - Convert to PNN string representation
257
- - `#to_pin` - Convert to underlying PIN representation
258
- - `#inspect` - Detailed string representation for debugging
465
+ ## Implementation Notes
259
466
 
260
- ## Examples in Common Games
467
+ ### Style Derivation Convention
261
468
 
262
- ### Single-Style Games
469
+ PNN follows a strict style derivation convention:
263
470
 
264
- ```ruby
265
- # Western Chess (both players use Chess style)
266
- white_king = Sashite::Pnn::Piece.parse("K") # White king
267
- black_king = Sashite::Pnn::Piece.parse("k") # Black king
268
- castling_rook = Sashite::Pnn::Piece.parse("+R") # Rook that can castle
269
-
270
- # Japanese Shōgi (both players use Shōgi style)
271
- white_king = Sashite::Pnn::Piece.parse("K") # White king
272
- promoted_pawn = Sashite::Pnn::Piece.parse("+P") # Promoted pawn (tokin)
273
- ```
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
274
475
 
275
- ### Cross-Style Games
476
+ ### Example Flow
276
477
 
277
478
  ```ruby
278
- # Chess vs Shōgi hybrid
279
- chess_queen = Sashite::Pnn::Piece.parse("Q") # Chess queen (native to first player)
280
- shogi_gold = Sashite::Pnn::Piece.parse("G'") # Shōgi gold (foreign to first player)
281
- shogi_king = Sashite::Pnn::Piece.parse("k") # Shōgi king (native to second player)
282
- chess_knight = Sashite::Pnn::Piece.parse("n'") # Chess knight (foreign to second player)
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)
283
487
  ```
284
488
 
285
- ## Advanced Usage
489
+ This ensures that `parse(pnn).to_s == pnn` for all valid PNN strings while enabling cross-style gameplay.
286
490
 
287
- ### Chaining Operations
491
+ ## System Constraints
288
492
 
289
- ```ruby
290
- piece = Sashite::Pnn::Piece.parse("k")
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
291
498
 
292
- # Chain multiple operations
293
- result = piece.enhance.foreignize.flip
294
- result.to_s # => "+K'"
499
+ ## Related Specifications
295
500
 
296
- # Complex transformations
297
- captured_and_converted = piece.flip.nativize.enhance
298
- captured_and_converted.to_s # => "+K"
299
- ```
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
300
508
 
301
- ### Validation
509
+ ## Documentation
302
510
 
303
- All parsing automatically validates input according to the PNN specification:
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)
304
515
 
305
- ```ruby
306
- # Valid PNN strings
307
- Sashite::Pnn::Piece.parse("k") # ✓
308
- Sashite::Pnn::Piece.parse("k'") # ✓
309
- Sashite::Pnn::Piece.parse("+p") # ✓
310
- Sashite::Pnn::Piece.parse("+p'") # ✓
311
-
312
- # Check validity
313
- Sashite::Pnn.valid?("k'") # => true
314
- Sashite::Pnn.valid?("invalid") # => false
315
-
316
- # Invalid PNN strings raise ArgumentError
317
- Sashite::Pnn::Piece.parse("") # ✗ ArgumentError
318
- Sashite::Pnn::Piece.parse("k''") # ✗ ArgumentError
319
- Sashite::Pnn::Piece.parse("++k") # ✗ ArgumentError
320
- ```
516
+ ## Development
321
517
 
322
- ## Properties of PNN
518
+ ```sh
519
+ # Clone the repository
520
+ git clone https://github.com/sashite/pnn.rb.git
521
+ cd pnn.rb
323
522
 
324
- * **PIN compatibility**: All valid PIN strings are valid PNN strings
325
- * **Style awareness**: Distinguishes pieces by their style origin
326
- * **Rule-agnostic**: PNN does not encode legality, validity, or game-specific conditions
327
- * **Cross-tradition support**: Enables hybrid game scenarios
328
- * **Immutable objects**: All operations return new instances, ensuring thread safety
329
- * **Compact format**: Minimal overhead (single character suffix for style)
523
+ # Install dependencies
524
+ bundle install
330
525
 
331
- ## Constraints
526
+ # Run tests
527
+ ruby test.rb
332
528
 
333
- * PNN inherits all PIN constraints (two players, 26 piece types maximum)
334
- * Each side must have a defined native style
335
- * Style assignment is rule-dependent and remains fixed throughout the match
336
- * Foreign style pieces represent adoption of the opponent's style system
529
+ # Generate documentation
530
+ yard doc
531
+ ```
337
532
 
338
- ## Documentation
533
+ ## Contributing
339
534
 
340
- - [Official PNN Specification](https://sashite.dev/specs/pnn/1.0.0/)
341
- - [PIN Specification](https://sashite.dev/specs/pin/1.0.0/)
342
- - [API Documentation](https://rubydoc.info/github/sashite/pnn.rb/main)
535
+ 1. Fork the repository
536
+ 2. Create a feature branch (`git checkout -b feature/new-feature`)
537
+ 3. Add tests for your changes
538
+ 4. Ensure all tests pass (`ruby test.rb`)
539
+ 5. Commit your changes (`git commit -am 'Add new feature'`)
540
+ 6. Push to the branch (`git push origin feature/new-feature`)
541
+ 7. Create a Pull Request
343
542
 
344
543
  ## License
345
544