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.
- checksums.yaml +4 -4
- data/LICENSE +201 -0
- data/README.md +115 -274
- data/lib/sashite/sin/constants.rb +38 -0
- data/lib/sashite/sin/errors/argument/messages.rb +45 -0
- data/lib/sashite/sin/errors/argument.rb +21 -0
- data/lib/sashite/sin/errors.rb +19 -0
- data/lib/sashite/sin/identifier.rb +113 -330
- data/lib/sashite/sin/parser.rb +137 -0
- data/lib/sashite/sin.rb +48 -166
- data/lib/sashite-sin.rb +0 -11
- metadata +14 -12
- data/LICENSE.md +0 -22
|
@@ -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
|
|
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
|
-
#
|
|
39
|
-
#
|
|
40
|
-
#
|
|
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
|
-
#
|
|
14
|
+
# Instances are immutable (frozen after creation).
|
|
43
15
|
#
|
|
44
|
-
#
|
|
45
|
-
#
|
|
16
|
+
# @example Creating identifiers
|
|
17
|
+
# sin = Identifier.new(:C, :first)
|
|
18
|
+
# sin = Identifier.new(:S, :second)
|
|
46
19
|
#
|
|
47
|
-
# @example
|
|
48
|
-
# #
|
|
49
|
-
#
|
|
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
|
-
#
|
|
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
|
-
#
|
|
70
|
-
|
|
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
|
|
81
|
-
VALID_SIDES =
|
|
29
|
+
# Valid side symbols.
|
|
30
|
+
VALID_SIDES = Constants::VALID_SIDES
|
|
82
31
|
|
|
83
|
-
#
|
|
84
|
-
|
|
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]
|
|
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
|
-
#
|
|
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
|
-
# @
|
|
103
|
-
#
|
|
104
|
-
#
|
|
105
|
-
#
|
|
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
|
|
108
|
-
#
|
|
109
|
-
#
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
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
|
-
@
|
|
52
|
+
@abbr = abbr
|
|
117
53
|
@side = side
|
|
118
54
|
|
|
119
55
|
freeze
|
|
120
56
|
end
|
|
121
57
|
|
|
122
|
-
#
|
|
123
|
-
#
|
|
124
|
-
#
|
|
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
|
-
#
|
|
62
|
+
# Returns the SIN string representation.
|
|
178
63
|
#
|
|
179
|
-
#
|
|
64
|
+
# @return [String] The single-character SIN string
|
|
180
65
|
#
|
|
181
|
-
# @
|
|
182
|
-
#
|
|
183
|
-
#
|
|
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
|
-
|
|
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
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
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
|
-
#
|
|
222
|
-
#
|
|
223
|
-
#
|
|
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
|
-
#
|
|
82
|
+
# Checks if the Identifier belongs to the first player.
|
|
242
83
|
#
|
|
243
|
-
#
|
|
84
|
+
# @return [Boolean] true if first player
|
|
244
85
|
#
|
|
245
|
-
# @
|
|
246
|
-
#
|
|
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
|
|
89
|
+
side.equal?(:first)
|
|
270
90
|
end
|
|
271
91
|
|
|
272
|
-
#
|
|
92
|
+
# Checks if the Identifier belongs to the second player.
|
|
273
93
|
#
|
|
274
|
-
# @return [Boolean] true if second player
|
|
94
|
+
# @return [Boolean] true if second player
|
|
275
95
|
#
|
|
276
|
-
# @example
|
|
277
|
-
#
|
|
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
|
|
99
|
+
side.equal?(:second)
|
|
281
100
|
end
|
|
282
101
|
|
|
283
|
-
#
|
|
284
|
-
#
|
|
285
|
-
#
|
|
286
|
-
|
|
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
|
-
# @
|
|
292
|
-
#
|
|
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
|
-
#
|
|
297
|
-
#
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
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
|
-
#
|
|
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]
|
|
310
|
-
# @return [Boolean] true if
|
|
121
|
+
# @param other [Identifier] The other Identifier to compare
|
|
122
|
+
# @return [Boolean] true if same side
|
|
311
123
|
#
|
|
312
|
-
# @example
|
|
313
|
-
#
|
|
314
|
-
#
|
|
315
|
-
#
|
|
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
|
-
|
|
321
|
-
|
|
322
|
-
side == other.side
|
|
129
|
+
side.equal?(other.side)
|
|
323
130
|
end
|
|
324
131
|
|
|
325
|
-
#
|
|
326
|
-
#
|
|
327
|
-
#
|
|
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
|
-
#
|
|
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
|
-
# @
|
|
342
|
-
#
|
|
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
|
-
#
|
|
347
|
-
#
|
|
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
|
|
146
|
+
return false unless self.class === other
|
|
350
147
|
|
|
351
|
-
|
|
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
|
-
#
|
|
153
|
+
# Returns a hash code for the Identifier.
|
|
358
154
|
#
|
|
359
|
-
# @return [Integer]
|
|
155
|
+
# @return [Integer] Hash code
|
|
360
156
|
def hash
|
|
361
|
-
[
|
|
157
|
+
[abbr, side].hash
|
|
362
158
|
end
|
|
363
159
|
|
|
364
|
-
#
|
|
160
|
+
# Returns an inspect string for the Identifier.
|
|
365
161
|
#
|
|
366
|
-
# @
|
|
367
|
-
#
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
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
|
-
|
|
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
|
-
|
|
382
|
-
|
|
172
|
+
# ========================================================================
|
|
173
|
+
# Private Validation
|
|
174
|
+
# ========================================================================
|
|
383
175
|
|
|
384
|
-
|
|
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 ::
|
|
179
|
+
raise Errors::Argument, Errors::Argument::Messages::INVALID_ABBR
|
|
392
180
|
end
|
|
393
181
|
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
private
|
|
182
|
+
def validate_side!(side)
|
|
183
|
+
return if ::Symbol === side && Constants::VALID_SIDES.include?(side)
|
|
397
184
|
|
|
398
|
-
|
|
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
|