sashite-snn 2.0.0 → 3.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +69 -338
- data/lib/sashite/snn/name.rb +92 -0
- data/lib/sashite/snn.rb +32 -38
- metadata +10 -10
- data/lib/sashite/snn/style.rb +0 -250
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 517b0310bd57b5d0b931a5f14eb1c4d6257bbd6d4f215c467ff475c8b42542fc
|
4
|
+
data.tar.gz: 2fa5a177d6350dd2501b87eac5a567f5cf48c681439b3af23f2bae1df99579a3
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 200ef38765b8c581f93c51657699e4b420c26abe1d355227ff47892b493f787ec9c03e7202a91b9466c99c97a95a1b914837e59bb2784ef6568cc2ca3f94dd49
|
7
|
+
data.tar.gz: 6b01e73ad4e61246e8e61a4e0bd85f7b3f0fcb964f7532c42d6c95a2b5b665c64487e032e46cb7b77806b3a410137536812aac3278428b2c31af8d91811597ec
|
data/README.md
CHANGED
@@ -9,16 +9,16 @@
|
|
9
9
|
|
10
10
|
## What is SNN?
|
11
11
|
|
12
|
-
SNN (Style Name Notation)
|
12
|
+
SNN (Style Name Notation) is a formal, rule-agnostic naming system for identifying **styles** in abstract strategy board games such as chess, shōgi, xiangqi, and their many variants. Each style is represented by a canonical, human-readable ASCII name (e.g., `"Chess"`, `"Shogi"`, `"Xiangqi"`, `"Minishogi"`).
|
13
13
|
|
14
|
-
This gem implements the [SNN Specification v1.0.0](https://sashite.dev/specs/snn/1.0.0/),
|
14
|
+
This gem implements the [SNN Specification v1.0.0](https://sashite.dev/specs/snn/1.0.0/), supporting validation, parsing, and comparison of style names.
|
15
15
|
|
16
16
|
## Installation
|
17
17
|
|
18
18
|
```ruby
|
19
19
|
# In your Gemfile
|
20
20
|
gem "sashite-snn"
|
21
|
-
|
21
|
+
````
|
22
22
|
|
23
23
|
Or install manually:
|
24
24
|
|
@@ -33,392 +33,123 @@ gem install sashite-snn
|
|
33
33
|
```ruby
|
34
34
|
require "sashite/snn"
|
35
35
|
|
36
|
-
# Parse SNN strings into style objects
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
style.side # => :first
|
36
|
+
# Parse SNN strings into style name objects
|
37
|
+
name = Sashite::Snn.parse("Shogi") # => #<Snn::Name value="Shogi">
|
38
|
+
name.to_s # => "Shogi"
|
39
|
+
name.value # => "Shogi"
|
41
40
|
|
42
|
-
# Create
|
43
|
-
|
44
|
-
|
41
|
+
# Create from string or symbol
|
42
|
+
name = Sashite::Snn.name("Chess") # => #<Snn::Name value="Chess">
|
43
|
+
name = Sashite::Snn::Name.new(:Xiangqi) # => #<Snn::Name value="Xiangqi">
|
45
44
|
|
46
45
|
# Validate SNN strings
|
47
|
-
Sashite::Snn.valid?("
|
48
|
-
Sashite::Snn.valid?("
|
49
|
-
Sashite::Snn.valid?("
|
50
|
-
Sashite::Snn.valid?("CC") # => false (not single character)
|
46
|
+
Sashite::Snn.valid?("Go9x9") # => true
|
47
|
+
Sashite::Snn.valid?("chess") # => false (must start with uppercase)
|
48
|
+
Sashite::Snn.valid?("3DChess") # => false (invalid character)
|
51
49
|
```
|
52
50
|
|
53
|
-
###
|
51
|
+
### Normalization and Comparison
|
54
52
|
|
55
53
|
```ruby
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
# Flip player assignment
|
60
|
-
flipped = style.flip # => #<Snn::Style letter=:c side=:second>
|
61
|
-
flipped.to_s # => "c"
|
62
|
-
|
63
|
-
# Change letter
|
64
|
-
changed = style.with_letter(:S) # => #<Snn::Style letter=:S side=:first>
|
65
|
-
changed.to_s # => "S"
|
54
|
+
a = Sashite::Snn.parse("Chess960")
|
55
|
+
b = Sashite::Snn.parse("Chess960")
|
66
56
|
|
67
|
-
#
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
# Chain transformations
|
72
|
-
result = style.flip.with_letter(:M) # => #<Snn::Style letter=:m side=:second>
|
73
|
-
result.to_s # => "m"
|
57
|
+
a == b # => true
|
58
|
+
a.same_base_name?(Sashite::Snn.parse("Chess")) # => true if both resolve to same SIN
|
59
|
+
a.to_s # => "Chess960"
|
74
60
|
```
|
75
61
|
|
76
|
-
###
|
62
|
+
### Canonical Representation
|
77
63
|
|
78
64
|
```ruby
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
#
|
83
|
-
style.first_player? # => true
|
84
|
-
style.second_player? # => false
|
85
|
-
opposite.first_player? # => false
|
86
|
-
opposite.second_player? # => true
|
87
|
-
|
88
|
-
# Letter comparison
|
89
|
-
chess1 = Sashite::Snn.parse("C")
|
90
|
-
chess2 = Sashite::Snn.parse("c")
|
91
|
-
shogi = Sashite::Snn.parse("S")
|
92
|
-
|
93
|
-
chess1.same_letter?(chess2) # => true (both use letter C)
|
94
|
-
chess1.same_side?(shogi) # => true (both first player)
|
95
|
-
chess1.same_letter?(shogi) # => false (different letters)
|
65
|
+
# All names are normalized to a canonical format
|
66
|
+
name = Sashite::Snn.parse("Minishogi")
|
67
|
+
name.value # => "Minishogi"
|
68
|
+
name.to_s # => "Minishogi"
|
96
69
|
```
|
97
70
|
|
98
|
-
###
|
71
|
+
### Collections and Filtering
|
99
72
|
|
100
73
|
```ruby
|
101
|
-
|
102
|
-
styles = %w[C c S s M m].map { |snn| Sashite::Snn.parse(snn) }
|
103
|
-
|
104
|
-
# Filter by player
|
105
|
-
first_player_styles = styles.select(&:first_player?)
|
106
|
-
first_player_styles.map(&:to_s) # => ["C", "S", "M"]
|
74
|
+
names = %w[Chess Shogi Makruk Antichess Minishogi].map { |n| Sashite::Snn.parse(n) }
|
107
75
|
|
108
|
-
#
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
# Find specific combinations
|
113
|
-
chess_styles = styles.select { |s| s.letter.to_s.upcase == "C" }
|
114
|
-
chess_styles.map(&:to_s) # => ["C", "c"]
|
76
|
+
# Filter by prefix
|
77
|
+
names.select { |n| n.value.start_with?("Mini") }.map(&:to_s)
|
78
|
+
# => ["Minishogi"]
|
115
79
|
```
|
116
80
|
|
117
81
|
## Format Specification
|
118
82
|
|
119
83
|
### Structure
|
84
|
+
|
120
85
|
```
|
121
|
-
<
|
86
|
+
<uppercase-letter>[<lowercase-letter | digit>]*
|
122
87
|
```
|
123
88
|
|
124
89
|
### Grammar (BNF)
|
90
|
+
|
125
91
|
```bnf
|
126
|
-
<snn> ::= <uppercase-letter>
|
92
|
+
<snn> ::= <uppercase-letter> <tail>
|
93
|
+
|
94
|
+
<tail> ::= "" ; Single letter (e.g., "X")
|
95
|
+
| <alphanumeric-char> <tail> ; Extended name
|
96
|
+
|
97
|
+
<alphanumeric-char> ::= <lowercase-letter> | <digit>
|
127
98
|
|
128
99
|
<uppercase-letter> ::= "A" | "B" | "C" | ... | "Z"
|
129
100
|
<lowercase-letter> ::= "a" | "b" | "c" | ... | "z"
|
101
|
+
<digit> ::= "0" | "1" | "2" | "3" | ... | "9"
|
130
102
|
```
|
131
103
|
|
132
104
|
### Regular Expression
|
133
|
-
```ruby
|
134
|
-
/\A[A-Za-z]\z/
|
135
|
-
```
|
136
|
-
|
137
|
-
### Style Attribute Mapping
|
138
|
-
|
139
|
-
| Style Attribute | SNN Encoding | Examples |
|
140
|
-
|-----------------|--------------|----------|
|
141
|
-
| **Style Family** | Letter choice | `C`/`c` = Chess family |
|
142
|
-
| **Player Assignment** | Letter case | `C` = First player, `c` = Second player |
|
143
|
-
|
144
|
-
## Game Examples
|
145
|
-
|
146
|
-
The SNN specification is rule-agnostic and does not define specific letter assignments. However, here are common usage patterns:
|
147
|
-
|
148
|
-
### Traditional Game Families
|
149
105
|
|
150
106
|
```ruby
|
151
|
-
|
152
|
-
chess_white = Sashite::Snn.parse("C") # First player, Chess family
|
153
|
-
chess_black = Sashite::Snn.parse("c") # Second player, Chess family
|
154
|
-
|
155
|
-
# Shōgi family styles
|
156
|
-
shogi_sente = Sashite::Snn.parse("S") # First player, Shōgi family
|
157
|
-
shogi_gote = Sashite::Snn.parse("s") # Second player, Shōgi family
|
158
|
-
|
159
|
-
# Xiangqi family styles
|
160
|
-
xiangqi_red = Sashite::Snn.parse("X") # First player, Xiangqi family
|
161
|
-
xiangqi_black = Sashite::Snn.parse("x") # Second player, Xiangqi family
|
162
|
-
```
|
163
|
-
|
164
|
-
### Cross-Style Scenarios
|
165
|
-
|
166
|
-
```ruby
|
167
|
-
# Different families in one match
|
168
|
-
def create_hybrid_match
|
169
|
-
[
|
170
|
-
Sashite::Snn.parse("C"), # First player uses Chess family
|
171
|
-
Sashite::Snn.parse("s") # Second player uses Shōgi family
|
172
|
-
]
|
173
|
-
end
|
174
|
-
|
175
|
-
styles = create_hybrid_match
|
176
|
-
styles[0].same_side?(styles[1]) # => false (different players)
|
177
|
-
styles[0].same_letter?(styles[1]) # => false (different families)
|
107
|
+
/\A[A-Z][a-z0-9]*\z/
|
178
108
|
```
|
179
109
|
|
180
|
-
|
110
|
+
## Design Principles
|
181
111
|
|
182
|
-
|
183
|
-
|
184
|
-
|
185
|
-
|
186
|
-
ogi = Sashite::Snn.parse("O") # Ōgi (王棋) family
|
187
|
-
|
188
|
-
# Each family can have both players
|
189
|
-
makruk_black = makruk.flip # Second player Makruk
|
190
|
-
makruk_black.to_s # => "m"
|
191
|
-
```
|
192
|
-
|
193
|
-
## API Reference
|
112
|
+
* **Human-readable**: Names like `"Shogi"` or `"Chess960"` are intuitive and descriptive.
|
113
|
+
* **Canonical**: One valid name per game style within a given context.
|
114
|
+
* **ASCII-only**: Compatible with all systems.
|
115
|
+
* **Scalable**: Supports unlimited distinct names for current and future game variants.
|
194
116
|
|
195
|
-
|
117
|
+
## Integration with SIN
|
196
118
|
|
197
|
-
|
198
|
-
- `Sashite::Snn.parse(snn_string)` - Parse SNN string into Style object
|
199
|
-
- `Sashite::Snn.style(letter, side)` - Create style instance directly
|
119
|
+
SNN names serve as the formal source for SIN character identifiers. For example:
|
200
120
|
|
201
|
-
|
121
|
+
| SNN | SIN |
|
122
|
+
| --------- | ------- |
|
123
|
+
| `Chess` | `C`/`c` |
|
124
|
+
| `Shogi` | `S`/`s` |
|
125
|
+
| `Xiangqi` | `X`/`x` |
|
126
|
+
| `Makruk` | `M`/`m` |
|
202
127
|
|
203
|
-
|
204
|
-
- `Sashite::Snn::Style.new(letter, side)` - Create style instance
|
205
|
-
- `Sashite::Snn::Style.parse(snn_string)` - Parse SNN string
|
128
|
+
Multiple SNN names may map to the same SIN character (e.g., `"Chess"` and `"Chess960"` both → `C`), but SNN provides unambiguous naming within broader contexts.
|
206
129
|
|
207
|
-
|
208
|
-
- `#letter` - Get style letter (symbol :A through :z)
|
209
|
-
- `#side` - Get player side (:first or :second)
|
210
|
-
- `#to_s` - Convert to SNN string representation
|
211
|
-
|
212
|
-
#### Player Queries
|
213
|
-
- `#first_player?` - Check if first player style
|
214
|
-
- `#second_player?` - Check if second player style
|
215
|
-
|
216
|
-
#### Transformations (immutable - return new instances)
|
217
|
-
- `#flip` - Switch player assignment
|
218
|
-
- `#with_letter(new_letter)` - Create style with different letter
|
219
|
-
- `#with_side(new_side)` - Create style with different side
|
220
|
-
|
221
|
-
#### Comparison Methods
|
222
|
-
- `#same_letter?(other)` - Check if same style letter (case-insensitive)
|
223
|
-
- `#same_side?(other)` - Check if same player side
|
224
|
-
- `#==(other)` - Full equality comparison
|
225
|
-
|
226
|
-
### Style Class Constants
|
227
|
-
|
228
|
-
- `Sashite::Snn::Style::FIRST_PLAYER` - Symbol for first player (:first)
|
229
|
-
- `Sashite::Snn::Style::SECOND_PLAYER` - Symbol for second player (:second)
|
230
|
-
- `Sashite::Snn::Style::VALID_SIDES` - Array of valid sides
|
231
|
-
- `Sashite::Snn::Style::SNN_PATTERN` - Regular expression for SNN validation
|
232
|
-
|
233
|
-
## Advanced Usage
|
234
|
-
|
235
|
-
### Letter Case and Side Mapping
|
130
|
+
## Examples
|
236
131
|
|
237
132
|
```ruby
|
238
|
-
#
|
239
|
-
|
240
|
-
|
241
|
-
|
242
|
-
# All uppercase letters are first player
|
243
|
-
upper_case_letters.all?(&:first_player?) # => true
|
244
|
-
|
245
|
-
# All lowercase letters are second player
|
246
|
-
lower_case_letters.all?(&:second_player?) # => true
|
247
|
-
|
248
|
-
# Letter families are related by case
|
249
|
-
letter_a_first = Sashite::Snn.parse("A")
|
250
|
-
letter_a_second = Sashite::Snn.parse("a")
|
251
|
-
|
252
|
-
letter_a_first.same_letter?(letter_a_second) # => true
|
253
|
-
letter_a_first.same_side?(letter_a_second) # => false
|
254
|
-
```
|
255
|
-
|
256
|
-
### Immutable Transformations
|
257
|
-
|
258
|
-
```ruby
|
259
|
-
# All transformations return new instances
|
260
|
-
original = Sashite::Snn.style(:C, :first)
|
261
|
-
flipped = original.flip
|
262
|
-
changed_letter = original.with_letter(:S)
|
263
|
-
|
264
|
-
# Original style is never modified
|
265
|
-
original.to_s # => "C" (unchanged)
|
266
|
-
flipped.to_s # => "c"
|
267
|
-
changed_letter.to_s # => "S"
|
268
|
-
|
269
|
-
# Transformations can be chained
|
270
|
-
result = original.flip.with_letter(:M).flip
|
271
|
-
result.to_s # => "M"
|
272
|
-
```
|
273
|
-
|
274
|
-
### Game Configuration Management
|
275
|
-
|
276
|
-
```ruby
|
277
|
-
class GameConfiguration
|
278
|
-
def initialize
|
279
|
-
@player_styles = {}
|
280
|
-
end
|
281
|
-
|
282
|
-
def set_player_style(player, letter)
|
283
|
-
side = player == :white ? :first : :second
|
284
|
-
@player_styles[player] = Sashite::Snn.style(letter, side)
|
285
|
-
end
|
286
|
-
|
287
|
-
def get_player_style(player)
|
288
|
-
@player_styles[player]
|
289
|
-
end
|
290
|
-
|
291
|
-
def cross_family_match?
|
292
|
-
return false if @player_styles.size < 2
|
293
|
-
|
294
|
-
styles = @player_styles.values
|
295
|
-
!styles.all? { |style| style.same_letter?(styles.first) }
|
296
|
-
end
|
297
|
-
|
298
|
-
def same_family_match?
|
299
|
-
!cross_family_match?
|
300
|
-
end
|
301
|
-
end
|
302
|
-
|
303
|
-
# Usage
|
304
|
-
config = GameConfiguration.new
|
305
|
-
config.set_player_style(:white, :C) # Chess family, first player
|
306
|
-
config.set_player_style(:black, :S) # Shōgi family, second player
|
307
|
-
|
308
|
-
config.cross_family_match? # => true
|
309
|
-
|
310
|
-
white_style = config.get_player_style(:white)
|
311
|
-
white_style.to_s # => "C"
|
133
|
+
Sashite::Snn.parse("Chess") # => #<Snn::Name value="Chess">
|
134
|
+
Sashite::Snn.parse("Chess960") # => #<Snn::Name value="Chess960">
|
135
|
+
Sashite::Snn.valid?("Minishogi") # => true
|
136
|
+
Sashite::Snn.valid?("miniShogi") # => false
|
312
137
|
```
|
313
138
|
|
314
|
-
|
315
|
-
|
316
|
-
```ruby
|
317
|
-
def analyze_styles(snns)
|
318
|
-
styles = snns.map { |snn| Sashite::Snn.parse(snn) }
|
319
|
-
|
320
|
-
{
|
321
|
-
total: styles.size,
|
322
|
-
by_side: styles.group_by(&:side),
|
323
|
-
by_letter: styles.group_by { |s| s.letter.to_s.upcase },
|
324
|
-
unique_letters: styles.map { |s| s.letter.to_s.upcase }.uniq.size,
|
325
|
-
cross_family: styles.map { |s| s.letter.to_s.upcase }.uniq.size > 1
|
326
|
-
}
|
327
|
-
end
|
328
|
-
|
329
|
-
snns = %w[C c S s X x]
|
330
|
-
analysis = analyze_styles(snns)
|
331
|
-
analysis[:by_side][:first].size # => 3
|
332
|
-
analysis[:unique_letters] # => 3
|
333
|
-
analysis[:cross_family] # => true
|
334
|
-
```
|
335
|
-
|
336
|
-
### Tournament Style Registry
|
337
|
-
|
338
|
-
```ruby
|
339
|
-
class TournamentStyleRegistry
|
340
|
-
def initialize
|
341
|
-
@registered_styles = Set.new
|
342
|
-
end
|
343
|
-
|
344
|
-
def register_letter(letter)
|
345
|
-
# Register both sides of a letter family
|
346
|
-
first_player_style = Sashite::Snn.style(letter.to_s.upcase.to_sym, :first)
|
347
|
-
second_player_style = first_player_style.flip
|
348
|
-
|
349
|
-
@registered_styles.add(first_player_style)
|
350
|
-
@registered_styles.add(second_player_style)
|
351
|
-
|
352
|
-
[first_player_style, second_player_style]
|
353
|
-
end
|
354
|
-
|
355
|
-
def valid_pairing?(style1, style2)
|
356
|
-
@registered_styles.include?(style1) &&
|
357
|
-
@registered_styles.include?(style2) &&
|
358
|
-
!style1.same_side?(style2)
|
359
|
-
end
|
360
|
-
|
361
|
-
def available_styles_for_side(side)
|
362
|
-
@registered_styles.select { |style| style.side == side }
|
363
|
-
end
|
364
|
-
|
365
|
-
def supported_families
|
366
|
-
@registered_styles.map { |s| s.letter.to_s.upcase }.uniq.sort
|
367
|
-
end
|
368
|
-
end
|
369
|
-
|
370
|
-
# Usage
|
371
|
-
registry = TournamentStyleRegistry.new
|
372
|
-
registry.register_letter(:C)
|
373
|
-
registry.register_letter(:S)
|
374
|
-
|
375
|
-
chess_white = Sashite::Snn.parse("C")
|
376
|
-
shogi_black = Sashite::Snn.parse("s")
|
377
|
-
|
378
|
-
registry.valid_pairing?(chess_white, shogi_black) # => true
|
379
|
-
registry.supported_families # => ["C", "S"]
|
380
|
-
```
|
381
|
-
|
382
|
-
## Protocol Mapping
|
383
|
-
|
384
|
-
Following the [Sashité Protocol](https://sashite.dev/protocol/):
|
385
|
-
|
386
|
-
| Protocol Attribute | SNN Encoding | Examples | Notes |
|
387
|
-
|-------------------|--------------|----------|-------|
|
388
|
-
| **Style Family** | Letter choice | `C`, `S`, `X` | Rule-agnostic letter assignment |
|
389
|
-
| **Player Assignment** | Case encoding | `C` = First player, `c` = Second player | Case determines side |
|
390
|
-
|
391
|
-
## System Constraints
|
392
|
-
|
393
|
-
- **26 possible identifiers** per player using ASCII letters (A-Z, a-z)
|
394
|
-
- **Exactly 2 players** through case distinction
|
395
|
-
- **Single character** per style-player combination
|
396
|
-
- **Rule-agnostic** - no predefined letter meanings
|
397
|
-
|
398
|
-
## Design Properties
|
399
|
-
|
400
|
-
- **ASCII compatibility**: Maximum portability across systems
|
401
|
-
- **Rule-agnostic**: Independent of specific game mechanics
|
402
|
-
- **Minimal overhead**: Single character per style-player combination
|
403
|
-
- **Canonical representation**: Each style-player combination has exactly one SNN identifier
|
404
|
-
- **Immutable**: All style instances are frozen and transformations return new objects
|
405
|
-
- **Functional**: Pure functions with no side effects
|
139
|
+
## API Reference
|
406
140
|
|
407
|
-
|
141
|
+
### Main Module
|
408
142
|
|
409
|
-
|
410
|
-
|
411
|
-
|
412
|
-
- [PIN](https://sashite.dev/specs/pin/) - Piece Identifier Notation
|
413
|
-
- [PNN](https://sashite.dev/specs/pnn/) - Piece Name Notation (style-aware piece representation)
|
414
|
-
- [QPI](https://sashite.dev/specs/qpi/) - Qualified Piece Identifier
|
143
|
+
* `Sashite::Snn.valid?(str)` – Returns `true` if the string is valid SNN.
|
144
|
+
* `Sashite::Snn.parse(str)` – Returns a `Sashite::Snn::Name` object.
|
145
|
+
* `Sashite::Snn.name(sym_or_str)` – Alias for constructing a name.
|
415
146
|
|
416
|
-
|
147
|
+
### `Sashite::Snn::Name`
|
417
148
|
|
418
|
-
|
419
|
-
|
420
|
-
|
421
|
-
|
149
|
+
* `#value` – Returns the canonical string value.
|
150
|
+
* `#to_s` – Returns the string representation.
|
151
|
+
* `#==`, `#eql?`, `#hash` – Value-based equality.
|
152
|
+
* `#same_base_name?(other)` – Optional helper for SIN mapping equivalence.
|
422
153
|
|
423
154
|
## Development
|
424
155
|
|
@@ -0,0 +1,92 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Sashite
|
4
|
+
module Snn
|
5
|
+
# Represents a style name in SNN (Style Name Notation) format.
|
6
|
+
#
|
7
|
+
# SNN provides a canonical naming system for abstract strategy game styles.
|
8
|
+
# Each name must start with an uppercase ASCII letter, followed by zero or more
|
9
|
+
# lowercase letters or digits.
|
10
|
+
#
|
11
|
+
# All instances are immutable.
|
12
|
+
class Name
|
13
|
+
# SNN validation pattern matching the specification
|
14
|
+
SNN_PATTERN = /\A[A-Z][a-z0-9]*\z/
|
15
|
+
|
16
|
+
# Error messages
|
17
|
+
ERROR_INVALID_NAME = "Invalid SNN string: %s"
|
18
|
+
|
19
|
+
# @return [String] the canonical style name
|
20
|
+
attr_reader :value
|
21
|
+
|
22
|
+
# Create a new style name instance
|
23
|
+
#
|
24
|
+
# @param name [String, Symbol] the style name (e.g., "Shogi", :Chess960)
|
25
|
+
# @raise [ArgumentError] if the name does not match SNN pattern
|
26
|
+
def initialize(name)
|
27
|
+
string_value = name.to_s
|
28
|
+
self.class.validate_format(string_value)
|
29
|
+
|
30
|
+
@value = string_value.freeze
|
31
|
+
freeze
|
32
|
+
end
|
33
|
+
|
34
|
+
# Parse an SNN string into a Name object
|
35
|
+
#
|
36
|
+
# @param string [String] the SNN-formatted style name
|
37
|
+
# @return [Name] a new Name instance
|
38
|
+
# @raise [ArgumentError] if the string is invalid
|
39
|
+
#
|
40
|
+
# @example
|
41
|
+
# Sashite::Snn::Name.parse("Shogi") # => #<Snn::Name value="Shogi">
|
42
|
+
def self.parse(string)
|
43
|
+
new(string)
|
44
|
+
end
|
45
|
+
|
46
|
+
# Check whether the given string is a valid SNN name
|
47
|
+
#
|
48
|
+
# @param string [String] input string to validate
|
49
|
+
# @return [Boolean] true if valid, false otherwise
|
50
|
+
#
|
51
|
+
# @example
|
52
|
+
# Sashite::Snn::Name.valid?("Chess") # => true
|
53
|
+
# Sashite::Snn::Name.valid?("chess") # => false
|
54
|
+
def self.valid?(string)
|
55
|
+
string.is_a?(::String) && string.match?(SNN_PATTERN)
|
56
|
+
end
|
57
|
+
|
58
|
+
# Returns the string representation of the name
|
59
|
+
#
|
60
|
+
# @return [String]
|
61
|
+
def to_s
|
62
|
+
value
|
63
|
+
end
|
64
|
+
|
65
|
+
# Equality based on normalized string value
|
66
|
+
#
|
67
|
+
# @param other [Object]
|
68
|
+
# @return [Boolean]
|
69
|
+
def ==(other)
|
70
|
+
other.is_a?(self.class) && value == other.value
|
71
|
+
end
|
72
|
+
|
73
|
+
# Required for correct Set/hash behavior
|
74
|
+
alias eql? ==
|
75
|
+
|
76
|
+
# Hash based on class and value
|
77
|
+
#
|
78
|
+
# @return [Integer]
|
79
|
+
def hash
|
80
|
+
[self.class, value].hash
|
81
|
+
end
|
82
|
+
|
83
|
+
# Validate that the string is in proper SNN format
|
84
|
+
#
|
85
|
+
# @param str [String]
|
86
|
+
# @raise [ArgumentError] if invalid
|
87
|
+
def self.validate_format(str)
|
88
|
+
raise ::ArgumentError, format(ERROR_INVALID_NAME, str.inspect) unless str.match?(SNN_PATTERN)
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
data/lib/sashite/snn.rb
CHANGED
@@ -1,65 +1,59 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require_relative "snn/
|
3
|
+
require_relative "snn/name"
|
4
4
|
|
5
5
|
module Sashite
|
6
6
|
# SNN (Style Name Notation) implementation for Ruby
|
7
7
|
#
|
8
|
-
# Provides a
|
9
|
-
# SNN uses
|
10
|
-
#
|
8
|
+
# Provides a formal naming system for identifying styles in abstract strategy board games.
|
9
|
+
# SNN uses canonical, human-readable ASCII names beginning with an uppercase letter.
|
10
|
+
# It supports unlimited unique style identifiers with consistent, rule-agnostic semantics.
|
11
11
|
#
|
12
|
-
# Format: <
|
13
|
-
# - Uppercase letter: First player styles (A, B, C, ..., Z)
|
14
|
-
# - Lowercase letter: Second player styles (a, b, c, ..., z)
|
15
|
-
# - Single character only: Each SNN identifier is exactly one ASCII letter
|
12
|
+
# Format: <uppercase-letter>[<lowercase-letter | digit>]*
|
16
13
|
#
|
17
14
|
# Examples:
|
18
|
-
# "
|
19
|
-
# "
|
20
|
-
# "
|
21
|
-
# "
|
15
|
+
# "Chess" - Standard Western chess
|
16
|
+
# "Shogi" - Japanese chess
|
17
|
+
# "Minishogi" - 5×5 compact shōgi variant
|
18
|
+
# "Chess960" - Fischer random chess
|
22
19
|
#
|
23
20
|
# See: https://sashite.dev/specs/snn/1.0.0/
|
24
21
|
module Snn
|
25
|
-
# Check if a string is
|
22
|
+
# Check if a string is valid SNN notation
|
26
23
|
#
|
27
24
|
# @param snn_string [String] the string to validate
|
28
25
|
# @return [Boolean] true if valid SNN, false otherwise
|
29
26
|
#
|
30
|
-
# @example Validate
|
31
|
-
# Sashite::Snn.valid?("
|
32
|
-
# Sashite::Snn.valid?("
|
33
|
-
# Sashite::Snn.valid?("
|
34
|
-
# Sashite::Snn.valid?("1") # => false (not a letter)
|
27
|
+
# @example Validate SNN strings
|
28
|
+
# Sashite::Snn.valid?("Chess") # => true
|
29
|
+
# Sashite::Snn.valid?("minishogi") # => false
|
30
|
+
# Sashite::Snn.valid?("Go9x9") # => true
|
35
31
|
def self.valid?(snn_string)
|
36
|
-
|
32
|
+
Name.valid?(snn_string)
|
37
33
|
end
|
38
34
|
|
39
|
-
# Parse an SNN string into a
|
35
|
+
# Parse an SNN string into a Name object
|
40
36
|
#
|
41
|
-
# @param snn_string [String]
|
42
|
-
# @return [Snn::
|
43
|
-
# @raise [ArgumentError] if the
|
44
|
-
#
|
45
|
-
#
|
46
|
-
# Sashite::Snn.parse("
|
47
|
-
# Sashite::Snn.parse("S") # => #<Snn::Style letter=:S side=:first>
|
37
|
+
# @param snn_string [String] the name string
|
38
|
+
# @return [Snn::Name] a parsed name object
|
39
|
+
# @raise [ArgumentError] if the name is invalid
|
40
|
+
#
|
41
|
+
# @example Parse valid SNN names
|
42
|
+
# Sashite::Snn.parse("Shogi") # => #<Snn::Name value="Shogi">
|
48
43
|
def self.parse(snn_string)
|
49
|
-
|
44
|
+
Name.parse(snn_string)
|
50
45
|
end
|
51
46
|
|
52
|
-
# Create a new
|
47
|
+
# Create a new Name instance directly
|
48
|
+
#
|
49
|
+
# @param value [String, Symbol] style name to construct
|
50
|
+
# @return [Snn::Name] new name instance
|
51
|
+
# @raise [ArgumentError] if name format is invalid
|
53
52
|
#
|
54
|
-
# @
|
55
|
-
#
|
56
|
-
|
57
|
-
|
58
|
-
# @example Create styles directly
|
59
|
-
# Sashite::Snn.style(:C, :first) # => #<Snn::Style letter=:C side=:first>
|
60
|
-
# Sashite::Snn.style(:s, :second) # => #<Snn::Style letter=:s side=:second>
|
61
|
-
def self.style(letter, side)
|
62
|
-
Style.new(letter, side)
|
53
|
+
# @example
|
54
|
+
# Sashite::Snn.name("Xiangqi") # => #<Snn::Name value="Xiangqi">
|
55
|
+
def self.name(value)
|
56
|
+
Name.new(value)
|
63
57
|
end
|
64
58
|
end
|
65
59
|
end
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: sashite-snn
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version:
|
4
|
+
version: 3.0.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Cyril Kato
|
@@ -10,13 +10,12 @@ cert_chain: []
|
|
10
10
|
date: 1980-01-02 00:00:00.000000000 Z
|
11
11
|
dependencies: []
|
12
12
|
description: |
|
13
|
-
SNN (Style Name Notation) provides a rule-agnostic
|
14
|
-
|
15
|
-
a modern Ruby interface featuring immutable style objects and functional programming
|
16
|
-
principles. SNN uses
|
17
|
-
|
18
|
-
|
19
|
-
gaming systems requiring compact style identification.
|
13
|
+
SNN (Style Name Notation) provides a rule-agnostic, scalable naming system for identifying
|
14
|
+
abstract strategy board game styles. This gem implements the SNN Specification v1.0.0 with
|
15
|
+
a modern Ruby interface featuring immutable style name objects and functional programming
|
16
|
+
principles. SNN uses canonical ASCII names (e.g., "Shogi", "Go9x9") to unambiguously refer
|
17
|
+
to game styles across variants and traditions. Ideal for engines, protocols, and tools that
|
18
|
+
need clear and extensible style identifiers.
|
20
19
|
email: contact@cyril.email
|
21
20
|
executables: []
|
22
21
|
extensions: []
|
@@ -26,7 +25,7 @@ files:
|
|
26
25
|
- README.md
|
27
26
|
- lib/sashite-snn.rb
|
28
27
|
- lib/sashite/snn.rb
|
29
|
-
- lib/sashite/snn/
|
28
|
+
- lib/sashite/snn/name.rb
|
30
29
|
homepage: https://github.com/sashite/snn.rb
|
31
30
|
licenses:
|
32
31
|
- MIT
|
@@ -53,5 +52,6 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
53
52
|
requirements: []
|
54
53
|
rubygems_version: 3.6.9
|
55
54
|
specification_version: 4
|
56
|
-
summary: SNN (Style Name Notation) implementation for Ruby with immutable style
|
55
|
+
summary: SNN (Style Name Notation) implementation for Ruby with immutable style name
|
56
|
+
objects
|
57
57
|
test_files: []
|
data/lib/sashite/snn/style.rb
DELETED
@@ -1,250 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
module Sashite
|
4
|
-
module Snn
|
5
|
-
# Represents a style in SNN (Style Name Notation) format.
|
6
|
-
#
|
7
|
-
# A style consists of a single ASCII letter with case-based side encoding:
|
8
|
-
# - Uppercase letter: first player (A, B, C, ..., Z)
|
9
|
-
# - Lowercase letter: second player (a, b, c, ..., z)
|
10
|
-
#
|
11
|
-
# All instances are immutable - transformation methods return new instances.
|
12
|
-
# This follows the SNN Specification v1.0.0 with Letter and Side attributes.
|
13
|
-
class Style
|
14
|
-
# SNN validation pattern matching the specification
|
15
|
-
SNN_PATTERN = /\A[A-Za-z]\z/
|
16
|
-
|
17
|
-
# Player side constants
|
18
|
-
FIRST_PLAYER = :first
|
19
|
-
SECOND_PLAYER = :second
|
20
|
-
|
21
|
-
# Valid sides
|
22
|
-
VALID_SIDES = [FIRST_PLAYER, SECOND_PLAYER].freeze
|
23
|
-
|
24
|
-
# Error messages
|
25
|
-
ERROR_INVALID_SNN = "Invalid SNN string: %s"
|
26
|
-
ERROR_INVALID_LETTER = "Letter must be a single ASCII letter symbol (A-Z, a-z), got: %s"
|
27
|
-
ERROR_INVALID_SIDE = "Side must be :first or :second, got: %s"
|
28
|
-
|
29
|
-
# @return [Symbol] the style letter (single ASCII letter as symbol)
|
30
|
-
attr_reader :letter
|
31
|
-
|
32
|
-
# @return [Symbol] the player side (:first or :second)
|
33
|
-
attr_reader :side
|
34
|
-
|
35
|
-
# Create a new style instance
|
36
|
-
#
|
37
|
-
# @param letter [Symbol] style letter (single ASCII letter as symbol)
|
38
|
-
# @param side [Symbol] player side (:first or :second)
|
39
|
-
# @raise [ArgumentError] if parameters are invalid
|
40
|
-
def initialize(letter, side)
|
41
|
-
self.class.validate_letter(letter)
|
42
|
-
self.class.validate_side(side)
|
43
|
-
|
44
|
-
@letter = letter
|
45
|
-
@side = side
|
46
|
-
|
47
|
-
freeze
|
48
|
-
end
|
49
|
-
|
50
|
-
# Parse an SNN string into a Style object
|
51
|
-
#
|
52
|
-
# @param snn_string [String] SNN notation string (single ASCII letter)
|
53
|
-
# @return [Style] parsed style object with letter and inferred side
|
54
|
-
# @raise [ArgumentError] if the SNN string is invalid
|
55
|
-
# @example Parse SNN strings with case-based side inference
|
56
|
-
# Sashite::Snn::Style.parse("C") # => #<Snn::Style letter=:C side=:first>
|
57
|
-
# Sashite::Snn::Style.parse("c") # => #<Snn::Style letter=:c side=:second>
|
58
|
-
# Sashite::Snn::Style.parse("S") # => #<Snn::Style letter=:S side=:first>
|
59
|
-
def self.parse(snn_string)
|
60
|
-
string_value = String(snn_string)
|
61
|
-
validate_snn_string(string_value)
|
62
|
-
|
63
|
-
# Determine side from case
|
64
|
-
style_side = string_value == string_value.upcase ? FIRST_PLAYER : SECOND_PLAYER
|
65
|
-
|
66
|
-
# Use the letter directly as symbol
|
67
|
-
style_letter = string_value.to_sym
|
68
|
-
|
69
|
-
new(style_letter, style_side)
|
70
|
-
end
|
71
|
-
|
72
|
-
# Check if a string is a valid SNN notation
|
73
|
-
#
|
74
|
-
# @param snn_string [String] the string to validate
|
75
|
-
# @return [Boolean] true if valid SNN, false otherwise
|
76
|
-
#
|
77
|
-
# @example Validate SNN strings
|
78
|
-
# Sashite::Snn::Style.valid?("C") # => true
|
79
|
-
# Sashite::Snn::Style.valid?("c") # => true
|
80
|
-
# Sashite::Snn::Style.valid?("CHESS") # => false (multi-character)
|
81
|
-
def self.valid?(snn_string)
|
82
|
-
return false unless snn_string.is_a?(::String)
|
83
|
-
|
84
|
-
snn_string.match?(SNN_PATTERN)
|
85
|
-
end
|
86
|
-
|
87
|
-
# Convert the style to its SNN string representation
|
88
|
-
#
|
89
|
-
# @return [String] SNN notation string (single ASCII letter)
|
90
|
-
# @example Display styles
|
91
|
-
# style.to_s # => "C" (first player, C family)
|
92
|
-
# style.to_s # => "c" (second player, C family)
|
93
|
-
# style.to_s # => "S" (first player, S family)
|
94
|
-
def to_s
|
95
|
-
letter.to_s
|
96
|
-
end
|
97
|
-
|
98
|
-
# Create a new style with opposite ownership (side)
|
99
|
-
#
|
100
|
-
# @return [Style] new immutable style instance with flipped side
|
101
|
-
# @example Flip player sides
|
102
|
-
# style.flip # (:C, :first) => (:c, :second)
|
103
|
-
def flip
|
104
|
-
new_letter = first_player? ? letter.to_s.downcase.to_sym : letter.to_s.upcase.to_sym
|
105
|
-
self.class.new(new_letter, opposite_side)
|
106
|
-
end
|
107
|
-
|
108
|
-
# Create a new style with a different letter (keeping same side)
|
109
|
-
#
|
110
|
-
# @param new_letter [Symbol] new letter (single ASCII letter as symbol)
|
111
|
-
# @return [Style] new immutable style instance with different letter
|
112
|
-
# @example Change style letter
|
113
|
-
# style.with_letter(:S) # (:C, :first) => (:S, :first)
|
114
|
-
def with_letter(new_letter)
|
115
|
-
self.class.validate_letter(new_letter)
|
116
|
-
return self if letter == new_letter
|
117
|
-
|
118
|
-
# Ensure the new letter has the correct case for the current side
|
119
|
-
adjusted_letter = first_player? ? new_letter.to_s.upcase.to_sym : new_letter.to_s.downcase.to_sym
|
120
|
-
self.class.new(adjusted_letter, side)
|
121
|
-
end
|
122
|
-
|
123
|
-
# Create a new style with a different side (keeping same letter family)
|
124
|
-
#
|
125
|
-
# @param new_side [Symbol] :first or :second
|
126
|
-
# @return [Style] new immutable style instance with different side
|
127
|
-
# @example Change player side
|
128
|
-
# style.with_side(:second) # (:C, :first) => (:c, :second)
|
129
|
-
def with_side(new_side)
|
130
|
-
self.class.validate_side(new_side)
|
131
|
-
return self if side == new_side
|
132
|
-
|
133
|
-
# Adjust letter case for the new side
|
134
|
-
new_letter = new_side == FIRST_PLAYER ? letter.to_s.upcase.to_sym : letter.to_s.downcase.to_sym
|
135
|
-
self.class.new(new_letter, new_side)
|
136
|
-
end
|
137
|
-
|
138
|
-
# Check if the style belongs to the first player
|
139
|
-
#
|
140
|
-
# @return [Boolean] true if first player
|
141
|
-
def first_player?
|
142
|
-
side == FIRST_PLAYER
|
143
|
-
end
|
144
|
-
|
145
|
-
# Check if the style belongs to the second player
|
146
|
-
#
|
147
|
-
# @return [Boolean] true if second player
|
148
|
-
def second_player?
|
149
|
-
side == SECOND_PLAYER
|
150
|
-
end
|
151
|
-
|
152
|
-
# Check if this style has the same letter family as another
|
153
|
-
#
|
154
|
-
# @param other [Style] style to compare with
|
155
|
-
# @return [Boolean] true if both styles use the same letter family (case-insensitive)
|
156
|
-
# @example Compare style letter families
|
157
|
-
# c_style.same_letter?(C_style) # (:c, :second) and (:C, :first) => true
|
158
|
-
def same_letter?(other)
|
159
|
-
return false unless other.is_a?(self.class)
|
160
|
-
|
161
|
-
letter.to_s.upcase == other.letter.to_s.upcase
|
162
|
-
end
|
163
|
-
|
164
|
-
# Check if this style belongs to the same side as another
|
165
|
-
#
|
166
|
-
# @param other [Style] style to compare with
|
167
|
-
# @return [Boolean] true if both styles belong to the same side
|
168
|
-
def same_side?(other)
|
169
|
-
return false unless other.is_a?(self.class)
|
170
|
-
|
171
|
-
side == other.side
|
172
|
-
end
|
173
|
-
|
174
|
-
# Custom equality comparison
|
175
|
-
#
|
176
|
-
# @param other [Object] object to compare with
|
177
|
-
# @return [Boolean] true if both objects are styles with identical letter and side
|
178
|
-
def ==(other)
|
179
|
-
return false unless other.is_a?(self.class)
|
180
|
-
|
181
|
-
letter == other.letter && side == other.side
|
182
|
-
end
|
183
|
-
|
184
|
-
# Alias for == to ensure Set functionality works correctly
|
185
|
-
alias eql? ==
|
186
|
-
|
187
|
-
# Custom hash implementation for use in collections
|
188
|
-
#
|
189
|
-
# @return [Integer] hash value based on class, letter, and side
|
190
|
-
def hash
|
191
|
-
[self.class, letter, side].hash
|
192
|
-
end
|
193
|
-
|
194
|
-
# Validate that the letter is a valid single ASCII letter symbol
|
195
|
-
#
|
196
|
-
# @param letter [Symbol] the letter to validate
|
197
|
-
# @raise [ArgumentError] if invalid
|
198
|
-
def self.validate_letter(letter)
|
199
|
-
return if valid_letter?(letter)
|
200
|
-
|
201
|
-
raise ::ArgumentError, format(ERROR_INVALID_LETTER, letter.inspect)
|
202
|
-
end
|
203
|
-
|
204
|
-
# Validate that the side is a valid symbol
|
205
|
-
#
|
206
|
-
# @param side [Symbol] the side to validate
|
207
|
-
# @raise [ArgumentError] if invalid
|
208
|
-
def self.validate_side(side)
|
209
|
-
return if VALID_SIDES.include?(side)
|
210
|
-
|
211
|
-
raise ::ArgumentError, format(ERROR_INVALID_SIDE, side.inspect)
|
212
|
-
end
|
213
|
-
|
214
|
-
# Check if a letter is valid (single ASCII letter symbol)
|
215
|
-
#
|
216
|
-
# @param letter [Object] the letter to check
|
217
|
-
# @return [Boolean] true if valid
|
218
|
-
def self.valid_letter?(letter)
|
219
|
-
return false unless letter.is_a?(::Symbol)
|
220
|
-
|
221
|
-
letter_string = letter.to_s
|
222
|
-
return false if letter_string.empty?
|
223
|
-
|
224
|
-
# Must be exactly one ASCII letter
|
225
|
-
letter_string.match?(SNN_PATTERN)
|
226
|
-
end
|
227
|
-
|
228
|
-
# Validate SNN string format
|
229
|
-
#
|
230
|
-
# @param string [String] string to validate
|
231
|
-
# @raise [ArgumentError] if string doesn't match SNN pattern
|
232
|
-
def self.validate_snn_string(string)
|
233
|
-
return if string.match?(SNN_PATTERN)
|
234
|
-
|
235
|
-
raise ::ArgumentError, format(ERROR_INVALID_SNN, string)
|
236
|
-
end
|
237
|
-
|
238
|
-
private_class_method :valid_letter?, :validate_snn_string
|
239
|
-
|
240
|
-
private
|
241
|
-
|
242
|
-
# Get the opposite side
|
243
|
-
#
|
244
|
-
# @return [Symbol] the opposite side
|
245
|
-
def opposite_side
|
246
|
-
first_player? ? SECOND_PLAYER : FIRST_PLAYER
|
247
|
-
end
|
248
|
-
end
|
249
|
-
end
|
250
|
-
end
|