sashite-snn 1.1.1 → 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 +74 -372
- data/lib/sashite/snn/name.rb +92 -0
- data/lib/sashite/snn.rb +32 -36
- data/lib/sashite-snn.rb +2 -2
- metadata +11 -10
- data/lib/sashite/snn/style.rb +0 -263
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
|
|
@@ -28,426 +28,128 @@ gem install sashite-snn
|
|
28
28
|
|
29
29
|
## Usage
|
30
30
|
|
31
|
+
### Basic Operations
|
32
|
+
|
31
33
|
```ruby
|
32
34
|
require "sashite/snn"
|
33
35
|
|
34
|
-
# Parse SNN strings into style objects
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
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"
|
39
40
|
|
40
|
-
# Create
|
41
|
-
|
42
|
-
|
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">
|
43
44
|
|
44
45
|
# Validate SNN strings
|
45
|
-
Sashite::Snn.valid?("
|
46
|
-
Sashite::Snn.valid?("chess")
|
47
|
-
Sashite::Snn.valid?("
|
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
|
-
```
|
75
|
-
|
76
|
-
## Format Specification
|
77
|
-
|
78
|
-
### Structure
|
79
|
-
```
|
80
|
-
<style-identifier>
|
46
|
+
Sashite::Snn.valid?("Go9x9") # => true
|
47
|
+
Sashite::Snn.valid?("chess") # => false (must start with uppercase)
|
48
|
+
Sashite::Snn.valid?("3DChess") # => false (invalid character)
|
81
49
|
```
|
82
50
|
|
83
|
-
###
|
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)
|
89
|
-
|
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
|
-
```
|
95
|
-
|
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
|
103
|
-
|
104
|
-
## Game Examples
|
105
|
-
|
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"
|
118
|
-
```
|
51
|
+
### Normalization and Comparison
|
119
52
|
|
120
|
-
### Chess Variants
|
121
53
|
```ruby
|
122
|
-
|
123
|
-
|
124
|
-
chess960_black = chess960_white.flip
|
125
|
-
chess960_black.to_s # => "chess960"
|
54
|
+
a = Sashite::Snn.parse("Chess960")
|
55
|
+
b = Sashite::Snn.parse("Chess960")
|
126
56
|
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
# Three-Check Chess
|
132
|
-
threecheck = Sashite::Snn.style(:Threecheck, :first)
|
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"
|
133
60
|
```
|
134
61
|
|
135
|
-
###
|
136
|
-
```ruby
|
137
|
-
# Traditional Shōgi
|
138
|
-
standard_shogi = Sashite::Snn.style(:Shogi, :first)
|
139
|
-
|
140
|
-
# Mini Shōgi
|
141
|
-
mini_shogi = Sashite::Snn.style(:Minishogi, :first)
|
142
|
-
|
143
|
-
# Chu Shōgi
|
144
|
-
chu_shogi = Sashite::Snn.style(:Chushogi, :first)
|
145
|
-
```
|
62
|
+
### Canonical Representation
|
146
63
|
|
147
|
-
### Multi-Style Gaming
|
148
64
|
```ruby
|
149
|
-
#
|
150
|
-
|
151
|
-
|
152
|
-
|
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
|
159
|
-
|
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
|
165
|
-
|
166
|
-
chess_white = Sashite::Snn.parse("CHESS")
|
167
|
-
shogi_black = Sashite::Snn.parse("shogi")
|
168
|
-
puts compatible_styles?(chess_white, shogi_black) # => true
|
65
|
+
# All names are normalized to a canonical format
|
66
|
+
name = Sashite::Snn.parse("Minishogi")
|
67
|
+
name.value # => "Minishogi"
|
68
|
+
name.to_s # => "Minishogi"
|
169
69
|
```
|
170
70
|
|
171
|
-
|
172
|
-
|
173
|
-
### Main Module Methods
|
174
|
-
|
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
|
178
|
-
|
179
|
-
### Style Class
|
180
|
-
|
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
|
189
|
-
|
190
|
-
#### Name and Case Handling
|
191
|
-
|
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:
|
71
|
+
### Collections and Filtering
|
193
72
|
|
194
73
|
```ruby
|
195
|
-
|
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)
|
74
|
+
names = %w[Chess Shogi Makruk Antichess Minishogi].map { |n| Sashite::Snn.parse(n) }
|
201
75
|
|
202
|
-
|
203
|
-
|
76
|
+
# Filter by prefix
|
77
|
+
names.select { |n| n.value.start_with?("Mini") }.map(&:to_s)
|
78
|
+
# => ["Minishogi"]
|
204
79
|
```
|
205
80
|
|
206
|
-
|
207
|
-
- `#first_player?` - Check if first player style
|
208
|
-
- `#second_player?` - Check if second player style
|
209
|
-
|
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
|
214
|
-
|
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
|
219
|
-
|
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
|
226
|
-
|
227
|
-
## Advanced Usage
|
228
|
-
|
229
|
-
### Name Normalization Examples
|
230
|
-
|
231
|
-
```ruby
|
232
|
-
# Parsing different cases results in same name
|
233
|
-
white_chess = Sashite::Snn.parse("CHESS")
|
234
|
-
black_chess = Sashite::Snn.parse("chess")
|
235
|
-
|
236
|
-
# Names are normalized with proper capitalization
|
237
|
-
white_chess.name # => :Chess
|
238
|
-
black_chess.name # => :Chess (same name!)
|
239
|
-
|
240
|
-
# Sides are different
|
241
|
-
white_chess.side # => :first
|
242
|
-
black_chess.side # => :second
|
81
|
+
## Format Specification
|
243
82
|
|
244
|
-
|
245
|
-
white_chess.to_s # => "CHESS"
|
246
|
-
black_chess.to_s # => "chess"
|
83
|
+
### Structure
|
247
84
|
|
248
|
-
# Same name, different sides
|
249
|
-
white_chess.same_name?(black_chess) # => true
|
250
|
-
white_chess.same_side?(black_chess) # => false
|
251
85
|
```
|
252
|
-
|
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"
|
86
|
+
<uppercase-letter>[<lowercase-letter | digit>]*
|
268
87
|
```
|
269
88
|
|
270
|
-
###
|
271
|
-
```ruby
|
272
|
-
class GameConfiguration
|
273
|
-
def initialize
|
274
|
-
@player_styles = {}
|
275
|
-
end
|
276
|
-
|
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
|
89
|
+
### Grammar (BNF)
|
281
90
|
|
282
|
-
|
283
|
-
|
284
|
-
end
|
91
|
+
```bnf
|
92
|
+
<snn> ::= <uppercase-letter> <tail>
|
285
93
|
|
286
|
-
|
287
|
-
|
94
|
+
<tail> ::= "" ; Single letter (e.g., "X")
|
95
|
+
| <alphanumeric-char> <tail> ; Extended name
|
288
96
|
|
289
|
-
|
290
|
-
!styles.all? { |style| style.same_name?(styles.first) }
|
291
|
-
end
|
97
|
+
<alphanumeric-char> ::= <lowercase-letter> | <digit>
|
292
98
|
|
293
|
-
|
294
|
-
|
295
|
-
|
296
|
-
style_names = @player_styles.values.map(&:name).uniq
|
297
|
-
style_names.size > 1
|
298
|
-
end
|
299
|
-
end
|
300
|
-
|
301
|
-
# Usage
|
302
|
-
config = GameConfiguration.new
|
303
|
-
config.set_player_style(:white, :Chess)
|
304
|
-
config.set_player_style(:black, :Shogi)
|
305
|
-
|
306
|
-
puts config.cross_tradition_match? # => true
|
307
|
-
puts config.style_mismatch? # => true
|
308
|
-
|
309
|
-
white_style = config.get_player_style(:white)
|
310
|
-
puts white_style.to_s # => "CHESS"
|
99
|
+
<uppercase-letter> ::= "A" | "B" | "C" | ... | "Z"
|
100
|
+
<lowercase-letter> ::= "a" | "b" | "c" | ... | "z"
|
101
|
+
<digit> ::= "0" | "1" | "2" | "3" | ... | "9"
|
311
102
|
```
|
312
103
|
|
313
|
-
###
|
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
|
-
```
|
104
|
+
### Regular Expression
|
333
105
|
|
334
|
-
### Tournament Style Management
|
335
106
|
```ruby
|
336
|
-
|
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]
|
107
|
+
/\A[A-Z][a-z0-9]*\z/
|
377
108
|
```
|
378
109
|
|
379
|
-
##
|
380
|
-
|
381
|
-
Following the [Game Protocol](https://sashite.dev/game-protocol/):
|
110
|
+
## Design Principles
|
382
111
|
|
383
|
-
|
384
|
-
|
385
|
-
|
386
|
-
|
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.
|
387
116
|
|
388
|
-
|
117
|
+
## Integration with SIN
|
389
118
|
|
390
|
-
|
119
|
+
SNN names serve as the formal source for SIN character identifiers. For example:
|
391
120
|
|
392
|
-
|
121
|
+
| SNN | SIN |
|
122
|
+
| --------- | ------- |
|
123
|
+
| `Chess` | `C`/`c` |
|
124
|
+
| `Shogi` | `S`/`s` |
|
125
|
+
| `Xiangqi` | `X`/`x` |
|
126
|
+
| `Makruk` | `M`/`m` |
|
393
127
|
|
394
|
-
|
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
|
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.
|
400
129
|
|
401
|
-
##
|
402
|
-
|
403
|
-
### Name Normalization Convention
|
404
|
-
|
405
|
-
SNN follows a strict name normalization convention:
|
406
|
-
|
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
|
411
|
-
|
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
|
416
|
-
|
417
|
-
### Example Flow
|
130
|
+
## Examples
|
418
131
|
|
419
132
|
```ruby
|
420
|
-
#
|
421
|
-
#
|
422
|
-
#
|
423
|
-
#
|
424
|
-
# ↓ Display
|
425
|
-
# SNN: "chess" (final representation)
|
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
|
426
137
|
```
|
427
138
|
|
428
|
-
|
429
|
-
|
430
|
-
## System Constraints
|
431
|
-
|
432
|
-
- **Alphanumeric identifiers** starting with a letter
|
433
|
-
- **Exactly 2 players** (uppercase/lowercase distinction)
|
434
|
-
- **Case consistency** within each identifier (no mixed case)
|
139
|
+
## API Reference
|
435
140
|
|
436
|
-
|
141
|
+
### Main Module
|
437
142
|
|
438
|
-
|
439
|
-
|
440
|
-
|
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
|
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.
|
444
146
|
|
445
|
-
|
147
|
+
### `Sashite::Snn::Name`
|
446
148
|
|
447
|
-
|
448
|
-
|
449
|
-
|
450
|
-
|
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.
|
451
153
|
|
452
154
|
## Development
|
453
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,63 +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 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
|
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?("
|
27
|
+
# @example Validate SNN strings
|
28
|
+
# Sashite::Snn.valid?("Chess") # => true
|
29
|
+
# Sashite::Snn.valid?("minishogi") # => false
|
30
|
+
# Sashite::Snn.valid?("Go9x9") # => true
|
33
31
|
def self.valid?(snn_string)
|
34
|
-
|
32
|
+
Name.valid?(snn_string)
|
35
33
|
end
|
36
34
|
|
37
|
-
# Parse an SNN string into a
|
35
|
+
# Parse an SNN string into a Name object
|
38
36
|
#
|
39
|
-
# @param snn_string [String]
|
40
|
-
# @return [Snn::
|
41
|
-
# @raise [ArgumentError] if the
|
42
|
-
#
|
43
|
-
#
|
44
|
-
# Sashite::Snn.parse("
|
45
|
-
# Sashite::Snn.parse("SHOGI") # => #<Snn::Style name=:Shogi 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">
|
46
43
|
def self.parse(snn_string)
|
47
|
-
|
44
|
+
Name.parse(snn_string)
|
48
45
|
end
|
49
46
|
|
50
|
-
# 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
|
51
52
|
#
|
52
|
-
# @
|
53
|
-
#
|
54
|
-
|
55
|
-
|
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)
|
53
|
+
# @example
|
54
|
+
# Sashite::Snn.name("Xiangqi") # => #<Snn::Name value="Xiangqi">
|
55
|
+
def self.name(value)
|
56
|
+
Name.new(value)
|
61
57
|
end
|
62
58
|
end
|
63
59
|
end
|
data/lib/sashite-snn.rb
CHANGED
@@ -5,9 +5,9 @@ require_relative "sashite/snn"
|
|
5
5
|
# Sashité namespace for board game notation libraries
|
6
6
|
#
|
7
7
|
# Sashité provides a collection of libraries for representing and manipulating
|
8
|
-
# board game concepts according to the
|
8
|
+
# board game concepts according to the Sashité Protocol specifications.
|
9
9
|
#
|
10
|
-
# @see https://sashite.dev/
|
10
|
+
# @see https://sashite.dev/protocol/ Sashité Protocol
|
11
11
|
# @see https://sashite.dev/specs/ Sashité Specifications
|
12
12
|
# @author Sashité
|
13
13
|
module Sashite
|
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,12 +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
|
-
|
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.
|
19
19
|
email: contact@cyril.email
|
20
20
|
executables: []
|
21
21
|
extensions: []
|
@@ -25,7 +25,7 @@ files:
|
|
25
25
|
- README.md
|
26
26
|
- lib/sashite-snn.rb
|
27
27
|
- lib/sashite/snn.rb
|
28
|
-
- lib/sashite/snn/
|
28
|
+
- lib/sashite/snn/name.rb
|
29
29
|
homepage: https://github.com/sashite/snn.rb
|
30
30
|
licenses:
|
31
31
|
- MIT
|
@@ -34,7 +34,7 @@ metadata:
|
|
34
34
|
documentation_uri: https://rubydoc.info/github/sashite/snn.rb/main
|
35
35
|
homepage_uri: https://github.com/sashite/snn.rb
|
36
36
|
source_code_uri: https://github.com/sashite/snn.rb
|
37
|
-
specification_uri: https://sashite.dev/
|
37
|
+
specification_uri: https://sashite.dev/specs/snn/1.0.0/
|
38
38
|
rubygems_mfa_required: 'true'
|
39
39
|
rdoc_options: []
|
40
40
|
require_paths:
|
@@ -52,5 +52,6 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
52
52
|
requirements: []
|
53
53
|
rubygems_version: 3.6.9
|
54
54
|
specification_version: 4
|
55
|
-
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
|
56
57
|
test_files: []
|
data/lib/sashite/snn/style.rb
DELETED
@@ -1,263 +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 an alphanumeric identifier with case-based side encoding:
|
8
|
-
# - Uppercase identifier: first player (CHESS, SHOGI, XIANGQI)
|
9
|
-
# - Lowercase identifier: second player (chess, shogi, xiangqi)
|
10
|
-
#
|
11
|
-
# All instances are immutable - transformation methods return new instances.
|
12
|
-
# This follows the Game Protocol's style model with Name and Side attributes.
|
13
|
-
class Style
|
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
|
37
|
-
|
38
|
-
# Create a new style instance
|
39
|
-
#
|
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)
|
46
|
-
|
47
|
-
@name = name
|
48
|
-
@side = side
|
49
|
-
|
50
|
-
freeze
|
51
|
-
end
|
52
|
-
|
53
|
-
# Parse an SNN string into a Style object
|
54
|
-
#
|
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>
|
62
|
-
def self.parse(snn_string)
|
63
|
-
string_value = String(snn_string)
|
64
|
-
matches = match_pattern(string_value)
|
65
|
-
|
66
|
-
identifier = matches[:identifier]
|
67
|
-
|
68
|
-
# Determine side from case
|
69
|
-
style_side = identifier == identifier.upcase ? FIRST_PLAYER : SECOND_PLAYER
|
70
|
-
|
71
|
-
# Normalize name to proper capitalization
|
72
|
-
style_name = normalize_name(identifier)
|
73
|
-
|
74
|
-
new(style_name, style_side)
|
75
|
-
end
|
76
|
-
|
77
|
-
# Check if a string is a valid SNN notation
|
78
|
-
#
|
79
|
-
# @param snn_string [String] the string to validate
|
80
|
-
# @return [Boolean] true if valid SNN, false otherwise
|
81
|
-
#
|
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)
|
90
|
-
end
|
91
|
-
|
92
|
-
# Convert the style to its SNN string representation
|
93
|
-
#
|
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
|
101
|
-
end
|
102
|
-
|
103
|
-
# Create a new style with opposite ownership (side)
|
104
|
-
#
|
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)
|
110
|
-
end
|
111
|
-
|
112
|
-
# Create a new style with a different name (keeping same side)
|
113
|
-
#
|
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)
|
123
|
-
end
|
124
|
-
|
125
|
-
# Create a new style with a different side (keeping same name)
|
126
|
-
#
|
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
|
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 name as another
|
153
|
-
#
|
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
|
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 name and side
|
178
|
-
def ==(other)
|
179
|
-
return false unless other.is_a?(self.class)
|
180
|
-
|
181
|
-
name == other.name && 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, name, and side
|
190
|
-
def hash
|
191
|
-
[self.class, name, side].hash
|
192
|
-
end
|
193
|
-
|
194
|
-
# Validate that the name is a valid symbol with proper capitalization
|
195
|
-
#
|
196
|
-
# @param name [Symbol] the name to validate
|
197
|
-
# @raise [ArgumentError] if invalid
|
198
|
-
def self.validate_name(name)
|
199
|
-
return if valid_name?(name)
|
200
|
-
|
201
|
-
raise ::ArgumentError, format(ERROR_INVALID_NAME, name.inspect)
|
202
|
-
end
|
203
|
-
|
204
|
-
# Validate that the side is a valid symbol
|
205
|
-
#
|
206
|
-
# @param side [Symbol] the side to validate
|
207
|
-
# @raise [ArgumentError] if invalid
|
208
|
-
def self.validate_side(side)
|
209
|
-
return if VALID_SIDES.include?(side)
|
210
|
-
|
211
|
-
raise ::ArgumentError, format(ERROR_INVALID_SIDE, side.inspect)
|
212
|
-
end
|
213
|
-
|
214
|
-
# Check if a name is valid (symbol with proper capitalization)
|
215
|
-
#
|
216
|
-
# @param name [Object] the name to check
|
217
|
-
# @return [Boolean] true if valid
|
218
|
-
def self.valid_name?(name)
|
219
|
-
return false unless name.is_a?(::Symbol)
|
220
|
-
|
221
|
-
name_string = name.to_s
|
222
|
-
return false if name_string.empty?
|
223
|
-
|
224
|
-
# Must match proper capitalization pattern
|
225
|
-
name_string.match?(PROPER_NAME_PATTERN)
|
226
|
-
end
|
227
|
-
|
228
|
-
# Normalize identifier to proper capitalization symbol
|
229
|
-
#
|
230
|
-
# @param identifier [String] the identifier to normalize
|
231
|
-
# @return [Symbol] normalized name symbol
|
232
|
-
def self.normalize_name(identifier)
|
233
|
-
# Convert to proper capitalization: first letter uppercase, rest lowercase
|
234
|
-
normalized = identifier.downcase
|
235
|
-
normalized[0] = normalized[0].upcase if normalized.length > 0
|
236
|
-
normalized.to_sym
|
237
|
-
end
|
238
|
-
|
239
|
-
# Match SNN pattern against string
|
240
|
-
#
|
241
|
-
# @param string [String] string to match
|
242
|
-
# @return [MatchData] match data
|
243
|
-
# @raise [ArgumentError] if string doesn't match
|
244
|
-
def self.match_pattern(string)
|
245
|
-
matches = SNN_PATTERN.match(string)
|
246
|
-
return matches if matches
|
247
|
-
|
248
|
-
raise ::ArgumentError, format(ERROR_INVALID_SNN, string)
|
249
|
-
end
|
250
|
-
|
251
|
-
private_class_method :valid_name?, :normalize_name, :match_pattern
|
252
|
-
|
253
|
-
private
|
254
|
-
|
255
|
-
# Get the opposite side
|
256
|
-
#
|
257
|
-
# @return [Symbol] the opposite side
|
258
|
-
def opposite_side
|
259
|
-
first_player? ? SECOND_PLAYER : FIRST_PLAYER
|
260
|
-
end
|
261
|
-
end
|
262
|
-
end
|
263
|
-
end
|