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.
- checksums.yaml +4 -4
- data/LICENSE.md +18 -17
- data/README.md +184 -437
- data/lib/sashite/pnn/name.rb +223 -0
- data/lib/sashite/pnn.rb +39 -42
- data/lib/sashite-pnn.rb +2 -2
- metadata +13 -27
- data/lib/sashite/pnn/piece.rb +0 -441
data/README.md
CHANGED
|
@@ -9,512 +9,259 @@
|
|
|
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 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/),
|
|
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
|
-
|
|
36
|
-
|
|
37
|
-
|
|
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
|
|
43
|
-
|
|
44
|
-
|
|
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?("
|
|
48
|
-
Sashite::Pnn.valid?("
|
|
49
|
-
Sashite::Pnn.valid?("
|
|
50
|
-
|
|
51
|
-
#
|
|
52
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
###
|
|
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
|
-
#
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
#
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
#
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
#
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
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
|
-
###
|
|
177
|
-
|
|
94
|
+
### Player Assignment
|
|
178
95
|
```ruby
|
|
179
|
-
#
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
#
|
|
189
|
-
|
|
190
|
-
|
|
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
|
-
###
|
|
194
|
-
|
|
115
|
+
### Normalization and Comparison
|
|
195
116
|
```ruby
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
#
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
```
|
|
131
|
+
# Filter by player
|
|
132
|
+
first_player_pieces = pieces.select(&:first_player?).map(&:to_s)
|
|
133
|
+
# => ["KING^", "+ROOK", "BISHOP", "GENERAL^"]
|
|
322
134
|
|
|
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
|
-
```
|
|
135
|
+
# Filter by state
|
|
136
|
+
enhanced_pieces = pieces.select(&:enhanced?).map(&:to_s)
|
|
137
|
+
# => ["+ROOK"]
|
|
368
138
|
|
|
369
|
-
|
|
370
|
-
|
|
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
|
-
|
|
378
|
-
|
|
379
|
-
|
|
142
|
+
# Filter by terminal status
|
|
143
|
+
terminal_pieces = pieces.select(&:terminal?).map(&:to_s)
|
|
144
|
+
# => ["KING^", "knight^", "GENERAL^"]
|
|
380
145
|
|
|
381
|
-
|
|
382
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
422
|
-
|
|
423
|
-
|
|
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
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
-
|
|
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
|
-
|
|
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
|
-
|
|
172
|
+
<name-body> ::= <uppercase-name> | <lowercase-name>
|
|
437
173
|
|
|
438
|
-
|
|
174
|
+
<uppercase-name> ::= <uppercase-letter>+
|
|
175
|
+
<lowercase-name> ::= <lowercase-letter>+
|
|
439
176
|
|
|
440
|
-
|
|
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
|
-
|
|
448
|
-
-
|
|
449
|
-
|
|
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
|
-
|
|
183
|
+
### Regular Expression
|
|
184
|
+
```ruby
|
|
185
|
+
/\A[+-]?([A-Z]+|[a-z]+)\^?\z/
|
|
186
|
+
```
|
|
454
187
|
|
|
455
|
-
|
|
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
|
-
|
|
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
|
-
|
|
198
|
+
## Integration with PIN
|
|
468
199
|
|
|
469
|
-
PNN
|
|
200
|
+
PNN names serve as the formal source for PIN character identifiers. For example:
|
|
470
201
|
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
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
|
-
|
|
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
|
-
#
|
|
480
|
-
#
|
|
481
|
-
#
|
|
482
|
-
|
|
483
|
-
#
|
|
484
|
-
#
|
|
485
|
-
#
|
|
486
|
-
|
|
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
|
-
|
|
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
|
-
|
|
244
|
+
### Main Module
|
|
500
245
|
|
|
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
|
|
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
|
-
|
|
250
|
+
### `Sashite::Pnn::Name`
|
|
510
251
|
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
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
|