sashite-sin 2.1.0 → 3.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.
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Sashite
4
+ module Sin
5
+ # Constants for the SIN (Style Identifier Notation) specification.
6
+ #
7
+ # Defines valid values for abbreviations and sides, as well as formatting constants.
8
+ #
9
+ # @example Accessing valid abbreviations
10
+ # Constants::VALID_ABBRS # => [:A, :B, ..., :Z]
11
+ #
12
+ # @example Accessing valid sides
13
+ # Constants::VALID_SIDES # => [:first, :second]
14
+ #
15
+ # @see https://sashite.dev/specs/sin/1.0.0/
16
+ module Constants
17
+ # Valid abbreviation symbols (A-Z as uppercase symbols).
18
+ #
19
+ # @return [Array<Symbol>] Array of 26 valid abbreviation symbols
20
+ VALID_ABBRS = %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
21
+
22
+ # Valid side symbols.
23
+ #
24
+ # @return [Array<Symbol>] Array of valid side symbols
25
+ VALID_SIDES = %i[first second].freeze
26
+
27
+ # Maximum length of a valid SIN string.
28
+ #
29
+ # @return [Integer] Maximum string length (1)
30
+ MAX_STRING_LENGTH = 1
31
+
32
+ # Empty string constant for internal use.
33
+ #
34
+ # @return [String] Empty string
35
+ EMPTY_STRING = ""
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Sashite
4
+ module Sin
5
+ module Errors
6
+ class Argument < ::ArgumentError
7
+ # Error messages for SIN parsing and validation.
8
+ #
9
+ # Provides centralized, immutable error message constants for consistent
10
+ # error reporting across the library.
11
+ #
12
+ # @example Using an error message
13
+ # raise ArgumentError, Messages::EMPTY_INPUT
14
+ #
15
+ # @see https://sashite.dev/specs/sin/1.0.0/
16
+ module Messages
17
+ # Error message for empty input string.
18
+ #
19
+ # @return [String] Error message
20
+ EMPTY_INPUT = "empty input"
21
+
22
+ # Error message for input exceeding maximum length.
23
+ #
24
+ # @return [String] Error message
25
+ INPUT_TOO_LONG = "input exceeds 1 character"
26
+
27
+ # Error message for invalid character (not a letter).
28
+ #
29
+ # @return [String] Error message
30
+ MUST_BE_LETTER = "must be a letter"
31
+
32
+ # Error message for invalid abbreviation value.
33
+ #
34
+ # @return [String] Error message
35
+ INVALID_ABBR = "invalid abbr"
36
+
37
+ # Error message for invalid side value.
38
+ #
39
+ # @return [String] Error message
40
+ INVALID_SIDE = "invalid side"
41
+ end
42
+ end
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "argument/messages"
4
+
5
+ module Sashite
6
+ module Sin
7
+ module Errors
8
+ # Namespace for ArgumentError-related constants and messages.
9
+ #
10
+ # Provides structured access to error messages used when raising
11
+ # ArgumentError exceptions throughout the library.
12
+ #
13
+ # @example Raising an error with a message
14
+ # raise ArgumentError, Argument::Messages::EMPTY_INPUT
15
+ #
16
+ # @see Argument::Messages
17
+ class Argument < ::ArgumentError
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "errors/argument"
4
+
5
+ module Sashite
6
+ module Sin
7
+ # Namespace for error-related constants and messages.
8
+ #
9
+ # Provides structured access to error messages used throughout the library.
10
+ #
11
+ # @example Accessing error messages
12
+ # Errors::Argument::Messages::EMPTY_INPUT # => "empty input"
13
+ #
14
+ # @see Errors::Argument
15
+ # @see Errors::Argument::Messages
16
+ module Errors
17
+ end
18
+ end
19
+ end
@@ -1,405 +1,188 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative "constants"
4
+ require_relative "errors"
5
+
3
6
  module Sashite
4
7
  module Sin
5
- # Represents an identifier in SIN (Style Identifier Notation) format.
6
- #
7
- # ## Concept
8
- #
9
- # SIN addresses the fundamental need to identify which style system governs piece behavior
10
- # while simultaneously indicating which player controls pieces of that style. In cross-style
11
- # scenarios where different players use different game traditions, this dual encoding becomes
12
- # essential for unambiguous piece identification.
13
- #
14
- # ## Dual-Purpose Encoding
15
- #
16
- # Each SIN identifier serves two functions:
17
- # - **Style Family Identification**: The family choice indicates which rule system applies
18
- # - **Player Assignment**: The side indicates which player uses this style as their native system
19
- #
20
- # ## Format Structure
21
- #
22
- # An identifier consists of a single ASCII letter with case-based side encoding:
23
- # - Uppercase letter: first player (A, B, C, ..., Z)
24
- # - Lowercase letter: second player (a, b, c, ..., z)
25
- #
26
- # The letter representation combines two distinct semantic components:
27
- # - **Style Family**: The underlying ASCII character (A-Z), representing the game tradition or rule system
28
- # - **Player Assignment**: The case of the character (uppercase/lowercase), representing which player uses this style
29
- #
30
- # Examples of letter composition:
31
- # - Family :C + Side :first → Letter "C" (Chess, First player)
32
- # - Family :C + Side :second → Letter "c" (Chess, Second player)
33
- # - Family :S + Side :first → Letter "S" (Shōgi, First player)
34
- # - Family :S + Side :second → Letter "s" (Shōgi, Second player)
35
- #
36
- # ## Canonical Representation
8
+ # Represents a parsed SIN (Style Identifier Notation) identifier.
37
9
  #
38
- # SIN enforces canonical representation where each style-player combination has exactly one
39
- # valid identifier within a given context. This ensures consistent interpretation across
40
- # different implementations while allowing flexibility for collision resolution.
10
+ # An Identifier encodes two attributes:
11
+ # - Abbr: the style abbreviation (A-Z as uppercase symbol)
12
+ # - Side: the player side (:first or :second)
41
13
  #
42
- # ## Immutability
14
+ # Instances are immutable (frozen after creation).
43
15
  #
44
- # All instances are immutable - transformation methods return new instances.
45
- # This follows the SIN Specification v1.0.0 functional design principles.
16
+ # @example Creating identifiers
17
+ # sin = Identifier.new(:C, :first)
18
+ # sin = Identifier.new(:S, :second)
46
19
  #
47
- # @example Basic usage with traditional game styles
48
- # # Chess family identifiers
49
- # chess_white = Sashite::Sin::Identifier.parse("C") # Family :C, Side :first
50
- # chess_black = Sashite::Sin::Identifier.parse("c") # Family :C, Side :second
20
+ # @example String conversion
21
+ # Identifier.new(:C, :first).to_s # => "C"
22
+ # Identifier.new(:C, :second).to_s # => "c"
51
23
  #
52
- # # Shōgi family identifiers
53
- # shogi_sente = Sashite::Sin::Identifier.parse("S") # Family :S, Side :first
54
- # shogi_gote = Sashite::Sin::Identifier.parse("s") # Family :S, Side :second
55
- #
56
- # @example Dual-purpose encoding demonstration
57
- # identifier = Sashite::Sin::Identifier.parse("C")
58
- # identifier.family # => :C (Style Family)
59
- # identifier.side # => :first (Player Assignment)
60
- # identifier.letter # => "C" (Combined representation)
61
- #
62
- # @example Cross-style scenarios
63
- # # Different families in one match (requires compatible board structures)
64
- # chess_style = Sashite::Sin::Identifier.parse("C") # First player uses Chess family
65
- # ogi_style = Sashite::Sin::Identifier.parse("o") # Second player uses Ōgi family
66
- #
67
- # @see https://sashite.dev/specs/sin/1.0.0/ SIN Specification v1.0.0
24
+ # @see https://sashite.dev/specs/sin/1.0.0/
68
25
  class Identifier
69
- # SIN validation pattern matching the specification regular expression
70
- # Grammar: <sin> ::= <uppercase-letter> | <lowercase-letter>
71
- SIN_PATTERN = /\A[A-Za-z]\z/
72
-
73
- # Player side constants following SIN v1.0.0 two-player constraint
74
- FIRST_PLAYER = :first
75
- SECOND_PLAYER = :second
76
-
77
- # Valid families (A-Z)
78
- VALID_FAMILIES = (:A..:Z).to_a.freeze
26
+ # Valid abbreviation symbols (A-Z).
27
+ VALID_ABBRS = Constants::VALID_ABBRS
79
28
 
80
- # Valid sides array for validation
81
- VALID_SIDES = [FIRST_PLAYER, SECOND_PLAYER].freeze
29
+ # Valid side symbols.
30
+ VALID_SIDES = Constants::VALID_SIDES
82
31
 
83
- # Error messages with SIN-compliant terminology
84
- ERROR_INVALID_SIN = "Invalid SIN string: %s. Must be a single ASCII letter (A-Z, a-z)"
85
- ERROR_INVALID_FAMILY = "Family must be a symbol from :A to :Z representing Style Family, got: %s"
86
- ERROR_INVALID_SIDE = "Side must be :first or :second following SIN two-player constraint, got: %s"
32
+ # @return [Symbol] Style abbreviation (:A to :Z, always uppercase)
33
+ attr_reader :abbr
87
34
 
88
- # @return [Symbol] the style family (:A to :Z)
89
- # This represents the Style Family component - the game tradition or rule system
90
- attr_reader :family
91
-
92
- # @return [Symbol] the player side (:first or :second)
93
- # This represents the Player Assignment component
35
+ # @return [Symbol] Player side (:first or :second)
94
36
  attr_reader :side
95
37
 
96
- # Create a new identifier instance with canonical representation
97
- #
98
- # @param family [Symbol] style family (:A to :Z representing Style Family)
99
- # @param side [Symbol] player side (:first or :second representing Player Assignment)
100
- # @raise [ArgumentError] if parameters are invalid
38
+ # Creates a new Identifier instance.
101
39
  #
102
- # @example Create identifiers with family and side separation
103
- # # Chess family identifiers
104
- # chess_first = Sashite::Sin::Identifier.new(:C, :first) # => Family=:C, Side=:first
105
- # chess_second = Sashite::Sin::Identifier.new(:C, :second) # => Family=:C, Side=:second
40
+ # @param abbr [Symbol] Style abbreviation (:A to :Z)
41
+ # @param side [Symbol] Player side (:first or :second)
42
+ # @return [Identifier] A new frozen Identifier instance
43
+ # @raise [Errors::Argument] If any attribute is invalid
106
44
  #
107
- # @example Style Family and Player Assignment demonstration
108
- # identifier = Sashite::Sin::Identifier.new(:S, :first)
109
- # identifier.family # => :S (Shōgi Style Family)
110
- # identifier.side # => :first (First Player Assignment)
111
- # identifier.letter # => "S" (Combined representation)
112
- def initialize(family, side)
113
- self.class.validate_family(family)
114
- self.class.validate_side(side)
45
+ # @example
46
+ # Identifier.new(:C, :first)
47
+ # Identifier.new(:S, :second)
48
+ def initialize(abbr, side)
49
+ validate_abbr!(abbr)
50
+ validate_side!(side)
115
51
 
116
- @family = family
52
+ @abbr = abbr
117
53
  @side = side
118
54
 
119
55
  freeze
120
56
  end
121
57
 
122
- # Parse an SIN string into an Identifier object with dual-purpose encoding
123
- #
124
- # The family and side are inferred from both the character choice (Style Family)
125
- # and case (Player Assignment):
126
- # - Uppercase letter → Style Family + First player
127
- # - Lowercase letter → Style Family + Second player
128
- #
129
- # @param sin_string [String] SIN notation string (single ASCII letter)
130
- # @return [Identifier] parsed identifier object with Family and Side attributes
131
- # @raise [ArgumentError] if the SIN string is invalid
132
- #
133
- # @example Parse SIN strings with case-based Player Assignment inference
134
- # Sashite::Sin::Identifier.parse("C") # => Family=:C, Side=:first (Chess, White)
135
- # Sashite::Sin::Identifier.parse("c") # => Family=:C, Side=:second (Chess, Black)
136
- # Sashite::Sin::Identifier.parse("S") # => Family=:S, Side=:first (Shōgi, Sente)
137
- # Sashite::Sin::Identifier.parse("s") # => Family=:S, Side=:second (Shōgi, Gote)
138
- #
139
- # @example Traditional game styles from SIN Examples
140
- # # Chess (8×8 board)
141
- # chess_white = Sashite::Sin::Identifier.parse("C") # First player (White pieces)
142
- # chess_black = Sashite::Sin::Identifier.parse("c") # Second player (Black pieces)
143
- #
144
- # # Xiangqi (9×10 board)
145
- # xiangqi_red = Sashite::Sin::Identifier.parse("X") # First player (Red pieces)
146
- # xiangqi_black = Sashite::Sin::Identifier.parse("x") # Second player (Black pieces)
147
- def self.parse(sin_string)
148
- string_value = String(sin_string)
149
- validate_sin_string(string_value)
150
-
151
- # Extract Style Family (case-insensitive) and Player Assignment (case-sensitive)
152
- family_symbol = string_value.upcase.to_sym
153
- identifier_side = string_value == string_value.upcase ? FIRST_PLAYER : SECOND_PLAYER
154
-
155
- new(family_symbol, identifier_side)
156
- end
157
-
158
- # Check if a string is a valid SIN notation according to specification
159
- #
160
- # Validates against the SIN grammar:
161
- # <sin> ::= <uppercase-letter> | <lowercase-letter>
162
- #
163
- # @param sin_string [String] the string to validate
164
- # @return [Boolean] true if valid SIN, false otherwise
165
- #
166
- # @example Validate SIN strings against specification
167
- # Sashite::Sin::Identifier.valid?("C") # => true (Chess first player)
168
- # Sashite::Sin::Identifier.valid?("c") # => true (Chess second player)
169
- # Sashite::Sin::Identifier.valid?("CHESS") # => false (multi-character)
170
- # Sashite::Sin::Identifier.valid?("1") # => false (not ASCII letter)
171
- def self.valid?(sin_string)
172
- return false unless sin_string.is_a?(::String)
173
-
174
- sin_string.match?(SIN_PATTERN)
175
- end
58
+ # ========================================================================
59
+ # String Conversion
60
+ # ========================================================================
176
61
 
177
- # Convert the identifier to its SIN string representation
62
+ # Returns the SIN string representation.
178
63
  #
179
- # Returns the canonical SIN notation with proper case encoding for Player Assignment.
64
+ # @return [String] The single-character SIN string
180
65
  #
181
- # @return [String] SIN notation string (single ASCII letter)
182
- #
183
- # @example Display identifiers in canonical SIN format
184
- # chess_first.to_s # => "C" (Chess family, first player)
185
- # chess_second.to_s # => "c" (Chess family, second player)
186
- # shogi_first.to_s # => "S" (Shōgi family, first player)
66
+ # @example
67
+ # Identifier.new(:C, :first).to_s # => "C"
68
+ # Identifier.new(:C, :second).to_s # => "c"
187
69
  def to_s
188
- letter
189
- end
190
-
191
- # Get the letter representation combining Style Family and Player Assignment
192
- #
193
- # @return [String] letter representation with proper case encoding
194
- #
195
- # @example Letter representation with dual-purpose encoding
196
- # chess_first.letter # => "C" (Chess family, first player)
197
- # chess_second.letter # => "c" (Chess family, second player)
198
- # shogi_first.letter # => "S" (Shōgi family, first player)
199
- def letter
200
- first_player? ? family.to_s.upcase : family.to_s.downcase
201
- end
70
+ base = String(abbr)
202
71
 
203
- # Create a new identifier with opposite Player Assignment (flip sides)
204
- #
205
- # Transforms Player Assignment while maintaining Style Family:
206
- # - First player → Second player (uppercase → lowercase)
207
- # - Second player → First player (lowercase → uppercase)
208
- #
209
- # @return [Identifier] new immutable identifier instance with flipped Player Assignment
210
- #
211
- # @example Flip Player Assignment within same Style Family
212
- # chess_white = Sashite::Sin::Identifier.parse("C")
213
- # chess_black = chess_white.flip # => Family=:C, Side=:second
214
- #
215
- # shogi_sente = Sashite::Sin::Identifier.parse("S")
216
- # shogi_gote = shogi_sente.flip # => Family=:S, Side=:second
217
- def flip
218
- self.class.new(family, opposite_side)
72
+ case side
73
+ when :first then base.upcase
74
+ when :second then base.downcase
75
+ end
219
76
  end
220
77
 
221
- # Create a new identifier with a different Style Family (keeping same Player Assignment)
222
- #
223
- # Changes the Style Family component while preserving Player Assignment.
224
- #
225
- # @param new_family [Symbol] new Style Family (:A to :Z)
226
- # @return [Identifier] new immutable identifier instance with different Style Family
227
- #
228
- # @example Change Style Family while preserving Player Assignment
229
- # chess_white = Sashite::Sin::Identifier.parse("C") # Chess, first player
230
- # shogi_white = chess_white.with_family(:S) # Shōgi, first player
231
- #
232
- # chess_black = Sashite::Sin::Identifier.parse("c") # Chess, second player
233
- # xiangqi_black = chess_black.with_family(:X) # Xiangqi, second player
234
- def with_family(new_family)
235
- self.class.validate_family(new_family)
236
- return self if family == new_family
237
-
238
- self.class.new(new_family, side)
239
- end
78
+ # ========================================================================
79
+ # Side Queries
80
+ # ========================================================================
240
81
 
241
- # Create a new identifier with a different Player Assignment (keeping same Style Family)
82
+ # Checks if the Identifier belongs to the first player.
242
83
  #
243
- # Changes the Player Assignment component while preserving Style Family.
84
+ # @return [Boolean] true if first player
244
85
  #
245
- # @param new_side [Symbol] :first or :second
246
- # @return [Identifier] new immutable identifier instance with different Player Assignment
247
- #
248
- # @example Change Player Assignment within same Style Family
249
- # chess_white = Sashite::Sin::Identifier.parse("C") # Chess, first player
250
- # chess_black = chess_white.with_side(:second) # Chess, second player
251
- #
252
- # shogi_sente = Sashite::Sin::Identifier.parse("S") # Shōgi, first player
253
- # shogi_gote = shogi_sente.with_side(:second) # Shōgi, second player
254
- def with_side(new_side)
255
- self.class.validate_side(new_side)
256
- return self if side == new_side
257
-
258
- self.class.new(family, new_side)
259
- end
260
-
261
- # Check if the identifier belongs to the first player
262
- #
263
- # @return [Boolean] true if first player (uppercase letter)
264
- #
265
- # @example Player identification
266
- # Sashite::Sin::Identifier.parse("C").first_player? # => true
267
- # Sashite::Sin::Identifier.parse("c").first_player? # => false
86
+ # @example
87
+ # Identifier.new(:C, :first).first_player? # => true
268
88
  def first_player?
269
- side == FIRST_PLAYER
89
+ side.equal?(:first)
270
90
  end
271
91
 
272
- # Check if the identifier belongs to the second player
92
+ # Checks if the Identifier belongs to the second player.
273
93
  #
274
- # @return [Boolean] true if second player (lowercase letter)
94
+ # @return [Boolean] true if second player
275
95
  #
276
- # @example Player identification
277
- # Sashite::Sin::Identifier.parse("c").second_player? # => true
278
- # Sashite::Sin::Identifier.parse("C").second_player? # => false
96
+ # @example
97
+ # Identifier.new(:C, :second).second_player? # => true
279
98
  def second_player?
280
- side == SECOND_PLAYER
99
+ side.equal?(:second)
281
100
  end
282
101
 
283
- # Check if this identifier has the same Style Family as another
284
- #
285
- # Compares the Style Family component, ignoring Player Assignment.
286
- # This is useful for identifying pieces from the same game tradition in cross-style scenarios.
287
- #
288
- # @param other [Identifier] identifier to compare with
289
- # @return [Boolean] true if both identifiers use the same Style Family
102
+ # ========================================================================
103
+ # Comparison Queries
104
+ # ========================================================================
105
+
106
+ # Checks if two Identifiers have the same abbreviation.
290
107
  #
291
- # @example Compare Style Families across different Player Assignments
292
- # chess_white = Sashite::Sin::Identifier.parse("C") # Chess, first player
293
- # chess_black = Sashite::Sin::Identifier.parse("c") # Chess, second player
294
- # shogi_white = Sashite::Sin::Identifier.parse("S") # Shōgi, first player
108
+ # @param other [Identifier] The other Identifier to compare
109
+ # @return [Boolean] true if same abbreviation
295
110
  #
296
- # chess_white.same_family?(chess_black) # => true (both Chess family)
297
- # chess_white.same_family?(shogi_white) # => false (different families)
298
- def same_family?(other)
299
- return false unless other.is_a?(self.class)
300
-
301
- family == other.family
111
+ # @example
112
+ # sin1 = Identifier.new(:C, :first)
113
+ # sin2 = Identifier.new(:C, :second)
114
+ # sin1.same_abbr?(sin2) # => true
115
+ def same_abbr?(other)
116
+ abbr.equal?(other.abbr)
302
117
  end
303
118
 
304
- # Check if this identifier belongs to the same Player Assignment as another
305
- #
306
- # Compares the Player Assignment component of identifiers across different Style Families.
307
- # This is useful for grouping pieces by controlling player in multi-style games.
119
+ # Checks if two Identifiers have the same side.
308
120
  #
309
- # @param other [Identifier] identifier to compare with
310
- # @return [Boolean] true if both identifiers belong to the same Player Assignment
121
+ # @param other [Identifier] The other Identifier to compare
122
+ # @return [Boolean] true if same side
311
123
  #
312
- # @example Compare Player Assignments across different Style Families
313
- # chess_white = Sashite::Sin::Identifier.parse("C") # Chess, first player
314
- # shogi_white = Sashite::Sin::Identifier.parse("S") # Shōgi, first player
315
- # chess_black = Sashite::Sin::Identifier.parse("c") # Chess, second player
316
- #
317
- # chess_white.same_side?(shogi_white) # => true (both first player)
318
- # chess_white.same_side?(chess_black) # => false (different players)
124
+ # @example
125
+ # sin1 = Identifier.new(:C, :first)
126
+ # sin2 = Identifier.new(:S, :first)
127
+ # sin1.same_side?(sin2) # => true
319
128
  def same_side?(other)
320
- return false unless other.is_a?(self.class)
321
-
322
- side == other.side
129
+ side.equal?(other.side)
323
130
  end
324
131
 
325
- # Compatibility alias for same_family? to maintain API consistency
326
- #
327
- # @deprecated Use {#same_family?} instead for clearer semantics
328
- # @param other [Identifier] identifier to compare with
329
- # @return [Boolean] true if both identifiers use the same Style Family
330
- def same_letter?(other)
331
- same_family?(other)
332
- end
132
+ # ========================================================================
133
+ # Equality
134
+ # ========================================================================
333
135
 
334
- # Custom equality comparison
335
- #
336
- # Two identifiers are equal if they have identical Family and Side attributes.
337
- #
338
- # @param other [Object] object to compare with
339
- # @return [Boolean] true if both objects are identifiers with identical Family and Side
136
+ # Checks equality with another Identifier.
340
137
  #
341
- # @example Equality comparison
342
- # id1 = Sashite::Sin::Identifier.parse("C")
343
- # id2 = Sashite::Sin::Identifier.parse("C")
344
- # id3 = Sashite::Sin::Identifier.parse("c")
138
+ # @param other [Object] The object to compare
139
+ # @return [Boolean] true if equal
345
140
  #
346
- # id1 == id2 # => true (identical Family and Side)
347
- # id1 == id3 # => false (different Player Assignment)
141
+ # @example
142
+ # sin1 = Identifier.new(:C, :first)
143
+ # sin2 = Identifier.new(:C, :first)
144
+ # sin1 == sin2 # => true
348
145
  def ==(other)
349
- return false unless other.is_a?(self.class)
146
+ return false unless self.class === other
350
147
 
351
- family == other.family && side == other.side
148
+ abbr.equal?(other.abbr) && side.equal?(other.side)
352
149
  end
353
150
 
354
- # Alias for == to ensure Set functionality works correctly
355
151
  alias eql? ==
356
152
 
357
- # Custom hash implementation for use in collections
153
+ # Returns a hash code for the Identifier.
358
154
  #
359
- # @return [Integer] hash value based on class, Family, and Side
155
+ # @return [Integer] Hash code
360
156
  def hash
361
- [self.class, family, side].hash
157
+ [abbr, side].hash
362
158
  end
363
159
 
364
- # Validate that the family is a valid Style Family symbol
160
+ # Returns an inspect string for the Identifier.
365
161
  #
366
- # @param family [Symbol] the family to validate
367
- # @raise [ArgumentError] if invalid
368
- def self.validate_family(family)
369
- return if VALID_FAMILIES.include?(family)
370
-
371
- raise ::ArgumentError, format(ERROR_INVALID_FAMILY, family.inspect)
162
+ # @return [String] Inspect representation
163
+ #
164
+ # @example
165
+ # Identifier.new(:C, :first).inspect # => "#<Sashite::Sin::Identifier C>"
166
+ def inspect
167
+ "#<#{self.class} #{self}>"
372
168
  end
373
169
 
374
- # Validate that the side follows SIN two-player constraint
375
- #
376
- # @param side [Symbol] the side to validate
377
- # @raise [ArgumentError] if invalid
378
- def self.validate_side(side)
379
- return if VALID_SIDES.include?(side)
170
+ private
380
171
 
381
- raise ::ArgumentError, format(ERROR_INVALID_SIDE, side.inspect)
382
- end
172
+ # ========================================================================
173
+ # Private Validation
174
+ # ========================================================================
383
175
 
384
- # Validate SIN string format against specification grammar
385
- #
386
- # @param string [String] string to validate
387
- # @raise [ArgumentError] if string doesn't match SIN pattern
388
- def self.validate_sin_string(string)
389
- return if string.match?(SIN_PATTERN)
176
+ def validate_abbr!(abbr)
177
+ return if ::Symbol === abbr && Constants::VALID_ABBRS.include?(abbr)
390
178
 
391
- raise ::ArgumentError, format(ERROR_INVALID_SIN, string)
179
+ raise Errors::Argument, Errors::Argument::Messages::INVALID_ABBR
392
180
  end
393
181
 
394
- private_class_method :validate_sin_string
395
-
396
- private
182
+ def validate_side!(side)
183
+ return if ::Symbol === side && Constants::VALID_SIDES.include?(side)
397
184
 
398
- # Get the opposite Player Assignment
399
- #
400
- # @return [Symbol] the opposite side
401
- def opposite_side
402
- first_player? ? SECOND_PLAYER : FIRST_PLAYER
185
+ raise Errors::Argument, Errors::Argument::Messages::INVALID_SIDE
403
186
  end
404
187
  end
405
188
  end