sashite-snn 1.0.1 → 1.1.1

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: 2f0f7d4b0f73aa8c0d12c8d7cdbcd910d667f6f87a2edf24ad2d97c177749d21
4
- data.tar.gz: 13e22095a02f4b7f51d0b93d602974a2d6e3571db880524330e897f584d08e00
3
+ metadata.gz: c3327ae1106ccede78736585e812180c12d7a56852a82ae70b7745e233ef5496
4
+ data.tar.gz: ba5892640791f51341e52c883f52f9f21bac64e21001052e1fcbc17965a156a7
5
5
  SHA512:
6
- metadata.gz: b6e76c3b7058d5ae8cf82cde6a67f7690bc87017b127da8eae4c1389dc0d67924956bad2d4347119fa4e870f63a2e76d57910a1184f6e761b23bad53d80aad9b
7
- data.tar.gz: 8b17d5c49b83f6408d5c99a28a5bf06929e6b1088daeea9b679b67eb24c8cd0dc8bf45db780ccda5497fb368fc9e8037b61003fa4c4d10d10797f41f4cde941d
6
+ metadata.gz: 1798b7aec7b88cc2baddcd9fa7f3518c2d97bf16343caac92ec84f9bfad3dc0ac20830d95144984ea27795d867e80c008a090f9de7fc8d284ac853f9c326b144
7
+ data.tar.gz: a6a548c819cf763b914cc1b70ce82ba86d86b7973f0231df5075931788d19fb2732d4138896b347cf6a1842c554116578e59cdfb619408acb370c2c43d469753
data/README.md CHANGED
@@ -5,13 +5,13 @@
5
5
  ![Ruby](https://github.com/sashite/snn.rb/actions/workflows/main.yml/badge.svg?branch=main)
6
6
  [![License](https://img.shields.io/github/license/sashite/snn.rb?label=License&logo=github)](https://github.com/sashite/snn.rb/raw/main/LICENSE.md)
7
7
 
8
- > **SNN** (Style Name Notation) support for the Ruby language.
8
+ > **SNN** (Style Name Notation) implementation for the Ruby language.
9
9
 
10
10
  ## What is SNN?
11
11
 
12
- SNN (Style Name Notation) is a rule-agnostic format for identifying piece styles in abstract strategy board games, as defined in the [Game Protocol](https://sashite.dev/game-protocol/). It provides unambiguous identification of piece styles using standardized naming conventions with case-based player assignment, enabling clear distinction between different piece traditions in multi-style gaming environments.
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.
13
13
 
14
- This gem implements the [SNN Specification v1.0.0](https://sashite.dev/specs/snn/1.0.0/), providing a Ruby interface for working with style identifiers through a clean and minimal API.
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.
15
15
 
16
16
  ## Installation
17
17
 
@@ -26,289 +26,421 @@ Or install manually:
26
26
  gem install sashite-snn
27
27
  ```
28
28
 
29
- ## SNN Format
29
+ ## Usage
30
30
 
31
- A SNN record consists of a style identifier that maps to the **Style attribute** from the [Game Protocol](https://sashite.dev/game-protocol/):
31
+ ```ruby
32
+ require "sashite/snn"
33
+
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
39
+
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>
43
+
44
+ # 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
+ ```
32
75
 
76
+ ## Format Specification
77
+
78
+ ### Structure
33
79
  ```
34
80
  <style-identifier>
35
81
  ```
36
82
 
37
- ### Grammar Specification
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)
38
89
 
39
- ```bnf
40
- <snn> ::= <uppercase-style> | <lowercase-style>
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
+ ```
41
95
 
42
- <uppercase-style> ::= <letter-uppercase> <identifier-tail-uppercase>*
43
- <lowercase-style> ::= <letter-lowercase> <identifier-tail-lowercase>*
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
44
103
 
45
- <identifier-tail-uppercase> ::= <letter-uppercase> | <digit>
46
- <identifier-tail-lowercase> ::= <letter-lowercase> | <digit>
104
+ ## Game Examples
47
105
 
48
- <letter-uppercase> ::= "A" | "B" | "C" | ... | "Z"
49
- <letter-lowercase> ::= "a" | "b" | "c" | ... | "z"
50
- <digit> ::= "0" | "1" | "2" | "3" | "4" | "5" | "6" | "7" | "8" | "9"
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"
51
118
  ```
52
119
 
53
- ### Protocol Mapping
120
+ ### Chess Variants
121
+ ```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
126
 
55
- SNN encodes style information with player association according to the Game Protocol:
127
+ # King of the Hill Chess
128
+ koth = Sashite::Snn.style(:Koth, :first)
129
+ koth.to_s # => "KOTH"
56
130
 
57
- | Protocol Attribute | SNN Encoding | Examples |
58
- |-------------------|--------------|----------|
59
- | **Style** | Alphanumeric identifier | `CHESS`, `SHOGI`, `XIANGQI` |
60
- | **Player Association** | Case encoding | `CHESS` = First player, `chess` = Second player |
131
+ # Three-Check Chess
132
+ threecheck = Sashite::Snn.style(:Threecheck, :first)
133
+ ```
61
134
 
62
- ### Format Rules
135
+ ### Shōgi Variants
136
+ ```ruby
137
+ # Traditional Shōgi
138
+ standard_shogi = Sashite::Snn.style(:Shogi, :first)
63
139
 
64
- - **Start character**: Must be alphabetic (`A-Z` for first player, `a-z` for second player)
65
- - **Subsequent characters**: Alphabetic characters and digits only
66
- - **Case consistency**: Entire identifier must be uppercase or lowercase (no mixed case)
67
- - **Player assignment**: Uppercase = first player, lowercase = second player
68
- - **Fixed assignment**: Player-style association remains constant throughout the match
140
+ # Mini Shōgi
141
+ mini_shogi = Sashite::Snn.style(:Minishogi, :first)
69
142
 
70
- ## Basic Usage
143
+ # Chu Shōgi
144
+ chu_shogi = Sashite::Snn.style(:Chushogi, :first)
145
+ ```
71
146
 
72
- ### Creating Style Objects
147
+ ### Multi-Style Gaming
148
+ ```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
73
159
 
74
- The primary interface is the `Sashite::Snn::Style` class, which represents a style identifier in SNN format:
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
75
165
 
76
- ```ruby
77
- require "sashite/snn"
166
+ chess_white = Sashite::Snn.parse("CHESS")
167
+ shogi_black = Sashite::Snn.parse("shogi")
168
+ puts compatible_styles?(chess_white, shogi_black) # => true
169
+ ```
170
+
171
+ ## API Reference
78
172
 
79
- # Parse a SNN string into a style object
80
- style = Sashite::Snn::Style.parse("CHESS")
81
- # => #<Sashite::Snn::Style:0x... @identifier="CHESS">
173
+ ### Main Module Methods
82
174
 
83
- lowercase_style = Sashite::Snn::Style.parse("shogi")
84
- # => #<Sashite::Snn::Style:0x... @identifier="shogi">
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
85
178
 
86
- # Create directly with constructor
87
- style = Sashite::Snn::Style.new("CHESS")
88
- lowercase_style = Sashite::Snn::Style.new("makruk")
179
+ ### Style Class
89
180
 
90
- # Convenience method
91
- style = Sashite::Snn.style("XIANGQI")
92
- ```
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
93
189
 
94
- ### Converting to String and Symbol
190
+ #### Name and Case Handling
95
191
 
96
- Convert a style object to different representations:
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:
97
193
 
98
194
  ```ruby
99
- style = Sashite::Snn::Style.parse("CHESS")
100
- style.to_s # => "CHESS"
101
- style.to_sym # => :CHESS
102
-
103
- variant_style = Sashite::Snn::Style.parse("chess960")
104
- variant_style.to_s # => "chess960"
105
- variant_style.to_sym # => :chess960
106
-
107
- # Useful for hash keys and case statements
108
- game_config = {
109
- style.to_sym => { pieces: chess_pieces, rules: chess_rules }
110
- }
111
-
112
- case style.to_sym
113
- when :CHESS then setup_chess_game
114
- when :SHOGI then setup_shogi_game
115
- end
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)
201
+
202
+ style1.to_s # => "CHESS" (uppercase display)
203
+ style2.to_s # => "chess" (lowercase display)
116
204
  ```
117
205
 
118
- ### Player Association
206
+ #### Side Queries
207
+ - `#first_player?` - Check if first player style
208
+ - `#second_player?` - Check if second player style
119
209
 
120
- Check which player a style belongs to according to the Game Protocol:
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
121
214
 
122
- ```ruby
123
- first_player_style = Sashite::Snn::Style.parse("CHESS")
124
- first_player_style.first_player? # => true (uppercase = first player)
125
- first_player_style.second_player? # => false
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
126
219
 
127
- second_player_style = Sashite::Snn::Style.parse("shogi")
128
- second_player_style.first_player? # => false
129
- second_player_style.second_player? # => true (lowercase = second player)
130
- ```
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
131
226
 
132
- ## Validation
227
+ ## Advanced Usage
133
228
 
134
- All parsing automatically validates input according to the SNN specification:
229
+ ### Name Normalization Examples
135
230
 
136
231
  ```ruby
137
- # Valid SNN strings
138
- Sashite::Snn::Style.parse("CHESS") # ✓ First player chess
139
- Sashite::Snn::Style.parse("shogi") # ✓ Second player shogi
140
- Sashite::Snn::Style.parse("CHESS960") # ✓ First player Chess960 variant
141
- Sashite::Snn::Style.parse("makruk") # ✓ Second player makruk
142
-
143
- # Valid constructor calls
144
- Sashite::Snn::Style.new("XIANGQI") # ✓ First player xiangqi
145
- Sashite::Snn::Style.new("janggi") # ✓ Second player janggi
146
-
147
- # Convenience method
148
- Sashite::Snn.style("MINISHOGI") # ✓ First player minishogi
149
-
150
- # Check validity
151
- Sashite::Snn.valid?("CHESS") # => true
152
- Sashite::Snn.valid?("Chess") # => false (mixed case not allowed)
153
- Sashite::Snn.valid?("123") # => false (must start with letter)
154
- Sashite::Snn.valid?("") # => false (empty string)
155
-
156
- # Invalid SNN strings raise ArgumentError
157
- Sashite::Snn::Style.parse("") # ✗ ArgumentError
158
- Sashite::Snn::Style.parse("Chess") # ✗ ArgumentError (mixed case)
159
- Sashite::Snn::Style.parse("9CHESS") # ✗ ArgumentError (starts with digit)
160
- Sashite::Snn::Style.parse("CHESS-960") # ✗ ArgumentError (contains hyphen)
161
- ```
232
+ # Parsing different cases results in same name
233
+ white_chess = Sashite::Snn.parse("CHESS")
234
+ black_chess = Sashite::Snn.parse("chess")
162
235
 
163
- ## Examples of SNN in Practice
236
+ # Names are normalized with proper capitalization
237
+ white_chess.name # => :Chess
238
+ black_chess.name # => :Chess (same name!)
164
239
 
165
- ### Classic Game Styles
240
+ # Sides are different
241
+ white_chess.side # => :first
242
+ black_chess.side # => :second
166
243
 
167
- ```ruby
168
- # Traditional chess match (both players use chess pieces)
169
- first_player = Sashite::Snn::Style.parse("CHESS") # First player
170
- second_player = Sashite::Snn::Style.parse("chess") # Second player
244
+ # Display follows side convention
245
+ white_chess.to_s # => "CHESS"
246
+ black_chess.to_s # => "chess"
171
247
 
172
- # Cross-style game: Chess vs Shogi
173
- first_player = Sashite::Snn::Style.parse("CHESS") # First player uses chess pieces
174
- second_player = Sashite::Snn::Style.parse("shogi") # Second player uses shogi pieces
248
+ # Same name, different sides
249
+ white_chess.same_name?(black_chess) # => true
250
+ white_chess.same_side?(black_chess) # => false
251
+ ```
175
252
 
176
- # Variant games
177
- first_player = Sashite::Snn::Style.parse("CHESS960") # First player uses Chess960 variant
178
- second_player = Sashite::Snn::Style.parse("chess960") # Second player uses Chess960 variant
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"
179
268
  ```
180
269
 
181
- ### Style Examples from SNN Specification
270
+ ### Game Configuration Management
271
+ ```ruby
272
+ class GameConfiguration
273
+ def initialize
274
+ @player_styles = {}
275
+ end
182
276
 
183
- According to the [SNN Examples Documentation](https://sashite.dev/specs/snn/1.0.0/examples/):
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
184
281
 
185
- | SNN | Interpretation |
186
- |-----|----------------|
187
- | `CHESS` | Chess first player (white) |
188
- | `chess` | Chess second player (black) |
189
- | `SHOGI` | Shōgi first player (sente) |
190
- | `shogi` | Shōgi second player (gote) |
191
- | `XIANGQI` | Xiangqi first player (red) |
192
- | `xiangqi` | Xiangqi second player (black) |
282
+ def get_player_style(player)
283
+ @player_styles[player]
284
+ end
193
285
 
194
- ```ruby
195
- # Standard game representations
196
- chess_white = Sashite::Snn::Style.parse("CHESS") # White pieces (first player)
197
- chess_black = Sashite::Snn::Style.parse("chess") # Black pieces (second player)
286
+ def style_mismatch?
287
+ return false if @player_styles.size < 2
198
288
 
199
- shogi_sente = Sashite::Snn::Style.parse("SHOGI") # Sente (first player)
200
- shogi_gote = Sashite::Snn::Style.parse("shogi") # Gote (second player)
289
+ styles = @player_styles.values
290
+ !styles.all? { |style| style.same_name?(styles.first) }
291
+ end
201
292
 
202
- xiangqi_red = Sashite::Snn::Style.parse("XIANGQI") # Red pieces (first player)
203
- xiangqi_black = Sashite::Snn::Style.parse("xiangqi") # Black pieces (second player)
204
- ```
293
+ def cross_tradition_match?
294
+ return false if @player_styles.size < 2
205
295
 
206
- ### Variant Styles
296
+ style_names = @player_styles.values.map(&:name).uniq
297
+ style_names.size > 1
298
+ end
299
+ end
207
300
 
208
- ```ruby
209
- # Chess variants
210
- fischer_random = Sashite::Snn::Style.parse("CHESS960")
211
- king_of_hill = Sashite::Snn::Style.parse("CHESSKING")
301
+ # Usage
302
+ config = GameConfiguration.new
303
+ config.set_player_style(:white, :Chess)
304
+ config.set_player_style(:black, :Shogi)
212
305
 
213
- # Shogi variants
214
- mini_shogi = Sashite::Snn::Style.parse("MINISHOGI")
215
- handicap_shogi = Sashite::Snn::Style.parse("SHOGI9")
306
+ puts config.cross_tradition_match? # => true
307
+ puts config.style_mismatch? # => true
216
308
 
217
- # Other traditions
218
- thai_makruk = Sashite::Snn::Style.parse("MAKRUK")
219
- korean_janggi = Sashite::Snn::Style.parse("JANGGI")
309
+ white_style = config.get_player_style(:white)
310
+ puts white_style.to_s # => "CHESS"
220
311
  ```
221
312
 
222
- ### Cross-Style Gaming
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
+ ```
223
333
 
334
+ ### Tournament Style Management
224
335
  ```ruby
225
- # Create a cross-style game setup
226
- first_player_style = Sashite::Snn::Style.parse("CHESS")
227
- second_player_style = Sashite::Snn::Style.parse("makruk")
228
-
229
- puts "Game: #{first_player_style} vs #{second_player_style}"
230
- # => "Game: CHESS vs makruk"
231
-
232
- # This represents a unique game where chess pieces face makruk pieces
233
-
234
- # Each player keeps their assigned style throughout the game
235
- game_config = {
236
- first_player: first_player_style, # Fixed: CHESS
237
- second_player: second_player_style # Fixed: makruk
238
- }
239
-
240
- # Using symbols for configuration
241
- style_configs = {
242
- first_player_style.to_sym => { piece_set: :western, board: :"8x8" },
243
- second_player_style.to_sym => { piece_set: :thai, board: :"8x8" }
244
- }
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]
245
377
  ```
246
378
 
247
- ## API Reference
379
+ ## Protocol Mapping
248
380
 
249
- ### Module Methods
381
+ Following the [Game Protocol](https://sashite.dev/game-protocol/):
250
382
 
251
- - `Sashite::Snn.valid?(snn_string)` - Check if a string is valid SNN notation
252
- - `Sashite::Snn.style(identifier)` - Convenience method to create style objects
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 |
253
387
 
254
- ### Sashite::Snn::Style Class Methods
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.
255
389
 
256
- - `Sashite::Snn::Style.parse(snn_string)` - Parse a SNN string into a style object
257
- - `Sashite::Snn::Style.new(identifier)` - Create a new style instance
390
+ **Canonical principle**: Identical styles must have identical SNN representations.
258
391
 
259
- ### Instance Methods
392
+ ## Properties
260
393
 
261
- #### Player Queries
262
- - `#first_player?` - Check if style belongs to first player (uppercase)
263
- - `#second_player?` - Check if style belongs to second player (lowercase)
264
- - `#uppercase?` - Check if identifier is uppercase
265
- - `#lowercase?` - Check if identifier is lowercase
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
266
400
 
267
- #### Conversion
268
- - `#to_s` - Convert to SNN string representation
269
- - `#to_sym` - Convert to symbol representation
270
- - `#inspect` - Detailed string representation for debugging
401
+ ## Implementation Notes
271
402
 
272
- ## Design Properties
403
+ ### Name Normalization Convention
273
404
 
274
- Following the SNN specification, this implementation provides:
405
+ SNN follows a strict name normalization convention:
275
406
 
276
- - **Rule-agnostic**: Independent of specific game mechanics
277
- - **Unambiguous identification**: Clear distinction between different piece traditions
278
- - **Cross-style support**: Enables multi-tradition gaming environments
279
- - **Canonical representation**: Consistent naming for equivalent styles
280
- - **Player clarity**: Case-based player association throughout gameplay
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
281
411
 
282
- ## System Constraints
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
283
416
 
284
- * SNN supports exactly **two players**
285
- * Players are distinguished by casing: **uppercase** for first player, **lowercase** for second player
286
- * Style identifiers must start with an alphabetic character
287
- * Subsequent characters may include alphabetic characters and digits only
288
- * Mixed casing is not permitted within a single identifier
289
- * Style assignment to players remains **fixed throughout a game**
290
- * Total piece count must remain constant (Game Protocol conservation principle)
417
+ ### Example Flow
291
418
 
292
- ## Use Cases
419
+ ```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)
426
+ ```
293
427
 
294
- SNN is particularly useful in the following scenarios:
428
+ This ensures that `parse(snn).to_s == snn` for all valid SNN strings while maintaining internal consistency.
295
429
 
296
- 1. **Multi-style environments**: When games involve pieces from multiple traditions or variants
297
- 2. **Game engine development**: When implementing engines that need to distinguish between different piece style traditions
298
- 3. **Hybrid games**: When creating or analyzing games that combine elements from different piece traditions
299
- 4. **Database systems**: When storing game data that must avoid naming conflicts between similar styles
300
- 5. **Cross-tradition analysis**: When comparing or analyzing strategic elements across different piece traditions
301
- 6. **Tournament systems**: When organizing events that allow players to choose from different piece style traditions
430
+ ## System Constraints
302
431
 
303
- ## Game Protocol Compliance
432
+ - **Alphanumeric identifiers** starting with a letter
433
+ - **Exactly 2 players** (uppercase/lowercase distinction)
434
+ - **Case consistency** within each identifier (no mixed case)
304
435
 
305
- This implementation fully complies with the [Game Protocol](https://sashite.dev/game-protocol/) by:
436
+ ## Related Specifications
306
437
 
307
- - Representing the **Style attribute** of pieces
308
- - Supporting **two-player** constraint
309
- - Maintaining **piece conservation** (styles are immutable)
310
- - Enabling **cross-style** gameplay scenarios
311
- - Providing **deterministic** style identification
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
312
444
 
313
445
  ## Documentation
314
446
 
@@ -317,6 +449,33 @@ This implementation fully complies with the [Game Protocol](https://sashite.dev/
317
449
  - [Game Protocol Foundation](https://sashite.dev/game-protocol/)
318
450
  - [API Documentation](https://rubydoc.info/github/sashite/snn.rb/main)
319
451
 
452
+ ## Development
453
+
454
+ ```sh
455
+ # Clone the repository
456
+ git clone https://github.com/sashite/snn.rb.git
457
+ cd snn.rb
458
+
459
+ # Install dependencies
460
+ bundle install
461
+
462
+ # Run tests
463
+ ruby test.rb
464
+
465
+ # Generate documentation
466
+ yard doc
467
+ ```
468
+
469
+ ## Contributing
470
+
471
+ 1. Fork the repository
472
+ 2. Create a feature branch (`git checkout -b feature/new-feature`)
473
+ 3. Add tests for your changes
474
+ 4. Ensure all tests pass (`ruby test.rb`)
475
+ 5. Commit your changes (`git commit -am 'Add new feature'`)
476
+ 6. Push to the branch (`git push origin feature/new-feature`)
477
+ 7. Create a Pull Request
478
+
320
479
  ## License
321
480
 
322
481
  Available as open source under the [MIT License](https://opensource.org/licenses/MIT).
@@ -2,141 +2,261 @@
2
2
 
3
3
  module Sashite
4
4
  module Snn
5
- # Represents a style identifier in SNN format
5
+ # Represents a style in SNN (Style Name Notation) format.
6
6
  #
7
- # A style represents a particular tradition, variant, or design approach for game pieces.
8
- # The casing of the identifier determines player association:
9
- # - Uppercase identifiers belong to the first player
10
- # - Lowercase identifiers belong to the second player
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)
11
10
  #
12
- # @example
13
- # # First player styles (uppercase)
14
- # chess_style = Sashite::Snn::Style.new("CHESS")
15
- # shogi_style = Sashite::Snn::Style.new("SHOGI")
16
- #
17
- # # Second player styles (lowercase)
18
- # makruk_style = Sashite::Snn::Style.new("makruk")
19
- # chess960_style = Sashite::Snn::Style.new("chess960")
11
+ # All instances are immutable - transformation methods return new instances.
12
+ # This follows the Game Protocol's style model with Name and Side attributes.
20
13
  class Style
21
- # @return [String] The style identifier
22
- attr_reader :identifier
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
23
37
 
24
38
  # Create a new style instance
25
39
  #
26
- # @param identifier [String] The style identifier
27
- # @raise [ArgumentError] if the identifier is invalid SNN notation
28
- #
29
- # @example
30
- # style = Sashite::Snn::Style.new("CHESS")
31
- # # => #<Sashite::Snn::Style:0x... @identifier="CHESS">
32
- def initialize(identifier)
33
- raise ArgumentError, "Invalid SNN format: #{identifier.inspect}" unless Snn.valid?(identifier)
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)
34
46
 
35
- @identifier = identifier.freeze
47
+ @name = name
48
+ @side = side
36
49
 
37
50
  freeze
38
51
  end
39
52
 
40
- # Parse a SNN string into a style object
53
+ # Parse an SNN string into a Style object
41
54
  #
42
- # @param snn_string [String] The SNN string to parse
43
- # @return [Sashite::Snn::Style] A new style object
44
- # @raise [ArgumentError] if the string is invalid SNN notation
45
- #
46
- # @example
47
- # style = Sashite::Snn::Style.parse("CHESS")
48
- # # => #<Sashite::Snn::Style:0x... @identifier="CHESS">
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>
49
62
  def self.parse(snn_string)
50
- new(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)
51
75
  end
52
76
 
53
- # Check if this style belongs to the first player
77
+ # Check if a string is a valid SNN notation
54
78
  #
55
- # @return [Boolean] true if the style is uppercase (first player), false otherwise
79
+ # @param snn_string [String] the string to validate
80
+ # @return [Boolean] true if valid SNN, false otherwise
56
81
  #
57
- # @example
58
- # Sashite::Snn::Style.new("CHESS").first_player? # => true
59
- # Sashite::Snn::Style.new("shogi").first_player? # => false
60
- def first_player?
61
- uppercase?
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)
62
90
  end
63
91
 
64
- # Check if this style belongs to the second player
92
+ # Convert the style to its SNN string representation
65
93
  #
66
- # @return [Boolean] true if the style is lowercase (second player), false otherwise
67
- #
68
- # @example
69
- # Sashite::Snn::Style.new("CHESS").second_player? # => false
70
- # Sashite::Snn::Style.new("shogi").second_player? # => true
71
- def second_player?
72
- lowercase?
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
73
101
  end
74
102
 
75
- # Check if the style identifier is uppercase
76
- #
77
- # @return [Boolean] true if the identifier is uppercase, false otherwise
103
+ # Create a new style with opposite ownership (side)
78
104
  #
79
- # @example
80
- # Sashite::Snn::Style.new("CHESS").uppercase? # => true
81
- # Sashite::Snn::Style.new("chess").uppercase? # => false
82
- def uppercase?
83
- identifier == identifier.upcase
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)
84
110
  end
85
111
 
86
- # Check if the style identifier is lowercase
112
+ # Create a new style with a different name (keeping same side)
87
113
  #
88
- # @return [Boolean] true if the identifier is lowercase, false otherwise
89
- #
90
- # @example
91
- # Sashite::Snn::Style.new("CHESS").lowercase? # => false
92
- # Sashite::Snn::Style.new("chess").lowercase? # => true
93
- def lowercase?
94
- identifier == identifier.downcase
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)
95
123
  end
96
124
 
97
- # Convert the style to its string representation
125
+ # Create a new style with a different side (keeping same name)
98
126
  #
99
- # @return [String] The style identifier
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
100
139
  #
101
- # @example
102
- # Sashite::Snn::Style.new("CHESS").to_s # => "CHESS"
103
- def to_s
104
- identifier
140
+ # @return [Boolean] true if first player
141
+ def first_player?
142
+ side == FIRST_PLAYER
105
143
  end
106
144
 
107
- # Convert the style to a symbol
145
+ # Check if the style belongs to the second player
108
146
  #
109
- # @return [Symbol] The style identifier as a symbol
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
110
153
  #
111
- # @example
112
- # Sashite::Snn::Style.new("CHESS").to_sym # => :CHESS
113
- def to_sym
114
- identifier.to_sym
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
115
162
  end
116
163
 
117
- # String representation for debugging
164
+ # Check if this style belongs to the same side as another
118
165
  #
119
- # @return [String] A detailed string representation
120
- def inspect
121
- "#<#{self.class}:0x#{object_id.to_s(16)} @identifier=#{identifier.inspect}>"
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
122
172
  end
123
173
 
124
- # Equality comparison
174
+ # Custom equality comparison
125
175
  #
126
- # @param other [Object] The object to compare with
127
- # @return [Boolean] true if both objects are Style instances with the same identifier
176
+ # @param other [Object] object to compare with
177
+ # @return [Boolean] true if both objects are styles with identical name and side
128
178
  def ==(other)
129
- other.is_a?(Style) && identifier == other.identifier
179
+ return false unless other.is_a?(self.class)
180
+
181
+ name == other.name && side == other.side
130
182
  end
131
183
 
132
- # Alias for equality comparison
184
+ # Alias for == to ensure Set functionality works correctly
133
185
  alias eql? ==
134
186
 
135
- # Hash code for use in hashes and sets
187
+ # Custom hash implementation for use in collections
136
188
  #
137
- # @return [Integer] The hash code
189
+ # @return [Integer] hash value based on class, name, and side
138
190
  def hash
139
- [self.class, identifier].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
140
260
  end
141
261
  end
142
262
  end
data/lib/sashite/snn.rb CHANGED
@@ -3,47 +3,61 @@
3
3
  require_relative "snn/style"
4
4
 
5
5
  module Sashite
6
- # Style Name Notation (SNN) module
6
+ # SNN (Style Name Notation) implementation for Ruby
7
7
  #
8
- # SNN provides a consistent and rule-agnostic format for identifying piece styles
9
- # in abstract strategy board games. It enables clear distinction between different
10
- # piece traditions, variants, or design approaches within multi-style gaming environments.
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.
11
11
  #
12
- # @see https://sashite.dev/documents/snn/1.0.0/ SNN Specification v1.0.0
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
16
+ #
17
+ # 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
22
+ #
23
+ # See: https://sashite.dev/specs/snn/1.0.0/
13
24
  module Snn
14
- # SNN validation regular expression
15
- # Matches: uppercase style (A-Z followed by A-Z0-9*) or lowercase style (a-z followed by a-z0-9*)
16
- VALIDATION_REGEX = /\A([A-Z][A-Z0-9]*|[a-z][a-z0-9]*)\z/
17
-
18
- # Check if a string is valid SNN notation
25
+ # Check if a string is a valid SNN notation
19
26
  #
20
- # @param snn_string [String] The string to validate
21
- # @return [Boolean] true if the string is valid SNN notation, false otherwise
27
+ # @param snn_string [String] the string to validate
28
+ # @return [Boolean] true if valid SNN, false otherwise
22
29
  #
23
- # @example
24
- # Sashite::Snn.valid?("CHESS") # => true
25
- # Sashite::Snn.valid?("shogi") # => true
26
- # Sashite::Snn.valid?("Chess") # => false (mixed case)
27
- # Sashite::Snn.valid?("123") # => false (must start with letter)
28
- # Sashite::Snn.valid?("") # => false (empty string)
30
+ # @example Validate various SNN formats
31
+ # Sashite::Snn.valid?("CHESS") # => true
32
+ # Sashite::Snn.valid?("Chess") # => false
29
33
  def self.valid?(snn_string)
30
- return false unless snn_string.is_a?(String)
31
- return false if snn_string.empty?
32
-
33
- VALIDATION_REGEX.match?(snn_string)
34
+ Style.valid?(snn_string)
34
35
  end
35
36
 
36
- # Convenience method to create a style object
37
+ # Parse an SNN string into a Style object
37
38
  #
38
- # @param identifier [String] The style identifier
39
- # @return [Sashite::Snn::Style] A new style object
40
- # @raise [ArgumentError] if the identifier is invalid
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>
46
+ def self.parse(snn_string)
47
+ Style.parse(snn_string)
48
+ end
49
+
50
+ # Create a new style instance
41
51
  #
42
- # @example
43
- # style = Sashite::Snn.style("CHESS")
44
- # # => #<Sashite::Snn::Style:0x... @identifier="CHESS">
45
- def self.style(identifier)
46
- Style.new(identifier)
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)
47
61
  end
48
62
  end
49
63
  end
data/lib/sashite-snn.rb CHANGED
@@ -1,18 +1,14 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative "sashite/snn"
4
+
3
5
  # Sashité namespace for board game notation libraries
6
+ #
7
+ # Sashité provides a collection of libraries for representing and manipulating
8
+ # board game concepts according to the Game Protocol specifications.
9
+ #
10
+ # @see https://sashite.dev/game-protocol/ Game Protocol Foundation
11
+ # @see https://sashite.dev/specs/ Sashité Specifications
12
+ # @author Sashité
4
13
  module Sashite
5
- # Style Name Notation (SNN) implementation for Ruby
6
- #
7
- # SNN is a consistent and rule-agnostic format for identifying piece styles
8
- # in abstract strategy board games. It provides unambiguous identification
9
- # of piece styles by using standardized naming conventions, enabling clear
10
- # distinction between different piece traditions, variants, or design
11
- # approaches within multi-style gaming environments.
12
- #
13
- # @see https://sashite.dev/documents/snn/1.0.0/ SNN Specification v1.0.0
14
- # @author Sashité
15
- # @since 1.0.0
16
14
  end
17
-
18
- require_relative "sashite/snn"
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.0.1
4
+ version: 1.1.1
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
- A clean, minimal Ruby implementation of Style Name Notation (SNN) for abstract strategy games.
14
- SNN provides a consistent and rule-agnostic format for identifying piece styles, enabling
15
- clear distinction between different piece traditions, variants, or design approaches within
16
- multi-style gaming environments. Features include player-based casing, style validation,
17
- and cross-style compatibility. Perfect for game engines, multi-tradition environments,
18
- and hybrid gaming systems.
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.
19
19
  email: contact@cyril.email
20
20
  executables: []
21
21
  extensions: []
@@ -52,5 +52,5 @@ required_rubygems_version: !ruby/object:Gem::Requirement
52
52
  requirements: []
53
53
  rubygems_version: 3.6.9
54
54
  specification_version: 4
55
- summary: Style Name Notation (SNN) support for the Ruby language.
55
+ summary: SNN (Style Name Notation) implementation for Ruby with immutable style objects
56
56
  test_files: []