sashite-epin 1.2.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 +299 -420
- data/lib/sashite/epin.rb +274 -56
- metadata +6 -11
- data/lib/sashite/epin/identifier.rb +0 -588
data/README.md
CHANGED
|
@@ -1,17 +1,19 @@
|
|
|
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
|
|
|
12
|
-
EPIN (Extended Piece Identifier Notation) extends [PIN
|
|
12
|
+
EPIN (Extended Piece Identifier Notation) extends [PIN](https://sashite.dev/specs/pin/1.0.0/) by adding a **derivation marker** to track piece style in cross-style games.
|
|
13
13
|
|
|
14
|
-
|
|
14
|
+
**EPIN is simply: PIN + optional style derivation marker (`'`)**
|
|
15
|
+
|
|
16
|
+
This gem implements the [EPIN Specification v1.0.0](https://sashite.dev/specs/epin/1.0.0/) with a minimal compositional API.
|
|
15
17
|
|
|
16
18
|
## Installation
|
|
17
19
|
|
|
@@ -26,515 +28,392 @@ Or install manually:
|
|
|
26
28
|
gem install sashite-epin
|
|
27
29
|
```
|
|
28
30
|
|
|
29
|
-
|
|
31
|
+
This will also install `sashite-pin` as a dependency.
|
|
32
|
+
|
|
33
|
+
## Core Concept
|
|
30
34
|
|
|
31
35
|
```ruby
|
|
32
36
|
require "sashite/epin"
|
|
33
37
|
|
|
34
|
-
#
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
identifier.type # => :K
|
|
38
|
-
identifier.side # => :first
|
|
39
|
-
identifier.state # => :normal
|
|
40
|
-
identifier.native? # => true
|
|
41
|
-
|
|
42
|
-
# Create identifiers directly
|
|
43
|
-
identifier = Sashite::Epin::Identifier.new(:R, :second, :enhanced, false) # => #<Epin::Identifier type=:R side=:second state=:enhanced native=false>
|
|
44
|
-
|
|
45
|
-
# Validate EPIN strings
|
|
46
|
-
Sashite::Epin.valid?("K") # => true
|
|
47
|
-
Sashite::Epin.valid?("+R'") # => true
|
|
48
|
-
Sashite::Epin.valid?("invalid") # => false
|
|
49
|
-
|
|
50
|
-
# Style derivation with apostrophe suffix
|
|
51
|
-
native_king = Sashite::Epin.parse("K") # => #<Epin::Identifier type=:K side=:first state=:normal native=true>
|
|
52
|
-
foreign_king = Sashite::Epin.parse("K'") # => #<Epin::Identifier type=:K side=:first state=:normal native=false>
|
|
53
|
-
|
|
54
|
-
native_king.to_s # => "K"
|
|
55
|
-
foreign_king.to_s # => "K'"
|
|
56
|
-
|
|
57
|
-
# State manipulation (returns new immutable instances)
|
|
58
|
-
enhanced = identifier.enhance # => #<Epin::Identifier type=:K side=:first state=:enhanced native=true>
|
|
59
|
-
enhanced.to_s # => "+K"
|
|
60
|
-
diminished = identifier.diminish # => #<Epin::Identifier type=:K side=:first state=:diminished native=true>
|
|
61
|
-
diminished.to_s # => "-K"
|
|
62
|
-
|
|
63
|
-
# Style derivation manipulation
|
|
64
|
-
foreign_piece = identifier.derive # => #<Epin::Identifier type=:K side=:first state=:normal native=false>
|
|
65
|
-
foreign_piece.to_s # => "K'"
|
|
66
|
-
back_to_native = foreign_piece.underive # => #<Epin::Identifier type=:K side=:first state=:normal native=true>
|
|
67
|
-
back_to_native.to_s # => "K"
|
|
68
|
-
|
|
69
|
-
# Side manipulation
|
|
70
|
-
flipped = identifier.flip # => #<Epin::Identifier type=:K side=:second state=:normal native=true>
|
|
71
|
-
flipped.to_s # => "k"
|
|
72
|
-
|
|
73
|
-
# Type manipulation
|
|
74
|
-
queen = identifier.with_type(:Q) # => #<Epin::Identifier type=:Q side=:first state=:normal native=true>
|
|
75
|
-
queen.to_s # => "Q"
|
|
76
|
-
|
|
77
|
-
# Style queries
|
|
78
|
-
identifier.native? # => true
|
|
79
|
-
foreign_king.derived? # => true
|
|
80
|
-
foreign_king.foreign? # => true (alias for derived?)
|
|
81
|
-
|
|
82
|
-
# State queries
|
|
83
|
-
identifier.normal? # => true
|
|
84
|
-
enhanced.enhanced? # => true
|
|
85
|
-
diminished.diminished? # => true
|
|
86
|
-
|
|
87
|
-
# Side queries
|
|
88
|
-
identifier.first_player? # => true
|
|
89
|
-
flipped.second_player? # => true
|
|
90
|
-
|
|
91
|
-
# Attribute access
|
|
92
|
-
identifier.letter # => "K"
|
|
93
|
-
enhanced.prefix # => "+"
|
|
94
|
-
foreign_king.suffix # => "'"
|
|
95
|
-
identifier.suffix # => ""
|
|
96
|
-
|
|
97
|
-
# Type and side comparison
|
|
98
|
-
king1 = Sashite::Epin.parse("K")
|
|
99
|
-
king2 = Sashite::Epin.parse("k")
|
|
100
|
-
queen = Sashite::Epin.parse("Q")
|
|
101
|
-
|
|
102
|
-
king1.same_type?(king2) # => true (both kings)
|
|
103
|
-
king1.same_side?(queen) # => true (both first player)
|
|
104
|
-
king1.same_type?(queen) # => false (different types)
|
|
105
|
-
|
|
106
|
-
# Style comparison
|
|
107
|
-
native_king = Sashite::Epin.parse("K")
|
|
108
|
-
foreign_king = Sashite::Epin.parse("K'")
|
|
109
|
-
|
|
110
|
-
native_king.same_style?(foreign_king) # => false (different derivation)
|
|
111
|
-
|
|
112
|
-
# Functional transformations can be chained
|
|
113
|
-
pawn = Sashite::Epin.parse("P")
|
|
114
|
-
enemy_foreign_promoted = pawn.flip.derive.enhance # => "+p'" (second player foreign promoted pawn)
|
|
115
|
-
```
|
|
38
|
+
# EPIN is just PIN + derived flag
|
|
39
|
+
pin = Sashite::Pin.parse("K^")
|
|
40
|
+
epin = Sashite::Epin.new(pin)
|
|
116
41
|
|
|
117
|
-
|
|
42
|
+
epin.to_s # => "K^" (native)
|
|
43
|
+
epin.pin # => #<Sashite::Pin K^>
|
|
44
|
+
epin.derived # => false
|
|
118
45
|
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
46
|
+
# Mark as derived
|
|
47
|
+
derived_epin = epin.mark_derived
|
|
48
|
+
derived_epin.to_s # => "K^'" (derived from opposite side's style)
|
|
122
49
|
```
|
|
123
50
|
|
|
124
|
-
|
|
51
|
+
**That's it.** All piece attributes come from the PIN component.
|
|
52
|
+
|
|
53
|
+
## Usage
|
|
125
54
|
|
|
126
|
-
|
|
55
|
+
```ruby
|
|
56
|
+
require "sashite/epin"
|
|
127
57
|
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
58
|
+
# Parse EPIN strings
|
|
59
|
+
epin = Sashite::Epin.parse("K^'")
|
|
60
|
+
epin.to_s # => "K^'"
|
|
61
|
+
|
|
62
|
+
# Access five fundamental attributes through PIN component + derived flag
|
|
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)
|
|
68
|
+
|
|
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
|
+
```
|
|
136
74
|
|
|
137
|
-
|
|
138
|
-
- `'`: Foreign style (piece has opposite side's native style)
|
|
139
|
-
- No suffix: Native style (piece has current side's native style)
|
|
75
|
+
### Creating Identifiers
|
|
140
76
|
|
|
141
|
-
### Regular Expression
|
|
142
77
|
```ruby
|
|
143
|
-
|
|
78
|
+
# Parse from string
|
|
79
|
+
epin = Sashite::Epin.parse("K^") # Native
|
|
80
|
+
epin = Sashite::Epin.parse("K^'") # Derived
|
|
81
|
+
|
|
82
|
+
# Create from PIN component
|
|
83
|
+
pin = Sashite::Pin.parse("K^")
|
|
84
|
+
epin = Sashite::Epin.new(pin) # Native (default)
|
|
85
|
+
epin = Sashite::Epin.new(pin, derived: true) # Derived
|
|
86
|
+
|
|
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)
|
|
144
92
|
```
|
|
145
93
|
|
|
146
|
-
###
|
|
147
|
-
- `K` - First player king (native style, normal state)
|
|
148
|
-
- `k'` - Second player king (foreign style, normal state)
|
|
149
|
-
- `+R'` - First player rook (foreign style, enhanced state)
|
|
150
|
-
- `-p` - Second player pawn (native style, diminished state)
|
|
94
|
+
### Accessing Components
|
|
151
95
|
|
|
152
|
-
|
|
96
|
+
```ruby
|
|
97
|
+
epin = Sashite::Epin.parse("+R^'")
|
|
153
98
|
|
|
154
|
-
|
|
99
|
+
# Get PIN component
|
|
100
|
+
epin.pin # => #<Sashite::Pin +R^>
|
|
101
|
+
epin.pin.to_s # => "+R^"
|
|
155
102
|
|
|
156
|
-
|
|
157
|
-
#
|
|
158
|
-
#
|
|
103
|
+
# Check derivation
|
|
104
|
+
epin.derived # => true
|
|
105
|
+
epin.derived? # => true
|
|
106
|
+
epin.native? # => false
|
|
107
|
+
|
|
108
|
+
# Serialize
|
|
109
|
+
epin.to_s # => "+R^'"
|
|
110
|
+
```
|
|
159
111
|
|
|
160
|
-
|
|
161
|
-
white_king = Sashite::Epin.identifier(:K, :first, :normal, true) # => "K" (Chess king)
|
|
162
|
-
black_king = Sashite::Epin.identifier(:K, :second, :normal, true) # => "k" (Shōgi king)
|
|
112
|
+
### Transformations
|
|
163
113
|
|
|
164
|
-
|
|
165
|
-
white_shogi_king = Sashite::Epin.identifier(:K, :first, :normal, false) # => "K'" (Shōgi king for white)
|
|
166
|
-
black_chess_king = Sashite::Epin.identifier(:K, :second, :normal, false) # => "k'" (Chess king for black)
|
|
114
|
+
All transformations return new immutable instances.
|
|
167
115
|
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
116
|
+
```ruby
|
|
117
|
+
epin = Sashite::Epin.parse("K^")
|
|
118
|
+
|
|
119
|
+
# Mark as derived
|
|
120
|
+
derived = epin.mark_derived
|
|
121
|
+
derived.to_s # => "K^'"
|
|
122
|
+
|
|
123
|
+
# Mark as native
|
|
124
|
+
native = derived.unmark_derived
|
|
125
|
+
native.to_s # => "K^"
|
|
171
126
|
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
black_promoted_pawn.native? # => true
|
|
127
|
+
# Set explicitly
|
|
128
|
+
toggled = epin.with_derived(true)
|
|
129
|
+
toggled.to_s # => "K^'"
|
|
176
130
|
```
|
|
177
131
|
|
|
178
|
-
###
|
|
132
|
+
### Transform via PIN Component
|
|
179
133
|
|
|
180
134
|
```ruby
|
|
181
|
-
|
|
182
|
-
# All pieces are native, so EPIN behaves exactly like PIN
|
|
135
|
+
epin = Sashite::Epin.parse("K^'")
|
|
183
136
|
|
|
184
|
-
|
|
185
|
-
|
|
137
|
+
# Replace PIN component
|
|
138
|
+
new_pin = epin.pin.with_type(:Q)
|
|
139
|
+
epin.with_pin(new_pin).to_s # => "Q^'"
|
|
186
140
|
|
|
187
|
-
|
|
188
|
-
|
|
141
|
+
# Change state
|
|
142
|
+
new_pin = epin.pin.enhance
|
|
143
|
+
epin.with_pin(new_pin).to_s # => "+K^'"
|
|
189
144
|
|
|
190
|
-
#
|
|
191
|
-
|
|
192
|
-
|
|
145
|
+
# Remove terminal marker
|
|
146
|
+
new_pin = epin.pin.unmark_terminal
|
|
147
|
+
epin.with_pin(new_pin).to_s # => "K'"
|
|
148
|
+
|
|
149
|
+
# Change side
|
|
150
|
+
new_pin = epin.pin.flip
|
|
151
|
+
epin.with_pin(new_pin).to_s # => "k^'"
|
|
193
152
|
```
|
|
194
153
|
|
|
195
|
-
###
|
|
154
|
+
### Multiple Transformations
|
|
196
155
|
|
|
197
156
|
```ruby
|
|
198
|
-
|
|
199
|
-
chess_queen = Sashite::Epin.parse("q'") # Black chess queen (foreign for shōgi player)
|
|
200
|
-
captured = chess_queen.flip.with_type(:P).underive # Becomes white native pawn
|
|
157
|
+
epin = Sashite::Epin.parse("K^")
|
|
201
158
|
|
|
202
|
-
|
|
203
|
-
|
|
159
|
+
# Transform PIN and derivation
|
|
160
|
+
new_pin = epin.pin.with_type(:Q).enhance
|
|
161
|
+
transformed = epin.with_pin(new_pin).mark_derived
|
|
204
162
|
|
|
205
|
-
#
|
|
206
|
-
shogi_piece = Sashite::Epin.parse("r") # Black shōgi rook (native)
|
|
207
|
-
foreign_piece = shogi_piece.derive # Convert to foreign style
|
|
208
|
-
foreign_piece.to_s # => "r'" (black foreign rook)
|
|
163
|
+
transformed.to_s # => "+Q^'"
|
|
209
164
|
```
|
|
210
165
|
|
|
211
|
-
|
|
166
|
+
### Component Queries
|
|
212
167
|
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
- `Sashite::Epin.valid?(epin_string)` - Check if string is valid EPIN notation
|
|
216
|
-
- `Sashite::Epin.parse(epin_string)` - Parse EPIN string into Identifier object
|
|
217
|
-
- `Sashite::Epin.identifier(type, side, state, native)` - Create identifier instance directly
|
|
218
|
-
|
|
219
|
-
### Identifier Class
|
|
220
|
-
|
|
221
|
-
#### Creation and Parsing
|
|
222
|
-
- `Sashite::Epin::Identifier.new(type, side, state = :normal, native = true)` - Create identifier instance
|
|
223
|
-
- `Sashite::Epin::Identifier.parse(epin_string)` - Parse EPIN string (same as module method)
|
|
224
|
-
- `Sashite::Epin::Identifier.valid?(epin_string)` - Validate EPIN string (class method)
|
|
225
|
-
|
|
226
|
-
#### Attribute Access
|
|
227
|
-
- `#type` - Get piece type (symbol :A to :Z, always uppercase)
|
|
228
|
-
- `#side` - Get player side (:first or :second)
|
|
229
|
-
- `#state` - Get state (:normal, :enhanced, or :diminished)
|
|
230
|
-
- `#native` - Get style derivation (true for native, false for foreign)
|
|
231
|
-
- `#letter` - Get letter representation (string, case determined by side)
|
|
232
|
-
- `#prefix` - Get state prefix (string: "+", "-", or "")
|
|
233
|
-
- `#suffix` - Get derivation suffix (string: "'" or "")
|
|
234
|
-
- `#to_s` - Convert to EPIN string representation
|
|
235
|
-
|
|
236
|
-
#### Style Queries
|
|
237
|
-
- `#native?` - Check if native style (current side's native style)
|
|
238
|
-
- `#derived?` - Check if foreign style (opposite side's native style)
|
|
239
|
-
- `#foreign?` - Alias for `#derived?`
|
|
240
|
-
|
|
241
|
-
#### State Queries
|
|
242
|
-
- `#normal?` - Check if normal state (no modifiers)
|
|
243
|
-
- `#enhanced?` - Check if enhanced state
|
|
244
|
-
- `#diminished?` - Check if diminished state
|
|
245
|
-
|
|
246
|
-
#### Side Queries
|
|
247
|
-
- `#first_player?` - Check if first player identifier
|
|
248
|
-
- `#second_player?` - Check if second player identifier
|
|
249
|
-
|
|
250
|
-
#### State Transformations (immutable - return new instances)
|
|
251
|
-
- `#enhance` - Create enhanced version
|
|
252
|
-
- `#unenhance` - Remove enhanced state
|
|
253
|
-
- `#diminish` - Create diminished version
|
|
254
|
-
- `#undiminish` - Remove diminished state
|
|
255
|
-
- `#normalize` - Remove all state modifiers
|
|
256
|
-
|
|
257
|
-
#### Style Transformations (immutable - return new instances)
|
|
258
|
-
- `#derive` - Convert to foreign style (add derivation suffix)
|
|
259
|
-
- `#underive` - Convert to native style (remove derivation suffix)
|
|
260
|
-
- `#flip` - Switch player (change side)
|
|
261
|
-
|
|
262
|
-
#### Attribute Transformations (immutable - return new instances)
|
|
263
|
-
- `#with_type(new_type)` - Create identifier with different type
|
|
264
|
-
- `#with_side(new_side)` - Create identifier with different side
|
|
265
|
-
- `#with_state(new_state)` - Create identifier with different state
|
|
266
|
-
- `#with_derivation(native)` - Create identifier with different derivation
|
|
267
|
-
|
|
268
|
-
#### Comparison Methods
|
|
269
|
-
- `#same_type?(other)` - Check if same piece type
|
|
270
|
-
- `#same_side?(other)` - Check if same side
|
|
271
|
-
- `#same_state?(other)` - Check if same state
|
|
272
|
-
- `#same_style?(other)` - Check if same style derivation
|
|
273
|
-
- `#==(other)` - Full equality comparison
|
|
274
|
-
|
|
275
|
-
### Constants
|
|
276
|
-
- `Sashite::Epin::Identifier::NATIVE` - Constant for native style (`true`)
|
|
277
|
-
- `Sashite::Epin::Identifier::FOREIGN` - Constant for foreign style (`false`)
|
|
278
|
-
- `Sashite::Epin::Identifier::DERIVATION_SUFFIX` - Derivation suffix for foreign pieces (`"'"`)
|
|
279
|
-
|
|
280
|
-
## Advanced Usage
|
|
281
|
-
|
|
282
|
-
### Style Derivation Examples
|
|
168
|
+
Use the PIN API directly:
|
|
283
169
|
|
|
284
170
|
```ruby
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
#
|
|
288
|
-
#
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
#
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
171
|
+
epin = Sashite::Epin.parse("+P^'")
|
|
172
|
+
|
|
173
|
+
# PIN queries (name, side, state, terminal)
|
|
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 # => "^"
|
|
183
|
+
|
|
184
|
+
# EPIN queries (style)
|
|
185
|
+
epin.derived? # => true
|
|
186
|
+
epin.native? # => false
|
|
187
|
+
|
|
188
|
+
# Compare EPINs
|
|
189
|
+
other = Sashite::Epin.parse("+P^")
|
|
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)
|
|
301
193
|
```
|
|
302
194
|
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
195
|
+
## Five Fundamental Attributes
|
|
196
|
+
|
|
197
|
+
EPIN exposes all five attributes from the [Sashité Game Protocol](https://sashite.dev/game-protocol/):
|
|
198
|
+
|
|
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) |
|
|
206
|
+
|
|
207
|
+
## Format Specification
|
|
208
|
+
|
|
209
|
+
### Structure
|
|
210
|
+
|
|
211
|
+
```
|
|
212
|
+
<pin>[']
|
|
320
213
|
```
|
|
321
214
|
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
def initialize(first_style, second_style)
|
|
326
|
-
@first_style = first_style
|
|
327
|
-
@second_style = second_style
|
|
328
|
-
@pieces = {}
|
|
329
|
-
end
|
|
330
|
-
|
|
331
|
-
def place(square, piece)
|
|
332
|
-
@pieces[square] = piece
|
|
333
|
-
end
|
|
334
|
-
|
|
335
|
-
def capture_with_style_change(from_square, to_square, new_type = nil)
|
|
336
|
-
captured = @pieces[to_square]
|
|
337
|
-
capturing = @pieces.delete(from_square)
|
|
338
|
-
|
|
339
|
-
return nil unless captured && capturing
|
|
340
|
-
|
|
341
|
-
# Style mutation: captured piece becomes native to capturing side
|
|
342
|
-
mutated = captured.flip.underive
|
|
343
|
-
mutated = mutated.with_type(new_type) if new_type
|
|
344
|
-
|
|
345
|
-
@pieces[to_square] = capturing
|
|
346
|
-
mutated # Return mutated captured piece for hand
|
|
347
|
-
end
|
|
348
|
-
|
|
349
|
-
def pieces_by_style_derivation
|
|
350
|
-
{
|
|
351
|
-
native: @pieces.select { |_, piece| piece.native? },
|
|
352
|
-
foreign: @pieces.select { |_, piece| piece.derived? }
|
|
353
|
-
}
|
|
354
|
-
end
|
|
355
|
-
end
|
|
215
|
+
Where:
|
|
216
|
+
- `<pin>` is any valid PIN token
|
|
217
|
+
- `'` is the optional derivation marker
|
|
356
218
|
|
|
357
|
-
|
|
358
|
-
board = CrossStyleGameBoard.new(:chess, :shogi)
|
|
359
|
-
board.place("e1", Sashite::Epin.identifier(:K, :first, :normal, true)) # Chess king
|
|
360
|
-
board.place("e8", Sashite::Epin.identifier(:K, :second, :normal, true)) # Shōgi king
|
|
361
|
-
board.place("d4", Sashite::Epin.identifier(:Q, :first, :normal, false)) # Chess queen using Shōgi style
|
|
219
|
+
### Grammar (EBNF)
|
|
362
220
|
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
221
|
+
```ebnf
|
|
222
|
+
epin ::= pin | pin "'"
|
|
223
|
+
pin ::= ["+" | "-"] letter ["^"]
|
|
224
|
+
letter ::= "A" | ... | "Z" | "a" | ... | "z"
|
|
366
225
|
```
|
|
367
226
|
|
|
368
|
-
###
|
|
227
|
+
### Regular Expression
|
|
228
|
+
|
|
369
229
|
```ruby
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
# All PIN strings are valid EPIN strings (native pieces)
|
|
373
|
-
Sashite::Epin.parse(pin_string)
|
|
374
|
-
end
|
|
230
|
+
/\A[-+]?[A-Za-z]\^?'?\z/
|
|
231
|
+
```
|
|
375
232
|
|
|
376
|
-
|
|
377
|
-
# Only native EPIN pieces can be converted to PIN
|
|
378
|
-
return nil unless epin_identifier.native?
|
|
233
|
+
### Examples
|
|
379
234
|
|
|
380
|
-
|
|
381
|
-
|
|
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 |
|
|
382
243
|
|
|
383
|
-
|
|
384
|
-
pin_pieces = %w[K Q +R -P k q r p]
|
|
385
|
-
epin_pieces = pin_pieces.map { |pin| convert_pin_to_epin(pin) }
|
|
244
|
+
## Cross-Style Game Example
|
|
386
245
|
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
246
|
+
In a chess-vs-makruk cross-style match where:
|
|
247
|
+
- First side native style = chess
|
|
248
|
+
- Second side native style = makruk
|
|
390
249
|
|
|
391
|
-
### Move Validation Example
|
|
392
250
|
```ruby
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
case [piece.type, piece.native? ? style_rules[:native] : style_rules[:foreign]]
|
|
397
|
-
when %i[P chess] # Chess pawn
|
|
398
|
-
(piece.first_player? && target_rank == 8) ||
|
|
399
|
-
(piece.second_player? && target_rank == 1)
|
|
400
|
-
when %i[P shogi] # Shōgi pawn
|
|
401
|
-
(piece.first_player? && target_rank >= 7) ||
|
|
402
|
-
(piece.second_player? && target_rank <= 3)
|
|
403
|
-
when %i[R shogi], %i[B shogi] # Shōgi major pieces
|
|
404
|
-
true
|
|
405
|
-
else
|
|
406
|
-
false
|
|
407
|
-
end
|
|
408
|
-
end
|
|
251
|
+
# First player pieces
|
|
252
|
+
chess_king = Sashite::Epin.parse("K^") # Native Chess king
|
|
253
|
+
makruk_pawn = Sashite::Epin.parse("P'") # Derived Makruk pawn (foreign)
|
|
409
254
|
|
|
410
|
-
#
|
|
411
|
-
|
|
255
|
+
chess_king.native? # => true (uses own style)
|
|
256
|
+
makruk_pawn.derived? # => true (uses opponent's style)
|
|
412
257
|
|
|
413
|
-
|
|
258
|
+
# Second player pieces
|
|
259
|
+
makruk_king = Sashite::Epin.parse("k^") # Native Makruk king
|
|
260
|
+
chess_pawn = Sashite::Epin.parse("p'") # Derived Chess pawn (foreign)
|
|
414
261
|
|
|
415
|
-
|
|
416
|
-
|
|
262
|
+
makruk_king.native? # => true
|
|
263
|
+
chess_pawn.derived? # => true
|
|
417
264
|
```
|
|
418
265
|
|
|
419
|
-
##
|
|
266
|
+
## API Reference
|
|
420
267
|
|
|
421
|
-
|
|
268
|
+
### Parsing and Validation
|
|
422
269
|
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
270
|
+
```ruby
|
|
271
|
+
Sashite::Epin.parse(epin_string) # => Sashite::Epin | raises ArgumentError
|
|
272
|
+
Sashite::Epin.valid?(epin_string) # => boolean
|
|
273
|
+
```
|
|
427
274
|
|
|
428
|
-
|
|
429
|
-
- **Reliability**: Reuses battle-tested PIN logic
|
|
430
|
-
- **Maintainability**: PIN updates automatically benefit EPIN
|
|
431
|
-
- **Consistency**: PIN and EPIN identifiers behave identically for shared attributes
|
|
432
|
-
- **Performance**: Minimal overhead over pure PIN implementation
|
|
275
|
+
### Creation
|
|
433
276
|
|
|
434
|
-
|
|
277
|
+
```ruby
|
|
278
|
+
Sashite::Epin.new(pin) # Native (default)
|
|
279
|
+
Sashite::Epin.new(pin, derived: true) # Derived
|
|
280
|
+
```
|
|
435
281
|
|
|
436
|
-
|
|
282
|
+
### Conversion
|
|
437
283
|
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
| **Side** | Letter case in display | `K` = First player, `k` = Second player | Case is determined by side during rendering |
|
|
442
|
-
| **State** | Optional prefix | `+K` = Enhanced, `-K` = Diminished, `K` = Normal | |
|
|
443
|
-
| **Style** | Derivation suffix | `K` = Native style, `K'` = Foreign style | |
|
|
284
|
+
```ruby
|
|
285
|
+
epin.to_s # => String
|
|
286
|
+
```
|
|
444
287
|
|
|
445
|
-
|
|
446
|
-
- **No suffix**: Piece has the **native style** of its current side
|
|
447
|
-
- **Apostrophe suffix (`'`)**: Piece has the **foreign style** (opposite side's native style)
|
|
288
|
+
### Transformations
|
|
448
289
|
|
|
449
|
-
|
|
290
|
+
All transformations return new `Sashite::Epin` instances:
|
|
450
291
|
|
|
451
|
-
|
|
292
|
+
```ruby
|
|
293
|
+
# PIN replacement
|
|
294
|
+
epin.with_pin(new_pin) # => Sashite::Epin with different PIN
|
|
452
295
|
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
* **Visual Distinction**: Clear player and style differentiation
|
|
459
|
-
* **Protocol Compliant**: Complete implementation of Sashité piece attributes
|
|
460
|
-
* **Immutable**: All identifier instances are frozen and transformations return new objects
|
|
461
|
-
* **Functional**: Pure functions with no side effects
|
|
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
|
+
```
|
|
462
301
|
|
|
463
|
-
|
|
302
|
+
### Queries
|
|
464
303
|
|
|
465
|
-
|
|
304
|
+
```ruby
|
|
305
|
+
# Derivation
|
|
306
|
+
epin.derived? # => true if derived
|
|
307
|
+
epin.native? # => true if not derived
|
|
466
308
|
|
|
467
|
-
|
|
309
|
+
# Comparison
|
|
310
|
+
epin.same_derived?(other) # => true if same derivation status
|
|
311
|
+
```
|
|
468
312
|
|
|
469
|
-
|
|
470
|
-
2. **Foreign pieces** (`'` suffix): Use the opposite side's native style
|
|
471
|
-
3. **Match context**: Each side has a defined native style for the entire match
|
|
472
|
-
4. **Style mutations**: Pieces can change derivation through gameplay mechanics
|
|
313
|
+
## Data Structure
|
|
473
314
|
|
|
474
|
-
|
|
315
|
+
```ruby
|
|
316
|
+
Sashite::Epin
|
|
317
|
+
#pin => Sashite::Pin # Underlying PIN instance
|
|
318
|
+
#derived => true | false # Derivation status
|
|
319
|
+
```
|
|
320
|
+
|
|
321
|
+
## Comparison with PIN
|
|
322
|
+
|
|
323
|
+
### What EPIN Adds
|
|
475
324
|
|
|
476
325
|
```ruby
|
|
477
|
-
#
|
|
478
|
-
|
|
479
|
-
#
|
|
480
|
-
#
|
|
481
|
-
#
|
|
482
|
-
#
|
|
483
|
-
|
|
484
|
-
# EPIN:
|
|
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)
|
|
485
340
|
```
|
|
486
341
|
|
|
487
|
-
|
|
342
|
+
### When to Use EPIN vs PIN
|
|
488
343
|
|
|
489
|
-
|
|
344
|
+
**Use PIN when:**
|
|
345
|
+
- Single-style games (both players use same style)
|
|
346
|
+
- Style information not needed
|
|
347
|
+
- Maximum compatibility required
|
|
490
348
|
|
|
491
|
-
|
|
492
|
-
-
|
|
493
|
-
-
|
|
494
|
-
-
|
|
495
|
-
- **Style context dependency**: Requires match-level side-style associations
|
|
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
|
|
496
353
|
|
|
497
|
-
##
|
|
354
|
+
## Design Principles
|
|
498
355
|
|
|
499
|
-
|
|
500
|
-
- [Game Protocol](https://sashite.dev/game-protocol/) - Conceptual foundation for abstract strategy board games
|
|
501
|
-
- [CELL](https://sashite.dev/specs/cell/) - Board position coordinates
|
|
502
|
-
- [HAND](https://sashite.dev/specs/hand/) - Reserve location notation
|
|
503
|
-
- [PMN](https://sashite.dev/specs/pmn/) - Portable Move Notation
|
|
356
|
+
### 1. Pure Composition
|
|
504
357
|
|
|
505
|
-
|
|
358
|
+
EPIN doesn't reimplement PIN features — it extends PIN minimally:
|
|
506
359
|
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
360
|
+
```ruby
|
|
361
|
+
def initialize(pin, derived: false)
|
|
362
|
+
@pin = pin
|
|
363
|
+
@derived = !!derived
|
|
364
|
+
freeze
|
|
365
|
+
end
|
|
366
|
+
```
|
|
511
367
|
|
|
512
|
-
|
|
368
|
+
### 2. Minimal API
|
|
513
369
|
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
370
|
+
**6 core methods only:**
|
|
371
|
+
1. `new` — create from PIN
|
|
372
|
+
2. `pin` — get PIN component
|
|
373
|
+
3. `derived` / `derived?` — check derivation
|
|
374
|
+
4. `to_s` — serialize
|
|
375
|
+
5. `with_pin` — replace PIN
|
|
376
|
+
6. `with_derived` / `mark_derived` / `unmark_derived` — change derivation
|
|
377
|
+
|
|
378
|
+
Everything else uses the PIN API directly.
|
|
518
379
|
|
|
519
|
-
|
|
520
|
-
bundle install
|
|
380
|
+
### 3. Component Transparency
|
|
521
381
|
|
|
522
|
-
|
|
523
|
-
ruby test.rb
|
|
382
|
+
Access PIN directly — no wrappers:
|
|
524
383
|
|
|
525
|
-
|
|
526
|
-
|
|
384
|
+
```ruby
|
|
385
|
+
# Use PIN API directly
|
|
386
|
+
epin.pin.type
|
|
387
|
+
epin.pin.with_type(:Q)
|
|
388
|
+
epin.pin.enhanced?
|
|
389
|
+
epin.pin.flip
|
|
390
|
+
|
|
391
|
+
# No need for wrapper methods like:
|
|
392
|
+
# epin.type
|
|
393
|
+
# epin.with_type(:Q)
|
|
394
|
+
# epin.enhanced?
|
|
395
|
+
# epin.flip
|
|
396
|
+
```
|
|
397
|
+
|
|
398
|
+
### 4. Backward Compatibility
|
|
399
|
+
|
|
400
|
+
Every valid PIN is a valid EPIN (without derivation marker):
|
|
401
|
+
|
|
402
|
+
```ruby
|
|
403
|
+
# All PIN identifiers work as EPIN
|
|
404
|
+
%w[K +R -p K^ +R^].each do |token|
|
|
405
|
+
epin = Sashite::Epin.parse(token)
|
|
406
|
+
epin.native? # => true
|
|
407
|
+
epin.to_s # => token
|
|
408
|
+
end
|
|
527
409
|
```
|
|
528
410
|
|
|
529
|
-
##
|
|
411
|
+
## Related Specifications
|
|
530
412
|
|
|
531
|
-
1.
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
5. Commit your changes (`git commit -am 'Add new feature'`)
|
|
536
|
-
6. Push to the branch (`git push origin feature/new-feature`)
|
|
537
|
-
7. Create a Pull Request
|
|
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
|
|
538
417
|
|
|
539
418
|
## License
|
|
540
419
|
|