sashite-epin 2.0.0 → 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 +192 -426
- data/lib/sashite/epin.rb +268 -191
- metadata +6 -11
- data/lib/sashite/epin/identifier.rb +0 -293
data/README.md
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
# Epin
|
|
1
|
+
# Sashite::Epin
|
|
2
2
|
|
|
3
3
|
[](https://github.com/sashite/epin.rb/tags)
|
|
4
4
|
[](https://rubydoc.info/github/sashite/epin.rb/main)
|
|
5
5
|

|
|
6
6
|
[](https://github.com/sashite/epin.rb/raw/main/LICENSE.md)
|
|
7
7
|
|
|
8
|
-
> **EPIN** (Extended Piece Identifier Notation) implementation for
|
|
8
|
+
> **EPIN** (Extended Piece Identifier Notation) implementation for Ruby.
|
|
9
9
|
|
|
10
10
|
## What is EPIN?
|
|
11
11
|
|
|
@@ -15,24 +15,6 @@ EPIN (Extended Piece Identifier Notation) extends [PIN](https://sashite.dev/spec
|
|
|
15
15
|
|
|
16
16
|
This gem implements the [EPIN Specification v1.0.0](https://sashite.dev/specs/epin/1.0.0/) with a minimal compositional API.
|
|
17
17
|
|
|
18
|
-
## Core Concept
|
|
19
|
-
|
|
20
|
-
```ruby
|
|
21
|
-
# EPIN is just PIN + derived flag
|
|
22
|
-
pin = Sashite::Pin.parse("K^")
|
|
23
|
-
epin = Sashite::Epin.new(pin, derived: false)
|
|
24
|
-
|
|
25
|
-
epin.to_s # => "K^" (native)
|
|
26
|
-
epin.pin # => PIN::Identifier instance
|
|
27
|
-
epin.derived? # => false
|
|
28
|
-
|
|
29
|
-
# Mark as derived
|
|
30
|
-
derived_epin = epin.mark_derived
|
|
31
|
-
derived_epin.to_s # => "K^'" (derived from opposite side's style)
|
|
32
|
-
```
|
|
33
|
-
|
|
34
|
-
**That's it.** All piece attributes come from the PIN component.
|
|
35
|
-
|
|
36
18
|
## Installation
|
|
37
19
|
|
|
38
20
|
```ruby
|
|
@@ -46,51 +28,67 @@ Or install manually:
|
|
|
46
28
|
gem install sashite-epin
|
|
47
29
|
```
|
|
48
30
|
|
|
49
|
-
|
|
31
|
+
This will also install `sashite-pin` as a dependency.
|
|
32
|
+
|
|
33
|
+
## Core Concept
|
|
50
34
|
|
|
51
35
|
```ruby
|
|
52
|
-
|
|
36
|
+
require "sashite/epin"
|
|
37
|
+
|
|
38
|
+
# EPIN is just PIN + derived flag
|
|
39
|
+
pin = Sashite::Pin.parse("K^")
|
|
40
|
+
epin = Sashite::Epin.new(pin)
|
|
41
|
+
|
|
42
|
+
epin.to_s # => "K^" (native)
|
|
43
|
+
epin.pin # => #<Sashite::Pin K^>
|
|
44
|
+
epin.derived # => false
|
|
45
|
+
|
|
46
|
+
# Mark as derived
|
|
47
|
+
derived_epin = epin.mark_derived
|
|
48
|
+
derived_epin.to_s # => "K^'" (derived from opposite side's style)
|
|
53
49
|
```
|
|
54
50
|
|
|
55
|
-
|
|
51
|
+
**That's it.** All piece attributes come from the PIN component.
|
|
52
|
+
|
|
53
|
+
## Usage
|
|
56
54
|
|
|
57
55
|
```ruby
|
|
58
56
|
require "sashite/epin"
|
|
59
57
|
|
|
60
|
-
# Parse
|
|
58
|
+
# Parse EPIN strings
|
|
61
59
|
epin = Sashite::Epin.parse("K^'")
|
|
62
60
|
epin.to_s # => "K^'"
|
|
63
61
|
|
|
64
62
|
# Access five fundamental attributes through PIN component + derived flag
|
|
65
|
-
epin.pin.type
|
|
66
|
-
epin.pin.side
|
|
67
|
-
epin.pin.state
|
|
68
|
-
epin.pin.terminal
|
|
69
|
-
epin.derived
|
|
70
|
-
|
|
71
|
-
# PIN component is a full PIN::Identifier
|
|
72
|
-
epin.pin.enhanced? # => false
|
|
73
|
-
epin.pin.letter # => "K"
|
|
74
|
-
```
|
|
63
|
+
epin.pin.type # => :K (Piece Name)
|
|
64
|
+
epin.pin.side # => :first (Piece Side)
|
|
65
|
+
epin.pin.state # => :normal (Piece State)
|
|
66
|
+
epin.pin.terminal # => true (Terminal Status)
|
|
67
|
+
epin.derived # => true (Piece Style: derived vs native)
|
|
75
68
|
|
|
76
|
-
|
|
69
|
+
# PIN component is a full Sashite::Pin instance
|
|
70
|
+
epin.pin.enhanced? # => false
|
|
71
|
+
epin.pin.letter # => "K"
|
|
72
|
+
epin.pin.first_player? # => true
|
|
73
|
+
```
|
|
77
74
|
|
|
78
75
|
### Creating Identifiers
|
|
79
76
|
|
|
80
77
|
```ruby
|
|
81
78
|
# Parse from string
|
|
82
|
-
epin = Sashite::Epin.parse("K^")
|
|
83
|
-
epin = Sashite::Epin.parse("K^'")
|
|
79
|
+
epin = Sashite::Epin.parse("K^") # Native
|
|
80
|
+
epin = Sashite::Epin.parse("K^'") # Derived
|
|
84
81
|
|
|
85
82
|
# Create from PIN component
|
|
86
83
|
pin = Sashite::Pin.parse("K^")
|
|
87
|
-
epin = Sashite::Epin.new(pin
|
|
88
|
-
epin = Sashite::Epin.new(pin, derived: true)
|
|
84
|
+
epin = Sashite::Epin.new(pin) # Native (default)
|
|
85
|
+
epin = Sashite::Epin.new(pin, derived: true) # Derived
|
|
89
86
|
|
|
90
|
-
#
|
|
91
|
-
Sashite::Epin.valid?("K^")
|
|
92
|
-
Sashite::Epin.valid?("K^'")
|
|
93
|
-
Sashite::Epin.valid?("K^''")
|
|
87
|
+
# Validation
|
|
88
|
+
Sashite::Epin.valid?("K^") # => true
|
|
89
|
+
Sashite::Epin.valid?("K^'") # => true
|
|
90
|
+
Sashite::Epin.valid?("K^''") # => false (multiple markers)
|
|
91
|
+
Sashite::Epin.valid?("K'^") # => false (wrong order)
|
|
94
92
|
```
|
|
95
93
|
|
|
96
94
|
### Accessing Components
|
|
@@ -99,38 +97,21 @@ Sashite::Epin.valid?("K^''") # => false (multiple markers)
|
|
|
99
97
|
epin = Sashite::Epin.parse("+R^'")
|
|
100
98
|
|
|
101
99
|
# Get PIN component
|
|
102
|
-
epin.pin
|
|
103
|
-
epin.pin.to_s
|
|
100
|
+
epin.pin # => #<Sashite::Pin +R^>
|
|
101
|
+
epin.pin.to_s # => "+R^"
|
|
104
102
|
|
|
105
103
|
# Check derivation
|
|
106
|
-
epin.derived
|
|
104
|
+
epin.derived # => true
|
|
105
|
+
epin.derived? # => true
|
|
106
|
+
epin.native? # => false
|
|
107
107
|
|
|
108
108
|
# Serialize
|
|
109
109
|
epin.to_s # => "+R^'"
|
|
110
110
|
```
|
|
111
111
|
|
|
112
|
-
###
|
|
112
|
+
### Transformations
|
|
113
113
|
|
|
114
|
-
All
|
|
115
|
-
|
|
116
|
-
```ruby
|
|
117
|
-
epin = Sashite::Epin.parse("+R^'")
|
|
118
|
-
|
|
119
|
-
# From PIN component (4 attributes)
|
|
120
|
-
epin.pin.type # => :R (Piece Name)
|
|
121
|
-
epin.pin.side # => :first (Piece Side)
|
|
122
|
-
epin.pin.state # => :enhanced (Piece State)
|
|
123
|
-
epin.pin.terminal? # => true (Terminal Status)
|
|
124
|
-
|
|
125
|
-
# From EPIN (5th attribute)
|
|
126
|
-
epin.derived? # => true (Piece Style: native vs derived)
|
|
127
|
-
```
|
|
128
|
-
|
|
129
|
-
## Transformations
|
|
130
|
-
|
|
131
|
-
All transformations return new immutable instances:
|
|
132
|
-
|
|
133
|
-
### Change Derivation Status
|
|
114
|
+
All transformations return new immutable instances.
|
|
134
115
|
|
|
135
116
|
```ruby
|
|
136
117
|
epin = Sashite::Epin.parse("K^")
|
|
@@ -140,11 +121,11 @@ derived = epin.mark_derived
|
|
|
140
121
|
derived.to_s # => "K^'"
|
|
141
122
|
|
|
142
123
|
# Mark as native
|
|
143
|
-
native = derived.
|
|
124
|
+
native = derived.unmark_derived
|
|
144
125
|
native.to_s # => "K^"
|
|
145
126
|
|
|
146
|
-
#
|
|
147
|
-
toggled = epin.with_derived(
|
|
127
|
+
# Set explicitly
|
|
128
|
+
toggled = epin.with_derived(true)
|
|
148
129
|
toggled.to_s # => "K^'"
|
|
149
130
|
```
|
|
150
131
|
|
|
@@ -157,17 +138,17 @@ epin = Sashite::Epin.parse("K^'")
|
|
|
157
138
|
new_pin = epin.pin.with_type(:Q)
|
|
158
139
|
epin.with_pin(new_pin).to_s # => "Q^'"
|
|
159
140
|
|
|
160
|
-
# Change type
|
|
161
|
-
epin.with_pin(epin.pin.with_type(:Q)).to_s # => "Q^'"
|
|
162
|
-
|
|
163
141
|
# Change state
|
|
164
|
-
epin.
|
|
142
|
+
new_pin = epin.pin.enhance
|
|
143
|
+
epin.with_pin(new_pin).to_s # => "+K^'"
|
|
165
144
|
|
|
166
145
|
# Remove terminal marker
|
|
167
|
-
epin.
|
|
146
|
+
new_pin = epin.pin.unmark_terminal
|
|
147
|
+
epin.with_pin(new_pin).to_s # => "K'"
|
|
168
148
|
|
|
169
149
|
# Change side
|
|
170
|
-
epin.
|
|
150
|
+
new_pin = epin.pin.flip
|
|
151
|
+
epin.with_pin(new_pin).to_s # => "k^'"
|
|
171
152
|
```
|
|
172
153
|
|
|
173
154
|
### Multiple Transformations
|
|
@@ -176,99 +157,57 @@ epin.with_pin(epin.pin.flip).to_s # => "k^'"
|
|
|
176
157
|
epin = Sashite::Epin.parse("K^")
|
|
177
158
|
|
|
178
159
|
# Transform PIN and derivation
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
.mark_derived
|
|
160
|
+
new_pin = epin.pin.with_type(:Q).enhance
|
|
161
|
+
transformed = epin.with_pin(new_pin).mark_derived
|
|
182
162
|
|
|
183
163
|
transformed.to_s # => "+Q^'"
|
|
184
164
|
```
|
|
185
165
|
|
|
186
|
-
|
|
166
|
+
### Component Queries
|
|
187
167
|
|
|
188
|
-
Use the PIN
|
|
168
|
+
Use the PIN API directly:
|
|
189
169
|
|
|
190
170
|
```ruby
|
|
191
171
|
epin = Sashite::Epin.parse("+P^'")
|
|
192
172
|
|
|
193
173
|
# PIN queries (name, side, state, terminal)
|
|
194
|
-
epin.pin.type
|
|
195
|
-
epin.pin.side
|
|
196
|
-
epin.pin.state
|
|
197
|
-
epin.pin.terminal
|
|
198
|
-
epin.pin.first_player?
|
|
199
|
-
epin.pin.enhanced?
|
|
200
|
-
epin.pin.letter
|
|
201
|
-
epin.pin.prefix
|
|
202
|
-
epin.pin.suffix
|
|
174
|
+
epin.pin.type # => :P
|
|
175
|
+
epin.pin.side # => :first
|
|
176
|
+
epin.pin.state # => :enhanced
|
|
177
|
+
epin.pin.terminal # => true
|
|
178
|
+
epin.pin.first_player? # => true
|
|
179
|
+
epin.pin.enhanced? # => true
|
|
180
|
+
epin.pin.letter # => "P"
|
|
181
|
+
epin.pin.prefix # => "+"
|
|
182
|
+
epin.pin.suffix # => "^"
|
|
203
183
|
|
|
204
184
|
# EPIN queries (style)
|
|
205
|
-
epin.derived?
|
|
206
|
-
epin.native?
|
|
185
|
+
epin.derived? # => true
|
|
186
|
+
epin.native? # => false
|
|
207
187
|
|
|
208
188
|
# Compare EPINs
|
|
209
189
|
other = Sashite::Epin.parse("+P^")
|
|
210
|
-
epin.pin.same_type?(other.pin)
|
|
211
|
-
epin.pin.same_state?(other.pin)
|
|
212
|
-
epin.
|
|
213
|
-
```
|
|
214
|
-
|
|
215
|
-
## API Reference
|
|
216
|
-
|
|
217
|
-
### Main Module
|
|
218
|
-
|
|
219
|
-
```ruby
|
|
220
|
-
# Parse EPIN string
|
|
221
|
-
Sashite::Epin.parse(epin_string) # => Epin::Identifier
|
|
222
|
-
|
|
223
|
-
# Create from PIN component
|
|
224
|
-
Sashite::Epin.new(pin, derived: false) # => Epin::Identifier
|
|
225
|
-
|
|
226
|
-
# Validate string
|
|
227
|
-
Sashite::Epin.valid?(epin_string) # => Boolean
|
|
228
|
-
```
|
|
229
|
-
|
|
230
|
-
### Identifier Class
|
|
231
|
-
|
|
232
|
-
#### Core Methods (6 total)
|
|
233
|
-
|
|
234
|
-
```ruby
|
|
235
|
-
# Creation
|
|
236
|
-
Sashite::Epin.new(pin, derived: false) # Create from PIN + derivation flag
|
|
237
|
-
|
|
238
|
-
# Component access
|
|
239
|
-
epin.pin # => PIN::Identifier
|
|
240
|
-
epin.derived? # => Boolean
|
|
241
|
-
|
|
242
|
-
# Serialization
|
|
243
|
-
epin.to_s # => "K^'" or "K^"
|
|
244
|
-
|
|
245
|
-
# PIN replacement
|
|
246
|
-
epin.with_pin(new_pin) # New EPIN with different PIN
|
|
247
|
-
|
|
248
|
-
# Derivation transformation
|
|
249
|
-
epin.mark_derived # Mark as derived (add ')
|
|
250
|
-
epin.unmark_native # Mark as native (remove ')
|
|
251
|
-
epin.with_derived(boolean) # Set derivation explicitly
|
|
252
|
-
```
|
|
253
|
-
|
|
254
|
-
#### Convenience Queries
|
|
255
|
-
|
|
256
|
-
```ruby
|
|
257
|
-
epin.native? # !derived?
|
|
258
|
-
epin.same_derivation?(other) # Compare derivation status
|
|
190
|
+
epin.pin.same_type?(other.pin) # => true (both P)
|
|
191
|
+
epin.pin.same_state?(other.pin) # => true (both enhanced)
|
|
192
|
+
epin.same_derived?(other) # => false (different derivation)
|
|
259
193
|
```
|
|
260
194
|
|
|
261
|
-
|
|
195
|
+
## Five Fundamental Attributes
|
|
262
196
|
|
|
263
|
-
|
|
264
|
-
epin1 == epin2 # True if both PIN and derived flag equal
|
|
265
|
-
```
|
|
197
|
+
EPIN exposes all five attributes from the [Sashité Game Protocol](https://sashite.dev/game-protocol/):
|
|
266
198
|
|
|
267
|
-
|
|
199
|
+
| Protocol Attribute | EPIN Access | Example |
|
|
200
|
+
|-------------------|-------------|---------|
|
|
201
|
+
| **Piece Name** | `epin.pin.type` | `:K` (King), `:R` (Rook) |
|
|
202
|
+
| **Piece Side** | `epin.pin.side` | `:first`, `:second` |
|
|
203
|
+
| **Piece State** | `epin.pin.state` | `:normal`, `:enhanced`, `:diminished` |
|
|
204
|
+
| **Terminal Status** | `epin.pin.terminal` | `true`, `false` |
|
|
205
|
+
| **Piece Style** | `epin.derived` | `false` (native), `true` (derived) |
|
|
268
206
|
|
|
269
207
|
## Format Specification
|
|
270
208
|
|
|
271
209
|
### Structure
|
|
210
|
+
|
|
272
211
|
```
|
|
273
212
|
<pin>[']
|
|
274
213
|
```
|
|
@@ -277,148 +216,140 @@ Where:
|
|
|
277
216
|
- `<pin>` is any valid PIN token
|
|
278
217
|
- `'` is the optional derivation marker
|
|
279
218
|
|
|
280
|
-
### Grammar (
|
|
281
|
-
```bnf
|
|
282
|
-
<epin> ::= <pin> | <pin> "'"
|
|
219
|
+
### Grammar (EBNF)
|
|
283
220
|
|
|
284
|
-
|
|
285
|
-
|
|
221
|
+
```ebnf
|
|
222
|
+
epin ::= pin | pin "'"
|
|
223
|
+
pin ::= ["+" | "-"] letter ["^"]
|
|
224
|
+
letter ::= "A" | ... | "Z" | "a" | ... | "z"
|
|
286
225
|
```
|
|
287
226
|
|
|
288
227
|
### Regular Expression
|
|
228
|
+
|
|
289
229
|
```ruby
|
|
290
230
|
/\A[-+]?[A-Za-z]\^?'?\z/
|
|
291
231
|
```
|
|
292
232
|
|
|
293
|
-
|
|
233
|
+
### Examples
|
|
294
234
|
|
|
295
|
-
|
|
235
|
+
| EPIN | Side | State | Terminal | Derived | Description |
|
|
236
|
+
|------|------|-------|----------|---------|-------------|
|
|
237
|
+
| `K` | First | Normal | No | No | Standard native king |
|
|
238
|
+
| `K'` | First | Normal | No | Yes | Derived king |
|
|
239
|
+
| `K^` | First | Normal | Yes | No | Terminal native king |
|
|
240
|
+
| `K^'` | First | Normal | Yes | Yes | Terminal derived king |
|
|
241
|
+
| `+R'` | First | Enhanced | No | Yes | Enhanced derived rook |
|
|
242
|
+
| `-p` | Second | Diminished | No | No | Diminished native pawn |
|
|
296
243
|
|
|
297
|
-
|
|
298
|
-
# Native pieces (no derivation marker)
|
|
299
|
-
native_king = Sashite::Epin.parse("K^")
|
|
300
|
-
native_king.pin.type # => :K
|
|
301
|
-
native_king.derived? # => false
|
|
302
|
-
|
|
303
|
-
# Derived pieces (with derivation marker)
|
|
304
|
-
derived_king = Sashite::Epin.parse("K^'")
|
|
305
|
-
derived_king.pin.type # => :K
|
|
306
|
-
derived_king.derived? # => true
|
|
307
|
-
|
|
308
|
-
# Enhanced pieces
|
|
309
|
-
enhanced_rook = Sashite::Epin.parse("+R'")
|
|
310
|
-
enhanced_rook.pin.enhanced? # => true
|
|
311
|
-
enhanced_rook.derived? # => true
|
|
312
|
-
|
|
313
|
-
# Diminished pieces
|
|
314
|
-
diminished_pawn = Sashite::Epin.parse("-p")
|
|
315
|
-
diminished_pawn.pin.diminished? # => true
|
|
316
|
-
diminished_pawn.native? # => true
|
|
317
|
-
```
|
|
318
|
-
|
|
319
|
-
### Cross-Style Scenarios
|
|
244
|
+
## Cross-Style Game Example
|
|
320
245
|
|
|
321
|
-
|
|
246
|
+
In a chess-vs-makruk cross-style match where:
|
|
247
|
+
- First side native style = chess
|
|
248
|
+
- Second side native style = makruk
|
|
322
249
|
|
|
323
250
|
```ruby
|
|
324
251
|
# First player pieces
|
|
325
|
-
chess_king = Sashite::Epin.parse("K^")
|
|
326
|
-
makruk_pawn = Sashite::Epin.parse("P'")
|
|
252
|
+
chess_king = Sashite::Epin.parse("K^") # Native Chess king
|
|
253
|
+
makruk_pawn = Sashite::Epin.parse("P'") # Derived Makruk pawn (foreign)
|
|
327
254
|
|
|
328
|
-
chess_king.native?
|
|
329
|
-
makruk_pawn.derived?
|
|
255
|
+
chess_king.native? # => true (uses own style)
|
|
256
|
+
makruk_pawn.derived? # => true (uses opponent's style)
|
|
330
257
|
|
|
331
258
|
# Second player pieces
|
|
332
|
-
makruk_king = Sashite::Epin.parse("k^")
|
|
333
|
-
chess_pawn = Sashite::Epin.parse("p'")
|
|
259
|
+
makruk_king = Sashite::Epin.parse("k^") # Native Makruk king
|
|
260
|
+
chess_pawn = Sashite::Epin.parse("p'") # Derived Chess pawn (foreign)
|
|
334
261
|
|
|
335
|
-
makruk_king.native?
|
|
336
|
-
chess_pawn.derived?
|
|
262
|
+
makruk_king.native? # => true
|
|
263
|
+
chess_pawn.derived? # => true
|
|
337
264
|
```
|
|
338
265
|
|
|
339
|
-
|
|
266
|
+
## API Reference
|
|
340
267
|
|
|
341
|
-
|
|
342
|
-
# Start with native king
|
|
343
|
-
epin = Sashite::Epin.parse("K^")
|
|
268
|
+
### Parsing and Validation
|
|
344
269
|
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
270
|
+
```ruby
|
|
271
|
+
Sashite::Epin.parse(epin_string) # => Sashite::Epin | raises ArgumentError
|
|
272
|
+
Sashite::Epin.valid?(epin_string) # => boolean
|
|
273
|
+
```
|
|
348
274
|
|
|
349
|
-
|
|
350
|
-
queen = derived.with_pin(derived.pin.with_type(:Q))
|
|
351
|
-
queen.to_s # => "Q^'"
|
|
275
|
+
### Creation
|
|
352
276
|
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
277
|
+
```ruby
|
|
278
|
+
Sashite::Epin.new(pin) # Native (default)
|
|
279
|
+
Sashite::Epin.new(pin, derived: true) # Derived
|
|
280
|
+
```
|
|
356
281
|
|
|
357
|
-
|
|
358
|
-
opponent = derived.with_pin(derived.pin.flip)
|
|
359
|
-
opponent.to_s # => "k^'"
|
|
282
|
+
### Conversion
|
|
360
283
|
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
native.to_s # => "K^"
|
|
284
|
+
```ruby
|
|
285
|
+
epin.to_s # => String
|
|
364
286
|
```
|
|
365
287
|
|
|
366
|
-
###
|
|
288
|
+
### Transformations
|
|
367
289
|
|
|
368
|
-
|
|
369
|
-
epin = Sashite::Epin.parse("+R^'")
|
|
290
|
+
All transformations return new `Sashite::Epin` instances:
|
|
370
291
|
|
|
371
|
-
|
|
372
|
-
|
|
292
|
+
```ruby
|
|
293
|
+
# PIN replacement
|
|
294
|
+
epin.with_pin(new_pin) # => Sashite::Epin with different PIN
|
|
373
295
|
|
|
374
|
-
#
|
|
375
|
-
|
|
296
|
+
# Derivation
|
|
297
|
+
epin.mark_derived # => Sashite::Epin with derived: true
|
|
298
|
+
epin.unmark_derived # => Sashite::Epin with derived: false
|
|
299
|
+
epin.with_derived(boolean) # => Sashite::Epin with specified derivation
|
|
300
|
+
```
|
|
376
301
|
|
|
377
|
-
|
|
378
|
-
new_epin = epin.with_pin(new_pin)
|
|
379
|
-
new_epin.to_s # => "B^'"
|
|
302
|
+
### Queries
|
|
380
303
|
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
.with_terminal(false)
|
|
386
|
-
.flip
|
|
304
|
+
```ruby
|
|
305
|
+
# Derivation
|
|
306
|
+
epin.derived? # => true if derived
|
|
307
|
+
epin.native? # => true if not derived
|
|
387
308
|
|
|
388
|
-
|
|
309
|
+
# Comparison
|
|
310
|
+
epin.same_derived?(other) # => true if same derivation status
|
|
389
311
|
```
|
|
390
312
|
|
|
391
|
-
|
|
313
|
+
## Data Structure
|
|
392
314
|
|
|
393
315
|
```ruby
|
|
394
|
-
|
|
316
|
+
Sashite::Epin
|
|
317
|
+
#pin => Sashite::Pin # Underlying PIN instance
|
|
318
|
+
#derived => true | false # Derivation status
|
|
319
|
+
```
|
|
395
320
|
|
|
396
|
-
|
|
397
|
-
derived = original.mark_derived
|
|
398
|
-
changed_pin = original.with_pin(original.pin.with_type(:Q))
|
|
321
|
+
## Comparison with PIN
|
|
399
322
|
|
|
400
|
-
|
|
401
|
-
original.to_s # => "K^"
|
|
402
|
-
derived.to_s # => "K^'"
|
|
403
|
-
changed_pin.to_s # => "Q^"
|
|
323
|
+
### What EPIN Adds
|
|
404
324
|
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
pin
|
|
408
|
-
|
|
325
|
+
```ruby
|
|
326
|
+
# PIN: 4 attributes
|
|
327
|
+
pin = Sashite::Pin.parse("K^")
|
|
328
|
+
pin.type # Piece Name
|
|
329
|
+
pin.side # Piece Side
|
|
330
|
+
pin.state # Piece State
|
|
331
|
+
pin.terminal # Terminal Status
|
|
332
|
+
|
|
333
|
+
# EPIN: 5 attributes (PIN + style)
|
|
334
|
+
epin = Sashite::Epin.parse("K^'")
|
|
335
|
+
epin.pin.type # Piece Name
|
|
336
|
+
epin.pin.side # Piece Side
|
|
337
|
+
epin.pin.state # Piece State
|
|
338
|
+
epin.pin.terminal # Terminal Status
|
|
339
|
+
epin.derived # Piece Style (5th attribute)
|
|
409
340
|
```
|
|
410
341
|
|
|
411
|
-
|
|
342
|
+
### When to Use EPIN vs PIN
|
|
412
343
|
|
|
413
|
-
|
|
344
|
+
**Use PIN when:**
|
|
345
|
+
- Single-style games (both players use same style)
|
|
346
|
+
- Style information not needed
|
|
347
|
+
- Maximum compatibility required
|
|
414
348
|
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
| **Piece State** | `epin.pin.state` | `:normal`, `:enhanced`, `:diminished` |
|
|
420
|
-
| **Terminal Status** | `epin.pin.terminal?` | `true`, `false` |
|
|
421
|
-
| **Piece Style** | `epin.derived?` | `false` (native), `true` (derived) |
|
|
349
|
+
**Use EPIN when:**
|
|
350
|
+
- Cross-style games (different styles per player)
|
|
351
|
+
- Pieces can change style (promotion to foreign piece)
|
|
352
|
+
- Need to track native vs derived pieces
|
|
422
353
|
|
|
423
354
|
## Design Principles
|
|
424
355
|
|
|
@@ -427,26 +358,24 @@ EPIN exposes all five fundamental attributes from the Sashité Game Protocol:
|
|
|
427
358
|
EPIN doesn't reimplement PIN features — it extends PIN minimally:
|
|
428
359
|
|
|
429
360
|
```ruby
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
@derived = !!derived
|
|
435
|
-
end
|
|
361
|
+
def initialize(pin, derived: false)
|
|
362
|
+
@pin = pin
|
|
363
|
+
@derived = !!derived
|
|
364
|
+
freeze
|
|
436
365
|
end
|
|
437
366
|
```
|
|
438
367
|
|
|
439
|
-
### 2.
|
|
368
|
+
### 2. Minimal API
|
|
440
369
|
|
|
441
370
|
**6 core methods only:**
|
|
442
|
-
1. `new
|
|
371
|
+
1. `new` — create from PIN
|
|
443
372
|
2. `pin` — get PIN component
|
|
444
|
-
3. `derived?` — check derivation
|
|
373
|
+
3. `derived` / `derived?` — check derivation
|
|
445
374
|
4. `to_s` — serialize
|
|
446
|
-
5. `with_pin
|
|
447
|
-
6. `with_derived
|
|
375
|
+
5. `with_pin` — replace PIN
|
|
376
|
+
6. `with_derived` / `mark_derived` / `unmark_derived` — change derivation
|
|
448
377
|
|
|
449
|
-
Everything else uses the PIN
|
|
378
|
+
Everything else uses the PIN API directly.
|
|
450
379
|
|
|
451
380
|
### 3. Component Transparency
|
|
452
381
|
|
|
@@ -461,7 +390,7 @@ epin.pin.flip
|
|
|
461
390
|
|
|
462
391
|
# No need for wrapper methods like:
|
|
463
392
|
# epin.type
|
|
464
|
-
# epin.with_type
|
|
393
|
+
# epin.with_type(:Q)
|
|
465
394
|
# epin.enhanced?
|
|
466
395
|
# epin.flip
|
|
467
396
|
```
|
|
@@ -472,182 +401,19 @@ Every valid PIN is a valid EPIN (without derivation marker):
|
|
|
472
401
|
|
|
473
402
|
```ruby
|
|
474
403
|
# All PIN identifiers work as EPIN
|
|
475
|
-
|
|
476
|
-
pin_tokens.each do |token|
|
|
404
|
+
%w[K +R -p K^ +R^].each do |token|
|
|
477
405
|
epin = Sashite::Epin.parse(token)
|
|
478
|
-
epin.native?
|
|
479
|
-
epin.to_s
|
|
406
|
+
epin.native? # => true
|
|
407
|
+
epin.to_s # => token
|
|
480
408
|
end
|
|
481
409
|
```
|
|
482
410
|
|
|
483
|
-
### 5. Immutability
|
|
484
|
-
|
|
485
|
-
All instances frozen. Transformations return new instances:
|
|
486
|
-
|
|
487
|
-
```ruby
|
|
488
|
-
epin1 = Sashite::Epin.parse("K^")
|
|
489
|
-
epin2 = epin1.mark_derived
|
|
490
|
-
epin1.frozen? # => true
|
|
491
|
-
epin2.frozen? # => true
|
|
492
|
-
epin1.equal?(epin2) # => false
|
|
493
|
-
```
|
|
494
|
-
|
|
495
|
-
## Semantics
|
|
496
|
-
|
|
497
|
-
### Native vs Derived
|
|
498
|
-
|
|
499
|
-
In cross-style games:
|
|
500
|
-
- **Native piece**: Uses its own side's style (no `'` marker)
|
|
501
|
-
- **Derived piece**: Uses opponent's style (`'` marker present)
|
|
502
|
-
|
|
503
|
-
```ruby
|
|
504
|
-
# Chess vs Makruk match
|
|
505
|
-
# First player = Chess, Second player = Makruk
|
|
506
|
-
|
|
507
|
-
"K" # First player king in Chess style (native)
|
|
508
|
-
"K'" # First player king in Makruk style (derived from opponent)
|
|
509
|
-
"k" # Second player king in Makruk style (native)
|
|
510
|
-
"k'" # Second player king in Chess style (derived from opponent)
|
|
511
|
-
```
|
|
512
|
-
|
|
513
|
-
### Style Derivation Logic
|
|
514
|
-
|
|
515
|
-
```ruby
|
|
516
|
-
# Assume: first=Chess, second=Makruk
|
|
517
|
-
|
|
518
|
-
epin = Sashite::Epin.parse("P")
|
|
519
|
-
# Piece Side: first
|
|
520
|
-
# Style: Chess (first's native) → Native piece
|
|
521
|
-
|
|
522
|
-
epin = Sashite::Epin.parse("P'")
|
|
523
|
-
# Piece Side: first
|
|
524
|
-
# Style: Makruk (second's native) → Derived piece
|
|
525
|
-
|
|
526
|
-
epin = Sashite::Epin.parse("p")
|
|
527
|
-
# Piece Side: second
|
|
528
|
-
# Style: Makruk (second's native) → Native piece
|
|
529
|
-
|
|
530
|
-
epin = Sashite::Epin.parse("p'")
|
|
531
|
-
# Piece Side: second
|
|
532
|
-
# Style: Chess (first's native) → Derived piece
|
|
533
|
-
```
|
|
534
|
-
|
|
535
|
-
## Error Handling
|
|
536
|
-
|
|
537
|
-
```ruby
|
|
538
|
-
# Invalid EPIN string
|
|
539
|
-
begin
|
|
540
|
-
Sashite::Epin.parse("invalid")
|
|
541
|
-
rescue ArgumentError => e
|
|
542
|
-
e.message # => "Invalid EPIN string: invalid"
|
|
543
|
-
end
|
|
544
|
-
|
|
545
|
-
# Multiple derivation markers
|
|
546
|
-
begin
|
|
547
|
-
Sashite::Epin.parse("K''")
|
|
548
|
-
rescue ArgumentError => e
|
|
549
|
-
# Invalid format
|
|
550
|
-
end
|
|
551
|
-
|
|
552
|
-
# PIN validation errors delegate
|
|
553
|
-
begin
|
|
554
|
-
Sashite::Epin.parse("KK'")
|
|
555
|
-
rescue ArgumentError => e
|
|
556
|
-
# PIN validation error
|
|
557
|
-
end
|
|
558
|
-
```
|
|
559
|
-
|
|
560
|
-
## Performance Considerations
|
|
561
|
-
|
|
562
|
-
### Efficient Composition
|
|
563
|
-
|
|
564
|
-
```ruby
|
|
565
|
-
# PIN component created once
|
|
566
|
-
pin = Sashite::Pin.parse("K^")
|
|
567
|
-
epin = Sashite::Epin.new(pin, derived: true)
|
|
568
|
-
|
|
569
|
-
# Accessing components is O(1)
|
|
570
|
-
epin.pin # => direct reference
|
|
571
|
-
epin.derived? # => direct boolean check
|
|
572
|
-
|
|
573
|
-
# No overhead from method delegation
|
|
574
|
-
epin.pin.type # => direct method call on PIN component
|
|
575
|
-
```
|
|
576
|
-
|
|
577
|
-
### Transformation Patterns
|
|
578
|
-
|
|
579
|
-
```ruby
|
|
580
|
-
epin = Sashite::Epin.parse("K^'")
|
|
581
|
-
|
|
582
|
-
# Pattern 1: Change derivation only
|
|
583
|
-
epin.unmark_native
|
|
584
|
-
|
|
585
|
-
# Pattern 2: Change PIN only
|
|
586
|
-
epin.with_pin(epin.pin.with_type(:Q))
|
|
587
|
-
|
|
588
|
-
# Pattern 3: Change both
|
|
589
|
-
new_pin = epin.pin.with_type(:Q)
|
|
590
|
-
epin.with_pin(new_pin).unmark_native
|
|
591
|
-
|
|
592
|
-
# Pattern 4: Complex PIN transformation
|
|
593
|
-
new_pin = epin.pin
|
|
594
|
-
.with_type(:Q)
|
|
595
|
-
.with_state(:enhanced)
|
|
596
|
-
.flip
|
|
597
|
-
epin.with_pin(new_pin).mark_derived
|
|
598
|
-
```
|
|
599
|
-
|
|
600
|
-
## Comparison with PIN
|
|
601
|
-
|
|
602
|
-
### What EPIN Adds
|
|
603
|
-
|
|
604
|
-
```ruby
|
|
605
|
-
# PIN: 4 attributes
|
|
606
|
-
pin = Sashite::Pin.parse("K^")
|
|
607
|
-
pin.type # Piece Name
|
|
608
|
-
pin.side # Piece Side
|
|
609
|
-
pin.state # Piece State
|
|
610
|
-
pin.terminal? # Terminal Status
|
|
611
|
-
|
|
612
|
-
# EPIN: 5 attributes (PIN + style)
|
|
613
|
-
epin = Sashite::Epin.parse("K^'")
|
|
614
|
-
epin.pin.type # Piece Name
|
|
615
|
-
epin.pin.side # Piece Side
|
|
616
|
-
epin.pin.state # Piece State
|
|
617
|
-
epin.pin.terminal? # Terminal Status
|
|
618
|
-
epin.derived? # Piece Style (5th attribute)
|
|
619
|
-
```
|
|
620
|
-
|
|
621
|
-
### When to Use EPIN vs PIN
|
|
622
|
-
|
|
623
|
-
**Use PIN when:**
|
|
624
|
-
- Single-style games (both players use same style)
|
|
625
|
-
- Style information not needed
|
|
626
|
-
- Maximum compatibility required
|
|
627
|
-
|
|
628
|
-
**Use EPIN when:**
|
|
629
|
-
- Cross-style games (different styles per player)
|
|
630
|
-
- Pieces can change style (promotion to foreign piece)
|
|
631
|
-
- Need to track native vs derived pieces
|
|
632
|
-
|
|
633
|
-
## Design Properties
|
|
634
|
-
|
|
635
|
-
- **Rule-agnostic**: Independent of game mechanics
|
|
636
|
-
- **Pure composition**: Extends PIN minimally
|
|
637
|
-
- **Minimal API**: Only 6 core methods
|
|
638
|
-
- **Component transparency**: Direct PIN access
|
|
639
|
-
- **Backward compatible**: All PIN tokens valid
|
|
640
|
-
- **Immutable**: Frozen instances
|
|
641
|
-
- **Type-safe**: Full PIN type preservation
|
|
642
|
-
- **Style-aware**: Tracks native vs derived
|
|
643
|
-
- **Compact**: Single character overhead for derivation
|
|
644
|
-
|
|
645
411
|
## Related Specifications
|
|
646
412
|
|
|
647
|
-
- [EPIN Specification v1.0.0](https://sashite.dev/specs/epin/1.0.0/)
|
|
648
|
-
- [EPIN Examples](https://sashite.dev/specs/epin/1.0.0/examples/)
|
|
649
|
-
- [PIN Specification v1.0.0](https://sashite.dev/specs/pin/1.0.0/)
|
|
650
|
-
- [Sashité Game Protocol](https://sashite.dev/game-protocol/)
|
|
413
|
+
- [EPIN Specification v1.0.0](https://sashite.dev/specs/epin/1.0.0/) — Technical specification
|
|
414
|
+
- [EPIN Examples](https://sashite.dev/specs/epin/1.0.0/examples/) — Usage examples
|
|
415
|
+
- [PIN Specification v1.0.0](https://sashite.dev/specs/pin/1.0.0/) — Base component
|
|
416
|
+
- [Sashité Game Protocol](https://sashite.dev/game-protocol/) — Foundation
|
|
651
417
|
|
|
652
418
|
## License
|
|
653
419
|
|