sashite-epin 2.0.0 → 2.1.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,293 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "sashite/pin"
4
-
5
- module Sashite
6
- module Epin
7
- # Represents an identifier in EPIN (Extended Piece Identifier Notation) format.
8
- #
9
- # EPIN extends PIN by adding a derivation marker to track piece style in cross-style games.
10
- # An EPIN identifier is simply a PIN identifier plus a boolean flag indicating whether
11
- # the piece uses its own side's native style (native) or the opponent's style (derived).
12
- #
13
- # ## Pure Composition Design
14
- #
15
- # EPIN doesn't reimplement PIN features - it's pure composition:
16
- # - All piece attributes (name, side, state, terminal) come from the PIN component
17
- # - EPIN adds only style derivation tracking (native vs derived)
18
- # - Zero code duplication
19
- #
20
- # ## Minimal API
21
- #
22
- # Core methods (6 total):
23
- # 1. new(pin, derived: false) - create from PIN component
24
- # 2. pin - get PIN component
25
- # 3. derived? - check derivation status
26
- # 4. to_s - serialize
27
- # 5. with_pin(new_pin) - replace PIN component
28
- # 6. with_derived(boolean) - change derivation status
29
- #
30
- # Everything else uses the PIN component API directly.
31
- #
32
- # All instances are immutable - transformation methods return new instances.
33
- #
34
- # @example Basic usage
35
- # # Create from PIN component
36
- # pin = Sashite::Pin.parse("K^")
37
- # epin = Sashite::Epin::Identifier.new(pin, derived: false)
38
- # epin.to_s # => "K^" (native)
39
- #
40
- # # Mark as derived
41
- # derived = epin.mark_derived
42
- # derived.to_s # => "K^'" (derived from opponent's style)
43
- #
44
- # @example Accessing attributes via PIN component
45
- # epin = Sashite::Epin.parse("+R^'")
46
- # epin.pin.type # => :R (Piece Name)
47
- # epin.pin.side # => :first (Piece Side)
48
- # epin.pin.state # => :enhanced (Piece State)
49
- # epin.pin.terminal? # => true (Terminal Status)
50
- # epin.derived? # => true (Piece Style)
51
- #
52
- # @example Transformations
53
- # epin = Sashite::Epin.parse("K^")
54
- #
55
- # # Transform PIN component
56
- # epin.with_pin(epin.pin.with_type(:Q)) # => "Q^"
57
- #
58
- # # Transform derivation
59
- # epin.mark_derived # => "K^'"
60
- # epin.with_derived(true) # => "K^'"
61
- #
62
- # @see https://sashite.dev/specs/epin/1.0.0/ EPIN Specification v1.0.0
63
- class Identifier
64
- # EPIN validation pattern matching the specification
65
- # Grammar: <epin> ::= <pin> | <pin> "'"
66
- EPIN_PATTERN = /\A[-+]?[A-Za-z]\^?'?\z/
67
-
68
- # Derivation marker character
69
- DERIVATION_MARKER = "'"
70
-
71
- # Error messages
72
- ERROR_INVALID_EPIN = "Invalid EPIN string: %s"
73
- ERROR_INVALID_PIN = "PIN component must be a Pin::Identifier, got: %s"
74
- ERROR_MULTIPLE_MARKERS = "EPIN string cannot have multiple derivation markers: %s"
75
-
76
- # @return [Pin::Identifier] the PIN component
77
- attr_reader :pin
78
-
79
- # @return [Boolean] whether the piece uses derived style (opponent's native style)
80
- def derived?
81
- @derived
82
- end
83
-
84
- # Create a new EPIN identifier from PIN component and derivation flag
85
- #
86
- # @param pin [Pin::Identifier] the PIN component
87
- # @param derived [Boolean] whether the piece uses derived style (default: false)
88
- # @raise [ArgumentError] if pin is not a Pin::Identifier
89
- #
90
- # @example Create EPIN identifiers
91
- # pin = Sashite::Pin.parse("K^")
92
- # native = Sashite::Epin::Identifier.new(pin, derived: false) # => "K^"
93
- # derived = Sashite::Epin::Identifier.new(pin, derived: true) # => "K^'"
94
- def initialize(pin, derived: false)
95
- raise ::ArgumentError, format(ERROR_INVALID_PIN, pin.class) unless pin.is_a?(Pin::Identifier)
96
-
97
- @pin = pin
98
- @derived = !!derived
99
-
100
- freeze
101
- end
102
-
103
- # Parse an EPIN string into an Identifier object
104
- #
105
- # @param epin_string [String] EPIN notation string
106
- # @return [Identifier] new identifier instance
107
- # @raise [ArgumentError] if the EPIN string is invalid
108
- #
109
- # @example Parse EPIN strings
110
- # Sashite::Epin::Identifier.parse("K^") # => Native king
111
- # Sashite::Epin::Identifier.parse("K^'") # => Derived king
112
- # Sashite::Epin::Identifier.parse("+R'") # => Derived enhanced rook
113
- def self.parse(epin_string)
114
- string_value = String(epin_string)
115
- validate_epin_string(string_value)
116
-
117
- # Check for derivation marker
118
- has_marker = string_value.end_with?(DERIVATION_MARKER)
119
-
120
- # Extract PIN part (remove derivation marker if present)
121
- pin_part = has_marker ? string_value[0...-1] : string_value
122
-
123
- # Parse PIN component
124
- pin_identifier = Pin::Identifier.parse(pin_part)
125
-
126
- new(pin_identifier, derived: has_marker)
127
- end
128
-
129
- # Check if a string is a valid EPIN notation
130
- #
131
- # @param epin_string [String] the string to validate
132
- # @return [Boolean] true if valid EPIN, false otherwise
133
- #
134
- # @example Validate EPIN strings
135
- # Sashite::Epin::Identifier.valid?("K^") # => true
136
- # Sashite::Epin::Identifier.valid?("K^'") # => true
137
- # Sashite::Epin::Identifier.valid?("+R'") # => true
138
- # Sashite::Epin::Identifier.valid?("K^''") # => false (multiple markers)
139
- # Sashite::Epin::Identifier.valid?("KK'") # => false (invalid PIN)
140
- def self.valid?(epin_string)
141
- return false unless epin_string.is_a?(::String)
142
- return false unless epin_string.match?(EPIN_PATTERN)
143
-
144
- # Check for multiple derivation markers
145
- marker_count = epin_string.count(DERIVATION_MARKER)
146
- return false if marker_count > 1
147
-
148
- # Validate PIN part
149
- has_marker = epin_string.end_with?(DERIVATION_MARKER)
150
- pin_part = has_marker ? epin_string[0...-1] : epin_string
151
-
152
- Pin::Identifier.valid?(pin_part)
153
- end
154
-
155
- # Convert the identifier to its EPIN string representation
156
- #
157
- # @return [String] EPIN notation string
158
- #
159
- # @example Serialize identifiers
160
- # native.to_s # => "K^"
161
- # derived.to_s # => "K^'"
162
- def to_s
163
- pin.to_s + suffix
164
- end
165
-
166
- # Get the derivation marker suffix
167
- #
168
- # @return [String] derivation marker if derived, empty string if native
169
- def suffix
170
- derived? ? DERIVATION_MARKER : ""
171
- end
172
-
173
- # Create a new identifier with a different PIN component
174
- #
175
- # @param new_pin [Pin::Identifier] new PIN component
176
- # @return [Identifier] new identifier with different PIN
177
- # @raise [ArgumentError] if new_pin is not a Pin::Identifier
178
- #
179
- # @example Replace PIN component
180
- # epin = Sashite::Epin.parse("K^'")
181
- # new_pin = epin.pin.with_type(:Q)
182
- # epin.with_pin(new_pin).to_s # => "Q^'"
183
- def with_pin(new_pin)
184
- raise ::ArgumentError, format(ERROR_INVALID_PIN, new_pin.class) unless new_pin.is_a?(Pin::Identifier)
185
- return self if pin == new_pin
186
-
187
- self.class.new(new_pin, derived: derived?)
188
- end
189
-
190
- # Create a new identifier with different derivation status
191
- #
192
- # @param new_derived [Boolean] new derivation status
193
- # @return [Identifier] new identifier with different derivation
194
- #
195
- # @example Change derivation status
196
- # native = Sashite::Epin.parse("K^")
197
- # derived = native.with_derived(true)
198
- # derived.to_s # => "K^'"
199
- def with_derived(new_derived)
200
- new_derived_bool = !!new_derived
201
- return self if derived? == new_derived_bool
202
-
203
- self.class.new(pin, derived: new_derived_bool)
204
- end
205
-
206
- # Create a new identifier marked as derived (opponent's native style)
207
- #
208
- # @return [Identifier] new identifier marked as derived
209
- #
210
- # @example Mark as derived
211
- # native = Sashite::Epin.parse("K^")
212
- # derived = native.mark_derived
213
- # derived.to_s # => "K^'"
214
- def mark_derived
215
- return self if derived?
216
-
217
- self.class.new(pin, derived: true)
218
- end
219
-
220
- # Create a new identifier marked as native (own side's native style)
221
- #
222
- # @return [Identifier] new identifier marked as native
223
- #
224
- # @example Mark as native
225
- # derived = Sashite::Epin.parse("K^'")
226
- # native = derived.unmark_native
227
- # native.to_s # => "K^"
228
- def unmark_native
229
- return self unless derived?
230
-
231
- self.class.new(pin, derived: false)
232
- end
233
-
234
- # Check if the identifier uses native style (own side's native style)
235
- #
236
- # @return [Boolean] true if native (not derived)
237
- def native?
238
- !derived?
239
- end
240
-
241
- # Check if this identifier has the same derivation status as another
242
- #
243
- # @param other [Identifier] identifier to compare with
244
- # @return [Boolean] true if same derivation status
245
- #
246
- # @example Compare derivation status
247
- # native = Sashite::Epin.parse("K^")
248
- # derived = Sashite::Epin.parse("K^'")
249
- # native.same_derivation?(derived) # => false
250
- def same_derivation?(other)
251
- return false unless other.is_a?(self.class)
252
-
253
- derived? == other.derived?
254
- end
255
-
256
- # Custom equality comparison
257
- #
258
- # @param other [Object] object to compare with
259
- # @return [Boolean] true if identifiers are equal
260
- def ==(other)
261
- return false unless other.is_a?(self.class)
262
-
263
- pin == other.pin && derived? == other.derived?
264
- end
265
-
266
- # Alias for == to ensure Set functionality works correctly
267
- alias eql? ==
268
-
269
- # Custom hash implementation for use in collections
270
- #
271
- # @return [Integer] hash value
272
- def hash
273
- [self.class, pin, derived?].hash
274
- end
275
-
276
- # Validate EPIN string format
277
- #
278
- # @param string [String] string to validate
279
- # @raise [ArgumentError] if string doesn't match EPIN pattern or has multiple markers
280
- def self.validate_epin_string(string)
281
- raise ::ArgumentError, format(ERROR_INVALID_EPIN, string) unless string.match?(EPIN_PATTERN)
282
-
283
- # Check for multiple derivation markers
284
- marker_count = string.count(DERIVATION_MARKER)
285
- return unless marker_count > 1
286
-
287
- raise ::ArgumentError, format(ERROR_MULTIPLE_MARKERS, string)
288
- end
289
-
290
- private_class_method :validate_epin_string
291
- end
292
- end
293
- end