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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 8c243c47d75b03455ad8fa85bebd13cf90b6e3abb2c37f93cf425a1a51f14b16
4
- data.tar.gz: 89a3d75180c92c5627e6dfb9c301f0ba3637e59c923448b7e5c9cde175805144
3
+ metadata.gz: '0584c1ee7a42c3f556efe5e27e106c07b7e12c37148ba345238ef9fb02622c76'
4
+ data.tar.gz: b31fcbfb3442f0b130662a0cbd7e644c127b3c8a9247f3380f85056aa0be6ea1
5
5
  SHA512:
6
- metadata.gz: 688721867ee73331f8b2b49606caefafe3239efccc7756d122b1cb76453c472d9a71dba2de13224388a6ea110651abb5386d87039a3c110e0366a03c00cfd4f0
7
- data.tar.gz: 82e0e2c1d4f058a8d09c107ad1f6beef50a0a2c826b9467504fca411281ce766d8cac9468cca105760ba16db1a014d5d0db1c92b6a4fdd268f24c07c273274e6
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 (`=`, `<`, or `>`)
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
- ### Parsing PNN Strings
44
+ ### Creating Piece Objects
48
45
 
49
- Convert a PNN string into a structured Ruby hash:
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
- # Basic letter
55
- result = Pnn.parse("k")
56
- # => { letter: "k" }
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 prefix
59
- result = Pnn.parse("+k")
60
- # => { letter: "k", prefix: "+" }
55
+ # With modifiers
56
+ enhanced_piece = Pnn::Piece.parse("+k'")
57
+ # => #<Pnn::Piece:0x... @letter="k", @enhanced=true, @intermediate=true>
61
58
 
62
- # With suffix
63
- result = Pnn.parse("k=")
64
- # => { letter: "k", suffix: "=" }
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
- # With both prefix and suffix
67
- result = Pnn.parse("+k=")
68
- # => { letter: "k", prefix: "+", suffix: "=" }
63
+ # Convenience method
64
+ piece = Pnn.piece("k", enhanced: true)
69
65
  ```
70
66
 
71
- ### Safe Parsing
67
+ ### Converting to PNN String
72
68
 
73
- Parse a PNN string without raising exceptions:
69
+ Convert a piece object back to its PNN string representation:
74
70
 
75
71
  ```ruby
76
- require "pnn"
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
- # Valid PNN string
79
- result = Pnn.safe_parse("+k=")
80
- # => { letter: "k", prefix: "+", suffix: "=" }
100
+ # Remove states
101
+ restored = enhanced.unenhance
102
+ restored.to_s # => "k"
81
103
 
82
- # Invalid PNN string
83
- result = Pnn.safe_parse("invalid pnn string")
84
- # => nil
104
+ # Combine states
105
+ complex = piece.enhance.intermediate
106
+ complex.to_s # => "+k'"
85
107
  ```
86
108
 
87
- ### Creating PNN Strings
109
+ ### Ownership Changes
88
110
 
89
- Convert piece components into a PNN string:
111
+ Change piece ownership (case conversion):
90
112
 
91
113
  ```ruby
92
- require "pnn"
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
- # Basic letter
95
- Pnn.dump(letter: "k")
96
- # => "k"
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
- # With prefix
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
- # With suffix
103
- Pnn.dump(letter: "k", suffix: "=")
104
- # => "k="
170
+ ## Examples of PNN in Common Games
105
171
 
106
- # With both prefix and suffix
107
- Pnn.dump(letter: "p", prefix: "+", suffix: ">")
108
- # => "+p>"
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
- Check if a string is valid PNN notation:
245
+ All parsing automatically validates input according to the PNN specification:
114
246
 
115
247
  ```ruby
116
- require "pnn"
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
- Pnn.valid?("k") # => true
119
- Pnn.valid?("+p") # => true
120
- Pnn.valid?("k=") # => true
121
- Pnn.valid?("+p>") # => true
279
+ ### Inspection and Debugging
122
280
 
123
- Pnn.valid?("") # => false
124
- Pnn.valid?("kp") # => false
125
- Pnn.valid?("++k") # => false
126
- Pnn.valid?("k==") # => false
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
- ### Piece Modifiers
296
+ ## API Reference
297
+
298
+ ### Module Methods
130
299
 
131
- PNN supports prefixes and suffixes for pieces to denote various states or capabilities:
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
- - **Prefix `+`**: Alternative or enhanced state
134
- - Example in shogi: `+p` may represent a promoted pawn
303
+ ### Pnn::Piece Class Methods
135
304
 
136
- - **Prefix `-`**: Diminished or restricted state
137
- - Example: `-k` may represent a king with restricted movement
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
- - **Suffix `=`**: Bidirectional or dual-option state
140
- - Example in chess: `k=` may represent a king eligible for both kingside and queenside castling
308
+ ### Instance Methods
141
309
 
142
- - **Suffix `<`**: Left-side constraint or condition
143
- - Example in chess: `k<` may represent a king eligible for queenside castling only
144
- - Example in chess: `p<` may represent a pawn that may be captured _en passant_ from the left
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
- - **Suffix `>`**: Right-side constraint or condition
147
- - Example in chess: `k>` may represent a king eligible for kingside castling only
148
- - Example in chess: `p>` may represent a pawn that may be captured en passant from the right
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
- These modifiers have no defined semantics in the PNN specification itself but provide a flexible framework for representing piece-specific conditions while maintaining PNN's rule-agnostic nature.
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/) - a project dedicated to promoting chess variants and sharing the beauty of Chinese, Japanese, and Western chess cultures.
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", "dumper")
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 serialization and
8
- # deserialization of piece identifiers in PNN format.
5
+ # This module provides a Ruby interface for working with piece identifiers
6
+ # in PNN (Piece Name Notation) format.
9
7
  #
10
- # PNN (Piece Name Notation) defines a consistent and rule-agnostic
11
- # format for representing pieces in abstract strategy board games.
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
- # Serializes a piece identifier into a PNN string.
33
+ # Validate if the given string is a valid PNN string.
16
34
  #
17
- # @param prefix [String, nil] Optional modifier preceding the letter ('+' or '-')
18
- # @param letter [String] A single ASCII letter ('a-z' or 'A-Z')
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.parse("+k=")
39
- # # => { letter: "k", prefix: "+", suffix: "=" }
40
- def self.parse(pnn_string)
41
- Parser.parse(pnn_string)
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
- # Safely parses a PNN string into its component parts without raising exceptions.
46
+ # Create a new piece instance.
45
47
  #
46
- # @param pnn_string [String] PNN notation string
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 pnn_string [String] PNN string to validate
63
- # @return [Boolean] True if the string is a valid PNN string
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.valid?("k=") # => true
66
- # Pnn.valid?("invalid") # => false
67
- def self.valid?(pnn_string)
68
- Validator.valid?(pnn_string)
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: 1.0.1
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 serialization and deserialization of piece identifiers
13
- in PNN format. PNN is a consistent and rule-agnostic format for representing pieces
14
- in abstract strategy board games, providing a standardized way to identify pieces
15
- independent of any specific game rules or mechanics.
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/dumper.rb
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: PNN (Piece Name Notation) support for the Ruby language.
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