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.
@@ -1,442 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Sashite
4
- module Pin
5
- # Represents an identifier in PIN (Piece Identifier Notation) format.
6
- #
7
- # An identifier consists of a single ASCII letter with optional state modifiers:
8
- # - Enhanced state: prefix '+'
9
- # - Diminished state: prefix '-'
10
- # - Normal state: no modifier
11
- #
12
- # The case of the letter determines ownership:
13
- # - Uppercase (A-Z): first player
14
- # - Lowercase (a-z): second player
15
- #
16
- # All instances are immutable - state manipulation methods return new instances.
17
- # This follows the Game Protocol's piece model with Type, Side, and State attributes.
18
- class Identifier
19
- # PIN validation pattern matching the specification
20
- PIN_PATTERN = /\A(?<prefix>[-+])?(?<letter>[a-zA-Z])(?<terminal>\^)?\z/
21
-
22
- # Valid state modifiers
23
- ENHANCED_PREFIX = "+"
24
- DIMINISHED_PREFIX = "-"
25
- NORMAL_PREFIX = ""
26
-
27
- # Terminal marker
28
- TERMINAL_MARKER = "^"
29
-
30
- # State constants
31
- ENHANCED_STATE = :enhanced
32
- DIMINISHED_STATE = :diminished
33
- NORMAL_STATE = :normal
34
-
35
- # Player side constants
36
- FIRST_PLAYER = :first
37
- SECOND_PLAYER = :second
38
-
39
- # Valid types (A-Z)
40
- VALID_TYPES = (:A..:Z).to_a.freeze
41
-
42
- # Valid sides
43
- VALID_SIDES = [FIRST_PLAYER, SECOND_PLAYER].freeze
44
-
45
- # Valid states
46
- VALID_STATES = [NORMAL_STATE, ENHANCED_STATE, DIMINISHED_STATE].freeze
47
-
48
- # Error messages
49
- ERROR_INVALID_PIN = "Invalid PIN string: %s"
50
- ERROR_INVALID_TYPE = "Type must be a symbol from :A to :Z, got: %s"
51
- ERROR_INVALID_SIDE = "Side must be :first or :second, got: %s"
52
- ERROR_INVALID_STATE = "State must be :normal, :enhanced, or :diminished, got: %s"
53
-
54
- # @return [Symbol] the piece type (:A to :Z)
55
- attr_reader :type
56
-
57
- # @return [Symbol] the player side (:first or :second)
58
- attr_reader :side
59
-
60
- # @return [Symbol] the piece state (:normal, :enhanced, or :diminished)
61
- attr_reader :state
62
-
63
- # @return [Boolean] whether the piece is a terminal piece
64
- attr_reader :terminal
65
-
66
- # Create a new identifier instance
67
- #
68
- # @param type [Symbol] piece type (:A to :Z)
69
- # @param side [Symbol] player side (:first or :second)
70
- # @param state [Symbol] piece state (:normal, :enhanced, or :diminished)
71
- # @param terminal [Boolean] whether the piece is a terminal piece
72
- # @raise [ArgumentError] if parameters are invalid
73
- def initialize(type, side, state = NORMAL_STATE, terminal: false)
74
- self.class.validate_type(type)
75
- self.class.validate_side(side)
76
- self.class.validate_state(state)
77
-
78
- @type = type
79
- @side = side
80
- @state = state
81
- @terminal = !!terminal
82
-
83
- freeze
84
- end
85
-
86
- # Parse a PIN string into an Identifier object
87
- #
88
- # @param pin_string [String] PIN notation string
89
- # @return [Identifier] new identifier instance
90
- # @raise [ArgumentError] if the PIN string is invalid
91
- # @example
92
- # Pin::Identifier.parse("k") # => #<Pin::Identifier type=:K side=:second state=:normal terminal=false>
93
- # Pin::Identifier.parse("+R") # => #<Pin::Identifier type=:R side=:first state=:enhanced terminal=false>
94
- # Pin::Identifier.parse("-p") # => #<Pin::Identifier type=:P side=:second state=:diminished terminal=false>
95
- # Pin::Identifier.parse("K^") # => #<Pin::Identifier type=:K side=:first state=:normal terminal=true>
96
- # Pin::Identifier.parse("+K^") # => #<Pin::Identifier type=:K side=:first state=:enhanced terminal=true>
97
- def self.parse(pin_string)
98
- string_value = String(pin_string)
99
- matches = match_pattern(string_value)
100
-
101
- letter = matches[:letter]
102
- enhanced = matches[:prefix] == ENHANCED_PREFIX
103
- diminished = matches[:prefix] == DIMINISHED_PREFIX
104
- is_terminal = matches[:terminal] == TERMINAL_MARKER
105
-
106
- type = letter.upcase.to_sym
107
- side = letter == letter.upcase ? FIRST_PLAYER : SECOND_PLAYER
108
- state = if enhanced
109
- ENHANCED_STATE
110
- elsif diminished
111
- DIMINISHED_STATE
112
- else
113
- NORMAL_STATE
114
- end
115
-
116
- new(type, side, state, terminal: is_terminal)
117
- end
118
-
119
- # Check if a string is a valid PIN notation
120
- #
121
- # @param pin_string [String] The string to validate
122
- # @return [Boolean] true if valid PIN, false otherwise
123
- #
124
- # @example
125
- # Sashite::Pin::Identifier.valid?("K") # => true
126
- # Sashite::Pin::Identifier.valid?("+R") # => true
127
- # Sashite::Pin::Identifier.valid?("-p") # => true
128
- # Sashite::Pin::Identifier.valid?("KK") # => false
129
- # Sashite::Pin::Identifier.valid?("++K") # => false
130
- def self.valid?(pin_string)
131
- return false unless pin_string.is_a?(::String)
132
-
133
- pin_string.match?(PIN_PATTERN)
134
- end
135
-
136
- # Convert the identifier to its PIN string representation
137
- #
138
- # @return [String] PIN notation string
139
- # @example
140
- # identifier.to_s # => "+R"
141
- # terminal_king.to_s # => "K^"
142
- # enhanced_terminal.to_s # => "+K^"
143
- def to_s
144
- "#{prefix}#{letter}#{suffix}"
145
- end
146
-
147
- # Get the letter representation
148
- #
149
- # @return [String] letter representation combining type and side
150
- def letter
151
- first_player? ? type.to_s.upcase : type.to_s.downcase
152
- end
153
-
154
- # Get the prefix representation
155
- #
156
- # @return [String] prefix representing the state
157
- def prefix
158
- case state
159
- when ENHANCED_STATE then ENHANCED_PREFIX
160
- when DIMINISHED_STATE then DIMINISHED_PREFIX
161
- else NORMAL_PREFIX
162
- end
163
- end
164
-
165
- # Get the suffix representation
166
- #
167
- # @return [String] suffix representing terminal status
168
- def suffix
169
- terminal? ? TERMINAL_MARKER : ""
170
- end
171
-
172
- # Create a new identifier with enhanced state
173
- #
174
- # @return [Identifier] new identifier instance with enhanced state
175
- def enhance
176
- return self if enhanced?
177
-
178
- self.class.new(type, side, ENHANCED_STATE, terminal: terminal)
179
- end
180
-
181
- # Create a new identifier without enhanced state
182
- #
183
- # @return [Identifier] new identifier instance with normal state
184
- def unenhance
185
- return self unless enhanced?
186
-
187
- self.class.new(type, side, NORMAL_STATE, terminal: terminal)
188
- end
189
-
190
- # Create a new identifier with diminished state
191
- #
192
- # @return [Identifier] new identifier instance with diminished state
193
- def diminish
194
- return self if diminished?
195
-
196
- self.class.new(type, side, DIMINISHED_STATE, terminal: terminal)
197
- end
198
-
199
- # Create a new identifier without diminished state
200
- #
201
- # @return [Identifier] new identifier instance with normal state
202
- def undiminish
203
- return self unless diminished?
204
-
205
- self.class.new(type, side, NORMAL_STATE, terminal: terminal)
206
- end
207
-
208
- # Create a new identifier with normal state (no modifiers)
209
- #
210
- # @return [Identifier] new identifier instance with normal state
211
- def normalize
212
- return self if normal?
213
-
214
- self.class.new(type, side, NORMAL_STATE, terminal: terminal)
215
- end
216
-
217
- # Create a new identifier marked as terminal
218
- #
219
- # @return [Identifier] new identifier instance marked as terminal
220
- def mark_terminal
221
- return self if terminal?
222
-
223
- self.class.new(type, side, state, terminal: true)
224
- end
225
-
226
- # Create a new identifier unmarked as terminal
227
- #
228
- # @return [Identifier] new identifier instance unmarked as terminal
229
- def unmark_terminal
230
- return self unless terminal?
231
-
232
- self.class.new(type, side, state, terminal: false)
233
- end
234
-
235
- # Create a new identifier with opposite side
236
- #
237
- # @return [Identifier] new identifier instance with opposite side
238
- def flip
239
- self.class.new(type, opposite_side, state, terminal: terminal)
240
- end
241
-
242
- # Create a new identifier with a different type
243
- #
244
- # @param new_type [Symbol] new type (:A to :Z)
245
- # @return [Identifier] new identifier instance with new type
246
- def with_type(new_type)
247
- self.class.validate_type(new_type)
248
- return self if type == new_type
249
-
250
- self.class.new(new_type, side, state, terminal: terminal)
251
- end
252
-
253
- # Create a new identifier with a different side
254
- #
255
- # @param new_side [Symbol] new side (:first or :second)
256
- # @return [Identifier] new identifier instance with new side
257
- def with_side(new_side)
258
- self.class.validate_side(new_side)
259
- return self if side == new_side
260
-
261
- self.class.new(type, new_side, state, terminal: terminal)
262
- end
263
-
264
- # Create a new identifier with a different state
265
- #
266
- # @param new_state [Symbol] new state (:normal, :enhanced, or :diminished)
267
- # @return [Identifier] new identifier instance with new state
268
- def with_state(new_state)
269
- self.class.validate_state(new_state)
270
- return self if state == new_state
271
-
272
- self.class.new(type, side, new_state, terminal: terminal)
273
- end
274
-
275
- # Create a new identifier with a different terminal status
276
- #
277
- # @param new_terminal [Boolean] new terminal status
278
- # @return [Identifier] new identifier instance with new terminal status
279
- def with_terminal(new_terminal)
280
- new_terminal_bool = !!new_terminal
281
- return self if terminal? == new_terminal_bool
282
-
283
- self.class.new(type, side, state, terminal: new_terminal_bool)
284
- end
285
-
286
- # Check if the identifier has enhanced state
287
- #
288
- # @return [Boolean] true if enhanced
289
- def enhanced?
290
- state == ENHANCED_STATE
291
- end
292
-
293
- # Check if the identifier has diminished state
294
- #
295
- # @return [Boolean] true if diminished
296
- def diminished?
297
- state == DIMINISHED_STATE
298
- end
299
-
300
- # Check if the identifier has normal state
301
- #
302
- # @return [Boolean] true if normal
303
- def normal?
304
- state == NORMAL_STATE
305
- end
306
-
307
- # Check if the identifier belongs to the first player
308
- #
309
- # @return [Boolean] true if first player
310
- def first_player?
311
- side == FIRST_PLAYER
312
- end
313
-
314
- # Check if the identifier belongs to the second player
315
- #
316
- # @return [Boolean] true if second player
317
- def second_player?
318
- side == SECOND_PLAYER
319
- end
320
-
321
- # Check if the identifier is a terminal piece
322
- #
323
- # @return [Boolean] true if terminal
324
- def terminal?
325
- terminal
326
- end
327
-
328
- # Check if this identifier is the same type as another
329
- #
330
- # @param other [Identifier] identifier to compare with
331
- # @return [Boolean] true if same type
332
- def same_type?(other)
333
- return false unless other.is_a?(self.class)
334
-
335
- type == other.type
336
- end
337
-
338
- # Check if this identifier has the same side as another
339
- #
340
- # @param other [Identifier] identifier to compare with
341
- # @return [Boolean] true if same side
342
- def same_side?(other)
343
- return false unless other.is_a?(self.class)
344
-
345
- side == other.side
346
- end
347
-
348
- # Check if this identifier has the same state as another
349
- #
350
- # @param other [Identifier] identifier to compare with
351
- # @return [Boolean] true if same state
352
- def same_state?(other)
353
- return false unless other.is_a?(self.class)
354
-
355
- state == other.state
356
- end
357
-
358
- # Check if this identifier has the same terminal status as another
359
- #
360
- # @param other [Identifier] identifier to compare with
361
- # @return [Boolean] true if same terminal status
362
- def same_terminal?(other)
363
- return false unless other.is_a?(self.class)
364
-
365
- terminal? == other.terminal?
366
- end
367
-
368
- # Custom equality comparison
369
- #
370
- # @param other [Object] object to compare with
371
- # @return [Boolean] true if identifiers are equal
372
- def ==(other)
373
- return false unless other.is_a?(self.class)
374
-
375
- type == other.type && side == other.side && state == other.state && terminal? == other.terminal?
376
- end
377
-
378
- # Alias for == to ensure Set functionality works correctly
379
- alias eql? ==
380
-
381
- # Custom hash implementation for use in collections
382
- #
383
- # @return [Integer] hash value
384
- def hash
385
- [self.class, type, side, state, terminal?].hash
386
- end
387
-
388
- # Validate that the type is a valid symbol
389
- #
390
- # @param type [Symbol] the type to validate
391
- # @raise [ArgumentError] if invalid
392
- def self.validate_type(type)
393
- return if VALID_TYPES.include?(type)
394
-
395
- raise ::ArgumentError, format(ERROR_INVALID_TYPE, type.inspect)
396
- end
397
-
398
- # Validate that the side is a valid symbol
399
- #
400
- # @param side [Symbol] the side to validate
401
- # @raise [ArgumentError] if invalid
402
- def self.validate_side(side)
403
- return if VALID_SIDES.include?(side)
404
-
405
- raise ::ArgumentError, format(ERROR_INVALID_SIDE, side.inspect)
406
- end
407
-
408
- # Validate that the state is a valid symbol
409
- #
410
- # @param state [Symbol] the state to validate
411
- # @raise [ArgumentError] if invalid
412
- def self.validate_state(state)
413
- return if VALID_STATES.include?(state)
414
-
415
- raise ::ArgumentError, format(ERROR_INVALID_STATE, state.inspect)
416
- end
417
-
418
- # Match PIN pattern against string
419
- #
420
- # @param string [String] string to match
421
- # @return [MatchData] match data
422
- # @raise [ArgumentError] if string doesn't match
423
- def self.match_pattern(string)
424
- matches = PIN_PATTERN.match(string)
425
- return matches if matches
426
-
427
- raise ::ArgumentError, format(ERROR_INVALID_PIN, string)
428
- end
429
-
430
- private_class_method :match_pattern
431
-
432
- private
433
-
434
- # Get the opposite side of the current identifier
435
- #
436
- # @return [Symbol] :first if current side is :second, :second if current side is :first
437
- def opposite_side
438
- first_player? ? SECOND_PLAYER : FIRST_PLAYER
439
- end
440
- end
441
- end
442
- end