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.
- checksums.yaml +4 -4
- data/README.md +430 -231
- data/lib/sashite/pnn/piece.rb +339 -132
- data/lib/sashite/pnn.rb +38 -29
- data/lib/sashite-pnn.rb +9 -16
- metadata +13 -11
data/README.md
CHANGED
|
@@ -5,13 +5,13 @@
|
|
|
5
5
|

|
|
6
6
|
[](https://github.com/sashite/pnn.rb/raw/main/LICENSE.md)
|
|
7
7
|
|
|
8
|
-
> **PNN** (Piece Name Notation)
|
|
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.
|
|
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
|
|
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
|
-
##
|
|
29
|
+
## Usage
|
|
30
30
|
|
|
31
|
-
|
|
31
|
+
```ruby
|
|
32
|
+
require "sashite/pnn"
|
|
32
33
|
|
|
33
|
-
|
|
34
|
-
|
|
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
|
-
|
|
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
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
- `+R'` - First player rook with enhanced state and foreign style
|
|
119
|
+
### Structure
|
|
120
|
+
```
|
|
121
|
+
<pin>[<suffix>]
|
|
122
|
+
```
|
|
46
123
|
|
|
47
|
-
|
|
124
|
+
### Components
|
|
48
125
|
|
|
49
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
61
|
-
|
|
62
|
-
|
|
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
|
-
|
|
65
|
-
enhanced_foreign = Sashite::Pnn::Piece.parse("+k'")
|
|
66
|
-
# => #<Sashite::Pnn::Piece letter="k" enhanced=true native=false>
|
|
150
|
+
## Game Examples
|
|
67
151
|
|
|
68
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
79
|
-
|
|
80
|
-
piece
|
|
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
|
-
|
|
84
|
-
|
|
85
|
-
|
|
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
|
-
|
|
88
|
-
|
|
89
|
-
# =>
|
|
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
|
|
93
|
-
|
|
94
|
-
Create new piece instances with different styles:
|
|
176
|
+
### Single-Style Games (PIN Compatibility)
|
|
95
177
|
|
|
96
178
|
```ruby
|
|
97
|
-
|
|
179
|
+
# Traditional Chess (both players use Chess style)
|
|
180
|
+
# All pieces are native, so PNN behaves exactly like PIN
|
|
98
181
|
|
|
99
|
-
|
|
100
|
-
|
|
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
|
-
#
|
|
104
|
-
native
|
|
105
|
-
native.to_s # => "k"
|
|
185
|
+
white_pieces.all?(&:native?) # => true
|
|
186
|
+
black_pieces.all?(&:native?) # => true
|
|
106
187
|
|
|
107
|
-
#
|
|
108
|
-
|
|
109
|
-
|
|
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
|
-
###
|
|
113
|
-
|
|
114
|
-
All PIN state manipulation methods are available:
|
|
193
|
+
### Style Mutation During Gameplay
|
|
115
194
|
|
|
116
195
|
```ruby
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
#
|
|
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
|
-
#
|
|
124
|
-
|
|
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
|
-
#
|
|
128
|
-
|
|
129
|
-
|
|
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
|
-
|
|
209
|
+
## API Reference
|
|
133
210
|
|
|
134
|
-
|
|
211
|
+
### Main Module Methods
|
|
135
212
|
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
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
|
-
###
|
|
217
|
+
### Piece Class
|
|
150
218
|
|
|
151
|
-
|
|
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
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
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
|
-
|
|
159
|
-
|
|
160
|
-
|
|
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
|
-
|
|
163
|
-
promoted_shogi = Sashite::Pnn::Piece.parse("+P'") # Promoted Shōgi pawn (foreign to first player)
|
|
164
|
-
```
|
|
281
|
+
## Advanced Usage
|
|
165
282
|
|
|
166
|
-
### Style
|
|
283
|
+
### Style Derivation Examples
|
|
167
284
|
|
|
168
285
|
```ruby
|
|
169
|
-
#
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
#
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
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
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
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
|
-
|
|
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
|
-
|
|
193
|
-
|
|
194
|
-
|
|
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
|
-
|
|
197
|
-
|
|
198
|
-
|
|
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
|
-
|
|
202
|
-
|
|
203
|
-
native.foreign? # => false
|
|
204
|
-
```
|
|
381
|
+
"#{pnn_piece.prefix}#{pnn_piece.letter}"
|
|
382
|
+
end
|
|
205
383
|
|
|
206
|
-
|
|
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
|
-
|
|
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
|
-
###
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
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
|
-
|
|
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
|
-
|
|
423
|
+
This gem uses **composition over inheritance** by building upon the proven [sashite-pin](https://github.com/sashite/pin.rb) gem:
|
|
228
424
|
|
|
229
|
-
|
|
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
|
-
|
|
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
|
-
|
|
234
|
-
- `Sashite::Pnn.parse(pnn_string)` - Parse a PNN string into a piece object
|
|
436
|
+
## Protocol Mapping
|
|
235
437
|
|
|
236
|
-
|
|
438
|
+
Following the [Game Protocol](https://sashite.dev/game-protocol/):
|
|
237
439
|
|
|
238
|
-
|
|
239
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
253
|
-
|
|
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
|
-
|
|
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
|
-
|
|
467
|
+
### Style Derivation Convention
|
|
261
468
|
|
|
262
|
-
|
|
469
|
+
PNN follows a strict style derivation convention:
|
|
263
470
|
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
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
|
-
###
|
|
476
|
+
### Example Flow
|
|
276
477
|
|
|
277
478
|
```ruby
|
|
278
|
-
# Chess
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
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
|
-
|
|
489
|
+
This ensures that `parse(pnn).to_s == pnn` for all valid PNN strings while enabling cross-style gameplay.
|
|
286
490
|
|
|
287
|
-
|
|
491
|
+
## System Constraints
|
|
288
492
|
|
|
289
|
-
|
|
290
|
-
|
|
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
|
-
|
|
293
|
-
result = piece.enhance.foreignize.flip
|
|
294
|
-
result.to_s # => "+K'"
|
|
499
|
+
## Related Specifications
|
|
295
500
|
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
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
|
-
|
|
509
|
+
## Documentation
|
|
302
510
|
|
|
303
|
-
|
|
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
|
-
|
|
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
|
-
|
|
518
|
+
```sh
|
|
519
|
+
# Clone the repository
|
|
520
|
+
git clone https://github.com/sashite/pnn.rb.git
|
|
521
|
+
cd pnn.rb
|
|
323
522
|
|
|
324
|
-
|
|
325
|
-
|
|
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
|
-
|
|
526
|
+
# Run tests
|
|
527
|
+
ruby test.rb
|
|
332
528
|
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
* Foreign style pieces represent adoption of the opponent's style system
|
|
529
|
+
# Generate documentation
|
|
530
|
+
yard doc
|
|
531
|
+
```
|
|
337
532
|
|
|
338
|
-
##
|
|
533
|
+
## Contributing
|
|
339
534
|
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
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
|
|