sashite-pnn 1.0.1 → 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 +142 -249
- data/lib/sashite/pnn/name.rb +196 -0
- data/lib/sashite/pnn.rb +43 -37
- data/lib/sashite-pnn.rb +9 -16
- metadata +13 -26
- data/lib/sashite/pnn/piece.rb +0 -234
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 4d74e4979082abf26a0ec1d7e871f9d3a31834cbbca683e73952f789f88b50c8
|
|
4
|
+
data.tar.gz: 5587bdfc31daba336a8c50be37ee8f342ab90c10b4f1d61b8b73426f1a9c071e
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: '04313675987fd9ec5b2262ce3aeea7263e0a0cbd863a4a894def608a6b6b716c8991447227c9194260e248a0bde325958459eea80e1480117435f0b3ff903ea4'
|
|
7
|
+
data.tar.gz: 618b5df20b597a34228458430f88b9cf4134b8997348e7f2691a3411534001ea74358b62cd914b66338c6bd6a6e047ec43077775d9af6d3971ed227696e804f8
|
data/LICENSE.md
CHANGED
|
@@ -1,21 +1,22 @@
|
|
|
1
|
-
|
|
1
|
+
Copyright (c) 2025 Cyril Kato
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
MIT License
|
|
4
4
|
|
|
5
|
-
Permission is hereby granted, free of charge, to any person obtaining
|
|
6
|
-
of this software and associated documentation files (the
|
|
7
|
-
in the Software without restriction, including
|
|
8
|
-
to use, copy, modify, merge, publish,
|
|
9
|
-
copies of the Software, and to
|
|
10
|
-
furnished to do so, subject to
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
|
6
|
+
a copy of this software and associated documentation files (the
|
|
7
|
+
"Software"), to deal in the Software without restriction, including
|
|
8
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
|
9
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
|
10
|
+
permit persons to whom the Software is furnished to do so, subject to
|
|
11
|
+
the following conditions:
|
|
11
12
|
|
|
12
|
-
The above copyright notice and this permission notice shall be
|
|
13
|
-
all copies or substantial portions of the Software.
|
|
13
|
+
The above copyright notice and this permission notice shall be
|
|
14
|
+
included in all copies or substantial portions of the Software.
|
|
14
15
|
|
|
15
|
-
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
|
16
|
-
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
|
17
|
-
FITNESS FOR A PARTICULAR PURPOSE AND
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
THE SOFTWARE.
|
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
|
19
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
|
20
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
|
21
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
|
22
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
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)
|
|
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
|
|
|
@@ -26,320 +26,213 @@ Or install manually:
|
|
|
26
26
|
gem install sashite-pnn
|
|
27
27
|
```
|
|
28
28
|
|
|
29
|
-
##
|
|
29
|
+
## Usage
|
|
30
30
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
```
|
|
34
|
-
<pin>[<suffix>]
|
|
35
|
-
```
|
|
36
|
-
|
|
37
|
-
Where:
|
|
38
|
-
- `<pin>` is a valid PIN string (`[<state>]<letter>`)
|
|
39
|
-
- `<suffix>` is an optional derivation marker (`'` for foreign style)
|
|
40
|
-
|
|
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
|
|
46
|
-
|
|
47
|
-
## Basic Usage
|
|
48
|
-
|
|
49
|
-
### Creating Piece Objects
|
|
50
|
-
|
|
51
|
-
The primary interface is the `Sashite::Pnn::Piece` class, which represents a single piece in PNN format:
|
|
31
|
+
### Basic Operations
|
|
52
32
|
|
|
53
33
|
```ruby
|
|
54
34
|
require "sashite/pnn"
|
|
55
35
|
|
|
56
|
-
# Parse
|
|
57
|
-
|
|
58
|
-
# =>
|
|
59
|
-
|
|
60
|
-
# With derivation marker
|
|
61
|
-
foreign_piece = Sashite::Pnn::Piece.parse("k'")
|
|
62
|
-
# => #<Sashite::Pnn::Piece letter="k" native=false>
|
|
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"
|
|
63
40
|
|
|
64
|
-
#
|
|
65
|
-
|
|
66
|
-
# => #<
|
|
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">
|
|
67
44
|
|
|
68
|
-
#
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
45
|
+
# Validate PNN strings
|
|
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)
|
|
72
50
|
```
|
|
73
51
|
|
|
74
|
-
###
|
|
75
|
-
|
|
76
|
-
Convert a piece object back to its PNN string representation:
|
|
52
|
+
### State Modifiers
|
|
77
53
|
|
|
78
54
|
```ruby
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
# =>
|
|
55
|
+
# Enhanced pieces (+ prefix)
|
|
56
|
+
enhanced = Sashite::Pnn.parse("+QUEEN")
|
|
57
|
+
enhanced.enhanced? # => true
|
|
58
|
+
enhanced.normal? # => false
|
|
59
|
+
enhanced.base_name # => "QUEEN"
|
|
82
60
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
# =>
|
|
61
|
+
# Diminished pieces (- prefix)
|
|
62
|
+
diminished = Sashite::Pnn.parse("-pawn")
|
|
63
|
+
diminished.diminished? # => true
|
|
64
|
+
diminished.base_name # => "pawn"
|
|
86
65
|
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
# =>
|
|
66
|
+
# Normal pieces (no prefix)
|
|
67
|
+
normal = Sashite::Pnn.parse("KNIGHT")
|
|
68
|
+
normal.normal? # => true
|
|
69
|
+
normal.enhanced? # => false
|
|
70
|
+
normal.diminished? # => false
|
|
90
71
|
```
|
|
91
72
|
|
|
92
|
-
###
|
|
93
|
-
|
|
94
|
-
Create new piece instances with different styles:
|
|
73
|
+
### Player Assignment
|
|
95
74
|
|
|
96
75
|
```ruby
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
#
|
|
100
|
-
|
|
101
|
-
foreign.to_s # => "k'"
|
|
76
|
+
# First player pieces (uppercase)
|
|
77
|
+
first_player = Sashite::Pnn.parse("KING")
|
|
78
|
+
first_player.first_player? # => true
|
|
79
|
+
first_player.second_player? # => false
|
|
102
80
|
|
|
103
|
-
#
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
# Toggle style
|
|
108
|
-
toggled = piece.toggle_style
|
|
109
|
-
toggled.to_s # => "k'"
|
|
81
|
+
# Second player pieces (lowercase)
|
|
82
|
+
second_player = Sashite::Pnn.parse("king")
|
|
83
|
+
second_player.first_player? # => false
|
|
84
|
+
second_player.second_player? # => true
|
|
110
85
|
```
|
|
111
86
|
|
|
112
|
-
###
|
|
113
|
-
|
|
114
|
-
All PIN state manipulation methods are available:
|
|
87
|
+
### Normalization and Comparison
|
|
115
88
|
|
|
116
89
|
```ruby
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
# Enhanced state (+ prefix)
|
|
120
|
-
enhanced = piece.enhance
|
|
121
|
-
enhanced.to_s # => "+k"
|
|
90
|
+
a = Sashite::Pnn.parse("ROOK")
|
|
91
|
+
b = Sashite::Pnn.parse("ROOK")
|
|
122
92
|
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
# Combine with style changes
|
|
128
|
-
enhanced_foreign = piece.enhance.foreignize
|
|
129
|
-
enhanced_foreign.to_s # => "+k'"
|
|
93
|
+
a == b # => true
|
|
94
|
+
a.same_base_name?(Sashite::Pnn.parse("rook")) # => true (same piece, different player)
|
|
95
|
+
a.to_s # => "ROOK"
|
|
130
96
|
```
|
|
131
97
|
|
|
132
|
-
###
|
|
133
|
-
|
|
134
|
-
Change piece ownership (case conversion):
|
|
98
|
+
### Collections and Filtering
|
|
135
99
|
|
|
136
100
|
```ruby
|
|
137
|
-
|
|
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
|
-
```
|
|
101
|
+
pieces = %w[KING queen +ROOK -pawn BISHOP knight].map { |n| Sashite::Pnn.parse(n) }
|
|
146
102
|
|
|
147
|
-
|
|
103
|
+
# Filter by player
|
|
104
|
+
first_player_pieces = pieces.select(&:first_player?).map(&:to_s)
|
|
105
|
+
# => ["KING", "+ROOK", "BISHOP"]
|
|
148
106
|
|
|
149
|
-
|
|
107
|
+
# Filter by state
|
|
108
|
+
enhanced_pieces = pieces.select(&:enhanced?).map(&:to_s)
|
|
109
|
+
# => ["+ROOK"]
|
|
150
110
|
|
|
151
|
-
|
|
111
|
+
diminished_pieces = pieces.select(&:diminished?).map(&:to_s)
|
|
112
|
+
# => ["-pawn"]
|
|
113
|
+
```
|
|
152
114
|
|
|
153
|
-
|
|
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)
|
|
115
|
+
## Format Specification
|
|
157
116
|
|
|
158
|
-
|
|
159
|
-
black_pawn = Sashite::Pnn::Piece.parse("p") # Shōgi pawn (native)
|
|
160
|
-
black_chess_pawn = Sashite::Pnn::Piece.parse("p'") # Chess pawn (foreign)
|
|
117
|
+
### Structure
|
|
161
118
|
|
|
162
|
-
# Promoted pieces with style
|
|
163
|
-
promoted_shogi = Sashite::Pnn::Piece.parse("+P'") # Promoted Shōgi pawn (foreign to first player)
|
|
164
119
|
```
|
|
165
|
-
|
|
166
|
-
### Style Conversions During Gameplay
|
|
167
|
-
|
|
168
|
-
```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'"
|
|
120
|
+
<state-modifier>?<piece-name>
|
|
178
121
|
```
|
|
179
122
|
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
123
|
+
Where:
|
|
124
|
+
- `<state-modifier>` is optional `+` (enhanced) or `-` (diminished)
|
|
125
|
+
- `<piece-name>` is case-consistent alphabetic characters
|
|
183
126
|
|
|
184
|
-
|
|
185
|
-
- **Apostrophe suffix (`'`)**: Piece has the **foreign style** (opposite side's native style)
|
|
127
|
+
### Grammar (BNF)
|
|
186
128
|
|
|
187
|
-
|
|
129
|
+
```bnf
|
|
130
|
+
<pnn> ::= <state-modifier> <name-body>
|
|
131
|
+
| <name-body>
|
|
188
132
|
|
|
189
|
-
|
|
190
|
-
piece = Sashite::Pnn::Piece.parse("K")
|
|
133
|
+
<state-modifier> ::= "+" | "-"
|
|
191
134
|
|
|
192
|
-
|
|
193
|
-
piece.native? # => true
|
|
194
|
-
piece.foreign? # => false
|
|
135
|
+
<name-body> ::= <uppercase-name> | <lowercase-name>
|
|
195
136
|
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
foreign.native? # => false
|
|
199
|
-
foreign.foreign? # => true
|
|
137
|
+
<uppercase-name> ::= <uppercase-letter>+
|
|
138
|
+
<lowercase-name> ::= <lowercase-letter>+
|
|
200
139
|
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
native.foreign? # => false
|
|
140
|
+
<uppercase-letter> ::= "A" | "B" | "C" | ... | "Z"
|
|
141
|
+
<lowercase-letter> ::= "a" | "b" | "c" | ... | "z"
|
|
204
142
|
```
|
|
205
143
|
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
The `Sashite::Pnn::Piece` class inherits all PIN functionality and adds style methods:
|
|
209
|
-
|
|
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` |
|
|
219
|
-
|
|
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` |
|
|
226
|
-
|
|
227
|
-
All methods return new `Sashite::Pnn::Piece` instances, leaving the original unchanged (immutable design).
|
|
144
|
+
### Regular Expression
|
|
228
145
|
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
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
|
|
235
|
-
|
|
236
|
-
### Sashite::Pnn::Piece Class Methods
|
|
237
|
-
|
|
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
|
|
146
|
+
```ruby
|
|
147
|
+
/\A[+-]?([A-Z]+|[a-z]+)\z/
|
|
148
|
+
```
|
|
240
149
|
|
|
241
|
-
|
|
150
|
+
## Design Principles
|
|
242
151
|
|
|
243
|
-
|
|
244
|
-
-
|
|
245
|
-
|
|
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.
|
|
246
158
|
|
|
247
|
-
|
|
248
|
-
- `#foreignize` - Convert to foreign style
|
|
249
|
-
- `#nativize` - Convert to native style
|
|
250
|
-
- `#toggle_style` - Toggle between native/foreign style
|
|
159
|
+
## Integration with PIN
|
|
251
160
|
|
|
252
|
-
|
|
253
|
-
All methods from `Sashite::Pin::Piece` are available, including state queries, state manipulation, and conversion methods.
|
|
161
|
+
PNN names serve as the formal source for PIN character identifiers. For example:
|
|
254
162
|
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
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 |
|
|
259
169
|
|
|
260
|
-
|
|
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.
|
|
261
171
|
|
|
262
|
-
|
|
172
|
+
## Examples
|
|
263
173
|
|
|
264
174
|
```ruby
|
|
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
|
-
```
|
|
175
|
+
# Traditional pieces
|
|
176
|
+
Sashite::Pnn.parse("KING") # => #<Pnn::Name value="KING">
|
|
177
|
+
Sashite::Pnn.parse("queen") # => #<Pnn::Name value="queen">
|
|
274
178
|
|
|
275
|
-
|
|
179
|
+
# State modifiers
|
|
180
|
+
Sashite::Pnn.parse("+ROOK") # => #<Pnn::Name value="+ROOK">
|
|
181
|
+
Sashite::Pnn.parse("-pawn") # => #<Pnn::Name value="-pawn">
|
|
276
182
|
|
|
277
|
-
|
|
278
|
-
#
|
|
279
|
-
|
|
280
|
-
|
|
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)
|
|
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)
|
|
283
187
|
```
|
|
284
188
|
|
|
285
|
-
##
|
|
286
|
-
|
|
287
|
-
### Chaining Operations
|
|
288
|
-
|
|
289
|
-
```ruby
|
|
290
|
-
piece = Sashite::Pnn::Piece.parse("k")
|
|
189
|
+
## API Reference
|
|
291
190
|
|
|
292
|
-
|
|
293
|
-
result = piece.enhance.foreignize.flip
|
|
294
|
-
result.to_s # => "+K'"
|
|
191
|
+
### Main Module
|
|
295
192
|
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
```
|
|
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.
|
|
300
196
|
|
|
301
|
-
###
|
|
197
|
+
### `Sashite::Pnn::Name`
|
|
302
198
|
|
|
303
|
-
|
|
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.
|
|
304
209
|
|
|
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
|
-
```
|
|
210
|
+
## Development
|
|
321
211
|
|
|
322
|
-
|
|
212
|
+
```sh
|
|
213
|
+
# Clone the repository
|
|
214
|
+
git clone https://github.com/sashite/pnn.rb.git
|
|
215
|
+
cd pnn.rb
|
|
323
216
|
|
|
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)
|
|
217
|
+
# Install dependencies
|
|
218
|
+
bundle install
|
|
330
219
|
|
|
331
|
-
|
|
220
|
+
# Run tests
|
|
221
|
+
ruby test.rb
|
|
332
222
|
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
* Foreign style pieces represent adoption of the opponent's style system
|
|
223
|
+
# Generate documentation
|
|
224
|
+
yard doc
|
|
225
|
+
```
|
|
337
226
|
|
|
338
|
-
##
|
|
227
|
+
## Contributing
|
|
339
228
|
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
229
|
+
1. Fork the repository
|
|
230
|
+
2. Create a feature branch (`git checkout -b feature/new-feature`)
|
|
231
|
+
3. Add tests for your changes
|
|
232
|
+
4. Ensure all tests pass (`ruby test.rb`)
|
|
233
|
+
5. Commit your changes (`git commit -am 'Add new feature'`)
|
|
234
|
+
6. Push to the branch (`git push origin feature/new-feature`)
|
|
235
|
+
7. Create a Pull Request
|
|
343
236
|
|
|
344
237
|
## License
|
|
345
238
|
|
|
@@ -347,4 +240,4 @@ Available as open source under the [MIT License](https://opensource.org/licenses
|
|
|
347
240
|
|
|
348
241
|
## About
|
|
349
242
|
|
|
350
|
-
Maintained by [Sashité](https://sashite.com/)
|
|
243
|
+
Maintained by [Sashité](https://sashite.com/) â€" promoting chess variants and sharing the beauty of board game cultures.
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Sashite
|
|
4
|
+
module Pnn
|
|
5
|
+
# Represents a piece name in PNN (Piece Name Notation) format.
|
|
6
|
+
#
|
|
7
|
+
# PNN provides a canonical naming system for abstract strategy game pieces.
|
|
8
|
+
# Each name consists of an optional state modifier (+ or -) followed by a
|
|
9
|
+
# case-consistent alphabetic name, encoding piece identity, player assignment,
|
|
10
|
+
# and state in a human-readable format.
|
|
11
|
+
#
|
|
12
|
+
# All instances are immutable.
|
|
13
|
+
class Name
|
|
14
|
+
# PNN validation pattern matching the specification
|
|
15
|
+
PNN_PATTERN = /\A([+-]?)([A-Z]+|[a-z]+)\z/
|
|
16
|
+
|
|
17
|
+
# Error messages
|
|
18
|
+
ERROR_INVALID_NAME = "Invalid PNN string: %s"
|
|
19
|
+
|
|
20
|
+
# @return [String] the canonical piece name
|
|
21
|
+
attr_reader :value
|
|
22
|
+
|
|
23
|
+
# Create a new piece name instance
|
|
24
|
+
#
|
|
25
|
+
# @param name [String, Symbol] the piece name (e.g., "KING", :queen, "+ROOK", "-pawn")
|
|
26
|
+
# @raise [ArgumentError] if the name does not match PNN pattern
|
|
27
|
+
def initialize(name)
|
|
28
|
+
string_value = name.to_s
|
|
29
|
+
self.class.validate_format(string_value)
|
|
30
|
+
|
|
31
|
+
@value = string_value.freeze
|
|
32
|
+
@parsed = parse_components(string_value)
|
|
33
|
+
freeze
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# Parse a PNN string into a Name object
|
|
37
|
+
#
|
|
38
|
+
# @param string [String] the PNN-formatted piece name
|
|
39
|
+
# @return [Name] a new Name instance
|
|
40
|
+
# @raise [ArgumentError] if the string is invalid
|
|
41
|
+
#
|
|
42
|
+
# @example
|
|
43
|
+
# Sashite::Pnn::Name.parse("KING") # => #<Pnn::Name value="KING">
|
|
44
|
+
# Sashite::Pnn::Name.parse("+queen") # => #<Pnn::Name value="+queen">
|
|
45
|
+
def self.parse(string)
|
|
46
|
+
new(string)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# Check whether the given string is a valid PNN name
|
|
50
|
+
#
|
|
51
|
+
# @param string [String] input string to validate
|
|
52
|
+
# @return [Boolean] true if valid, false otherwise
|
|
53
|
+
#
|
|
54
|
+
# @example
|
|
55
|
+
# Sashite::Pnn::Name.valid?("KING") # => true
|
|
56
|
+
# Sashite::Pnn::Name.valid?("King") # => false (mixed case)
|
|
57
|
+
# Sashite::Pnn::Name.valid?("+queen") # => true
|
|
58
|
+
# Sashite::Pnn::Name.valid?("KING1") # => false (contains digit)
|
|
59
|
+
def self.valid?(string)
|
|
60
|
+
string.is_a?(::String) && string.match?(PNN_PATTERN)
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# Validate that the string is in proper PNN format
|
|
64
|
+
#
|
|
65
|
+
# @param str [String]
|
|
66
|
+
# @raise [ArgumentError] if invalid
|
|
67
|
+
def self.validate_format(str)
|
|
68
|
+
raise ::ArgumentError, format(ERROR_INVALID_NAME, str.inspect) unless str.match?(PNN_PATTERN)
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# Returns the string representation of the name
|
|
72
|
+
#
|
|
73
|
+
# @return [String]
|
|
74
|
+
def to_s
|
|
75
|
+
value
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# Returns the base name without state modifier
|
|
79
|
+
#
|
|
80
|
+
# @return [String] the piece name without + or - prefix
|
|
81
|
+
#
|
|
82
|
+
# @example
|
|
83
|
+
# Sashite::Pnn::Name.parse("KING").base_name # => "KING"
|
|
84
|
+
# Sashite::Pnn::Name.parse("+queen").base_name # => "queen"
|
|
85
|
+
# Sashite::Pnn::Name.parse("-ROOK").base_name # => "ROOK"
|
|
86
|
+
def base_name
|
|
87
|
+
@parsed[:base_name]
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
# Check if the piece has enhanced state (+)
|
|
91
|
+
#
|
|
92
|
+
# @return [Boolean] true if enhanced, false otherwise
|
|
93
|
+
#
|
|
94
|
+
# @example
|
|
95
|
+
# Sashite::Pnn::Name.parse("+KING").enhanced? # => true
|
|
96
|
+
# Sashite::Pnn::Name.parse("KING").enhanced? # => false
|
|
97
|
+
def enhanced?
|
|
98
|
+
@parsed[:state_modifier] == "+"
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
# Check if the piece has diminished state (-)
|
|
102
|
+
#
|
|
103
|
+
# @return [Boolean] true if diminished, false otherwise
|
|
104
|
+
#
|
|
105
|
+
# @example
|
|
106
|
+
# Sashite::Pnn::Name.parse("-pawn").diminished? # => true
|
|
107
|
+
# Sashite::Pnn::Name.parse("pawn").diminished? # => false
|
|
108
|
+
def diminished?
|
|
109
|
+
@parsed[:state_modifier] == "-"
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
# Check if the piece has normal state (no modifier)
|
|
113
|
+
#
|
|
114
|
+
# @return [Boolean] true if normal, false otherwise
|
|
115
|
+
#
|
|
116
|
+
# @example
|
|
117
|
+
# Sashite::Pnn::Name.parse("KING").normal? # => true
|
|
118
|
+
# Sashite::Pnn::Name.parse("+KING").normal? # => false
|
|
119
|
+
def normal?
|
|
120
|
+
@parsed[:state_modifier].empty?
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
# Check if the piece belongs to the first player (uppercase)
|
|
124
|
+
#
|
|
125
|
+
# @return [Boolean] true if first player, false otherwise
|
|
126
|
+
#
|
|
127
|
+
# @example
|
|
128
|
+
# Sashite::Pnn::Name.parse("KING").first_player? # => true
|
|
129
|
+
# Sashite::Pnn::Name.parse("queen").first_player? # => false
|
|
130
|
+
def first_player?
|
|
131
|
+
@parsed[:base_name] == @parsed[:base_name].upcase
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
# Check if the piece belongs to the second player (lowercase)
|
|
135
|
+
#
|
|
136
|
+
# @return [Boolean] true if second player, false otherwise
|
|
137
|
+
#
|
|
138
|
+
# @example
|
|
139
|
+
# Sashite::Pnn::Name.parse("king").second_player? # => true
|
|
140
|
+
# Sashite::Pnn::Name.parse("QUEEN").second_player? # => false
|
|
141
|
+
def second_player?
|
|
142
|
+
@parsed[:base_name] == @parsed[:base_name].downcase
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
# Check if another piece has the same base name (ignoring case and state)
|
|
146
|
+
#
|
|
147
|
+
# @param other [Name] another piece name to compare
|
|
148
|
+
# @return [Boolean] true if same base name, false otherwise
|
|
149
|
+
#
|
|
150
|
+
# @example
|
|
151
|
+
# king = Sashite::Pnn::Name.parse("KING")
|
|
152
|
+
# queen = Sashite::Pnn::Name.parse("king")
|
|
153
|
+
# enhanced = Sashite::Pnn::Name.parse("+KING")
|
|
154
|
+
#
|
|
155
|
+
# king.same_base_name?(queen) # => true (same piece, different player)
|
|
156
|
+
# king.same_base_name?(enhanced) # => true (same piece, different state)
|
|
157
|
+
def same_base_name?(other)
|
|
158
|
+
return false unless other.is_a?(self.class)
|
|
159
|
+
|
|
160
|
+
base_name.downcase == other.base_name.downcase
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
# Equality based on normalized string value
|
|
164
|
+
#
|
|
165
|
+
# @param other [Object]
|
|
166
|
+
# @return [Boolean]
|
|
167
|
+
def ==(other)
|
|
168
|
+
other.is_a?(self.class) && value == other.value
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
# Required for correct Set/hash behavior
|
|
172
|
+
alias eql? ==
|
|
173
|
+
|
|
174
|
+
# Hash based on class and value
|
|
175
|
+
#
|
|
176
|
+
# @return [Integer]
|
|
177
|
+
def hash
|
|
178
|
+
[self.class, value].hash
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
private
|
|
182
|
+
|
|
183
|
+
# Parse the components of a PNN string
|
|
184
|
+
#
|
|
185
|
+
# @param str [String] the PNN string
|
|
186
|
+
# @return [Hash] parsed components
|
|
187
|
+
def parse_components(str)
|
|
188
|
+
match = str.match(PNN_PATTERN)
|
|
189
|
+
{
|
|
190
|
+
state_modifier: match[1],
|
|
191
|
+
base_name: match[2]
|
|
192
|
+
}
|
|
193
|
+
end
|
|
194
|
+
end
|
|
195
|
+
end
|
|
196
|
+
end
|
data/lib/sashite/pnn.rb
CHANGED
|
@@ -1,60 +1,66 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require_relative "pnn/
|
|
3
|
+
require_relative "pnn/name"
|
|
4
4
|
|
|
5
5
|
module Sashite
|
|
6
6
|
# PNN (Piece Name Notation) implementation for Ruby
|
|
7
7
|
#
|
|
8
|
-
#
|
|
9
|
-
# PNN
|
|
10
|
-
#
|
|
8
|
+
# Provides a formal naming system for identifying pieces in abstract strategy board games.
|
|
9
|
+
# PNN uses canonical, human-readable ASCII names with optional state modifiers and case
|
|
10
|
+
# encoding for player assignment. It supports unlimited unique piece identifiers with
|
|
11
|
+
# consistent, rule-agnostic semantics.
|
|
11
12
|
#
|
|
12
|
-
# Format: <
|
|
13
|
-
# - PIN component: [<state>]<letter> (state modifier + letter)
|
|
14
|
-
# - Suffix: "'" for foreign style, none for native style
|
|
13
|
+
# Format: [<state-modifier>]<case-consistent-name>
|
|
15
14
|
#
|
|
16
15
|
# Examples:
|
|
17
|
-
# "
|
|
18
|
-
# "
|
|
19
|
-
# "+
|
|
20
|
-
# "
|
|
21
|
-
# "
|
|
16
|
+
# "KING" - First player king (normal state)
|
|
17
|
+
# "queen" - Second player queen (normal state)
|
|
18
|
+
# "+ROOK" - First player rook (enhanced state)
|
|
19
|
+
# "-pawn" - Second player pawn (diminished state)
|
|
20
|
+
# "BISHOP" - First player bishop (normal state)
|
|
22
21
|
#
|
|
23
22
|
# See: https://sashite.dev/specs/pnn/1.0.0/
|
|
24
23
|
module Pnn
|
|
25
|
-
#
|
|
26
|
-
# Matches: optional state modifier, letter, optional derivation marker
|
|
27
|
-
PNN_REGEX = /\A[-+]?[A-Za-z]'?\z/
|
|
28
|
-
|
|
29
|
-
# Check if a string is a valid PNN notation
|
|
24
|
+
# Check if a string is valid PNN notation
|
|
30
25
|
#
|
|
31
|
-
# @param
|
|
26
|
+
# @param pnn_string [String] the string to validate
|
|
32
27
|
# @return [Boolean] true if valid PNN, false otherwise
|
|
33
28
|
#
|
|
34
|
-
# @example
|
|
35
|
-
# Sashite::Pnn.valid?("
|
|
36
|
-
# Sashite::Pnn.valid?("
|
|
37
|
-
# Sashite::Pnn.valid?("+
|
|
38
|
-
# Sashite::Pnn.valid?("-
|
|
39
|
-
# Sashite::Pnn.valid?("
|
|
40
|
-
# Sashite::Pnn.valid?("
|
|
41
|
-
def self.valid?(
|
|
42
|
-
|
|
29
|
+
# @example Validate PNN strings
|
|
30
|
+
# Sashite::Pnn.valid?("KING") # => true
|
|
31
|
+
# Sashite::Pnn.valid?("queen") # => true
|
|
32
|
+
# Sashite::Pnn.valid?("+ROOK") # => true
|
|
33
|
+
# Sashite::Pnn.valid?("-pawn") # => true
|
|
34
|
+
# Sashite::Pnn.valid?("King") # => false (mixed case)
|
|
35
|
+
# Sashite::Pnn.valid?("KING1") # => false (contains digit)
|
|
36
|
+
def self.valid?(pnn_string)
|
|
37
|
+
Name.valid?(pnn_string)
|
|
38
|
+
end
|
|
43
39
|
|
|
44
|
-
|
|
40
|
+
# Parse a PNN string into a Name object
|
|
41
|
+
#
|
|
42
|
+
# @param pnn_string [String] the piece name string
|
|
43
|
+
# @return [Pnn::Name] a parsed name object
|
|
44
|
+
# @raise [ArgumentError] if the name is invalid
|
|
45
|
+
#
|
|
46
|
+
# @example Parse valid PNN names
|
|
47
|
+
# Sashite::Pnn.parse("KING") # => #<Pnn::Name value="KING">
|
|
48
|
+
# Sashite::Pnn.parse("+queen") # => #<Pnn::Name value="+queen">
|
|
49
|
+
def self.parse(pnn_string)
|
|
50
|
+
Name.parse(pnn_string)
|
|
45
51
|
end
|
|
46
52
|
|
|
47
|
-
#
|
|
53
|
+
# Create a new Name instance directly
|
|
54
|
+
#
|
|
55
|
+
# @param value [String, Symbol] piece name to construct
|
|
56
|
+
# @return [Pnn::Name] new name instance
|
|
57
|
+
# @raise [ArgumentError] if name format is invalid
|
|
48
58
|
#
|
|
49
|
-
# @param pnn_string [String] PNN notation string
|
|
50
|
-
# @return [Pnn::Piece] new piece instance
|
|
51
|
-
# @raise [ArgumentError] if the PNN string is invalid
|
|
52
59
|
# @example
|
|
53
|
-
# Sashite::Pnn.
|
|
54
|
-
# Sashite::Pnn.
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
Piece.parse(pnn_string)
|
|
60
|
+
# Sashite::Pnn.name("BISHOP") # => #<Pnn::Name value="BISHOP">
|
|
61
|
+
# Sashite::Pnn.name(:queen) # => #<Pnn::Name value="queen">
|
|
62
|
+
def self.name(value)
|
|
63
|
+
Name.new(value)
|
|
58
64
|
end
|
|
59
65
|
end
|
|
60
66
|
end
|
data/lib/sashite-pnn.rb
CHANGED
|
@@ -1,21 +1,14 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require_relative "sashite/pnn"
|
|
4
|
+
|
|
3
5
|
# Sashité namespace for board game notation libraries
|
|
6
|
+
#
|
|
7
|
+
# Sashité provides a collection of libraries for representing and manipulating
|
|
8
|
+
# board game concepts according to the Sashité Protocol specifications.
|
|
9
|
+
#
|
|
10
|
+
# @see https://sashite.dev/protocol/ Sashité Protocol
|
|
11
|
+
# @see https://sashite.dev/specs/ Sashité Specifications
|
|
12
|
+
# @author Sashité
|
|
4
13
|
module Sashite
|
|
5
|
-
# Piece Name Notation (PNN) implementation for Ruby
|
|
6
|
-
#
|
|
7
|
-
# PNN extends PIN (Piece Identifier Notation) to provide style-aware piece
|
|
8
|
-
# representation in abstract strategy board games. PNN adds a derivation marker
|
|
9
|
-
# that distinguishes pieces by their style origin, enabling cross-style game
|
|
10
|
-
# scenarios and piece origin tracking.
|
|
11
|
-
#
|
|
12
|
-
# Format: <pin>[<suffix>]
|
|
13
|
-
# - PIN component: [<state>]<letter> (from PIN specification)
|
|
14
|
-
# - Suffix: "'" for foreign style, none for native style
|
|
15
|
-
#
|
|
16
|
-
# @see https://sashite.dev/specs/pnn/1.0.0/ PNN Specification v1.0.0
|
|
17
|
-
# @see https://sashite.dev/specs/pin/1.0.0/ PIN Specification v1.0.0
|
|
18
|
-
# @author Sashité
|
|
19
14
|
end
|
|
20
|
-
|
|
21
|
-
require_relative "sashite/pnn"
|
metadata
CHANGED
|
@@ -1,34 +1,21 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: sashite-pnn
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version:
|
|
4
|
+
version: 3.0.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Cyril Kato
|
|
8
8
|
bindir: bin
|
|
9
9
|
cert_chain: []
|
|
10
10
|
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
|
-
dependencies:
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
type: :runtime
|
|
20
|
-
prerelease: false
|
|
21
|
-
version_requirements: !ruby/object:Gem::Requirement
|
|
22
|
-
requirements:
|
|
23
|
-
- - "~>"
|
|
24
|
-
- !ruby/object:Gem::Version
|
|
25
|
-
version: 1.1.0
|
|
26
|
-
description: A clean, immutable Ruby interface for working with piece identifiers
|
|
27
|
-
in PNN format. PNN extends PIN notation to provide style-aware piece representation
|
|
28
|
-
with derivation markers for cross-style games. Features include all four fundamental
|
|
29
|
-
piece attributes (type, side, state, style), enabling hybrid game scenarios and
|
|
30
|
-
piece origin tracking. Perfect for game engines, analysis tools, and cross-tradition
|
|
31
|
-
board game applications.
|
|
11
|
+
dependencies: []
|
|
12
|
+
description: |
|
|
13
|
+
PNN (Piece Name Notation) provides a rule-agnostic, scalable naming system for identifying
|
|
14
|
+
abstract strategy board game pieces. This gem implements the PNN Specification v1.0.0 with
|
|
15
|
+
a modern Ruby interface featuring immutable piece name objects and functional programming
|
|
16
|
+
principles. PNN uses canonical ASCII names with optional state modifiers (e.g., "KING", "queen",
|
|
17
|
+
"+ROOK", "-pawn") to unambiguously refer to game pieces across variants and traditions.
|
|
18
|
+
Ideal for engines, protocols, and tools that need clear and extensible piece identifiers.
|
|
32
19
|
email: contact@cyril.email
|
|
33
20
|
executables: []
|
|
34
21
|
extensions: []
|
|
@@ -38,7 +25,7 @@ files:
|
|
|
38
25
|
- README.md
|
|
39
26
|
- lib/sashite-pnn.rb
|
|
40
27
|
- lib/sashite/pnn.rb
|
|
41
|
-
- lib/sashite/pnn/
|
|
28
|
+
- lib/sashite/pnn/name.rb
|
|
42
29
|
homepage: https://github.com/sashite/pnn.rb
|
|
43
30
|
licenses:
|
|
44
31
|
- MIT
|
|
@@ -63,8 +50,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
63
50
|
- !ruby/object:Gem::Version
|
|
64
51
|
version: '0'
|
|
65
52
|
requirements: []
|
|
66
|
-
rubygems_version: 3.
|
|
53
|
+
rubygems_version: 3.7.1
|
|
67
54
|
specification_version: 4
|
|
68
|
-
summary:
|
|
69
|
-
|
|
55
|
+
summary: PNN (Piece Name Notation) implementation for Ruby with immutable piece name
|
|
56
|
+
objects
|
|
70
57
|
test_files: []
|
data/lib/sashite/pnn/piece.rb
DELETED
|
@@ -1,234 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require "sashite/pin"
|
|
4
|
-
|
|
5
|
-
module Sashite
|
|
6
|
-
module Pnn
|
|
7
|
-
# Represents a piece in PNN (Piece Name Notation) format.
|
|
8
|
-
#
|
|
9
|
-
# Extends PIN::Piece to add style awareness through derivation markers.
|
|
10
|
-
# A PNN piece consists of a PIN string with an optional derivation suffix:
|
|
11
|
-
# - No suffix: native style
|
|
12
|
-
# - Apostrophe suffix ('): foreign style
|
|
13
|
-
#
|
|
14
|
-
# All instances are immutable - style manipulation methods return new instances.
|
|
15
|
-
class Piece < ::Sashite::Pin::Piece
|
|
16
|
-
# PNN validation pattern extending PIN
|
|
17
|
-
PNN_PATTERN = /\A(?<prefix>[-+])?(?<letter>[a-zA-Z])(?<suffix>'?)\z/
|
|
18
|
-
|
|
19
|
-
# Derivation marker for foreign style
|
|
20
|
-
FOREIGN_SUFFIX = "'"
|
|
21
|
-
|
|
22
|
-
# Error messages
|
|
23
|
-
ERROR_INVALID_PNN = "Invalid PNN string: %s"
|
|
24
|
-
ERROR_INVALID_NATIVE = "Native must be true or false: %s"
|
|
25
|
-
|
|
26
|
-
# @return [Boolean] whether the piece has native style
|
|
27
|
-
attr_reader :native
|
|
28
|
-
alias native? native
|
|
29
|
-
|
|
30
|
-
# Create a new piece instance with style information
|
|
31
|
-
#
|
|
32
|
-
# @param letter [String] single ASCII letter (a-z or A-Z)
|
|
33
|
-
# @param native [Boolean] whether the piece has native style
|
|
34
|
-
# @raise [ArgumentError] if parameters are invalid
|
|
35
|
-
def initialize(letter, native: true, **)
|
|
36
|
-
raise ::ArgumentError, format(ERROR_INVALID_NATIVE, native) unless [true, false].include?(native)
|
|
37
|
-
|
|
38
|
-
@native = native
|
|
39
|
-
super(letter, **)
|
|
40
|
-
end
|
|
41
|
-
|
|
42
|
-
# Parse a PNN string into a Piece object
|
|
43
|
-
#
|
|
44
|
-
# @param pnn_string [String] PNN notation string
|
|
45
|
-
# @return [Piece] new piece instance
|
|
46
|
-
# @raise [ArgumentError] if the PNN string is invalid
|
|
47
|
-
# @example
|
|
48
|
-
# Pnn::Piece.parse("k") # => #<Pnn::Piece letter="k" native=true>
|
|
49
|
-
# Pnn::Piece.parse("k'") # => #<Pnn::Piece letter="k" native=false>
|
|
50
|
-
# Pnn::Piece.parse("+R'") # => #<Pnn::Piece letter="R" enhanced=true native=false>
|
|
51
|
-
def self.parse(pnn_string)
|
|
52
|
-
string_value = String(pnn_string)
|
|
53
|
-
matches = match_pattern(string_value)
|
|
54
|
-
|
|
55
|
-
letter = matches[:letter]
|
|
56
|
-
enhanced = matches[:prefix] == ENHANCED_PREFIX
|
|
57
|
-
diminished = matches[:prefix] == DIMINISHED_PREFIX
|
|
58
|
-
native = matches[:suffix] != FOREIGN_SUFFIX
|
|
59
|
-
|
|
60
|
-
new(
|
|
61
|
-
letter,
|
|
62
|
-
native: native,
|
|
63
|
-
enhanced: enhanced,
|
|
64
|
-
diminished: diminished
|
|
65
|
-
)
|
|
66
|
-
end
|
|
67
|
-
|
|
68
|
-
# Convert the piece to its PNN string representation
|
|
69
|
-
#
|
|
70
|
-
# @return [String] PNN notation string
|
|
71
|
-
# @example
|
|
72
|
-
# piece.to_s # => "k"
|
|
73
|
-
# piece.to_s # => "k'"
|
|
74
|
-
# piece.to_s # => "+R'"
|
|
75
|
-
def to_s
|
|
76
|
-
pin_string = super
|
|
77
|
-
suffix = native? ? "" : FOREIGN_SUFFIX
|
|
78
|
-
"#{pin_string}#{suffix}"
|
|
79
|
-
end
|
|
80
|
-
|
|
81
|
-
# Convert the piece to its underlying PIN representation
|
|
82
|
-
#
|
|
83
|
-
# @return [String] PIN notation string without style information
|
|
84
|
-
def to_pin
|
|
85
|
-
nativize.to_s
|
|
86
|
-
end
|
|
87
|
-
|
|
88
|
-
# Check if the piece has foreign style
|
|
89
|
-
#
|
|
90
|
-
# @return [Boolean] true if foreign style
|
|
91
|
-
def foreign?
|
|
92
|
-
!native?
|
|
93
|
-
end
|
|
94
|
-
|
|
95
|
-
# Create a new piece with native style
|
|
96
|
-
#
|
|
97
|
-
# @return [Piece] new piece instance with native style
|
|
98
|
-
# @example
|
|
99
|
-
# piece.nativize # k' => k
|
|
100
|
-
def nativize
|
|
101
|
-
return self if native?
|
|
102
|
-
|
|
103
|
-
self.class.new(
|
|
104
|
-
letter,
|
|
105
|
-
native: true,
|
|
106
|
-
enhanced: enhanced?,
|
|
107
|
-
diminished: diminished?
|
|
108
|
-
)
|
|
109
|
-
end
|
|
110
|
-
|
|
111
|
-
# Create a new piece with foreign style
|
|
112
|
-
#
|
|
113
|
-
# @return [Piece] new piece instance with foreign style
|
|
114
|
-
# @example
|
|
115
|
-
# piece.foreignize # k => k'
|
|
116
|
-
def foreignize
|
|
117
|
-
return self if foreign?
|
|
118
|
-
|
|
119
|
-
self.class.new(
|
|
120
|
-
letter,
|
|
121
|
-
native: false,
|
|
122
|
-
enhanced: enhanced?,
|
|
123
|
-
diminished: diminished?
|
|
124
|
-
)
|
|
125
|
-
end
|
|
126
|
-
|
|
127
|
-
# Create a new piece with toggled style
|
|
128
|
-
#
|
|
129
|
-
# @return [Piece] new piece instance with opposite style
|
|
130
|
-
# @example
|
|
131
|
-
# piece.toggle_style # k => k', k' => k
|
|
132
|
-
def toggle_style
|
|
133
|
-
native? ? foreignize : nativize
|
|
134
|
-
end
|
|
135
|
-
|
|
136
|
-
# Override parent methods to maintain PNN type in return values
|
|
137
|
-
|
|
138
|
-
def enhance
|
|
139
|
-
return self if enhanced?
|
|
140
|
-
|
|
141
|
-
self.class.new(
|
|
142
|
-
letter,
|
|
143
|
-
native: native?,
|
|
144
|
-
enhanced: true,
|
|
145
|
-
diminished: false
|
|
146
|
-
)
|
|
147
|
-
end
|
|
148
|
-
|
|
149
|
-
def unenhance
|
|
150
|
-
return self unless enhanced?
|
|
151
|
-
|
|
152
|
-
self.class.new(
|
|
153
|
-
letter,
|
|
154
|
-
native: native?,
|
|
155
|
-
enhanced: false,
|
|
156
|
-
diminished: diminished?
|
|
157
|
-
)
|
|
158
|
-
end
|
|
159
|
-
|
|
160
|
-
def diminish
|
|
161
|
-
return self if diminished?
|
|
162
|
-
|
|
163
|
-
self.class.new(
|
|
164
|
-
letter,
|
|
165
|
-
native: native?,
|
|
166
|
-
enhanced: false,
|
|
167
|
-
diminished: true
|
|
168
|
-
)
|
|
169
|
-
end
|
|
170
|
-
|
|
171
|
-
def undiminish
|
|
172
|
-
return self unless diminished?
|
|
173
|
-
|
|
174
|
-
self.class.new(
|
|
175
|
-
letter,
|
|
176
|
-
native: native?,
|
|
177
|
-
enhanced: enhanced?,
|
|
178
|
-
diminished: false
|
|
179
|
-
)
|
|
180
|
-
end
|
|
181
|
-
|
|
182
|
-
def normalize
|
|
183
|
-
return self if normal?
|
|
184
|
-
|
|
185
|
-
self.class.new(
|
|
186
|
-
letter,
|
|
187
|
-
native: native?
|
|
188
|
-
)
|
|
189
|
-
end
|
|
190
|
-
|
|
191
|
-
def flip
|
|
192
|
-
flipped_letter = letter.swapcase
|
|
193
|
-
|
|
194
|
-
self.class.new(
|
|
195
|
-
flipped_letter,
|
|
196
|
-
native: native?,
|
|
197
|
-
enhanced: enhanced?,
|
|
198
|
-
diminished: diminished?
|
|
199
|
-
)
|
|
200
|
-
end
|
|
201
|
-
|
|
202
|
-
# Custom equality comparison including style
|
|
203
|
-
#
|
|
204
|
-
# @param other [Object] object to compare with
|
|
205
|
-
# @return [Boolean] true if pieces are equal
|
|
206
|
-
def ==(other)
|
|
207
|
-
return false unless other.is_a?(self.class)
|
|
208
|
-
|
|
209
|
-
super && native? == other.native?
|
|
210
|
-
end
|
|
211
|
-
|
|
212
|
-
# Custom hash implementation including style
|
|
213
|
-
#
|
|
214
|
-
# @return [Integer] hash value
|
|
215
|
-
def hash
|
|
216
|
-
[super, @native].hash
|
|
217
|
-
end
|
|
218
|
-
|
|
219
|
-
# Match PNN pattern against string
|
|
220
|
-
#
|
|
221
|
-
# @param string [String] string to match
|
|
222
|
-
# @return [MatchData] match data
|
|
223
|
-
# @raise [ArgumentError] if string doesn't match
|
|
224
|
-
def self.match_pattern(string)
|
|
225
|
-
matches = PNN_PATTERN.match(string)
|
|
226
|
-
return matches if matches
|
|
227
|
-
|
|
228
|
-
raise ::ArgumentError, format(ERROR_INVALID_PNN, string)
|
|
229
|
-
end
|
|
230
|
-
|
|
231
|
-
private_class_method :match_pattern
|
|
232
|
-
end
|
|
233
|
-
end
|
|
234
|
-
end
|