sashite-snn 1.1.1 → 3.0.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: c3327ae1106ccede78736585e812180c12d7a56852a82ae70b7745e233ef5496
4
- data.tar.gz: ba5892640791f51341e52c883f52f9f21bac64e21001052e1fcbc17965a156a7
3
+ metadata.gz: 517b0310bd57b5d0b931a5f14eb1c4d6257bbd6d4f215c467ff475c8b42542fc
4
+ data.tar.gz: 2fa5a177d6350dd2501b87eac5a567f5cf48c681439b3af23f2bae1df99579a3
5
5
  SHA512:
6
- metadata.gz: 1798b7aec7b88cc2baddcd9fa7f3518c2d97bf16343caac92ec84f9bfad3dc0ac20830d95144984ea27795d867e80c008a090f9de7fc8d284ac853f9c326b144
7
- data.tar.gz: a6a548c819cf763b914cc1b70ce82ba86d86b7973f0231df5075931788d19fb2732d4138896b347cf6a1842c554116578e59cdfb619408acb370c2c43d469753
6
+ metadata.gz: 200ef38765b8c581f93c51657699e4b420c26abe1d355227ff47892b493f787ec9c03e7202a91b9466c99c97a95a1b914837e59bb2784ef6568cc2ca3f94dd49
7
+ data.tar.gz: 6b01e73ad4e61246e8e61a4e0bd85f7b3f0fcb964f7532c42d6c95a2b5b665c64487e032e46cb7b77806b3a410137536812aac3278428b2c31af8d91811597ec
data/README.md CHANGED
@@ -9,16 +9,16 @@
9
9
 
10
10
  ## What is SNN?
11
11
 
12
- SNN (Style Name Notation) provides a rule-agnostic format for identifying styles in abstract strategy board games. SNN uses standardized naming conventions with case-based side encoding, enabling clear distinction between different traditions in multi-style gaming environments.
12
+ SNN (Style Name Notation) is a formal, rule-agnostic naming system for identifying **styles** in abstract strategy board games such as chess, shōgi, xiangqi, and their many variants. Each style is represented by a canonical, human-readable ASCII name (e.g., `"Chess"`, `"Shogi"`, `"Xiangqi"`, `"Minishogi"`).
13
13
 
14
- This gem implements the [SNN Specification v1.0.0](https://sashite.dev/specs/snn/1.0.0/), providing a modern Ruby interface with immutable style objects and functional programming principles.
14
+ This gem implements the [SNN Specification v1.0.0](https://sashite.dev/specs/snn/1.0.0/), supporting validation, parsing, and comparison of style names.
15
15
 
16
16
  ## Installation
17
17
 
18
18
  ```ruby
19
19
  # In your Gemfile
20
20
  gem "sashite-snn"
21
- ```
21
+ ````
22
22
 
23
23
  Or install manually:
24
24
 
@@ -28,426 +28,128 @@ gem install sashite-snn
28
28
 
29
29
  ## Usage
30
30
 
31
+ ### Basic Operations
32
+
31
33
  ```ruby
32
34
  require "sashite/snn"
33
35
 
34
- # Parse SNN strings into style objects
35
- style = Sashite::Snn.parse("CHESS") # => #<Snn::Style name=:Chess side=:first>
36
- style.to_s # => "CHESS"
37
- style.name # => :Chess
38
- style.side # => :first
36
+ # Parse SNN strings into style name objects
37
+ name = Sashite::Snn.parse("Shogi") # => #<Snn::Name value="Shogi">
38
+ name.to_s # => "Shogi"
39
+ name.value # => "Shogi"
39
40
 
40
- # Create styles directly
41
- style = Sashite::Snn.style(:Chess, :first) # => #<Snn::Style name=:Chess side=:first>
42
- style = Sashite::Snn::Style.new(:Shogi, :second) # => #<Snn::Style name=:Shogi side=:second>
41
+ # Create from string or symbol
42
+ name = Sashite::Snn.name("Chess") # => #<Snn::Name value="Chess">
43
+ name = Sashite::Snn::Name.new(:Xiangqi) # => #<Snn::Name value="Xiangqi">
43
44
 
44
45
  # Validate SNN strings
45
- Sashite::Snn.valid?("CHESS") # => true
46
- Sashite::Snn.valid?("chess") # => true
47
- Sashite::Snn.valid?("Chess") # => false (mixed case)
48
- Sashite::Snn.valid?("123") # => false (must start with letter)
49
-
50
- # Side manipulation (returns new immutable instances)
51
- black_chess = style.flip # => #<Snn::Style name=:Chess side=:second>
52
- black_chess.to_s # => "chess"
53
-
54
- # Name manipulation
55
- shogi_style = style.with_name(:Shogi) # => #<Snn::Style name=:Shogi side=:first>
56
- shogi_style.to_s # => "SHOGI"
57
-
58
- # Side queries
59
- style.first_player? # => true
60
- black_chess.second_player? # => true
61
-
62
- # Name and side comparison
63
- chess1 = Sashite::Snn.parse("CHESS")
64
- chess2 = Sashite::Snn.parse("chess")
65
- shogi = Sashite::Snn.parse("SHOGI")
66
-
67
- chess1.same_name?(chess2) # => true (both chess)
68
- chess1.same_side?(shogi) # => true (both first player)
69
- chess1.same_name?(shogi) # => false (different styles)
70
-
71
- # Functional transformations can be chained
72
- black_shogi = Sashite::Snn.parse("CHESS").with_name(:Shogi).flip
73
- black_shogi.to_s # => "shogi"
74
- ```
75
-
76
- ## Format Specification
77
-
78
- ### Structure
79
- ```
80
- <style-identifier>
46
+ Sashite::Snn.valid?("Go9x9") # => true
47
+ Sashite::Snn.valid?("chess") # => false (must start with uppercase)
48
+ Sashite::Snn.valid?("3DChess") # => false (invalid character)
81
49
  ```
82
50
 
83
- ### Components
84
-
85
- - **Identifier**: Alphanumeric string starting with a letter
86
- - Uppercase: First player styles (`CHESS`, `SHOGI`, `XIANGQI`)
87
- - Lowercase: Second player styles (`chess`, `shogi`, `xiangqi`)
88
- - **Case Consistency**: Entire identifier must be uppercase or lowercase (no mixed case)
89
-
90
- ### Regular Expression
91
- ```ruby
92
- # Pattern accessible via Sashite::Snn::Style::SNN_PATTERN
93
- /\A([A-Z][A-Z0-9]*|[a-z][a-z0-9]*)\z/
94
- ```
95
-
96
- ### Examples
97
- - `CHESS` - First player chess style
98
- - `chess` - Second player chess style
99
- - `CHESS960` - First player Fischer Random Chess style
100
- - `SHOGI` - First player shōgi style
101
- - `shogi` - Second player shōgi style
102
- - `XIANGQI` - First player xiangqi style
103
-
104
- ## Game Examples
105
-
106
- ### Classic Styles
107
- ```ruby
108
- # Traditional game styles
109
- chess = Sashite::Snn.style(:Chess, :first) # => traditional chess
110
- shogi = Sashite::Snn.style(:Shogi, :first) # => traditional shōgi
111
- xiangqi = Sashite::Snn.style(:Xiangqi, :first) # => traditional xiangqi
112
- makruk = Sashite::Snn.style(:Makruk, :first) # => traditional makruk
113
-
114
- # Player variations
115
- white_chess = chess # => first player
116
- black_chess = chess.flip # => second player
117
- black_chess.to_s # => "chess"
118
- ```
51
+ ### Normalization and Comparison
119
52
 
120
- ### Chess Variants
121
53
  ```ruby
122
- # Fischer Random Chess
123
- chess960_white = Sashite::Snn.style(:Chess960, :first)
124
- chess960_black = chess960_white.flip
125
- chess960_black.to_s # => "chess960"
54
+ a = Sashite::Snn.parse("Chess960")
55
+ b = Sashite::Snn.parse("Chess960")
126
56
 
127
- # King of the Hill Chess
128
- koth = Sashite::Snn.style(:Koth, :first)
129
- koth.to_s # => "KOTH"
130
-
131
- # Three-Check Chess
132
- threecheck = Sashite::Snn.style(:Threecheck, :first)
57
+ a == b # => true
58
+ a.same_base_name?(Sashite::Snn.parse("Chess")) # => true if both resolve to same SIN
59
+ a.to_s # => "Chess960"
133
60
  ```
134
61
 
135
- ### Shōgi Variants
136
- ```ruby
137
- # Traditional Shōgi
138
- standard_shogi = Sashite::Snn.style(:Shogi, :first)
139
-
140
- # Mini Shōgi
141
- mini_shogi = Sashite::Snn.style(:Minishogi, :first)
142
-
143
- # Chu Shōgi
144
- chu_shogi = Sashite::Snn.style(:Chushogi, :first)
145
- ```
62
+ ### Canonical Representation
146
63
 
147
- ### Multi-Style Gaming
148
64
  ```ruby
149
- # Cross-tradition match
150
- def create_hybrid_match
151
- styles = [
152
- Sashite::Snn.style(:Chess, :first), # White uses chess pieces
153
- Sashite::Snn.style(:Shogi, :second) # Black uses shōgi pieces
154
- ]
155
-
156
- # Each player uses their preferred piece style
157
- styles
158
- end
159
-
160
- # Style compatibility check
161
- def compatible_styles?(style1, style2)
162
- # Styles are compatible if they have different sides
163
- !style1.same_side?(style2)
164
- end
165
-
166
- chess_white = Sashite::Snn.parse("CHESS")
167
- shogi_black = Sashite::Snn.parse("shogi")
168
- puts compatible_styles?(chess_white, shogi_black) # => true
65
+ # All names are normalized to a canonical format
66
+ name = Sashite::Snn.parse("Minishogi")
67
+ name.value # => "Minishogi"
68
+ name.to_s # => "Minishogi"
169
69
  ```
170
70
 
171
- ## API Reference
172
-
173
- ### Main Module Methods
174
-
175
- - `Sashite::Snn.valid?(snn_string)` - Check if string is valid SNN notation
176
- - `Sashite::Snn.parse(snn_string)` - Parse SNN string into Style object
177
- - `Sashite::Snn.style(name, side)` - Create style instance directly
178
-
179
- ### Style Class
180
-
181
- #### Creation and Parsing
182
- - `Sashite::Snn::Style.new(name, side)` - Create style instance
183
- - `Sashite::Snn::Style.parse(snn_string)` - Parse SNN string (same as module method)
184
-
185
- #### Attribute Access
186
- - `#name` - Get style name (symbol with proper capitalization)
187
- - `#side` - Get player side (:first or :second)
188
- - `#to_s` - Convert to SNN string representation
189
-
190
- #### Name and Case Handling
191
-
192
- **Important**: The `name` attribute is always stored with proper capitalization (first letter uppercase, rest lowercase), regardless of the input case when parsing. The display case in `#to_s` is determined by the `side` attribute:
71
+ ### Collections and Filtering
193
72
 
194
73
  ```ruby
195
- # Both create the same internal name representation
196
- style1 = Sashite::Snn.parse("CHESS") # name: :Chess, side: :first
197
- style2 = Sashite::Snn.parse("chess") # name: :Chess, side: :second
198
-
199
- style1.name # => :Chess (proper capitalization)
200
- style2.name # => :Chess (same capitalization)
74
+ names = %w[Chess Shogi Makruk Antichess Minishogi].map { |n| Sashite::Snn.parse(n) }
201
75
 
202
- style1.to_s # => "CHESS" (uppercase display)
203
- style2.to_s # => "chess" (lowercase display)
76
+ # Filter by prefix
77
+ names.select { |n| n.value.start_with?("Mini") }.map(&:to_s)
78
+ # => ["Minishogi"]
204
79
  ```
205
80
 
206
- #### Side Queries
207
- - `#first_player?` - Check if first player style
208
- - `#second_player?` - Check if second player style
209
-
210
- #### Transformations (immutable - return new instances)
211
- - `#flip` - Switch player (change side)
212
- - `#with_name(new_name)` - Create style with different name
213
- - `#with_side(new_side)` - Create style with different side
214
-
215
- #### Comparison Methods
216
- - `#same_name?(other)` - Check if same style name
217
- - `#same_side?(other)` - Check if same side
218
- - `#==(other)` - Full equality comparison
219
-
220
- ### Style Class Constants
221
-
222
- - `Sashite::Snn::Style::FIRST_PLAYER` - Symbol for first player (:first)
223
- - `Sashite::Snn::Style::SECOND_PLAYER` - Symbol for second player (:second)
224
- - `Sashite::Snn::Style::VALID_SIDES` - Array of valid sides
225
- - `Sashite::Snn::Style::SNN_PATTERN` - Regular expression for SNN validation
226
-
227
- ## Advanced Usage
228
-
229
- ### Name Normalization Examples
230
-
231
- ```ruby
232
- # Parsing different cases results in same name
233
- white_chess = Sashite::Snn.parse("CHESS")
234
- black_chess = Sashite::Snn.parse("chess")
235
-
236
- # Names are normalized with proper capitalization
237
- white_chess.name # => :Chess
238
- black_chess.name # => :Chess (same name!)
239
-
240
- # Sides are different
241
- white_chess.side # => :first
242
- black_chess.side # => :second
81
+ ## Format Specification
243
82
 
244
- # Display follows side convention
245
- white_chess.to_s # => "CHESS"
246
- black_chess.to_s # => "chess"
83
+ ### Structure
247
84
 
248
- # Same name, different sides
249
- white_chess.same_name?(black_chess) # => true
250
- white_chess.same_side?(black_chess) # => false
251
85
  ```
252
-
253
- ### Immutable Transformations
254
- ```ruby
255
- # All transformations return new instances
256
- original = Sashite::Snn.style(:Chess, :first)
257
- flipped = original.flip
258
- renamed = original.with_name(:Shogi)
259
-
260
- # Original style is never modified
261
- puts original.to_s # => "CHESS"
262
- puts flipped.to_s # => "chess"
263
- puts renamed.to_s # => "SHOGI"
264
-
265
- # Transformations can be chained
266
- result = original.flip.with_name(:Xiangqi)
267
- puts result.to_s # => "xiangqi"
86
+ <uppercase-letter>[<lowercase-letter | digit>]*
268
87
  ```
269
88
 
270
- ### Game Configuration Management
271
- ```ruby
272
- class GameConfiguration
273
- def initialize
274
- @player_styles = {}
275
- end
276
-
277
- def set_player_style(player, style_name)
278
- side = player == :white ? :first : :second
279
- @player_styles[player] = Sashite::Snn.style(style_name, side)
280
- end
89
+ ### Grammar (BNF)
281
90
 
282
- def get_player_style(player)
283
- @player_styles[player]
284
- end
91
+ ```bnf
92
+ <snn> ::= <uppercase-letter> <tail>
285
93
 
286
- def style_mismatch?
287
- return false if @player_styles.size < 2
94
+ <tail> ::= "" ; Single letter (e.g., "X")
95
+ | <alphanumeric-char> <tail> ; Extended name
288
96
 
289
- styles = @player_styles.values
290
- !styles.all? { |style| style.same_name?(styles.first) }
291
- end
97
+ <alphanumeric-char> ::= <lowercase-letter> | <digit>
292
98
 
293
- def cross_tradition_match?
294
- return false if @player_styles.size < 2
295
-
296
- style_names = @player_styles.values.map(&:name).uniq
297
- style_names.size > 1
298
- end
299
- end
300
-
301
- # Usage
302
- config = GameConfiguration.new
303
- config.set_player_style(:white, :Chess)
304
- config.set_player_style(:black, :Shogi)
305
-
306
- puts config.cross_tradition_match? # => true
307
- puts config.style_mismatch? # => true
308
-
309
- white_style = config.get_player_style(:white)
310
- puts white_style.to_s # => "CHESS"
99
+ <uppercase-letter> ::= "A" | "B" | "C" | ... | "Z"
100
+ <lowercase-letter> ::= "a" | "b" | "c" | ... | "z"
101
+ <digit> ::= "0" | "1" | "2" | "3" | ... | "9"
311
102
  ```
312
103
 
313
- ### Style Analysis
314
- ```ruby
315
- def analyze_styles(snns)
316
- styles = snns.map { |snn| Sashite::Snn.parse(snn) }
317
-
318
- {
319
- total: styles.size,
320
- by_side: styles.group_by(&:side),
321
- by_name: styles.group_by(&:name),
322
- unique_names: styles.map(&:name).uniq.size,
323
- cross_tradition: styles.map(&:name).uniq.size > 1
324
- }
325
- end
326
-
327
- snns = %w[CHESS chess SHOGI shogi XIANGQI xiangqi]
328
- analysis = analyze_styles(snns)
329
- puts analysis[:by_side][:first].size # => 3
330
- puts analysis[:unique_names] # => 3
331
- puts analysis[:cross_tradition] # => true
332
- ```
104
+ ### Regular Expression
333
105
 
334
- ### Tournament Style Management
335
106
  ```ruby
336
- class TournamentStyleRegistry
337
- def initialize
338
- @registered_styles = Set.new
339
- end
340
-
341
- def register_style(style_name)
342
- # Register both sides of a style
343
- first_player_style = Sashite::Snn.style(style_name, :first)
344
- second_player_style = first_player_style.flip
345
-
346
- @registered_styles.add(first_player_style)
347
- @registered_styles.add(second_player_style)
348
-
349
- [first_player_style, second_player_style]
350
- end
351
-
352
- def valid_pairing?(style1, style2)
353
- @registered_styles.include?(style1) &&
354
- @registered_styles.include?(style2) &&
355
- !style1.same_side?(style2)
356
- end
357
-
358
- def available_styles_for_side(side)
359
- @registered_styles.select { |style| style.side == side }
360
- end
361
-
362
- def supported_traditions
363
- @registered_styles.map(&:name).uniq
364
- end
365
- end
366
-
367
- # Usage
368
- registry = TournamentStyleRegistry.new
369
- registry.register_style(:Chess)
370
- registry.register_style(:Shogi)
371
-
372
- chess_white = Sashite::Snn.parse("CHESS")
373
- shogi_black = Sashite::Snn.parse("shogi")
374
-
375
- puts registry.valid_pairing?(chess_white, shogi_black) # => true
376
- puts registry.supported_traditions # => [:Chess, :Shogi]
107
+ /\A[A-Z][a-z0-9]*\z/
377
108
  ```
378
109
 
379
- ## Protocol Mapping
380
-
381
- Following the [Game Protocol](https://sashite.dev/game-protocol/):
110
+ ## Design Principles
382
111
 
383
- | Protocol Attribute | SNN Encoding | Examples | Notes |
384
- |-------------------|--------------|----------|-------|
385
- | **Style** | Alphanumeric identifier | `CHESS`, `SHOGI`, `XIANGQI` | Name is always stored with proper capitalization |
386
- | **Side** | Case encoding | `CHESS` = First player, `chess` = Second player | Case is determined by side during rendering |
112
+ * **Human-readable**: Names like `"Shogi"` or `"Chess960"` are intuitive and descriptive.
113
+ * **Canonical**: One valid name per game style within a given context.
114
+ * **ASCII-only**: Compatible with all systems.
115
+ * **Scalable**: Supports unlimited distinct names for current and future game variants.
387
116
 
388
- **Name Convention**: All style names are internally represented with proper capitalization (first letter uppercase, rest lowercase). The display case is determined by the `side` attribute: first player styles display as uppercase, second player styles as lowercase.
117
+ ## Integration with SIN
389
118
 
390
- **Canonical principle**: Identical styles must have identical SNN representations.
119
+ SNN names serve as the formal source for SIN character identifiers. For example:
391
120
 
392
- ## Properties
121
+ | SNN | SIN |
122
+ | --------- | ------- |
123
+ | `Chess` | `C`/`c` |
124
+ | `Shogi` | `S`/`s` |
125
+ | `Xiangqi` | `X`/`x` |
126
+ | `Makruk` | `M`/`m` |
393
127
 
394
- * **Rule-Agnostic**: Independent of specific game mechanics
395
- * **Cross-Style Support**: Enables multi-tradition gaming environments
396
- * **Canonical Representation**: Consistent naming for equivalent styles
397
- * **Name Normalization**: Consistent proper capitalization representation internally
398
- * **Immutable**: All style instances are frozen and transformations return new objects
399
- * **Functional**: Pure functions with no side effects
128
+ Multiple SNN names may map to the same SIN character (e.g., `"Chess"` and `"Chess960"` both → `C`), but SNN provides unambiguous naming within broader contexts.
400
129
 
401
- ## Implementation Notes
402
-
403
- ### Name Normalization Convention
404
-
405
- SNN follows a strict name normalization convention:
406
-
407
- 1. **Internal Storage**: All style names are stored with proper capitalization (first letter uppercase, rest lowercase)
408
- 2. **Input Flexibility**: Both `"CHESS"` and `"chess"` are valid input during parsing
409
- 3. **Case Semantics**: Input case determines the `side` attribute, not the `name`
410
- 4. **Display Logic**: Output case is computed from `side` during rendering
411
-
412
- This design ensures:
413
- - Consistent internal representation regardless of input format
414
- - Clear separation between style identity (name) and ownership (side)
415
- - Predictable behavior when comparing styles of the same name
416
-
417
- ### Example Flow
130
+ ## Examples
418
131
 
419
132
  ```ruby
420
- # Input: "chess" (lowercase)
421
- # Parsing
422
- # name: :Chess (normalized with proper capitalization)
423
- # side: :second (inferred from lowercase input)
424
- # ↓ Display
425
- # SNN: "chess" (final representation)
133
+ Sashite::Snn.parse("Chess") # => #<Snn::Name value="Chess">
134
+ Sashite::Snn.parse("Chess960") # => #<Snn::Name value="Chess960">
135
+ Sashite::Snn.valid?("Minishogi") # => true
136
+ Sashite::Snn.valid?("miniShogi") # => false
426
137
  ```
427
138
 
428
- This ensures that `parse(snn).to_s == snn` for all valid SNN strings while maintaining internal consistency.
429
-
430
- ## System Constraints
431
-
432
- - **Alphanumeric identifiers** starting with a letter
433
- - **Exactly 2 players** (uppercase/lowercase distinction)
434
- - **Case consistency** within each identifier (no mixed case)
139
+ ## API Reference
435
140
 
436
- ## Related Specifications
141
+ ### Main Module
437
142
 
438
- - [Game Protocol](https://sashite.dev/game-protocol/) - Conceptual foundation for abstract strategy board games
439
- - [PIN](https://sashite.dev/specs/pin/) - Piece Identifier Notation (ASCII piece representation)
440
- - [PNN](https://sashite.dev/specs/pnn/) - Piece Name Notation (style-aware piece representation)
441
- - [CELL](https://sashite.dev/specs/cell/) - Board position coordinates
442
- - [HAND](https://sashite.dev/specs/hand/) - Reserve location notation
443
- - [PMN](https://sashite.dev/specs/pmn/) - Portable Move Notation
143
+ * `Sashite::Snn.valid?(str)` Returns `true` if the string is valid SNN.
144
+ * `Sashite::Snn.parse(str)` Returns a `Sashite::Snn::Name` object.
145
+ * `Sashite::Snn.name(sym_or_str)` Alias for constructing a name.
444
146
 
445
- ## Documentation
147
+ ### `Sashite::Snn::Name`
446
148
 
447
- - [Official SNN Specification v1.0.0](https://sashite.dev/specs/snn/1.0.0/)
448
- - [SNN Examples Documentation](https://sashite.dev/specs/snn/1.0.0/examples/)
449
- - [Game Protocol Foundation](https://sashite.dev/game-protocol/)
450
- - [API Documentation](https://rubydoc.info/github/sashite/snn.rb/main)
149
+ * `#value` Returns the canonical string value.
150
+ * `#to_s` Returns the string representation.
151
+ * `#==`, `#eql?`, `#hash` – Value-based equality.
152
+ * `#same_base_name?(other)` – Optional helper for SIN mapping equivalence.
451
153
 
452
154
  ## Development
453
155
 
@@ -0,0 +1,92 @@
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 canonical naming system for abstract strategy game styles.
8
+ # Each name must start with an uppercase ASCII letter, followed by zero or more
9
+ # lowercase letters or digits.
10
+ #
11
+ # All instances are immutable.
12
+ class Name
13
+ # SNN validation pattern matching the specification
14
+ SNN_PATTERN = /\A[A-Z][a-z0-9]*\z/
15
+
16
+ # Error messages
17
+ ERROR_INVALID_NAME = "Invalid SNN string: %s"
18
+
19
+ # @return [String] the canonical style name
20
+ attr_reader :value
21
+
22
+ # Create a new style name instance
23
+ #
24
+ # @param name [String, Symbol] the style name (e.g., "Shogi", :Chess960)
25
+ # @raise [ArgumentError] if the name does not match SNN pattern
26
+ def initialize(name)
27
+ string_value = name.to_s
28
+ self.class.validate_format(string_value)
29
+
30
+ @value = string_value.freeze
31
+ freeze
32
+ end
33
+
34
+ # Parse an SNN string into a Name object
35
+ #
36
+ # @param string [String] the SNN-formatted style name
37
+ # @return [Name] a new Name instance
38
+ # @raise [ArgumentError] if the string is invalid
39
+ #
40
+ # @example
41
+ # Sashite::Snn::Name.parse("Shogi") # => #<Snn::Name value="Shogi">
42
+ def self.parse(string)
43
+ new(string)
44
+ end
45
+
46
+ # Check whether the given string is a valid SNN name
47
+ #
48
+ # @param string [String] input string to validate
49
+ # @return [Boolean] true if valid, false otherwise
50
+ #
51
+ # @example
52
+ # Sashite::Snn::Name.valid?("Chess") # => true
53
+ # Sashite::Snn::Name.valid?("chess") # => false
54
+ def self.valid?(string)
55
+ string.is_a?(::String) && string.match?(SNN_PATTERN)
56
+ end
57
+
58
+ # Returns the string representation of the name
59
+ #
60
+ # @return [String]
61
+ def to_s
62
+ value
63
+ end
64
+
65
+ # Equality based on normalized string value
66
+ #
67
+ # @param other [Object]
68
+ # @return [Boolean]
69
+ def ==(other)
70
+ other.is_a?(self.class) && value == other.value
71
+ end
72
+
73
+ # Required for correct Set/hash behavior
74
+ alias eql? ==
75
+
76
+ # Hash based on class and value
77
+ #
78
+ # @return [Integer]
79
+ def hash
80
+ [self.class, value].hash
81
+ end
82
+
83
+ # Validate that the string is in proper SNN format
84
+ #
85
+ # @param str [String]
86
+ # @raise [ArgumentError] if invalid
87
+ def self.validate_format(str)
88
+ raise ::ArgumentError, format(ERROR_INVALID_NAME, str.inspect) unless str.match?(SNN_PATTERN)
89
+ end
90
+ end
91
+ end
92
+ end
data/lib/sashite/snn.rb CHANGED
@@ -1,63 +1,59 @@
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 standardized naming conventions with case-based side encoding, enabling clear
10
- # distinction between different traditions in multi-style gaming environments.
8
+ # Provides a formal naming system for identifying styles in abstract strategy board games.
9
+ # SNN uses canonical, human-readable ASCII names beginning with an uppercase letter.
10
+ # It supports unlimited unique style identifiers with consistent, rule-agnostic semantics.
11
11
  #
12
- # Format: <style-identifier>
13
- # - Uppercase identifier: First player styles (CHESS, SHOGI, XIANGQI)
14
- # - Lowercase identifier: Second player styles (chess, shogi, xiangqi)
15
- # - Case consistency: Entire identifier must be uppercase or lowercase
12
+ # Format: <uppercase-letter>[<lowercase-letter | digit>]*
16
13
  #
17
14
  # Examples:
18
- # "CHESS" - First player chess style
19
- # "chess" - Second player chess style
20
- # "SHOGI" - First player shōgi style
21
- # "shogi" - Second player shōgi style
15
+ # "Chess" - Standard Western chess
16
+ # "Shogi" - Japanese chess
17
+ # "Minishogi" - 5×5 compact shōgi variant
18
+ # "Chess960" - Fischer random chess
22
19
  #
23
20
  # See: https://sashite.dev/specs/snn/1.0.0/
24
21
  module Snn
25
- # Check if a string is a valid SNN notation
22
+ # Check if a string is valid SNN notation
26
23
  #
27
24
  # @param snn_string [String] the string to validate
28
25
  # @return [Boolean] true if valid SNN, false otherwise
29
26
  #
30
- # @example Validate various SNN formats
31
- # Sashite::Snn.valid?("CHESS") # => true
32
- # Sashite::Snn.valid?("Chess") # => false
27
+ # @example Validate SNN strings
28
+ # Sashite::Snn.valid?("Chess") # => true
29
+ # Sashite::Snn.valid?("minishogi") # => false
30
+ # Sashite::Snn.valid?("Go9x9") # => true
33
31
  def self.valid?(snn_string)
34
- Style.valid?(snn_string)
32
+ Name.valid?(snn_string)
35
33
  end
36
34
 
37
- # Parse an SNN string into a Style object
35
+ # Parse an SNN string into a Name object
38
36
  #
39
- # @param snn_string [String] SNN notation string
40
- # @return [Snn::Style] parsed style object with name and side attributes
41
- # @raise [ArgumentError] if the SNN string is invalid
42
- # @example Parse different SNN formats
43
- # Sashite::Snn.parse("CHESS") # => #<Snn::Style name=:Chess side=:first>
44
- # Sashite::Snn.parse("chess") # => #<Snn::Style name=:Chess side=:second>
45
- # Sashite::Snn.parse("SHOGI") # => #<Snn::Style name=:Shogi side=:first>
37
+ # @param snn_string [String] the name string
38
+ # @return [Snn::Name] a parsed name object
39
+ # @raise [ArgumentError] if the name is invalid
40
+ #
41
+ # @example Parse valid SNN names
42
+ # Sashite::Snn.parse("Shogi") # => #<Snn::Name value="Shogi">
46
43
  def self.parse(snn_string)
47
- Style.parse(snn_string)
44
+ Name.parse(snn_string)
48
45
  end
49
46
 
50
- # Create a new style instance
47
+ # Create a new Name instance directly
48
+ #
49
+ # @param value [String, Symbol] style name to construct
50
+ # @return [Snn::Name] new name instance
51
+ # @raise [ArgumentError] if name format is invalid
51
52
  #
52
- # @param name [Symbol] style name (with proper capitalization)
53
- # @param side [Symbol] player side (:first or :second)
54
- # @return [Snn::Style] new immutable style instance
55
- # @raise [ArgumentError] if parameters are invalid
56
- # @example Create styles directly
57
- # Sashite::Snn.style(:Chess, :first) # => #<Snn::Style name=:Chess side=:first>
58
- # Sashite::Snn.style(:Shogi, :second) # => #<Snn::Style name=:Shogi side=:second>
59
- def self.style(name, side)
60
- Style.new(name, side)
53
+ # @example
54
+ # Sashite::Snn.name("Xiangqi") # => #<Snn::Name value="Xiangqi">
55
+ def self.name(value)
56
+ Name.new(value)
61
57
  end
62
58
  end
63
59
  end
data/lib/sashite-snn.rb CHANGED
@@ -5,9 +5,9 @@ require_relative "sashite/snn"
5
5
  # Sashité namespace for board game notation libraries
6
6
  #
7
7
  # Sashité provides a collection of libraries for representing and manipulating
8
- # board game concepts according to the Game Protocol specifications.
8
+ # board game concepts according to the Sashité Protocol specifications.
9
9
  #
10
- # @see https://sashite.dev/game-protocol/ Game Protocol Foundation
10
+ # @see https://sashite.dev/protocol/ Sashité Protocol
11
11
  # @see https://sashite.dev/specs/ Sashité Specifications
12
12
  # @author Sashité
13
13
  module Sashite
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: 1.1.1
4
+ version: 3.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Cyril Kato
@@ -10,12 +10,12 @@ 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 standardized naming conventions with case-based side encoding,
17
- enabling clear distinction between different traditions in multi-style gaming environments.
18
- Perfect for cross-tradition matches, game engines, and hybrid gaming systems.
13
+ SNN (Style Name Notation) provides a rule-agnostic, scalable naming system for identifying
14
+ abstract strategy board game styles. This gem implements the SNN Specification v1.0.0 with
15
+ a modern Ruby interface featuring immutable style name objects and functional programming
16
+ principles. SNN uses canonical ASCII names (e.g., "Shogi", "Go9x9") to unambiguously refer
17
+ to game styles across variants and traditions. Ideal for engines, protocols, and tools that
18
+ need clear and extensible style identifiers.
19
19
  email: contact@cyril.email
20
20
  executables: []
21
21
  extensions: []
@@ -25,7 +25,7 @@ files:
25
25
  - README.md
26
26
  - lib/sashite-snn.rb
27
27
  - lib/sashite/snn.rb
28
- - lib/sashite/snn/style.rb
28
+ - lib/sashite/snn/name.rb
29
29
  homepage: https://github.com/sashite/snn.rb
30
30
  licenses:
31
31
  - MIT
@@ -34,7 +34,7 @@ metadata:
34
34
  documentation_uri: https://rubydoc.info/github/sashite/snn.rb/main
35
35
  homepage_uri: https://github.com/sashite/snn.rb
36
36
  source_code_uri: https://github.com/sashite/snn.rb
37
- specification_uri: https://sashite.dev/documents/snn/1.0.0/
37
+ specification_uri: https://sashite.dev/specs/snn/1.0.0/
38
38
  rubygems_mfa_required: 'true'
39
39
  rdoc_options: []
40
40
  require_paths:
@@ -52,5 +52,6 @@ required_rubygems_version: !ruby/object:Gem::Requirement
52
52
  requirements: []
53
53
  rubygems_version: 3.6.9
54
54
  specification_version: 4
55
- summary: SNN (Style Name Notation) implementation for Ruby with immutable style objects
55
+ summary: SNN (Style Name Notation) implementation for Ruby with immutable style name
56
+ objects
56
57
  test_files: []
@@ -1,263 +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 an alphanumeric identifier with case-based side encoding:
8
- # - Uppercase identifier: first player (CHESS, SHOGI, XIANGQI)
9
- # - Lowercase identifier: second player (chess, shogi, xiangqi)
10
- #
11
- # All instances are immutable - transformation methods return new instances.
12
- # This follows the Game Protocol's style model with Name and Side attributes.
13
- class Style
14
- # SNN validation pattern matching the specification
15
- SNN_PATTERN = /\A(?<identifier>[A-Z][A-Z0-9]*|[a-z][a-z0-9]*)\z/
16
-
17
- # Pattern for proper name capitalization (first letter uppercase, rest lowercase/digits)
18
- PROPER_NAME_PATTERN = /\A[A-Z][a-z0-9]*\z/
19
-
20
- # Player side constants
21
- FIRST_PLAYER = :first
22
- SECOND_PLAYER = :second
23
-
24
- # Valid sides
25
- VALID_SIDES = [FIRST_PLAYER, SECOND_PLAYER].freeze
26
-
27
- # Error messages
28
- ERROR_INVALID_SNN = "Invalid SNN string: %s"
29
- ERROR_INVALID_NAME = "Name must be a symbol with proper capitalization (first letter uppercase, rest lowercase), got: %s"
30
- ERROR_INVALID_SIDE = "Side must be :first or :second, got: %s"
31
-
32
- # @return [Symbol] the style name (with proper capitalization)
33
- attr_reader :name
34
-
35
- # @return [Symbol] the player side (:first or :second)
36
- attr_reader :side
37
-
38
- # Create a new style instance
39
- #
40
- # @param name [Symbol] style name (with proper capitalization)
41
- # @param side [Symbol] player side (:first or :second)
42
- # @raise [ArgumentError] if parameters are invalid
43
- def initialize(name, side)
44
- self.class.validate_name(name)
45
- self.class.validate_side(side)
46
-
47
- @name = name
48
- @side = side
49
-
50
- freeze
51
- end
52
-
53
- # Parse an SNN string into a Style object
54
- #
55
- # @param snn_string [String] SNN notation string
56
- # @return [Style] parsed style object with normalized name and inferred side
57
- # @raise [ArgumentError] if the SNN string is invalid
58
- # @example Parse SNN strings with case normalization
59
- # Sashite::Snn::Style.parse("CHESS") # => #<Snn::Style name=:Chess side=:first>
60
- # Sashite::Snn::Style.parse("chess") # => #<Snn::Style name=:Chess side=:second>
61
- # Sashite::Snn::Style.parse("SHOGI") # => #<Snn::Style name=:Shogi side=:first>
62
- def self.parse(snn_string)
63
- string_value = String(snn_string)
64
- matches = match_pattern(string_value)
65
-
66
- identifier = matches[:identifier]
67
-
68
- # Determine side from case
69
- style_side = identifier == identifier.upcase ? FIRST_PLAYER : SECOND_PLAYER
70
-
71
- # Normalize name to proper capitalization
72
- style_name = normalize_name(identifier)
73
-
74
- new(style_name, style_side)
75
- end
76
-
77
- # Check if a string is a valid SNN notation
78
- #
79
- # @param snn_string [String] the string to validate
80
- # @return [Boolean] true if valid SNN, false otherwise
81
- #
82
- # @example Validate SNN strings
83
- # Sashite::Snn::Style.valid?("CHESS") # => true
84
- # Sashite::Snn::Style.valid?("chess") # => true
85
- # Sashite::Snn::Style.valid?("Chess") # => false
86
- def self.valid?(snn_string)
87
- return false unless snn_string.is_a?(::String)
88
-
89
- snn_string.match?(SNN_PATTERN)
90
- end
91
-
92
- # Convert the style to its SNN string representation
93
- #
94
- # @return [String] SNN notation string with case based on side
95
- # @example Display different sides
96
- # style.to_s # => "CHESS" (first player)
97
- # style.to_s # => "chess" (second player)
98
- # style.to_s # => "SHOGI" (first player)
99
- def to_s
100
- first_player? ? name.to_s.upcase : name.to_s.downcase
101
- end
102
-
103
- # Create a new style with opposite ownership (side)
104
- #
105
- # @return [Style] new immutable style instance with flipped side
106
- # @example Flip player sides
107
- # style.flip # (:Chess, :first) => (:Chess, :second)
108
- def flip
109
- self.class.new(name, opposite_side)
110
- end
111
-
112
- # Create a new style with a different name (keeping same side)
113
- #
114
- # @param new_name [Symbol] new name (with proper capitalization)
115
- # @return [Style] new immutable style instance with different name
116
- # @example Change style name
117
- # style.with_name(:Shogi) # (:Chess, :first) => (:Shogi, :first)
118
- def with_name(new_name)
119
- self.class.validate_name(new_name)
120
- return self if name == new_name
121
-
122
- self.class.new(new_name, side)
123
- end
124
-
125
- # Create a new style with a different side (keeping same name)
126
- #
127
- # @param new_side [Symbol] :first or :second
128
- # @return [Style] new immutable style instance with different side
129
- # @example Change player side
130
- # style.with_side(:second) # (:Chess, :first) => (:Chess, :second)
131
- def with_side(new_side)
132
- self.class.validate_side(new_side)
133
- return self if side == new_side
134
-
135
- self.class.new(name, 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 name as another
153
- #
154
- # @param other [Style] style to compare with
155
- # @return [Boolean] true if both styles have the same name
156
- # @example Compare style names
157
- # chess1.same_name?(chess2) # (:Chess, :first) and (:Chess, :second) => true
158
- def same_name?(other)
159
- return false unless other.is_a?(self.class)
160
-
161
- name == other.name
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 name and side
178
- def ==(other)
179
- return false unless other.is_a?(self.class)
180
-
181
- name == other.name && 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, name, and side
190
- def hash
191
- [self.class, name, side].hash
192
- end
193
-
194
- # Validate that the name is a valid symbol with proper capitalization
195
- #
196
- # @param name [Symbol] the name to validate
197
- # @raise [ArgumentError] if invalid
198
- def self.validate_name(name)
199
- return if valid_name?(name)
200
-
201
- raise ::ArgumentError, format(ERROR_INVALID_NAME, name.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 name is valid (symbol with proper capitalization)
215
- #
216
- # @param name [Object] the name to check
217
- # @return [Boolean] true if valid
218
- def self.valid_name?(name)
219
- return false unless name.is_a?(::Symbol)
220
-
221
- name_string = name.to_s
222
- return false if name_string.empty?
223
-
224
- # Must match proper capitalization pattern
225
- name_string.match?(PROPER_NAME_PATTERN)
226
- end
227
-
228
- # Normalize identifier to proper capitalization symbol
229
- #
230
- # @param identifier [String] the identifier to normalize
231
- # @return [Symbol] normalized name symbol
232
- def self.normalize_name(identifier)
233
- # Convert to proper capitalization: first letter uppercase, rest lowercase
234
- normalized = identifier.downcase
235
- normalized[0] = normalized[0].upcase if normalized.length > 0
236
- normalized.to_sym
237
- end
238
-
239
- # Match SNN pattern against string
240
- #
241
- # @param string [String] string to match
242
- # @return [MatchData] match data
243
- # @raise [ArgumentError] if string doesn't match
244
- def self.match_pattern(string)
245
- matches = SNN_PATTERN.match(string)
246
- return matches if matches
247
-
248
- raise ::ArgumentError, format(ERROR_INVALID_SNN, string)
249
- end
250
-
251
- private_class_method :valid_name?, :normalize_name, :match_pattern
252
-
253
- private
254
-
255
- # Get the opposite side
256
- #
257
- # @return [Symbol] the opposite side
258
- def opposite_side
259
- first_player? ? SECOND_PLAYER : FIRST_PLAYER
260
- end
261
- end
262
- end
263
- end