sashite-sin 2.0.1 → 2.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 +109 -109
- data/lib/sashite/sin/identifier.rb +254 -98
- data/lib/sashite/sin.rb +155 -32
- metadata +1 -1
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 28dcd89f0b19451fd1f6455117c8094083dd0da16300e59c3b8aad0987353cc2
|
4
|
+
data.tar.gz: a21dd87d8a1437cad75f0c55c74e7cce93a3d69897384346dcb0ecedf5f7f6c1
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: dc4c9ae269c163a60e65c052013154c1709b79e781077ccf16c084a540e13f1303d2122de46f956e2e79d312d6ff106b4081ce0200bfa4296fe0a9e9402dda49
|
7
|
+
data.tar.gz: bcfa66181f441356daa3f7bfb5dfaf609b629728d17c8c798d0ac4af26da3d159954e13e9aa9fb87483c0979ab8f3cb54b7f6bdb3cca870d2ae0a2819a0a0f89
|
data/README.md
CHANGED
@@ -11,7 +11,7 @@
|
|
11
11
|
|
12
12
|
SIN (Style Identifier Notation) provides a compact, ASCII-based format for identifying **styles** in abstract strategy board games. SIN uses single-character identifiers with case encoding to represent both style identity and player assignment simultaneously.
|
13
13
|
|
14
|
-
This gem implements the [SIN Specification v1.0.0](https://sashite.dev/specs/sin/1.0.0/), providing a rule-agnostic notation system for style identification in board games.
|
14
|
+
This gem implements the [SIN Specification v1.0.0](https://sashite.dev/specs/sin/1.0.0/) exactly, providing a rule-agnostic notation system for style identification in board games.
|
15
15
|
|
16
16
|
## Installation
|
17
17
|
|
@@ -34,14 +34,15 @@ gem install sashite-sin
|
|
34
34
|
require "sashite/sin"
|
35
35
|
|
36
36
|
# Parse SIN strings into identifier objects
|
37
|
-
identifier = Sashite::Sin.parse("C") #
|
37
|
+
identifier = Sashite::Sin.parse("C") # Family=:C, Side=:first
|
38
38
|
identifier.to_s # => "C"
|
39
|
-
identifier.
|
39
|
+
identifier.family # => :C
|
40
40
|
identifier.side # => :first
|
41
|
+
identifier.letter # => "C" (combined representation)
|
41
42
|
|
42
43
|
# Create identifiers directly
|
43
|
-
identifier = Sashite::Sin.identifier(:C, :first) #
|
44
|
-
identifier = Sashite::Sin::Identifier.new(:
|
44
|
+
identifier = Sashite::Sin.identifier(:C, :first) # Family=:C, Side=:first
|
45
|
+
identifier = Sashite::Sin::Identifier.new(:C, :second) # Family=:C, Side=:second
|
45
46
|
|
46
47
|
# Validate SIN strings
|
47
48
|
Sashite::Sin.valid?("C") # => true
|
@@ -57,19 +58,19 @@ Sashite::Sin.valid?("CC") # => false (not single character)
|
|
57
58
|
identifier = Sashite::Sin.parse("C")
|
58
59
|
|
59
60
|
# Flip player assignment
|
60
|
-
flipped = identifier.flip #
|
61
|
+
flipped = identifier.flip # Family=:C, Side=:second
|
61
62
|
flipped.to_s # => "c"
|
62
63
|
|
63
|
-
# Change
|
64
|
-
changed = identifier.
|
64
|
+
# Change family
|
65
|
+
changed = identifier.with_family(:S) # Family=:S, Side=:first
|
65
66
|
changed.to_s # => "S"
|
66
67
|
|
67
68
|
# Change side
|
68
|
-
other_side = identifier.with_side(:second) #
|
69
|
+
other_side = identifier.with_side(:second) # Family=:C, Side=:second
|
69
70
|
other_side.to_s # => "c"
|
70
71
|
|
71
72
|
# Chain transformations
|
72
|
-
result = identifier.flip.
|
73
|
+
result = identifier.flip.with_family(:M) # Family=:M, Side=:second
|
73
74
|
result.to_s # => "m"
|
74
75
|
```
|
75
76
|
|
@@ -82,17 +83,17 @@ opposite = Sashite::Sin.parse("s")
|
|
82
83
|
# Player identification
|
83
84
|
identifier.first_player? # => true
|
84
85
|
identifier.second_player? # => false
|
85
|
-
opposite.first_player?
|
86
|
-
opposite.second_player?
|
86
|
+
opposite.first_player? # => false
|
87
|
+
opposite.second_player? # => true
|
87
88
|
|
88
|
-
#
|
89
|
+
# Family and side comparison
|
89
90
|
chess1 = Sashite::Sin.parse("C")
|
90
91
|
chess2 = Sashite::Sin.parse("c")
|
91
92
|
shogi = Sashite::Sin.parse("S")
|
92
93
|
|
93
|
-
chess1.
|
94
|
+
chess1.same_family?(chess2) # => true (both Chess family)
|
94
95
|
chess1.same_side?(shogi) # => true (both first player)
|
95
|
-
chess1.
|
96
|
+
chess1.same_family?(shogi) # => false (different families)
|
96
97
|
```
|
97
98
|
|
98
99
|
### Identifier Collections
|
@@ -105,12 +106,12 @@ identifiers = %w[C c S s M m].map { |sin| Sashite::Sin.parse(sin) }
|
|
105
106
|
first_player_identifiers = identifiers.select(&:first_player?)
|
106
107
|
first_player_identifiers.map(&:to_s) # => ["C", "S", "M"]
|
107
108
|
|
108
|
-
# Group by
|
109
|
-
|
110
|
-
|
109
|
+
# Group by family
|
110
|
+
by_family = identifiers.group_by(&:family)
|
111
|
+
by_family[:C].size # => 2 (both C and c)
|
111
112
|
|
112
|
-
# Find specific
|
113
|
-
chess_identifiers = identifiers.select { |i| i.
|
113
|
+
# Find specific families
|
114
|
+
chess_identifiers = identifiers.select { |i| i.family == :C }
|
114
115
|
chess_identifiers.map(&:to_s) # => ["C", "c"]
|
115
116
|
```
|
116
117
|
|
@@ -136,35 +137,60 @@ chess_identifiers.map(&:to_s) # => ["C", "c"]
|
|
136
137
|
|
137
138
|
### Style Attribute Mapping
|
138
139
|
|
140
|
+
SIN encodes style attributes using the following correspondence:
|
141
|
+
|
139
142
|
| Style Attribute | SIN Encoding | Examples |
|
140
143
|
|-----------------|--------------|----------|
|
141
|
-
| **
|
142
|
-
| **
|
144
|
+
| **Family** | Style family symbol | `:C`, `:S`, `:X` |
|
145
|
+
| **Side** | Player assignment | `:first`, `:second` |
|
146
|
+
| **Letter** | Combined representation | `"C"`, `"c"`, `"S"`, `"s"` |
|
147
|
+
|
148
|
+
#### Dual-Purpose Encoding
|
143
149
|
|
144
|
-
|
150
|
+
The **Letter** combines two distinct semantic components:
|
151
|
+
- **Style Family**: The underlying family symbol (:A-:Z), representing the game tradition or rule system
|
152
|
+
- **Player Assignment**: The side (:first or :second), encoded as case in the letter representation
|
145
153
|
|
146
|
-
|
154
|
+
**Examples**:
|
155
|
+
- Family `:C` + Side `:first` → Letter `"C"` (Chess, First player)
|
156
|
+
- Family `:C` + Side `:second` → Letter `"c"` (Chess, Second player)
|
157
|
+
- Family `:S` + Side `:first` → Letter `"S"` (Shōgi, First player)
|
158
|
+
- Family `:S` + Side `:second` → Letter `"s"` (Shōgi, Second player)
|
147
159
|
|
148
|
-
|
160
|
+
## Traditional Game Style Examples
|
161
|
+
|
162
|
+
The SIN specification is rule-agnostic and does not define specific letter assignments. However, here are common usage patterns following [SIN Examples](https://sashite.dev/specs/sin/1.0.0/examples/):
|
149
163
|
|
150
164
|
```ruby
|
151
|
-
# Chess
|
152
|
-
chess_white = Sashite::Sin.parse("C") # First player
|
153
|
-
chess_black = Sashite::Sin.parse("c") # Second player
|
165
|
+
# Chess (8×8 board)
|
166
|
+
chess_white = Sashite::Sin.parse("C") # First player (White pieces)
|
167
|
+
chess_black = Sashite::Sin.parse("c") # Second player (Black pieces)
|
168
|
+
|
169
|
+
# Shōgi (9×9 board)
|
170
|
+
shogi_sente = Sashite::Sin.parse("S") # First player (Sente 先手)
|
171
|
+
shogi_gote = Sashite::Sin.parse("s") # Second player (Gote 後手)
|
154
172
|
|
155
|
-
#
|
156
|
-
|
157
|
-
|
173
|
+
# Xiangqi (9×10 board)
|
174
|
+
xiangqi_red = Sashite::Sin.parse("X") # First player (Red pieces)
|
175
|
+
xiangqi_black = Sashite::Sin.parse("x") # Second player (Black pieces)
|
158
176
|
|
159
|
-
#
|
160
|
-
|
161
|
-
|
177
|
+
# Makruk (8×8 board)
|
178
|
+
makruk_white = Sashite::Sin.parse("M") # First player (White pieces)
|
179
|
+
makruk_black = Sashite::Sin.parse("m") # Second player (Black pieces)
|
180
|
+
|
181
|
+
# Janggi (9×10 board)
|
182
|
+
janggi_cho = Sashite::Sin.parse("J") # First player (Cho 초)
|
183
|
+
janggi_han = Sashite::Sin.parse("j") # Second player (Han 한)
|
162
184
|
```
|
163
185
|
|
164
|
-
|
186
|
+
## Cross-Style Scenarios
|
165
187
|
|
166
188
|
```ruby
|
167
|
-
#
|
189
|
+
# Chess vs. Ōgi Match (both 8×8 compatible)
|
190
|
+
chess_white = Sashite::Sin.parse("C") # Chess style, first player
|
191
|
+
ogi_black = Sashite::Sin.parse("o") # Ōgi style, second player
|
192
|
+
|
193
|
+
# Cross-Style Match Setup
|
168
194
|
def create_hybrid_match
|
169
195
|
[
|
170
196
|
Sashite::Sin.parse("C"), # First player uses Chess family
|
@@ -173,22 +199,22 @@ def create_hybrid_match
|
|
173
199
|
end
|
174
200
|
|
175
201
|
identifiers = create_hybrid_match
|
176
|
-
identifiers[0].same_side?(identifiers[1])
|
177
|
-
identifiers[0].
|
202
|
+
identifiers[0].same_side?(identifiers[1]) # => false (different players)
|
203
|
+
identifiers[0].same_family?(identifiers[1]) # => false (different families)
|
178
204
|
```
|
179
205
|
|
180
|
-
|
206
|
+
## System Constraints
|
181
207
|
|
182
|
-
|
183
|
-
|
184
|
-
|
185
|
-
|
186
|
-
|
187
|
-
|
188
|
-
|
189
|
-
|
190
|
-
|
191
|
-
|
208
|
+
### Character Limitation
|
209
|
+
SIN provides **26 possible identifiers** per player using ASCII letters (A-Z, a-z).
|
210
|
+
|
211
|
+
### Player Limitation
|
212
|
+
SIN supports exactly **two players** through case distinction:
|
213
|
+
- **First player**: Uppercase letters (A-Z) → `:first`
|
214
|
+
- **Second player**: Lowercase letters (a-z) → `:second`
|
215
|
+
|
216
|
+
### Context Dependency
|
217
|
+
The specific SIN assignment for a style may vary between different game contexts based on collision avoidance, historical precedence, and community conventions.
|
192
218
|
|
193
219
|
## API Reference
|
194
220
|
|
@@ -196,17 +222,18 @@ makruk_black.to_s # => "m"
|
|
196
222
|
|
197
223
|
- `Sashite::Sin.valid?(sin_string)` - Check if string is valid SIN notation
|
198
224
|
- `Sashite::Sin.parse(sin_string)` - Parse SIN string into Identifier object
|
199
|
-
- `Sashite::Sin.identifier(
|
225
|
+
- `Sashite::Sin.identifier(family, side)` - Create identifier instance directly
|
200
226
|
|
201
227
|
### Identifier Class
|
202
228
|
|
203
229
|
#### Creation and Parsing
|
204
|
-
- `Sashite::Sin::Identifier.new(
|
230
|
+
- `Sashite::Sin::Identifier.new(family, side)` - Create identifier instance
|
205
231
|
- `Sashite::Sin::Identifier.parse(sin_string)` - Parse SIN string
|
206
232
|
|
207
233
|
#### Attribute Access
|
208
|
-
- `#
|
234
|
+
- `#family` - Get style family (symbol :A through :Z)
|
209
235
|
- `#side` - Get player side (:first or :second)
|
236
|
+
- `#letter` - Get combined letter representation (string)
|
210
237
|
- `#to_s` - Convert to SIN string representation
|
211
238
|
|
212
239
|
#### Player Queries
|
@@ -215,42 +242,38 @@ makruk_black.to_s # => "m"
|
|
215
242
|
|
216
243
|
#### Transformations (immutable - return new instances)
|
217
244
|
- `#flip` - Switch player assignment
|
218
|
-
- `#
|
245
|
+
- `#with_family(new_family)` - Create identifier with different family
|
219
246
|
- `#with_side(new_side)` - Create identifier with different side
|
220
247
|
|
221
248
|
#### Comparison Methods
|
222
|
-
- `#
|
249
|
+
- `#same_family?(other)` - Check if same style family
|
223
250
|
- `#same_side?(other)` - Check if same player side
|
224
251
|
- `#==(other)` - Full equality comparison
|
252
|
+
- `#same_letter?(other)` - Alias for `same_family?` (deprecated)
|
225
253
|
|
226
|
-
###
|
254
|
+
### Constants
|
227
255
|
|
228
|
-
- `Sashite::Sin::Identifier::FIRST_PLAYER` - Symbol for first player (
|
229
|
-
- `Sashite::Sin::Identifier::SECOND_PLAYER` - Symbol for second player (
|
256
|
+
- `Sashite::Sin::Identifier::FIRST_PLAYER` - Symbol for first player (`:first`)
|
257
|
+
- `Sashite::Sin::Identifier::SECOND_PLAYER` - Symbol for second player (`:second`)
|
258
|
+
- `Sashite::Sin::Identifier::VALID_FAMILIES` - Array of valid families (`:A` to `:Z`)
|
230
259
|
- `Sashite::Sin::Identifier::VALID_SIDES` - Array of valid sides
|
231
260
|
- `Sashite::Sin::Identifier::SIN_PATTERN` - Regular expression for SIN validation
|
232
261
|
|
233
262
|
## Advanced Usage
|
234
263
|
|
235
|
-
###
|
264
|
+
### Family and Side Separation
|
236
265
|
|
237
266
|
```ruby
|
238
|
-
#
|
239
|
-
|
240
|
-
|
241
|
-
|
242
|
-
#
|
243
|
-
|
244
|
-
|
245
|
-
|
246
|
-
|
247
|
-
|
248
|
-
# Letter families are related by case
|
249
|
-
letter_a_first = Sashite::Sin.parse("A")
|
250
|
-
letter_a_second = Sashite::Sin.parse("a")
|
251
|
-
|
252
|
-
letter_a_first.same_letter?(letter_a_second) # => true
|
253
|
-
letter_a_first.same_side?(letter_a_second) # => false
|
267
|
+
# Clear separation of concerns
|
268
|
+
identifier = Sashite::Sin.parse("C")
|
269
|
+
identifier.family # => :C (Style Family - invariant)
|
270
|
+
identifier.side # => :first (Player Assignment)
|
271
|
+
identifier.letter # => "C" (Combined representation)
|
272
|
+
|
273
|
+
# Transformations are explicit
|
274
|
+
chess_white = Sashite::Sin.identifier(:C, :first)
|
275
|
+
shogi_white = chess_white.with_family(:S) # Change family, keep side
|
276
|
+
chess_black = chess_white.with_side(:second) # Change side, keep family
|
254
277
|
```
|
255
278
|
|
256
279
|
### Immutable Transformations
|
@@ -259,39 +282,29 @@ letter_a_first.same_side?(letter_a_second) # => false
|
|
259
282
|
# All transformations return new instances
|
260
283
|
original = Sashite::Sin.identifier(:C, :first)
|
261
284
|
flipped = original.flip
|
262
|
-
|
285
|
+
changed_family = original.with_family(:S)
|
263
286
|
|
264
287
|
# Original identifier is never modified
|
265
|
-
original.to_s
|
266
|
-
flipped.to_s
|
267
|
-
|
288
|
+
original.to_s # => "C" (unchanged)
|
289
|
+
flipped.to_s # => "c"
|
290
|
+
changed_family.to_s # => "S"
|
268
291
|
|
269
292
|
# Transformations can be chained
|
270
|
-
result = original.flip.
|
293
|
+
result = original.flip.with_family(:M).flip
|
271
294
|
result.to_s # => "M"
|
272
295
|
```
|
273
296
|
|
274
|
-
## Protocol Mapping
|
275
|
-
|
276
|
-
Following the [Sashité Protocol](https://sashite.dev/protocol/):
|
277
|
-
|
278
|
-
| Protocol Attribute | SIN Encoding | Examples | Notes |
|
279
|
-
|-------------------|--------------|----------|-------|
|
280
|
-
| **Style Family** | Letter choice | `C`, `S`, `X` | Rule-agnostic letter assignment |
|
281
|
-
| **Player Assignment** | Case encoding | `C` = First player, `c` = Second player | Case determines side |
|
282
|
-
|
283
|
-
## System Constraints
|
284
|
-
|
285
|
-
- **26 possible identifiers** per player using ASCII letters (A-Z, a-z)
|
286
|
-
- **Exactly 2 players** through case distinction
|
287
|
-
- **Single character** per style-player combination
|
288
|
-
- **Rule-agnostic** - no predefined letter meanings
|
289
|
-
|
290
297
|
## Design Properties
|
291
298
|
|
299
|
+
Following the SIN v1.0.0 specification, this implementation provides:
|
300
|
+
|
292
301
|
- **ASCII compatibility**: Maximum portability across systems
|
293
302
|
- **Rule-agnostic**: Independent of specific game mechanics
|
294
303
|
- **Minimal overhead**: Single character per style-player combination
|
304
|
+
- **Flexible collision resolution**: Systematic approaches for identifier conflicts
|
305
|
+
- **Semantic clarity**: Distinct concepts for Family, Side, and Letter
|
306
|
+
- **SNN coordination**: Works harmoniously with formal style naming
|
307
|
+
- **Context-aware**: Adapts to avoid conflicts within specific game scenarios
|
295
308
|
- **Canonical representation**: Each style-player combination has exactly one SIN identifier
|
296
309
|
- **Immutable**: All identifier instances are frozen and transformations return new objects
|
297
310
|
- **Functional**: Pure functions with no side effects
|
@@ -300,16 +313,13 @@ Following the [Sashité Protocol](https://sashite.dev/protocol/):
|
|
300
313
|
|
301
314
|
- [SIN Specification v1.0.0](https://sashite.dev/specs/sin/1.0.0/) - Complete technical specification
|
302
315
|
- [SIN Examples](https://sashite.dev/specs/sin/1.0.0/examples/) - Practical implementation examples
|
316
|
+
- [Style Name Notation (SNN)](https://sashite.dev/specs/snn/) - Formal naming for game styles
|
303
317
|
- [Sashité Protocol](https://sashite.dev/protocol/) - Conceptual foundation for abstract strategy board games
|
304
|
-
- [PIN](https://sashite.dev/specs/pin/) - Piece Identifier Notation
|
305
|
-
- [PNN](https://sashite.dev/specs/pnn/) - Piece Name Notation (style-aware piece representation)
|
306
|
-
- [QPI](https://sashite.dev/specs/qpi/) - Qualified Piece Identifier
|
307
318
|
|
308
319
|
## Documentation
|
309
320
|
|
310
321
|
- [Official SIN Specification v1.0.0](https://sashite.dev/specs/sin/1.0.0/)
|
311
322
|
- [SIN Examples Documentation](https://sashite.dev/specs/sin/1.0.0/examples/)
|
312
|
-
- [Sashité Protocol Foundation](https://sashite.dev/protocol/)
|
313
323
|
- [API Documentation](https://rubydoc.info/github/sashite/sin.rb/main)
|
314
324
|
|
315
325
|
## Development
|
@@ -329,16 +339,6 @@ ruby test.rb
|
|
329
339
|
yard doc
|
330
340
|
```
|
331
341
|
|
332
|
-
## Contributing
|
333
|
-
|
334
|
-
1. Fork the repository
|
335
|
-
2. Create a feature branch (`git checkout -b feature/new-feature`)
|
336
|
-
3. Add tests for your changes
|
337
|
-
4. Ensure all tests pass (`ruby test.rb`)
|
338
|
-
5. Commit your changes (`git commit -am 'Add new feature'`)
|
339
|
-
6. Push to the branch (`git push origin feature/new-feature`)
|
340
|
-
7. Create a Pull Request
|
341
|
-
|
342
342
|
## License
|
343
343
|
|
344
344
|
Available as open source under the [MIT License](https://opensource.org/licenses/MIT).
|
@@ -4,80 +4,170 @@ module Sashite
|
|
4
4
|
module Sin
|
5
5
|
# Represents an identifier in SIN (Style Identifier Notation) format.
|
6
6
|
#
|
7
|
+
# ## Concept
|
8
|
+
#
|
9
|
+
# SIN addresses the fundamental need to identify which style system governs piece behavior
|
10
|
+
# while simultaneously indicating which player controls pieces of that style. In cross-style
|
11
|
+
# scenarios where different players use different game traditions, this dual encoding becomes
|
12
|
+
# essential for unambiguous piece identification.
|
13
|
+
#
|
14
|
+
# ## Dual-Purpose Encoding
|
15
|
+
#
|
16
|
+
# Each SIN identifier serves two functions:
|
17
|
+
# - **Style Family Identification**: The family choice indicates which rule system applies
|
18
|
+
# - **Player Assignment**: The side indicates which player uses this style as their native system
|
19
|
+
#
|
20
|
+
# ## Format Structure
|
21
|
+
#
|
7
22
|
# An identifier consists of a single ASCII letter with case-based side encoding:
|
8
23
|
# - Uppercase letter: first player (A, B, C, ..., Z)
|
9
24
|
# - Lowercase letter: second player (a, b, c, ..., z)
|
10
25
|
#
|
26
|
+
# The letter representation combines two distinct semantic components:
|
27
|
+
# - **Style Family**: The underlying ASCII character (A-Z), representing the game tradition or rule system
|
28
|
+
# - **Player Assignment**: The case of the character (uppercase/lowercase), representing which player uses this style
|
29
|
+
#
|
30
|
+
# Examples of letter composition:
|
31
|
+
# - Family :C + Side :first → Letter "C" (Chess, First player)
|
32
|
+
# - Family :C + Side :second → Letter "c" (Chess, Second player)
|
33
|
+
# - Family :S + Side :first → Letter "S" (Shōgi, First player)
|
34
|
+
# - Family :S + Side :second → Letter "s" (Shōgi, Second player)
|
35
|
+
#
|
36
|
+
# ## Canonical Representation
|
37
|
+
#
|
38
|
+
# SIN enforces canonical representation where each style-player combination has exactly one
|
39
|
+
# valid identifier within a given context. This ensures consistent interpretation across
|
40
|
+
# different implementations while allowing flexibility for collision resolution.
|
41
|
+
#
|
42
|
+
# ## Immutability
|
43
|
+
#
|
11
44
|
# All instances are immutable - transformation methods return new instances.
|
12
|
-
# This follows the SIN Specification v1.0.0
|
45
|
+
# This follows the SIN Specification v1.0.0 functional design principles.
|
46
|
+
#
|
47
|
+
# @example Basic usage with traditional game styles
|
48
|
+
# # Chess family identifiers
|
49
|
+
# chess_white = Sashite::Sin::Identifier.parse("C") # Family :C, Side :first
|
50
|
+
# chess_black = Sashite::Sin::Identifier.parse("c") # Family :C, Side :second
|
51
|
+
#
|
52
|
+
# # Shōgi family identifiers
|
53
|
+
# shogi_sente = Sashite::Sin::Identifier.parse("S") # Family :S, Side :first
|
54
|
+
# shogi_gote = Sashite::Sin::Identifier.parse("s") # Family :S, Side :second
|
55
|
+
#
|
56
|
+
# @example Dual-purpose encoding demonstration
|
57
|
+
# identifier = Sashite::Sin::Identifier.parse("C")
|
58
|
+
# identifier.family # => :C (Style Family)
|
59
|
+
# identifier.side # => :first (Player Assignment)
|
60
|
+
# identifier.letter # => "C" (Combined representation)
|
61
|
+
#
|
62
|
+
# @example Cross-style scenarios
|
63
|
+
# # Different families in one match (requires compatible board structures)
|
64
|
+
# chess_style = Sashite::Sin::Identifier.parse("C") # First player uses Chess family
|
65
|
+
# ogi_style = Sashite::Sin::Identifier.parse("o") # Second player uses Ōgi family
|
66
|
+
#
|
67
|
+
# @see https://sashite.dev/specs/sin/1.0.0/ SIN Specification v1.0.0
|
13
68
|
class Identifier
|
14
|
-
# SIN validation pattern matching the specification
|
69
|
+
# SIN validation pattern matching the specification regular expression
|
70
|
+
# Grammar: <sin> ::= <uppercase-letter> | <lowercase-letter>
|
15
71
|
SIN_PATTERN = /\A[A-Za-z]\z/
|
16
72
|
|
17
|
-
# Player side constants
|
73
|
+
# Player side constants following SIN v1.0.0 two-player constraint
|
18
74
|
FIRST_PLAYER = :first
|
19
75
|
SECOND_PLAYER = :second
|
20
76
|
|
21
|
-
# Valid
|
77
|
+
# Valid families (A-Z)
|
78
|
+
VALID_FAMILIES = (:A..:Z).to_a.freeze
|
79
|
+
|
80
|
+
# Valid sides array for validation
|
22
81
|
VALID_SIDES = [FIRST_PLAYER, SECOND_PLAYER].freeze
|
23
82
|
|
24
|
-
# Error messages
|
25
|
-
ERROR_INVALID_SIN = "Invalid SIN string: %s"
|
26
|
-
|
27
|
-
ERROR_INVALID_SIDE = "Side must be :first or :second, got: %s"
|
83
|
+
# Error messages with SIN-compliant terminology
|
84
|
+
ERROR_INVALID_SIN = "Invalid SIN string: %s. Must be a single ASCII letter (A-Z, a-z)"
|
85
|
+
ERROR_INVALID_FAMILY = "Family must be a symbol from :A to :Z representing Style Family, got: %s"
|
86
|
+
ERROR_INVALID_SIDE = "Side must be :first or :second following SIN two-player constraint, got: %s"
|
28
87
|
|
29
|
-
# @return [Symbol] the style
|
30
|
-
|
88
|
+
# @return [Symbol] the style family (:A to :Z)
|
89
|
+
# This represents the Style Family component - the game tradition or rule system
|
90
|
+
attr_reader :family
|
31
91
|
|
32
92
|
# @return [Symbol] the player side (:first or :second)
|
93
|
+
# This represents the Player Assignment component
|
33
94
|
attr_reader :side
|
34
95
|
|
35
|
-
# Create a new identifier instance
|
96
|
+
# Create a new identifier instance with canonical representation
|
36
97
|
#
|
37
|
-
# @param
|
38
|
-
# @param side [Symbol] player side (:first or :second)
|
98
|
+
# @param family [Symbol] style family (:A to :Z representing Style Family)
|
99
|
+
# @param side [Symbol] player side (:first or :second representing Player Assignment)
|
39
100
|
# @raise [ArgumentError] if parameters are invalid
|
40
|
-
|
41
|
-
|
101
|
+
#
|
102
|
+
# @example Create identifiers with family and side separation
|
103
|
+
# # Chess family identifiers
|
104
|
+
# chess_first = Sashite::Sin::Identifier.new(:C, :first) # => Family=:C, Side=:first
|
105
|
+
# chess_second = Sashite::Sin::Identifier.new(:C, :second) # => Family=:C, Side=:second
|
106
|
+
#
|
107
|
+
# @example Style Family and Player Assignment demonstration
|
108
|
+
# identifier = Sashite::Sin::Identifier.new(:S, :first)
|
109
|
+
# identifier.family # => :S (Shōgi Style Family)
|
110
|
+
# identifier.side # => :first (First Player Assignment)
|
111
|
+
# identifier.letter # => "S" (Combined representation)
|
112
|
+
def initialize(family, side)
|
113
|
+
self.class.validate_family(family)
|
42
114
|
self.class.validate_side(side)
|
43
115
|
|
44
|
-
@
|
116
|
+
@family = family
|
45
117
|
@side = side
|
46
118
|
|
47
119
|
freeze
|
48
120
|
end
|
49
121
|
|
50
|
-
# Parse an SIN string into an Identifier object
|
122
|
+
# Parse an SIN string into an Identifier object with dual-purpose encoding
|
123
|
+
#
|
124
|
+
# The family and side are inferred from both the character choice (Style Family)
|
125
|
+
# and case (Player Assignment):
|
126
|
+
# - Uppercase letter → Style Family + First player
|
127
|
+
# - Lowercase letter → Style Family + Second player
|
51
128
|
#
|
52
129
|
# @param sin_string [String] SIN notation string (single ASCII letter)
|
53
|
-
# @return [Identifier] parsed identifier object with
|
130
|
+
# @return [Identifier] parsed identifier object with Family and Side attributes
|
54
131
|
# @raise [ArgumentError] if the SIN string is invalid
|
55
|
-
#
|
56
|
-
#
|
57
|
-
# Sashite::Sin::Identifier.parse("
|
58
|
-
# Sashite::Sin::Identifier.parse("
|
132
|
+
#
|
133
|
+
# @example Parse SIN strings with case-based Player Assignment inference
|
134
|
+
# Sashite::Sin::Identifier.parse("C") # => Family=:C, Side=:first (Chess, White)
|
135
|
+
# Sashite::Sin::Identifier.parse("c") # => Family=:C, Side=:second (Chess, Black)
|
136
|
+
# Sashite::Sin::Identifier.parse("S") # => Family=:S, Side=:first (Shōgi, Sente)
|
137
|
+
# Sashite::Sin::Identifier.parse("s") # => Family=:S, Side=:second (Shōgi, Gote)
|
138
|
+
#
|
139
|
+
# @example Traditional game styles from SIN Examples
|
140
|
+
# # Chess (8×8 board)
|
141
|
+
# chess_white = Sashite::Sin::Identifier.parse("C") # First player (White pieces)
|
142
|
+
# chess_black = Sashite::Sin::Identifier.parse("c") # Second player (Black pieces)
|
143
|
+
#
|
144
|
+
# # Xiangqi (9×10 board)
|
145
|
+
# xiangqi_red = Sashite::Sin::Identifier.parse("X") # First player (Red pieces)
|
146
|
+
# xiangqi_black = Sashite::Sin::Identifier.parse("x") # Second player (Black pieces)
|
59
147
|
def self.parse(sin_string)
|
60
148
|
string_value = String(sin_string)
|
61
149
|
validate_sin_string(string_value)
|
62
150
|
|
63
|
-
#
|
151
|
+
# Extract Style Family (case-insensitive) and Player Assignment (case-sensitive)
|
152
|
+
family_symbol = string_value.upcase.to_sym
|
64
153
|
identifier_side = string_value == string_value.upcase ? FIRST_PLAYER : SECOND_PLAYER
|
65
154
|
|
66
|
-
|
67
|
-
identifier_letter = string_value.to_sym
|
68
|
-
|
69
|
-
new(identifier_letter, identifier_side)
|
155
|
+
new(family_symbol, identifier_side)
|
70
156
|
end
|
71
157
|
|
72
|
-
# Check if a string is a valid SIN notation
|
158
|
+
# Check if a string is a valid SIN notation according to specification
|
159
|
+
#
|
160
|
+
# Validates against the SIN grammar:
|
161
|
+
# <sin> ::= <uppercase-letter> | <lowercase-letter>
|
73
162
|
#
|
74
163
|
# @param sin_string [String] the string to validate
|
75
164
|
# @return [Boolean] true if valid SIN, false otherwise
|
76
165
|
#
|
77
|
-
# @example Validate SIN strings
|
78
|
-
# Sashite::Sin::Identifier.valid?("C")
|
79
|
-
# Sashite::Sin::Identifier.valid?("c")
|
80
|
-
# Sashite::Sin::Identifier.valid?("CHESS")
|
166
|
+
# @example Validate SIN strings against specification
|
167
|
+
# Sashite::Sin::Identifier.valid?("C") # => true (Chess first player)
|
168
|
+
# Sashite::Sin::Identifier.valid?("c") # => true (Chess second player)
|
169
|
+
# Sashite::Sin::Identifier.valid?("CHESS") # => false (multi-character)
|
170
|
+
# Sashite::Sin::Identifier.valid?("1") # => false (not ASCII letter)
|
81
171
|
def self.valid?(sin_string)
|
82
172
|
return false unless sin_string.is_a?(::String)
|
83
173
|
|
@@ -86,99 +176,179 @@ module Sashite
|
|
86
176
|
|
87
177
|
# Convert the identifier to its SIN string representation
|
88
178
|
#
|
179
|
+
# Returns the canonical SIN notation with proper case encoding for Player Assignment.
|
180
|
+
#
|
89
181
|
# @return [String] SIN notation string (single ASCII letter)
|
90
|
-
#
|
91
|
-
#
|
92
|
-
#
|
93
|
-
#
|
182
|
+
#
|
183
|
+
# @example Display identifiers in canonical SIN format
|
184
|
+
# chess_first.to_s # => "C" (Chess family, first player)
|
185
|
+
# chess_second.to_s # => "c" (Chess family, second player)
|
186
|
+
# shogi_first.to_s # => "S" (Shōgi family, first player)
|
94
187
|
def to_s
|
95
|
-
letter
|
188
|
+
letter
|
96
189
|
end
|
97
190
|
|
98
|
-
#
|
191
|
+
# Get the letter representation combining Style Family and Player Assignment
|
192
|
+
#
|
193
|
+
# @return [String] letter representation with proper case encoding
|
194
|
+
#
|
195
|
+
# @example Letter representation with dual-purpose encoding
|
196
|
+
# chess_first.letter # => "C" (Chess family, first player)
|
197
|
+
# chess_second.letter # => "c" (Chess family, second player)
|
198
|
+
# shogi_first.letter # => "S" (Shōgi family, first player)
|
199
|
+
def letter
|
200
|
+
first_player? ? family.to_s.upcase : family.to_s.downcase
|
201
|
+
end
|
202
|
+
|
203
|
+
# Create a new identifier with opposite Player Assignment (flip sides)
|
204
|
+
#
|
205
|
+
# Transforms Player Assignment while maintaining Style Family:
|
206
|
+
# - First player → Second player (uppercase → lowercase)
|
207
|
+
# - Second player → First player (lowercase → uppercase)
|
99
208
|
#
|
100
|
-
# @return [Identifier] new immutable identifier instance with flipped
|
101
|
-
#
|
102
|
-
#
|
209
|
+
# @return [Identifier] new immutable identifier instance with flipped Player Assignment
|
210
|
+
#
|
211
|
+
# @example Flip Player Assignment within same Style Family
|
212
|
+
# chess_white = Sashite::Sin::Identifier.parse("C")
|
213
|
+
# chess_black = chess_white.flip # => Family=:C, Side=:second
|
214
|
+
#
|
215
|
+
# shogi_sente = Sashite::Sin::Identifier.parse("S")
|
216
|
+
# shogi_gote = shogi_sente.flip # => Family=:S, Side=:second
|
103
217
|
def flip
|
104
|
-
|
105
|
-
self.class.new(new_letter, opposite_side)
|
218
|
+
self.class.new(family, opposite_side)
|
106
219
|
end
|
107
220
|
|
108
|
-
# Create a new identifier with a different
|
221
|
+
# Create a new identifier with a different Style Family (keeping same Player Assignment)
|
222
|
+
#
|
223
|
+
# Changes the Style Family component while preserving Player Assignment.
|
224
|
+
#
|
225
|
+
# @param new_family [Symbol] new Style Family (:A to :Z)
|
226
|
+
# @return [Identifier] new immutable identifier instance with different Style Family
|
109
227
|
#
|
110
|
-
# @
|
111
|
-
#
|
112
|
-
#
|
113
|
-
#
|
114
|
-
|
115
|
-
|
116
|
-
|
228
|
+
# @example Change Style Family while preserving Player Assignment
|
229
|
+
# chess_white = Sashite::Sin::Identifier.parse("C") # Chess, first player
|
230
|
+
# shogi_white = chess_white.with_family(:S) # Shōgi, first player
|
231
|
+
#
|
232
|
+
# chess_black = Sashite::Sin::Identifier.parse("c") # Chess, second player
|
233
|
+
# xiangqi_black = chess_black.with_family(:X) # Xiangqi, second player
|
234
|
+
def with_family(new_family)
|
235
|
+
self.class.validate_family(new_family)
|
236
|
+
return self if family == new_family
|
117
237
|
|
118
|
-
|
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)
|
238
|
+
self.class.new(new_family, side)
|
121
239
|
end
|
122
240
|
|
123
|
-
# Create a new identifier with a different
|
241
|
+
# Create a new identifier with a different Player Assignment (keeping same Style Family)
|
242
|
+
#
|
243
|
+
# Changes the Player Assignment component while preserving Style Family.
|
124
244
|
#
|
125
245
|
# @param new_side [Symbol] :first or :second
|
126
|
-
# @return [Identifier] new immutable identifier instance with different
|
127
|
-
#
|
128
|
-
#
|
246
|
+
# @return [Identifier] new immutable identifier instance with different Player Assignment
|
247
|
+
#
|
248
|
+
# @example Change Player Assignment within same Style Family
|
249
|
+
# chess_white = Sashite::Sin::Identifier.parse("C") # Chess, first player
|
250
|
+
# chess_black = chess_white.with_side(:second) # Chess, second player
|
251
|
+
#
|
252
|
+
# shogi_sente = Sashite::Sin::Identifier.parse("S") # Shōgi, first player
|
253
|
+
# shogi_gote = shogi_sente.with_side(:second) # Shōgi, second player
|
129
254
|
def with_side(new_side)
|
130
255
|
self.class.validate_side(new_side)
|
131
256
|
return self if side == new_side
|
132
257
|
|
133
|
-
|
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)
|
258
|
+
self.class.new(family, new_side)
|
136
259
|
end
|
137
260
|
|
138
261
|
# Check if the identifier belongs to the first player
|
139
262
|
#
|
140
|
-
# @return [Boolean] true if first player
|
263
|
+
# @return [Boolean] true if first player (uppercase letter)
|
264
|
+
#
|
265
|
+
# @example Player identification
|
266
|
+
# Sashite::Sin::Identifier.parse("C").first_player? # => true
|
267
|
+
# Sashite::Sin::Identifier.parse("c").first_player? # => false
|
141
268
|
def first_player?
|
142
269
|
side == FIRST_PLAYER
|
143
270
|
end
|
144
271
|
|
145
272
|
# Check if the identifier belongs to the second player
|
146
273
|
#
|
147
|
-
# @return [Boolean] true if second player
|
274
|
+
# @return [Boolean] true if second player (lowercase letter)
|
275
|
+
#
|
276
|
+
# @example Player identification
|
277
|
+
# Sashite::Sin::Identifier.parse("c").second_player? # => true
|
278
|
+
# Sashite::Sin::Identifier.parse("C").second_player? # => false
|
148
279
|
def second_player?
|
149
280
|
side == SECOND_PLAYER
|
150
281
|
end
|
151
282
|
|
152
|
-
# Check if this identifier has the same
|
283
|
+
# Check if this identifier has the same Style Family as another
|
284
|
+
#
|
285
|
+
# Compares the Style Family component, ignoring Player Assignment.
|
286
|
+
# This is useful for identifying pieces from the same game tradition in cross-style scenarios.
|
153
287
|
#
|
154
288
|
# @param other [Identifier] identifier to compare with
|
155
|
-
# @return [Boolean] true if both identifiers use the same
|
156
|
-
#
|
157
|
-
#
|
158
|
-
|
289
|
+
# @return [Boolean] true if both identifiers use the same Style Family
|
290
|
+
#
|
291
|
+
# @example Compare Style Families across different Player Assignments
|
292
|
+
# chess_white = Sashite::Sin::Identifier.parse("C") # Chess, first player
|
293
|
+
# chess_black = Sashite::Sin::Identifier.parse("c") # Chess, second player
|
294
|
+
# shogi_white = Sashite::Sin::Identifier.parse("S") # Shōgi, first player
|
295
|
+
#
|
296
|
+
# chess_white.same_family?(chess_black) # => true (both Chess family)
|
297
|
+
# chess_white.same_family?(shogi_white) # => false (different families)
|
298
|
+
def same_family?(other)
|
159
299
|
return false unless other.is_a?(self.class)
|
160
300
|
|
161
|
-
|
301
|
+
family == other.family
|
162
302
|
end
|
163
303
|
|
164
|
-
# Check if this identifier belongs to the same
|
304
|
+
# Check if this identifier belongs to the same Player Assignment as another
|
305
|
+
#
|
306
|
+
# Compares the Player Assignment component of identifiers across different Style Families.
|
307
|
+
# This is useful for grouping pieces by controlling player in multi-style games.
|
165
308
|
#
|
166
309
|
# @param other [Identifier] identifier to compare with
|
167
|
-
# @return [Boolean] true if both identifiers belong to the same
|
310
|
+
# @return [Boolean] true if both identifiers belong to the same Player Assignment
|
311
|
+
#
|
312
|
+
# @example Compare Player Assignments across different Style Families
|
313
|
+
# chess_white = Sashite::Sin::Identifier.parse("C") # Chess, first player
|
314
|
+
# shogi_white = Sashite::Sin::Identifier.parse("S") # Shōgi, first player
|
315
|
+
# chess_black = Sashite::Sin::Identifier.parse("c") # Chess, second player
|
316
|
+
#
|
317
|
+
# chess_white.same_side?(shogi_white) # => true (both first player)
|
318
|
+
# chess_white.same_side?(chess_black) # => false (different players)
|
168
319
|
def same_side?(other)
|
169
320
|
return false unless other.is_a?(self.class)
|
170
321
|
|
171
322
|
side == other.side
|
172
323
|
end
|
173
324
|
|
325
|
+
# Compatibility alias for same_family? to maintain API consistency
|
326
|
+
#
|
327
|
+
# @deprecated Use {#same_family?} instead for clearer semantics
|
328
|
+
# @param other [Identifier] identifier to compare with
|
329
|
+
# @return [Boolean] true if both identifiers use the same Style Family
|
330
|
+
def same_letter?(other)
|
331
|
+
same_family?(other)
|
332
|
+
end
|
333
|
+
|
174
334
|
# Custom equality comparison
|
175
335
|
#
|
336
|
+
# Two identifiers are equal if they have identical Family and Side attributes.
|
337
|
+
#
|
176
338
|
# @param other [Object] object to compare with
|
177
|
-
# @return [Boolean] true if both objects are identifiers with identical
|
339
|
+
# @return [Boolean] true if both objects are identifiers with identical Family and Side
|
340
|
+
#
|
341
|
+
# @example Equality comparison
|
342
|
+
# id1 = Sashite::Sin::Identifier.parse("C")
|
343
|
+
# id2 = Sashite::Sin::Identifier.parse("C")
|
344
|
+
# id3 = Sashite::Sin::Identifier.parse("c")
|
345
|
+
#
|
346
|
+
# id1 == id2 # => true (identical Family and Side)
|
347
|
+
# id1 == id3 # => false (different Player Assignment)
|
178
348
|
def ==(other)
|
179
349
|
return false unless other.is_a?(self.class)
|
180
350
|
|
181
|
-
|
351
|
+
family == other.family && side == other.side
|
182
352
|
end
|
183
353
|
|
184
354
|
# Alias for == to ensure Set functionality works correctly
|
@@ -186,22 +356,22 @@ module Sashite
|
|
186
356
|
|
187
357
|
# Custom hash implementation for use in collections
|
188
358
|
#
|
189
|
-
# @return [Integer] hash value based on class,
|
359
|
+
# @return [Integer] hash value based on class, Family, and Side
|
190
360
|
def hash
|
191
|
-
[self.class,
|
361
|
+
[self.class, family, side].hash
|
192
362
|
end
|
193
363
|
|
194
|
-
# Validate that the
|
364
|
+
# Validate that the family is a valid Style Family symbol
|
195
365
|
#
|
196
|
-
# @param
|
366
|
+
# @param family [Symbol] the family to validate
|
197
367
|
# @raise [ArgumentError] if invalid
|
198
|
-
def self.
|
199
|
-
return if
|
368
|
+
def self.validate_family(family)
|
369
|
+
return if VALID_FAMILIES.include?(family)
|
200
370
|
|
201
|
-
raise ::ArgumentError, format(
|
371
|
+
raise ::ArgumentError, format(ERROR_INVALID_FAMILY, family.inspect)
|
202
372
|
end
|
203
373
|
|
204
|
-
# Validate that the side
|
374
|
+
# Validate that the side follows SIN two-player constraint
|
205
375
|
#
|
206
376
|
# @param side [Symbol] the side to validate
|
207
377
|
# @raise [ArgumentError] if invalid
|
@@ -211,21 +381,7 @@ module Sashite
|
|
211
381
|
raise ::ArgumentError, format(ERROR_INVALID_SIDE, side.inspect)
|
212
382
|
end
|
213
383
|
|
214
|
-
#
|
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?(SIN_PATTERN)
|
226
|
-
end
|
227
|
-
|
228
|
-
# Validate SIN string format
|
384
|
+
# Validate SIN string format against specification grammar
|
229
385
|
#
|
230
386
|
# @param string [String] string to validate
|
231
387
|
# @raise [ArgumentError] if string doesn't match SIN pattern
|
@@ -235,11 +391,11 @@ module Sashite
|
|
235
391
|
raise ::ArgumentError, format(ERROR_INVALID_SIN, string)
|
236
392
|
end
|
237
393
|
|
238
|
-
private_class_method :
|
394
|
+
private_class_method :validate_sin_string
|
239
395
|
|
240
396
|
private
|
241
397
|
|
242
|
-
# Get the opposite
|
398
|
+
# Get the opposite Player Assignment
|
243
399
|
#
|
244
400
|
# @return [Symbol] the opposite side
|
245
401
|
def opposite_side
|
data/lib/sashite/sin.rb
CHANGED
@@ -5,22 +5,118 @@ require_relative "sin/identifier"
|
|
5
5
|
module Sashite
|
6
6
|
# SIN (Style Identifier Notation) implementation for Ruby
|
7
7
|
#
|
8
|
-
# Provides a
|
9
|
-
# SIN uses single
|
10
|
-
#
|
11
|
-
#
|
12
|
-
#
|
13
|
-
#
|
14
|
-
#
|
15
|
-
#
|
16
|
-
#
|
17
|
-
#
|
18
|
-
#
|
19
|
-
#
|
20
|
-
#
|
21
|
-
#
|
22
|
-
#
|
23
|
-
#
|
8
|
+
# Provides a compact, ASCII-based format for identifying styles in abstract strategy board games.
|
9
|
+
# SIN uses single-character identifiers with case encoding to represent both style identity
|
10
|
+
# and player assignment simultaneously.
|
11
|
+
#
|
12
|
+
# ## Concept
|
13
|
+
#
|
14
|
+
# SIN addresses the fundamental need to identify which style system governs piece behavior
|
15
|
+
# while simultaneously indicating which player controls pieces of that style. In cross-style
|
16
|
+
# scenarios where different players use different game traditions, this dual encoding becomes
|
17
|
+
# essential for unambiguous piece identification.
|
18
|
+
#
|
19
|
+
# ## Dual-Purpose Encoding
|
20
|
+
#
|
21
|
+
# Each SIN identifier serves two functions:
|
22
|
+
# - **Style Family Identification**: The letter choice indicates which rule system applies
|
23
|
+
# - **Player Assignment**: The letter case indicates which player uses this style as their native system
|
24
|
+
#
|
25
|
+
# ## Format Specification
|
26
|
+
#
|
27
|
+
# Structure: `<style-letter>`
|
28
|
+
#
|
29
|
+
# Grammar (BNF):
|
30
|
+
# <sin> ::= <uppercase-letter> | <lowercase-letter>
|
31
|
+
# <uppercase-letter> ::= "A" | "B" | "C" | ... | "Z"
|
32
|
+
# <lowercase-letter> ::= "a" | "b" | "c" | ... | "z"
|
33
|
+
#
|
34
|
+
# Regular Expression: `/\A[A-Za-z]\z/`
|
35
|
+
#
|
36
|
+
# ## Style Attribute Mapping
|
37
|
+
#
|
38
|
+
# SIN encodes style attributes using the following correspondence:
|
39
|
+
#
|
40
|
+
# | Style Attribute | SIN Encoding | Examples |
|
41
|
+
# |-----------------|--------------|----------|
|
42
|
+
# | **Letter** | Single ASCII character | `C`, `c`, `S`, `s` |
|
43
|
+
# | **Style Family** | ASCII letter choice (A-Z) | `C`/`c` = Chess, `S`/`s` = Shōgi |
|
44
|
+
# | **Player Assignment** | Letter case | `C` = First player, `c` = Second player |
|
45
|
+
#
|
46
|
+
# The **Letter** attribute combines two distinct semantic components:
|
47
|
+
# - **Style Family**: The underlying ASCII character (A-Z), representing the game tradition or rule system
|
48
|
+
# - **Player Assignment**: The case of the character (uppercase/lowercase), representing which player uses this style
|
49
|
+
#
|
50
|
+
# ## Character Selection Conventions
|
51
|
+
#
|
52
|
+
# ### Primary Convention: First Letter
|
53
|
+
# By convention, SIN identifiers should preferably use the **first letter** of the corresponding SNN style name:
|
54
|
+
# - `Chess` → `C`/`c`
|
55
|
+
# - `Shogi` → `S`/`s`
|
56
|
+
# - `Xiangqi` → `X`/`x`
|
57
|
+
# - `Makruk` → `M`/`m`
|
58
|
+
# - `Janggi` → `J`/`j`
|
59
|
+
#
|
60
|
+
# ### Collision Resolution
|
61
|
+
# When multiple styles would claim the same first letter, systematic collision resolution applies
|
62
|
+
# using sequential letter selection from the SNN name until a unique identifier is found.
|
63
|
+
#
|
64
|
+
# ### Compatibility Groups
|
65
|
+
# Styles requiring incompatible board structures can safely share SIN letters since they
|
66
|
+
# cannot coexist in the same match.
|
67
|
+
#
|
68
|
+
# ## System Constraints
|
69
|
+
#
|
70
|
+
# - **26 possible identifiers** per player using ASCII letters
|
71
|
+
# - **Exactly 2 players** through case distinction:
|
72
|
+
# - First player: Uppercase letters (`A-Z`)
|
73
|
+
# - Second player: Lowercase letters (`a-z`)
|
74
|
+
# - **Single character** per style-player combination
|
75
|
+
# - **Rule-agnostic** - independent of specific game mechanics
|
76
|
+
#
|
77
|
+
# ## Examples
|
78
|
+
#
|
79
|
+
# ### Traditional Game Styles
|
80
|
+
#
|
81
|
+
# # Chess (8×8)
|
82
|
+
# chess_white = Sashite::Sin.parse("C") # First player (White pieces)
|
83
|
+
# chess_black = Sashite::Sin.parse("c") # Second player (Black pieces)
|
84
|
+
#
|
85
|
+
# # Shōgi (9×9)
|
86
|
+
# shogi_sente = Sashite::Sin.parse("S") # First player (Sente 先手)
|
87
|
+
# shogi_gote = Sashite::Sin.parse("s") # Second player (Gote 後手)
|
88
|
+
#
|
89
|
+
# # Xiangqi (9×10)
|
90
|
+
# xiangqi_red = Sashite::Sin.parse("X") # First player (Red pieces)
|
91
|
+
# xiangqi_black = Sashite::Sin.parse("x") # Second player (Black pieces)
|
92
|
+
#
|
93
|
+
# ### Cross-Style Scenarios
|
94
|
+
#
|
95
|
+
# # Chess vs. Ōgi Match (both 8×8 compatible)
|
96
|
+
# chess_style = Sashite::Sin.parse("C") # Chess style, first player
|
97
|
+
# ogi_style = Sashite::Sin.parse("o") # Ōgi style, second player
|
98
|
+
#
|
99
|
+
# ### All 26 Letters
|
100
|
+
#
|
101
|
+
# # First player identifiers (A-Z)
|
102
|
+
# ("A".."Z").each { |letter| Sashite::Sin.parse(letter).first_player? } # => all true
|
103
|
+
#
|
104
|
+
# # Second player identifiers (a-z)
|
105
|
+
# ("a".."z").each { |letter| Sashite::Sin.parse(letter).second_player? } # => all true
|
106
|
+
#
|
107
|
+
# ## Design Properties
|
108
|
+
#
|
109
|
+
# - **ASCII compatibility**: Maximum portability across systems
|
110
|
+
# - **Rule-agnostic**: Independent of specific game mechanics
|
111
|
+
# - **Minimal overhead**: Single character per style-player combination
|
112
|
+
# - **Flexible collision resolution**: Systematic approaches for identifier conflicts
|
113
|
+
# - **Semantic clarity**: Distinct concepts for Letter (Style Family + Player Assignment)
|
114
|
+
# - **SNN coordination**: Works harmoniously with formal style naming
|
115
|
+
# - **Context-aware**: Adapts to avoid conflicts within specific game scenarios
|
116
|
+
#
|
117
|
+
# @see https://sashite.dev/specs/sin/1.0.0/ SIN Specification v1.0.0
|
118
|
+
# @see https://sashite.dev/specs/sin/1.0.0/examples/ SIN Examples
|
119
|
+
# @see https://sashite.dev/specs/snn/ Style Name Notation (SNN)
|
24
120
|
module Sin
|
25
121
|
# Check if a string is a valid SIN notation
|
26
122
|
#
|
@@ -28,38 +124,65 @@ module Sashite
|
|
28
124
|
# @return [Boolean] true if valid SIN, false otherwise
|
29
125
|
#
|
30
126
|
# @example Validate various SIN formats
|
31
|
-
# Sashite::Sin.valid?("C")
|
32
|
-
# Sashite::Sin.valid?("c")
|
33
|
-
# Sashite::Sin.valid?("
|
34
|
-
# Sashite::Sin.valid?("
|
127
|
+
# Sashite::Sin.valid?("C") # => true (Chess first player)
|
128
|
+
# Sashite::Sin.valid?("c") # => true (Chess second player)
|
129
|
+
# Sashite::Sin.valid?("S") # => true (Shōgi first player)
|
130
|
+
# Sashite::Sin.valid?("s") # => true (Shōgi second player)
|
131
|
+
# Sashite::Sin.valid?("CHESS") # => false (multi-character)
|
132
|
+
# Sashite::Sin.valid?("1") # => false (not a letter)
|
133
|
+
# Sashite::Sin.valid?("") # => false (empty string)
|
35
134
|
def self.valid?(sin_string)
|
36
135
|
Identifier.valid?(sin_string)
|
37
136
|
end
|
38
137
|
|
39
138
|
# Parse an SIN string into an Identifier object
|
40
139
|
#
|
41
|
-
#
|
140
|
+
# The identifier will have both letter and side attributes inferred from the case:
|
141
|
+
# - Uppercase letter → first player (:first)
|
142
|
+
# - Lowercase letter → second player (:second)
|
143
|
+
#
|
144
|
+
# @param sin_string [String] SIN notation string (single ASCII letter)
|
42
145
|
# @return [Sin::Identifier] parsed identifier object with letter and side attributes
|
43
146
|
# @raise [ArgumentError] if the SIN string is invalid
|
44
|
-
#
|
45
|
-
#
|
46
|
-
# Sashite::Sin.parse("
|
47
|
-
# Sashite::Sin.parse("
|
147
|
+
#
|
148
|
+
# @example Parse different SIN formats with dual-purpose encoding
|
149
|
+
# Sashite::Sin.parse("C") # => #<Sashite::Sin::Identifier @family=:C, @side=:first>
|
150
|
+
# Sashite::Sin.parse("c") # => #<Sashite::Sin::Identifier @family=:C, @side=:second>
|
151
|
+
# Sashite::Sin.parse("S") # => #<Sashite::Sin::Identifier @family=:S, @side=:first>
|
152
|
+
# Sashite::Sin.parse("s") # => #<Sashite::Sin::Identifier @family=:S, @side=:second>
|
153
|
+
#
|
154
|
+
# @example Traditional game styles
|
155
|
+
# chess_white = Sashite::Sin.parse("C") # Chess, first player (White)
|
156
|
+
# chess_black = Sashite::Sin.parse("c") # Chess, second player (Black)
|
157
|
+
# shogi_sente = Sashite::Sin.parse("S") # Shōgi, first player (Sente)
|
158
|
+
# shogi_gote = Sashite::Sin.parse("s") # Shōgi, second player (Gote)
|
48
159
|
def self.parse(sin_string)
|
49
160
|
Identifier.parse(sin_string)
|
50
161
|
end
|
51
162
|
|
52
|
-
# Create a new identifier instance
|
163
|
+
# Create a new identifier instance with canonical representation
|
53
164
|
#
|
54
|
-
#
|
165
|
+
# Ensures the letter case matches the specified side:
|
166
|
+
# - :first side → uppercase letter
|
167
|
+
# - :second side → lowercase letter
|
168
|
+
#
|
169
|
+
# @param family [Symbol] style family (:A to :Z representing Style Family)
|
55
170
|
# @param side [Symbol] player side (:first or :second)
|
56
171
|
# @return [Sin::Identifier] new immutable identifier instance
|
57
172
|
# @raise [ArgumentError] if parameters are invalid
|
58
|
-
#
|
59
|
-
#
|
60
|
-
# Sashite::Sin.identifier(:
|
61
|
-
|
62
|
-
|
173
|
+
#
|
174
|
+
# @example Create identifiers with family and side separation
|
175
|
+
# Sashite::Sin.identifier(:C, :first) # => #<Sashite::Sin::Identifier @family=:C, @side=:first>
|
176
|
+
# Sashite::Sin.identifier(:C, :second) # => #<Sashite::Sin::Identifier @family=:C, @side=:second>
|
177
|
+
#
|
178
|
+
# @example Style family and player assignment
|
179
|
+
# chess_first = Sashite::Sin.identifier(:C, :first) # Chess family, first player
|
180
|
+
# chess_second = Sashite::Sin.identifier(:C, :second) # Chess family, second player
|
181
|
+
#
|
182
|
+
# chess_first.same_family?(chess_second) # => true (same style family)
|
183
|
+
# chess_first.same_side?(chess_second) # => false (different players)
|
184
|
+
def self.identifier(family, side)
|
185
|
+
Identifier.new(family, side)
|
63
186
|
end
|
64
187
|
end
|
65
188
|
end
|