sashite-sin 1.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 +7 -0
- data/LICENSE.md +22 -0
- data/README.md +348 -0
- data/lib/sashite/sin/style.rb +250 -0
- data/lib/sashite/sin.rb +65 -0
- data/lib/sashite-sin.rb +14 -0
- metadata +58 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 3411653c81ceb4f7711d40c8a924a2efc16e4573797dbd593d5e97cc95a5c643
|
4
|
+
data.tar.gz: 00776c4a29a213e1146cbbee42792c132706f64ac0b500c0389b3b0814fa9264
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: d818c870d4a9dc08f74da00acf89662c0e4794ba55bc658d4aca25a1895edc8ded12df5a83f4378375392c1576b12bbb29e47668732089b096fde659153a87da
|
7
|
+
data.tar.gz: 2ebe9efb770a4ca03319d74bc53965dc40d7e5778663a6b4d4a1ab6f7f07a7ef338a313e50f664a49068f0593a88ff5b893f62a3b91c7a88b1fe5a73729bd9ef
|
data/LICENSE.md
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2025 Cyril Kato
|
2
|
+
|
3
|
+
MIT License
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
6
|
+
a copy of this software and associated documentation files (the
|
7
|
+
"Software"), to deal in the Software without restriction, including
|
8
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
9
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
10
|
+
permit persons to whom the Software is furnished to do so, subject to
|
11
|
+
the following conditions:
|
12
|
+
|
13
|
+
The above copyright notice and this permission notice shall be
|
14
|
+
included in all copies or substantial portions of the Software.
|
15
|
+
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
19
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
20
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
21
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
22
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,348 @@
|
|
1
|
+
# Sin.rb
|
2
|
+
|
3
|
+
[](https://github.com/sashite/sin.rb/tags)
|
4
|
+
[](https://rubydoc.info/github/sashite/sin.rb/main)
|
5
|
+

|
6
|
+
[](https://github.com/sashite/sin.rb/raw/main/LICENSE.md)
|
7
|
+
|
8
|
+
> **SIN** (Style Identifier Notation) implementation for the Ruby language.
|
9
|
+
|
10
|
+
## What is SIN?
|
11
|
+
|
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
|
+
|
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.
|
15
|
+
|
16
|
+
## Installation
|
17
|
+
|
18
|
+
```ruby
|
19
|
+
# In your Gemfile
|
20
|
+
gem "sashite-sin"
|
21
|
+
```
|
22
|
+
|
23
|
+
Or install manually:
|
24
|
+
|
25
|
+
```sh
|
26
|
+
gem install sashite-sin
|
27
|
+
```
|
28
|
+
|
29
|
+
## Usage
|
30
|
+
|
31
|
+
### Basic Operations
|
32
|
+
|
33
|
+
```ruby
|
34
|
+
require "sashite/sin"
|
35
|
+
|
36
|
+
# Parse SIN strings into style objects
|
37
|
+
style = Sashite::Sin.parse("C") # => #<Sin::Style letter=:C side=:first>
|
38
|
+
style.to_s # => "C"
|
39
|
+
style.letter # => :C
|
40
|
+
style.side # => :first
|
41
|
+
|
42
|
+
# Create styles directly
|
43
|
+
style = Sashite::Sin.style(:C, :first) # => #<Sin::Style letter=:C side=:first>
|
44
|
+
style = Sashite::Sin::Style.new(:c, :second) # => #<Sin::Style letter=:c side=:second>
|
45
|
+
|
46
|
+
# Validate SIN strings
|
47
|
+
Sashite::Sin.valid?("C") # => true
|
48
|
+
Sashite::Sin.valid?("c") # => true
|
49
|
+
Sashite::Sin.valid?("1") # => false (not a letter)
|
50
|
+
Sashite::Sin.valid?("CC") # => false (not single character)
|
51
|
+
```
|
52
|
+
|
53
|
+
### Style Transformations
|
54
|
+
|
55
|
+
```ruby
|
56
|
+
# All transformations return new immutable instances
|
57
|
+
style = Sashite::Sin.parse("C")
|
58
|
+
|
59
|
+
# Flip player assignment
|
60
|
+
flipped = style.flip # => #<Sin::Style letter=:c side=:second>
|
61
|
+
flipped.to_s # => "c"
|
62
|
+
|
63
|
+
# Change letter
|
64
|
+
changed = style.with_letter(:S) # => #<Sin::Style letter=:S side=:first>
|
65
|
+
changed.to_s # => "S"
|
66
|
+
|
67
|
+
# Change side
|
68
|
+
other_side = style.with_side(:second) # => #<Sin::Style letter=:c side=:second>
|
69
|
+
other_side.to_s # => "c"
|
70
|
+
|
71
|
+
# Chain transformations
|
72
|
+
result = style.flip.with_letter(:M) # => #<Sin::Style letter=:m side=:second>
|
73
|
+
result.to_s # => "m"
|
74
|
+
```
|
75
|
+
|
76
|
+
### Player and Style Queries
|
77
|
+
|
78
|
+
```ruby
|
79
|
+
style = Sashite::Sin.parse("C")
|
80
|
+
opposite = Sashite::Sin.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::Sin.parse("C")
|
90
|
+
chess2 = Sashite::Sin.parse("c")
|
91
|
+
shogi = Sashite::Sin.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)
|
96
|
+
```
|
97
|
+
|
98
|
+
### Style Collections
|
99
|
+
|
100
|
+
```ruby
|
101
|
+
# Working with multiple styles
|
102
|
+
styles = %w[C c S s M m].map { |sin| Sashite::Sin.parse(sin) }
|
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
|
+
```
|
116
|
+
|
117
|
+
## Format Specification
|
118
|
+
|
119
|
+
### Structure
|
120
|
+
```
|
121
|
+
<style-letter>
|
122
|
+
```
|
123
|
+
|
124
|
+
### Grammar (BNF)
|
125
|
+
```bnf
|
126
|
+
<sin> ::= <uppercase-letter> | <lowercase-letter>
|
127
|
+
|
128
|
+
<uppercase-letter> ::= "A" | "B" | "C" | ... | "Z"
|
129
|
+
<lowercase-letter> ::= "a" | "b" | "c" | ... | "z"
|
130
|
+
```
|
131
|
+
|
132
|
+
### Regular Expression
|
133
|
+
```ruby
|
134
|
+
/\A[A-Za-z]\z/
|
135
|
+
```
|
136
|
+
|
137
|
+
### Style Attribute Mapping
|
138
|
+
|
139
|
+
| Style Attribute | SIN Encoding | Examples |
|
140
|
+
|-----------------|--------------|----------|
|
141
|
+
| **Style Family** | Letter choice | `C`/`c` = Chess family |
|
142
|
+
| **Player Assignment** | Letter case | `C` = First player, `c` = Second player |
|
143
|
+
|
144
|
+
## Game Examples
|
145
|
+
|
146
|
+
The SIN specification is rule-agnostic and does not define specific letter assignments. However, here are common usage patterns:
|
147
|
+
|
148
|
+
### Traditional Game Families
|
149
|
+
|
150
|
+
```ruby
|
151
|
+
# Chess family styles
|
152
|
+
chess_white = Sashite::Sin.parse("C") # First player, Chess family
|
153
|
+
chess_black = Sashite::Sin.parse("c") # Second player, Chess family
|
154
|
+
|
155
|
+
# Shōgi family styles
|
156
|
+
shogi_sente = Sashite::Sin.parse("S") # First player, Shōgi family
|
157
|
+
shogi_gote = Sashite::Sin.parse("s") # Second player, Shōgi family
|
158
|
+
|
159
|
+
# Xiangqi family styles
|
160
|
+
xiangqi_red = Sashite::Sin.parse("X") # First player, Xiangqi family
|
161
|
+
xiangqi_black = Sashite::Sin.parse("x") # Second player, Xiangqi family
|
162
|
+
```
|
163
|
+
|
164
|
+
### Cross-Style Scenarios
|
165
|
+
|
166
|
+
```ruby
|
167
|
+
# Different families in one match
|
168
|
+
def create_hybrid_match
|
169
|
+
[
|
170
|
+
Sashite::Sin.parse("C"), # First player uses Chess family
|
171
|
+
Sashite::Sin.parse("s") # Second player uses Shōgi family
|
172
|
+
]
|
173
|
+
end
|
174
|
+
|
175
|
+
styles = create_hybrid_match
|
176
|
+
styles[0].same_side?(styles[1]) # => false (different players)
|
177
|
+
styles[0].same_letter?(styles[1]) # => false (different families)
|
178
|
+
```
|
179
|
+
|
180
|
+
### Variant Families
|
181
|
+
|
182
|
+
```ruby
|
183
|
+
# Different letters can represent variants within traditions
|
184
|
+
makruk = Sashite::Sin.parse("M") # Makruk (Thai Chess) family
|
185
|
+
janggi = Sashite::Sin.parse("J") # Janggi (Korean Chess) family
|
186
|
+
ogi = Sashite::Sin.parse("O") # Ōgi (王棋) family
|
187
|
+
|
188
|
+
# Each family can have both players
|
189
|
+
makruk_black = makruk.flip # Second player Makruk
|
190
|
+
makruk_black.to_s # => "m"
|
191
|
+
```
|
192
|
+
|
193
|
+
## API Reference
|
194
|
+
|
195
|
+
### Main Module Methods
|
196
|
+
|
197
|
+
- `Sashite::Sin.valid?(sin_string)` - Check if string is valid SIN notation
|
198
|
+
- `Sashite::Sin.parse(sin_string)` - Parse SIN string into Style object
|
199
|
+
- `Sashite::Sin.style(letter, side)` - Create style instance directly
|
200
|
+
|
201
|
+
### Style Class
|
202
|
+
|
203
|
+
#### Creation and Parsing
|
204
|
+
- `Sashite::Sin::Style.new(letter, side)` - Create style instance
|
205
|
+
- `Sashite::Sin::Style.parse(sin_string)` - Parse SIN string
|
206
|
+
|
207
|
+
#### Attribute Access
|
208
|
+
- `#letter` - Get style letter (symbol :A through :z)
|
209
|
+
- `#side` - Get player side (:first or :second)
|
210
|
+
- `#to_s` - Convert to SIN string representation
|
211
|
+
|
212
|
+
#### Player Queries
|
213
|
+
- `#first_player?` - Check if first player style
|
214
|
+
- `#second_player?` - Check if second player style
|
215
|
+
|
216
|
+
#### Transformations (immutable - return new instances)
|
217
|
+
- `#flip` - Switch player assignment
|
218
|
+
- `#with_letter(new_letter)` - Create style with different letter
|
219
|
+
- `#with_side(new_side)` - Create style with different side
|
220
|
+
|
221
|
+
#### Comparison Methods
|
222
|
+
- `#same_letter?(other)` - Check if same style letter (case-insensitive)
|
223
|
+
- `#same_side?(other)` - Check if same player side
|
224
|
+
- `#==(other)` - Full equality comparison
|
225
|
+
|
226
|
+
### Style Class Constants
|
227
|
+
|
228
|
+
- `Sashite::Sin::Style::FIRST_PLAYER` - Symbol for first player (:first)
|
229
|
+
- `Sashite::Sin::Style::SECOND_PLAYER` - Symbol for second player (:second)
|
230
|
+
- `Sashite::Sin::Style::VALID_SIDES` - Array of valid sides
|
231
|
+
- `Sashite::Sin::Style::SIN_PATTERN` - Regular expression for SIN validation
|
232
|
+
|
233
|
+
## Advanced Usage
|
234
|
+
|
235
|
+
### Letter Case and Side Mapping
|
236
|
+
|
237
|
+
```ruby
|
238
|
+
# SIN encodes player assignment through case
|
239
|
+
upper_case_letters = ("A".."Z").map { |letter| Sashite::Sin.parse(letter) }
|
240
|
+
lower_case_letters = ("a".."z").map { |letter| Sashite::Sin.parse(letter) }
|
241
|
+
|
242
|
+
# All uppercase letters are first player
|
243
|
+
upper_case_letters.all?(&:first_player?) # => true
|
244
|
+
|
245
|
+
# All lowercase letters are second player
|
246
|
+
lower_case_letters.all?(&:second_player?) # => true
|
247
|
+
|
248
|
+
# Letter families are related by case
|
249
|
+
letter_a_first = Sashite::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
|
254
|
+
```
|
255
|
+
|
256
|
+
### Immutable Transformations
|
257
|
+
|
258
|
+
```ruby
|
259
|
+
# All transformations return new instances
|
260
|
+
original = Sashite::Sin.style(:C, :first)
|
261
|
+
flipped = original.flip
|
262
|
+
changed_letter = original.with_letter(:S)
|
263
|
+
|
264
|
+
# Original style is never modified
|
265
|
+
original.to_s # => "C" (unchanged)
|
266
|
+
flipped.to_s # => "c"
|
267
|
+
changed_letter.to_s # => "S"
|
268
|
+
|
269
|
+
# Transformations can be chained
|
270
|
+
result = original.flip.with_letter(:M).flip
|
271
|
+
result.to_s # => "M"
|
272
|
+
```
|
273
|
+
|
274
|
+
## 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
|
+
## Design Properties
|
291
|
+
|
292
|
+
- **ASCII compatibility**: Maximum portability across systems
|
293
|
+
- **Rule-agnostic**: Independent of specific game mechanics
|
294
|
+
- **Minimal overhead**: Single character per style-player combination
|
295
|
+
- **Canonical representation**: Each style-player combination has exactly one SIN identifier
|
296
|
+
- **Immutable**: All style instances are frozen and transformations return new objects
|
297
|
+
- **Functional**: Pure functions with no side effects
|
298
|
+
|
299
|
+
## Related Specifications
|
300
|
+
|
301
|
+
- [SIN Specification v1.0.0](https://sashite.dev/specs/sin/1.0.0/) - Complete technical specification
|
302
|
+
- [SIN Examples](https://sashite.dev/specs/sin/1.0.0/examples/) - Practical implementation examples
|
303
|
+
- [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
|
+
|
308
|
+
## Documentation
|
309
|
+
|
310
|
+
- [Official SIN Specification v1.0.0](https://sashite.dev/specs/sin/1.0.0/)
|
311
|
+
- [SIN Examples Documentation](https://sashite.dev/specs/sin/1.0.0/examples/)
|
312
|
+
- [Sashité Protocol Foundation](https://sashite.dev/protocol/)
|
313
|
+
- [API Documentation](https://rubydoc.info/github/sashite/sin.rb/main)
|
314
|
+
|
315
|
+
## Development
|
316
|
+
|
317
|
+
```sh
|
318
|
+
# Clone the repository
|
319
|
+
git clone https://github.com/sashite/sin.rb.git
|
320
|
+
cd sin.rb
|
321
|
+
|
322
|
+
# Install dependencies
|
323
|
+
bundle install
|
324
|
+
|
325
|
+
# Run tests
|
326
|
+
ruby test.rb
|
327
|
+
|
328
|
+
# Generate documentation
|
329
|
+
yard doc
|
330
|
+
```
|
331
|
+
|
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
|
+
## License
|
343
|
+
|
344
|
+
Available as open source under the [MIT License](https://opensource.org/licenses/MIT).
|
345
|
+
|
346
|
+
## About
|
347
|
+
|
348
|
+
Maintained by [Sashité](https://sashite.com/) — promoting chess variants and sharing the beauty of board game cultures.
|
@@ -0,0 +1,250 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Sashite
|
4
|
+
module Sin
|
5
|
+
# Represents a style in SIN (Style Identifier 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 SIN Specification v1.0.0 with Letter and Side attributes.
|
13
|
+
class Style
|
14
|
+
# SIN validation pattern matching the specification
|
15
|
+
SIN_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_SIN = "Invalid SIN 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 SIN string into a Style object
|
51
|
+
#
|
52
|
+
# @param sin_string [String] SIN notation string (single ASCII letter)
|
53
|
+
# @return [Style] parsed style object with letter and inferred side
|
54
|
+
# @raise [ArgumentError] if the SIN string is invalid
|
55
|
+
# @example Parse SIN strings with case-based side inference
|
56
|
+
# Sashite::Sin::Style.parse("C") # => #<Sin::Style letter=:C side=:first>
|
57
|
+
# Sashite::Sin::Style.parse("c") # => #<Sin::Style letter=:c side=:second>
|
58
|
+
# Sashite::Sin::Style.parse("S") # => #<Sin::Style letter=:S side=:first>
|
59
|
+
def self.parse(sin_string)
|
60
|
+
string_value = String(sin_string)
|
61
|
+
validate_sin_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 SIN notation
|
73
|
+
#
|
74
|
+
# @param sin_string [String] the string to validate
|
75
|
+
# @return [Boolean] true if valid SIN, false otherwise
|
76
|
+
#
|
77
|
+
# @example Validate SIN strings
|
78
|
+
# Sashite::Sin::Style.valid?("C") # => true
|
79
|
+
# Sashite::Sin::Style.valid?("c") # => true
|
80
|
+
# Sashite::Sin::Style.valid?("CHESS") # => false (multi-character)
|
81
|
+
def self.valid?(sin_string)
|
82
|
+
return false unless sin_string.is_a?(::String)
|
83
|
+
|
84
|
+
sin_string.match?(SIN_PATTERN)
|
85
|
+
end
|
86
|
+
|
87
|
+
# Convert the style to its SIN string representation
|
88
|
+
#
|
89
|
+
# @return [String] SIN 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?(SIN_PATTERN)
|
226
|
+
end
|
227
|
+
|
228
|
+
# Validate SIN string format
|
229
|
+
#
|
230
|
+
# @param string [String] string to validate
|
231
|
+
# @raise [ArgumentError] if string doesn't match SIN pattern
|
232
|
+
def self.validate_sin_string(string)
|
233
|
+
return if string.match?(SIN_PATTERN)
|
234
|
+
|
235
|
+
raise ::ArgumentError, format(ERROR_INVALID_SIN, string)
|
236
|
+
end
|
237
|
+
|
238
|
+
private_class_method :valid_letter?, :validate_sin_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
|
data/lib/sashite/sin.rb
ADDED
@@ -0,0 +1,65 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "sin/style"
|
4
|
+
|
5
|
+
module Sashite
|
6
|
+
# SIN (Style Identifier Notation) implementation for Ruby
|
7
|
+
#
|
8
|
+
# Provides a rule-agnostic format for identifying styles in abstract strategy board games.
|
9
|
+
# SIN uses single ASCII letters with case-based side encoding, enabling clear
|
10
|
+
# distinction between different style families in multi-style gaming environments.
|
11
|
+
#
|
12
|
+
# Format: <style-letter>
|
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 SIN identifier is exactly one ASCII letter
|
16
|
+
#
|
17
|
+
# Examples:
|
18
|
+
# "C" - First player, C style family
|
19
|
+
# "c" - Second player, C style family
|
20
|
+
# "S" - First player, S style family
|
21
|
+
# "s" - Second player, S style family
|
22
|
+
#
|
23
|
+
# See: https://sashite.dev/specs/sin/1.0.0/
|
24
|
+
module Sin
|
25
|
+
# Check if a string is a valid SIN notation
|
26
|
+
#
|
27
|
+
# @param sin_string [String] the string to validate
|
28
|
+
# @return [Boolean] true if valid SIN, false otherwise
|
29
|
+
#
|
30
|
+
# @example Validate various SIN formats
|
31
|
+
# Sashite::Sin.valid?("C") # => true
|
32
|
+
# Sashite::Sin.valid?("c") # => true
|
33
|
+
# Sashite::Sin.valid?("CHESS") # => false (multi-character)
|
34
|
+
# Sashite::Sin.valid?("1") # => false (not a letter)
|
35
|
+
def self.valid?(sin_string)
|
36
|
+
Style.valid?(sin_string)
|
37
|
+
end
|
38
|
+
|
39
|
+
# Parse an SIN string into a Style object
|
40
|
+
#
|
41
|
+
# @param sin_string [String] SIN notation string
|
42
|
+
# @return [Sin::Style] parsed style object with letter and side attributes
|
43
|
+
# @raise [ArgumentError] if the SIN string is invalid
|
44
|
+
# @example Parse different SIN formats
|
45
|
+
# Sashite::Sin.parse("C") # => #<Sin::Style letter=:C side=:first>
|
46
|
+
# Sashite::Sin.parse("c") # => #<Sin::Style letter=:c side=:second>
|
47
|
+
# Sashite::Sin.parse("S") # => #<Sin::Style letter=:S side=:first>
|
48
|
+
def self.parse(sin_string)
|
49
|
+
Style.parse(sin_string)
|
50
|
+
end
|
51
|
+
|
52
|
+
# Create a new style instance
|
53
|
+
#
|
54
|
+
# @param letter [Symbol] style letter (single ASCII letter as symbol)
|
55
|
+
# @param side [Symbol] player side (:first or :second)
|
56
|
+
# @return [Sin::Style] new immutable style instance
|
57
|
+
# @raise [ArgumentError] if parameters are invalid
|
58
|
+
# @example Create styles directly
|
59
|
+
# Sashite::Sin.style(:C, :first) # => #<Sin::Style letter=:C side=:first>
|
60
|
+
# Sashite::Sin.style(:s, :second) # => #<Sin::Style letter=:s side=:second>
|
61
|
+
def self.style(letter, side)
|
62
|
+
Style.new(letter, side)
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
data/lib/sashite-sin.rb
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "sashite/sin"
|
4
|
+
|
5
|
+
# Sashité namespace for board game notation libraries
|
6
|
+
#
|
7
|
+
# Sashité provides a collection of libraries for representing and manipulating
|
8
|
+
# board game concepts according to the Sashité Protocol specifications.
|
9
|
+
#
|
10
|
+
# @see https://sashite.dev/protocol/ Sashité Protocol
|
11
|
+
# @see https://sashite.dev/specs/ Sashité Specifications
|
12
|
+
# @author Sashité
|
13
|
+
module Sashite
|
14
|
+
end
|
metadata
ADDED
@@ -0,0 +1,58 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: sashite-sin
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 1.0.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Cyril Kato
|
8
|
+
bindir: bin
|
9
|
+
cert_chain: []
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
11
|
+
dependencies: []
|
12
|
+
description: |
|
13
|
+
SIN (Style Identifier Notation) provides a rule-agnostic format for identifying styles
|
14
|
+
in abstract strategy board games. This gem implements the SIN Specification v1.0.0 with
|
15
|
+
a modern Ruby interface featuring immutable style objects and functional programming
|
16
|
+
principles. SIN uses single ASCII letters with case-based side encoding (A-Z for first player,
|
17
|
+
a-z for second player), enabling clear distinction between different style families in
|
18
|
+
multi-style gaming environments. Perfect for cross-style matches, game engines, and hybrid
|
19
|
+
gaming systems requiring compact style identification.
|
20
|
+
email: contact@cyril.email
|
21
|
+
executables: []
|
22
|
+
extensions: []
|
23
|
+
extra_rdoc_files: []
|
24
|
+
files:
|
25
|
+
- LICENSE.md
|
26
|
+
- README.md
|
27
|
+
- lib/sashite-sin.rb
|
28
|
+
- lib/sashite/sin.rb
|
29
|
+
- lib/sashite/sin/style.rb
|
30
|
+
homepage: https://github.com/sashite/sin.rb
|
31
|
+
licenses:
|
32
|
+
- MIT
|
33
|
+
metadata:
|
34
|
+
bug_tracker_uri: https://github.com/sashite/sin.rb/issues
|
35
|
+
documentation_uri: https://rubydoc.info/github/sashite/sin.rb/main
|
36
|
+
homepage_uri: https://github.com/sashite/sin.rb
|
37
|
+
source_code_uri: https://github.com/sashite/sin.rb
|
38
|
+
specification_uri: https://sashite.dev/specs/sin/1.0.0/
|
39
|
+
rubygems_mfa_required: 'true'
|
40
|
+
rdoc_options: []
|
41
|
+
require_paths:
|
42
|
+
- lib
|
43
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - ">="
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: 3.2.0
|
48
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
49
|
+
requirements:
|
50
|
+
- - ">="
|
51
|
+
- !ruby/object:Gem::Version
|
52
|
+
version: '0'
|
53
|
+
requirements: []
|
54
|
+
rubygems_version: 3.6.9
|
55
|
+
specification_version: 4
|
56
|
+
summary: SIN (Style Identifier Notation) implementation for Ruby with immutable style
|
57
|
+
objects
|
58
|
+
test_files: []
|