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 +4 -4
- data/README.md +368 -209
- data/lib/sashite/snn/style.rb +208 -88
- data/lib/sashite/snn.rb +45 -31
- data/lib/sashite-snn.rb +9 -13
- metadata +8 -8
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: c3327ae1106ccede78736585e812180c12d7a56852a82ae70b7745e233ef5496
|
4
|
+
data.tar.gz: ba5892640791f51341e52c883f52f9f21bac64e21001052e1fcbc17965a156a7
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 1798b7aec7b88cc2baddcd9fa7f3518c2d97bf16343caac92ec84f9bfad3dc0ac20830d95144984ea27795d867e80c008a090f9de7fc8d284ac853f9c326b144
|
7
|
+
data.tar.gz: a6a548c819cf763b914cc1b70ce82ba86d86b7973f0231df5075931788d19fb2732d4138896b347cf6a1842c554116578e59cdfb619408acb370c2c43d469753
|
data/README.md
CHANGED
@@ -5,13 +5,13 @@
|
|
5
5
|

|
6
6
|
[](https://github.com/sashite/snn.rb/raw/main/LICENSE.md)
|
7
7
|
|
8
|
-
> **SNN** (Style Name Notation)
|
8
|
+
> **SNN** (Style Name Notation) implementation for the Ruby language.
|
9
9
|
|
10
10
|
## What is SNN?
|
11
11
|
|
12
|
-
SNN (Style Name Notation)
|
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
|
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
|
-
##
|
29
|
+
## Usage
|
30
30
|
|
31
|
-
|
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
|
-
###
|
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
|
-
|
40
|
-
|
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
|
-
|
43
|
-
|
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
|
-
|
46
|
-
<identifier-tail-lowercase> ::= <letter-lowercase> | <digit>
|
104
|
+
## Game Examples
|
47
105
|
|
48
|
-
|
49
|
-
|
50
|
-
|
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
|
-
###
|
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
|
-
|
127
|
+
# King of the Hill Chess
|
128
|
+
koth = Sashite::Snn.style(:Koth, :first)
|
129
|
+
koth.to_s # => "KOTH"
|
56
130
|
|
57
|
-
|
58
|
-
|
59
|
-
|
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
|
-
###
|
135
|
+
### Shōgi Variants
|
136
|
+
```ruby
|
137
|
+
# Traditional Shōgi
|
138
|
+
standard_shogi = Sashite::Snn.style(:Shogi, :first)
|
63
139
|
|
64
|
-
|
65
|
-
|
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
|
-
|
143
|
+
# Chu Shōgi
|
144
|
+
chu_shogi = Sashite::Snn.style(:Chushogi, :first)
|
145
|
+
```
|
71
146
|
|
72
|
-
###
|
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
|
-
|
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
|
-
|
77
|
-
|
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
|
-
|
80
|
-
style = Sashite::Snn::Style.parse("CHESS")
|
81
|
-
# => #<Sashite::Snn::Style:0x... @identifier="CHESS">
|
173
|
+
### Main Module Methods
|
82
174
|
|
83
|
-
|
84
|
-
|
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
|
-
|
87
|
-
style = Sashite::Snn::Style.new("CHESS")
|
88
|
-
lowercase_style = Sashite::Snn::Style.new("makruk")
|
179
|
+
### Style Class
|
89
180
|
|
90
|
-
|
91
|
-
|
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
|
-
|
190
|
+
#### Name and Case Handling
|
95
191
|
|
96
|
-
|
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
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
#
|
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
|
-
|
206
|
+
#### Side Queries
|
207
|
+
- `#first_player?` - Check if first player style
|
208
|
+
- `#second_player?` - Check if second player style
|
119
209
|
|
120
|
-
|
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
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
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
|
-
|
128
|
-
|
129
|
-
|
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
|
-
##
|
227
|
+
## Advanced Usage
|
133
228
|
|
134
|
-
|
229
|
+
### Name Normalization Examples
|
135
230
|
|
136
231
|
```ruby
|
137
|
-
#
|
138
|
-
Sashite::Snn
|
139
|
-
Sashite::Snn
|
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
|
-
|
236
|
+
# Names are normalized with proper capitalization
|
237
|
+
white_chess.name # => :Chess
|
238
|
+
black_chess.name # => :Chess (same name!)
|
164
239
|
|
165
|
-
|
240
|
+
# Sides are different
|
241
|
+
white_chess.side # => :first
|
242
|
+
black_chess.side # => :second
|
166
243
|
|
167
|
-
|
168
|
-
#
|
169
|
-
|
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
|
-
#
|
173
|
-
|
174
|
-
|
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
|
-
|
177
|
-
|
178
|
-
|
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
|
-
###
|
270
|
+
### Game Configuration Management
|
271
|
+
```ruby
|
272
|
+
class GameConfiguration
|
273
|
+
def initialize
|
274
|
+
@player_styles = {}
|
275
|
+
end
|
182
276
|
|
183
|
-
|
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
|
-
|
186
|
-
|
187
|
-
|
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
|
-
|
195
|
-
|
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
|
-
|
200
|
-
|
289
|
+
styles = @player_styles.values
|
290
|
+
!styles.all? { |style| style.same_name?(styles.first) }
|
291
|
+
end
|
201
292
|
|
202
|
-
|
203
|
-
|
204
|
-
```
|
293
|
+
def cross_tradition_match?
|
294
|
+
return false if @player_styles.size < 2
|
205
295
|
|
206
|
-
|
296
|
+
style_names = @player_styles.values.map(&:name).uniq
|
297
|
+
style_names.size > 1
|
298
|
+
end
|
299
|
+
end
|
207
300
|
|
208
|
-
|
209
|
-
|
210
|
-
|
211
|
-
|
301
|
+
# Usage
|
302
|
+
config = GameConfiguration.new
|
303
|
+
config.set_player_style(:white, :Chess)
|
304
|
+
config.set_player_style(:black, :Shogi)
|
212
305
|
|
213
|
-
#
|
214
|
-
|
215
|
-
handicap_shogi = Sashite::Snn::Style.parse("SHOGI9")
|
306
|
+
puts config.cross_tradition_match? # => true
|
307
|
+
puts config.style_mismatch? # => true
|
216
308
|
|
217
|
-
|
218
|
-
|
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
|
-
###
|
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
|
-
|
226
|
-
|
227
|
-
|
228
|
-
|
229
|
-
|
230
|
-
|
231
|
-
|
232
|
-
|
233
|
-
|
234
|
-
|
235
|
-
|
236
|
-
|
237
|
-
|
238
|
-
|
239
|
-
|
240
|
-
|
241
|
-
|
242
|
-
|
243
|
-
|
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
|
-
##
|
379
|
+
## Protocol Mapping
|
248
380
|
|
249
|
-
|
381
|
+
Following the [Game Protocol](https://sashite.dev/game-protocol/):
|
250
382
|
|
251
|
-
|
252
|
-
|
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
|
-
|
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
|
-
|
257
|
-
- `Sashite::Snn::Style.new(identifier)` - Create a new style instance
|
390
|
+
**Canonical principle**: Identical styles must have identical SNN representations.
|
258
391
|
|
259
|
-
|
392
|
+
## Properties
|
260
393
|
|
261
|
-
|
262
|
-
|
263
|
-
|
264
|
-
|
265
|
-
|
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
|
-
|
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
|
-
|
403
|
+
### Name Normalization Convention
|
273
404
|
|
274
|
-
|
405
|
+
SNN follows a strict name normalization convention:
|
275
406
|
|
276
|
-
|
277
|
-
|
278
|
-
|
279
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
428
|
+
This ensures that `parse(snn).to_s == snn` for all valid SNN strings while maintaining internal consistency.
|
295
429
|
|
296
|
-
|
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
|
-
|
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
|
-
|
436
|
+
## Related Specifications
|
306
437
|
|
307
|
-
-
|
308
|
-
-
|
309
|
-
-
|
310
|
-
-
|
311
|
-
-
|
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).
|
data/lib/sashite/snn/style.rb
CHANGED
@@ -2,141 +2,261 @@
|
|
2
2
|
|
3
3
|
module Sashite
|
4
4
|
module Snn
|
5
|
-
# Represents a style
|
5
|
+
# Represents a style in SNN (Style Name Notation) format.
|
6
6
|
#
|
7
|
-
# A style
|
8
|
-
#
|
9
|
-
# -
|
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
|
-
#
|
13
|
-
#
|
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
|
-
#
|
22
|
-
|
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
|
27
|
-
# @
|
28
|
-
#
|
29
|
-
|
30
|
-
|
31
|
-
|
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
|
-
@
|
47
|
+
@name = name
|
48
|
+
@side = side
|
36
49
|
|
37
50
|
freeze
|
38
51
|
end
|
39
52
|
|
40
|
-
# Parse
|
53
|
+
# Parse an SNN string into a Style object
|
41
54
|
#
|
42
|
-
# @param snn_string [String]
|
43
|
-
# @return [
|
44
|
-
# @raise [ArgumentError] if the string is invalid
|
45
|
-
#
|
46
|
-
#
|
47
|
-
#
|
48
|
-
# # => #<
|
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
|
-
|
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
|
77
|
+
# Check if a string is a valid SNN notation
|
54
78
|
#
|
55
|
-
# @
|
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.
|
59
|
-
# Sashite::Snn::Style.
|
60
|
-
|
61
|
-
|
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
|
-
#
|
92
|
+
# Convert the style to its SNN string representation
|
65
93
|
#
|
66
|
-
# @return [
|
67
|
-
#
|
68
|
-
#
|
69
|
-
#
|
70
|
-
#
|
71
|
-
def
|
72
|
-
|
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
|
-
#
|
76
|
-
#
|
77
|
-
# @return [Boolean] true if the identifier is uppercase, false otherwise
|
103
|
+
# Create a new style with opposite ownership (side)
|
78
104
|
#
|
79
|
-
# @
|
80
|
-
#
|
81
|
-
#
|
82
|
-
def
|
83
|
-
|
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
|
-
#
|
112
|
+
# Create a new style with a different name (keeping same side)
|
87
113
|
#
|
88
|
-
# @
|
89
|
-
#
|
90
|
-
# @example
|
91
|
-
#
|
92
|
-
|
93
|
-
|
94
|
-
|
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
|
-
#
|
125
|
+
# Create a new style with a different side (keeping same name)
|
98
126
|
#
|
99
|
-
# @
|
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
|
-
# @
|
102
|
-
|
103
|
-
|
104
|
-
identifier
|
140
|
+
# @return [Boolean] true if first player
|
141
|
+
def first_player?
|
142
|
+
side == FIRST_PLAYER
|
105
143
|
end
|
106
144
|
|
107
|
-
#
|
145
|
+
# Check if the style belongs to the second player
|
108
146
|
#
|
109
|
-
# @return [
|
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
|
-
# @
|
112
|
-
#
|
113
|
-
|
114
|
-
|
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
|
-
#
|
164
|
+
# Check if this style belongs to the same side as another
|
118
165
|
#
|
119
|
-
# @
|
120
|
-
|
121
|
-
|
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
|
-
#
|
174
|
+
# Custom equality comparison
|
125
175
|
#
|
126
|
-
# @param other [Object]
|
127
|
-
# @return [Boolean] true if both objects are
|
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?(
|
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
|
184
|
+
# Alias for == to ensure Set functionality works correctly
|
133
185
|
alias eql? ==
|
134
186
|
|
135
|
-
#
|
187
|
+
# Custom hash implementation for use in collections
|
136
188
|
#
|
137
|
-
# @return [Integer]
|
189
|
+
# @return [Integer] hash value based on class, name, and side
|
138
190
|
def hash
|
139
|
-
[self.class,
|
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
|
6
|
+
# SNN (Style Name Notation) implementation for Ruby
|
7
7
|
#
|
8
|
-
#
|
9
|
-
#
|
10
|
-
#
|
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
|
-
#
|
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
|
-
#
|
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]
|
21
|
-
# @return [Boolean] true if
|
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")
|
25
|
-
# Sashite::Snn.valid?("
|
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
|
-
|
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
|
-
#
|
37
|
+
# Parse an SNN string into a Style object
|
37
38
|
#
|
38
|
-
# @param
|
39
|
-
# @return [
|
40
|
-
# @raise [ArgumentError] if the
|
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
|
-
# @
|
43
|
-
#
|
44
|
-
#
|
45
|
-
|
46
|
-
|
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.
|
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
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
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
|
55
|
+
summary: SNN (Style Name Notation) implementation for Ruby with immutable style objects
|
56
56
|
test_files: []
|