pnn 1.1.0 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: b48e598bec9d6aeb11a7212428d279f153bbfd00ae6f1a7dc3d0485f4985695c
4
- data.tar.gz: d7d8c7da11021a1056e048ab17605bd5db9abdc4cbbdafc90bd00820e651fedc
3
+ metadata.gz: '0584c1ee7a42c3f556efe5e27e106c07b7e12c37148ba345238ef9fb02622c76'
4
+ data.tar.gz: b31fcbfb3442f0b130662a0cbd7e644c127b3c8a9247f3380f85056aa0be6ea1
5
5
  SHA512:
6
- metadata.gz: 3957d2e98391d216817a37cf4c9ebc479172c1aeb3415ecc19c1e8abb2d9b29298277312c5b187cdbf1ede2a6f30414bf11b5b05a35ef324abf6a314fc5e7108
7
- data.tar.gz: 10665b2f56ae9869dc68db08066dd2e1520691bc4e649ae48cf22643100427d0d0af9981708e3427aab0b8c0894feef37feda79a8c73bd036c020eb423052730
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,163 +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
- ### 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"
77
-
78
- # Valid PNN string
79
- result = Pnn.safe_parse("+k'")
80
- # => { letter: "k", prefix: "+", suffix: "'" }
72
+ piece = Pnn::Piece.parse("k")
73
+ piece.to_s
74
+ # => "k"
81
75
 
82
- # Invalid PNN string
83
- result = Pnn.safe_parse("invalid pnn string")
84
- # => nil
76
+ enhanced_piece = Pnn::Piece.parse("+k'")
77
+ enhanced_piece.to_s
78
+ # => "+k'"
85
79
  ```
86
80
 
87
- ### Creating PNN Strings
81
+ ### State Manipulation
88
82
 
89
- Convert piece components into a PNN string:
83
+ Create new piece instances with different states:
90
84
 
91
85
  ```ruby
92
- require "pnn"
86
+ piece = Pnn::Piece.parse("k")
93
87
 
94
- # Basic letter
95
- Pnn.dump(letter: "k")
96
- # => "k"
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"
97
95
 
98
- # With prefix
99
- Pnn.dump(letter: "p", prefix: "+")
100
- # => "+p"
96
+ # Intermediate state (' suffix)
97
+ intermediate = piece.intermediate
98
+ intermediate.to_s # => "k'"
101
99
 
102
- # With suffix
103
- Pnn.dump(letter: "k", suffix: "'")
104
- # => "k'"
100
+ # Remove states
101
+ restored = enhanced.unenhance
102
+ restored.to_s # => "k"
105
103
 
106
- # With both prefix and suffix
107
- Pnn.dump(letter: "p", prefix: "+", suffix: "'")
108
- # => "+p'"
104
+ # Combine states
105
+ complex = piece.enhance.intermediate
106
+ complex.to_s # => "+k'"
109
107
  ```
110
108
 
111
- ### Validation
109
+ ### Ownership Changes
112
110
 
113
- Check if a string is valid PNN notation:
111
+ Change piece ownership (case conversion):
114
112
 
115
113
  ```ruby
116
- 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
+ ```
117
123
 
118
- Pnn.valid?("k") # => true
119
- Pnn.valid?("+p") # => true
120
- Pnn.valid?("k'") # => true
121
- Pnn.valid?("+p'") # => true
124
+ ### Clean State
122
125
 
123
- Pnn.valid?("") # => false
124
- Pnn.valid?("kp") # => false
125
- Pnn.valid?("++k") # => false
126
- Pnn.valid?("k''") # => false
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"
127
132
  ```
128
133
 
129
- ### Piece Modifiers
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
130
152
 
131
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:
132
154
 
133
- - **Prefix `+`**: Enhanced state
134
- - Example in shogi: `+p` represents a promoted pawn with enhanced movement capabilities
155
+ - **Enhanced state (`+`)**: Represents pieces with enhanced capabilities
156
+ - Example in shogi: `+p` represents a promoted pawn (tokin)
135
157
  - Example in chess variants: `+Q` might represent a queen with special powers
136
158
 
137
- - **Prefix `-`**: Diminished state
138
- - Example in variants: `-R` might represent a rook with restricted movement abilities
139
- - Example in weakened pieces: `-N` could indicate a knight that has been partially immobilized
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
140
162
 
141
- - **Suffix `'`**: Intermediate state
163
+ - **Intermediate state (`'`)**: Represents pieces with special temporary states
142
164
  - Example in chess: `R'` represents a rook that can still be used for castling
143
165
  - Example in chess: `P'` represents a pawn that can be captured en passant
144
166
  - Example in variants: `B'` might indicate a bishop with a special one-time ability
145
167
 
146
- 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. Game implementations are responsible for interpreting these modifiers according to their specific rules.
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.
147
169
 
148
170
  ## Examples of PNN in Common Games
149
171
 
150
- The following examples demonstrate how PNN might be used in familiar games. Remember that PNN itself defines only the notation format, not the game-specific interpretations.
151
-
152
172
  ### Chess Examples
153
173
 
154
- In the context of chess:
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
155
179
 
156
- ```
157
- K # King (first player)
158
- k # King (second player)
159
- Q # Queen (first player)
160
- R ' # Rook that has not moved yet and can be used for castling
161
- P' # Pawn that can be captured en passant
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'"
162
198
  ```
163
199
 
164
200
  ### Shogi Examples
165
201
 
166
- In the context of shogi:
202
+ ```ruby
203
+ # Standard pieces
204
+ king = Pnn::Piece.parse("K") # Oushou (King)
205
+ pawn = Pnn::Piece.parse("P") # Fuhyou (Pawn)
167
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)
168
225
  ```
169
- K # King (first player)
170
- k # King (second player)
171
- +P # Promoted pawn (tokin)
172
- +L # Promoted lance (narikyou)
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"
173
241
  ```
174
242
 
175
- ### Example: A Complete Chess Position with PNN
243
+ ### Validation
176
244
 
177
- A chess position might contain a mix of standard and modified pieces. Here's an example after the moves 1. e4 c5 2. e5 d5:
245
+ All parsing automatically validates input according to the PNN specification:
178
246
 
247
+ ```ruby
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
179
277
  ```
180
- r' n b q k b n r' # Eighth rank with unmoved rooks (castling rights)
181
- p p . . p p p p # Seventh rank pawns (d and c pawns have moved)
182
- . . . . . . . . # Empty sixth rank
183
- . . p p' P . . . # Fifth rank with pawn that can be captured en passant (d5) and other pawns
184
- . . . . . . . . # Empty fourth rank
185
- . . . . . . . . # Empty third rank
186
- P P P P . P P P # Second rank pawns (e pawn has moved)
187
- R' N B Q K B N R' # First rank with unmoved rooks (castling rights)
278
+
279
+ ### Inspection and Debugging
280
+
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
188
294
  ```
189
295
 
190
- In this position, White could capture Black's queen pawn (d5) en passant with the e5 pawn moving to d6.
296
+ ## API Reference
297
+
298
+ ### Module Methods
299
+
300
+ - `Pnn.valid?(pnn_string)` - Check if a string is valid PNN notation
301
+ - `Pnn.piece(letter, **options)` - Convenience method to create pieces
302
+
303
+ ### Pnn::Piece Class Methods
304
+
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
307
+
308
+ ### Instance Methods
309
+
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
317
+
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)
191
327
 
192
- Note: The above representation is merely illustrative; PNN itself only defines the notation for individual pieces, not complete board states.
328
+ #### Conversion
329
+ - `#to_s` - Convert to PNN string representation
330
+ - `#inspect` - Detailed string representation for debugging
193
331
 
194
332
  ## Properties of PNN
195
333
 
196
334
  * **Rule-agnostic**: PNN does not encode legality, validity, or game-specific conditions.
197
335
  * **Canonical representation**: Ensures that equivalent pieces yield identical strings.
198
336
  * **State modifiers**: Express special conditions without compromising rule neutrality.
337
+ * **Immutable objects**: All state changes return new instances, ensuring thread safety.
199
338
 
200
339
  ## Constraints
201
340
 
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 letter [String] A single ASCII letter ('a-z' or 'A-Z')
18
- # @param prefix [String, nil] Optional modifier preceding the letter ('+' or '-')
19
- # @param suffix [String, nil] Optional modifier following the letter (''')
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.1.0
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,72 +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
- # Letter validation pattern
13
- LETTER_PATTERN = /\A[a-zA-Z]\z/
14
-
15
- # Error messages
16
- ERROR_INVALID_LETTER = "Letter must be a single ASCII letter (a-z or A-Z): %s"
17
- ERROR_INVALID_PREFIX = "Invalid prefix: %s. Must be '+', '-', or nil."
18
- ERROR_INVALID_SUFFIX = "Invalid suffix: %s. Must be ''', or nil."
19
-
20
- # Serialize piece components into a PNN string
21
- #
22
- # @param letter [String] The single ASCII letter identifier
23
- # @param prefix [String, nil] Optional prefix modifier
24
- # @param suffix [String, nil] Optional suffix modifier
25
- # @return [String] PNN notation string
26
- # @raise [ArgumentError] If any component is invalid
27
- def self.dump(letter:, prefix: nil, suffix: nil)
28
- validate_letter(letter)
29
- validate_prefix(prefix)
30
- validate_suffix(suffix)
31
-
32
- "#{prefix}#{letter}#{suffix}"
33
- end
34
-
35
- # Validates that the letter is a single ASCII letter
36
- #
37
- # @param letter [Object] The letter to validate
38
- # @return [void]
39
- # @raise [ArgumentError] If the letter is invalid
40
- def self.validate_letter(letter)
41
- letter_str = String(letter)
42
-
43
- return if letter_str.match?(LETTER_PATTERN)
44
-
45
- raise ArgumentError, format(ERROR_INVALID_LETTER, letter_str)
46
- end
47
-
48
- # Validates that the prefix is valid
49
- #
50
- # @param prefix [String, nil] The prefix to validate
51
- # @return [void]
52
- # @raise [ArgumentError] If the prefix is invalid
53
- def self.validate_prefix(prefix)
54
- return if VALID_PREFIXES.include?(prefix)
55
-
56
- raise ArgumentError, format(ERROR_INVALID_PREFIX, prefix)
57
- end
58
-
59
- # Validates that the suffix is valid
60
- #
61
- # @param suffix [String, nil] The suffix to validate
62
- # @return [void]
63
- # @raise [ArgumentError] If the suffix is invalid
64
- def self.validate_suffix(suffix)
65
- return if VALID_SUFFIXES.include?(suffix)
66
-
67
- raise ArgumentError, format(ERROR_INVALID_SUFFIX, suffix)
68
- end
69
-
70
- private_class_method :validate_letter, :validate_prefix, :validate_suffix
71
- end
72
- end
data/lib/pnn/parser.rb DELETED
@@ -1,62 +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
- # Error message for invalid PNN string
10
- ERROR_INVALID_PNN = "Invalid PNN string: %s"
11
-
12
- # Component keys for the result hash
13
- COMPONENT_KEYS = %i[letter prefix suffix].freeze
14
-
15
- # Parse a PNN string into its components
16
- #
17
- # @param pnn_string [String] The PNN string to parse
18
- # @return [Hash] Hash containing the parsed components
19
- # @raise [ArgumentError] If the PNN string is invalid
20
- def self.parse(pnn_string)
21
- string_value = String(pnn_string)
22
- matches = match_pattern(string_value)
23
- extract_components(matches)
24
- end
25
-
26
- # Safely parse a PNN string without raising exceptions
27
- #
28
- # @param pnn_string [String] The PNN string to parse
29
- # @return [Hash, nil] Hash containing the parsed components or nil if invalid
30
- def self.safe_parse(pnn_string)
31
- parse(pnn_string)
32
- rescue ArgumentError
33
- nil
34
- end
35
-
36
- # Match the PNN pattern against a string
37
- #
38
- # @param string [String] The string to match
39
- # @return [MatchData] The match data
40
- # @raise [ArgumentError] If the string doesn't match the pattern
41
- def self.match_pattern(string)
42
- matches = PATTERN.match(string)
43
-
44
- return matches if matches
45
-
46
- raise ArgumentError, format(ERROR_INVALID_PNN, string)
47
- end
48
-
49
- # Extract components from match data
50
- #
51
- # @param matches [MatchData] The match data
52
- # @return [Hash] Hash containing the parsed components
53
- def self.extract_components(matches)
54
- COMPONENT_KEYS.each_with_object({}) do |key, result|
55
- value = matches[key]
56
- result[key] = value if value
57
- end
58
- end
59
-
60
- private_class_method :match_pattern, :extract_components
61
- end
62
- end
data/lib/pnn/validator.rb DELETED
@@ -1,27 +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 [Object] 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
- validate_string(String(pnn_string))
15
- end
16
-
17
- # Validates the given string against the PNN pattern
18
- #
19
- # @param string [String] The string to validate
20
- # @return [Boolean] True if the string matches the PNN pattern
21
- def self.validate_string(string)
22
- string.match?(PATTERN)
23
- end
24
-
25
- private_class_method :validate_string
26
- end
27
- end