sashite-pnn 2.0.0 → 3.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.
- checksums.yaml +4 -4
- data/LICENSE.md +18 -17
- data/README.md +122 -428
- data/lib/sashite/pnn/name.rb +196 -0
- data/lib/sashite/pnn.rb +39 -42
- data/lib/sashite-pnn.rb +2 -2
- metadata +12 -27
- data/lib/sashite/pnn/piece.rb +0 -441
data/README.md
CHANGED
|
@@ -9,9 +9,9 @@
|
|
|
9
9
|
|
|
10
10
|
## What is PNN?
|
|
11
11
|
|
|
12
|
-
PNN (Piece Name Notation)
|
|
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 (e.g., `"KING"`, `"queen"`, `"+ROOK"`, `"-pawn"`).
|
|
13
13
|
|
|
14
|
-
This gem implements the [PNN Specification v1.0.0](https://sashite.dev/specs/pnn/1.0.0/),
|
|
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.
|
|
15
15
|
|
|
16
16
|
## Installation
|
|
17
17
|
|
|
@@ -28,490 +28,184 @@ gem install sashite-pnn
|
|
|
28
28
|
|
|
29
29
|
## Usage
|
|
30
30
|
|
|
31
|
+
### Basic Operations
|
|
32
|
+
|
|
31
33
|
```ruby
|
|
32
34
|
require "sashite/pnn"
|
|
33
35
|
|
|
34
|
-
# Parse PNN strings into piece objects
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
piece.side # => :first
|
|
39
|
-
piece.state # => :normal
|
|
40
|
-
piece.native? # => true
|
|
36
|
+
# Parse PNN strings into piece name objects
|
|
37
|
+
name = Sashite::Pnn.parse("KING") # => #<Pnn::Name value="KING">
|
|
38
|
+
name.to_s # => "KING"
|
|
39
|
+
name.value # => "KING"
|
|
41
40
|
|
|
42
|
-
# Create
|
|
43
|
-
|
|
44
|
-
|
|
41
|
+
# Create from string or symbol
|
|
42
|
+
name = Sashite::Pnn.name("queen") # => #<Pnn::Name value="queen">
|
|
43
|
+
name = Sashite::Pnn::Name.new(:ROOK) # => #<Pnn::Name value="ROOK">
|
|
45
44
|
|
|
46
45
|
# Validate PNN strings
|
|
47
|
-
Sashite::Pnn.valid?("
|
|
48
|
-
Sashite::Pnn.valid?("
|
|
49
|
-
Sashite::Pnn.valid?("
|
|
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)
|
|
46
|
+
Sashite::Pnn.valid?("BISHOP") # => true
|
|
47
|
+
Sashite::Pnn.valid?("King") # => false (mixed case not allowed)
|
|
48
|
+
Sashite::Pnn.valid?("+ROOK") # => true (enhanced state)
|
|
49
|
+
Sashite::Pnn.valid?("-pawn") # => true (diminished state)
|
|
115
50
|
```
|
|
116
51
|
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
### Structure
|
|
120
|
-
```
|
|
121
|
-
<pin>[<suffix>]
|
|
122
|
-
```
|
|
52
|
+
### State Modifiers
|
|
123
53
|
|
|
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
|
|
140
54
|
```ruby
|
|
141
|
-
|
|
142
|
-
|
|
55
|
+
# Enhanced pieces (+ prefix)
|
|
56
|
+
enhanced = Sashite::Pnn.parse("+QUEEN")
|
|
57
|
+
enhanced.enhanced? # => true
|
|
58
|
+
enhanced.normal? # => false
|
|
59
|
+
enhanced.base_name # => "QUEEN"
|
|
143
60
|
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
- `-p` - Second player pawn (native style, diminished state)
|
|
61
|
+
# Diminished pieces (- prefix)
|
|
62
|
+
diminished = Sashite::Pnn.parse("-pawn")
|
|
63
|
+
diminished.diminished? # => true
|
|
64
|
+
diminished.base_name # => "pawn"
|
|
149
65
|
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
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
|
|
66
|
+
# Normal pieces (no prefix)
|
|
67
|
+
normal = Sashite::Pnn.parse("KNIGHT")
|
|
68
|
+
normal.normal? # => true
|
|
69
|
+
normal.enhanced? # => false
|
|
70
|
+
normal.diminished? # => false
|
|
174
71
|
```
|
|
175
72
|
|
|
176
|
-
###
|
|
73
|
+
### Player Assignment
|
|
177
74
|
|
|
178
75
|
```ruby
|
|
179
|
-
#
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
black_pieces = %w[k q +r b n p].map { |pin| Sashite::Pnn.parse(pin) }
|
|
76
|
+
# First player pieces (uppercase)
|
|
77
|
+
first_player = Sashite::Pnn.parse("KING")
|
|
78
|
+
first_player.first_player? # => true
|
|
79
|
+
first_player.second_player? # => false
|
|
184
80
|
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
#
|
|
189
|
-
white_pieces.map(&:to_s) # => ["K", "Q", "+R", "B", "N", "P"]
|
|
190
|
-
black_pieces.map(&:to_s) # => ["k", "q", "+r", "b", "n", "p"]
|
|
81
|
+
# Second player pieces (lowercase)
|
|
82
|
+
second_player = Sashite::Pnn.parse("king")
|
|
83
|
+
second_player.first_player? # => false
|
|
84
|
+
second_player.second_player? # => true
|
|
191
85
|
```
|
|
192
86
|
|
|
193
|
-
###
|
|
87
|
+
### Normalization and Comparison
|
|
194
88
|
|
|
195
89
|
```ruby
|
|
196
|
-
|
|
197
|
-
|
|
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)
|
|
90
|
+
a = Sashite::Pnn.parse("ROOK")
|
|
91
|
+
b = Sashite::Pnn.parse("ROOK")
|
|
202
92
|
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
foreign_piece.to_s # => "r'" (black foreign rook)
|
|
93
|
+
a == b # => true
|
|
94
|
+
a.same_base_name?(Sashite::Pnn.parse("rook")) # => true (same piece, different player)
|
|
95
|
+
a.to_s # => "ROOK"
|
|
207
96
|
```
|
|
208
97
|
|
|
209
|
-
|
|
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
|
|
98
|
+
### Collections and Filtering
|
|
284
99
|
|
|
285
100
|
```ruby
|
|
286
|
-
|
|
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
|
-
```
|
|
101
|
+
pieces = %w[KING queen +ROOK -pawn BISHOP knight].map { |n| Sashite::Pnn.parse(n) }
|
|
303
102
|
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
#
|
|
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
|
-
```
|
|
103
|
+
# Filter by player
|
|
104
|
+
first_player_pieces = pieces.select(&:first_player?).map(&:to_s)
|
|
105
|
+
# => ["KING", "+ROOK", "BISHOP"]
|
|
322
106
|
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
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
|
-
```
|
|
368
|
-
|
|
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
|
|
107
|
+
# Filter by state
|
|
108
|
+
enhanced_pieces = pieces.select(&:enhanced?).map(&:to_s)
|
|
109
|
+
# => ["+ROOK"]
|
|
376
110
|
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
111
|
+
diminished_pieces = pieces.select(&:diminished?).map(&:to_s)
|
|
112
|
+
# => ["-pawn"]
|
|
113
|
+
```
|
|
380
114
|
|
|
381
|
-
|
|
382
|
-
end
|
|
115
|
+
## Format Specification
|
|
383
116
|
|
|
384
|
-
|
|
385
|
-
pin_pieces = %w[K Q +R -P k q r p]
|
|
386
|
-
pnn_pieces = pin_pieces.map { |pin| convert_pin_to_pnn(pin) }
|
|
117
|
+
### Structure
|
|
387
118
|
|
|
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
119
|
```
|
|
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)
|
|
120
|
+
<state-modifier>?<piece-name>
|
|
419
121
|
```
|
|
420
122
|
|
|
421
|
-
|
|
123
|
+
Where:
|
|
124
|
+
- `<state-modifier>` is optional `+` (enhanced) or `-` (diminished)
|
|
125
|
+
- `<piece-name>` is case-consistent alphabetic characters
|
|
422
126
|
|
|
423
|
-
|
|
127
|
+
### Grammar (BNF)
|
|
424
128
|
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
-
|
|
428
|
-
- **Immutability**: All transformations return new instances, maintaining functional programming principles
|
|
129
|
+
```bnf
|
|
130
|
+
<pnn> ::= <state-modifier> <name-body>
|
|
131
|
+
| <name-body>
|
|
429
132
|
|
|
430
|
-
|
|
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
|
|
133
|
+
<state-modifier> ::= "+" | "-"
|
|
435
134
|
|
|
436
|
-
|
|
135
|
+
<name-body> ::= <uppercase-name> | <lowercase-name>
|
|
437
136
|
|
|
438
|
-
|
|
137
|
+
<uppercase-name> ::= <uppercase-letter>+
|
|
138
|
+
<lowercase-name> ::= <lowercase-letter>+
|
|
439
139
|
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
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 | |
|
|
140
|
+
<uppercase-letter> ::= "A" | "B" | "C" | ... | "Z"
|
|
141
|
+
<lowercase-letter> ::= "a" | "b" | "c" | ... | "z"
|
|
142
|
+
```
|
|
446
143
|
|
|
447
|
-
|
|
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)
|
|
144
|
+
### Regular Expression
|
|
450
145
|
|
|
451
|
-
|
|
146
|
+
```ruby
|
|
147
|
+
/\A[+-]?([A-Z]+|[a-z]+)\z/
|
|
148
|
+
```
|
|
452
149
|
|
|
453
|
-
##
|
|
150
|
+
## Design Principles
|
|
454
151
|
|
|
455
|
-
* **
|
|
456
|
-
* **
|
|
457
|
-
* **
|
|
458
|
-
* **
|
|
459
|
-
* **
|
|
460
|
-
* **
|
|
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
|
|
152
|
+
* **Human-readable**: Names like `"KING"` or `"queen"` are intuitive and descriptive.
|
|
153
|
+
* **State-aware**: Integrated state management through `+` and `-` modifiers.
|
|
154
|
+
* **Rule-agnostic**: Independent of specific game mechanics.
|
|
155
|
+
* **Case-consistent**: Visual distinction between players through case.
|
|
156
|
+
* **Canonical**: One valid name per piece state within a given context.
|
|
157
|
+
* **ASCII-only**: Compatible with all systems.
|
|
464
158
|
|
|
465
|
-
##
|
|
159
|
+
## Integration with PIN
|
|
466
160
|
|
|
467
|
-
|
|
161
|
+
PNN names serve as the formal source for PIN character identifiers. For example:
|
|
468
162
|
|
|
469
|
-
PNN
|
|
163
|
+
| PNN | PIN | Description |
|
|
164
|
+
| --------- | ------- | ----------- |
|
|
165
|
+
| `KING` | `K` | First player king |
|
|
166
|
+
| `king` | `k` | Second player king |
|
|
167
|
+
| `+ROOK` | `+R` | Enhanced first player rook |
|
|
168
|
+
| `-pawn` | `-p` | Diminished second player pawn |
|
|
470
169
|
|
|
471
|
-
|
|
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
|
|
170
|
+
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.
|
|
475
171
|
|
|
476
|
-
|
|
172
|
+
## Examples
|
|
477
173
|
|
|
478
174
|
```ruby
|
|
479
|
-
#
|
|
480
|
-
#
|
|
481
|
-
#
|
|
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)
|
|
487
|
-
```
|
|
175
|
+
# Traditional pieces
|
|
176
|
+
Sashite::Pnn.parse("KING") # => #<Pnn::Name value="KING">
|
|
177
|
+
Sashite::Pnn.parse("queen") # => #<Pnn::Name value="queen">
|
|
488
178
|
|
|
489
|
-
|
|
179
|
+
# State modifiers
|
|
180
|
+
Sashite::Pnn.parse("+ROOK") # => #<Pnn::Name value="+ROOK">
|
|
181
|
+
Sashite::Pnn.parse("-pawn") # => #<Pnn::Name value="-pawn">
|
|
490
182
|
|
|
491
|
-
|
|
183
|
+
# Validation
|
|
184
|
+
Sashite::Pnn.valid?("BISHOP") # => true
|
|
185
|
+
Sashite::Pnn.valid?("Bishop") # => false (mixed case)
|
|
186
|
+
Sashite::Pnn.valid?("KING1") # => false (no digits allowed)
|
|
187
|
+
```
|
|
492
188
|
|
|
493
|
-
|
|
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
|
|
189
|
+
## API Reference
|
|
498
190
|
|
|
499
|
-
|
|
191
|
+
### Main Module
|
|
500
192
|
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
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
|
|
193
|
+
* `Sashite::Pnn.valid?(str)` â€" Returns `true` if the string is valid PNN.
|
|
194
|
+
* `Sashite::Pnn.parse(str)` â€" Returns a `Sashite::Pnn::Name` object.
|
|
195
|
+
* `Sashite::Pnn.name(sym_or_str)` â€" Alias for constructing a name.
|
|
508
196
|
|
|
509
|
-
|
|
197
|
+
### `Sashite::Pnn::Name`
|
|
510
198
|
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
199
|
+
* `#value` â€" Returns the canonical string value.
|
|
200
|
+
* `#to_s` â€" Returns the string representation.
|
|
201
|
+
* `#base_name` â€" Returns the name without state modifier.
|
|
202
|
+
* `#enhanced?` â€" Returns `true` if piece has enhanced state (`+` prefix).
|
|
203
|
+
* `#diminished?` â€" Returns `true` if piece has diminished state (`-` prefix).
|
|
204
|
+
* `#normal?` â€" Returns `true` if piece has normal state (no prefix).
|
|
205
|
+
* `#first_player?` â€" Returns `true` if piece belongs to first player (uppercase).
|
|
206
|
+
* `#second_player?` â€" Returns `true` if piece belongs to second player (lowercase).
|
|
207
|
+
* `#same_base_name?(other)` â€" Returns `true` if both pieces have same base name.
|
|
208
|
+
* `#==`, `#eql?`, `#hash` â€" Value-based equality.
|
|
515
209
|
|
|
516
210
|
## Development
|
|
517
211
|
|
|
@@ -546,4 +240,4 @@ Available as open source under the [MIT License](https://opensource.org/licenses
|
|
|
546
240
|
|
|
547
241
|
## About
|
|
548
242
|
|
|
549
|
-
Maintained by [Sashité](https://sashite.com/)
|
|
243
|
+
Maintained by [Sashité](https://sashite.com/) â€" promoting chess variants and sharing the beauty of board game cultures.
|