sashite-pnn 2.0.0 → 3.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,441 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "sashite/pin"
4
-
5
- module Sashite
6
- module Pnn
7
- # Represents a piece in PNN (Piece Name Notation) format.
8
- #
9
- # 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)
12
- #
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
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
34
-
35
- # Error messages
36
- ERROR_INVALID_PNN = "Invalid PNN string: %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
53
-
54
- # @return [Boolean] the style derivation (true for native, false for foreign)
55
- attr_reader :native
56
-
57
- # Create a new piece instance
58
- #
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)
63
- # @raise [ArgumentError] if parameters are invalid
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)
75
- @native = native
76
-
77
- freeze
78
- end
79
-
80
- # Parse a PNN string into a Piece object
81
- #
82
- # @param pnn_string [String] PNN notation string
83
- # @return [Piece] new piece instance
84
- # @raise [ArgumentError] if the PNN string is invalid
85
- # @example
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>
89
- def self.parse(pnn_string)
90
- string_value = String(pnn_string)
91
-
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
106
-
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)
135
- end
136
-
137
- # Convert the piece to its PNN string representation
138
- #
139
- # @return [String] PNN notation string
140
- # @example
141
- # piece.to_s # => "+R'"
142
- # piece.to_s # => "-p"
143
- # piece.to_s # => "K"
144
- def to_s
145
- "#{prefix}#{letter}#{suffix}"
146
- end
147
-
148
- # Get the letter representation (inherited from PIN logic)
149
- #
150
- # @return [String] letter representation combining type and side
151
- def letter
152
- @piece.letter
153
- end
154
-
155
- # Get the prefix representation (inherited from PIN logic)
156
- #
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)
189
- end
190
-
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)
245
- #
246
- # @return [Piece] new piece instance with native style
247
- # @example
248
- # piece.underive # (:K, :first, :normal, false) => (:K, :first, :normal, true)
249
- def underive
250
- return self if native?
251
-
252
- self.class.new(type, side, state, NATIVE)
253
- end
254
-
255
- # Create a new piece with a different type (keeping same side, state, and derivation)
256
- #
257
- # @param new_type [Symbol] new type (:A to :Z)
258
- # @return [Piece] new piece instance with different type
259
- # @example
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
264
-
265
- self.class.new(new_type, side, state, native)
266
- end
267
-
268
- # Create a new piece with a different side (keeping same type, state, and derivation)
269
- #
270
- # @param new_side [Symbol] :first or :second
271
- # @return [Piece] new piece instance with different side
272
- # @example
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)
279
- end
280
-
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
290
-
291
- self.class.new(type, side, new_state, native)
292
- end
293
-
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)
305
- end
306
-
307
- # Check if the piece has enhanced state
308
- #
309
- # @return [Boolean] true if enhanced
310
- def enhanced?
311
- @piece.enhanced?
312
- end
313
-
314
- # Check if the piece has diminished state
315
- #
316
- # @return [Boolean] true if diminished
317
- def diminished?
318
- @piece.diminished?
319
- end
320
-
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
327
-
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?
333
- end
334
-
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
341
-
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
347
- end
348
-
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
355
-
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))
369
- end
370
-
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)
387
-
388
- @piece.same_state?(other.instance_variable_get(:@piece))
389
- end
390
-
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
402
- #
403
- # @param other [Object] object to compare with
404
- # @return [Boolean] true if pieces are equal
405
- def ==(other)
406
- return false unless other.is_a?(self.class)
407
-
408
- @piece == other.instance_variable_get(:@piece) && native == other.native
409
- end
410
-
411
- # Alias for == to ensure Set functionality works correctly
412
- alias eql? ==
413
-
414
- # Custom hash implementation for use in collections
415
- #
416
- # @return [Integer] hash value
417
- def hash
418
- [self.class, @piece, native].hash
419
- end
420
-
421
- # Validate that the derivation is a valid boolean
422
- #
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)
427
-
428
- raise ::ArgumentError, format(ERROR_INVALID_DERIVATION, derivation.inspect)
429
- end
430
-
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
439
- end
440
- end
441
- end