sashite-epin 2.0.0 → 2.2.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 +158 -539
- data/lib/sashite/epin/constants.rb +15 -0
- data/lib/sashite/epin/errors/argument/messages.rb +25 -0
- data/lib/sashite/epin/errors/argument.rb +16 -0
- data/lib/sashite/epin/errors.rb +3 -0
- data/lib/sashite/epin/identifier.rb +118 -211
- data/lib/sashite/epin/parser.rb +101 -0
- data/lib/sashite/epin.rb +54 -198
- data/lib/sashite-epin.rb +0 -11
- metadata +12 -12
- data/LICENSE.md +0 -21
data/README.md
CHANGED
|
@@ -1,37 +1,17 @@
|
|
|
1
|
-
#
|
|
1
|
+
# epin.rb
|
|
2
2
|
|
|
3
3
|
[](https://github.com/sashite/epin.rb/tags)
|
|
4
4
|
[](https://rubydoc.info/github/sashite/epin.rb/main)
|
|
5
|
-
](https://github.com/sashite/epin.rb/raw/main/LICENSE
|
|
5
|
+
[](https://github.com/sashite/epin.rb/actions)
|
|
6
|
+
[](https://github.com/sashite/epin.rb/raw/main/LICENSE)
|
|
7
7
|
|
|
8
|
-
> **EPIN** (Extended Piece Identifier Notation) implementation for
|
|
8
|
+
> **EPIN** (Extended Piece Identifier Notation) implementation for Ruby.
|
|
9
9
|
|
|
10
|
-
##
|
|
10
|
+
## Overview
|
|
11
11
|
|
|
12
|
-
|
|
12
|
+
This library implements the [EPIN Specification v1.0.0](https://sashite.dev/specs/epin/1.0.0/).
|
|
13
13
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
This gem implements the [EPIN Specification v1.0.0](https://sashite.dev/specs/epin/1.0.0/) with a minimal compositional API.
|
|
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.
|
|
14
|
+
EPIN extends [PIN](https://sashite.dev/specs/pin/1.0.0/) with an optional derivation marker (`'`) that flags whether a piece uses a native or derived style.
|
|
35
15
|
|
|
36
16
|
## Installation
|
|
37
17
|
|
|
@@ -49,48 +29,64 @@ gem install sashite-epin
|
|
|
49
29
|
## Dependencies
|
|
50
30
|
|
|
51
31
|
```ruby
|
|
52
|
-
gem "sashite-pin"
|
|
32
|
+
gem "sashite-pin" # Piece Identifier Notation
|
|
53
33
|
```
|
|
54
34
|
|
|
55
|
-
##
|
|
35
|
+
## Usage
|
|
36
|
+
|
|
37
|
+
### Parsing (String → Identifier)
|
|
38
|
+
|
|
39
|
+
Convert an EPIN string into an `Identifier` object.
|
|
56
40
|
|
|
57
41
|
```ruby
|
|
58
42
|
require "sashite/epin"
|
|
59
43
|
|
|
60
|
-
#
|
|
44
|
+
# Standard parsing (raises on error)
|
|
61
45
|
epin = Sashite::Epin.parse("K^'")
|
|
62
|
-
epin.to_s
|
|
63
|
-
|
|
64
|
-
# Access
|
|
65
|
-
epin.pin.
|
|
66
|
-
epin.pin.side
|
|
67
|
-
epin.pin.state
|
|
68
|
-
epin.pin.terminal?
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
#
|
|
72
|
-
epin.
|
|
73
|
-
|
|
46
|
+
epin.to_s # => "K^'"
|
|
47
|
+
|
|
48
|
+
# Access PIN attributes through the component
|
|
49
|
+
epin.pin.abbr # => :K
|
|
50
|
+
epin.pin.side # => :first
|
|
51
|
+
epin.pin.state # => :normal
|
|
52
|
+
epin.pin.terminal? # => true
|
|
53
|
+
|
|
54
|
+
# Access derivation status
|
|
55
|
+
epin.derived? # => true
|
|
56
|
+
epin.native? # => false
|
|
57
|
+
|
|
58
|
+
# PIN component is a full Sashite::Pin::Identifier instance
|
|
59
|
+
epin.pin.enhanced? # => false
|
|
60
|
+
epin.pin.first_player? # => true
|
|
61
|
+
|
|
62
|
+
# Invalid input raises ArgumentError
|
|
63
|
+
Sashite::Epin.parse("invalid") # => raises ArgumentError
|
|
74
64
|
```
|
|
75
65
|
|
|
76
|
-
|
|
66
|
+
### Formatting (Identifier → String)
|
|
77
67
|
|
|
78
|
-
|
|
68
|
+
Convert an `Identifier` back to an EPIN string.
|
|
79
69
|
|
|
80
70
|
```ruby
|
|
81
|
-
#
|
|
82
|
-
epin = Sashite::Epin.parse("K^") # Native
|
|
83
|
-
epin = Sashite::Epin.parse("K^'") # Derived
|
|
84
|
-
|
|
85
|
-
# Create from PIN component
|
|
71
|
+
# From PIN component
|
|
86
72
|
pin = Sashite::Pin.parse("K^")
|
|
87
|
-
epin = Sashite::Epin.new(pin
|
|
88
|
-
epin
|
|
73
|
+
epin = Sashite::Epin::Identifier.new(pin)
|
|
74
|
+
epin.to_s # => "K^"
|
|
89
75
|
|
|
90
|
-
#
|
|
91
|
-
Sashite::Epin.
|
|
92
|
-
|
|
93
|
-
|
|
76
|
+
# With derivation
|
|
77
|
+
epin = Sashite::Epin::Identifier.new(pin, derived: true)
|
|
78
|
+
epin.to_s # => "K^'"
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
### Validation
|
|
82
|
+
|
|
83
|
+
```ruby
|
|
84
|
+
# Boolean check
|
|
85
|
+
Sashite::Epin.valid?("K") # => true
|
|
86
|
+
Sashite::Epin.valid?("+R^'") # => true
|
|
87
|
+
Sashite::Epin.valid?("invalid") # => false
|
|
88
|
+
Sashite::Epin.valid?("K''") # => false
|
|
89
|
+
Sashite::Epin.valid?("K'^") # => false
|
|
94
90
|
```
|
|
95
91
|
|
|
96
92
|
### Accessing Components
|
|
@@ -99,53 +95,31 @@ Sashite::Epin.valid?("K^''") # => false (multiple markers)
|
|
|
99
95
|
epin = Sashite::Epin.parse("+R^'")
|
|
100
96
|
|
|
101
97
|
# Get PIN component
|
|
102
|
-
epin.pin
|
|
103
|
-
epin.pin.to_s
|
|
98
|
+
epin.pin # => #<Sashite::Pin::Identifier +R^>
|
|
99
|
+
epin.pin.to_s # => "+R^"
|
|
104
100
|
|
|
105
101
|
# Check derivation
|
|
106
|
-
epin.derived?
|
|
102
|
+
epin.derived? # => true
|
|
103
|
+
epin.native? # => false
|
|
107
104
|
|
|
108
105
|
# Serialize
|
|
109
|
-
epin.to_s
|
|
110
|
-
```
|
|
111
|
-
|
|
112
|
-
### Five Fundamental Attributes
|
|
113
|
-
|
|
114
|
-
All attributes accessible via PIN component + derived flag:
|
|
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)
|
|
106
|
+
epin.to_s # => "+R^'"
|
|
127
107
|
```
|
|
128
108
|
|
|
129
|
-
|
|
109
|
+
### Transformations
|
|
130
110
|
|
|
131
|
-
All transformations return new immutable instances
|
|
132
|
-
|
|
133
|
-
### Change Derivation Status
|
|
111
|
+
All transformations return new immutable instances.
|
|
134
112
|
|
|
135
113
|
```ruby
|
|
136
114
|
epin = Sashite::Epin.parse("K^")
|
|
137
115
|
|
|
138
|
-
#
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
# Mark as native
|
|
143
|
-
native = derived.unmark_native
|
|
144
|
-
native.to_s # => "K^"
|
|
116
|
+
# Derivation transformations
|
|
117
|
+
epin.derive.to_s # => "K^'"
|
|
118
|
+
epin.native.to_s # => "K^"
|
|
145
119
|
|
|
146
|
-
#
|
|
147
|
-
|
|
148
|
-
|
|
120
|
+
# Replace PIN component
|
|
121
|
+
new_pin = Sashite::Pin.parse("+Q^")
|
|
122
|
+
epin.with_pin(new_pin).to_s # => "+Q^"
|
|
149
123
|
```
|
|
150
124
|
|
|
151
125
|
### Transform via PIN Component
|
|
@@ -153,506 +127,151 @@ toggled.to_s # => "K^'"
|
|
|
153
127
|
```ruby
|
|
154
128
|
epin = Sashite::Epin.parse("K^'")
|
|
155
129
|
|
|
156
|
-
#
|
|
157
|
-
|
|
158
|
-
epin.with_pin(new_pin).to_s # => "Q^'"
|
|
159
|
-
|
|
160
|
-
# Change type
|
|
161
|
-
epin.with_pin(epin.pin.with_type(:Q)).to_s # => "Q^'"
|
|
130
|
+
# Change abbr
|
|
131
|
+
epin.with_pin(epin.pin.with_abbr(:Q)).to_s # => "Q^'"
|
|
162
132
|
|
|
163
133
|
# Change state
|
|
164
|
-
epin.with_pin(epin.pin.
|
|
165
|
-
|
|
166
|
-
# Remove terminal marker
|
|
167
|
-
epin.with_pin(epin.pin.with_terminal(false)).to_s # => "K'"
|
|
134
|
+
epin.with_pin(epin.pin.enhance).to_s # => "+K^'"
|
|
168
135
|
|
|
169
136
|
# Change side
|
|
170
|
-
epin.with_pin(epin.pin.flip).to_s
|
|
171
|
-
```
|
|
172
|
-
|
|
173
|
-
### Multiple Transformations
|
|
174
|
-
|
|
175
|
-
```ruby
|
|
176
|
-
epin = Sashite::Epin.parse("K^")
|
|
137
|
+
epin.with_pin(epin.pin.flip).to_s # => "k^'"
|
|
177
138
|
|
|
178
|
-
#
|
|
179
|
-
|
|
180
|
-
.with_pin(epin.pin.with_type(:Q).with_state(:enhanced))
|
|
181
|
-
.mark_derived
|
|
182
|
-
|
|
183
|
-
transformed.to_s # => "+Q^'"
|
|
139
|
+
# Remove terminal
|
|
140
|
+
epin.with_pin(epin.pin.non_terminal).to_s # => "K'"
|
|
184
141
|
```
|
|
185
142
|
|
|
186
|
-
|
|
143
|
+
### Component Queries
|
|
187
144
|
|
|
188
|
-
Use the PIN
|
|
145
|
+
Use the PIN API directly:
|
|
189
146
|
|
|
190
147
|
```ruby
|
|
191
148
|
epin = Sashite::Epin.parse("+P^'")
|
|
192
149
|
|
|
193
|
-
# PIN queries
|
|
194
|
-
epin.pin.
|
|
195
|
-
epin.pin.side
|
|
196
|
-
epin.pin.state
|
|
197
|
-
epin.pin.terminal?
|
|
198
|
-
epin.pin.first_player?
|
|
199
|
-
epin.pin.enhanced?
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
epin.
|
|
203
|
-
|
|
204
|
-
# EPIN queries (style)
|
|
205
|
-
epin.derived? # => true
|
|
206
|
-
epin.native? # => false
|
|
150
|
+
# PIN queries
|
|
151
|
+
epin.pin.abbr # => :P
|
|
152
|
+
epin.pin.side # => :first
|
|
153
|
+
epin.pin.state # => :enhanced
|
|
154
|
+
epin.pin.terminal? # => true
|
|
155
|
+
epin.pin.first_player? # => true
|
|
156
|
+
epin.pin.enhanced? # => true
|
|
157
|
+
|
|
158
|
+
# EPIN queries
|
|
159
|
+
epin.derived? # => true
|
|
160
|
+
epin.native? # => false
|
|
207
161
|
|
|
208
162
|
# Compare EPINs
|
|
209
163
|
other = Sashite::Epin.parse("+P^")
|
|
210
|
-
epin.pin.
|
|
211
|
-
epin.pin.same_state?(other.pin)
|
|
212
|
-
epin.
|
|
164
|
+
epin.pin.same_abbr?(other.pin) # => true
|
|
165
|
+
epin.pin.same_state?(other.pin) # => true
|
|
166
|
+
epin.same_derived?(other) # => false
|
|
213
167
|
```
|
|
214
168
|
|
|
215
169
|
## API Reference
|
|
216
170
|
|
|
217
|
-
###
|
|
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
|
|
259
|
-
```
|
|
260
|
-
|
|
261
|
-
#### Equality
|
|
262
|
-
|
|
263
|
-
```ruby
|
|
264
|
-
epin1 == epin2 # True if both PIN and derived flag equal
|
|
265
|
-
```
|
|
266
|
-
|
|
267
|
-
**That's the entire API.** Everything else uses the PIN component API directly.
|
|
268
|
-
|
|
269
|
-
## Format Specification
|
|
270
|
-
|
|
271
|
-
### Structure
|
|
272
|
-
```
|
|
273
|
-
<pin>[']
|
|
274
|
-
```
|
|
275
|
-
|
|
276
|
-
Where:
|
|
277
|
-
- `<pin>` is any valid PIN token
|
|
278
|
-
- `'` is the optional derivation marker
|
|
279
|
-
|
|
280
|
-
### Grammar (BNF)
|
|
281
|
-
```bnf
|
|
282
|
-
<epin> ::= <pin> | <pin> "'"
|
|
283
|
-
|
|
284
|
-
<pin> ::= ["+" | "-"] <letter> ["^"]
|
|
285
|
-
<letter> ::= "A" | ... | "Z" | "a" | ... | "z"
|
|
286
|
-
```
|
|
287
|
-
|
|
288
|
-
### Regular Expression
|
|
289
|
-
```ruby
|
|
290
|
-
/\A[-+]?[A-Za-z]\^?'?\z/
|
|
291
|
-
```
|
|
292
|
-
|
|
293
|
-
## Examples
|
|
294
|
-
|
|
295
|
-
### Basic Identifiers
|
|
171
|
+
### Types
|
|
296
172
|
|
|
297
173
|
```ruby
|
|
298
|
-
#
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
#
|
|
304
|
-
|
|
305
|
-
|
|
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
|
|
320
|
-
|
|
321
|
-
Assume first player uses Chess style, second player uses Makruk style:
|
|
322
|
-
|
|
323
|
-
```ruby
|
|
324
|
-
# First player pieces
|
|
325
|
-
chess_king = Sashite::Epin.parse("K^") # Native Chess king
|
|
326
|
-
makruk_pawn = Sashite::Epin.parse("P'") # Derived Makruk pawn (foreign)
|
|
327
|
-
|
|
328
|
-
chess_king.native? # => true (uses own style)
|
|
329
|
-
makruk_pawn.derived? # => true (uses opponent's style)
|
|
330
|
-
|
|
331
|
-
# Second player pieces
|
|
332
|
-
makruk_king = Sashite::Epin.parse("k^") # Native Makruk king
|
|
333
|
-
chess_pawn = Sashite::Epin.parse("p'") # Derived Chess pawn (foreign)
|
|
334
|
-
|
|
335
|
-
makruk_king.native? # => true
|
|
336
|
-
chess_pawn.derived? # => true
|
|
337
|
-
```
|
|
338
|
-
|
|
339
|
-
### Component Manipulation
|
|
340
|
-
|
|
341
|
-
```ruby
|
|
342
|
-
# Start with native king
|
|
343
|
-
epin = Sashite::Epin.parse("K^")
|
|
344
|
-
|
|
345
|
-
# Convert to derived
|
|
346
|
-
derived = epin.mark_derived
|
|
347
|
-
derived.to_s # => "K^'"
|
|
348
|
-
|
|
349
|
-
# Change to queen (keep derivation)
|
|
350
|
-
queen = derived.with_pin(derived.pin.with_type(:Q))
|
|
351
|
-
queen.to_s # => "Q^'"
|
|
352
|
-
|
|
353
|
-
# Enhance (keep derivation)
|
|
354
|
-
enhanced = derived.with_pin(derived.pin.with_state(:enhanced))
|
|
355
|
-
enhanced.to_s # => "+K^'"
|
|
356
|
-
|
|
357
|
-
# Change side (keep derivation)
|
|
358
|
-
opponent = derived.with_pin(derived.pin.flip)
|
|
359
|
-
opponent.to_s # => "k^'"
|
|
360
|
-
|
|
361
|
-
# Back to native
|
|
362
|
-
native = derived.unmark_native
|
|
363
|
-
native.to_s # => "K^"
|
|
364
|
-
```
|
|
365
|
-
|
|
366
|
-
### Working with PIN Component
|
|
367
|
-
|
|
368
|
-
```ruby
|
|
369
|
-
epin = Sashite::Epin.parse("+R^'")
|
|
370
|
-
|
|
371
|
-
# Extract PIN component
|
|
372
|
-
pin = epin.pin # => "+R^"
|
|
373
|
-
|
|
374
|
-
# Transform PIN
|
|
375
|
-
new_pin = pin.with_type(:B).with_state(:normal) # => "B^"
|
|
376
|
-
|
|
377
|
-
# Create new EPIN with transformed PIN
|
|
378
|
-
new_epin = epin.with_pin(new_pin)
|
|
379
|
-
new_epin.to_s # => "B^'"
|
|
380
|
-
|
|
381
|
-
# Multiple PIN transformations
|
|
382
|
-
complex_pin = pin
|
|
383
|
-
.with_type(:Q)
|
|
384
|
-
.with_state(:diminished)
|
|
385
|
-
.with_terminal(false)
|
|
386
|
-
.flip
|
|
387
|
-
|
|
388
|
-
epin.with_pin(complex_pin).to_s # => "-q'"
|
|
389
|
-
```
|
|
390
|
-
|
|
391
|
-
### Immutability
|
|
392
|
-
|
|
393
|
-
```ruby
|
|
394
|
-
original = Sashite::Epin.parse("K^")
|
|
395
|
-
|
|
396
|
-
# All transformations return new instances
|
|
397
|
-
derived = original.mark_derived
|
|
398
|
-
changed_pin = original.with_pin(original.pin.with_type(:Q))
|
|
399
|
-
|
|
400
|
-
# Original unchanged
|
|
401
|
-
original.to_s # => "K^"
|
|
402
|
-
derived.to_s # => "K^'"
|
|
403
|
-
changed_pin.to_s # => "Q^"
|
|
404
|
-
|
|
405
|
-
# Components are immutable
|
|
406
|
-
pin = original.pin
|
|
407
|
-
pin.frozen? # => true
|
|
408
|
-
original.frozen? # => true
|
|
409
|
-
```
|
|
410
|
-
|
|
411
|
-
## Attribute Mapping
|
|
412
|
-
|
|
413
|
-
EPIN exposes all five fundamental attributes from the Sashité Game Protocol:
|
|
414
|
-
|
|
415
|
-
| Protocol Attribute | EPIN Access | Example |
|
|
416
|
-
|-------------------|-------------|---------|
|
|
417
|
-
| **Piece Name** | `epin.pin.type` | `:K` (King), `:R` (Rook) |
|
|
418
|
-
| **Piece Side** | `epin.pin.side` | `:first`, `:second` |
|
|
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) |
|
|
422
|
-
|
|
423
|
-
## Design Principles
|
|
424
|
-
|
|
425
|
-
### 1. Pure Composition
|
|
426
|
-
|
|
427
|
-
EPIN doesn't reimplement PIN features — it extends PIN minimally:
|
|
428
|
-
|
|
429
|
-
```ruby
|
|
430
|
-
# EPIN is just PIN + derived flag
|
|
431
|
-
class Identifier
|
|
174
|
+
# Identifier represents a parsed EPIN combining PIN with derivation status.
|
|
175
|
+
class Sashite::Epin::Identifier
|
|
176
|
+
# Creates an Identifier from a PIN component.
|
|
177
|
+
# Raises ArgumentError if the PIN is invalid.
|
|
178
|
+
#
|
|
179
|
+
# @param pin [Sashite::Pin::Identifier] PIN component
|
|
180
|
+
# @param derived [Boolean] Derived status
|
|
181
|
+
# @return [Identifier]
|
|
432
182
|
def initialize(pin, derived: false)
|
|
433
|
-
@pin = pin
|
|
434
|
-
@derived = !!derived
|
|
435
|
-
end
|
|
436
|
-
end
|
|
437
|
-
```
|
|
438
|
-
|
|
439
|
-
### 2. Absolute Minimal API
|
|
440
|
-
|
|
441
|
-
**6 core methods only:**
|
|
442
|
-
1. `new(pin, derived: false)` — create from PIN
|
|
443
|
-
2. `pin` — get PIN component
|
|
444
|
-
3. `derived?` — check derivation
|
|
445
|
-
4. `to_s` — serialize
|
|
446
|
-
5. `with_pin(new_pin)` — replace PIN
|
|
447
|
-
6. `with_derived(boolean)` — change derivation
|
|
448
|
-
|
|
449
|
-
Everything else uses the PIN component API directly.
|
|
450
|
-
|
|
451
|
-
### 3. Component Transparency
|
|
452
|
-
|
|
453
|
-
Access PIN directly — no wrappers:
|
|
454
|
-
|
|
455
|
-
```ruby
|
|
456
|
-
# Use PIN API directly
|
|
457
|
-
epin.pin.type
|
|
458
|
-
epin.pin.with_type(:Q)
|
|
459
|
-
epin.pin.enhanced?
|
|
460
|
-
epin.pin.flip
|
|
461
|
-
|
|
462
|
-
# No need for wrapper methods like:
|
|
463
|
-
# epin.type
|
|
464
|
-
# epin.with_type
|
|
465
|
-
# epin.enhanced?
|
|
466
|
-
# epin.flip
|
|
467
|
-
```
|
|
468
183
|
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
#
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
184
|
+
# Returns the PIN component.
|
|
185
|
+
#
|
|
186
|
+
# @return [Sashite::Pin::Identifier]
|
|
187
|
+
def pin
|
|
188
|
+
|
|
189
|
+
# Returns true if derived style.
|
|
190
|
+
#
|
|
191
|
+
# @return [Boolean]
|
|
192
|
+
def derived?
|
|
193
|
+
|
|
194
|
+
# Returns true if native style.
|
|
195
|
+
#
|
|
196
|
+
# @return [Boolean]
|
|
197
|
+
def native?
|
|
198
|
+
|
|
199
|
+
# Returns the EPIN string representation.
|
|
200
|
+
#
|
|
201
|
+
# @return [String]
|
|
202
|
+
def to_s
|
|
480
203
|
end
|
|
481
204
|
```
|
|
482
205
|
|
|
483
|
-
###
|
|
484
|
-
|
|
485
|
-
All instances frozen. Transformations return new instances:
|
|
206
|
+
### Parsing
|
|
486
207
|
|
|
487
208
|
```ruby
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
209
|
+
# Parses an EPIN string into an Identifier.
|
|
210
|
+
# Raises ArgumentError if the string is not valid.
|
|
211
|
+
#
|
|
212
|
+
# @param string [String] EPIN string
|
|
213
|
+
# @return [Identifier]
|
|
214
|
+
# @raise [ArgumentError] if invalid
|
|
215
|
+
def Sashite::Epin.parse(string)
|
|
493
216
|
```
|
|
494
217
|
|
|
495
|
-
|
|
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)
|
|
218
|
+
### Validation
|
|
502
219
|
|
|
503
220
|
```ruby
|
|
504
|
-
#
|
|
505
|
-
#
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
"k" # Second player king in Makruk style (native)
|
|
510
|
-
"k'" # Second player king in Chess style (derived from opponent)
|
|
221
|
+
# Reports whether string is a valid EPIN.
|
|
222
|
+
#
|
|
223
|
+
# @param string [String] EPIN string
|
|
224
|
+
# @return [Boolean]
|
|
225
|
+
def Sashite::Epin.valid?(string)
|
|
511
226
|
```
|
|
512
227
|
|
|
513
|
-
###
|
|
228
|
+
### Transformations
|
|
514
229
|
|
|
515
230
|
```ruby
|
|
516
|
-
#
|
|
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
|
|
231
|
+
# PIN replacement (returns new Identifier)
|
|
232
|
+
def with_pin(new_pin) # => Identifier with different PIN
|
|
525
233
|
|
|
526
|
-
|
|
527
|
-
#
|
|
528
|
-
#
|
|
529
|
-
|
|
530
|
-
epin = Sashite::Epin.parse("p'")
|
|
531
|
-
# Piece Side: second
|
|
532
|
-
# Style: Chess (first's native) → Derived piece
|
|
234
|
+
# Derivation transformations
|
|
235
|
+
def derive # => Identifier with derived: true
|
|
236
|
+
def native # => Identifier with derived: false
|
|
533
237
|
```
|
|
534
238
|
|
|
535
|
-
|
|
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
|
|
239
|
+
### Errors
|
|
544
240
|
|
|
545
|
-
|
|
546
|
-
begin
|
|
547
|
-
Sashite::Epin.parse("K''")
|
|
548
|
-
rescue ArgumentError => e
|
|
549
|
-
# Invalid format
|
|
550
|
-
end
|
|
241
|
+
All parsing and validation errors raise `ArgumentError` with descriptive messages:
|
|
551
242
|
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
# PIN validation error
|
|
557
|
-
end
|
|
558
|
-
```
|
|
243
|
+
| Message | Cause |
|
|
244
|
+
|---------|-------|
|
|
245
|
+
| `"invalid derivation marker"` | Derivation marker misplaced or duplicated |
|
|
246
|
+
| `"invalid PIN component: ..."` | PIN parsing failed |
|
|
559
247
|
|
|
560
|
-
##
|
|
248
|
+
## PIN Compatibility
|
|
561
249
|
|
|
562
|
-
|
|
250
|
+
Every valid PIN is a valid EPIN (native by default):
|
|
563
251
|
|
|
564
252
|
```ruby
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
epin
|
|
568
|
-
|
|
569
|
-
|
|
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)
|
|
253
|
+
%w[K +R -p K^ +R^].each do |pin_token|
|
|
254
|
+
epin = Sashite::Epin.parse(pin_token)
|
|
255
|
+
epin.native? # => true
|
|
256
|
+
epin.to_s # => pin_token
|
|
257
|
+
end
|
|
619
258
|
```
|
|
620
259
|
|
|
621
|
-
|
|
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
|
|
260
|
+
## Design Principles
|
|
634
261
|
|
|
635
|
-
- **
|
|
636
|
-
- **
|
|
637
|
-
- **
|
|
638
|
-
- **
|
|
639
|
-
- **
|
|
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
|
|
262
|
+
- **Pure composition**: EPIN composes PIN without reimplementing features
|
|
263
|
+
- **Minimal API**: Core methods (`pin`, `derived?`, `native?`, `to_s`) plus transformations
|
|
264
|
+
- **Component transparency**: Access PIN directly, no wrapper methods
|
|
265
|
+
- **Immutable identifiers**: Frozen instances prevent mutation
|
|
266
|
+
- **Ruby idioms**: `valid?` predicate, `to_s` conversion, `ArgumentError` for invalid input
|
|
644
267
|
|
|
645
268
|
## Related Specifications
|
|
646
269
|
|
|
647
|
-
- [
|
|
648
|
-
- [EPIN
|
|
649
|
-
- [
|
|
650
|
-
- [
|
|
270
|
+
- [Game Protocol](https://sashite.dev/game-protocol/) — Conceptual foundation
|
|
271
|
+
- [EPIN Specification](https://sashite.dev/specs/epin/1.0.0/) — Official specification
|
|
272
|
+
- [EPIN Examples](https://sashite.dev/specs/epin/1.0.0/examples/) — Usage examples
|
|
273
|
+
- [PIN Specification](https://sashite.dev/specs/pin/1.0.0/) — Base component
|
|
651
274
|
|
|
652
275
|
## License
|
|
653
276
|
|
|
654
|
-
Available as open source under the [
|
|
655
|
-
|
|
656
|
-
## About
|
|
657
|
-
|
|
658
|
-
Maintained by [Sashité](https://sashite.com/) — promoting chess variants and sharing the beauty of board game cultures.
|
|
277
|
+
Available as open source under the [Apache License 2.0](https://opensource.org/licenses/Apache-2.0).
|