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 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
+ [![Version](https://img.shields.io/github/v/tag/sashite/sin.rb?label=Version&logo=github)](https://github.com/sashite/sin.rb/tags)
4
+ [![Yard documentation](https://img.shields.io/badge/Yard-documentation-blue.svg?logo=github)](https://rubydoc.info/github/sashite/sin.rb/main)
5
+ ![Ruby](https://github.com/sashite/sin.rb/actions/workflows/main.yml/badge.svg?branch=main)
6
+ [![License](https://img.shields.io/github/license/sashite/sin.rb?label=License&logo=github)](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
@@ -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
@@ -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: []