sashite-snn 2.0.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 6fb3b57b1f252e95f7e7d3e8484d6729b90ca6f13dfd19705c9ac61d6095cf25
4
- data.tar.gz: 296ec73c256baf0ea7999b74b7933b0e95f92b2b7488e3d5efb69b43b7132951
3
+ metadata.gz: 1528b909fb1159a1b9d3fb06e4b62114efa110d991226945024f442fb08501e9
4
+ data.tar.gz: 3c6c44c3d69d1eacad6141d9309a27e5972dde45067bc8272e9584ed6cfc56ee
5
5
  SHA512:
6
- metadata.gz: 1e7b8270ca459703a6bd41f47424af55452f8c287567abc8bdf0e85acc69914f20a1830361ce10e3f0e157c137e3f31f60149481274b24e6dedb597d109927d6
7
- data.tar.gz: 9a684bb46685f7a77fbade6caaf2d642f31a6b31b142e1fb5a47512e8a4133fa50adf0f24c8b551d87c67232c23db6c5436e95d819a38ac72e069f37da63aff5
6
+ metadata.gz: fc20688ee514ec76125132aba5b121aa4b705230eb716f2abee6c5c1a88978aadfa52bf8cfa1c870315284050808f5fe5903a5e141c760af92f7aaca9bac82d5
7
+ data.tar.gz: b44e7af48ae53d52ca2b1961831b5eca677c2b8182589062139af966d3e960f0638720fba33c7e9099b6a8fe9424124f49386e3c9b025e71f93edfb797e9e558
data/README.md CHANGED
@@ -9,9 +9,13 @@
9
9
 
10
10
  ## What is SNN?
11
11
 
12
- SNN (Style Name Notation) provides a compact, ASCII-based format for identifying **styles** in abstract strategy board games. SNN uses single-character identifiers with case encoding to represent both style identity and player assignment simultaneously.
12
+ SNN (Style Name Notation) is a **foundational**, human-readable naming system for identifying game styles in abstract strategy board games. SNN serves as a primitive building block using descriptive alphabetic names with case encoding to represent style identity and player assignment.
13
13
 
14
- This gem implements the [SNN Specification v1.0.0](https://sashite.dev/specs/snn/1.0.0/), providing a rule-agnostic notation system for style identification in board games.
14
+ Each SNN name is a case-consistent alphabetic identifier where:
15
+ - **Uppercase names** (e.g., `CHESS`, `SHOGI`) represent the **first player's** style
16
+ - **Lowercase names** (e.g., `chess`, `shogi`) represent the **second player's** style
17
+
18
+ This gem implements the [SNN Specification v1.0.0](https://sashite.dev/specs/snn/1.0.0/) as a foundational primitive with no dependencies.
15
19
 
16
20
  ## Installation
17
21
 
@@ -26,399 +30,320 @@ Or install manually:
26
30
  gem install sashite-snn
27
31
  ```
28
32
 
29
- ## Usage
30
-
31
- ### Basic Operations
33
+ ## Quick Start
32
34
 
33
35
  ```ruby
34
36
  require "sashite/snn"
35
37
 
36
- # Parse SNN strings into style objects
37
- style = Sashite::Snn.parse("C") # => #<Snn::Style letter=:C side=:first>
38
- style.to_s # => "C"
39
- style.letter # => :C
40
- style.side # => :first
38
+ # Parse SNN strings into style name objects
39
+ name = Sashite::Snn.parse("SHOGI") # => #<Snn::Name value="SHOGI">
40
+ name.to_s # => "SHOGI"
41
+ name.value # => "SHOGI"
41
42
 
42
- # Create styles directly
43
- style = Sashite::Snn.style(:C, :first) # => #<Snn::Style letter=:C side=:first>
44
- style = Sashite::Snn::Style.new(:c, :second) # => #<Snn::Style letter=:c side=:second>
43
+ # Create from string or symbol
44
+ name = Sashite::Snn.name("CHESS") # => #<Snn::Name value="CHESS">
45
+ name = Sashite::Snn::Name.new(:xiangqi) # => #<Snn::Name value="xiangqi">
45
46
 
46
47
  # Validate SNN strings
47
- Sashite::Snn.valid?("C") # => true
48
- Sashite::Snn.valid?("c") # => true
49
- Sashite::Snn.valid?("1") # => false (not a letter)
50
- Sashite::Snn.valid?("CC") # => false (not single character)
48
+ Sashite::Snn.valid?("MAKRUK") # => true
49
+ Sashite::Snn.valid?("shogi") # => true
50
+ Sashite::Snn.valid?("Chess") # => false (mixed case)
51
+ Sashite::Snn.valid?("Chess960") # => false (contains digits)
51
52
  ```
52
53
 
53
- ### Style Transformations
54
-
55
- ```ruby
56
- # All transformations return new immutable instances
57
- style = Sashite::Snn.parse("C")
54
+ ## SNN Format
58
55
 
59
- # Flip player assignment
60
- flipped = style.flip # => #<Snn::Style letter=:c side=:second>
61
- flipped.to_s # => "c"
56
+ An SNN string consists of alphabetic characters only, all in the same case:
62
57
 
63
- # Change letter
64
- changed = style.with_letter(:S) # => #<Snn::Style letter=:S side=:first>
65
- changed.to_s # => "S"
66
-
67
- # Change side
68
- other_side = style.with_side(:second) # => #<Snn::Style letter=:c side=:second>
69
- other_side.to_s # => "c"
70
-
71
- # Chain transformations
72
- result = style.flip.with_letter(:M) # => #<Snn::Style letter=:m side=:second>
73
- result.to_s # => "m"
74
58
  ```
75
-
76
- ### Player and Style Queries
77
-
78
- ```ruby
79
- style = Sashite::Snn.parse("C")
80
- opposite = Sashite::Snn.parse("s")
81
-
82
- # Player identification
83
- style.first_player? # => true
84
- style.second_player? # => false
85
- opposite.first_player? # => false
86
- opposite.second_player? # => true
87
-
88
- # Letter comparison
89
- chess1 = Sashite::Snn.parse("C")
90
- chess2 = Sashite::Snn.parse("c")
91
- shogi = Sashite::Snn.parse("S")
92
-
93
- chess1.same_letter?(chess2) # => true (both use letter C)
94
- chess1.same_side?(shogi) # => true (both first player)
95
- chess1.same_letter?(shogi) # => false (different letters)
59
+ <alphabetic-name>
96
60
  ```
97
61
 
98
- ### Style Collections
99
-
100
- ```ruby
101
- # Working with multiple styles
102
- styles = %w[C c S s M m].map { |snn| Sashite::Snn.parse(snn) }
103
-
104
- # Filter by player
105
- first_player_styles = styles.select(&:first_player?)
106
- first_player_styles.map(&:to_s) # => ["C", "S", "M"]
107
-
108
- # Group by letter family
109
- by_letter = styles.group_by { |s| s.letter.to_s.upcase }
110
- by_letter["C"].size # => 2 (both C and c)
111
-
112
- # Find specific combinations
113
- chess_styles = styles.select { |s| s.letter.to_s.upcase == "C" }
114
- chess_styles.map(&:to_s) # => ["C", "c"]
115
- ```
62
+ **Examples:**
63
+ - `CHESS` — Chess style for first player
64
+ - `chess` — Chess style for second player
65
+ - `SHOGI` Shōgi style for first player
66
+ - `shogi` Shōgi style for second player
116
67
 
117
68
  ## Format Specification
118
69
 
119
70
  ### Structure
71
+
120
72
  ```
121
- <style-letter>
73
+ <alphabetic-name>
122
74
  ```
123
75
 
76
+ Where the name directly represents the style identity and player assignment through case.
77
+
124
78
  ### Grammar (BNF)
79
+
125
80
  ```bnf
126
- <snn> ::= <uppercase-letter> | <lowercase-letter>
81
+ <snn> ::= <uppercase-name> | <lowercase-name>
82
+
83
+ <uppercase-name> ::= <uppercase-letter>+
84
+ <lowercase-name> ::= <lowercase-letter>+
127
85
 
128
86
  <uppercase-letter> ::= "A" | "B" | "C" | ... | "Z"
129
87
  <lowercase-letter> ::= "a" | "b" | "c" | ... | "z"
130
88
  ```
131
89
 
132
90
  ### Regular Expression
91
+
133
92
  ```ruby
134
- /\A[A-Za-z]\z/
93
+ /\A([A-Z]+|[a-z]+)\z/
135
94
  ```
136
95
 
137
- ### Style Attribute Mapping
96
+ ### Constraints
138
97
 
139
- | Style Attribute | SNN Encoding | Examples |
140
- |-----------------|--------------|----------|
141
- | **Style Family** | Letter choice | `C`/`c` = Chess family |
142
- | **Player Assignment** | Letter case | `C` = First player, `c` = Second player |
98
+ 1. **Case consistency**: All letters must be either uppercase OR lowercase
99
+ 2. **Alphabetic only**: Only ASCII letters allowed (no digits, no special characters)
100
+ 3. **Direct assignment**: Names represent styles through explicit association
143
101
 
144
- ## Game Examples
102
+ ## API Reference
145
103
 
146
- The SNN specification is rule-agnostic and does not define specific letter assignments. However, here are common usage patterns:
104
+ ### Module Methods
147
105
 
148
- ### Traditional Game Families
106
+ #### `Sashite::Snn.valid?(string)`
149
107
 
150
- ```ruby
151
- # Chess family styles
152
- chess_white = Sashite::Snn.parse("C") # First player, Chess family
153
- chess_black = Sashite::Snn.parse("c") # Second player, Chess family
108
+ Returns `true` if the string is valid SNN notation.
154
109
 
155
- # Shōgi family styles
156
- shogi_sente = Sashite::Snn.parse("S") # First player, Shōgi family
157
- shogi_gote = Sashite::Snn.parse("s") # Second player, Shōgi family
110
+ - **Parameter**: `string` (String) - String to validate
111
+ - **Returns**: `Boolean` - `true` if valid, `false` otherwise
158
112
 
159
- # Xiangqi family styles
160
- xiangqi_red = Sashite::Snn.parse("X") # First player, Xiangqi family
161
- xiangqi_black = Sashite::Snn.parse("x") # Second player, Xiangqi family
113
+ ```ruby
114
+ Sashite::Snn.valid?("CHESS") # => true
115
+ Sashite::Snn.valid?("shogi") # => true
116
+ Sashite::Snn.valid?("Chess") # => false (mixed case)
117
+ Sashite::Snn.valid?("CHESS960") # => false (contains digits)
118
+ Sashite::Snn.valid?("3DChess") # => false (starts with digit)
162
119
  ```
163
120
 
164
- ### Cross-Style Scenarios
121
+ #### `Sashite::Snn.parse(string)`
165
122
 
166
- ```ruby
167
- # Different families in one match
168
- def create_hybrid_match
169
- [
170
- Sashite::Snn.parse("C"), # First player uses Chess family
171
- Sashite::Snn.parse("s") # Second player uses Shōgi family
172
- ]
173
- end
123
+ Parses an SNN string into a `Name` object.
174
124
 
175
- styles = create_hybrid_match
176
- styles[0].same_side?(styles[1]) # => false (different players)
177
- styles[0].same_letter?(styles[1]) # => false (different families)
178
- ```
179
-
180
- ### Variant Families
125
+ - **Parameter**: `string` (String) - SNN notation string
126
+ - **Returns**: `Name` - Immutable name object
127
+ - **Raises**: `ArgumentError` if the string is invalid
181
128
 
182
129
  ```ruby
183
- # Different letters can represent variants within traditions
184
- makruk = Sashite::Snn.parse("M") # Makruk (Thai Chess) family
185
- janggi = Sashite::Snn.parse("J") # Janggi (Korean Chess) family
186
- ogi = Sashite::Snn.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"
130
+ name = Sashite::Snn.parse("SHOGI") # => #<Snn::Name value="SHOGI">
131
+ name = Sashite::Snn.parse("chess") # => #<Snn::Name value="chess">
191
132
  ```
192
133
 
193
- ## API Reference
134
+ #### `Sashite::Snn.name(value)`
194
135
 
195
- ### Main Module Methods
136
+ Creates a new `Name` instance directly.
196
137
 
197
- - `Sashite::Snn.valid?(snn_string)` - Check if string is valid SNN notation
198
- - `Sashite::Snn.parse(snn_string)` - Parse SNN string into Style object
199
- - `Sashite::Snn.style(letter, side)` - Create style instance directly
138
+ - **Parameter**: `value` (String, Symbol) - Style name to construct
139
+ - **Returns**: `Name` - New name instance
140
+ - **Raises**: `ArgumentError` if name format is invalid
200
141
 
201
- ### Style Class
202
-
203
- #### Creation and Parsing
204
- - `Sashite::Snn::Style.new(letter, side)` - Create style instance
205
- - `Sashite::Snn::Style.parse(snn_string)` - Parse SNN string
142
+ ```ruby
143
+ Sashite::Snn.name("XIANGQI") # => #<Snn::Name value="XIANGQI">
144
+ Sashite::Snn.name(:makruk) # => #<Snn::Name value="makruk">
145
+ ```
206
146
 
207
- #### Attribute Access
208
- - `#letter` - Get style letter (symbol :A through :z)
209
- - `#side` - Get player side (:first or :second)
210
- - `#to_s` - Convert to SNN string representation
147
+ ### Name Object
211
148
 
212
- #### Player Queries
213
- - `#first_player?` - Check if first player style
214
- - `#second_player?` - Check if second player style
149
+ The `Name` object is immutable and provides read-only access to the style name:
215
150
 
216
- #### Transformations (immutable - return new instances)
217
- - `#flip` - Switch player assignment
218
- - `#with_letter(new_letter)` - Create style with different letter
219
- - `#with_side(new_side)` - Create style with different side
151
+ ```ruby
152
+ name = Sashite::Snn.parse("SHOGI")
220
153
 
221
- #### Comparison Methods
222
- - `#same_letter?(other)` - Check if same style letter (case-insensitive)
223
- - `#same_side?(other)` - Check if same player side
224
- - `#==(other)` - Full equality comparison
154
+ name.value # => "SHOGI"
155
+ name.to_s # => "SHOGI"
156
+ name.frozen? # => true
157
+ ```
225
158
 
226
- ### Style Class Constants
159
+ **Equality and hashing:**
160
+ ```ruby
161
+ name1 = Sashite::Snn.parse("CHESS")
162
+ name2 = Sashite::Snn.parse("CHESS")
163
+ name3 = Sashite::Snn.parse("chess")
227
164
 
228
- - `Sashite::Snn::Style::FIRST_PLAYER` - Symbol for first player (:first)
229
- - `Sashite::Snn::Style::SECOND_PLAYER` - Symbol for second player (:second)
230
- - `Sashite::Snn::Style::VALID_SIDES` - Array of valid sides
231
- - `Sashite::Snn::Style::SNN_PATTERN` - Regular expression for SNN validation
165
+ name1 == name2 # => true
166
+ name1.hash == name2.hash # => true
167
+ name1 == name3 # => false (different case = different player)
168
+ ```
232
169
 
233
- ## Advanced Usage
170
+ ## Examples
234
171
 
235
- ### Letter Case and Side Mapping
172
+ ### Traditional Chess Family
236
173
 
237
174
  ```ruby
238
- # SNN encodes player assignment through case
239
- upper_case_letters = ("A".."Z").map { |letter| Sashite::Snn.parse(letter) }
240
- lower_case_letters = ("a".."z").map { |letter| Sashite::Snn.parse(letter) }
175
+ # First player styles (uppercase)
176
+ chess = Sashite::Snn.parse("CHESS") # Western Chess
177
+ shogi = Sashite::Snn.parse("SHOGI") # Japanese Chess
178
+ xiangqi = Sashite::Snn.parse("XIANGQI") # Chinese Chess
179
+ makruk = Sashite::Snn.parse("MAKRUK") # Thai Chess
180
+
181
+ # Second player styles (lowercase)
182
+ chess_p2 = Sashite::Snn.parse("chess")
183
+ shogi_p2 = Sashite::Snn.parse("shogi")
184
+ ```
241
185
 
242
- # All uppercase letters are first player
243
- upper_case_letters.all?(&:first_player?) # => true
186
+ ### Historical Games
244
187
 
245
- # All lowercase letters are second player
246
- lower_case_letters.all?(&:second_player?) # => true
188
+ ```ruby
189
+ chaturanga = Sashite::Snn.parse("CHATURANGA") # Ancient Indian Chess
190
+ shatranj = Sashite::Snn.parse("SHATRANJ") # Medieval Islamic Chess
191
+ ```
247
192
 
248
- # Letter families are related by case
249
- letter_a_first = Sashite::Snn.parse("A")
250
- letter_a_second = Sashite::Snn.parse("a")
193
+ ### Modern Variants
251
194
 
252
- letter_a_first.same_letter?(letter_a_second) # => true
253
- letter_a_first.same_side?(letter_a_second) # => false
195
+ ```ruby
196
+ raumschach = Sashite::Snn.parse("RAUMSCHACH") # 3D Chess
197
+ omega = Sashite::Snn.parse("OMEGA") # Omega Chess
254
198
  ```
255
199
 
256
- ### Immutable Transformations
200
+ ### Case Consistency
257
201
 
258
202
  ```ruby
259
- # All transformations return new instances
260
- original = Sashite::Snn.style(:C, :first)
261
- flipped = original.flip
262
- changed_letter = original.with_letter(:S)
263
-
264
- # Original style is never modified
265
- original.to_s # => "C" (unchanged)
266
- flipped.to_s # => "c"
267
- changed_letter.to_s # => "S"
268
-
269
- # Transformations can be chained
270
- result = original.flip.with_letter(:M).flip
271
- result.to_s # => "M"
203
+ # Valid - all uppercase
204
+ Sashite::Snn.valid?("CHESS") # => true
205
+ Sashite::Snn.valid?("XIANGQI") # => true
206
+
207
+ # Valid - all lowercase
208
+ Sashite::Snn.valid?("shogi") # => true
209
+ Sashite::Snn.valid?("makruk") # => true
210
+
211
+ # Invalid - mixed case
212
+ Sashite::Snn.valid?("Chess") # => false
213
+ Sashite::Snn.valid?("Shogi") # => false
214
+ Sashite::Snn.valid?("XiangQi") # => false
215
+
216
+ # Invalid - contains non-alphabetic characters
217
+ Sashite::Snn.valid?("CHESS960") # => false
218
+ Sashite::Snn.valid?("GO9X9") # => false
219
+ Sashite::Snn.valid?("MINI_SHOGI") # => false
272
220
  ```
273
221
 
274
- ### Game Configuration Management
222
+ ### Working with Names
275
223
 
276
224
  ```ruby
277
- class GameConfiguration
278
- def initialize
279
- @player_styles = {}
280
- end
281
-
282
- def set_player_style(player, letter)
283
- side = player == :white ? :first : :second
284
- @player_styles[player] = Sashite::Snn.style(letter, side)
285
- end
286
-
287
- def get_player_style(player)
288
- @player_styles[player]
289
- end
290
-
291
- def cross_family_match?
292
- return false if @player_styles.size < 2
293
-
294
- styles = @player_styles.values
295
- !styles.all? { |style| style.same_letter?(styles.first) }
296
- end
297
-
298
- def same_family_match?
299
- !cross_family_match?
300
- end
301
- end
302
-
303
- # Usage
304
- config = GameConfiguration.new
305
- config.set_player_style(:white, :C) # Chess family, first player
306
- config.set_player_style(:black, :S) # Shōgi family, second player
225
+ # Create and compare
226
+ name1 = Sashite::Snn.parse("SHOGI")
227
+ name2 = Sashite::Snn.parse("SHOGI")
228
+ name1 == name2 # => true
229
+
230
+ # String and symbol inputs
231
+ name1 = Sashite::Snn.name("XIANGQI")
232
+ name2 = Sashite::Snn.name(:XIANGQI)
233
+ name1 == name2 # => true
234
+
235
+ # Immutability
236
+ name = Sashite::Snn.parse("CHESS")
237
+ name.frozen? # => true
238
+ name.value.frozen? # => true
239
+ ```
307
240
 
308
- config.cross_family_match? # => true
241
+ ### Collections
309
242
 
310
- white_style = config.get_player_style(:white)
311
- white_style.to_s # => "C"
243
+ ```ruby
244
+ # Create a set of styles
245
+ styles = %w[CHESS SHOGI XIANGQI MAKRUK].map { |n| Sashite::Snn.parse(n) }
246
+
247
+ # Filter by prefix
248
+ styles.select { |s| s.value.start_with?("X") }.map(&:to_s)
249
+ # => ["XIANGQI"]
250
+
251
+ # Use in hash
252
+ style_map = {
253
+ Sashite::Snn.parse("CHESS") => "Western Chess",
254
+ Sashite::Snn.parse("SHOGI") => "Japanese Chess"
255
+ }
312
256
  ```
313
257
 
314
- ### Style Analysis
258
+ ## Relationship with SIN
315
259
 
316
- ```ruby
317
- def analyze_styles(snns)
318
- styles = snns.map { |snn| Sashite::Snn.parse(snn) }
319
-
320
- {
321
- total: styles.size,
322
- by_side: styles.group_by(&:side),
323
- by_letter: styles.group_by { |s| s.letter.to_s.upcase },
324
- unique_letters: styles.map { |s| s.letter.to_s.upcase }.uniq.size,
325
- cross_family: styles.map { |s| s.letter.to_s.upcase }.uniq.size > 1
326
- }
327
- end
260
+ **SNN and SIN are independent primitives** that serve complementary roles:
328
261
 
329
- snns = %w[C c S s X x]
330
- analysis = analyze_styles(snns)
331
- analysis[:by_side][:first].size # => 3
332
- analysis[:unique_letters] # => 3
333
- analysis[:cross_family] # => true
334
- ```
262
+ - **SNN**: Human-readable, descriptive names (`CHESS`, `SHOGI`)
263
+ - **SIN**: Compact, single-character identification (`C`, `S`)
335
264
 
336
- ### Tournament Style Registry
265
+ ### Optional Correspondence
266
+
267
+ While both specifications can be used independently, they may be related through:
268
+
269
+ - **Mapping tables**: External context defining SNN ↔ SIN relationships
270
+ - **Case consistency**: When mapped, case must be preserved (`CHESS` ↔ `C`, `chess` ↔ `c`)
337
271
 
338
272
  ```ruby
339
- class TournamentStyleRegistry
340
- def initialize
341
- @registered_styles = Set.new
342
- end
343
-
344
- def register_letter(letter)
345
- # Register both sides of a letter family
346
- first_player_style = Sashite::Snn.style(letter.to_s.upcase.to_sym, :first)
347
- second_player_style = first_player_style.flip
348
-
349
- @registered_styles.add(first_player_style)
350
- @registered_styles.add(second_player_style)
351
-
352
- [first_player_style, second_player_style]
353
- end
354
-
355
- def valid_pairing?(style1, style2)
356
- @registered_styles.include?(style1) &&
357
- @registered_styles.include?(style2) &&
358
- !style1.same_side?(style2)
359
- end
360
-
361
- def available_styles_for_side(side)
362
- @registered_styles.select { |style| style.side == side }
363
- end
364
-
365
- def supported_families
366
- @registered_styles.map { |s| s.letter.to_s.upcase }.uniq.sort
367
- end
368
- end
273
+ # Example mapping (defined externally, not part of SNN)
274
+ SNN_TO_SIN = {
275
+ "CHESS" => "C",
276
+ "chess" => "c",
277
+ "SHOGI" => "S",
278
+ "shogi" => "s"
279
+ }
280
+
281
+ # Multiple SNN names may map to the same SIN
282
+ SNN_TO_SIN["CAPABLANCA"] = "C" # Also maps to "C"
283
+ SNN_TO_SIN["COURIER"] = "C" # Also maps to "C"
284
+ ```
285
+
286
+ ### Important Notes
369
287
 
370
- # Usage
371
- registry = TournamentStyleRegistry.new
372
- registry.register_letter(:C)
373
- registry.register_letter(:S)
288
+ 1. **No dependency**: SNN does not depend on SIN, nor SIN on SNN
289
+ 2. **Bidirectional mapping requires context**: Converting between SNN and SIN requires external mapping information
290
+ 3. **Independent usage**: Systems may use SNN alone, SIN alone, or both with defined mappings
291
+ 4. **Multiple mappings**: One SNN name may correspond to multiple SIN characters in different contexts, and vice versa
374
292
 
375
- chess_white = Sashite::Snn.parse("C")
376
- shogi_black = Sashite::Snn.parse("s")
293
+ ## Error Handling
377
294
 
378
- registry.valid_pairing?(chess_white, shogi_black) # => true
379
- registry.supported_families # => ["C", "S"]
295
+ ```ruby
296
+ begin
297
+ name = Sashite::Snn.parse("Chess960")
298
+ rescue ArgumentError => e
299
+ warn "Invalid SNN: #{e.message}"
300
+ # => "Invalid SNN string: \"Chess960\""
301
+ end
380
302
  ```
381
303
 
382
- ## Protocol Mapping
304
+ ### Common Errors
383
305
 
384
- Following the [Sashité Protocol](https://sashite.dev/protocol/):
306
+ ```ruby
307
+ # Mixed case
308
+ Sashite::Snn.parse("Chess")
309
+ # => ArgumentError: Invalid SNN string: "Chess"
385
310
 
386
- | Protocol Attribute | SNN Encoding | Examples | Notes |
387
- |-------------------|--------------|----------|-------|
388
- | **Style Family** | Letter choice | `C`, `S`, `X` | Rule-agnostic letter assignment |
389
- | **Player Assignment** | Case encoding | `C` = First player, `c` = Second player | Case determines side |
311
+ # Contains digits
312
+ Sashite::Snn.parse("CHESS960")
313
+ # => ArgumentError: Invalid SNN string: "CHESS960"
390
314
 
391
- ## System Constraints
315
+ # Contains special characters
316
+ Sashite::Snn.parse("MINI_SHOGI")
317
+ # => ArgumentError: Invalid SNN string: "MINI_SHOGI"
392
318
 
393
- - **26 possible identifiers** per player using ASCII letters (A-Z, a-z)
394
- - **Exactly 2 players** through case distinction
395
- - **Single character** per style-player combination
396
- - **Rule-agnostic** - no predefined letter meanings
319
+ # Empty string
320
+ Sashite::Snn.parse("")
321
+ # => ArgumentError: Invalid SNN string: ""
322
+ ```
397
323
 
398
- ## Design Properties
324
+ ## Design Principles
399
325
 
400
- - **ASCII compatibility**: Maximum portability across systems
326
+ - **Human-readable**: Descriptive names for better usability
327
+ - **Case-consistent**: Visual distinction between players through case
328
+ - **Foundational primitive**: Serves as building block for formal style identification
401
329
  - **Rule-agnostic**: Independent of specific game mechanics
402
- - **Minimal overhead**: Single character per style-player combination
403
- - **Canonical representation**: Each style-player combination has exactly one SNN identifier
404
- - **Immutable**: All style instances are frozen and transformations return new objects
405
- - **Functional**: Pure functions with no side effects
330
+ - **Self-contained**: No external dependencies
331
+ - **Immutable**: All objects are frozen and thread-safe
332
+ - **Canonical**: Each style has one valid representation per context
406
333
 
407
- ## Related Specifications
334
+ ## Properties
408
335
 
409
- - [SNN Specification v1.0.0](https://sashite.dev/specs/snn/1.0.0/) - Complete technical specification
410
- - [SNN Examples](https://sashite.dev/specs/snn/1.0.0/examples/) - Practical implementation examples
411
- - [Sashité Protocol](https://sashite.dev/protocol/) - Conceptual foundation for abstract strategy board games
412
- - [PIN](https://sashite.dev/specs/pin/) - Piece Identifier Notation
413
- - [PNN](https://sashite.dev/specs/pnn/) - Piece Name Notation (style-aware piece representation)
414
- - [QPI](https://sashite.dev/specs/qpi/) - Qualified Piece Identifier
336
+ - **Purely functional**: Immutable data structures, no side effects
337
+ - **Specification compliant**: Strict adherence to [SNN v1.0.0](https://sashite.dev/specs/snn/1.0.0/)
338
+ - **Minimal API**: Simple validation, parsing, and comparison
339
+ - **Universal**: Supports any abstract strategy board game style
340
+ - **No dependencies**: Foundational primitive requiring no external gems
415
341
 
416
342
  ## Documentation
417
343
 
418
- - [Official SNN Specification v1.0.0](https://sashite.dev/specs/snn/1.0.0/)
419
- - [SNN Examples Documentation](https://sashite.dev/specs/snn/1.0.0/examples/)
420
- - [Sashité Protocol Foundation](https://sashite.dev/protocol/)
421
- - [API Documentation](https://rubydoc.info/github/sashite/snn.rb/main)
344
+ - [SNN Specification v1.0.0](https://sashite.dev/specs/snn/1.0.0/) — Complete technical specification
345
+ - [SNN Examples](https://sashite.dev/specs/snn/1.0.0/examples/) — Comprehensive examples
346
+ - [API Documentation](https://rubydoc.info/github/sashite/snn.rb/main) — Full API reference
422
347
 
423
348
  ## Development
424
349
 
@@ -0,0 +1,170 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Sashite
4
+ module Snn
5
+ # Represents a style name in SNN (Style Name Notation) format.
6
+ #
7
+ # SNN provides a foundational naming system for abstract strategy game styles.
8
+ # Each name must consist of alphabetic characters only, all in the same case
9
+ # (either all uppercase or all lowercase).
10
+ #
11
+ # Case encoding:
12
+ # - UPPERCASE names represent the first player's style
13
+ # - lowercase names represent the second player's style
14
+ #
15
+ # Constraints:
16
+ # - Alphabetic characters only (A-Z or a-z)
17
+ # - Case consistency required (all uppercase OR all lowercase)
18
+ # - No digits, no special characters, no mixed case
19
+ #
20
+ # All instances are immutable.
21
+ #
22
+ # @example Valid names
23
+ # Sashite::Snn::Name.new("CHESS") # => #<Snn::Name value="CHESS">
24
+ # Sashite::Snn::Name.new("shogi") # => #<Snn::Name value="shogi">
25
+ # Sashite::Snn::Name.new("XIANGQI") # => #<Snn::Name value="XIANGQI">
26
+ #
27
+ # @example Invalid names
28
+ # Sashite::Snn::Name.new("Chess") # => ArgumentError (mixed case)
29
+ # Sashite::Snn::Name.new("CHESS960") # => ArgumentError (contains digits)
30
+ # Sashite::Snn::Name.new("GO9X9") # => ArgumentError (contains digits)
31
+ class Name
32
+ # SNN validation pattern matching the specification
33
+ # Format: All uppercase OR all lowercase alphabetic characters
34
+ SNN_PATTERN = /\A([A-Z]+|[a-z]+)\z/
35
+
36
+ # Error message for invalid SNN strings
37
+ ERROR_INVALID_NAME = "Invalid SNN string: %s"
38
+
39
+ # @return [String] the canonical style name
40
+ attr_reader :value
41
+
42
+ # Create a new style name instance
43
+ #
44
+ # The name must follow SNN format rules: all uppercase or all lowercase
45
+ # alphabetic characters only. No digits, special characters, or mixed case.
46
+ #
47
+ # @param name [String, Symbol] the style name (e.g., "SHOGI", :chess)
48
+ # @raise [ArgumentError] if the name does not match SNN pattern
49
+ #
50
+ # @example Create valid names
51
+ # Sashite::Snn::Name.new("CHESS") # First player Chess
52
+ # Sashite::Snn::Name.new("shogi") # Second player Shōgi
53
+ # Sashite::Snn::Name.new(:XIANGQI) # First player Xiangqi
54
+ #
55
+ # @example Invalid names raise errors
56
+ # Sashite::Snn::Name.new("Chess") # Mixed case
57
+ # Sashite::Snn::Name.new("CHESS960") # Contains digits
58
+ def initialize(name)
59
+ string_value = name.to_s
60
+ self.class.validate_format(string_value)
61
+
62
+ @value = string_value.freeze
63
+ freeze
64
+ end
65
+
66
+ # Parse an SNN string into a Name object
67
+ #
68
+ # This is an alias for the constructor, provided for consistency
69
+ # with other Sashité specifications.
70
+ #
71
+ # @param string [String] the SNN-formatted style name
72
+ # @return [Name] a new Name instance
73
+ # @raise [ArgumentError] if the string is invalid
74
+ #
75
+ # @example Parse valid names
76
+ # Sashite::Snn::Name.parse("SHOGI") # => #<Snn::Name value="SHOGI">
77
+ # Sashite::Snn::Name.parse("chess") # => #<Snn::Name value="chess">
78
+ def self.parse(string)
79
+ new(string)
80
+ end
81
+
82
+ # Check whether the given string is a valid SNN name
83
+ #
84
+ # Valid SNN strings must:
85
+ # - Contain only alphabetic characters (A-Z or a-z)
86
+ # - Have consistent case (all uppercase OR all lowercase)
87
+ # - Contain at least one character
88
+ #
89
+ # @param string [String] input string to validate
90
+ # @return [Boolean] true if valid, false otherwise
91
+ #
92
+ # @example Valid names
93
+ # Sashite::Snn::Name.valid?("CHESS") # => true
94
+ # Sashite::Snn::Name.valid?("shogi") # => true
95
+ # Sashite::Snn::Name.valid?("XIANGQI") # => true
96
+ #
97
+ # @example Invalid names
98
+ # Sashite::Snn::Name.valid?("Chess") # => false (mixed case)
99
+ # Sashite::Snn::Name.valid?("CHESS960") # => false (contains digits)
100
+ # Sashite::Snn::Name.valid?("GO9X9") # => false (contains digits)
101
+ # Sashite::Snn::Name.valid?("") # => false (empty)
102
+ def self.valid?(string)
103
+ string.is_a?(::String) && string.match?(SNN_PATTERN)
104
+ end
105
+
106
+ # Returns the string representation of the name
107
+ #
108
+ # @return [String] the canonical style name
109
+ #
110
+ # @example
111
+ # name = Sashite::Snn::Name.new("SHOGI")
112
+ # name.to_s # => "SHOGI"
113
+ def to_s
114
+ value
115
+ end
116
+
117
+ # Equality based on string value
118
+ #
119
+ # Two names are equal if they have the same string value. Case matters:
120
+ # "CHESS" (first player) is not equal to "chess" (second player).
121
+ #
122
+ # @param other [Object] object to compare with
123
+ # @return [Boolean] true if equal, false otherwise
124
+ #
125
+ # @example
126
+ # name1 = Sashite::Snn::Name.new("CHESS")
127
+ # name2 = Sashite::Snn::Name.new("CHESS")
128
+ # name3 = Sashite::Snn::Name.new("chess")
129
+ #
130
+ # name1 == name2 # => true
131
+ # name1 == name3 # => false (different case = different player)
132
+ def ==(other)
133
+ other.is_a?(self.class) && value == other.value
134
+ end
135
+
136
+ # Required for correct Set/Hash behavior
137
+ alias eql? ==
138
+
139
+ # Hash based on class and value
140
+ #
141
+ # Enables Name objects to be used as hash keys and in sets.
142
+ #
143
+ # @return [Integer] hash code
144
+ #
145
+ # @example Use as hash key
146
+ # styles = {
147
+ # Sashite::Snn::Name.new("CHESS") => "Western Chess",
148
+ # Sashite::Snn::Name.new("SHOGI") => "Japanese Chess"
149
+ # }
150
+ def hash
151
+ [self.class, value].hash
152
+ end
153
+
154
+ # Validate that the string is in proper SNN format
155
+ #
156
+ # @param str [String] string to validate
157
+ # @raise [ArgumentError] if the string does not match SNN pattern
158
+ #
159
+ # @example Valid format
160
+ # Sashite::Snn::Name.validate_format("CHESS") # No error
161
+ # Sashite::Snn::Name.validate_format("shogi") # No error
162
+ #
163
+ # @example Invalid format
164
+ # Sashite::Snn::Name.validate_format("Chess") # Raises ArgumentError
165
+ def self.validate_format(str)
166
+ raise ::ArgumentError, format(ERROR_INVALID_NAME, str.inspect) unless str.match?(SNN_PATTERN)
167
+ end
168
+ end
169
+ end
170
+ end
data/lib/sashite/snn.rb CHANGED
@@ -1,65 +1,93 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative "snn/style"
3
+ require_relative "snn/name"
4
4
 
5
5
  module Sashite
6
6
  # SNN (Style Name Notation) implementation for Ruby
7
7
  #
8
- # Provides a rule-agnostic format for identifying styles in abstract strategy board games.
9
- # SNN uses single ASCII letters with case-based side encoding, enabling clear
10
- # distinction between different style families in multi-style gaming environments.
8
+ # Provides a foundational naming system for identifying styles in abstract strategy board games.
9
+ # SNN uses canonical, human-readable alphabetic names with case encoding to represent both
10
+ # style identity and player assignment.
11
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 SNN identifier is exactly one ASCII letter
12
+ # Format: All uppercase OR all lowercase alphabetic characters
16
13
  #
17
14
  # 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
15
+ # "CHESS" - Chess style for first player
16
+ # "chess" - Chess style for second player
17
+ # "SHOGI" - Shōgi style for first player
18
+ # "shogi" - Shōgi style for second player
19
+ # "XIANGQI" - Xiangqi style for first player
20
+ # "xiangqi" - Xiangqi style for second player
21
+ #
22
+ # Case Encoding:
23
+ # - UPPERCASE names represent the first player's style
24
+ # - lowercase names represent the second player's style
25
+ #
26
+ # Constraints:
27
+ # - Alphabetic characters only (A-Z, a-z)
28
+ # - Case consistency required (all uppercase OR all lowercase)
29
+ # - No digits, no special characters, no mixed case
30
+ #
31
+ # As a foundational primitive, SNN has no dependencies and serves as a building block
32
+ # for formal style identification in the Sashité ecosystem.
22
33
  #
23
34
  # See: https://sashite.dev/specs/snn/1.0.0/
24
35
  module Snn
25
- # Check if a string is a valid SNN notation
36
+ # Check if a string is valid SNN notation
37
+ #
38
+ # Valid SNN strings must contain only alphabetic characters in consistent case
39
+ # (either all uppercase or all lowercase).
26
40
  #
27
41
  # @param snn_string [String] the string to validate
28
42
  # @return [Boolean] true if valid SNN, false otherwise
29
43
  #
30
- # @example Validate various SNN formats
31
- # Sashite::Snn.valid?("C") # => true
32
- # Sashite::Snn.valid?("c") # => true
33
- # Sashite::Snn.valid?("CHESS") # => false (multi-character)
34
- # Sashite::Snn.valid?("1") # => false (not a letter)
44
+ # @example Validate SNN strings
45
+ # Sashite::Snn.valid?("CHESS") # => true
46
+ # Sashite::Snn.valid?("shogi") # => true
47
+ # Sashite::Snn.valid?("Chess") # => false (mixed case)
48
+ # Sashite::Snn.valid?("CHESS960") # => false (contains digits)
49
+ # Sashite::Snn.valid?("GO9X9") # => false (contains digits)
35
50
  def self.valid?(snn_string)
36
- Style.valid?(snn_string)
51
+ Name.valid?(snn_string)
37
52
  end
38
53
 
39
- # Parse an SNN string into a Style object
54
+ # Parse an SNN string into a Name object
55
+ #
56
+ # Converts a valid SNN string into an immutable Name object. The name must follow
57
+ # SNN format rules: all uppercase or all lowercase alphabetic characters only.
40
58
  #
41
- # @param snn_string [String] SNN notation string
42
- # @return [Snn::Style] parsed style object with letter and side attributes
43
- # @raise [ArgumentError] if the SNN string is invalid
44
- # @example Parse different SNN formats
45
- # Sashite::Snn.parse("C") # => #<Snn::Style letter=:C side=:first>
46
- # Sashite::Snn.parse("c") # => #<Snn::Style letter=:c side=:second>
47
- # Sashite::Snn.parse("S") # => #<Snn::Style letter=:S side=:first>
59
+ # @param snn_string [String] the name string
60
+ # @return [Snn::Name] a parsed name object
61
+ # @raise [ArgumentError] if the name is invalid
62
+ #
63
+ # @example Parse valid SNN names
64
+ # Sashite::Snn.parse("SHOGI") # => #<Snn::Name value="SHOGI">
65
+ # Sashite::Snn.parse("chess") # => #<Snn::Name value="chess">
66
+ #
67
+ # @example Invalid names raise errors
68
+ # Sashite::Snn.parse("Chess") # => ArgumentError (mixed case)
69
+ # Sashite::Snn.parse("CHESS960") # => ArgumentError (contains digits)
48
70
  def self.parse(snn_string)
49
- Style.parse(snn_string)
71
+ Name.parse(snn_string)
50
72
  end
51
73
 
52
- # Create a new style instance
74
+ # Create a new Name instance directly
75
+ #
76
+ # Constructs a Name object from a string or symbol. The value must follow
77
+ # SNN format rules: all uppercase or all lowercase alphabetic characters only.
78
+ #
79
+ # @param value [String, Symbol] style name to construct
80
+ # @return [Snn::Name] new name instance
81
+ # @raise [ArgumentError] if name format is invalid
82
+ #
83
+ # @example Create names
84
+ # Sashite::Snn.name("XIANGQI") # => #<Snn::Name value="XIANGQI">
85
+ # Sashite::Snn.name(:makruk) # => #<Snn::Name value="makruk">
53
86
  #
54
- # @param letter [Symbol] style letter (single ASCII letter as symbol)
55
- # @param side [Symbol] player side (:first or :second)
56
- # @return [Snn::Style] new immutable style instance
57
- # @raise [ArgumentError] if parameters are invalid
58
- # @example Create styles directly
59
- # Sashite::Snn.style(:C, :first) # => #<Snn::Style letter=:C side=:first>
60
- # Sashite::Snn.style(:s, :second) # => #<Snn::Style letter=:s side=:second>
61
- def self.style(letter, side)
62
- Style.new(letter, side)
87
+ # @example Invalid formats raise errors
88
+ # Sashite::Snn.name("Chess960") # => ArgumentError
89
+ def self.name(value)
90
+ Name.new(value)
63
91
  end
64
92
  end
65
93
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: sashite-snn
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.0.0
4
+ version: 3.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Cyril Kato
@@ -10,13 +10,18 @@ cert_chain: []
10
10
  date: 1980-01-02 00:00:00.000000000 Z
11
11
  dependencies: []
12
12
  description: |
13
- SNN (Style Name Notation) provides a rule-agnostic format for identifying styles
14
- in abstract strategy board games. This gem implements the SNN Specification v1.0.0 with
15
- a modern Ruby interface featuring immutable style objects and functional programming
16
- principles. SNN uses single ASCII letters with case-based side encoding (A-Z for first player,
17
- a-z for second player), enabling clear distinction between different style families in
18
- multi-style gaming environments. Perfect for cross-style matches, game engines, and hybrid
19
- gaming systems requiring compact style identification.
13
+ SNN (Style Name Notation) provides a foundational, rule-agnostic naming system for identifying
14
+ game styles in abstract strategy board games. This gem implements the SNN Specification v1.0.0
15
+ with a modern Ruby interface featuring immutable style name objects and functional programming
16
+ principles.
17
+
18
+ SNN uses case-consistent alphabetic names (e.g., "CHESS", "SHOGI", "XIANGQI" for first player;
19
+ "chess", "shogi", "xiangqi" for second player) to unambiguously represent both style identity
20
+ and player assignment. As a foundational primitive with no dependencies, SNN serves as a building
21
+ block for formal style identification across the Sashité ecosystem.
22
+
23
+ Format: All uppercase OR all lowercase alphabetic characters only (no digits, no special characters).
24
+ Ideal for game engines, protocols, and tools requiring clear and extensible style identifiers.
20
25
  email: contact@cyril.email
21
26
  executables: []
22
27
  extensions: []
@@ -26,7 +31,7 @@ files:
26
31
  - README.md
27
32
  - lib/sashite-snn.rb
28
33
  - lib/sashite/snn.rb
29
- - lib/sashite/snn/style.rb
34
+ - lib/sashite/snn/name.rb
30
35
  homepage: https://github.com/sashite/snn.rb
31
36
  licenses:
32
37
  - MIT
@@ -51,7 +56,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
51
56
  - !ruby/object:Gem::Version
52
57
  version: '0'
53
58
  requirements: []
54
- rubygems_version: 3.6.9
59
+ rubygems_version: 3.7.1
55
60
  specification_version: 4
56
- summary: SNN (Style Name Notation) implementation for Ruby with immutable style objects
61
+ summary: SNN (Style Name Notation) - foundational naming system for abstract strategy
62
+ game styles
57
63
  test_files: []
@@ -1,250 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Sashite
4
- module Snn
5
- # Represents a style in SNN (Style Name Notation) format.
6
- #
7
- # A style consists of a single ASCII letter with case-based side encoding:
8
- # - Uppercase letter: first player (A, B, C, ..., Z)
9
- # - Lowercase letter: second player (a, b, c, ..., z)
10
- #
11
- # All instances are immutable - transformation methods return new instances.
12
- # This follows the SNN Specification v1.0.0 with Letter and Side attributes.
13
- class Style
14
- # SNN validation pattern matching the specification
15
- SNN_PATTERN = /\A[A-Za-z]\z/
16
-
17
- # Player side constants
18
- FIRST_PLAYER = :first
19
- SECOND_PLAYER = :second
20
-
21
- # Valid sides
22
- VALID_SIDES = [FIRST_PLAYER, SECOND_PLAYER].freeze
23
-
24
- # Error messages
25
- ERROR_INVALID_SNN = "Invalid SNN 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"
28
-
29
- # @return [Symbol] the style letter (single ASCII letter as symbol)
30
- attr_reader :letter
31
-
32
- # @return [Symbol] the player side (:first or :second)
33
- attr_reader :side
34
-
35
- # Create a new style instance
36
- #
37
- # @param letter [Symbol] style letter (single ASCII letter as symbol)
38
- # @param side [Symbol] player side (:first or :second)
39
- # @raise [ArgumentError] if parameters are invalid
40
- def initialize(letter, side)
41
- self.class.validate_letter(letter)
42
- self.class.validate_side(side)
43
-
44
- @letter = letter
45
- @side = side
46
-
47
- freeze
48
- end
49
-
50
- # Parse an SNN string into a Style object
51
- #
52
- # @param snn_string [String] SNN notation string (single ASCII letter)
53
- # @return [Style] parsed style object with letter and inferred side
54
- # @raise [ArgumentError] if the SNN string is invalid
55
- # @example Parse SNN strings with case-based side inference
56
- # Sashite::Snn::Style.parse("C") # => #<Snn::Style letter=:C side=:first>
57
- # Sashite::Snn::Style.parse("c") # => #<Snn::Style letter=:c side=:second>
58
- # Sashite::Snn::Style.parse("S") # => #<Snn::Style letter=:S side=:first>
59
- def self.parse(snn_string)
60
- string_value = String(snn_string)
61
- validate_snn_string(string_value)
62
-
63
- # Determine side from case
64
- style_side = string_value == string_value.upcase ? FIRST_PLAYER : SECOND_PLAYER
65
-
66
- # Use the letter directly as symbol
67
- style_letter = string_value.to_sym
68
-
69
- new(style_letter, style_side)
70
- end
71
-
72
- # Check if a string is a valid SNN notation
73
- #
74
- # @param snn_string [String] the string to validate
75
- # @return [Boolean] true if valid SNN, false otherwise
76
- #
77
- # @example Validate SNN strings
78
- # Sashite::Snn::Style.valid?("C") # => true
79
- # Sashite::Snn::Style.valid?("c") # => true
80
- # Sashite::Snn::Style.valid?("CHESS") # => false (multi-character)
81
- def self.valid?(snn_string)
82
- return false unless snn_string.is_a?(::String)
83
-
84
- snn_string.match?(SNN_PATTERN)
85
- end
86
-
87
- # Convert the style to its SNN string representation
88
- #
89
- # @return [String] SNN notation string (single ASCII letter)
90
- # @example Display styles
91
- # style.to_s # => "C" (first player, C family)
92
- # style.to_s # => "c" (second player, C family)
93
- # style.to_s # => "S" (first player, S family)
94
- def to_s
95
- letter.to_s
96
- end
97
-
98
- # Create a new style with opposite ownership (side)
99
- #
100
- # @return [Style] new immutable style instance with flipped side
101
- # @example Flip player sides
102
- # style.flip # (:C, :first) => (:c, :second)
103
- 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)
106
- end
107
-
108
- # Create a new style with a different letter (keeping same side)
109
- #
110
- # @param new_letter [Symbol] new letter (single ASCII letter as symbol)
111
- # @return [Style] new immutable style instance with different letter
112
- # @example Change style letter
113
- # style.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
117
-
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)
121
- end
122
-
123
- # Create a new style with a different side (keeping same letter family)
124
- #
125
- # @param new_side [Symbol] :first or :second
126
- # @return [Style] new immutable style instance with different side
127
- # @example Change player side
128
- # style.with_side(:second) # (:C, :first) => (:c, :second)
129
- def with_side(new_side)
130
- self.class.validate_side(new_side)
131
- return self if side == new_side
132
-
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)
136
- end
137
-
138
- # Check if the style belongs to the first player
139
- #
140
- # @return [Boolean] true if first player
141
- def first_player?
142
- side == FIRST_PLAYER
143
- end
144
-
145
- # Check if the style belongs to the second player
146
- #
147
- # @return [Boolean] true if second player
148
- def second_player?
149
- side == SECOND_PLAYER
150
- end
151
-
152
- # Check if this style has the same letter family as another
153
- #
154
- # @param other [Style] style to compare with
155
- # @return [Boolean] true if both styles use the same letter family (case-insensitive)
156
- # @example Compare style letter families
157
- # c_style.same_letter?(C_style) # (:c, :second) and (:C, :first) => true
158
- def same_letter?(other)
159
- return false unless other.is_a?(self.class)
160
-
161
- letter.to_s.upcase == other.letter.to_s.upcase
162
- end
163
-
164
- # Check if this style belongs to the same side as another
165
- #
166
- # @param other [Style] style to compare with
167
- # @return [Boolean] true if both styles belong to the same side
168
- def same_side?(other)
169
- return false unless other.is_a?(self.class)
170
-
171
- side == other.side
172
- end
173
-
174
- # Custom equality comparison
175
- #
176
- # @param other [Object] object to compare with
177
- # @return [Boolean] true if both objects are styles with identical letter and side
178
- def ==(other)
179
- return false unless other.is_a?(self.class)
180
-
181
- letter == other.letter && side == other.side
182
- end
183
-
184
- # Alias for == to ensure Set functionality works correctly
185
- alias eql? ==
186
-
187
- # Custom hash implementation for use in collections
188
- #
189
- # @return [Integer] hash value based on class, letter, and side
190
- def hash
191
- [self.class, letter, side].hash
192
- end
193
-
194
- # Validate that the letter is a valid single ASCII letter symbol
195
- #
196
- # @param letter [Symbol] the letter to validate
197
- # @raise [ArgumentError] if invalid
198
- def self.validate_letter(letter)
199
- return if valid_letter?(letter)
200
-
201
- raise ::ArgumentError, format(ERROR_INVALID_LETTER, letter.inspect)
202
- end
203
-
204
- # Validate that the side is a valid symbol
205
- #
206
- # @param side [Symbol] the side to validate
207
- # @raise [ArgumentError] if invalid
208
- def self.validate_side(side)
209
- return if VALID_SIDES.include?(side)
210
-
211
- raise ::ArgumentError, format(ERROR_INVALID_SIDE, side.inspect)
212
- end
213
-
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?(SNN_PATTERN)
226
- end
227
-
228
- # Validate SNN string format
229
- #
230
- # @param string [String] string to validate
231
- # @raise [ArgumentError] if string doesn't match SNN pattern
232
- def self.validate_snn_string(string)
233
- return if string.match?(SNN_PATTERN)
234
-
235
- raise ::ArgumentError, format(ERROR_INVALID_SNN, string)
236
- end
237
-
238
- private_class_method :valid_letter?, :validate_snn_string
239
-
240
- private
241
-
242
- # Get the opposite side
243
- #
244
- # @return [Symbol] the opposite side
245
- def opposite_side
246
- first_player? ? SECOND_PLAYER : FIRST_PLAYER
247
- end
248
- end
249
- end
250
- end