sashite-snn 2.0.0 → 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: 6fb3b57b1f252e95f7e7d3e8484d6729b90ca6f13dfd19705c9ac61d6095cf25
4
- data.tar.gz: 296ec73c256baf0ea7999b74b7933b0e95f92b2b7488e3d5efb69b43b7132951
3
+ metadata.gz: 517b0310bd57b5d0b931a5f14eb1c4d6257bbd6d4f215c467ff475c8b42542fc
4
+ data.tar.gz: 2fa5a177d6350dd2501b87eac5a567f5cf48c681439b3af23f2bae1df99579a3
5
5
  SHA512:
6
- metadata.gz: 1e7b8270ca459703a6bd41f47424af55452f8c287567abc8bdf0e85acc69914f20a1830361ce10e3f0e157c137e3f31f60149481274b24e6dedb597d109927d6
7
- data.tar.gz: 9a684bb46685f7a77fbade6caaf2d642f31a6b31b142e1fb5a47512e8a4133fa50adf0f24c8b551d87c67232c23db6c5436e95d819a38ac72e069f37da63aff5
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 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 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 rule-agnostic notation system for style identification in board games.
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
 
@@ -33,392 +33,123 @@ gem install sashite-snn
33
33
  ```ruby
34
34
  require "sashite/snn"
35
35
 
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
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"
41
40
 
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>
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">
45
44
 
46
45
  # 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)
46
+ Sashite::Snn.valid?("Go9x9") # => true
47
+ Sashite::Snn.valid?("chess") # => false (must start with uppercase)
48
+ Sashite::Snn.valid?("3DChess") # => false (invalid character)
51
49
  ```
52
50
 
53
- ### Style Transformations
51
+ ### Normalization and Comparison
54
52
 
55
53
  ```ruby
56
- # All transformations return new immutable instances
57
- style = Sashite::Snn.parse("C")
58
-
59
- # Flip player assignment
60
- flipped = style.flip # => #<Snn::Style letter=:c side=:second>
61
- flipped.to_s # => "c"
62
-
63
- # Change letter
64
- changed = style.with_letter(:S) # => #<Snn::Style letter=:S side=:first>
65
- changed.to_s # => "S"
54
+ a = Sashite::Snn.parse("Chess960")
55
+ b = Sashite::Snn.parse("Chess960")
66
56
 
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"
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"
74
60
  ```
75
61
 
76
- ### Player and Style Queries
62
+ ### Canonical Representation
77
63
 
78
64
  ```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)
65
+ # All names are normalized to a canonical format
66
+ name = Sashite::Snn.parse("Minishogi")
67
+ name.value # => "Minishogi"
68
+ name.to_s # => "Minishogi"
96
69
  ```
97
70
 
98
- ### Style Collections
71
+ ### Collections and Filtering
99
72
 
100
73
  ```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"]
74
+ names = %w[Chess Shogi Makruk Antichess Minishogi].map { |n| Sashite::Snn.parse(n) }
107
75
 
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"]
76
+ # Filter by prefix
77
+ names.select { |n| n.value.start_with?("Mini") }.map(&:to_s)
78
+ # => ["Minishogi"]
115
79
  ```
116
80
 
117
81
  ## Format Specification
118
82
 
119
83
  ### Structure
84
+
120
85
  ```
121
- <style-letter>
86
+ <uppercase-letter>[<lowercase-letter | digit>]*
122
87
  ```
123
88
 
124
89
  ### Grammar (BNF)
90
+
125
91
  ```bnf
126
- <snn> ::= <uppercase-letter> | <lowercase-letter>
92
+ <snn> ::= <uppercase-letter> <tail>
93
+
94
+ <tail> ::= "" ; Single letter (e.g., "X")
95
+ | <alphanumeric-char> <tail> ; Extended name
96
+
97
+ <alphanumeric-char> ::= <lowercase-letter> | <digit>
127
98
 
128
99
  <uppercase-letter> ::= "A" | "B" | "C" | ... | "Z"
129
100
  <lowercase-letter> ::= "a" | "b" | "c" | ... | "z"
101
+ <digit> ::= "0" | "1" | "2" | "3" | ... | "9"
130
102
  ```
131
103
 
132
104
  ### Regular Expression
133
- ```ruby
134
- /\A[A-Za-z]\z/
135
- ```
136
-
137
- ### Style Attribute Mapping
138
-
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 |
143
-
144
- ## Game Examples
145
-
146
- The SNN specification is rule-agnostic and does not define specific letter assignments. However, here are common usage patterns:
147
-
148
- ### Traditional Game Families
149
105
 
150
106
  ```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
154
-
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
158
-
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
162
- ```
163
-
164
- ### Cross-Style Scenarios
165
-
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
174
-
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)
107
+ /\A[A-Z][a-z0-9]*\z/
178
108
  ```
179
109
 
180
- ### Variant Families
110
+ ## Design Principles
181
111
 
182
- ```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"
191
- ```
192
-
193
- ## API Reference
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.
194
116
 
195
- ### Main Module Methods
117
+ ## Integration with SIN
196
118
 
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
119
+ SNN names serve as the formal source for SIN character identifiers. For example:
200
120
 
201
- ### Style Class
121
+ | SNN | SIN |
122
+ | --------- | ------- |
123
+ | `Chess` | `C`/`c` |
124
+ | `Shogi` | `S`/`s` |
125
+ | `Xiangqi` | `X`/`x` |
126
+ | `Makruk` | `M`/`m` |
202
127
 
203
- #### Creation and Parsing
204
- - `Sashite::Snn::Style.new(letter, side)` - Create style instance
205
- - `Sashite::Snn::Style.parse(snn_string)` - Parse SNN string
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.
206
129
 
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
211
-
212
- #### Player Queries
213
- - `#first_player?` - Check if first player style
214
- - `#second_player?` - Check if second player style
215
-
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
220
-
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
225
-
226
- ### Style Class Constants
227
-
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
232
-
233
- ## Advanced Usage
234
-
235
- ### Letter Case and Side Mapping
130
+ ## Examples
236
131
 
237
132
  ```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) }
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::Snn.parse("A")
250
- letter_a_second = Sashite::Snn.parse("a")
251
-
252
- letter_a_first.same_letter?(letter_a_second) # => true
253
- letter_a_first.same_side?(letter_a_second) # => false
254
- ```
255
-
256
- ### Immutable Transformations
257
-
258
- ```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"
272
- ```
273
-
274
- ### Game Configuration Management
275
-
276
- ```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
307
-
308
- config.cross_family_match? # => true
309
-
310
- white_style = config.get_player_style(:white)
311
- white_style.to_s # => "C"
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
312
137
  ```
313
138
 
314
- ### Style Analysis
315
-
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
328
-
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
- ```
335
-
336
- ### Tournament Style Registry
337
-
338
- ```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
369
-
370
- # Usage
371
- registry = TournamentStyleRegistry.new
372
- registry.register_letter(:C)
373
- registry.register_letter(:S)
374
-
375
- chess_white = Sashite::Snn.parse("C")
376
- shogi_black = Sashite::Snn.parse("s")
377
-
378
- registry.valid_pairing?(chess_white, shogi_black) # => true
379
- registry.supported_families # => ["C", "S"]
380
- ```
381
-
382
- ## Protocol Mapping
383
-
384
- Following the [Sashité Protocol](https://sashite.dev/protocol/):
385
-
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 |
390
-
391
- ## System Constraints
392
-
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
397
-
398
- ## Design Properties
399
-
400
- - **ASCII compatibility**: Maximum portability across systems
401
- - **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
139
+ ## API Reference
406
140
 
407
- ## Related Specifications
141
+ ### Main Module
408
142
 
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
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.
415
146
 
416
- ## Documentation
147
+ ### `Sashite::Snn::Name`
417
148
 
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)
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.
422
153
 
423
154
  ## Development
424
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,65 +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 single ASCII letters with case-based side encoding, enabling clear
10
- # distinction between different style families 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-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: <uppercase-letter>[<lowercase-letter | digit>]*
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" - 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?("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)
27
+ # @example Validate SNN strings
28
+ # Sashite::Snn.valid?("Chess") # => true
29
+ # Sashite::Snn.valid?("minishogi") # => false
30
+ # Sashite::Snn.valid?("Go9x9") # => true
35
31
  def self.valid?(snn_string)
36
- Style.valid?(snn_string)
32
+ Name.valid?(snn_string)
37
33
  end
38
34
 
39
- # Parse an SNN string into a Style object
35
+ # Parse an SNN string into a Name object
40
36
  #
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>
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">
48
43
  def self.parse(snn_string)
49
- Style.parse(snn_string)
44
+ Name.parse(snn_string)
50
45
  end
51
46
 
52
- # 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
53
52
  #
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)
53
+ # @example
54
+ # Sashite::Snn.name("Xiangqi") # => #<Snn::Name value="Xiangqi">
55
+ def self.name(value)
56
+ Name.new(value)
63
57
  end
64
58
  end
65
59
  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.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Cyril Kato
@@ -10,13 +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 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 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.
20
19
  email: contact@cyril.email
21
20
  executables: []
22
21
  extensions: []
@@ -26,7 +25,7 @@ files:
26
25
  - README.md
27
26
  - lib/sashite-snn.rb
28
27
  - lib/sashite/snn.rb
29
- - lib/sashite/snn/style.rb
28
+ - lib/sashite/snn/name.rb
30
29
  homepage: https://github.com/sashite/snn.rb
31
30
  licenses:
32
31
  - MIT
@@ -53,5 +52,6 @@ required_rubygems_version: !ruby/object:Gem::Requirement
53
52
  requirements: []
54
53
  rubygems_version: 3.6.9
55
54
  specification_version: 4
56
- 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
57
57
  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