sashite-pin 3.2.0 → 3.3.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.
data/lib/sashite/pin.rb CHANGED
@@ -1,68 +1,549 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative "pin/identifier"
4
-
5
3
  module Sashite
6
- # PIN (Piece Identifier Notation) implementation for Ruby
4
+ # PIN (Piece Identifier Notation) implementation for Ruby.
5
+ #
6
+ # PIN provides an ASCII-based format for representing pieces in abstract strategy
7
+ # board games. It translates piece attributes from the Game Protocol into a compact,
8
+ # portable notation system.
9
+ #
10
+ # == Format
11
+ #
12
+ # [<state-modifier>]<letter>[<terminal-marker>]
13
+ #
14
+ # - *Letter* (+A-Z+, +a-z+): Piece type and side
15
+ # - *State modifier*: <tt>+</tt> (enhanced), <tt>-</tt> (diminished), or none (normal)
16
+ # - *Terminal marker*: <tt>^</tt> (terminal piece) or none
17
+ #
18
+ # == Attributes
19
+ #
20
+ # A PIN token encodes exactly these attributes:
7
21
  #
8
- # Provides ASCII-based format for representing pieces in abstract strategy board games.
9
- # PIN translates piece attributes from the Game Protocol into a compact, portable notation system.
22
+ # - *Piece Name* → one ASCII letter chosen by the Game / Rule System
23
+ # - *Piece Side* the case of that letter (uppercase = first, lowercase = second)
24
+ # - *Piece State* → an optional prefix (<tt>+</tt> for enhanced, <tt>-</tt> for diminished)
25
+ # - *Terminal status* → an optional suffix (<tt>^</tt>)
10
26
  #
11
- # Format: [<state>]<letter>
12
- # - State modifier: "+" (enhanced), "-" (diminished), or none (normal)
13
- # - Letter: A-Z (first player), a-z (second player)
27
+ # == Examples
14
28
  #
15
- # Examples:
16
- # "K" - First player king (normal state)
17
- # "k" - Second player king (normal state)
18
- # "+R" - First player rook (enhanced state)
19
- # "-p" - Second player pawn (diminished state)
29
+ # pin = Sashite::Pin.parse("K")
30
+ # pin.type # => :K
31
+ # pin.side # => :first
32
+ # pin.state # => :normal
33
+ # pin.terminal # => false
20
34
  #
21
- # @see https://sashite.dev/specs/pin/1.0.0/
22
- module Pin
23
- # Check if a string is a valid PIN notation
35
+ # pin = Sashite::Pin.parse!("+R")
36
+ # pin.to_s # => "+R"
37
+ #
38
+ # pin = Sashite::Pin.parse("k^")
39
+ # pin.terminal # => true
40
+ #
41
+ # Sashite::Pin.valid?("K^") # => true
42
+ # Sashite::Pin.valid?("invalid") # => false
43
+ #
44
+ # See the PIN Specification (https://sashite.dev/specs/pin/1.0.0/) for details.
45
+ class Pin
46
+ # Valid piece types (uppercase symbols)
47
+ VALID_TYPES = %i[A B C D E F G H I J K L M N O P Q R S T U V W X Y Z].freeze
48
+
49
+ # Valid sides
50
+ VALID_SIDES = %i[first second].freeze
51
+
52
+ # Valid states
53
+ VALID_STATES = %i[normal enhanced diminished].freeze
54
+
55
+ # Pattern for validating PIN strings
56
+ PIN_PATTERN = /\A(?<prefix>[-+])?(?<letter>[a-zA-Z])(?<terminal>\^)?\z/
57
+
58
+ # @return [Symbol] Piece type (:A to :Z, always uppercase)
59
+ attr_reader :type
60
+
61
+ # @return [Symbol] Player side (:first or :second)
62
+ attr_reader :side
63
+
64
+ # @return [Symbol] Piece state (:normal, :enhanced, or :diminished)
65
+ attr_reader :state
66
+
67
+ # @return [Boolean] Terminal status
68
+ attr_reader :terminal
69
+
70
+ # ========================================================================
71
+ # Creation and Parsing
72
+ # ========================================================================
73
+
74
+ # Creates a new PIN instance.
75
+ #
76
+ # @param type [Symbol] Piece type (:A to :Z)
77
+ # @param side [Symbol] Player side (:first or :second)
78
+ # @param state [Symbol] Piece state (:normal, :enhanced, or :diminished)
79
+ # @param terminal [Boolean] Terminal status
80
+ # @return [Pin] A new frozen Pin instance
81
+ #
82
+ # @example
83
+ # Sashite::Pin.new(:K, :first)
84
+ # # => #<Sashite::Pin K>
85
+ #
86
+ # Sashite::Pin.new(:R, :second, :enhanced)
87
+ # # => #<Sashite::Pin +r>
88
+ #
89
+ # Sashite::Pin.new(:K, :first, :normal, terminal: true)
90
+ # # => #<Sashite::Pin K^>
91
+ def initialize(type, side, state = :normal, terminal: false)
92
+ validate_type!(type)
93
+ validate_side!(side)
94
+ validate_state!(state)
95
+
96
+ @type = type
97
+ @side = side
98
+ @state = state
99
+ @terminal = !!terminal
100
+
101
+ freeze
102
+ end
103
+
104
+ # Parses a PIN string into a Pin instance.
105
+ #
106
+ # @param pin_string [String] The PIN string to parse
107
+ # @return [Pin] A new Pin instance
108
+ # @raise [ArgumentError] If the string is not a valid PIN
109
+ #
110
+ # @example
111
+ # Sashite::Pin.parse("K")
112
+ # # => #<Sashite::Pin K>
113
+ #
114
+ # Sashite::Pin.parse("+r")
115
+ # # => #<Sashite::Pin +r>
116
+ #
117
+ # Sashite::Pin.parse("K^")
118
+ # # => #<Sashite::Pin K^>
119
+ #
120
+ # Sashite::Pin.parse("invalid")
121
+ # # => ArgumentError: Invalid PIN string: invalid
122
+ def self.parse(pin_string)
123
+ raise ArgumentError, "Invalid PIN string: #{pin_string.inspect}" unless pin_string.is_a?(String)
124
+
125
+ match = PIN_PATTERN.match(pin_string)
126
+ raise ArgumentError, "Invalid PIN string: #{pin_string}" unless match
127
+
128
+ letter = match[:letter]
129
+ prefix = match[:prefix]
130
+ terminal_marker = match[:terminal]
131
+
132
+ type = letter.upcase.to_sym
133
+ side = letter == letter.upcase ? :first : :second
134
+
135
+ state = case prefix
136
+ when "+" then :enhanced
137
+ when "-" then :diminished
138
+ else :normal
139
+ end
140
+
141
+ terminal = terminal_marker == "^"
142
+
143
+ new(type, side, state, terminal: terminal)
144
+ end
145
+
146
+ # Checks if a string is a valid PIN notation.
24
147
  #
25
148
  # @param pin_string [String] The string to validate
26
- # @return [Boolean] true if valid PIN, false otherwise
149
+ # @return [Boolean] true if valid, false otherwise
27
150
  #
28
151
  # @example
29
152
  # Sashite::Pin.valid?("K") # => true
30
153
  # Sashite::Pin.valid?("+R") # => true
31
- # Sashite::Pin.valid?("-p") # => true
32
- # Sashite::Pin.valid?("KK") # => false
33
- # Sashite::Pin.valid?("++K") # => false
154
+ # Sashite::Pin.valid?("K^") # => true
155
+ # Sashite::Pin.valid?("invalid") # => false
34
156
  def self.valid?(pin_string)
35
- Identifier.valid?(pin_string)
157
+ return false unless pin_string.is_a?(String)
158
+
159
+ PIN_PATTERN.match?(pin_string)
36
160
  end
37
161
 
38
- # Parse a PIN string into an Identifier object
162
+ # ========================================================================
163
+ # Conversion
164
+ # ========================================================================
165
+
166
+ # Converts the Pin to its string representation.
167
+ #
168
+ # @return [String] The PIN string
39
169
  #
40
- # @param pin_string [String] PIN notation string
41
- # @return [Pin::Identifier] new identifier instance
42
- # @raise [ArgumentError] if the PIN string is invalid
43
170
  # @example
44
- # Sashite::Pin.parse("K") # => #<Pin::Identifier type=:K side=:first state=:normal>
45
- # Sashite::Pin.parse("+R") # => #<Pin::Identifier type=:R side=:first state=:enhanced>
46
- # Sashite::Pin.parse("-p") # => #<Pin::Identifier type=:P side=:second state=:diminished>
47
- def self.parse(pin_string)
48
- Identifier.parse(pin_string)
171
+ # Sashite::Pin.new(:K, :first).to_s # => "K"
172
+ # Sashite::Pin.new(:R, :second, :enhanced).to_s # => "+r"
173
+ # Sashite::Pin.new(:K, :first, :normal, terminal: true).to_s # => "K^"
174
+ def to_s
175
+ "#{prefix}#{letter}#{suffix}"
49
176
  end
50
177
 
51
- # Create a new identifier instance
178
+ # Returns the letter representation of the PIN.
179
+ #
180
+ # @return [String] The letter (uppercase for first player, lowercase for second)
52
181
  #
53
- # @param type [Symbol] piece type (:A to :Z)
54
- # @param side [Symbol] player side (:first or :second)
55
- # @param state [Symbol] piece state (:normal, :enhanced, or :diminished)
56
- # @param terminal [Boolean] whether the piece is a terminal piece
57
- # @return [Pin::Identifier] new identifier instance
58
- # @raise [ArgumentError] if parameters are invalid
59
182
  # @example
60
- # Sashite::Pin.identifier(:K, :first, :normal) # => #<Pin::Identifier type=:K side=:first state=:normal terminal=false>
61
- # Sashite::Pin.identifier(:R, :first, :enhanced) # => #<Pin::Identifier type=:R side=:first state=:enhanced terminal=false>
62
- # Sashite::Pin.identifier(:P, :second, :diminished) # => #<Pin::Identifier type=:P side=:second state=:diminished terminal=false>
63
- # Sashite::Pin.identifier(:K, :first, :normal, terminal: true) # => #<Pin::Identifier type=:K side=:first state=:normal terminal=true>
64
- def self.identifier(type, side, state, terminal: false)
65
- Identifier.new(type, side, state, terminal: terminal)
183
+ # Sashite::Pin.new(:K, :first).letter # => "K"
184
+ # Sashite::Pin.new(:K, :second).letter # => "k"
185
+ def letter
186
+ case side
187
+ when :first then type.to_s
188
+ when :second then type.to_s.downcase
189
+ end
190
+ end
191
+
192
+ # Returns the state prefix of the PIN.
193
+ #
194
+ # @return [String] "+" for enhanced, "-" for diminished, "" for normal
195
+ #
196
+ # @example
197
+ # Sashite::Pin.new(:K, :first, :enhanced).prefix # => "+"
198
+ # Sashite::Pin.new(:K, :first, :diminished).prefix # => "-"
199
+ # Sashite::Pin.new(:K, :first, :normal).prefix # => ""
200
+ def prefix
201
+ case state
202
+ when :enhanced then "+"
203
+ when :diminished then "-"
204
+ else ""
205
+ end
206
+ end
207
+
208
+ # Returns the terminal suffix of the PIN.
209
+ #
210
+ # @return [String] "^" if terminal, "" otherwise
211
+ #
212
+ # @example
213
+ # Sashite::Pin.new(:K, :first, :normal, terminal: true).suffix # => "^"
214
+ # Sashite::Pin.new(:K, :first).suffix # => ""
215
+ def suffix
216
+ terminal ? "^" : ""
217
+ end
218
+
219
+ # ========================================================================
220
+ # State Transformations
221
+ # ========================================================================
222
+
223
+ # Returns a new Pin with enhanced state.
224
+ #
225
+ # @return [Pin] A new Pin with :enhanced state
226
+ #
227
+ # @example
228
+ # pin = Sashite::Pin.new(:K, :first)
229
+ # pin.enhance.state # => :enhanced
230
+ def enhance
231
+ return self if state == :enhanced
232
+
233
+ self.class.new(type, side, :enhanced, terminal: terminal)
234
+ end
235
+
236
+ # Returns a new Pin with diminished state.
237
+ #
238
+ # @return [Pin] A new Pin with :diminished state
239
+ #
240
+ # @example
241
+ # pin = Sashite::Pin.new(:K, :first)
242
+ # pin.diminish.state # => :diminished
243
+ def diminish
244
+ return self if state == :diminished
245
+
246
+ self.class.new(type, side, :diminished, terminal: terminal)
247
+ end
248
+
249
+ # Returns a new Pin with normal state.
250
+ #
251
+ # @return [Pin] A new Pin with :normal state
252
+ #
253
+ # @example
254
+ # pin = Sashite::Pin.new(:K, :first, :enhanced)
255
+ # pin.normalize.state # => :normal
256
+ def normalize
257
+ return self if state == :normal
258
+
259
+ self.class.new(type, side, :normal, terminal: terminal)
260
+ end
261
+
262
+ # ========================================================================
263
+ # Side Transformations
264
+ # ========================================================================
265
+
266
+ # Returns a new Pin with the opposite side.
267
+ #
268
+ # @return [Pin] A new Pin with flipped side
269
+ #
270
+ # @example
271
+ # pin = Sashite::Pin.new(:K, :first)
272
+ # pin.flip.side # => :second
273
+ def flip
274
+ new_side = side == :first ? :second : :first
275
+ self.class.new(type, new_side, state, terminal: terminal)
276
+ end
277
+
278
+ # ========================================================================
279
+ # Terminal Transformations
280
+ # ========================================================================
281
+
282
+ # Returns a new Pin marked as terminal.
283
+ #
284
+ # @return [Pin] A new Pin with terminal: true
285
+ #
286
+ # @example
287
+ # pin = Sashite::Pin.new(:K, :first)
288
+ # pin.mark_terminal.terminal # => true
289
+ def mark_terminal
290
+ return self if terminal
291
+
292
+ self.class.new(type, side, state, terminal: true)
293
+ end
294
+
295
+ # Returns a new Pin unmarked as terminal.
296
+ #
297
+ # @return [Pin] A new Pin with terminal: false
298
+ #
299
+ # @example
300
+ # pin = Sashite::Pin.new(:K, :first, :normal, terminal: true)
301
+ # pin.unmark_terminal.terminal # => false
302
+ def unmark_terminal
303
+ return self unless terminal
304
+
305
+ self.class.new(type, side, state, terminal: false)
306
+ end
307
+
308
+ # ========================================================================
309
+ # Attribute Transformations
310
+ # ========================================================================
311
+
312
+ # Returns a new Pin with a different type.
313
+ #
314
+ # @param new_type [Symbol] The new piece type (:A to :Z)
315
+ # @return [Pin] A new Pin with the specified type
316
+ #
317
+ # @example
318
+ # pin = Sashite::Pin.new(:K, :first)
319
+ # pin.with_type(:Q).type # => :Q
320
+ def with_type(new_type)
321
+ return self if type == new_type
322
+
323
+ self.class.new(new_type, side, state, terminal: terminal)
324
+ end
325
+
326
+ # Returns a new Pin with a different side.
327
+ #
328
+ # @param new_side [Symbol] The new side (:first or :second)
329
+ # @return [Pin] A new Pin with the specified side
330
+ #
331
+ # @example
332
+ # pin = Sashite::Pin.new(:K, :first)
333
+ # pin.with_side(:second).side # => :second
334
+ def with_side(new_side)
335
+ return self if side == new_side
336
+
337
+ self.class.new(type, new_side, state, terminal: terminal)
338
+ end
339
+
340
+ # Returns a new Pin with a different state.
341
+ #
342
+ # @param new_state [Symbol] The new state (:normal, :enhanced, or :diminished)
343
+ # @return [Pin] A new Pin with the specified state
344
+ #
345
+ # @example
346
+ # pin = Sashite::Pin.new(:K, :first)
347
+ # pin.with_state(:enhanced).state # => :enhanced
348
+ def with_state(new_state)
349
+ return self if state == new_state
350
+
351
+ self.class.new(type, side, new_state, terminal: terminal)
352
+ end
353
+
354
+ # Returns a new Pin with a different terminal status.
355
+ #
356
+ # @param new_terminal [Boolean] The new terminal status
357
+ # @return [Pin] A new Pin with the specified terminal status
358
+ #
359
+ # @example
360
+ # pin = Sashite::Pin.new(:K, :first)
361
+ # pin.with_terminal(true).terminal # => true
362
+ def with_terminal(new_terminal)
363
+ return self if terminal == !!new_terminal
364
+
365
+ self.class.new(type, side, state, terminal: !!new_terminal)
366
+ end
367
+
368
+ # ========================================================================
369
+ # State Queries
370
+ # ========================================================================
371
+
372
+ # Checks if the Pin has normal state.
373
+ #
374
+ # @return [Boolean] true if normal state
375
+ #
376
+ # @example
377
+ # Sashite::Pin.new(:K, :first).normal? # => true
378
+ def normal?
379
+ state == :normal
380
+ end
381
+
382
+ # Checks if the Pin has enhanced state.
383
+ #
384
+ # @return [Boolean] true if enhanced state
385
+ #
386
+ # @example
387
+ # Sashite::Pin.new(:K, :first, :enhanced).enhanced? # => true
388
+ def enhanced?
389
+ state == :enhanced
390
+ end
391
+
392
+ # Checks if the Pin has diminished state.
393
+ #
394
+ # @return [Boolean] true if diminished state
395
+ #
396
+ # @example
397
+ # Sashite::Pin.new(:K, :first, :diminished).diminished? # => true
398
+ def diminished?
399
+ state == :diminished
400
+ end
401
+
402
+ # ========================================================================
403
+ # Side Queries
404
+ # ========================================================================
405
+
406
+ # Checks if the Pin belongs to the first player.
407
+ #
408
+ # @return [Boolean] true if first player
409
+ #
410
+ # @example
411
+ # Sashite::Pin.new(:K, :first).first_player? # => true
412
+ def first_player?
413
+ side == :first
414
+ end
415
+
416
+ # Checks if the Pin belongs to the second player.
417
+ #
418
+ # @return [Boolean] true if second player
419
+ #
420
+ # @example
421
+ # Sashite::Pin.new(:K, :second).second_player? # => true
422
+ def second_player?
423
+ side == :second
424
+ end
425
+
426
+ # ========================================================================
427
+ # Terminal Queries
428
+ # ========================================================================
429
+
430
+ # Checks if the Pin is a terminal piece.
431
+ #
432
+ # @return [Boolean] true if terminal
433
+ #
434
+ # @example
435
+ # Sashite::Pin.new(:K, :first, :normal, terminal: true).terminal? # => true
436
+ def terminal?
437
+ terminal
438
+ end
439
+
440
+ # ========================================================================
441
+ # Comparison
442
+ # ========================================================================
443
+
444
+ # Checks if two Pins have the same type.
445
+ #
446
+ # @param other [Pin] The other Pin to compare
447
+ # @return [Boolean] true if same type
448
+ #
449
+ # @example
450
+ # pin1 = Sashite::Pin.parse("K")
451
+ # pin2 = Sashite::Pin.parse("k")
452
+ # pin1.same_type?(pin2) # => true
453
+ def same_type?(other)
454
+ type == other.type
455
+ end
456
+
457
+ # Checks if two Pins have the same side.
458
+ #
459
+ # @param other [Pin] The other Pin to compare
460
+ # @return [Boolean] true if same side
461
+ #
462
+ # @example
463
+ # pin1 = Sashite::Pin.parse("K")
464
+ # pin2 = Sashite::Pin.parse("Q")
465
+ # pin1.same_side?(pin2) # => true
466
+ def same_side?(other)
467
+ side == other.side
468
+ end
469
+
470
+ # Checks if two Pins have the same state.
471
+ #
472
+ # @param other [Pin] The other Pin to compare
473
+ # @return [Boolean] true if same state
474
+ #
475
+ # @example
476
+ # pin1 = Sashite::Pin.parse("+K")
477
+ # pin2 = Sashite::Pin.parse("+Q")
478
+ # pin1.same_state?(pin2) # => true
479
+ def same_state?(other)
480
+ state == other.state
481
+ end
482
+
483
+ # Checks if two Pins have the same terminal status.
484
+ #
485
+ # @param other [Pin] The other Pin to compare
486
+ # @return [Boolean] true if same terminal status
487
+ #
488
+ # @example
489
+ # pin1 = Sashite::Pin.parse("K^")
490
+ # pin2 = Sashite::Pin.parse("Q^")
491
+ # pin1.same_terminal?(pin2) # => true
492
+ def same_terminal?(other)
493
+ terminal == other.terminal
494
+ end
495
+
496
+ # Checks equality with another Pin.
497
+ #
498
+ # @param other [Object] The object to compare
499
+ # @return [Boolean] true if equal
500
+ def ==(other)
501
+ return false unless other.is_a?(self.class)
502
+
503
+ type == other.type &&
504
+ side == other.side &&
505
+ state == other.state &&
506
+ terminal == other.terminal
507
+ end
508
+
509
+ alias eql? ==
510
+
511
+ # Returns a hash code for the Pin.
512
+ #
513
+ # @return [Integer] Hash code
514
+ def hash
515
+ [type, side, state, terminal].hash
516
+ end
517
+
518
+ # Returns an inspect string for the Pin.
519
+ #
520
+ # @return [String] Inspect representation
521
+ def inspect
522
+ "#<#{self.class} #{self}>"
523
+ end
524
+
525
+ private
526
+
527
+ # ========================================================================
528
+ # Private Validation
529
+ # ========================================================================
530
+
531
+ def validate_type!(type)
532
+ return if VALID_TYPES.include?(type)
533
+
534
+ raise ArgumentError, "Type must be a symbol from :A to :Z, got: #{type.inspect}"
535
+ end
536
+
537
+ def validate_side!(side)
538
+ return if VALID_SIDES.include?(side)
539
+
540
+ raise ArgumentError, "Side must be :first or :second, got: #{side.inspect}"
541
+ end
542
+
543
+ def validate_state!(state)
544
+ return if VALID_STATES.include?(state)
545
+
546
+ raise ArgumentError, "State must be :normal, :enhanced, or :diminished, got: #{state.inspect}"
66
547
  end
67
548
  end
68
549
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: sashite-pin
3
3
  version: !ruby/object:Gem::Version
4
- version: 3.2.0
4
+ version: 3.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Cyril Kato
@@ -10,14 +10,9 @@ cert_chain: []
10
10
  date: 1980-01-02 00:00:00.000000000 Z
11
11
  dependencies: []
12
12
  description: |
13
- PIN (Piece Identifier Notation) provides a rule-agnostic format for identifying pieces
14
- in abstract strategy board games. This gem implements the PIN Specification v1.0.0 with
15
- a modern Ruby interface featuring immutable identifier objects and functional programming
16
- principles. PIN uses single ASCII letters with optional state modifiers, terminal markers,
17
- and case-based side encoding (A-Z for first player, a-z for second player), enabling
18
- precise and portable identification of pieces across multiple games. Perfect for game
19
- engines, board game notation systems, and hybrid gaming platforms requiring compact,
20
- stateful piece representation.
13
+ PIN (Piece Identifier Notation) implementation for Ruby.
14
+ Provides a rule-agnostic format for identifying pieces in abstract strategy
15
+ board games with immutable identifier objects and functional programming principles.
21
16
  email: contact@cyril.email
22
17
  executables: []
23
18
  extensions: []
@@ -27,7 +22,6 @@ files:
27
22
  - README.md
28
23
  - lib/sashite-pin.rb
29
24
  - lib/sashite/pin.rb
30
- - lib/sashite/pin/identifier.rb
31
25
  homepage: https://github.com/sashite/pin.rb
32
26
  licenses:
33
27
  - MIT