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.
- checksums.yaml +4 -4
- data/README.md +142 -411
- data/lib/sashite/pin.rb +524 -43
- metadata +4 -10
- data/lib/sashite/pin/identifier.rb +0 -442
|
@@ -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
|