sashite-pnn 1.0.0 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -6,37 +6,75 @@ module Sashite
6
6
  module Pnn
7
7
  # Represents a piece in PNN (Piece Name Notation) format.
8
8
  #
9
- # Extends PIN::Piece to add style awareness through derivation markers.
10
- # A PNN piece consists of a PIN string with an optional derivation suffix:
11
- # - No suffix: native style
12
- # - Apostrophe suffix ('): foreign style
9
+ # A piece consists of a PIN component with an optional derivation marker:
10
+ # - PIN component: [<state>]<letter> (from PIN specification)
11
+ # - Derivation marker: "'" (foreign style) or none (native style)
13
12
  #
14
- # All instances are immutable - style manipulation methods return new instances.
15
- class Piece < ::Sashite::Pin::Piece
16
- # PNN validation pattern extending PIN
17
- PNN_PATTERN = /\A(?<prefix>[-+])?(?<letter>[a-zA-Z])(?<suffix>'?)\z/
18
-
19
- # Derivation marker for foreign style
13
+ # The case of the letter determines ownership:
14
+ # - Uppercase (A-Z): first player
15
+ # - Lowercase (a-z): second player
16
+ #
17
+ # Style derivation logic:
18
+ # - No suffix: piece has the native style of its current side
19
+ # - Apostrophe suffix: piece has the foreign style (opposite side's native style)
20
+ #
21
+ # All instances are immutable - state manipulation methods return new instances.
22
+ # This extends the Game Protocol's piece model with Style support through derivation.
23
+ class Piece
24
+ # Valid derivation suffixes
20
25
  FOREIGN_SUFFIX = "'"
26
+ NATIVE_SUFFIX = ""
27
+
28
+ # Derivation constants
29
+ NATIVE = true
30
+ FOREIGN = false
31
+
32
+ # Valid derivations
33
+ VALID_DERIVATIONS = [NATIVE, FOREIGN].freeze
21
34
 
22
35
  # Error messages
23
36
  ERROR_INVALID_PNN = "Invalid PNN string: %s"
24
- ERROR_INVALID_NATIVE = "Native must be true or false: %s"
37
+ ERROR_INVALID_DERIVATION = "Derivation must be true (native) or false (foreign), got: %s"
38
+
39
+ # @return [Symbol] the piece type (:A to :Z)
40
+ def type
41
+ @piece.type
42
+ end
43
+
44
+ # @return [Symbol] the player side (:first or :second)
45
+ def side
46
+ @piece.side
47
+ end
48
+
49
+ # @return [Symbol] the piece state (:normal, :enhanced, or :diminished)
50
+ def state
51
+ @piece.state
52
+ end
25
53
 
26
- # @return [Boolean] whether the piece has native style
54
+ # @return [Boolean] the style derivation (true for native, false for foreign)
27
55
  attr_reader :native
28
- alias native? native
29
56
 
30
- # Create a new piece instance with style information
57
+ # Create a new piece instance
31
58
  #
32
- # @param letter [String] single ASCII letter (a-z or A-Z)
33
- # @param native [Boolean] whether the piece has native style
59
+ # @param type [Symbol] piece type (:A to :Z)
60
+ # @param side [Symbol] player side (:first or :second)
61
+ # @param state [Symbol] piece state (:normal, :enhanced, or :diminished)
62
+ # @param native [Boolean] style derivation (true for native, false for foreign)
34
63
  # @raise [ArgumentError] if parameters are invalid
35
- def initialize(letter, native: true, **)
36
- raise ::ArgumentError, format(ERROR_INVALID_NATIVE, native) unless [true, false].include?(native)
37
-
64
+ # @example
65
+ # Piece.new(:K, :first, :normal, true)
66
+ # Piece.new(:P, :second, :enhanced, false)
67
+ def initialize(type, side, state = Pin::Piece::NORMAL_STATE, native = NATIVE)
68
+ # Validate using PIN class methods for type, side, and state
69
+ Pin::Piece.validate_type(type)
70
+ Pin::Piece.validate_side(side)
71
+ Pin::Piece.validate_state(state)
72
+ self.class.validate_derivation(native)
73
+
74
+ @piece = Pin::Piece.new(type, side, state)
38
75
  @native = native
39
- super(letter, **)
76
+
77
+ freeze
40
78
  end
41
79
 
42
80
  # Parse a PNN string into a Piece object
@@ -45,190 +83,359 @@ module Sashite
45
83
  # @return [Piece] new piece instance
46
84
  # @raise [ArgumentError] if the PNN string is invalid
47
85
  # @example
48
- # Pnn::Piece.parse("k") # => #<Pnn::Piece letter="k" native=true>
49
- # Pnn::Piece.parse("k'") # => #<Pnn::Piece letter="k" native=false>
50
- # Pnn::Piece.parse("+R'") # => #<Pnn::Piece letter="R" enhanced=true native=false>
86
+ # Pnn::Piece.parse("k") # => #<Pnn::Piece type=:K side=:second state=:normal native=true>
87
+ # Pnn::Piece.parse("+R'") # => #<Pnn::Piece type=:R side=:first state=:enhanced native=false>
88
+ # Pnn::Piece.parse("-p") # => #<Pnn::Piece type=:P side=:second state=:diminished native=true>
51
89
  def self.parse(pnn_string)
52
90
  string_value = String(pnn_string)
53
- matches = match_pattern(string_value)
54
91
 
55
- letter = matches[:letter]
56
- enhanced = matches[:prefix] == ENHANCED_PREFIX
57
- diminished = matches[:prefix] == DIMINISHED_PREFIX
58
- native = matches[:suffix] != FOREIGN_SUFFIX
92
+ # Check for derivation suffix
93
+ if string_value.end_with?(FOREIGN_SUFFIX)
94
+ pin_part = string_value[0...-1] # Remove the apostrophe
95
+ foreign = true
96
+ else
97
+ pin_part = string_value
98
+ foreign = false
99
+ end
100
+
101
+ # Validate and parse the PIN part using existing PIN logic
102
+ raise ::ArgumentError, format(ERROR_INVALID_PNN, string_value) unless Pin::Piece.valid?(pin_part)
103
+
104
+ pin_piece = Pin::Piece.parse(pin_part)
105
+ piece_native = !foreign
59
106
 
60
- new(
61
- letter,
62
- native: native,
63
- enhanced: enhanced,
64
- diminished: diminished
65
- )
107
+ new(pin_piece.type, pin_piece.side, pin_piece.state, piece_native)
108
+ end
109
+
110
+ # Check if a string is a valid PNN notation
111
+ #
112
+ # @param pnn_string [String] The string to validate
113
+ # @return [Boolean] true if valid PNN, false otherwise
114
+ #
115
+ # @example
116
+ # Sashite::Pnn::Piece.valid?("K") # => true
117
+ # Sashite::Pnn::Piece.valid?("+R'") # => true
118
+ # Sashite::Pnn::Piece.valid?("-p") # => true
119
+ # Sashite::Pnn::Piece.valid?("KK") # => false
120
+ # Sashite::Pnn::Piece.valid?("++K") # => false
121
+ def self.valid?(pnn_string)
122
+ return false unless pnn_string.is_a?(::String)
123
+ return false if pnn_string.empty?
124
+
125
+ # Check for derivation suffix
126
+ if pnn_string.end_with?(FOREIGN_SUFFIX)
127
+ pin_part = pnn_string[0...-1] # Remove the apostrophe
128
+ return false if pin_part.empty? # Can't have just an apostrophe
129
+ else
130
+ pin_part = pnn_string
131
+ end
132
+
133
+ # Validate the PIN part using existing PIN validation
134
+ Pin::Piece.valid?(pin_part)
66
135
  end
67
136
 
68
137
  # Convert the piece to its PNN string representation
69
138
  #
70
139
  # @return [String] PNN notation string
71
140
  # @example
72
- # piece.to_s # => "k"
73
- # piece.to_s # => "k'"
74
141
  # piece.to_s # => "+R'"
142
+ # piece.to_s # => "-p"
143
+ # piece.to_s # => "K"
75
144
  def to_s
76
- pin_string = super
77
- suffix = native? ? "" : FOREIGN_SUFFIX
78
- "#{pin_string}#{suffix}"
145
+ "#{prefix}#{letter}#{suffix}"
79
146
  end
80
147
 
81
- # Convert the piece to its underlying PIN representation
148
+ # Get the letter representation (inherited from PIN logic)
82
149
  #
83
- # @return [String] PIN notation string without style information
84
- def to_pin
85
- nativize.to_s
150
+ # @return [String] letter representation combining type and side
151
+ def letter
152
+ @piece.letter
86
153
  end
87
154
 
88
- # Check if the piece has foreign style
155
+ # Get the prefix representation (inherited from PIN logic)
89
156
  #
90
- # @return [Boolean] true if foreign style
91
- def foreign?
92
- !native?
157
+ # @return [String] prefix representing the state
158
+ def prefix
159
+ @piece.prefix
160
+ end
161
+
162
+ # Get the suffix representation
163
+ #
164
+ # @return [String] suffix representing the derivation
165
+ def suffix
166
+ native? ? NATIVE_SUFFIX : FOREIGN_SUFFIX
167
+ end
168
+
169
+ # Create a new piece with enhanced state
170
+ #
171
+ # @return [Piece] new piece instance with enhanced state
172
+ # @example
173
+ # piece.enhance # (:K, :first, :normal, true) => (:K, :first, :enhanced, true)
174
+ def enhance
175
+ return self if enhanced?
176
+
177
+ self.class.new(type, side, Pin::Piece::ENHANCED_STATE, native)
178
+ end
179
+
180
+ # Create a new piece without enhanced state
181
+ #
182
+ # @return [Piece] new piece instance without enhanced state
183
+ # @example
184
+ # piece.unenhance # (:K, :first, :enhanced, true) => (:K, :first, :normal, true)
185
+ def unenhance
186
+ return self unless enhanced?
187
+
188
+ self.class.new(type, side, Pin::Piece::NORMAL_STATE, native)
93
189
  end
94
190
 
95
- # Create a new piece with native style
191
+ # Create a new piece with diminished state
192
+ #
193
+ # @return [Piece] new piece instance with diminished state
194
+ # @example
195
+ # piece.diminish # (:K, :first, :normal, true) => (:K, :first, :diminished, true)
196
+ def diminish
197
+ return self if diminished?
198
+
199
+ self.class.new(type, side, Pin::Piece::DIMINISHED_STATE, native)
200
+ end
201
+
202
+ # Create a new piece without diminished state
203
+ #
204
+ # @return [Piece] new piece instance without diminished state
205
+ # @example
206
+ # piece.undiminish # (:K, :first, :diminished, true) => (:K, :first, :normal, true)
207
+ def undiminish
208
+ return self unless diminished?
209
+
210
+ self.class.new(type, side, Pin::Piece::NORMAL_STATE, native)
211
+ end
212
+
213
+ # Create a new piece with normal state (no modifiers)
214
+ #
215
+ # @return [Piece] new piece instance with normal state
216
+ # @example
217
+ # piece.normalize # (:K, :first, :enhanced, true) => (:K, :first, :normal, true)
218
+ def normalize
219
+ return self if normal?
220
+
221
+ self.class.new(type, side, Pin::Piece::NORMAL_STATE, native)
222
+ end
223
+
224
+ # Create a new piece with opposite side
225
+ #
226
+ # @return [Piece] new piece instance with opposite side
227
+ # @example
228
+ # piece.flip # (:K, :first, :normal, true) => (:K, :second, :normal, true)
229
+ def flip
230
+ self.class.new(type, opposite_side, state, native)
231
+ end
232
+
233
+ # Create a new piece with foreign style (derivation marker)
234
+ #
235
+ # @return [Piece] new piece instance with foreign style
236
+ # @example
237
+ # piece.derive # (:K, :first, :normal, true) => (:K, :first, :normal, false)
238
+ def derive
239
+ return self if derived?
240
+
241
+ self.class.new(type, side, state, FOREIGN)
242
+ end
243
+
244
+ # Create a new piece with native style (no derivation marker)
96
245
  #
97
246
  # @return [Piece] new piece instance with native style
98
247
  # @example
99
- # piece.nativize # k' => k
100
- def nativize
248
+ # piece.underive # (:K, :first, :normal, false) => (:K, :first, :normal, true)
249
+ def underive
101
250
  return self if native?
102
251
 
103
- self.class.new(
104
- letter,
105
- native: true,
106
- enhanced: enhanced?,
107
- diminished: diminished?
108
- )
252
+ self.class.new(type, side, state, NATIVE)
109
253
  end
110
254
 
111
- # Create a new piece with foreign style
255
+ # Create a new piece with a different type (keeping same side, state, and derivation)
112
256
  #
113
- # @return [Piece] new piece instance with foreign style
257
+ # @param new_type [Symbol] new type (:A to :Z)
258
+ # @return [Piece] new piece instance with different type
114
259
  # @example
115
- # piece.foreignize # k => k'
116
- def foreignize
117
- return self if foreign?
260
+ # piece.with_type(:Q) # (:K, :first, :normal, true) => (:Q, :first, :normal, true)
261
+ def with_type(new_type)
262
+ Pin::Piece.validate_type(new_type)
263
+ return self if type == new_type
118
264
 
119
- self.class.new(
120
- letter,
121
- native: false,
122
- enhanced: enhanced?,
123
- diminished: diminished?
124
- )
265
+ self.class.new(new_type, side, state, native)
125
266
  end
126
267
 
127
- # Create a new piece with toggled style
268
+ # Create a new piece with a different side (keeping same type, state, and derivation)
128
269
  #
129
- # @return [Piece] new piece instance with opposite style
270
+ # @param new_side [Symbol] :first or :second
271
+ # @return [Piece] new piece instance with different side
130
272
  # @example
131
- # piece.toggle_style # k => k', k' => k
132
- def toggle_style
133
- native? ? foreignize : nativize
273
+ # piece.with_side(:second) # (:K, :first, :normal, true) => (:K, :second, :normal, true)
274
+ def with_side(new_side)
275
+ Pin::Piece.validate_side(new_side)
276
+ return self if side == new_side
277
+
278
+ self.class.new(type, new_side, state, native)
134
279
  end
135
280
 
136
- # Override parent methods to maintain PNN type in return values
281
+ # Create a new piece with a different state (keeping same type, side, and derivation)
282
+ #
283
+ # @param new_state [Symbol] :normal, :enhanced, or :diminished
284
+ # @return [Piece] new piece instance with different state
285
+ # @example
286
+ # piece.with_state(:enhanced) # (:K, :first, :normal, true) => (:K, :first, :enhanced, true)
287
+ def with_state(new_state)
288
+ Pin::Piece.validate_state(new_state)
289
+ return self if state == new_state
137
290
 
138
- def enhance
139
- return self if enhanced?
291
+ self.class.new(type, side, new_state, native)
292
+ end
140
293
 
141
- self.class.new(
142
- letter,
143
- native: native?,
144
- enhanced: true,
145
- diminished: false
146
- )
294
+ # Create a new piece with a different derivation (keeping same type, side, and state)
295
+ #
296
+ # @param new_native [Boolean] true for native, false for foreign
297
+ # @return [Piece] new piece instance with different derivation
298
+ # @example
299
+ # piece.with_derivation(false) # (:K, :first, :normal, true) => (:K, :first, :normal, false)
300
+ def with_derivation(new_native)
301
+ self.class.validate_derivation(new_native)
302
+ return self if native == new_native
303
+
304
+ self.class.new(type, side, state, new_native)
147
305
  end
148
306
 
149
- def unenhance
150
- return self unless enhanced?
307
+ # Check if the piece has enhanced state
308
+ #
309
+ # @return [Boolean] true if enhanced
310
+ def enhanced?
311
+ @piece.enhanced?
312
+ end
151
313
 
152
- self.class.new(
153
- letter,
154
- native: native?,
155
- enhanced: false,
156
- diminished: diminished?
157
- )
314
+ # Check if the piece has diminished state
315
+ #
316
+ # @return [Boolean] true if diminished
317
+ def diminished?
318
+ @piece.diminished?
158
319
  end
159
320
 
160
- def diminish
161
- return self if diminished?
321
+ # Check if the piece has normal state (no modifiers)
322
+ #
323
+ # @return [Boolean] true if no modifiers are present
324
+ def normal?
325
+ @piece.normal?
326
+ end
162
327
 
163
- self.class.new(
164
- letter,
165
- native: native?,
166
- enhanced: false,
167
- diminished: true
168
- )
328
+ # Check if the piece belongs to the first player
329
+ #
330
+ # @return [Boolean] true if first player
331
+ def first_player?
332
+ @piece.first_player?
169
333
  end
170
334
 
171
- def undiminish
172
- return self unless diminished?
335
+ # Check if the piece belongs to the second player
336
+ #
337
+ # @return [Boolean] true if second player
338
+ def second_player?
339
+ @piece.second_player?
340
+ end
173
341
 
174
- self.class.new(
175
- letter,
176
- native: native?,
177
- enhanced: enhanced?,
178
- diminished: false
179
- )
342
+ # Check if the piece has native style (no derivation marker)
343
+ #
344
+ # @return [Boolean] true if native style
345
+ def native?
346
+ native == NATIVE
180
347
  end
181
348
 
182
- def normalize
183
- return self if normal?
349
+ # Check if the piece has foreign style (derivation marker)
350
+ #
351
+ # @return [Boolean] true if foreign style
352
+ def derived?
353
+ native == FOREIGN
354
+ end
184
355
 
185
- self.class.new(
186
- letter,
187
- native: native?
188
- )
356
+ # Alias for derived? to match the specification terminology
357
+ alias foreign? derived?
358
+
359
+ # Check if this piece is the same type as another (ignoring side, state, and derivation)
360
+ #
361
+ # @param other [Piece] piece to compare with
362
+ # @return [Boolean] true if same type
363
+ # @example
364
+ # king1.same_type?(king2) # (:K, :first, :normal, true) and (:K, :second, :enhanced, false) => true
365
+ def same_type?(other)
366
+ return false unless other.is_a?(self.class)
367
+
368
+ @piece.same_type?(other.instance_variable_get(:@piece))
189
369
  end
190
370
 
191
- def flip
192
- flipped_letter = letter.swapcase
371
+ # Check if this piece belongs to the same side as another
372
+ #
373
+ # @param other [Piece] piece to compare with
374
+ # @return [Boolean] true if same side
375
+ def same_side?(other)
376
+ return false unless other.is_a?(self.class)
377
+
378
+ @piece.same_side?(other.instance_variable_get(:@piece))
379
+ end
380
+
381
+ # Check if this piece has the same state as another
382
+ #
383
+ # @param other [Piece] piece to compare with
384
+ # @return [Boolean] true if same state
385
+ def same_state?(other)
386
+ return false unless other.is_a?(self.class)
193
387
 
194
- self.class.new(
195
- flipped_letter,
196
- native: native?,
197
- enhanced: enhanced?,
198
- diminished: diminished?
199
- )
388
+ @piece.same_state?(other.instance_variable_get(:@piece))
200
389
  end
201
390
 
202
- # Custom equality comparison including style
391
+ # Check if this piece has the same style derivation as another
392
+ #
393
+ # @param other [Piece] piece to compare with
394
+ # @return [Boolean] true if same style derivation
395
+ def same_style?(other)
396
+ return false unless other.is_a?(self.class)
397
+
398
+ native == other.native
399
+ end
400
+
401
+ # Custom equality comparison
203
402
  #
204
403
  # @param other [Object] object to compare with
205
404
  # @return [Boolean] true if pieces are equal
206
405
  def ==(other)
207
406
  return false unless other.is_a?(self.class)
208
407
 
209
- super && native? == other.native?
408
+ @piece == other.instance_variable_get(:@piece) && native == other.native
210
409
  end
211
410
 
212
- # Custom hash implementation including style
411
+ # Alias for == to ensure Set functionality works correctly
412
+ alias eql? ==
413
+
414
+ # Custom hash implementation for use in collections
213
415
  #
214
416
  # @return [Integer] hash value
215
417
  def hash
216
- [super, @native].hash
418
+ [self.class, @piece, native].hash
217
419
  end
218
420
 
219
- # Match PNN pattern against string
421
+ # Validate that the derivation is a valid boolean
220
422
  #
221
- # @param string [String] string to match
222
- # @return [MatchData] match data
223
- # @raise [ArgumentError] if string doesn't match
224
- def self.match_pattern(string)
225
- matches = PNN_PATTERN.match(string)
226
- return matches if matches
423
+ # @param derivation [Boolean] the derivation to validate
424
+ # @raise [ArgumentError] if invalid
425
+ def self.validate_derivation(derivation)
426
+ return if VALID_DERIVATIONS.include?(derivation)
227
427
 
228
- raise ::ArgumentError, format(ERROR_INVALID_PNN, string)
428
+ raise ::ArgumentError, format(ERROR_INVALID_DERIVATION, derivation.inspect)
229
429
  end
230
430
 
231
- private_class_method :match_pattern
431
+ private
432
+
433
+ # Get the opposite side of the current piece
434
+ #
435
+ # @return [Symbol] :first if current side is :second, :second if current side is :first
436
+ def opposite_side
437
+ @piece.send(:opposite_side)
438
+ end
232
439
  end
233
440
  end
234
441
  end
data/lib/sashite/pnn.rb CHANGED
@@ -5,43 +5,36 @@ require_relative "pnn/piece"
5
5
  module Sashite
6
6
  # PNN (Piece Name Notation) implementation for Ruby
7
7
  #
8
- # Extends PIN to provide style-aware piece representation in abstract strategy board games.
9
- # PNN adds a derivation marker that distinguishes pieces by their style origin, enabling
10
- # cross-style game scenarios and piece origin tracking.
8
+ # Provides style-aware ASCII-based format for representing pieces in abstract strategy board games.
9
+ # PNN extends PIN by adding derivation markers that distinguish pieces by their style origin,
10
+ # enabling cross-style game scenarios and piece origin tracking.
11
11
  #
12
- # Format: <pin>[<suffix>]
13
- # - PIN component: [<state>]<letter> (state modifier + letter)
14
- # - Suffix: "'" for foreign style, none for native style
12
+ # Format: [<state>]<letter>[<derivation>]
13
+ # - State modifier: "+" (enhanced), "-" (diminished), or none (normal)
14
+ # - Letter: A-Z (first player), a-z (second player)
15
+ # - Derivation marker: "'" (foreign style), or none (native style)
15
16
  #
16
17
  # Examples:
17
- # "K" - First player king (native style)
18
- # "K'" - First player king (foreign style)
19
- # "+R" - First player rook, enhanced state (native style)
20
- # "+R'" - First player rook, enhanced state (foreign style)
21
- # "-p'" - Second player pawn, diminished state (foreign style)
18
+ # "K" - First player king (native style, normal state)
19
+ # "k'" - Second player king (foreign style, normal state)
20
+ # "+R'" - First player rook (foreign style, enhanced state)
21
+ # "-p" - Second player pawn (native style, diminished state)
22
22
  #
23
23
  # See: https://sashite.dev/specs/pnn/1.0.0/
24
24
  module Pnn
25
- # Regular expression for PNN validation
26
- # Matches: optional state modifier, letter, optional derivation marker
27
- PNN_REGEX = /\A[-+]?[A-Za-z]'?\z/
28
-
29
25
  # Check if a string is a valid PNN notation
30
26
  #
31
- # @param pnn [String] The string to validate
27
+ # @param pnn_string [String] The string to validate
32
28
  # @return [Boolean] true if valid PNN, false otherwise
33
29
  #
34
30
  # @example
35
- # Sashite::Pnn.valid?("K") # => true
36
- # Sashite::Pnn.valid?("K'") # => true
37
- # Sashite::Pnn.valid?("+R'") # => true
38
- # Sashite::Pnn.valid?("-p'") # => true
39
- # Sashite::Pnn.valid?("K''") # => false
40
- # Sashite::Pnn.valid?("++K'") # => false
41
- def self.valid?(pnn)
42
- return false unless pnn.is_a?(::String)
43
-
44
- pnn.match?(PNN_REGEX)
31
+ # Sashite::Pnn.valid?("K") # => true
32
+ # Sashite::Pnn.valid?("+R'") # => true
33
+ # Sashite::Pnn.valid?("-p") # => true
34
+ # Sashite::Pnn.valid?("KK") # => false
35
+ # Sashite::Pnn.valid?("++K") # => false
36
+ def self.valid?(pnn_string)
37
+ Piece.valid?(pnn_string)
45
38
  end
46
39
 
47
40
  # Parse a PNN string into a Piece object
@@ -50,11 +43,27 @@ module Sashite
50
43
  # @return [Pnn::Piece] new piece instance
51
44
  # @raise [ArgumentError] if the PNN string is invalid
52
45
  # @example
53
- # Sashite::Pnn.parse("K") # => #<Pnn::Piece letter="K" native=true>
54
- # Sashite::Pnn.parse("K'") # => #<Pnn::Piece letter="K" native=false>
55
- # Sashite::Pnn.parse("+R'") # => #<Pnn::Piece letter="R" enhanced=true native=false>
46
+ # Sashite::Pnn.parse("K") # => #<Pnn::Piece type=:K side=:first state=:normal native=true>
47
+ # Sashite::Pnn.parse("+R'") # => #<Pnn::Piece type=:R side=:first state=:enhanced native=false>
48
+ # Sashite::Pnn.parse("-p") # => #<Pnn::Piece type=:P side=:second state=:diminished native=true>
56
49
  def self.parse(pnn_string)
57
50
  Piece.parse(pnn_string)
58
51
  end
52
+
53
+ # Create a new piece instance
54
+ #
55
+ # @param type [Symbol] piece type (:A to :Z)
56
+ # @param side [Symbol] player side (:first or :second)
57
+ # @param state [Symbol] piece state (:normal, :enhanced, or :diminished)
58
+ # @param native [Boolean] style derivation (true for native, false for foreign)
59
+ # @return [Pnn::Piece] new piece instance
60
+ # @raise [ArgumentError] if parameters are invalid
61
+ # @example
62
+ # Sashite::Pnn.piece(:K, :first, :normal, true) # => #<Pnn::Piece type=:K side=:first state=:normal native=true>
63
+ # Sashite::Pnn.piece(:R, :first, :enhanced, false) # => #<Pnn::Piece type=:R side=:first state=:enhanced native=false>
64
+ # Sashite::Pnn.piece(:P, :second, :diminished, true) # => #<Pnn::Piece type=:P side=:second state=:diminished native=true>
65
+ def self.piece(type, side, state = Sashite::Pin::Piece::NORMAL_STATE, native = Piece::NATIVE)
66
+ Piece.new(type, side, state, native)
67
+ end
59
68
  end
60
69
  end