Skip to content

Commit b46340f

Browse files
committed
feat(sin): first commit
0 parents  commit b46340f

10 files changed

Lines changed: 1217 additions & 0 deletions

File tree

.formatter.exs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
# Used by "mix format"
2+
[
3+
inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"]
4+
]

.github/workflows/elixir.yml

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
name: CI
2+
3+
on:
4+
push:
5+
branches: [main]
6+
pull_request:
7+
branches: [main]
8+
9+
jobs:
10+
test:
11+
runs-on: ubuntu-latest
12+
name: Test Elixir ${{matrix.elixir}} / OTP ${{matrix.otp}}
13+
14+
strategy:
15+
fail-fast: false
16+
matrix:
17+
include:
18+
- elixir: "1.14"
19+
otp: "25"
20+
21+
- elixir: "1.15"
22+
otp: "26"
23+
24+
- elixir: "1.16"
25+
otp: "26"
26+
27+
- elixir: "1.17"
28+
otp: "26"
29+
30+
- elixir: "1.18"
31+
otp: "27"
32+
33+
- elixir: "1.19"
34+
otp: "27"
35+
36+
steps:
37+
- uses: actions/checkout@v4
38+
39+
- uses: erlef/setup-beam@v1
40+
with:
41+
otp-version: ${{matrix.otp}}
42+
elixir-version: ${{matrix.elixir}}
43+
44+
- name: Restore dependencies cache
45+
uses: actions/cache@v4
46+
with:
47+
path: deps
48+
key: ${{ runner.os }}-mix-${{ matrix.elixir }}-${{ matrix.otp }}-${{ hashFiles('**/mix.lock') }}
49+
restore-keys: ${{ runner.os }}-mix-${{ matrix.elixir }}-${{ matrix.otp }}-
50+
51+
- run: mix deps.get
52+
- run: mix compile --warnings-as-errors
53+
- run: mix test

.gitignore

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
# The directory Mix will write compiled artifacts to.
2+
/_build/
3+
4+
# If you run "mix test --cover", coverage assets end up here.
5+
/cover/
6+
7+
# The directory Mix downloads your dependencies sources to.
8+
/deps/
9+
10+
# Where third-party dependencies like ExDoc output generated docs.
11+
/doc/
12+
13+
# Temporary files, for example, from tests.
14+
/tmp/
15+
16+
# If the VM crashes, it generates a dump, let's ignore it too.
17+
erl_crash.dump
18+
19+
# Also ignore archive artifacts (built via "mix archive.build").
20+
*.ez
21+
22+
# Ignore package tarball (built via "mix hex.build").
23+
sashite_sin-*.tar
24+

LICENSE.md

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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.

README.md

Lines changed: 214 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,214 @@
1+
# Sashite.Sin
2+
3+
[![Hex.pm](https://img.shields.io/hexpm/v/sashite_sin.svg)](https://hex.pm/packages/sashite_sin)
4+
[![Docs](https://img.shields.io/badge/hex-docs-blue.svg)](https://hexdocs.pm/sashite_sin)
5+
[![License](https://img.shields.io/hexpm/l/sashite_sin.svg)](https://github.com/sashite/sin.ex/blob/main/LICENSE.md)
6+
7+
> **SIN** (Style Identifier Notation) implementation for Elixir.
8+
9+
## What is SIN?
10+
11+
SIN (Style Identifier Notation) provides a compact, ASCII-based format for encoding **Piece Style** with an associated **Side** in abstract strategy board games. It serves as a minimal building block that can be embedded in higher-level notations.
12+
13+
This library implements the [SIN Specification v1.0.0](https://sashite.dev/specs/sin/1.0.0/).
14+
15+
## Installation
16+
17+
Add `sashite_sin` to your list of dependencies in `mix.exs`:
18+
19+
```elixir
20+
def deps do
21+
[
22+
{:sashite_sin, "~> 1.0"}
23+
]
24+
end
25+
```
26+
27+
## Usage
28+
29+
```elixir
30+
# Parse SIN strings
31+
{:ok, sin} = Sashite.Sin.parse("C")
32+
sin.style # => :C
33+
sin.side # => :first
34+
35+
Sashite.Sin.to_string(sin) # => "C"
36+
37+
# Parse with pattern matching
38+
{:ok, chess_first} = Sashite.Sin.parse("C") # Chess-style, first player
39+
{:ok, chess_second} = Sashite.Sin.parse("c") # Chess-style, second player
40+
{:ok, shogi_first} = Sashite.Sin.parse("S") # Shogi-style, first player
41+
42+
# Bang version for direct access
43+
sin = Sashite.Sin.parse!("C")
44+
45+
# Create identifiers directly
46+
sin = Sashite.Sin.new(:C, :first)
47+
sin = Sashite.Sin.new(:S, :second)
48+
49+
# Validation
50+
Sashite.Sin.valid?("C") # => true
51+
Sashite.Sin.valid?("s") # => true
52+
Sashite.Sin.valid?("CC") # => false (more than one character)
53+
Sashite.Sin.valid?("1") # => false (digit instead of letter)
54+
Sashite.Sin.valid?("") # => false (empty string)
55+
56+
# Side transformation
57+
flipped = Sashite.Sin.flip(sin)
58+
Sashite.Sin.to_string(flipped) # => "c"
59+
60+
# Attribute changes
61+
shogi = Sashite.Sin.with_style(sin, :S)
62+
Sashite.Sin.to_string(shogi) # => "S"
63+
64+
second = Sashite.Sin.with_side(sin, :second)
65+
Sashite.Sin.to_string(second) # => "c"
66+
67+
# Side queries
68+
Sashite.Sin.first_player?(sin) # => true
69+
Sashite.Sin.second_player?(sin) # => false
70+
71+
# Comparison
72+
chess1 = Sashite.Sin.parse!("C")
73+
chess2 = Sashite.Sin.parse!("c")
74+
75+
Sashite.Sin.same_style?(chess1, chess2) # => true
76+
Sashite.Sin.same_side?(chess1, chess2) # => false
77+
```
78+
79+
## Format Specification
80+
81+
### Structure
82+
83+
```
84+
<letter>
85+
```
86+
87+
A SIN token is **exactly one** ASCII letter (`A-Z` or `a-z`).
88+
89+
### Attribute Mapping
90+
91+
| Attribute | Encoding |
92+
|-----------|----------|
93+
| Piece Style | Base letter (case-insensitive): `C` and `c` represent the same style |
94+
| Side | Letter case: uppercase → `first`, lowercase → `second` |
95+
96+
### Side Convention
97+
98+
- **Uppercase** (`A-Z`): First player (Side `first`)
99+
- **Lowercase** (`a-z`): Second player (Side `second`)
100+
101+
### Common Conventions
102+
103+
| SIN | Side | Typical Piece Style |
104+
|-----|------|---------------------|
105+
| `C` | First | Chess-style |
106+
| `c` | Second | Chess-style |
107+
| `S` | First | Shogi-style |
108+
| `s` | Second | Shogi-style |
109+
| `X` | First | Xiangqi-style |
110+
| `x` | Second | Xiangqi-style |
111+
| `M` | First | Makruk-style |
112+
| `m` | Second | Makruk-style |
113+
114+
### Invalid Token Examples
115+
116+
| String | Reason |
117+
|--------|--------|
118+
| `""` | Empty string |
119+
| `CC` | More than one character |
120+
| `c1` | Contains a digit |
121+
| `+C` | Contains a prefix character |
122+
| ` C` | Leading whitespace |
123+
| `C ` | Trailing whitespace |
124+
| `1` | Digit instead of letter |
125+
| `é` | Non-ASCII character |
126+
127+
## API Reference
128+
129+
### Parsing
130+
131+
```elixir
132+
Sashite.Sin.parse(sin_string) # => {:ok, %Sashite.Sin{}} | {:error, reason}
133+
Sashite.Sin.parse!(sin_string) # => %Sashite.Sin{} | raises ArgumentError
134+
Sashite.Sin.valid?(sin_string) # => boolean
135+
```
136+
137+
### Creation
138+
139+
```elixir
140+
Sashite.Sin.new(style, side)
141+
```
142+
143+
### Conversion
144+
145+
```elixir
146+
Sashite.Sin.to_string(sin) # => String.t()
147+
Sashite.Sin.letter(sin) # => String.t() (the single character)
148+
```
149+
150+
### Transformations
151+
152+
All transformations return new `%Sashite.Sin{}` structs:
153+
154+
```elixir
155+
# Side
156+
Sashite.Sin.flip(sin)
157+
158+
# Attribute changes
159+
Sashite.Sin.with_style(sin, new_style)
160+
Sashite.Sin.with_side(sin, new_side)
161+
```
162+
163+
### Queries
164+
165+
```elixir
166+
# Side
167+
Sashite.Sin.first_player?(sin)
168+
Sashite.Sin.second_player?(sin)
169+
170+
# Comparison
171+
Sashite.Sin.same_style?(sin1, sin2)
172+
Sashite.Sin.same_side?(sin1, sin2)
173+
```
174+
175+
## Data Structure
176+
177+
```elixir
178+
%Sashite.Sin{
179+
style: :A..:Z, # Piece style (always uppercase atom)
180+
side: :first | :second # Player side
181+
}
182+
```
183+
184+
## Protocol Mapping
185+
186+
Following the [Game Protocol](https://sashite.dev/game-protocol/):
187+
188+
| Protocol Attribute | SIN Encoding |
189+
|-------------------|--------------|
190+
| Piece Style | Base letter (case-insensitive) |
191+
| Side | Letter case |
192+
193+
## Relationship with SNN
194+
195+
**Style Name Notation (SNN)** and SIN are **independent primitives** that serve complementary roles:
196+
197+
- **SIN** — compact, single-character style identifiers (`C`, `s`, `X`)
198+
- **SNN** — human-readable style names (`CHESS`, `shogi`, `XIANGQI`)
199+
200+
Converting between SIN and SNN requires external, context-specific mapping.
201+
202+
## Related Specifications
203+
204+
- [Game Protocol](https://sashite.dev/game-protocol/) — Conceptual foundation
205+
- [PIN](https://sashite.dev/specs/pin/1.0.0/) — Piece Identifier Notation
206+
- [SIN Specification](https://sashite.dev/specs/sin/1.0.0/) — Official specification
207+
208+
## License
209+
210+
Available as open source under the [MIT License](https://opensource.org/licenses/MIT).
211+
212+
## About
213+
214+
Maintained by [Sashité](https://sashite.com/) — promoting chess variants and sharing the beauty of board game cultures.

0 commit comments

Comments
 (0)