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