pnn 1.0.1 → 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 +251 -70
- data/lib/pnn/piece.rb +325 -0
- data/lib/pnn.rb +51 -54
- metadata +10 -9
- data/lib/pnn/dumper.rb +0 -35
- data/lib/pnn/parser.rb +0 -38
- data/lib/pnn/validator.rb +0 -17
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: '0584c1ee7a42c3f556efe5e27e106c07b7e12c37148ba345238ef9fb02622c76'
|
4
|
+
data.tar.gz: b31fcbfb3442f0b130662a0cbd7e644c127b3c8a9247f3380f85056aa0be6ea1
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 53b6236d8342e095d69a9779ec01b1f87fc992b2927ae29689930cb5dac3d9a36fcf7efce7004c468cf4d4221e59b563167d637d8d24e7afdf3c5501d88818d3
|
7
|
+
data.tar.gz: 2f3832ea59e253bf8e5abd69dfaa39413c272e118756906d86fc89f91774b77d57b8bc991d2e4288b44b817df83039e740b928b1bcec7d4ac5499db12bf7b380
|
data/README.md
CHANGED
@@ -11,10 +11,7 @@
|
|
11
11
|
|
12
12
|
PNN (Piece Name Notation) is a consistent and rule-agnostic format for representing pieces in abstract strategy board games. It defines a standardized way to identify and represent pieces independent of any specific game rules or mechanics.
|
13
13
|
|
14
|
-
This gem implements the [PNN Specification v1.0.0](https://sashite.dev/documents/pnn/1.0.0/), providing a Ruby interface for
|
15
|
-
- Serializing piece identifiers to PNN strings
|
16
|
-
- Parsing PNN strings into their component parts
|
17
|
-
- Validating PNN strings according to the specification
|
14
|
+
This gem implements the [PNN Specification v1.0.0](https://sashite.dev/documents/pnn/1.0.0/), providing a Ruby interface for working with piece representations through an intuitive object-oriented API.
|
18
15
|
|
19
16
|
## Installation
|
20
17
|
|
@@ -39,121 +36,305 @@ A PNN record consists of a single ASCII letter that represents a piece, with opt
|
|
39
36
|
|
40
37
|
Where:
|
41
38
|
- `<letter>` is a single ASCII letter (`a-z` or `A-Z`), with uppercase representing the first player's pieces and lowercase representing the second player's pieces
|
42
|
-
- `<prefix>` is an optional modifier preceding the letter (`+` or `-`)
|
43
|
-
- `<suffix>` is an optional modifier following the letter (
|
39
|
+
- `<prefix>` is an optional modifier preceding the letter (`+` for Enhanced or `-` for Diminished state)
|
40
|
+
- `<suffix>` is an optional modifier following the letter (`'` for Intermediate state)
|
44
41
|
|
45
42
|
## Basic Usage
|
46
43
|
|
47
|
-
###
|
44
|
+
### Creating Piece Objects
|
48
45
|
|
49
|
-
|
46
|
+
The primary interface is the `Pnn::Piece` class, which represents a single piece in PNN format:
|
50
47
|
|
51
48
|
```ruby
|
52
49
|
require "pnn"
|
53
50
|
|
54
|
-
#
|
55
|
-
|
56
|
-
# =>
|
51
|
+
# Parse a PNN string into a piece object
|
52
|
+
piece = Pnn::Piece.parse("k")
|
53
|
+
# => #<Pnn::Piece:0x... @letter="k">
|
57
54
|
|
58
|
-
# With
|
59
|
-
|
60
|
-
# =>
|
55
|
+
# With modifiers
|
56
|
+
enhanced_piece = Pnn::Piece.parse("+k'")
|
57
|
+
# => #<Pnn::Piece:0x... @letter="k", @enhanced=true, @intermediate=true>
|
61
58
|
|
62
|
-
#
|
63
|
-
|
64
|
-
|
59
|
+
# Create directly with constructor
|
60
|
+
piece = Pnn::Piece.new("k")
|
61
|
+
enhanced_piece = Pnn::Piece.new("k", enhanced: true, intermediate: true)
|
65
62
|
|
66
|
-
#
|
67
|
-
|
68
|
-
# => { letter: "k", prefix: "+", suffix: "=" }
|
63
|
+
# Convenience method
|
64
|
+
piece = Pnn.piece("k", enhanced: true)
|
69
65
|
```
|
70
66
|
|
71
|
-
###
|
67
|
+
### Converting to PNN String
|
72
68
|
|
73
|
-
|
69
|
+
Convert a piece object back to its PNN string representation:
|
74
70
|
|
75
71
|
```ruby
|
76
|
-
|
72
|
+
piece = Pnn::Piece.parse("k")
|
73
|
+
piece.to_s
|
74
|
+
# => "k"
|
75
|
+
|
76
|
+
enhanced_piece = Pnn::Piece.parse("+k'")
|
77
|
+
enhanced_piece.to_s
|
78
|
+
# => "+k'"
|
79
|
+
```
|
80
|
+
|
81
|
+
### State Manipulation
|
82
|
+
|
83
|
+
Create new piece instances with different states:
|
84
|
+
|
85
|
+
```ruby
|
86
|
+
piece = Pnn::Piece.parse("k")
|
87
|
+
|
88
|
+
# Enhanced state (+ prefix)
|
89
|
+
enhanced = piece.enhance
|
90
|
+
enhanced.to_s # => "+k"
|
91
|
+
|
92
|
+
# Diminished state (- prefix)
|
93
|
+
diminished = piece.diminish
|
94
|
+
diminished.to_s # => "-k"
|
95
|
+
|
96
|
+
# Intermediate state (' suffix)
|
97
|
+
intermediate = piece.intermediate
|
98
|
+
intermediate.to_s # => "k'"
|
77
99
|
|
78
|
-
#
|
79
|
-
|
80
|
-
# =>
|
100
|
+
# Remove states
|
101
|
+
restored = enhanced.unenhance
|
102
|
+
restored.to_s # => "k"
|
81
103
|
|
82
|
-
#
|
83
|
-
|
84
|
-
# =>
|
104
|
+
# Combine states
|
105
|
+
complex = piece.enhance.intermediate
|
106
|
+
complex.to_s # => "+k'"
|
85
107
|
```
|
86
108
|
|
87
|
-
###
|
109
|
+
### Ownership Changes
|
88
110
|
|
89
|
-
|
111
|
+
Change piece ownership (case conversion):
|
90
112
|
|
91
113
|
```ruby
|
92
|
-
|
114
|
+
white_king = Pnn::Piece.parse("K")
|
115
|
+
black_king = white_king.flip
|
116
|
+
black_king.to_s # => "k"
|
117
|
+
|
118
|
+
# Works with modifiers too
|
119
|
+
enhanced_white = Pnn::Piece.parse("+K'")
|
120
|
+
enhanced_black = enhanced_white.flip
|
121
|
+
enhanced_black.to_s # => "+k'"
|
122
|
+
```
|
93
123
|
|
94
|
-
|
95
|
-
|
96
|
-
|
124
|
+
### Clean State
|
125
|
+
|
126
|
+
Get a piece without any modifiers:
|
127
|
+
|
128
|
+
```ruby
|
129
|
+
complex_piece = Pnn::Piece.parse("+k'")
|
130
|
+
clean_piece = complex_piece.bare
|
131
|
+
clean_piece.to_s # => "k"
|
132
|
+
```
|
133
|
+
|
134
|
+
## State Modifier Methods
|
135
|
+
|
136
|
+
The `Pnn::Piece` class provides methods to manipulate piece states:
|
137
|
+
|
138
|
+
| Method | Description | Example |
|
139
|
+
|--------|-------------|---------|
|
140
|
+
| `enhance` | Add Enhanced state (`+` prefix) | `k` → `+k` |
|
141
|
+
| `unenhance` | Remove Enhanced state | `+k` → `k` |
|
142
|
+
| `diminish` | Add Diminished state (`-` prefix) | `k` → `-k` |
|
143
|
+
| `undiminish` | Remove Diminished state | `-k` → `k` |
|
144
|
+
| `intermediate` | Add Intermediate state (`'` suffix) | `k` → `k'` |
|
145
|
+
| `unintermediate` | Remove Intermediate state | `k'` → `k` |
|
146
|
+
| `bare` | Remove all modifiers | `+k'` → `k` |
|
147
|
+
| `flip` | Change ownership (case) | `K` → `k`, `k` → `K` |
|
148
|
+
|
149
|
+
All state manipulation methods return new `Pnn::Piece` instances, leaving the original unchanged (immutable design).
|
150
|
+
|
151
|
+
## Piece Modifiers
|
152
|
+
|
153
|
+
PNN supports prefixes and suffixes for pieces to denote various states or capabilities. It's important to note that these modifiers are rule-agnostic - they provide a framework for representing piece states, but their specific meaning is determined by the game implementation:
|
154
|
+
|
155
|
+
- **Enhanced state (`+`)**: Represents pieces with enhanced capabilities
|
156
|
+
- Example in shogi: `+p` represents a promoted pawn (tokin)
|
157
|
+
- Example in chess variants: `+Q` might represent a queen with special powers
|
158
|
+
|
159
|
+
- **Diminished state (`-`)**: Represents pieces with reduced capabilities
|
160
|
+
- Example in variants: `-R` might represent a rook with restricted movement
|
161
|
+
- Example in chess: `-N` could indicate a knight that has been partially immobilized
|
162
|
+
|
163
|
+
- **Intermediate state (`'`)**: Represents pieces with special temporary states
|
164
|
+
- Example in chess: `R'` represents a rook that can still be used for castling
|
165
|
+
- Example in chess: `P'` represents a pawn that can be captured en passant
|
166
|
+
- Example in variants: `B'` might indicate a bishop with a special one-time ability
|
97
167
|
|
98
|
-
|
99
|
-
Pnn.dump(letter: "p", prefix: "+")
|
100
|
-
# => "+p"
|
168
|
+
These modifiers have no intrinsic semantics in the PNN specification itself. They merely provide a flexible framework for representing piece-specific conditions or states while maintaining PNN's rule-agnostic nature.
|
101
169
|
|
102
|
-
|
103
|
-
Pnn.dump(letter: "k", suffix: "=")
|
104
|
-
# => "k="
|
170
|
+
## Examples of PNN in Common Games
|
105
171
|
|
106
|
-
|
107
|
-
|
108
|
-
|
172
|
+
### Chess Examples
|
173
|
+
|
174
|
+
```ruby
|
175
|
+
# Standard pieces
|
176
|
+
king = Pnn::Piece.parse("K") # White king
|
177
|
+
black_king = Pnn::Piece.parse("k") # Black king
|
178
|
+
queen = Pnn::Piece.parse("Q") # White queen
|
179
|
+
|
180
|
+
# Create pieces directly
|
181
|
+
king = Pnn::Piece.new("K") # White king
|
182
|
+
black_king = Pnn::Piece.new("k") # Black king
|
183
|
+
|
184
|
+
# Pieces with special states
|
185
|
+
unmoved_rook = Pnn::Piece.parse("R'") # Rook that can castle
|
186
|
+
en_passant_pawn = Pnn::Piece.parse("P'") # Pawn vulnerable to en passant
|
187
|
+
|
188
|
+
# Creating modified pieces
|
189
|
+
promoted_pawn = Pnn::Piece.parse("p").enhance # "+p"
|
190
|
+
weakened_queen = Pnn::Piece.parse("Q").diminish # "-Q"
|
191
|
+
|
192
|
+
# Or create directly with modifiers
|
193
|
+
promoted_pawn = Pnn::Piece.new("p", enhanced: true) # "+p"
|
194
|
+
weakened_queen = Pnn::Piece.new("Q", diminished: true) # "-Q"
|
195
|
+
|
196
|
+
# Using convenience method
|
197
|
+
special_knight = Pnn.piece("N", intermediate: true) # "N'"
|
198
|
+
```
|
199
|
+
|
200
|
+
### Shogi Examples
|
201
|
+
|
202
|
+
```ruby
|
203
|
+
# Standard pieces
|
204
|
+
king = Pnn::Piece.parse("K") # Oushou (King)
|
205
|
+
pawn = Pnn::Piece.parse("P") # Fuhyou (Pawn)
|
206
|
+
|
207
|
+
# Create directly
|
208
|
+
king = Pnn::Piece.new("K") # Oushou (King)
|
209
|
+
pawn = Pnn::Piece.new("P") # Fuhyou (Pawn)
|
210
|
+
|
211
|
+
# Promoted pieces
|
212
|
+
tokin = Pnn::Piece.parse("P").enhance # "+P" (Promoted pawn)
|
213
|
+
narikyou = Pnn::Piece.parse("L").enhance # "+L" (Promoted lance)
|
214
|
+
|
215
|
+
# Or create promoted pieces directly
|
216
|
+
tokin = Pnn::Piece.new("P", enhanced: true) # "+P"
|
217
|
+
narikyou = Pnn::Piece.new("L", enhanced: true) # "+L"
|
218
|
+
|
219
|
+
# Using convenience method
|
220
|
+
promoted_silver = Pnn.piece("S", enhanced: true) # "+S"
|
221
|
+
|
222
|
+
# Converting between players (capture and drop)
|
223
|
+
enemy_piece = Pnn::Piece.parse("p")
|
224
|
+
captured_piece = enemy_piece.flip.bare # "P" (now belongs to other player, no modifiers)
|
225
|
+
```
|
226
|
+
|
227
|
+
## Advanced Usage
|
228
|
+
|
229
|
+
### Chaining State Changes
|
230
|
+
|
231
|
+
```ruby
|
232
|
+
piece = Pnn::Piece.parse("k")
|
233
|
+
|
234
|
+
# Chain multiple state changes
|
235
|
+
complex_piece = piece.enhance.intermediate.flip
|
236
|
+
complex_piece.to_s # => "+K'"
|
237
|
+
|
238
|
+
# Reverse the changes
|
239
|
+
simple_piece = complex_piece.unenhance.unintermediate.flip
|
240
|
+
simple_piece.to_s # => "k"
|
109
241
|
```
|
110
242
|
|
111
243
|
### Validation
|
112
244
|
|
113
|
-
|
245
|
+
All parsing automatically validates input according to the PNN specification:
|
114
246
|
|
115
247
|
```ruby
|
116
|
-
|
248
|
+
# Valid PNN strings
|
249
|
+
Pnn::Piece.parse("k") # ✓
|
250
|
+
Pnn::Piece.parse("+p") # ✓
|
251
|
+
Pnn::Piece.parse("K'") # ✓
|
252
|
+
Pnn::Piece.parse("+p'") # ✓
|
253
|
+
|
254
|
+
# Valid constructor calls
|
255
|
+
Pnn::Piece.new("k") # ✓
|
256
|
+
Pnn::Piece.new("p", enhanced: true) # ✓
|
257
|
+
Pnn::Piece.new("K", intermediate: true) # ✓
|
258
|
+
Pnn::Piece.new("p", enhanced: true, intermediate: true) # ✓
|
259
|
+
|
260
|
+
# Convenience method
|
261
|
+
Pnn.piece("k", enhanced: true) # ✓
|
262
|
+
|
263
|
+
# Check validity
|
264
|
+
Pnn.valid?("k'") # => true
|
265
|
+
Pnn.valid?("invalid") # => false
|
266
|
+
|
267
|
+
# Invalid PNN strings raise ArgumentError
|
268
|
+
Pnn::Piece.parse("") # ✗ ArgumentError
|
269
|
+
Pnn::Piece.parse("kp") # ✗ ArgumentError
|
270
|
+
Pnn::Piece.parse("++k") # ✗ ArgumentError
|
271
|
+
Pnn::Piece.parse("k''") # ✗ ArgumentError
|
272
|
+
|
273
|
+
# Invalid constructor calls raise ArgumentError
|
274
|
+
Pnn::Piece.new("") # ✗ ArgumentError
|
275
|
+
Pnn::Piece.new("kp") # ✗ ArgumentError
|
276
|
+
Pnn::Piece.new("k", enhanced: true, diminished: true) # ✗ ArgumentError
|
277
|
+
```
|
117
278
|
|
118
|
-
|
119
|
-
Pnn.valid?("+p") # => true
|
120
|
-
Pnn.valid?("k=") # => true
|
121
|
-
Pnn.valid?("+p>") # => true
|
279
|
+
### Inspection and Debugging
|
122
280
|
|
123
|
-
|
124
|
-
Pnn.
|
125
|
-
|
126
|
-
|
281
|
+
```ruby
|
282
|
+
piece = Pnn::Piece.parse("+k'")
|
283
|
+
|
284
|
+
# Get detailed information
|
285
|
+
piece.inspect
|
286
|
+
# => "#<Pnn::Piece:0x... letter='k' enhanced=true intermediate=true>"
|
287
|
+
|
288
|
+
# Check individual states
|
289
|
+
piece.enhanced? # => true
|
290
|
+
piece.diminished? # => false
|
291
|
+
piece.intermediate? # => true
|
292
|
+
piece.uppercase? # => false (it's lowercase 'k')
|
293
|
+
piece.lowercase? # => true
|
127
294
|
```
|
128
295
|
|
129
|
-
|
296
|
+
## API Reference
|
297
|
+
|
298
|
+
### Module Methods
|
130
299
|
|
131
|
-
|
300
|
+
- `Pnn.valid?(pnn_string)` - Check if a string is valid PNN notation
|
301
|
+
- `Pnn.piece(letter, **options)` - Convenience method to create pieces
|
132
302
|
|
133
|
-
|
134
|
-
- Example in shogi: `+p` may represent a promoted pawn
|
303
|
+
### Pnn::Piece Class Methods
|
135
304
|
|
136
|
-
-
|
137
|
-
|
305
|
+
- `Pnn::Piece.parse(pnn_string)` - Parse a PNN string into a piece object
|
306
|
+
- `Pnn::Piece.new(letter, **options)` - Create a new piece instance
|
138
307
|
|
139
|
-
|
140
|
-
- Example in chess: `k=` may represent a king eligible for both kingside and queenside castling
|
308
|
+
### Instance Methods
|
141
309
|
|
142
|
-
|
143
|
-
|
144
|
-
|
310
|
+
#### State Queries
|
311
|
+
- `#enhanced?` - Check if piece has enhanced state
|
312
|
+
- `#diminished?` - Check if piece has diminished state
|
313
|
+
- `#intermediate?` - Check if piece has intermediate state
|
314
|
+
- `#bare?` - Check if piece has no modifiers
|
315
|
+
- `#uppercase?` - Check if piece belongs to first player
|
316
|
+
- `#lowercase?` - Check if piece belongs to second player
|
145
317
|
|
146
|
-
|
147
|
-
|
148
|
-
|
318
|
+
#### State Manipulation
|
319
|
+
- `#enhance` - Add enhanced state
|
320
|
+
- `#unenhance` - Remove enhanced state
|
321
|
+
- `#diminish` - Add diminished state
|
322
|
+
- `#undiminish` - Remove diminished state
|
323
|
+
- `#intermediate` - Add intermediate state
|
324
|
+
- `#unintermediate` - Remove intermediate state
|
325
|
+
- `#bare` - Remove all modifiers
|
326
|
+
- `#flip` - Change ownership (case)
|
149
327
|
|
150
|
-
|
328
|
+
#### Conversion
|
329
|
+
- `#to_s` - Convert to PNN string representation
|
330
|
+
- `#inspect` - Detailed string representation for debugging
|
151
331
|
|
152
332
|
## Properties of PNN
|
153
333
|
|
154
334
|
* **Rule-agnostic**: PNN does not encode legality, validity, or game-specific conditions.
|
155
335
|
* **Canonical representation**: Ensures that equivalent pieces yield identical strings.
|
156
336
|
* **State modifiers**: Express special conditions without compromising rule neutrality.
|
337
|
+
* **Immutable objects**: All state changes return new instances, ensuring thread safety.
|
157
338
|
|
158
339
|
## Constraints
|
159
340
|
|
@@ -173,4 +354,4 @@ The [gem](https://rubygems.org/gems/pnn) is available as open source under the t
|
|
173
354
|
|
174
355
|
## About Sashité
|
175
356
|
|
176
|
-
This project is maintained by [Sashité](https://sashite.com/)
|
357
|
+
This project is maintained by [Sashité](https://sashite.com/) — promoting chess variants and sharing the beauty of Chinese, Japanese, and Western chess cultures.
|
data/lib/pnn/piece.rb
ADDED
@@ -0,0 +1,325 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Pnn
|
4
|
+
# Represents a piece in PNN (Piece Name Notation) format.
|
5
|
+
#
|
6
|
+
# A piece consists of a single ASCII letter with optional state modifiers:
|
7
|
+
# - Enhanced state: prefix '+'
|
8
|
+
# - Diminished state: prefix '-'
|
9
|
+
# - Intermediate state: suffix "'"
|
10
|
+
#
|
11
|
+
# The case of the letter determines ownership:
|
12
|
+
# - Uppercase (A-Z): first player
|
13
|
+
# - Lowercase (a-z): second player
|
14
|
+
#
|
15
|
+
# All instances are immutable - state manipulation methods return new instances.
|
16
|
+
class Piece
|
17
|
+
# PNN validation pattern matching the specification
|
18
|
+
PNN_PATTERN = /\A(?<prefix>[-+])?(?<letter>[a-zA-Z])(?<suffix>['])?\z/
|
19
|
+
|
20
|
+
# Valid state modifiers
|
21
|
+
ENHANCED_PREFIX = "+"
|
22
|
+
DIMINISHED_PREFIX = "-"
|
23
|
+
INTERMEDIATE_SUFFIX = "'"
|
24
|
+
|
25
|
+
# Error messages
|
26
|
+
ERROR_INVALID_PNN = "Invalid PNN string: %s"
|
27
|
+
ERROR_INVALID_LETTER = "Letter must be a single ASCII letter (a-z or A-Z): %s"
|
28
|
+
|
29
|
+
# @return [String] the base letter identifier
|
30
|
+
attr_reader :letter
|
31
|
+
|
32
|
+
# Create a new piece instance
|
33
|
+
#
|
34
|
+
# @param letter [String] single ASCII letter (a-z or A-Z)
|
35
|
+
# @param enhanced [Boolean] whether the piece has enhanced state
|
36
|
+
# @param diminished [Boolean] whether the piece has diminished state
|
37
|
+
# @param intermediate [Boolean] whether the piece has intermediate state
|
38
|
+
# @raise [ArgumentError] if parameters are invalid
|
39
|
+
def initialize(letter, enhanced: false, diminished: false, intermediate: false)
|
40
|
+
self.class.validate_letter(letter)
|
41
|
+
self.class.validate_state_combination(enhanced, diminished)
|
42
|
+
|
43
|
+
@letter = letter.freeze
|
44
|
+
@enhanced = enhanced
|
45
|
+
@diminished = diminished
|
46
|
+
@intermediate = intermediate
|
47
|
+
|
48
|
+
freeze
|
49
|
+
end
|
50
|
+
|
51
|
+
# Parse a PNN string into a Piece object
|
52
|
+
#
|
53
|
+
# @param pnn_string [String] PNN notation string
|
54
|
+
# @return [Piece] new piece instance
|
55
|
+
# @raise [ArgumentError] if the PNN string is invalid
|
56
|
+
# @example
|
57
|
+
# Pnn::Piece.parse("k") # => #<Pnn::Piece letter="k">
|
58
|
+
# Pnn::Piece.parse("+k'") # => #<Pnn::Piece letter="k" enhanced=true intermediate=true>
|
59
|
+
def self.parse(pnn_string)
|
60
|
+
string_value = String(pnn_string)
|
61
|
+
matches = match_pattern(string_value)
|
62
|
+
|
63
|
+
letter = matches[:letter]
|
64
|
+
enhanced = matches[:prefix] == ENHANCED_PREFIX
|
65
|
+
diminished = matches[:prefix] == DIMINISHED_PREFIX
|
66
|
+
intermediate = matches[:suffix] == INTERMEDIATE_SUFFIX
|
67
|
+
|
68
|
+
new(
|
69
|
+
letter,
|
70
|
+
enhanced: enhanced,
|
71
|
+
diminished: diminished,
|
72
|
+
intermediate: intermediate
|
73
|
+
)
|
74
|
+
end
|
75
|
+
|
76
|
+
# Convert the piece to its PNN string representation
|
77
|
+
#
|
78
|
+
# @return [String] PNN notation string
|
79
|
+
# @example
|
80
|
+
# piece.to_s # => "+k'"
|
81
|
+
def to_s
|
82
|
+
prefix = if @enhanced
|
83
|
+
ENHANCED_PREFIX
|
84
|
+
else
|
85
|
+
(@diminished ? DIMINISHED_PREFIX : "")
|
86
|
+
end
|
87
|
+
suffix = @intermediate ? INTERMEDIATE_SUFFIX : ""
|
88
|
+
"#{prefix}#{letter}#{suffix}"
|
89
|
+
end
|
90
|
+
|
91
|
+
# Create a new piece with enhanced state
|
92
|
+
#
|
93
|
+
# @return [Piece] new piece instance with enhanced state
|
94
|
+
# @example
|
95
|
+
# piece.enhance # k => +k
|
96
|
+
def enhance
|
97
|
+
return self if enhanced?
|
98
|
+
|
99
|
+
self.class.new(
|
100
|
+
letter,
|
101
|
+
enhanced: true,
|
102
|
+
diminished: false,
|
103
|
+
intermediate: @intermediate
|
104
|
+
)
|
105
|
+
end
|
106
|
+
|
107
|
+
# Create a new piece without enhanced state
|
108
|
+
#
|
109
|
+
# @return [Piece] new piece instance without enhanced state
|
110
|
+
# @example
|
111
|
+
# piece.unenhance # +k => k
|
112
|
+
def unenhance
|
113
|
+
return self unless enhanced?
|
114
|
+
|
115
|
+
self.class.new(
|
116
|
+
letter,
|
117
|
+
enhanced: false,
|
118
|
+
diminished: @diminished,
|
119
|
+
intermediate: @intermediate
|
120
|
+
)
|
121
|
+
end
|
122
|
+
|
123
|
+
# Create a new piece with diminished state
|
124
|
+
#
|
125
|
+
# @return [Piece] new piece instance with diminished state
|
126
|
+
# @example
|
127
|
+
# piece.diminish # k => -k
|
128
|
+
def diminish
|
129
|
+
return self if diminished?
|
130
|
+
|
131
|
+
self.class.new(
|
132
|
+
letter,
|
133
|
+
enhanced: false,
|
134
|
+
diminished: true,
|
135
|
+
intermediate: @intermediate
|
136
|
+
)
|
137
|
+
end
|
138
|
+
|
139
|
+
# Create a new piece without diminished state
|
140
|
+
#
|
141
|
+
# @return [Piece] new piece instance without diminished state
|
142
|
+
# @example
|
143
|
+
# piece.undiminish # -k => k
|
144
|
+
def undiminish
|
145
|
+
return self unless diminished?
|
146
|
+
|
147
|
+
self.class.new(
|
148
|
+
letter,
|
149
|
+
enhanced: @enhanced,
|
150
|
+
diminished: false,
|
151
|
+
intermediate: @intermediate
|
152
|
+
)
|
153
|
+
end
|
154
|
+
|
155
|
+
# Create a new piece with intermediate state
|
156
|
+
#
|
157
|
+
# @return [Piece] new piece instance with intermediate state
|
158
|
+
# @example
|
159
|
+
# piece.intermediate # k => k'
|
160
|
+
def intermediate
|
161
|
+
return self if intermediate?
|
162
|
+
|
163
|
+
self.class.new(
|
164
|
+
letter,
|
165
|
+
enhanced: @enhanced,
|
166
|
+
diminished: @diminished,
|
167
|
+
intermediate: true
|
168
|
+
)
|
169
|
+
end
|
170
|
+
|
171
|
+
# Create a new piece without intermediate state
|
172
|
+
#
|
173
|
+
# @return [Piece] new piece instance without intermediate state
|
174
|
+
# @example
|
175
|
+
# piece.unintermediate # k' => k
|
176
|
+
def unintermediate
|
177
|
+
return self unless intermediate?
|
178
|
+
|
179
|
+
self.class.new(
|
180
|
+
letter,
|
181
|
+
enhanced: @enhanced,
|
182
|
+
diminished: @diminished,
|
183
|
+
intermediate: false
|
184
|
+
)
|
185
|
+
end
|
186
|
+
|
187
|
+
# Create a new piece without any modifiers
|
188
|
+
#
|
189
|
+
# @return [Piece] new piece instance with only the base letter
|
190
|
+
# @example
|
191
|
+
# piece.bare # +k' => k
|
192
|
+
def bare
|
193
|
+
return self if bare?
|
194
|
+
|
195
|
+
self.class.new(letter)
|
196
|
+
end
|
197
|
+
|
198
|
+
# Create a new piece with opposite ownership (case)
|
199
|
+
#
|
200
|
+
# @return [Piece] new piece instance with flipped case
|
201
|
+
# @example
|
202
|
+
# piece.flip # K => k, k => K
|
203
|
+
def flip
|
204
|
+
flipped_letter = letter.swapcase
|
205
|
+
|
206
|
+
self.class.new(
|
207
|
+
flipped_letter,
|
208
|
+
enhanced: @enhanced,
|
209
|
+
diminished: @diminished,
|
210
|
+
intermediate: @intermediate
|
211
|
+
)
|
212
|
+
end
|
213
|
+
|
214
|
+
# Check if the piece has enhanced state
|
215
|
+
#
|
216
|
+
# @return [Boolean] true if enhanced
|
217
|
+
def enhanced?
|
218
|
+
@enhanced
|
219
|
+
end
|
220
|
+
|
221
|
+
# Check if the piece has diminished state
|
222
|
+
#
|
223
|
+
# @return [Boolean] true if diminished
|
224
|
+
def diminished?
|
225
|
+
@diminished
|
226
|
+
end
|
227
|
+
|
228
|
+
# Check if the piece has intermediate state
|
229
|
+
#
|
230
|
+
# @return [Boolean] true if intermediate
|
231
|
+
def intermediate?
|
232
|
+
@intermediate
|
233
|
+
end
|
234
|
+
|
235
|
+
# Check if the piece has no modifiers
|
236
|
+
#
|
237
|
+
# @return [Boolean] true if no modifiers are present
|
238
|
+
def bare?
|
239
|
+
!enhanced? && !diminished? && !intermediate?
|
240
|
+
end
|
241
|
+
|
242
|
+
# Check if the piece belongs to the first player (uppercase)
|
243
|
+
#
|
244
|
+
# @return [Boolean] true if uppercase letter
|
245
|
+
def uppercase?
|
246
|
+
letter == letter.upcase
|
247
|
+
end
|
248
|
+
|
249
|
+
# Check if the piece belongs to the second player (lowercase)
|
250
|
+
#
|
251
|
+
# @return [Boolean] true if lowercase letter
|
252
|
+
def lowercase?
|
253
|
+
letter == letter.downcase
|
254
|
+
end
|
255
|
+
|
256
|
+
# Custom equality comparison
|
257
|
+
#
|
258
|
+
# @param other [Object] object to compare with
|
259
|
+
# @return [Boolean] true if pieces are equal
|
260
|
+
def ==(other)
|
261
|
+
return false unless other.is_a?(self.class)
|
262
|
+
|
263
|
+
letter == other.letter &&
|
264
|
+
enhanced? == other.enhanced? &&
|
265
|
+
diminished? == other.diminished? &&
|
266
|
+
intermediate? == other.intermediate?
|
267
|
+
end
|
268
|
+
|
269
|
+
# Custom hash implementation for use in collections
|
270
|
+
#
|
271
|
+
# @return [Integer] hash value
|
272
|
+
def hash
|
273
|
+
[letter, @enhanced, @diminished, @intermediate].hash
|
274
|
+
end
|
275
|
+
|
276
|
+
# Custom inspection for debugging
|
277
|
+
#
|
278
|
+
# @return [String] detailed string representation
|
279
|
+
def inspect
|
280
|
+
modifiers = []
|
281
|
+
modifiers << "enhanced=true" if enhanced?
|
282
|
+
modifiers << "diminished=true" if diminished?
|
283
|
+
modifiers << "intermediate=true" if intermediate?
|
284
|
+
|
285
|
+
modifier_str = modifiers.empty? ? "" : " #{modifiers.join(' ')}"
|
286
|
+
"#<#{self.class.name}:0x#{object_id.to_s(16)} letter='#{letter}'#{modifier_str}>"
|
287
|
+
end
|
288
|
+
|
289
|
+
# Validate that the letter is a single ASCII letter
|
290
|
+
#
|
291
|
+
# @param letter [String] the letter to validate
|
292
|
+
# @raise [ArgumentError] if invalid
|
293
|
+
def self.validate_letter(letter)
|
294
|
+
letter_str = String(letter)
|
295
|
+
return if letter_str.match?(/\A[a-zA-Z]\z/)
|
296
|
+
|
297
|
+
raise ::ArgumentError, format(ERROR_INVALID_LETTER, letter_str)
|
298
|
+
end
|
299
|
+
|
300
|
+
# Validate that enhanced and diminished states are not both true
|
301
|
+
#
|
302
|
+
# @param enhanced [Boolean] enhanced state
|
303
|
+
# @param diminished [Boolean] diminished state
|
304
|
+
# @raise [ArgumentError] if both are true
|
305
|
+
def self.validate_state_combination(enhanced, diminished)
|
306
|
+
return unless enhanced && diminished
|
307
|
+
|
308
|
+
raise ::ArgumentError, "A piece cannot be both enhanced and diminished"
|
309
|
+
end
|
310
|
+
|
311
|
+
# Match PNN pattern against string
|
312
|
+
#
|
313
|
+
# @param string [String] string to match
|
314
|
+
# @return [MatchData] match data
|
315
|
+
# @raise [ArgumentError] if string doesn't match
|
316
|
+
def self.match_pattern(string)
|
317
|
+
matches = PNN_PATTERN.match(string)
|
318
|
+
return matches if matches
|
319
|
+
|
320
|
+
raise ::ArgumentError, format(ERROR_INVALID_PNN, string)
|
321
|
+
end
|
322
|
+
|
323
|
+
private_class_method :match_pattern
|
324
|
+
end
|
325
|
+
end
|
data/lib/pnn.rb
CHANGED
@@ -1,70 +1,67 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require_relative File.join("pnn", "
|
4
|
-
require_relative File.join("pnn", "parser")
|
5
|
-
require_relative File.join("pnn", "validator")
|
3
|
+
require_relative File.join("pnn", "piece")
|
6
4
|
|
7
|
-
# This module provides a Ruby interface for
|
8
|
-
#
|
5
|
+
# This module provides a Ruby interface for working with piece identifiers
|
6
|
+
# in PNN (Piece Name Notation) format.
|
9
7
|
#
|
10
|
-
# PNN
|
11
|
-
#
|
8
|
+
# PNN defines a consistent and rule-agnostic format for representing pieces
|
9
|
+
# in abstract strategy board games, providing a standardized way to identify
|
10
|
+
# pieces independent of any specific game rules or mechanics.
|
11
|
+
#
|
12
|
+
# The primary interface is the `Pnn::Piece` class, which provides an
|
13
|
+
# object-oriented API for creating, parsing, and manipulating piece representations.
|
14
|
+
#
|
15
|
+
# @example Basic usage with the Piece class
|
16
|
+
# # Parse a PNN string
|
17
|
+
# piece = Pnn::Piece.parse("k")
|
18
|
+
#
|
19
|
+
# # Create directly
|
20
|
+
# piece = Pnn::Piece.new("k")
|
21
|
+
#
|
22
|
+
# # Manipulate states
|
23
|
+
# enhanced = piece.enhance
|
24
|
+
# enhanced.to_s # => "+k"
|
25
|
+
#
|
26
|
+
# # Change ownership
|
27
|
+
# flipped = piece.flip
|
28
|
+
# flipped.to_s # => "K"
|
12
29
|
#
|
13
30
|
# @see https://sashite.dev/documents/pnn/1.0.0/
|
31
|
+
# @see Pnn::Piece
|
14
32
|
module Pnn
|
15
|
-
#
|
33
|
+
# Validate if the given string is a valid PNN string.
|
16
34
|
#
|
17
|
-
# @param
|
18
|
-
# @
|
19
|
-
# @param suffix [String, nil] Optional modifier following the letter ('=', '<', or '>')
|
20
|
-
# @return [String] PNN notation string
|
21
|
-
# @raise [ArgumentError] If any parameter is invalid
|
22
|
-
# @example
|
23
|
-
# Pnn.dump(letter: "k", suffix: "=")
|
24
|
-
# # => "k="
|
25
|
-
def self.dump(letter:, prefix: nil, suffix: nil)
|
26
|
-
Dumper.dump(letter:, prefix:, suffix:)
|
27
|
-
end
|
28
|
-
|
29
|
-
# Parses a PNN string into its component parts.
|
30
|
-
#
|
31
|
-
# @param pnn_string [String] PNN notation string
|
32
|
-
# @return [Hash] Hash containing the parsed piece data with the following keys:
|
33
|
-
# - :letter [String] - The base letter identifier
|
34
|
-
# - :prefix [String, nil] - The prefix modifier if present
|
35
|
-
# - :suffix [String, nil] - The suffix modifier if present
|
36
|
-
# @raise [ArgumentError] If the PNN string is invalid
|
35
|
+
# @param pnn_string [String] PNN string to validate
|
36
|
+
# @return [Boolean] True if the string is a valid PNN string
|
37
37
|
# @example
|
38
|
-
# Pnn.
|
39
|
-
#
|
40
|
-
def self.
|
41
|
-
|
38
|
+
# Pnn.valid?("k'") # => true
|
39
|
+
# Pnn.valid?("invalid") # => false
|
40
|
+
def self.valid?(pnn_string)
|
41
|
+
Piece.parse(pnn_string).to_s == pnn_string
|
42
|
+
rescue ArgumentError
|
43
|
+
false
|
42
44
|
end
|
43
45
|
|
44
|
-
#
|
46
|
+
# Create a new piece instance.
|
45
47
|
#
|
46
|
-
#
|
47
|
-
# @return [Hash, nil] Hash containing the parsed piece data or nil if parsing fails
|
48
|
-
# @example
|
49
|
-
# # Valid PNN string
|
50
|
-
# Pnn.safe_parse("+k=")
|
51
|
-
# # => { letter: "k", prefix: "+", suffix: "=" }
|
52
|
-
#
|
53
|
-
# # Invalid PNN string
|
54
|
-
# Pnn.safe_parse("invalid")
|
55
|
-
# # => nil
|
56
|
-
def self.safe_parse(pnn_string)
|
57
|
-
Parser.safe_parse(pnn_string)
|
58
|
-
end
|
59
|
-
|
60
|
-
# Validates if the given string is a valid PNN string
|
48
|
+
# This is a convenience method that delegates to `Pnn::Piece.new`.
|
61
49
|
#
|
62
|
-
# @param
|
63
|
-
# @
|
50
|
+
# @param letter [String] single ASCII letter (a-z or A-Z)
|
51
|
+
# @param enhanced [Boolean] whether the piece has enhanced state
|
52
|
+
# @param diminished [Boolean] whether the piece has diminished state
|
53
|
+
# @param intermediate [Boolean] whether the piece has intermediate state
|
54
|
+
# @return [Pnn::Piece] new piece instance
|
55
|
+
# @raise [ArgumentError] if parameters are invalid
|
64
56
|
# @example
|
65
|
-
# Pnn.
|
66
|
-
#
|
67
|
-
def self.
|
68
|
-
|
57
|
+
# piece = Pnn.piece("k", enhanced: true)
|
58
|
+
# piece.to_s # => "+k"
|
59
|
+
def self.piece(letter, enhanced: false, diminished: false, intermediate: false)
|
60
|
+
Piece.new(
|
61
|
+
letter,
|
62
|
+
enhanced:,
|
63
|
+
diminished:,
|
64
|
+
intermediate:
|
65
|
+
)
|
69
66
|
end
|
70
67
|
end
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: pnn
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version:
|
4
|
+
version: 2.0.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Cyril Kato
|
@@ -9,10 +9,12 @@ bindir: bin
|
|
9
9
|
cert_chain: []
|
10
10
|
date: 1980-01-02 00:00:00.000000000 Z
|
11
11
|
dependencies: []
|
12
|
-
description: A Ruby interface for
|
13
|
-
in PNN format. PNN
|
14
|
-
in abstract strategy board games
|
15
|
-
|
12
|
+
description: A clean, immutable Ruby interface for working with piece identifiers
|
13
|
+
in PNN format. PNN provides a consistent and rule-agnostic notation for representing
|
14
|
+
pieces in abstract strategy board games like chess, shogi, and xiangqi. Features
|
15
|
+
include state modifiers for enhanced/diminished/intermediate pieces, ownership changes,
|
16
|
+
and comprehensive validation. Perfect for game engines, analysis tools, and educational
|
17
|
+
applications.
|
16
18
|
email: contact@cyril.email
|
17
19
|
executables: []
|
18
20
|
extensions: []
|
@@ -21,9 +23,7 @@ files:
|
|
21
23
|
- LICENSE.md
|
22
24
|
- README.md
|
23
25
|
- lib/pnn.rb
|
24
|
-
- lib/pnn/
|
25
|
-
- lib/pnn/parser.rb
|
26
|
-
- lib/pnn/validator.rb
|
26
|
+
- lib/pnn/piece.rb
|
27
27
|
homepage: https://github.com/sashite/pnn.rb
|
28
28
|
licenses:
|
29
29
|
- MIT
|
@@ -50,5 +50,6 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
50
50
|
requirements: []
|
51
51
|
rubygems_version: 3.6.7
|
52
52
|
specification_version: 4
|
53
|
-
summary:
|
53
|
+
summary: Modern Ruby implementation of Piece Name Notation (PNN) for abstract strategy
|
54
|
+
games.
|
54
55
|
test_files: []
|
data/lib/pnn/dumper.rb
DELETED
@@ -1,35 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
module Pnn
|
4
|
-
# Serializes piece components into PNN strings
|
5
|
-
class Dumper
|
6
|
-
# Valid prefix modifiers
|
7
|
-
VALID_PREFIXES = ["+", "-", nil].freeze
|
8
|
-
|
9
|
-
# Valid suffix modifiers
|
10
|
-
VALID_SUFFIXES = ["=", "<", ">", nil].freeze
|
11
|
-
|
12
|
-
# Serialize piece components into a PNN string
|
13
|
-
#
|
14
|
-
# @param letter [String] The single ASCII letter identifier
|
15
|
-
# @param prefix [String, nil] Optional prefix modifier
|
16
|
-
# @param suffix [String, nil] Optional suffix modifier
|
17
|
-
# @return [String] PNN notation string
|
18
|
-
# @raise [ArgumentError] If any component is invalid
|
19
|
-
def self.dump(letter:, prefix: nil, suffix: nil)
|
20
|
-
letter = String(letter)
|
21
|
-
|
22
|
-
unless letter.match?(/^[a-zA-Z]$/)
|
23
|
-
raise ArgumentError, "Letter must be a single ASCII letter (a-z or A-Z): #{letter}"
|
24
|
-
end
|
25
|
-
|
26
|
-
raise ArgumentError, "Invalid prefix: #{prefix}. Must be '+', '-', or nil." unless VALID_PREFIXES.include?(prefix)
|
27
|
-
|
28
|
-
unless VALID_SUFFIXES.include?(suffix)
|
29
|
-
raise ArgumentError, "Invalid suffix: #{suffix}. Must be '=', '<', '>', or nil."
|
30
|
-
end
|
31
|
-
|
32
|
-
"#{prefix}#{letter}#{suffix}"
|
33
|
-
end
|
34
|
-
end
|
35
|
-
end
|
data/lib/pnn/parser.rb
DELETED
@@ -1,38 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
module Pnn
|
4
|
-
# Parses PNN strings into their component parts
|
5
|
-
class Parser
|
6
|
-
# PNN regex capture groups for parsing
|
7
|
-
PATTERN = /\A(?<prefix>[-+])?(?<letter>[a-zA-Z])(?<suffix>[=<>])?\z/
|
8
|
-
|
9
|
-
# Parse a PNN string into its components
|
10
|
-
#
|
11
|
-
# @param pnn_string [String] The PNN string to parse
|
12
|
-
# @return [Hash] Hash containing the parsed components
|
13
|
-
# @raise [ArgumentError] If the PNN string is invalid
|
14
|
-
def self.parse(pnn_string)
|
15
|
-
pnn_string = String(pnn_string)
|
16
|
-
|
17
|
-
matches = PATTERN.match(pnn_string)
|
18
|
-
|
19
|
-
raise ArgumentError, "Invalid PNN string: #{pnn_string}" if matches.nil?
|
20
|
-
|
21
|
-
{
|
22
|
-
letter: matches[:letter],
|
23
|
-
prefix: matches[:prefix],
|
24
|
-
suffix: matches[:suffix]
|
25
|
-
}.compact
|
26
|
-
end
|
27
|
-
|
28
|
-
# Safely parse a PNN string without raising exceptions
|
29
|
-
#
|
30
|
-
# @param pnn_string [String] The PNN string to parse
|
31
|
-
# @return [Hash, nil] Hash containing the parsed components or nil if invalid
|
32
|
-
def self.safe_parse(pnn_string)
|
33
|
-
parse(pnn_string)
|
34
|
-
rescue ArgumentError
|
35
|
-
nil
|
36
|
-
end
|
37
|
-
end
|
38
|
-
end
|
data/lib/pnn/validator.rb
DELETED
@@ -1,17 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
module Pnn
|
4
|
-
# Validates PNN strings according to the specification
|
5
|
-
class Validator
|
6
|
-
# PNN validation pattern matching the JSON schema pattern in the spec
|
7
|
-
PATTERN = /\A[-+]?[a-zA-Z][=<>]?\z/
|
8
|
-
|
9
|
-
# Class method to validate PNN strings
|
10
|
-
#
|
11
|
-
# @param pnn_string [String] The PNN string to validate
|
12
|
-
# @return [Boolean] True if the string is valid according to PNN specification
|
13
|
-
def self.valid?(pnn_string)
|
14
|
-
String(pnn_string).match?(PATTERN)
|
15
|
-
end
|
16
|
-
end
|
17
|
-
end
|