sashite-sin 2.0.1 → 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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 2b57d90bf310c7367cfb46d8fd7a927629c9daf51fea8fcb4c678abfe28bedba
4
- data.tar.gz: 535401969041a91f3d54f05e022ac7f0f78ea705075dccfd5f306b402f7fa8a7
3
+ metadata.gz: 28dcd89f0b19451fd1f6455117c8094083dd0da16300e59c3b8aad0987353cc2
4
+ data.tar.gz: a21dd87d8a1437cad75f0c55c74e7cce93a3d69897384346dcb0ecedf5f7f6c1
5
5
  SHA512:
6
- metadata.gz: 73e7edb26036041f09a93b5ae1f21c125ea34b8981dcc4d255674e2319d47bc733022e35d33d203792f5b750e9054695779c4e470d6500e87c7de35c2290653b
7
- data.tar.gz: 0b828c1a6e076de5c29e6eaf32149719cfd4a1fcc3a6f30fd276bd3afdbf923aee5aabb90812684a3f5c6d3106889422b76deaeadf60c7e9cff2a2dcdc7fdf80
6
+ metadata.gz: dc4c9ae269c163a60e65c052013154c1709b79e781077ccf16c084a540e13f1303d2122de46f956e2e79d312d6ff106b4081ce0200bfa4296fe0a9e9402dda49
7
+ data.tar.gz: bcfa66181f441356daa3f7bfb5dfaf609b629728d17c8c798d0ac4af26da3d159954e13e9aa9fb87483c0979ab8f3cb54b7f6bdb3cca870d2ae0a2819a0a0f89
data/README.md CHANGED
@@ -11,7 +11,7 @@
11
11
 
12
12
  SIN (Style Identifier Notation) provides a compact, ASCII-based format for identifying **styles** in abstract strategy board games. SIN uses single-character identifiers with case encoding to represent both style identity and player assignment simultaneously.
13
13
 
14
- This gem implements the [SIN Specification v1.0.0](https://sashite.dev/specs/sin/1.0.0/), providing a rule-agnostic notation system for style identification in board games.
14
+ This gem implements the [SIN Specification v1.0.0](https://sashite.dev/specs/sin/1.0.0/) exactly, providing a rule-agnostic notation system for style identification in board games.
15
15
 
16
16
  ## Installation
17
17
 
@@ -34,14 +34,15 @@ gem install sashite-sin
34
34
  require "sashite/sin"
35
35
 
36
36
  # Parse SIN strings into identifier objects
37
- identifier = Sashite::Sin.parse("C") # => #<Sin::Identifier letter=:C side=:first>
37
+ identifier = Sashite::Sin.parse("C") # Family=:C, Side=:first
38
38
  identifier.to_s # => "C"
39
- identifier.letter # => :C
39
+ identifier.family # => :C
40
40
  identifier.side # => :first
41
+ identifier.letter # => "C" (combined representation)
41
42
 
42
43
  # Create identifiers directly
43
- identifier = Sashite::Sin.identifier(:C, :first) # => #<Sin::Identifier letter=:C side=:first>
44
- identifier = Sashite::Sin::Identifier.new(:c, :second) # => #<Sin::Identifier letter=:c side=:second>
44
+ identifier = Sashite::Sin.identifier(:C, :first) # Family=:C, Side=:first
45
+ identifier = Sashite::Sin::Identifier.new(:C, :second) # Family=:C, Side=:second
45
46
 
46
47
  # Validate SIN strings
47
48
  Sashite::Sin.valid?("C") # => true
@@ -57,19 +58,19 @@ Sashite::Sin.valid?("CC") # => false (not single character)
57
58
  identifier = Sashite::Sin.parse("C")
58
59
 
59
60
  # Flip player assignment
60
- flipped = identifier.flip # => #<Sin::Identifier letter=:c side=:second>
61
+ flipped = identifier.flip # Family=:C, Side=:second
61
62
  flipped.to_s # => "c"
62
63
 
63
- # Change letter
64
- changed = identifier.with_letter(:S) # => #<Sin::Identifier letter=:S side=:first>
64
+ # Change family
65
+ changed = identifier.with_family(:S) # Family=:S, Side=:first
65
66
  changed.to_s # => "S"
66
67
 
67
68
  # Change side
68
- other_side = identifier.with_side(:second) # => #<Sin::Identifier letter=:c side=:second>
69
+ other_side = identifier.with_side(:second) # Family=:C, Side=:second
69
70
  other_side.to_s # => "c"
70
71
 
71
72
  # Chain transformations
72
- result = identifier.flip.with_letter(:M) # => #<Sin::Identifier letter=:m side=:second>
73
+ result = identifier.flip.with_family(:M) # Family=:M, Side=:second
73
74
  result.to_s # => "m"
74
75
  ```
75
76
 
@@ -82,17 +83,17 @@ opposite = Sashite::Sin.parse("s")
82
83
  # Player identification
83
84
  identifier.first_player? # => true
84
85
  identifier.second_player? # => false
85
- opposite.first_player? # => false
86
- opposite.second_player? # => true
86
+ opposite.first_player? # => false
87
+ opposite.second_player? # => true
87
88
 
88
- # Letter comparison
89
+ # Family and side comparison
89
90
  chess1 = Sashite::Sin.parse("C")
90
91
  chess2 = Sashite::Sin.parse("c")
91
92
  shogi = Sashite::Sin.parse("S")
92
93
 
93
- chess1.same_letter?(chess2) # => true (both use letter C)
94
+ chess1.same_family?(chess2) # => true (both Chess family)
94
95
  chess1.same_side?(shogi) # => true (both first player)
95
- chess1.same_letter?(shogi) # => false (different letters)
96
+ chess1.same_family?(shogi) # => false (different families)
96
97
  ```
97
98
 
98
99
  ### Identifier Collections
@@ -105,12 +106,12 @@ identifiers = %w[C c S s M m].map { |sin| Sashite::Sin.parse(sin) }
105
106
  first_player_identifiers = identifiers.select(&:first_player?)
106
107
  first_player_identifiers.map(&:to_s) # => ["C", "S", "M"]
107
108
 
108
- # Group by letter family
109
- by_letter = identifiers.group_by { |i| i.letter.to_s.upcase }
110
- by_letter["C"].size # => 2 (both C and c)
109
+ # Group by family
110
+ by_family = identifiers.group_by(&:family)
111
+ by_family[:C].size # => 2 (both C and c)
111
112
 
112
- # Find specific combinations
113
- chess_identifiers = identifiers.select { |i| i.letter.to_s.upcase == "C" }
113
+ # Find specific families
114
+ chess_identifiers = identifiers.select { |i| i.family == :C }
114
115
  chess_identifiers.map(&:to_s) # => ["C", "c"]
115
116
  ```
116
117
 
@@ -136,35 +137,60 @@ chess_identifiers.map(&:to_s) # => ["C", "c"]
136
137
 
137
138
  ### Style Attribute Mapping
138
139
 
140
+ SIN encodes style attributes using the following correspondence:
141
+
139
142
  | Style Attribute | SIN Encoding | Examples |
140
143
  |-----------------|--------------|----------|
141
- | **Style Family** | Letter choice | `C`/`c` = Chess family |
142
- | **Player Assignment** | Letter case | `C` = First player, `c` = Second player |
144
+ | **Family** | Style family symbol | `:C`, `:S`, `:X` |
145
+ | **Side** | Player assignment | `:first`, `:second` |
146
+ | **Letter** | Combined representation | `"C"`, `"c"`, `"S"`, `"s"` |
147
+
148
+ #### Dual-Purpose Encoding
143
149
 
144
- ## Game Examples
150
+ The **Letter** combines two distinct semantic components:
151
+ - **Style Family**: The underlying family symbol (:A-:Z), representing the game tradition or rule system
152
+ - **Player Assignment**: The side (:first or :second), encoded as case in the letter representation
145
153
 
146
- The SIN specification is rule-agnostic and does not define specific letter assignments. However, here are common usage patterns:
154
+ **Examples**:
155
+ - Family `:C` + Side `:first` → Letter `"C"` (Chess, First player)
156
+ - Family `:C` + Side `:second` → Letter `"c"` (Chess, Second player)
157
+ - Family `:S` + Side `:first` → Letter `"S"` (Shōgi, First player)
158
+ - Family `:S` + Side `:second` → Letter `"s"` (Shōgi, Second player)
147
159
 
148
- ### Traditional Game Families
160
+ ## Traditional Game Style Examples
161
+
162
+ The SIN specification is rule-agnostic and does not define specific letter assignments. However, here are common usage patterns following [SIN Examples](https://sashite.dev/specs/sin/1.0.0/examples/):
149
163
 
150
164
  ```ruby
151
- # Chess family identifiers
152
- chess_white = Sashite::Sin.parse("C") # First player, Chess family
153
- chess_black = Sashite::Sin.parse("c") # Second player, Chess family
165
+ # Chess (8×8 board)
166
+ chess_white = Sashite::Sin.parse("C") # First player (White pieces)
167
+ chess_black = Sashite::Sin.parse("c") # Second player (Black pieces)
168
+
169
+ # Shōgi (9×9 board)
170
+ shogi_sente = Sashite::Sin.parse("S") # First player (Sente 先手)
171
+ shogi_gote = Sashite::Sin.parse("s") # Second player (Gote 後手)
154
172
 
155
- # Shōgi family identifiers
156
- shogi_sente = Sashite::Sin.parse("S") # First player, Shōgi family
157
- shogi_gote = Sashite::Sin.parse("s") # Second player, Shōgi family
173
+ # Xiangqi (9×10 board)
174
+ xiangqi_red = Sashite::Sin.parse("X") # First player (Red pieces)
175
+ xiangqi_black = Sashite::Sin.parse("x") # Second player (Black pieces)
158
176
 
159
- # Xiangqi family identifiers
160
- xiangqi_red = Sashite::Sin.parse("X") # First player, Xiangqi family
161
- xiangqi_black = Sashite::Sin.parse("x") # Second player, Xiangqi family
177
+ # Makruk (8×8 board)
178
+ makruk_white = Sashite::Sin.parse("M") # First player (White pieces)
179
+ makruk_black = Sashite::Sin.parse("m") # Second player (Black pieces)
180
+
181
+ # Janggi (9×10 board)
182
+ janggi_cho = Sashite::Sin.parse("J") # First player (Cho 초)
183
+ janggi_han = Sashite::Sin.parse("j") # Second player (Han 한)
162
184
  ```
163
185
 
164
- ### Cross-Style Scenarios
186
+ ## Cross-Style Scenarios
165
187
 
166
188
  ```ruby
167
- # Different families in one match
189
+ # Chess vs. Ōgi Match (both 8×8 compatible)
190
+ chess_white = Sashite::Sin.parse("C") # Chess style, first player
191
+ ogi_black = Sashite::Sin.parse("o") # Ōgi style, second player
192
+
193
+ # Cross-Style Match Setup
168
194
  def create_hybrid_match
169
195
  [
170
196
  Sashite::Sin.parse("C"), # First player uses Chess family
@@ -173,22 +199,22 @@ def create_hybrid_match
173
199
  end
174
200
 
175
201
  identifiers = create_hybrid_match
176
- identifiers[0].same_side?(identifiers[1]) # => false (different players)
177
- identifiers[0].same_letter?(identifiers[1]) # => false (different families)
202
+ identifiers[0].same_side?(identifiers[1]) # => false (different players)
203
+ identifiers[0].same_family?(identifiers[1]) # => false (different families)
178
204
  ```
179
205
 
180
- ### Variant Families
206
+ ## System Constraints
181
207
 
182
- ```ruby
183
- # Different letters can represent variants within traditions
184
- makruk = Sashite::Sin.parse("M") # Makruk (Thai Chess) family
185
- janggi = Sashite::Sin.parse("J") # Janggi (Korean Chess) family
186
- ogi = Sashite::Sin.parse("O") # Ōgi (王棋) family
187
-
188
- # Each family can have both players
189
- makruk_black = makruk.flip # Second player Makruk
190
- makruk_black.to_s # => "m"
191
- ```
208
+ ### Character Limitation
209
+ SIN provides **26 possible identifiers** per player using ASCII letters (A-Z, a-z).
210
+
211
+ ### Player Limitation
212
+ SIN supports exactly **two players** through case distinction:
213
+ - **First player**: Uppercase letters (A-Z) → `:first`
214
+ - **Second player**: Lowercase letters (a-z) → `:second`
215
+
216
+ ### Context Dependency
217
+ The specific SIN assignment for a style may vary between different game contexts based on collision avoidance, historical precedence, and community conventions.
192
218
 
193
219
  ## API Reference
194
220
 
@@ -196,17 +222,18 @@ makruk_black.to_s # => "m"
196
222
 
197
223
  - `Sashite::Sin.valid?(sin_string)` - Check if string is valid SIN notation
198
224
  - `Sashite::Sin.parse(sin_string)` - Parse SIN string into Identifier object
199
- - `Sashite::Sin.identifier(letter, side)` - Create identifier instance directly
225
+ - `Sashite::Sin.identifier(family, side)` - Create identifier instance directly
200
226
 
201
227
  ### Identifier Class
202
228
 
203
229
  #### Creation and Parsing
204
- - `Sashite::Sin::Identifier.new(letter, side)` - Create identifier instance
230
+ - `Sashite::Sin::Identifier.new(family, side)` - Create identifier instance
205
231
  - `Sashite::Sin::Identifier.parse(sin_string)` - Parse SIN string
206
232
 
207
233
  #### Attribute Access
208
- - `#letter` - Get style letter (symbol :A through :z)
234
+ - `#family` - Get style family (symbol :A through :Z)
209
235
  - `#side` - Get player side (:first or :second)
236
+ - `#letter` - Get combined letter representation (string)
210
237
  - `#to_s` - Convert to SIN string representation
211
238
 
212
239
  #### Player Queries
@@ -215,42 +242,38 @@ makruk_black.to_s # => "m"
215
242
 
216
243
  #### Transformations (immutable - return new instances)
217
244
  - `#flip` - Switch player assignment
218
- - `#with_letter(new_letter)` - Create identifier with different letter
245
+ - `#with_family(new_family)` - Create identifier with different family
219
246
  - `#with_side(new_side)` - Create identifier with different side
220
247
 
221
248
  #### Comparison Methods
222
- - `#same_letter?(other)` - Check if same style letter (case-insensitive)
249
+ - `#same_family?(other)` - Check if same style family
223
250
  - `#same_side?(other)` - Check if same player side
224
251
  - `#==(other)` - Full equality comparison
252
+ - `#same_letter?(other)` - Alias for `same_family?` (deprecated)
225
253
 
226
- ### Identifier Class Constants
254
+ ### Constants
227
255
 
228
- - `Sashite::Sin::Identifier::FIRST_PLAYER` - Symbol for first player (:first)
229
- - `Sashite::Sin::Identifier::SECOND_PLAYER` - Symbol for second player (:second)
256
+ - `Sashite::Sin::Identifier::FIRST_PLAYER` - Symbol for first player (`:first`)
257
+ - `Sashite::Sin::Identifier::SECOND_PLAYER` - Symbol for second player (`:second`)
258
+ - `Sashite::Sin::Identifier::VALID_FAMILIES` - Array of valid families (`:A` to `:Z`)
230
259
  - `Sashite::Sin::Identifier::VALID_SIDES` - Array of valid sides
231
260
  - `Sashite::Sin::Identifier::SIN_PATTERN` - Regular expression for SIN validation
232
261
 
233
262
  ## Advanced Usage
234
263
 
235
- ### Letter Case and Side Mapping
264
+ ### Family and Side Separation
236
265
 
237
266
  ```ruby
238
- # SIN encodes player assignment through case
239
- upper_case_letters = ("A".."Z").map { |letter| Sashite::Sin.parse(letter) }
240
- lower_case_letters = ("a".."z").map { |letter| Sashite::Sin.parse(letter) }
241
-
242
- # All uppercase letters are first player
243
- upper_case_letters.all?(&:first_player?) # => true
244
-
245
- # All lowercase letters are second player
246
- lower_case_letters.all?(&:second_player?) # => true
247
-
248
- # Letter families are related by case
249
- letter_a_first = Sashite::Sin.parse("A")
250
- letter_a_second = Sashite::Sin.parse("a")
251
-
252
- letter_a_first.same_letter?(letter_a_second) # => true
253
- letter_a_first.same_side?(letter_a_second) # => false
267
+ # Clear separation of concerns
268
+ identifier = Sashite::Sin.parse("C")
269
+ identifier.family # => :C (Style Family - invariant)
270
+ identifier.side # => :first (Player Assignment)
271
+ identifier.letter # => "C" (Combined representation)
272
+
273
+ # Transformations are explicit
274
+ chess_white = Sashite::Sin.identifier(:C, :first)
275
+ shogi_white = chess_white.with_family(:S) # Change family, keep side
276
+ chess_black = chess_white.with_side(:second) # Change side, keep family
254
277
  ```
255
278
 
256
279
  ### Immutable Transformations
@@ -259,39 +282,29 @@ letter_a_first.same_side?(letter_a_second) # => false
259
282
  # All transformations return new instances
260
283
  original = Sashite::Sin.identifier(:C, :first)
261
284
  flipped = original.flip
262
- changed_letter = original.with_letter(:S)
285
+ changed_family = original.with_family(:S)
263
286
 
264
287
  # Original identifier is never modified
265
- original.to_s # => "C" (unchanged)
266
- flipped.to_s # => "c"
267
- changed_letter.to_s # => "S"
288
+ original.to_s # => "C" (unchanged)
289
+ flipped.to_s # => "c"
290
+ changed_family.to_s # => "S"
268
291
 
269
292
  # Transformations can be chained
270
- result = original.flip.with_letter(:M).flip
293
+ result = original.flip.with_family(:M).flip
271
294
  result.to_s # => "M"
272
295
  ```
273
296
 
274
- ## Protocol Mapping
275
-
276
- Following the [Sashité Protocol](https://sashite.dev/protocol/):
277
-
278
- | Protocol Attribute | SIN Encoding | Examples | Notes |
279
- |-------------------|--------------|----------|-------|
280
- | **Style Family** | Letter choice | `C`, `S`, `X` | Rule-agnostic letter assignment |
281
- | **Player Assignment** | Case encoding | `C` = First player, `c` = Second player | Case determines side |
282
-
283
- ## System Constraints
284
-
285
- - **26 possible identifiers** per player using ASCII letters (A-Z, a-z)
286
- - **Exactly 2 players** through case distinction
287
- - **Single character** per style-player combination
288
- - **Rule-agnostic** - no predefined letter meanings
289
-
290
297
  ## Design Properties
291
298
 
299
+ Following the SIN v1.0.0 specification, this implementation provides:
300
+
292
301
  - **ASCII compatibility**: Maximum portability across systems
293
302
  - **Rule-agnostic**: Independent of specific game mechanics
294
303
  - **Minimal overhead**: Single character per style-player combination
304
+ - **Flexible collision resolution**: Systematic approaches for identifier conflicts
305
+ - **Semantic clarity**: Distinct concepts for Family, Side, and Letter
306
+ - **SNN coordination**: Works harmoniously with formal style naming
307
+ - **Context-aware**: Adapts to avoid conflicts within specific game scenarios
295
308
  - **Canonical representation**: Each style-player combination has exactly one SIN identifier
296
309
  - **Immutable**: All identifier instances are frozen and transformations return new objects
297
310
  - **Functional**: Pure functions with no side effects
@@ -300,16 +313,13 @@ Following the [Sashité Protocol](https://sashite.dev/protocol/):
300
313
 
301
314
  - [SIN Specification v1.0.0](https://sashite.dev/specs/sin/1.0.0/) - Complete technical specification
302
315
  - [SIN Examples](https://sashite.dev/specs/sin/1.0.0/examples/) - Practical implementation examples
316
+ - [Style Name Notation (SNN)](https://sashite.dev/specs/snn/) - Formal naming for game styles
303
317
  - [Sashité Protocol](https://sashite.dev/protocol/) - Conceptual foundation for abstract strategy board games
304
- - [PIN](https://sashite.dev/specs/pin/) - Piece Identifier Notation
305
- - [PNN](https://sashite.dev/specs/pnn/) - Piece Name Notation (style-aware piece representation)
306
- - [QPI](https://sashite.dev/specs/qpi/) - Qualified Piece Identifier
307
318
 
308
319
  ## Documentation
309
320
 
310
321
  - [Official SIN Specification v1.0.0](https://sashite.dev/specs/sin/1.0.0/)
311
322
  - [SIN Examples Documentation](https://sashite.dev/specs/sin/1.0.0/examples/)
312
- - [Sashité Protocol Foundation](https://sashite.dev/protocol/)
313
323
  - [API Documentation](https://rubydoc.info/github/sashite/sin.rb/main)
314
324
 
315
325
  ## Development
@@ -329,16 +339,6 @@ ruby test.rb
329
339
  yard doc
330
340
  ```
331
341
 
332
- ## Contributing
333
-
334
- 1. Fork the repository
335
- 2. Create a feature branch (`git checkout -b feature/new-feature`)
336
- 3. Add tests for your changes
337
- 4. Ensure all tests pass (`ruby test.rb`)
338
- 5. Commit your changes (`git commit -am 'Add new feature'`)
339
- 6. Push to the branch (`git push origin feature/new-feature`)
340
- 7. Create a Pull Request
341
-
342
342
  ## License
343
343
 
344
344
  Available as open source under the [MIT License](https://opensource.org/licenses/MIT).
@@ -4,80 +4,170 @@ module Sashite
4
4
  module Sin
5
5
  # Represents an identifier in SIN (Style Identifier Notation) format.
6
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
+ #
7
22
  # An identifier consists of a single ASCII letter with case-based side encoding:
8
23
  # - Uppercase letter: first player (A, B, C, ..., Z)
9
24
  # - Lowercase letter: second player (a, b, c, ..., z)
10
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
37
+ #
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.
41
+ #
42
+ # ## Immutability
43
+ #
11
44
  # All instances are immutable - transformation methods return new instances.
12
- # This follows the SIN Specification v1.0.0 with Letter and Side attributes.
45
+ # This follows the SIN Specification v1.0.0 functional design principles.
46
+ #
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
51
+ #
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
13
68
  class Identifier
14
- # SIN validation pattern matching the specification
69
+ # SIN validation pattern matching the specification regular expression
70
+ # Grammar: <sin> ::= <uppercase-letter> | <lowercase-letter>
15
71
  SIN_PATTERN = /\A[A-Za-z]\z/
16
72
 
17
- # Player side constants
73
+ # Player side constants following SIN v1.0.0 two-player constraint
18
74
  FIRST_PLAYER = :first
19
75
  SECOND_PLAYER = :second
20
76
 
21
- # Valid sides
77
+ # Valid families (A-Z)
78
+ VALID_FAMILIES = (:A..:Z).to_a.freeze
79
+
80
+ # Valid sides array for validation
22
81
  VALID_SIDES = [FIRST_PLAYER, SECOND_PLAYER].freeze
23
82
 
24
- # Error messages
25
- ERROR_INVALID_SIN = "Invalid SIN string: %s"
26
- ERROR_INVALID_LETTER = "Letter must be a single ASCII letter symbol (A-Z, a-z), got: %s"
27
- ERROR_INVALID_SIDE = "Side must be :first or :second, got: %s"
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"
28
87
 
29
- # @return [Symbol] the style letter (single ASCII letter as symbol)
30
- attr_reader :letter
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
31
91
 
32
92
  # @return [Symbol] the player side (:first or :second)
93
+ # This represents the Player Assignment component
33
94
  attr_reader :side
34
95
 
35
- # Create a new identifier instance
96
+ # Create a new identifier instance with canonical representation
36
97
  #
37
- # @param letter [Symbol] style letter (single ASCII letter as symbol)
38
- # @param side [Symbol] player side (:first or :second)
98
+ # @param family [Symbol] style family (:A to :Z representing Style Family)
99
+ # @param side [Symbol] player side (:first or :second representing Player Assignment)
39
100
  # @raise [ArgumentError] if parameters are invalid
40
- def initialize(letter, side)
41
- self.class.validate_letter(letter)
101
+ #
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
106
+ #
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)
42
114
  self.class.validate_side(side)
43
115
 
44
- @letter = letter
116
+ @family = family
45
117
  @side = side
46
118
 
47
119
  freeze
48
120
  end
49
121
 
50
- # Parse an SIN string into an Identifier object
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
51
128
  #
52
129
  # @param sin_string [String] SIN notation string (single ASCII letter)
53
- # @return [Identifier] parsed identifier object with letter and inferred side
130
+ # @return [Identifier] parsed identifier object with Family and Side attributes
54
131
  # @raise [ArgumentError] if the SIN string is invalid
55
- # @example Parse SIN strings with case-based side inference
56
- # Sashite::Sin::Identifier.parse("C") # => #<Sin::Identifier letter=:C side=:first>
57
- # Sashite::Sin::Identifier.parse("c") # => #<Sin::Identifier letter=:c side=:second>
58
- # Sashite::Sin::Identifier.parse("S") # => #<Sin::Identifier letter=:S side=:first>
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)
59
147
  def self.parse(sin_string)
60
148
  string_value = String(sin_string)
61
149
  validate_sin_string(string_value)
62
150
 
63
- # Determine side from case
151
+ # Extract Style Family (case-insensitive) and Player Assignment (case-sensitive)
152
+ family_symbol = string_value.upcase.to_sym
64
153
  identifier_side = string_value == string_value.upcase ? FIRST_PLAYER : SECOND_PLAYER
65
154
 
66
- # Use the letter directly as symbol
67
- identifier_letter = string_value.to_sym
68
-
69
- new(identifier_letter, identifier_side)
155
+ new(family_symbol, identifier_side)
70
156
  end
71
157
 
72
- # Check if a string is a valid SIN notation
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>
73
162
  #
74
163
  # @param sin_string [String] the string to validate
75
164
  # @return [Boolean] true if valid SIN, false otherwise
76
165
  #
77
- # @example Validate SIN strings
78
- # Sashite::Sin::Identifier.valid?("C") # => true
79
- # Sashite::Sin::Identifier.valid?("c") # => true
80
- # Sashite::Sin::Identifier.valid?("CHESS") # => false (multi-character)
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)
81
171
  def self.valid?(sin_string)
82
172
  return false unless sin_string.is_a?(::String)
83
173
 
@@ -86,99 +176,179 @@ module Sashite
86
176
 
87
177
  # Convert the identifier to its SIN string representation
88
178
  #
179
+ # Returns the canonical SIN notation with proper case encoding for Player Assignment.
180
+ #
89
181
  # @return [String] SIN notation string (single ASCII letter)
90
- # @example Display identifiers
91
- # identifier.to_s # => "C" (first player, C family)
92
- # identifier.to_s # => "c" (second player, C family)
93
- # identifier.to_s # => "S" (first player, S family)
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)
94
187
  def to_s
95
- letter.to_s
188
+ letter
96
189
  end
97
190
 
98
- # Create a new identifier with opposite ownership (side)
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
202
+
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)
99
208
  #
100
- # @return [Identifier] new immutable identifier instance with flipped side
101
- # @example Flip player sides
102
- # identifier.flip # (:C, :first) => (:c, :second)
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
103
217
  def flip
104
- new_letter = first_player? ? letter.to_s.downcase.to_sym : letter.to_s.upcase.to_sym
105
- self.class.new(new_letter, opposite_side)
218
+ self.class.new(family, opposite_side)
106
219
  end
107
220
 
108
- # Create a new identifier with a different letter (keeping same side)
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
109
227
  #
110
- # @param new_letter [Symbol] new letter (single ASCII letter as symbol)
111
- # @return [Identifier] new immutable identifier instance with different letter
112
- # @example Change identifier letter
113
- # identifier.with_letter(:S) # (:C, :first) => (:S, :first)
114
- def with_letter(new_letter)
115
- self.class.validate_letter(new_letter)
116
- return self if letter == new_letter
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
117
237
 
118
- # Ensure the new letter has the correct case for the current side
119
- adjusted_letter = first_player? ? new_letter.to_s.upcase.to_sym : new_letter.to_s.downcase.to_sym
120
- self.class.new(adjusted_letter, side)
238
+ self.class.new(new_family, side)
121
239
  end
122
240
 
123
- # Create a new identifier with a different side (keeping same letter family)
241
+ # Create a new identifier with a different Player Assignment (keeping same Style Family)
242
+ #
243
+ # Changes the Player Assignment component while preserving Style Family.
124
244
  #
125
245
  # @param new_side [Symbol] :first or :second
126
- # @return [Identifier] new immutable identifier instance with different side
127
- # @example Change player side
128
- # identifier.with_side(:second) # (:C, :first) => (:c, :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
129
254
  def with_side(new_side)
130
255
  self.class.validate_side(new_side)
131
256
  return self if side == new_side
132
257
 
133
- # Adjust letter case for the new side
134
- new_letter = new_side == FIRST_PLAYER ? letter.to_s.upcase.to_sym : letter.to_s.downcase.to_sym
135
- self.class.new(new_letter, new_side)
258
+ self.class.new(family, new_side)
136
259
  end
137
260
 
138
261
  # Check if the identifier belongs to the first player
139
262
  #
140
- # @return [Boolean] true if first player
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
141
268
  def first_player?
142
269
  side == FIRST_PLAYER
143
270
  end
144
271
 
145
272
  # Check if the identifier belongs to the second player
146
273
  #
147
- # @return [Boolean] true if second player
274
+ # @return [Boolean] true if second player (lowercase letter)
275
+ #
276
+ # @example Player identification
277
+ # Sashite::Sin::Identifier.parse("c").second_player? # => true
278
+ # Sashite::Sin::Identifier.parse("C").second_player? # => false
148
279
  def second_player?
149
280
  side == SECOND_PLAYER
150
281
  end
151
282
 
152
- # Check if this identifier has the same letter family as another
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.
153
287
  #
154
288
  # @param other [Identifier] identifier to compare with
155
- # @return [Boolean] true if both identifiers use the same letter family (case-insensitive)
156
- # @example Compare identifier letter families
157
- # c_identifier.same_letter?(C_identifier) # (:c, :second) and (:C, :first) => true
158
- def same_letter?(other)
289
+ # @return [Boolean] true if both identifiers use the same Style Family
290
+ #
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
295
+ #
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)
159
299
  return false unless other.is_a?(self.class)
160
300
 
161
- letter.to_s.upcase == other.letter.to_s.upcase
301
+ family == other.family
162
302
  end
163
303
 
164
- # Check if this identifier belongs to the same side as another
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.
165
308
  #
166
309
  # @param other [Identifier] identifier to compare with
167
- # @return [Boolean] true if both identifiers belong to the same side
310
+ # @return [Boolean] true if both identifiers belong to the same Player Assignment
311
+ #
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)
168
319
  def same_side?(other)
169
320
  return false unless other.is_a?(self.class)
170
321
 
171
322
  side == other.side
172
323
  end
173
324
 
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
333
+
174
334
  # Custom equality comparison
175
335
  #
336
+ # Two identifiers are equal if they have identical Family and Side attributes.
337
+ #
176
338
  # @param other [Object] object to compare with
177
- # @return [Boolean] true if both objects are identifiers with identical letter and side
339
+ # @return [Boolean] true if both objects are identifiers with identical Family and Side
340
+ #
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")
345
+ #
346
+ # id1 == id2 # => true (identical Family and Side)
347
+ # id1 == id3 # => false (different Player Assignment)
178
348
  def ==(other)
179
349
  return false unless other.is_a?(self.class)
180
350
 
181
- letter == other.letter && side == other.side
351
+ family == other.family && side == other.side
182
352
  end
183
353
 
184
354
  # Alias for == to ensure Set functionality works correctly
@@ -186,22 +356,22 @@ module Sashite
186
356
 
187
357
  # Custom hash implementation for use in collections
188
358
  #
189
- # @return [Integer] hash value based on class, letter, and side
359
+ # @return [Integer] hash value based on class, Family, and Side
190
360
  def hash
191
- [self.class, letter, side].hash
361
+ [self.class, family, side].hash
192
362
  end
193
363
 
194
- # Validate that the letter is a valid single ASCII letter symbol
364
+ # Validate that the family is a valid Style Family symbol
195
365
  #
196
- # @param letter [Symbol] the letter to validate
366
+ # @param family [Symbol] the family to validate
197
367
  # @raise [ArgumentError] if invalid
198
- def self.validate_letter(letter)
199
- return if valid_letter?(letter)
368
+ def self.validate_family(family)
369
+ return if VALID_FAMILIES.include?(family)
200
370
 
201
- raise ::ArgumentError, format(ERROR_INVALID_LETTER, letter.inspect)
371
+ raise ::ArgumentError, format(ERROR_INVALID_FAMILY, family.inspect)
202
372
  end
203
373
 
204
- # Validate that the side is a valid symbol
374
+ # Validate that the side follows SIN two-player constraint
205
375
  #
206
376
  # @param side [Symbol] the side to validate
207
377
  # @raise [ArgumentError] if invalid
@@ -211,21 +381,7 @@ module Sashite
211
381
  raise ::ArgumentError, format(ERROR_INVALID_SIDE, side.inspect)
212
382
  end
213
383
 
214
- # Check if a letter is valid (single ASCII letter symbol)
215
- #
216
- # @param letter [Object] the letter to check
217
- # @return [Boolean] true if valid
218
- def self.valid_letter?(letter)
219
- return false unless letter.is_a?(::Symbol)
220
-
221
- letter_string = letter.to_s
222
- return false if letter_string.empty?
223
-
224
- # Must be exactly one ASCII letter
225
- letter_string.match?(SIN_PATTERN)
226
- end
227
-
228
- # Validate SIN string format
384
+ # Validate SIN string format against specification grammar
229
385
  #
230
386
  # @param string [String] string to validate
231
387
  # @raise [ArgumentError] if string doesn't match SIN pattern
@@ -235,11 +391,11 @@ module Sashite
235
391
  raise ::ArgumentError, format(ERROR_INVALID_SIN, string)
236
392
  end
237
393
 
238
- private_class_method :valid_letter?, :validate_sin_string
394
+ private_class_method :validate_sin_string
239
395
 
240
396
  private
241
397
 
242
- # Get the opposite side
398
+ # Get the opposite Player Assignment
243
399
  #
244
400
  # @return [Symbol] the opposite side
245
401
  def opposite_side
data/lib/sashite/sin.rb CHANGED
@@ -5,22 +5,118 @@ require_relative "sin/identifier"
5
5
  module Sashite
6
6
  # SIN (Style Identifier Notation) implementation for Ruby
7
7
  #
8
- # Provides a rule-agnostic format for identifying styles in abstract strategy board games.
9
- # SIN uses single ASCII letters with case-based side encoding, enabling clear
10
- # distinction between different style families in multi-style gaming environments.
11
- #
12
- # Format: <style-letter>
13
- # - Uppercase letter: First player styles (A, B, C, ..., Z)
14
- # - Lowercase letter: Second player styles (a, b, c, ..., z)
15
- # - Single character only: Each SIN identifier is exactly one ASCII letter
16
- #
17
- # Examples:
18
- # "C" - First player, C style family
19
- # "c" - Second player, C style family
20
- # "S" - First player, S style family
21
- # "s" - Second player, S style family
22
- #
23
- # @see https://sashite.dev/specs/sin/1.0.0/
8
+ # Provides a compact, ASCII-based format for identifying styles in abstract strategy board games.
9
+ # SIN uses single-character identifiers with case encoding to represent both style identity
10
+ # and player assignment simultaneously.
11
+ #
12
+ # ## Concept
13
+ #
14
+ # SIN addresses the fundamental need to identify which style system governs piece behavior
15
+ # while simultaneously indicating which player controls pieces of that style. In cross-style
16
+ # scenarios where different players use different game traditions, this dual encoding becomes
17
+ # essential for unambiguous piece identification.
18
+ #
19
+ # ## Dual-Purpose Encoding
20
+ #
21
+ # Each SIN identifier serves two functions:
22
+ # - **Style Family Identification**: The letter choice indicates which rule system applies
23
+ # - **Player Assignment**: The letter case indicates which player uses this style as their native system
24
+ #
25
+ # ## Format Specification
26
+ #
27
+ # Structure: `<style-letter>`
28
+ #
29
+ # Grammar (BNF):
30
+ # <sin> ::= <uppercase-letter> | <lowercase-letter>
31
+ # <uppercase-letter> ::= "A" | "B" | "C" | ... | "Z"
32
+ # <lowercase-letter> ::= "a" | "b" | "c" | ... | "z"
33
+ #
34
+ # Regular Expression: `/\A[A-Za-z]\z/`
35
+ #
36
+ # ## Style Attribute Mapping
37
+ #
38
+ # SIN encodes style attributes using the following correspondence:
39
+ #
40
+ # | Style Attribute | SIN Encoding | Examples |
41
+ # |-----------------|--------------|----------|
42
+ # | **Letter** | Single ASCII character | `C`, `c`, `S`, `s` |
43
+ # | **Style Family** | ASCII letter choice (A-Z) | `C`/`c` = Chess, `S`/`s` = Shōgi |
44
+ # | **Player Assignment** | Letter case | `C` = First player, `c` = Second player |
45
+ #
46
+ # The **Letter** attribute combines two distinct semantic components:
47
+ # - **Style Family**: The underlying ASCII character (A-Z), representing the game tradition or rule system
48
+ # - **Player Assignment**: The case of the character (uppercase/lowercase), representing which player uses this style
49
+ #
50
+ # ## Character Selection Conventions
51
+ #
52
+ # ### Primary Convention: First Letter
53
+ # By convention, SIN identifiers should preferably use the **first letter** of the corresponding SNN style name:
54
+ # - `Chess` → `C`/`c`
55
+ # - `Shogi` → `S`/`s`
56
+ # - `Xiangqi` → `X`/`x`
57
+ # - `Makruk` → `M`/`m`
58
+ # - `Janggi` → `J`/`j`
59
+ #
60
+ # ### Collision Resolution
61
+ # When multiple styles would claim the same first letter, systematic collision resolution applies
62
+ # using sequential letter selection from the SNN name until a unique identifier is found.
63
+ #
64
+ # ### Compatibility Groups
65
+ # Styles requiring incompatible board structures can safely share SIN letters since they
66
+ # cannot coexist in the same match.
67
+ #
68
+ # ## System Constraints
69
+ #
70
+ # - **26 possible identifiers** per player using ASCII letters
71
+ # - **Exactly 2 players** through case distinction:
72
+ # - First player: Uppercase letters (`A-Z`)
73
+ # - Second player: Lowercase letters (`a-z`)
74
+ # - **Single character** per style-player combination
75
+ # - **Rule-agnostic** - independent of specific game mechanics
76
+ #
77
+ # ## Examples
78
+ #
79
+ # ### Traditional Game Styles
80
+ #
81
+ # # Chess (8×8)
82
+ # chess_white = Sashite::Sin.parse("C") # First player (White pieces)
83
+ # chess_black = Sashite::Sin.parse("c") # Second player (Black pieces)
84
+ #
85
+ # # Shōgi (9×9)
86
+ # shogi_sente = Sashite::Sin.parse("S") # First player (Sente 先手)
87
+ # shogi_gote = Sashite::Sin.parse("s") # Second player (Gote 後手)
88
+ #
89
+ # # Xiangqi (9×10)
90
+ # xiangqi_red = Sashite::Sin.parse("X") # First player (Red pieces)
91
+ # xiangqi_black = Sashite::Sin.parse("x") # Second player (Black pieces)
92
+ #
93
+ # ### Cross-Style Scenarios
94
+ #
95
+ # # Chess vs. Ōgi Match (both 8×8 compatible)
96
+ # chess_style = Sashite::Sin.parse("C") # Chess style, first player
97
+ # ogi_style = Sashite::Sin.parse("o") # Ōgi style, second player
98
+ #
99
+ # ### All 26 Letters
100
+ #
101
+ # # First player identifiers (A-Z)
102
+ # ("A".."Z").each { |letter| Sashite::Sin.parse(letter).first_player? } # => all true
103
+ #
104
+ # # Second player identifiers (a-z)
105
+ # ("a".."z").each { |letter| Sashite::Sin.parse(letter).second_player? } # => all true
106
+ #
107
+ # ## Design Properties
108
+ #
109
+ # - **ASCII compatibility**: Maximum portability across systems
110
+ # - **Rule-agnostic**: Independent of specific game mechanics
111
+ # - **Minimal overhead**: Single character per style-player combination
112
+ # - **Flexible collision resolution**: Systematic approaches for identifier conflicts
113
+ # - **Semantic clarity**: Distinct concepts for Letter (Style Family + Player Assignment)
114
+ # - **SNN coordination**: Works harmoniously with formal style naming
115
+ # - **Context-aware**: Adapts to avoid conflicts within specific game scenarios
116
+ #
117
+ # @see https://sashite.dev/specs/sin/1.0.0/ SIN Specification v1.0.0
118
+ # @see https://sashite.dev/specs/sin/1.0.0/examples/ SIN Examples
119
+ # @see https://sashite.dev/specs/snn/ Style Name Notation (SNN)
24
120
  module Sin
25
121
  # Check if a string is a valid SIN notation
26
122
  #
@@ -28,38 +124,65 @@ module Sashite
28
124
  # @return [Boolean] true if valid SIN, false otherwise
29
125
  #
30
126
  # @example Validate various SIN formats
31
- # Sashite::Sin.valid?("C") # => true
32
- # Sashite::Sin.valid?("c") # => true
33
- # Sashite::Sin.valid?("CHESS") # => false (multi-character)
34
- # Sashite::Sin.valid?("1") # => false (not a letter)
127
+ # Sashite::Sin.valid?("C") # => true (Chess first player)
128
+ # Sashite::Sin.valid?("c") # => true (Chess second player)
129
+ # Sashite::Sin.valid?("S") # => true (Shōgi first player)
130
+ # Sashite::Sin.valid?("s") # => true (Shōgi second player)
131
+ # Sashite::Sin.valid?("CHESS") # => false (multi-character)
132
+ # Sashite::Sin.valid?("1") # => false (not a letter)
133
+ # Sashite::Sin.valid?("") # => false (empty string)
35
134
  def self.valid?(sin_string)
36
135
  Identifier.valid?(sin_string)
37
136
  end
38
137
 
39
138
  # Parse an SIN string into an Identifier object
40
139
  #
41
- # @param sin_string [String] SIN notation string
140
+ # The identifier will have both letter and side attributes inferred from the case:
141
+ # - Uppercase letter → first player (:first)
142
+ # - Lowercase letter → second player (:second)
143
+ #
144
+ # @param sin_string [String] SIN notation string (single ASCII letter)
42
145
  # @return [Sin::Identifier] parsed identifier object with letter and side attributes
43
146
  # @raise [ArgumentError] if the SIN string is invalid
44
- # @example Parse different SIN formats
45
- # Sashite::Sin.parse("C") # => #<Sin::Identifier letter=:C side=:first>
46
- # Sashite::Sin.parse("c") # => #<Sin::Identifier letter=:c side=:second>
47
- # Sashite::Sin.parse("S") # => #<Sin::Identifier letter=:S side=:first>
147
+ #
148
+ # @example Parse different SIN formats with dual-purpose encoding
149
+ # Sashite::Sin.parse("C") # => #<Sashite::Sin::Identifier @family=:C, @side=:first>
150
+ # Sashite::Sin.parse("c") # => #<Sashite::Sin::Identifier @family=:C, @side=:second>
151
+ # Sashite::Sin.parse("S") # => #<Sashite::Sin::Identifier @family=:S, @side=:first>
152
+ # Sashite::Sin.parse("s") # => #<Sashite::Sin::Identifier @family=:S, @side=:second>
153
+ #
154
+ # @example Traditional game styles
155
+ # chess_white = Sashite::Sin.parse("C") # Chess, first player (White)
156
+ # chess_black = Sashite::Sin.parse("c") # Chess, second player (Black)
157
+ # shogi_sente = Sashite::Sin.parse("S") # Shōgi, first player (Sente)
158
+ # shogi_gote = Sashite::Sin.parse("s") # Shōgi, second player (Gote)
48
159
  def self.parse(sin_string)
49
160
  Identifier.parse(sin_string)
50
161
  end
51
162
 
52
- # Create a new identifier instance
163
+ # Create a new identifier instance with canonical representation
53
164
  #
54
- # @param letter [Symbol] style letter (single ASCII letter as symbol)
165
+ # Ensures the letter case matches the specified side:
166
+ # - :first side → uppercase letter
167
+ # - :second side → lowercase letter
168
+ #
169
+ # @param family [Symbol] style family (:A to :Z representing Style Family)
55
170
  # @param side [Symbol] player side (:first or :second)
56
171
  # @return [Sin::Identifier] new immutable identifier instance
57
172
  # @raise [ArgumentError] if parameters are invalid
58
- # @example Create identifiers directly
59
- # Sashite::Sin.identifier(:C, :first) # => #<Sin::Identifier letter=:C side=:first>
60
- # Sashite::Sin.identifier(:s, :second) # => #<Sin::Identifier letter=:s side=:second>
61
- def self.identifier(letter, side)
62
- Identifier.new(letter, side)
173
+ #
174
+ # @example Create identifiers with family and side separation
175
+ # Sashite::Sin.identifier(:C, :first) # => #<Sashite::Sin::Identifier @family=:C, @side=:first>
176
+ # Sashite::Sin.identifier(:C, :second) # => #<Sashite::Sin::Identifier @family=:C, @side=:second>
177
+ #
178
+ # @example Style family and player assignment
179
+ # chess_first = Sashite::Sin.identifier(:C, :first) # Chess family, first player
180
+ # chess_second = Sashite::Sin.identifier(:C, :second) # Chess family, second player
181
+ #
182
+ # chess_first.same_family?(chess_second) # => true (same style family)
183
+ # chess_first.same_side?(chess_second) # => false (different players)
184
+ def self.identifier(family, side)
185
+ Identifier.new(family, side)
63
186
  end
64
187
  end
65
188
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: sashite-sin
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.0.1
4
+ version: 2.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Cyril Kato