sashite-pin 3.3.0 → 4.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.
data/lib/sashite/pin.rb CHANGED
@@ -1,5 +1,10 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative "pin/constants"
4
+ require_relative "pin/errors"
5
+ require_relative "pin/identifier"
6
+ require_relative "pin/parser"
7
+
3
8
  module Sashite
4
9
  # PIN (Piece Identifier Notation) implementation for Ruby.
5
10
  #
@@ -27,523 +32,63 @@ module Sashite
27
32
  # == Examples
28
33
  #
29
34
  # pin = Sashite::Pin.parse("K")
30
- # pin.type # => :K
31
- # pin.side # => :first
32
- # pin.state # => :normal
33
- # pin.terminal # => false
35
+ # pin.type # => :K
36
+ # pin.side # => :first
37
+ # pin.state # => :normal
38
+ # pin.terminal? # => false
34
39
  #
35
- # pin = Sashite::Pin.parse!("+R")
40
+ # pin = Sashite::Pin.parse("+R")
36
41
  # pin.to_s # => "+R"
37
42
  #
38
43
  # pin = Sashite::Pin.parse("k^")
39
- # pin.terminal # => true
44
+ # pin.terminal? # => true
40
45
  #
41
46
  # Sashite::Pin.valid?("K^") # => true
42
47
  # Sashite::Pin.valid?("invalid") # => false
43
48
  #
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.
49
+ # @see https://sashite.dev/specs/pin/1.0.0/
50
+ module Pin
51
+ # Parses a PIN string into an Identifier.
105
52
  #
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
53
+ # @param string [String] The PIN string to parse
54
+ # @return [Identifier] A new Identifier instance
55
+ # @raise [Errors::Argument] If the string is not a valid PIN
109
56
  #
110
57
  # @example
111
58
  # Sashite::Pin.parse("K")
112
- # # => #<Sashite::Pin K>
59
+ # # => #<Sashite::Pin::Identifier K>
113
60
  #
114
61
  # Sashite::Pin.parse("+r")
115
- # # => #<Sashite::Pin +r>
62
+ # # => #<Sashite::Pin::Identifier +r>
116
63
  #
117
64
  # Sashite::Pin.parse("K^")
118
- # # => #<Sashite::Pin K^>
65
+ # # => #<Sashite::Pin::Identifier K^>
119
66
  #
120
67
  # 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 == "^"
68
+ # # => raises Errors::Argument
69
+ def self.parse(string)
70
+ components = Parser.parse(string)
142
71
 
143
- new(type, side, state, terminal: terminal)
72
+ Identifier.new(
73
+ components[:type],
74
+ components[:side],
75
+ components[:state],
76
+ terminal: components[:terminal]
77
+ )
144
78
  end
145
79
 
146
80
  # Checks if a string is a valid PIN notation.
147
81
  #
148
- # @param pin_string [String] The string to validate
82
+ # @param string [String] The string to validate
149
83
  # @return [Boolean] true if valid, false otherwise
150
84
  #
151
85
  # @example
152
- # Sashite::Pin.valid?("K") # => true
153
- # Sashite::Pin.valid?("+R") # => true
154
- # Sashite::Pin.valid?("K^") # => true
155
- # Sashite::Pin.valid?("invalid") # => false
156
- def self.valid?(pin_string)
157
- return false unless pin_string.is_a?(String)
158
-
159
- PIN_PATTERN.match?(pin_string)
160
- end
161
-
162
- # ========================================================================
163
- # Conversion
164
- # ========================================================================
165
-
166
- # Converts the Pin to its string representation.
167
- #
168
- # @return [String] The PIN string
169
- #
170
- # @example
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}"
176
- end
177
-
178
- # Returns the letter representation of the PIN.
179
- #
180
- # @return [String] The letter (uppercase for first player, lowercase for second)
181
- #
182
- # @example
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}"
86
+ # Sashite::Pin.valid?("K") # => true
87
+ # Sashite::Pin.valid?("+R") # => true
88
+ # Sashite::Pin.valid?("K^") # => true
89
+ # Sashite::Pin.valid?("invalid") # => false
90
+ def self.valid?(string)
91
+ Parser.valid?(string)
547
92
  end
548
93
  end
549
94
  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.3.0
4
+ version: 4.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Cyril Kato
@@ -9,28 +9,35 @@ bindir: bin
9
9
  cert_chain: []
10
10
  date: 1980-01-02 00:00:00.000000000 Z
11
11
  dependencies: []
12
- description: |
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.
12
+ description: PIN (Piece Identifier Notation) implementation for Ruby. Provides a rule-agnostic
13
+ format for identifying pieces in abstract strategy board games with immutable identifier
14
+ objects and functional programming principles.
16
15
  email: contact@cyril.email
17
16
  executables: []
18
17
  extensions: []
19
18
  extra_rdoc_files: []
20
19
  files:
21
- - LICENSE.md
20
+ - LICENSE
22
21
  - README.md
23
22
  - lib/sashite-pin.rb
24
23
  - lib/sashite/pin.rb
24
+ - lib/sashite/pin/constants.rb
25
+ - lib/sashite/pin/errors.rb
26
+ - lib/sashite/pin/errors/argument.rb
27
+ - lib/sashite/pin/errors/argument/messages.rb
28
+ - lib/sashite/pin/identifier.rb
29
+ - lib/sashite/pin/parser.rb
25
30
  homepage: https://github.com/sashite/pin.rb
26
31
  licenses:
27
- - MIT
32
+ - Apache-2.0
28
33
  metadata:
29
34
  bug_tracker_uri: https://github.com/sashite/pin.rb/issues
30
35
  documentation_uri: https://rubydoc.info/github/sashite/pin.rb/main
31
36
  homepage_uri: https://github.com/sashite/pin.rb
32
37
  source_code_uri: https://github.com/sashite/pin.rb
33
38
  specification_uri: https://sashite.dev/specs/pin/1.0.0/
39
+ wiki_uri: https://sashite.dev/specs/pin/1.0.0/examples/
40
+ funding_uri: https://github.com/sponsors/sashite
34
41
  rubygems_mfa_required: 'true'
35
42
  rdoc_options: []
36
43
  require_paths:
@@ -46,7 +53,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
46
53
  - !ruby/object:Gem::Version
47
54
  version: '0'
48
55
  requirements: []
49
- rubygems_version: 3.7.2
56
+ rubygems_version: 4.0.3
50
57
  specification_version: 4
51
58
  summary: PIN (Piece Identifier Notation) implementation for Ruby with immutable identifier
52
59
  objects
data/LICENSE.md DELETED
@@ -1,22 +0,0 @@
1
- Copyright (c) 2025 Cyril Kato
2
-
3
- MIT License
4
-
5
- Permission is hereby granted, free of charge, to any person obtaining
6
- a copy of this software and associated documentation files (the
7
- "Software"), to deal in the Software without restriction, including
8
- without limitation the rights to use, copy, modify, merge, publish,
9
- distribute, sublicense, and/or sell copies of the Software, and to
10
- permit persons to whom the Software is furnished to do so, subject to
11
- the following conditions:
12
-
13
- The above copyright notice and this permission notice shall be
14
- included in all copies or substantial portions of the Software.
15
-
16
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
- EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
- MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
- NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
- LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
- OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
- WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.