sashite-snn 2.0.0 → 3.1.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 +222 -297
- data/lib/sashite/snn/name.rb +170 -0
- data/lib/sashite/snn.rb +66 -38
- metadata +17 -11
- 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: 1528b909fb1159a1b9d3fb06e4b62114efa110d991226945024f442fb08501e9
|
|
4
|
+
data.tar.gz: 3c6c44c3d69d1eacad6141d9309a27e5972dde45067bc8272e9584ed6cfc56ee
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: fc20688ee514ec76125132aba5b121aa4b705230eb716f2abee6c5c1a88978aadfa52bf8cfa1c870315284050808f5fe5903a5e141c760af92f7aaca9bac82d5
|
|
7
|
+
data.tar.gz: b44e7af48ae53d52ca2b1961831b5eca677c2b8182589062139af966d3e960f0638720fba33c7e9099b6a8fe9424124f49386e3c9b025e71f93edfb797e9e558
|
data/README.md
CHANGED
|
@@ -9,9 +9,13 @@
|
|
|
9
9
|
|
|
10
10
|
## What is SNN?
|
|
11
11
|
|
|
12
|
-
SNN (Style Name Notation)
|
|
12
|
+
SNN (Style Name Notation) is a **foundational**, human-readable naming system for identifying game styles in abstract strategy board games. SNN serves as a primitive building block using descriptive alphabetic names with case encoding to represent style identity and player assignment.
|
|
13
13
|
|
|
14
|
-
|
|
14
|
+
Each SNN name is a case-consistent alphabetic identifier where:
|
|
15
|
+
- **Uppercase names** (e.g., `CHESS`, `SHOGI`) represent the **first player's** style
|
|
16
|
+
- **Lowercase names** (e.g., `chess`, `shogi`) represent the **second player's** style
|
|
17
|
+
|
|
18
|
+
This gem implements the [SNN Specification v1.0.0](https://sashite.dev/specs/snn/1.0.0/) as a foundational primitive with no dependencies.
|
|
15
19
|
|
|
16
20
|
## Installation
|
|
17
21
|
|
|
@@ -26,399 +30,320 @@ Or install manually:
|
|
|
26
30
|
gem install sashite-snn
|
|
27
31
|
```
|
|
28
32
|
|
|
29
|
-
##
|
|
30
|
-
|
|
31
|
-
### Basic Operations
|
|
33
|
+
## Quick Start
|
|
32
34
|
|
|
33
35
|
```ruby
|
|
34
36
|
require "sashite/snn"
|
|
35
37
|
|
|
36
|
-
# Parse SNN strings into style objects
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
style.side # => :first
|
|
38
|
+
# Parse SNN strings into style name objects
|
|
39
|
+
name = Sashite::Snn.parse("SHOGI") # => #<Snn::Name value="SHOGI">
|
|
40
|
+
name.to_s # => "SHOGI"
|
|
41
|
+
name.value # => "SHOGI"
|
|
41
42
|
|
|
42
|
-
# Create
|
|
43
|
-
|
|
44
|
-
|
|
43
|
+
# Create from string or symbol
|
|
44
|
+
name = Sashite::Snn.name("CHESS") # => #<Snn::Name value="CHESS">
|
|
45
|
+
name = Sashite::Snn::Name.new(:xiangqi) # => #<Snn::Name value="xiangqi">
|
|
45
46
|
|
|
46
47
|
# Validate SNN strings
|
|
47
|
-
Sashite::Snn.valid?("
|
|
48
|
-
Sashite::Snn.valid?("
|
|
49
|
-
Sashite::Snn.valid?("
|
|
50
|
-
Sashite::Snn.valid?("
|
|
48
|
+
Sashite::Snn.valid?("MAKRUK") # => true
|
|
49
|
+
Sashite::Snn.valid?("shogi") # => true
|
|
50
|
+
Sashite::Snn.valid?("Chess") # => false (mixed case)
|
|
51
|
+
Sashite::Snn.valid?("Chess960") # => false (contains digits)
|
|
51
52
|
```
|
|
52
53
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
```ruby
|
|
56
|
-
# All transformations return new immutable instances
|
|
57
|
-
style = Sashite::Snn.parse("C")
|
|
54
|
+
## SNN Format
|
|
58
55
|
|
|
59
|
-
|
|
60
|
-
flipped = style.flip # => #<Snn::Style letter=:c side=:second>
|
|
61
|
-
flipped.to_s # => "c"
|
|
56
|
+
An SNN string consists of alphabetic characters only, all in the same case:
|
|
62
57
|
|
|
63
|
-
# Change letter
|
|
64
|
-
changed = style.with_letter(:S) # => #<Snn::Style letter=:S side=:first>
|
|
65
|
-
changed.to_s # => "S"
|
|
66
|
-
|
|
67
|
-
# Change side
|
|
68
|
-
other_side = style.with_side(:second) # => #<Snn::Style letter=:c side=:second>
|
|
69
|
-
other_side.to_s # => "c"
|
|
70
|
-
|
|
71
|
-
# Chain transformations
|
|
72
|
-
result = style.flip.with_letter(:M) # => #<Snn::Style letter=:m side=:second>
|
|
73
|
-
result.to_s # => "m"
|
|
74
58
|
```
|
|
75
|
-
|
|
76
|
-
### Player and Style Queries
|
|
77
|
-
|
|
78
|
-
```ruby
|
|
79
|
-
style = Sashite::Snn.parse("C")
|
|
80
|
-
opposite = Sashite::Snn.parse("s")
|
|
81
|
-
|
|
82
|
-
# Player identification
|
|
83
|
-
style.first_player? # => true
|
|
84
|
-
style.second_player? # => false
|
|
85
|
-
opposite.first_player? # => false
|
|
86
|
-
opposite.second_player? # => true
|
|
87
|
-
|
|
88
|
-
# Letter comparison
|
|
89
|
-
chess1 = Sashite::Snn.parse("C")
|
|
90
|
-
chess2 = Sashite::Snn.parse("c")
|
|
91
|
-
shogi = Sashite::Snn.parse("S")
|
|
92
|
-
|
|
93
|
-
chess1.same_letter?(chess2) # => true (both use letter C)
|
|
94
|
-
chess1.same_side?(shogi) # => true (both first player)
|
|
95
|
-
chess1.same_letter?(shogi) # => false (different letters)
|
|
59
|
+
<alphabetic-name>
|
|
96
60
|
```
|
|
97
61
|
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
# Filter by player
|
|
105
|
-
first_player_styles = styles.select(&:first_player?)
|
|
106
|
-
first_player_styles.map(&:to_s) # => ["C", "S", "M"]
|
|
107
|
-
|
|
108
|
-
# Group by letter family
|
|
109
|
-
by_letter = styles.group_by { |s| s.letter.to_s.upcase }
|
|
110
|
-
by_letter["C"].size # => 2 (both C and c)
|
|
111
|
-
|
|
112
|
-
# Find specific combinations
|
|
113
|
-
chess_styles = styles.select { |s| s.letter.to_s.upcase == "C" }
|
|
114
|
-
chess_styles.map(&:to_s) # => ["C", "c"]
|
|
115
|
-
```
|
|
62
|
+
**Examples:**
|
|
63
|
+
- `CHESS` — Chess style for first player
|
|
64
|
+
- `chess` — Chess style for second player
|
|
65
|
+
- `SHOGI` — Shōgi style for first player
|
|
66
|
+
- `shogi` — Shōgi style for second player
|
|
116
67
|
|
|
117
68
|
## Format Specification
|
|
118
69
|
|
|
119
70
|
### Structure
|
|
71
|
+
|
|
120
72
|
```
|
|
121
|
-
<
|
|
73
|
+
<alphabetic-name>
|
|
122
74
|
```
|
|
123
75
|
|
|
76
|
+
Where the name directly represents the style identity and player assignment through case.
|
|
77
|
+
|
|
124
78
|
### Grammar (BNF)
|
|
79
|
+
|
|
125
80
|
```bnf
|
|
126
|
-
<snn> ::= <uppercase-
|
|
81
|
+
<snn> ::= <uppercase-name> | <lowercase-name>
|
|
82
|
+
|
|
83
|
+
<uppercase-name> ::= <uppercase-letter>+
|
|
84
|
+
<lowercase-name> ::= <lowercase-letter>+
|
|
127
85
|
|
|
128
86
|
<uppercase-letter> ::= "A" | "B" | "C" | ... | "Z"
|
|
129
87
|
<lowercase-letter> ::= "a" | "b" | "c" | ... | "z"
|
|
130
88
|
```
|
|
131
89
|
|
|
132
90
|
### Regular Expression
|
|
91
|
+
|
|
133
92
|
```ruby
|
|
134
|
-
/\A[A-
|
|
93
|
+
/\A([A-Z]+|[a-z]+)\z/
|
|
135
94
|
```
|
|
136
95
|
|
|
137
|
-
###
|
|
96
|
+
### Constraints
|
|
138
97
|
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
| **Player Assignment** | Letter case | `C` = First player, `c` = Second player |
|
|
98
|
+
1. **Case consistency**: All letters must be either uppercase OR lowercase
|
|
99
|
+
2. **Alphabetic only**: Only ASCII letters allowed (no digits, no special characters)
|
|
100
|
+
3. **Direct assignment**: Names represent styles through explicit association
|
|
143
101
|
|
|
144
|
-
##
|
|
102
|
+
## API Reference
|
|
145
103
|
|
|
146
|
-
|
|
104
|
+
### Module Methods
|
|
147
105
|
|
|
148
|
-
|
|
106
|
+
#### `Sashite::Snn.valid?(string)`
|
|
149
107
|
|
|
150
|
-
|
|
151
|
-
# Chess family styles
|
|
152
|
-
chess_white = Sashite::Snn.parse("C") # First player, Chess family
|
|
153
|
-
chess_black = Sashite::Snn.parse("c") # Second player, Chess family
|
|
108
|
+
Returns `true` if the string is valid SNN notation.
|
|
154
109
|
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
shogi_gote = Sashite::Snn.parse("s") # Second player, Shōgi family
|
|
110
|
+
- **Parameter**: `string` (String) - String to validate
|
|
111
|
+
- **Returns**: `Boolean` - `true` if valid, `false` otherwise
|
|
158
112
|
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
113
|
+
```ruby
|
|
114
|
+
Sashite::Snn.valid?("CHESS") # => true
|
|
115
|
+
Sashite::Snn.valid?("shogi") # => true
|
|
116
|
+
Sashite::Snn.valid?("Chess") # => false (mixed case)
|
|
117
|
+
Sashite::Snn.valid?("CHESS960") # => false (contains digits)
|
|
118
|
+
Sashite::Snn.valid?("3DChess") # => false (starts with digit)
|
|
162
119
|
```
|
|
163
120
|
|
|
164
|
-
|
|
121
|
+
#### `Sashite::Snn.parse(string)`
|
|
165
122
|
|
|
166
|
-
|
|
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
|
|
123
|
+
Parses an SNN string into a `Name` object.
|
|
174
124
|
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
```
|
|
179
|
-
|
|
180
|
-
### Variant Families
|
|
125
|
+
- **Parameter**: `string` (String) - SNN notation string
|
|
126
|
+
- **Returns**: `Name` - Immutable name object
|
|
127
|
+
- **Raises**: `ArgumentError` if the string is invalid
|
|
181
128
|
|
|
182
129
|
```ruby
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
janggi = Sashite::Snn.parse("J") # Janggi (Korean Chess) family
|
|
186
|
-
ogi = Sashite::Snn.parse("O") # Ōgi (王棋) family
|
|
187
|
-
|
|
188
|
-
# Each family can have both players
|
|
189
|
-
makruk_black = makruk.flip # Second player Makruk
|
|
190
|
-
makruk_black.to_s # => "m"
|
|
130
|
+
name = Sashite::Snn.parse("SHOGI") # => #<Snn::Name value="SHOGI">
|
|
131
|
+
name = Sashite::Snn.parse("chess") # => #<Snn::Name value="chess">
|
|
191
132
|
```
|
|
192
133
|
|
|
193
|
-
|
|
134
|
+
#### `Sashite::Snn.name(value)`
|
|
194
135
|
|
|
195
|
-
|
|
136
|
+
Creates a new `Name` instance directly.
|
|
196
137
|
|
|
197
|
-
- `
|
|
198
|
-
- `
|
|
199
|
-
-
|
|
138
|
+
- **Parameter**: `value` (String, Symbol) - Style name to construct
|
|
139
|
+
- **Returns**: `Name` - New name instance
|
|
140
|
+
- **Raises**: `ArgumentError` if name format is invalid
|
|
200
141
|
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
- `Sashite::Snn::Style.parse(snn_string)` - Parse SNN string
|
|
142
|
+
```ruby
|
|
143
|
+
Sashite::Snn.name("XIANGQI") # => #<Snn::Name value="XIANGQI">
|
|
144
|
+
Sashite::Snn.name(:makruk) # => #<Snn::Name value="makruk">
|
|
145
|
+
```
|
|
206
146
|
|
|
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
|
|
147
|
+
### Name Object
|
|
211
148
|
|
|
212
|
-
|
|
213
|
-
- `#first_player?` - Check if first player style
|
|
214
|
-
- `#second_player?` - Check if second player style
|
|
149
|
+
The `Name` object is immutable and provides read-only access to the style name:
|
|
215
150
|
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
- `#with_letter(new_letter)` - Create style with different letter
|
|
219
|
-
- `#with_side(new_side)` - Create style with different side
|
|
151
|
+
```ruby
|
|
152
|
+
name = Sashite::Snn.parse("SHOGI")
|
|
220
153
|
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
154
|
+
name.value # => "SHOGI"
|
|
155
|
+
name.to_s # => "SHOGI"
|
|
156
|
+
name.frozen? # => true
|
|
157
|
+
```
|
|
225
158
|
|
|
226
|
-
|
|
159
|
+
**Equality and hashing:**
|
|
160
|
+
```ruby
|
|
161
|
+
name1 = Sashite::Snn.parse("CHESS")
|
|
162
|
+
name2 = Sashite::Snn.parse("CHESS")
|
|
163
|
+
name3 = Sashite::Snn.parse("chess")
|
|
227
164
|
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
165
|
+
name1 == name2 # => true
|
|
166
|
+
name1.hash == name2.hash # => true
|
|
167
|
+
name1 == name3 # => false (different case = different player)
|
|
168
|
+
```
|
|
232
169
|
|
|
233
|
-
##
|
|
170
|
+
## Examples
|
|
234
171
|
|
|
235
|
-
###
|
|
172
|
+
### Traditional Chess Family
|
|
236
173
|
|
|
237
174
|
```ruby
|
|
238
|
-
#
|
|
239
|
-
|
|
240
|
-
|
|
175
|
+
# First player styles (uppercase)
|
|
176
|
+
chess = Sashite::Snn.parse("CHESS") # Western Chess
|
|
177
|
+
shogi = Sashite::Snn.parse("SHOGI") # Japanese Chess
|
|
178
|
+
xiangqi = Sashite::Snn.parse("XIANGQI") # Chinese Chess
|
|
179
|
+
makruk = Sashite::Snn.parse("MAKRUK") # Thai Chess
|
|
180
|
+
|
|
181
|
+
# Second player styles (lowercase)
|
|
182
|
+
chess_p2 = Sashite::Snn.parse("chess")
|
|
183
|
+
shogi_p2 = Sashite::Snn.parse("shogi")
|
|
184
|
+
```
|
|
241
185
|
|
|
242
|
-
|
|
243
|
-
upper_case_letters.all?(&:first_player?) # => true
|
|
186
|
+
### Historical Games
|
|
244
187
|
|
|
245
|
-
|
|
246
|
-
|
|
188
|
+
```ruby
|
|
189
|
+
chaturanga = Sashite::Snn.parse("CHATURANGA") # Ancient Indian Chess
|
|
190
|
+
shatranj = Sashite::Snn.parse("SHATRANJ") # Medieval Islamic Chess
|
|
191
|
+
```
|
|
247
192
|
|
|
248
|
-
|
|
249
|
-
letter_a_first = Sashite::Snn.parse("A")
|
|
250
|
-
letter_a_second = Sashite::Snn.parse("a")
|
|
193
|
+
### Modern Variants
|
|
251
194
|
|
|
252
|
-
|
|
253
|
-
|
|
195
|
+
```ruby
|
|
196
|
+
raumschach = Sashite::Snn.parse("RAUMSCHACH") # 3D Chess
|
|
197
|
+
omega = Sashite::Snn.parse("OMEGA") # Omega Chess
|
|
254
198
|
```
|
|
255
199
|
|
|
256
|
-
###
|
|
200
|
+
### Case Consistency
|
|
257
201
|
|
|
258
202
|
```ruby
|
|
259
|
-
#
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
#
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
#
|
|
270
|
-
|
|
271
|
-
|
|
203
|
+
# Valid - all uppercase
|
|
204
|
+
Sashite::Snn.valid?("CHESS") # => true
|
|
205
|
+
Sashite::Snn.valid?("XIANGQI") # => true
|
|
206
|
+
|
|
207
|
+
# Valid - all lowercase
|
|
208
|
+
Sashite::Snn.valid?("shogi") # => true
|
|
209
|
+
Sashite::Snn.valid?("makruk") # => true
|
|
210
|
+
|
|
211
|
+
# Invalid - mixed case
|
|
212
|
+
Sashite::Snn.valid?("Chess") # => false
|
|
213
|
+
Sashite::Snn.valid?("Shogi") # => false
|
|
214
|
+
Sashite::Snn.valid?("XiangQi") # => false
|
|
215
|
+
|
|
216
|
+
# Invalid - contains non-alphabetic characters
|
|
217
|
+
Sashite::Snn.valid?("CHESS960") # => false
|
|
218
|
+
Sashite::Snn.valid?("GO9X9") # => false
|
|
219
|
+
Sashite::Snn.valid?("MINI_SHOGI") # => false
|
|
272
220
|
```
|
|
273
221
|
|
|
274
|
-
###
|
|
222
|
+
### Working with Names
|
|
275
223
|
|
|
276
224
|
```ruby
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
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
|
|
225
|
+
# Create and compare
|
|
226
|
+
name1 = Sashite::Snn.parse("SHOGI")
|
|
227
|
+
name2 = Sashite::Snn.parse("SHOGI")
|
|
228
|
+
name1 == name2 # => true
|
|
229
|
+
|
|
230
|
+
# String and symbol inputs
|
|
231
|
+
name1 = Sashite::Snn.name("XIANGQI")
|
|
232
|
+
name2 = Sashite::Snn.name(:XIANGQI)
|
|
233
|
+
name1 == name2 # => true
|
|
234
|
+
|
|
235
|
+
# Immutability
|
|
236
|
+
name = Sashite::Snn.parse("CHESS")
|
|
237
|
+
name.frozen? # => true
|
|
238
|
+
name.value.frozen? # => true
|
|
239
|
+
```
|
|
307
240
|
|
|
308
|
-
|
|
241
|
+
### Collections
|
|
309
242
|
|
|
310
|
-
|
|
311
|
-
|
|
243
|
+
```ruby
|
|
244
|
+
# Create a set of styles
|
|
245
|
+
styles = %w[CHESS SHOGI XIANGQI MAKRUK].map { |n| Sashite::Snn.parse(n) }
|
|
246
|
+
|
|
247
|
+
# Filter by prefix
|
|
248
|
+
styles.select { |s| s.value.start_with?("X") }.map(&:to_s)
|
|
249
|
+
# => ["XIANGQI"]
|
|
250
|
+
|
|
251
|
+
# Use in hash
|
|
252
|
+
style_map = {
|
|
253
|
+
Sashite::Snn.parse("CHESS") => "Western Chess",
|
|
254
|
+
Sashite::Snn.parse("SHOGI") => "Japanese Chess"
|
|
255
|
+
}
|
|
312
256
|
```
|
|
313
257
|
|
|
314
|
-
|
|
258
|
+
## Relationship with SIN
|
|
315
259
|
|
|
316
|
-
|
|
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
|
|
260
|
+
**SNN and SIN are independent primitives** that serve complementary roles:
|
|
328
261
|
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
analysis[:by_side][:first].size # => 3
|
|
332
|
-
analysis[:unique_letters] # => 3
|
|
333
|
-
analysis[:cross_family] # => true
|
|
334
|
-
```
|
|
262
|
+
- **SNN**: Human-readable, descriptive names (`CHESS`, `SHOGI`)
|
|
263
|
+
- **SIN**: Compact, single-character identification (`C`, `S`)
|
|
335
264
|
|
|
336
|
-
###
|
|
265
|
+
### Optional Correspondence
|
|
266
|
+
|
|
267
|
+
While both specifications can be used independently, they may be related through:
|
|
268
|
+
|
|
269
|
+
- **Mapping tables**: External context defining SNN ↔ SIN relationships
|
|
270
|
+
- **Case consistency**: When mapped, case must be preserved (`CHESS` ↔ `C`, `chess` ↔ `c`)
|
|
337
271
|
|
|
338
272
|
```ruby
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
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
|
|
273
|
+
# Example mapping (defined externally, not part of SNN)
|
|
274
|
+
SNN_TO_SIN = {
|
|
275
|
+
"CHESS" => "C",
|
|
276
|
+
"chess" => "c",
|
|
277
|
+
"SHOGI" => "S",
|
|
278
|
+
"shogi" => "s"
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
# Multiple SNN names may map to the same SIN
|
|
282
|
+
SNN_TO_SIN["CAPABLANCA"] = "C" # Also maps to "C"
|
|
283
|
+
SNN_TO_SIN["COURIER"] = "C" # Also maps to "C"
|
|
284
|
+
```
|
|
285
|
+
|
|
286
|
+
### Important Notes
|
|
369
287
|
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
288
|
+
1. **No dependency**: SNN does not depend on SIN, nor SIN on SNN
|
|
289
|
+
2. **Bidirectional mapping requires context**: Converting between SNN and SIN requires external mapping information
|
|
290
|
+
3. **Independent usage**: Systems may use SNN alone, SIN alone, or both with defined mappings
|
|
291
|
+
4. **Multiple mappings**: One SNN name may correspond to multiple SIN characters in different contexts, and vice versa
|
|
374
292
|
|
|
375
|
-
|
|
376
|
-
shogi_black = Sashite::Snn.parse("s")
|
|
293
|
+
## Error Handling
|
|
377
294
|
|
|
378
|
-
|
|
379
|
-
|
|
295
|
+
```ruby
|
|
296
|
+
begin
|
|
297
|
+
name = Sashite::Snn.parse("Chess960")
|
|
298
|
+
rescue ArgumentError => e
|
|
299
|
+
warn "Invalid SNN: #{e.message}"
|
|
300
|
+
# => "Invalid SNN string: \"Chess960\""
|
|
301
|
+
end
|
|
380
302
|
```
|
|
381
303
|
|
|
382
|
-
|
|
304
|
+
### Common Errors
|
|
383
305
|
|
|
384
|
-
|
|
306
|
+
```ruby
|
|
307
|
+
# Mixed case
|
|
308
|
+
Sashite::Snn.parse("Chess")
|
|
309
|
+
# => ArgumentError: Invalid SNN string: "Chess"
|
|
385
310
|
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
| **Player Assignment** | Case encoding | `C` = First player, `c` = Second player | Case determines side |
|
|
311
|
+
# Contains digits
|
|
312
|
+
Sashite::Snn.parse("CHESS960")
|
|
313
|
+
# => ArgumentError: Invalid SNN string: "CHESS960"
|
|
390
314
|
|
|
391
|
-
|
|
315
|
+
# Contains special characters
|
|
316
|
+
Sashite::Snn.parse("MINI_SHOGI")
|
|
317
|
+
# => ArgumentError: Invalid SNN string: "MINI_SHOGI"
|
|
392
318
|
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
319
|
+
# Empty string
|
|
320
|
+
Sashite::Snn.parse("")
|
|
321
|
+
# => ArgumentError: Invalid SNN string: ""
|
|
322
|
+
```
|
|
397
323
|
|
|
398
|
-
## Design
|
|
324
|
+
## Design Principles
|
|
399
325
|
|
|
400
|
-
- **
|
|
326
|
+
- **Human-readable**: Descriptive names for better usability
|
|
327
|
+
- **Case-consistent**: Visual distinction between players through case
|
|
328
|
+
- **Foundational primitive**: Serves as building block for formal style identification
|
|
401
329
|
- **Rule-agnostic**: Independent of specific game mechanics
|
|
402
|
-
- **
|
|
403
|
-
- **
|
|
404
|
-
- **
|
|
405
|
-
- **Functional**: Pure functions with no side effects
|
|
330
|
+
- **Self-contained**: No external dependencies
|
|
331
|
+
- **Immutable**: All objects are frozen and thread-safe
|
|
332
|
+
- **Canonical**: Each style has one valid representation per context
|
|
406
333
|
|
|
407
|
-
##
|
|
334
|
+
## Properties
|
|
408
335
|
|
|
409
|
-
-
|
|
410
|
-
- [SNN
|
|
411
|
-
-
|
|
412
|
-
-
|
|
413
|
-
-
|
|
414
|
-
- [QPI](https://sashite.dev/specs/qpi/) - Qualified Piece Identifier
|
|
336
|
+
- **Purely functional**: Immutable data structures, no side effects
|
|
337
|
+
- **Specification compliant**: Strict adherence to [SNN v1.0.0](https://sashite.dev/specs/snn/1.0.0/)
|
|
338
|
+
- **Minimal API**: Simple validation, parsing, and comparison
|
|
339
|
+
- **Universal**: Supports any abstract strategy board game style
|
|
340
|
+
- **No dependencies**: Foundational primitive requiring no external gems
|
|
415
341
|
|
|
416
342
|
## Documentation
|
|
417
343
|
|
|
418
|
-
- [
|
|
419
|
-
- [SNN Examples
|
|
420
|
-
- [
|
|
421
|
-
- [API Documentation](https://rubydoc.info/github/sashite/snn.rb/main)
|
|
344
|
+
- [SNN Specification v1.0.0](https://sashite.dev/specs/snn/1.0.0/) — Complete technical specification
|
|
345
|
+
- [SNN Examples](https://sashite.dev/specs/snn/1.0.0/examples/) — Comprehensive examples
|
|
346
|
+
- [API Documentation](https://rubydoc.info/github/sashite/snn.rb/main) — Full API reference
|
|
422
347
|
|
|
423
348
|
## Development
|
|
424
349
|
|
|
@@ -0,0 +1,170 @@
|
|
|
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 foundational naming system for abstract strategy game styles.
|
|
8
|
+
# Each name must consist of alphabetic characters only, all in the same case
|
|
9
|
+
# (either all uppercase or all lowercase).
|
|
10
|
+
#
|
|
11
|
+
# Case encoding:
|
|
12
|
+
# - UPPERCASE names represent the first player's style
|
|
13
|
+
# - lowercase names represent the second player's style
|
|
14
|
+
#
|
|
15
|
+
# Constraints:
|
|
16
|
+
# - Alphabetic characters only (A-Z or a-z)
|
|
17
|
+
# - Case consistency required (all uppercase OR all lowercase)
|
|
18
|
+
# - No digits, no special characters, no mixed case
|
|
19
|
+
#
|
|
20
|
+
# All instances are immutable.
|
|
21
|
+
#
|
|
22
|
+
# @example Valid names
|
|
23
|
+
# Sashite::Snn::Name.new("CHESS") # => #<Snn::Name value="CHESS">
|
|
24
|
+
# Sashite::Snn::Name.new("shogi") # => #<Snn::Name value="shogi">
|
|
25
|
+
# Sashite::Snn::Name.new("XIANGQI") # => #<Snn::Name value="XIANGQI">
|
|
26
|
+
#
|
|
27
|
+
# @example Invalid names
|
|
28
|
+
# Sashite::Snn::Name.new("Chess") # => ArgumentError (mixed case)
|
|
29
|
+
# Sashite::Snn::Name.new("CHESS960") # => ArgumentError (contains digits)
|
|
30
|
+
# Sashite::Snn::Name.new("GO9X9") # => ArgumentError (contains digits)
|
|
31
|
+
class Name
|
|
32
|
+
# SNN validation pattern matching the specification
|
|
33
|
+
# Format: All uppercase OR all lowercase alphabetic characters
|
|
34
|
+
SNN_PATTERN = /\A([A-Z]+|[a-z]+)\z/
|
|
35
|
+
|
|
36
|
+
# Error message for invalid SNN strings
|
|
37
|
+
ERROR_INVALID_NAME = "Invalid SNN string: %s"
|
|
38
|
+
|
|
39
|
+
# @return [String] the canonical style name
|
|
40
|
+
attr_reader :value
|
|
41
|
+
|
|
42
|
+
# Create a new style name instance
|
|
43
|
+
#
|
|
44
|
+
# The name must follow SNN format rules: all uppercase or all lowercase
|
|
45
|
+
# alphabetic characters only. No digits, special characters, or mixed case.
|
|
46
|
+
#
|
|
47
|
+
# @param name [String, Symbol] the style name (e.g., "SHOGI", :chess)
|
|
48
|
+
# @raise [ArgumentError] if the name does not match SNN pattern
|
|
49
|
+
#
|
|
50
|
+
# @example Create valid names
|
|
51
|
+
# Sashite::Snn::Name.new("CHESS") # First player Chess
|
|
52
|
+
# Sashite::Snn::Name.new("shogi") # Second player Shōgi
|
|
53
|
+
# Sashite::Snn::Name.new(:XIANGQI) # First player Xiangqi
|
|
54
|
+
#
|
|
55
|
+
# @example Invalid names raise errors
|
|
56
|
+
# Sashite::Snn::Name.new("Chess") # Mixed case
|
|
57
|
+
# Sashite::Snn::Name.new("CHESS960") # Contains digits
|
|
58
|
+
def initialize(name)
|
|
59
|
+
string_value = name.to_s
|
|
60
|
+
self.class.validate_format(string_value)
|
|
61
|
+
|
|
62
|
+
@value = string_value.freeze
|
|
63
|
+
freeze
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# Parse an SNN string into a Name object
|
|
67
|
+
#
|
|
68
|
+
# This is an alias for the constructor, provided for consistency
|
|
69
|
+
# with other Sashité specifications.
|
|
70
|
+
#
|
|
71
|
+
# @param string [String] the SNN-formatted style name
|
|
72
|
+
# @return [Name] a new Name instance
|
|
73
|
+
# @raise [ArgumentError] if the string is invalid
|
|
74
|
+
#
|
|
75
|
+
# @example Parse valid names
|
|
76
|
+
# Sashite::Snn::Name.parse("SHOGI") # => #<Snn::Name value="SHOGI">
|
|
77
|
+
# Sashite::Snn::Name.parse("chess") # => #<Snn::Name value="chess">
|
|
78
|
+
def self.parse(string)
|
|
79
|
+
new(string)
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
# Check whether the given string is a valid SNN name
|
|
83
|
+
#
|
|
84
|
+
# Valid SNN strings must:
|
|
85
|
+
# - Contain only alphabetic characters (A-Z or a-z)
|
|
86
|
+
# - Have consistent case (all uppercase OR all lowercase)
|
|
87
|
+
# - Contain at least one character
|
|
88
|
+
#
|
|
89
|
+
# @param string [String] input string to validate
|
|
90
|
+
# @return [Boolean] true if valid, false otherwise
|
|
91
|
+
#
|
|
92
|
+
# @example Valid names
|
|
93
|
+
# Sashite::Snn::Name.valid?("CHESS") # => true
|
|
94
|
+
# Sashite::Snn::Name.valid?("shogi") # => true
|
|
95
|
+
# Sashite::Snn::Name.valid?("XIANGQI") # => true
|
|
96
|
+
#
|
|
97
|
+
# @example Invalid names
|
|
98
|
+
# Sashite::Snn::Name.valid?("Chess") # => false (mixed case)
|
|
99
|
+
# Sashite::Snn::Name.valid?("CHESS960") # => false (contains digits)
|
|
100
|
+
# Sashite::Snn::Name.valid?("GO9X9") # => false (contains digits)
|
|
101
|
+
# Sashite::Snn::Name.valid?("") # => false (empty)
|
|
102
|
+
def self.valid?(string)
|
|
103
|
+
string.is_a?(::String) && string.match?(SNN_PATTERN)
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
# Returns the string representation of the name
|
|
107
|
+
#
|
|
108
|
+
# @return [String] the canonical style name
|
|
109
|
+
#
|
|
110
|
+
# @example
|
|
111
|
+
# name = Sashite::Snn::Name.new("SHOGI")
|
|
112
|
+
# name.to_s # => "SHOGI"
|
|
113
|
+
def to_s
|
|
114
|
+
value
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
# Equality based on string value
|
|
118
|
+
#
|
|
119
|
+
# Two names are equal if they have the same string value. Case matters:
|
|
120
|
+
# "CHESS" (first player) is not equal to "chess" (second player).
|
|
121
|
+
#
|
|
122
|
+
# @param other [Object] object to compare with
|
|
123
|
+
# @return [Boolean] true if equal, false otherwise
|
|
124
|
+
#
|
|
125
|
+
# @example
|
|
126
|
+
# name1 = Sashite::Snn::Name.new("CHESS")
|
|
127
|
+
# name2 = Sashite::Snn::Name.new("CHESS")
|
|
128
|
+
# name3 = Sashite::Snn::Name.new("chess")
|
|
129
|
+
#
|
|
130
|
+
# name1 == name2 # => true
|
|
131
|
+
# name1 == name3 # => false (different case = different player)
|
|
132
|
+
def ==(other)
|
|
133
|
+
other.is_a?(self.class) && value == other.value
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
# Required for correct Set/Hash behavior
|
|
137
|
+
alias eql? ==
|
|
138
|
+
|
|
139
|
+
# Hash based on class and value
|
|
140
|
+
#
|
|
141
|
+
# Enables Name objects to be used as hash keys and in sets.
|
|
142
|
+
#
|
|
143
|
+
# @return [Integer] hash code
|
|
144
|
+
#
|
|
145
|
+
# @example Use as hash key
|
|
146
|
+
# styles = {
|
|
147
|
+
# Sashite::Snn::Name.new("CHESS") => "Western Chess",
|
|
148
|
+
# Sashite::Snn::Name.new("SHOGI") => "Japanese Chess"
|
|
149
|
+
# }
|
|
150
|
+
def hash
|
|
151
|
+
[self.class, value].hash
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
# Validate that the string is in proper SNN format
|
|
155
|
+
#
|
|
156
|
+
# @param str [String] string to validate
|
|
157
|
+
# @raise [ArgumentError] if the string does not match SNN pattern
|
|
158
|
+
#
|
|
159
|
+
# @example Valid format
|
|
160
|
+
# Sashite::Snn::Name.validate_format("CHESS") # No error
|
|
161
|
+
# Sashite::Snn::Name.validate_format("shogi") # No error
|
|
162
|
+
#
|
|
163
|
+
# @example Invalid format
|
|
164
|
+
# Sashite::Snn::Name.validate_format("Chess") # Raises ArgumentError
|
|
165
|
+
def self.validate_format(str)
|
|
166
|
+
raise ::ArgumentError, format(ERROR_INVALID_NAME, str.inspect) unless str.match?(SNN_PATTERN)
|
|
167
|
+
end
|
|
168
|
+
end
|
|
169
|
+
end
|
|
170
|
+
end
|
data/lib/sashite/snn.rb
CHANGED
|
@@ -1,65 +1,93 @@
|
|
|
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 foundational naming system for identifying styles in abstract strategy board games.
|
|
9
|
+
# SNN uses canonical, human-readable alphabetic names with case encoding to represent both
|
|
10
|
+
# style identity and player assignment.
|
|
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: All uppercase OR all lowercase alphabetic characters
|
|
16
13
|
#
|
|
17
14
|
# Examples:
|
|
18
|
-
# "
|
|
19
|
-
# "
|
|
20
|
-
# "
|
|
21
|
-
# "
|
|
15
|
+
# "CHESS" - Chess style for first player
|
|
16
|
+
# "chess" - Chess style for second player
|
|
17
|
+
# "SHOGI" - Shōgi style for first player
|
|
18
|
+
# "shogi" - Shōgi style for second player
|
|
19
|
+
# "XIANGQI" - Xiangqi style for first player
|
|
20
|
+
# "xiangqi" - Xiangqi style for second player
|
|
21
|
+
#
|
|
22
|
+
# Case Encoding:
|
|
23
|
+
# - UPPERCASE names represent the first player's style
|
|
24
|
+
# - lowercase names represent the second player's style
|
|
25
|
+
#
|
|
26
|
+
# Constraints:
|
|
27
|
+
# - Alphabetic characters only (A-Z, a-z)
|
|
28
|
+
# - Case consistency required (all uppercase OR all lowercase)
|
|
29
|
+
# - No digits, no special characters, no mixed case
|
|
30
|
+
#
|
|
31
|
+
# As a foundational primitive, SNN has no dependencies and serves as a building block
|
|
32
|
+
# for formal style identification in the Sashité ecosystem.
|
|
22
33
|
#
|
|
23
34
|
# See: https://sashite.dev/specs/snn/1.0.0/
|
|
24
35
|
module Snn
|
|
25
|
-
# Check if a string is
|
|
36
|
+
# Check if a string is valid SNN notation
|
|
37
|
+
#
|
|
38
|
+
# Valid SNN strings must contain only alphabetic characters in consistent case
|
|
39
|
+
# (either all uppercase or all lowercase).
|
|
26
40
|
#
|
|
27
41
|
# @param snn_string [String] the string to validate
|
|
28
42
|
# @return [Boolean] true if valid SNN, false otherwise
|
|
29
43
|
#
|
|
30
|
-
# @example Validate
|
|
31
|
-
# Sashite::Snn.valid?("
|
|
32
|
-
# Sashite::Snn.valid?("
|
|
33
|
-
# Sashite::Snn.valid?("
|
|
34
|
-
# Sashite::Snn.valid?("
|
|
44
|
+
# @example Validate SNN strings
|
|
45
|
+
# Sashite::Snn.valid?("CHESS") # => true
|
|
46
|
+
# Sashite::Snn.valid?("shogi") # => true
|
|
47
|
+
# Sashite::Snn.valid?("Chess") # => false (mixed case)
|
|
48
|
+
# Sashite::Snn.valid?("CHESS960") # => false (contains digits)
|
|
49
|
+
# Sashite::Snn.valid?("GO9X9") # => false (contains digits)
|
|
35
50
|
def self.valid?(snn_string)
|
|
36
|
-
|
|
51
|
+
Name.valid?(snn_string)
|
|
37
52
|
end
|
|
38
53
|
|
|
39
|
-
# Parse an SNN string into a
|
|
54
|
+
# Parse an SNN string into a Name object
|
|
55
|
+
#
|
|
56
|
+
# Converts a valid SNN string into an immutable Name object. The name must follow
|
|
57
|
+
# SNN format rules: all uppercase or all lowercase alphabetic characters only.
|
|
40
58
|
#
|
|
41
|
-
# @param snn_string [String]
|
|
42
|
-
# @return [Snn::
|
|
43
|
-
# @raise [ArgumentError] if the
|
|
44
|
-
#
|
|
45
|
-
#
|
|
46
|
-
# Sashite::Snn.parse("
|
|
47
|
-
# Sashite::Snn.parse("
|
|
59
|
+
# @param snn_string [String] the name string
|
|
60
|
+
# @return [Snn::Name] a parsed name object
|
|
61
|
+
# @raise [ArgumentError] if the name is invalid
|
|
62
|
+
#
|
|
63
|
+
# @example Parse valid SNN names
|
|
64
|
+
# Sashite::Snn.parse("SHOGI") # => #<Snn::Name value="SHOGI">
|
|
65
|
+
# Sashite::Snn.parse("chess") # => #<Snn::Name value="chess">
|
|
66
|
+
#
|
|
67
|
+
# @example Invalid names raise errors
|
|
68
|
+
# Sashite::Snn.parse("Chess") # => ArgumentError (mixed case)
|
|
69
|
+
# Sashite::Snn.parse("CHESS960") # => ArgumentError (contains digits)
|
|
48
70
|
def self.parse(snn_string)
|
|
49
|
-
|
|
71
|
+
Name.parse(snn_string)
|
|
50
72
|
end
|
|
51
73
|
|
|
52
|
-
# Create a new
|
|
74
|
+
# Create a new Name instance directly
|
|
75
|
+
#
|
|
76
|
+
# Constructs a Name object from a string or symbol. The value must follow
|
|
77
|
+
# SNN format rules: all uppercase or all lowercase alphabetic characters only.
|
|
78
|
+
#
|
|
79
|
+
# @param value [String, Symbol] style name to construct
|
|
80
|
+
# @return [Snn::Name] new name instance
|
|
81
|
+
# @raise [ArgumentError] if name format is invalid
|
|
82
|
+
#
|
|
83
|
+
# @example Create names
|
|
84
|
+
# Sashite::Snn.name("XIANGQI") # => #<Snn::Name value="XIANGQI">
|
|
85
|
+
# Sashite::Snn.name(:makruk) # => #<Snn::Name value="makruk">
|
|
53
86
|
#
|
|
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)
|
|
87
|
+
# @example Invalid formats raise errors
|
|
88
|
+
# Sashite::Snn.name("Chess960") # => ArgumentError
|
|
89
|
+
def self.name(value)
|
|
90
|
+
Name.new(value)
|
|
63
91
|
end
|
|
64
92
|
end
|
|
65
93
|
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.1.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Cyril Kato
|
|
@@ -10,13 +10,18 @@ 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
|
-
in abstract strategy board games. This gem implements the SNN Specification v1.0.0
|
|
15
|
-
a modern Ruby interface featuring immutable style objects and functional programming
|
|
16
|
-
principles.
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
13
|
+
SNN (Style Name Notation) provides a foundational, rule-agnostic naming system for identifying
|
|
14
|
+
game styles in abstract strategy board games. This gem implements the SNN Specification v1.0.0
|
|
15
|
+
with a modern Ruby interface featuring immutable style name objects and functional programming
|
|
16
|
+
principles.
|
|
17
|
+
|
|
18
|
+
SNN uses case-consistent alphabetic names (e.g., "CHESS", "SHOGI", "XIANGQI" for first player;
|
|
19
|
+
"chess", "shogi", "xiangqi" for second player) to unambiguously represent both style identity
|
|
20
|
+
and player assignment. As a foundational primitive with no dependencies, SNN serves as a building
|
|
21
|
+
block for formal style identification across the Sashité ecosystem.
|
|
22
|
+
|
|
23
|
+
Format: All uppercase OR all lowercase alphabetic characters only (no digits, no special characters).
|
|
24
|
+
Ideal for game engines, protocols, and tools requiring clear and extensible style identifiers.
|
|
20
25
|
email: contact@cyril.email
|
|
21
26
|
executables: []
|
|
22
27
|
extensions: []
|
|
@@ -26,7 +31,7 @@ files:
|
|
|
26
31
|
- README.md
|
|
27
32
|
- lib/sashite-snn.rb
|
|
28
33
|
- lib/sashite/snn.rb
|
|
29
|
-
- lib/sashite/snn/
|
|
34
|
+
- lib/sashite/snn/name.rb
|
|
30
35
|
homepage: https://github.com/sashite/snn.rb
|
|
31
36
|
licenses:
|
|
32
37
|
- MIT
|
|
@@ -51,7 +56,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
51
56
|
- !ruby/object:Gem::Version
|
|
52
57
|
version: '0'
|
|
53
58
|
requirements: []
|
|
54
|
-
rubygems_version: 3.
|
|
59
|
+
rubygems_version: 3.7.1
|
|
55
60
|
specification_version: 4
|
|
56
|
-
summary: SNN (Style Name Notation)
|
|
61
|
+
summary: SNN (Style Name Notation) - foundational naming system for abstract strategy
|
|
62
|
+
game styles
|
|
57
63
|
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
|