sashite-epin 1.2.0 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
data/README.md CHANGED
@@ -9,9 +9,29 @@
9
9
 
10
10
  ## What is EPIN?
11
11
 
12
- EPIN (Extended Piece Identifier Notation) extends [PIN (Piece Identifier Notation)](https://sashite.dev/specs/pin/1.0.0/) to provide style-aware piece representation in abstract strategy board games. EPIN adds a derivation marker that distinguishes pieces by their style origin, enabling cross-style game scenarios and piece origin tracking.
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
- This gem implements the [EPIN Specification v1.0.0](https://sashite.dev/specs/epin/1.0.0/), providing a modern Ruby interface with immutable identifier objects and full backward compatibility with PIN while adding style differentiation capabilities.
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.
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.
15
35
 
16
36
  ## Installation
17
37
 
@@ -26,515 +46,608 @@ Or install manually:
26
46
  gem install sashite-epin
27
47
  ```
28
48
 
29
- ## Usage
49
+ ## Dependencies
50
+
51
+ ```ruby
52
+ gem "sashite-pin" # Piece Identifier Notation
53
+ ```
54
+
55
+ ## Quick Start
30
56
 
31
57
  ```ruby
32
58
  require "sashite/epin"
33
59
 
34
- # Parse EPIN strings into identifier objects
35
- identifier = Sashite::Epin.parse("K") # => #<Epin::Identifier type=:K side=:first state=:normal native=true>
36
- identifier.to_s # => "K"
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
- ```
60
+ # Parse an EPIN string
61
+ epin = Sashite::Epin.parse("K^'")
62
+ epin.to_s # => "K^'"
116
63
 
117
- ## Format Specification
64
+ # Access five fundamental attributes through PIN component + derived flag
65
+ epin.pin.type # => :K (Piece Name)
66
+ epin.pin.side # => :first (Piece Side)
67
+ epin.pin.state # => :normal (Piece State)
68
+ epin.pin.terminal? # => true (Terminal Status)
69
+ epin.derived? # => true (Piece Style: derived vs native)
118
70
 
119
- ### Structure
71
+ # PIN component is a full PIN::Identifier
72
+ epin.pin.enhanced? # => false
73
+ epin.pin.letter # => "K"
120
74
  ```
121
- <pin>[<suffix>]
75
+
76
+ ## Basic Usage
77
+
78
+ ### Creating Identifiers
79
+
80
+ ```ruby
81
+ # Parse from string
82
+ epin = Sashite::Epin.parse("K^") # Native
83
+ epin = Sashite::Epin.parse("K^'") # Derived
84
+
85
+ # Create from PIN component
86
+ pin = Sashite::Pin.parse("K^")
87
+ epin = Sashite::Epin.new(pin, derived: false) # Native
88
+ epin = Sashite::Epin.new(pin, derived: true) # Derived
89
+
90
+ # Validate
91
+ Sashite::Epin.valid?("K^") # => true
92
+ Sashite::Epin.valid?("K^'") # => true
93
+ Sashite::Epin.valid?("K^''") # => false (multiple markers)
122
94
  ```
123
95
 
124
- Where `<pin>` follows the PIN format: `[<state>]<letter>`
96
+ ### Accessing Components
97
+
98
+ ```ruby
99
+ epin = Sashite::Epin.parse("+R^'")
100
+
101
+ # Get PIN component
102
+ epin.pin # => #<Pin::Identifier type=:R state=:enhanced terminal=true>
103
+ epin.pin.to_s # => "+R^"
125
104
 
126
- ### Components
105
+ # Check derivation
106
+ epin.derived? # => true
127
107
 
128
- - **PIN part** (`[<state>]<letter>`): Standard PIN notation
129
- - **Letter** (`A-Z`, `a-z`): Represents piece type and side
130
- - Uppercase: First player pieces
131
- - Lowercase: Second player pieces
132
- - **State** (optional prefix):
133
- - `+`: Enhanced state (promoted, upgraded, empowered)
134
- - `-`: Diminished state (weakened, restricted, temporary)
135
- - No prefix: Normal state
108
+ # Serialize
109
+ epin.to_s # => "+R^'"
110
+ ```
136
111
 
137
- - **Derivation suffix** (optional):
138
- - `'`: Foreign style (piece has opposite side's native style)
139
- - No suffix: Native style (piece has current side's native style)
112
+ ### Five Fundamental Attributes
113
+
114
+ All attributes accessible via PIN component + derived flag:
140
115
 
141
- ### Regular Expression
142
116
  ```ruby
143
- /\A[-+]?[A-Za-z]'?\z/
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)
144
127
  ```
145
128
 
146
- ### Examples
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)
129
+ ## Transformations
151
130
 
152
- ## Game Examples
131
+ All transformations return new immutable instances:
153
132
 
154
- ### Cross-Style Chess vs. Shōgi
133
+ ### Change Derivation Status
155
134
 
156
135
  ```ruby
157
- # Match setup: First player uses Chess, Second player uses Shōgi
158
- # Native styles: first=Chess, second=Shōgi
159
-
160
- # Native pieces (no derivation suffix)
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)
136
+ epin = Sashite::Epin.parse("K^")
163
137
 
164
- # Foreign pieces (with derivation suffix)
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)
138
+ # Mark as derived
139
+ derived = epin.mark_derived
140
+ derived.to_s # => "K^'"
167
141
 
168
- # Promoted pieces in cross-style context
169
- white_promoted_rook = Sashite::Epin.parse("+R'") # White shōgi rook promoted to Dragon King
170
- black_promoted_pawn = Sashite::Epin.parse("+p") # Black shōgi pawn promoted to Tokin
142
+ # Mark as native
143
+ native = derived.unmark_native
144
+ native.to_s # => "K^"
171
145
 
172
- white_promoted_rook.enhanced? # => true
173
- white_promoted_rook.derived? # => true
174
- black_promoted_pawn.enhanced? # => true
175
- black_promoted_pawn.native? # => true
146
+ # Toggle
147
+ toggled = epin.with_derived(!epin.derived?)
148
+ toggled.to_s # => "K^'"
176
149
  ```
177
150
 
178
- ### Single-Style Games (PIN Compatibility)
151
+ ### Transform via PIN Component
179
152
 
180
153
  ```ruby
181
- # Traditional Chess (both players use Chess style)
182
- # All pieces are native, so EPIN behaves exactly like PIN
154
+ epin = Sashite::Epin.parse("K^'")
183
155
 
184
- white_pieces = %w[K Q +R B N P].map { |pin| Sashite::Epin.parse(pin) }
185
- black_pieces = %w[k q +r b n p].map { |pin| Sashite::Epin.parse(pin) }
156
+ # Replace PIN component
157
+ new_pin = epin.pin.with_type(:Q)
158
+ epin.with_pin(new_pin).to_s # => "Q^'"
186
159
 
187
- white_pieces.all?(&:native?) # => true
188
- black_pieces.all?(&:native?) # => true
160
+ # Change type
161
+ epin.with_pin(epin.pin.with_type(:Q)).to_s # => "Q^'"
189
162
 
190
- # EPIN strings match PIN strings for native pieces
191
- white_pieces.map(&:to_s) # => ["K", "Q", "+R", "B", "N", "P"]
192
- black_pieces.map(&:to_s) # => ["k", "q", "+r", "b", "n", "p"]
163
+ # Change state
164
+ epin.with_pin(epin.pin.with_state(:enhanced)).to_s # => "+K^'"
165
+
166
+ # Remove terminal marker
167
+ epin.with_pin(epin.pin.with_terminal(false)).to_s # => "K'"
168
+
169
+ # Change side
170
+ epin.with_pin(epin.pin.flip).to_s # => "k^'"
193
171
  ```
194
172
 
195
- ### Style Mutation During Gameplay
173
+ ### Multiple Transformations
196
174
 
197
175
  ```ruby
198
- # Simulate capture with style change (Ōgi rules)
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
176
+ epin = Sashite::Epin.parse("K^")
177
+
178
+ # Transform PIN and derivation
179
+ transformed = epin
180
+ .with_pin(epin.pin.with_type(:Q).with_state(:enhanced))
181
+ .mark_derived
182
+
183
+ transformed.to_s # => "+Q^'"
184
+ ```
185
+
186
+ ## Component Queries
201
187
 
202
- chess_queen.to_s # => "q'" (black foreign queen)
203
- captured.to_s # => "P" (white native pawn)
188
+ Use the PIN component API directly:
204
189
 
205
- # Style derivation changes during gameplay
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)
190
+ ```ruby
191
+ epin = Sashite::Epin.parse("+P^'")
192
+
193
+ # PIN queries (name, side, state, terminal)
194
+ epin.pin.type # => :P
195
+ epin.pin.side # => :first
196
+ epin.pin.state # => :enhanced
197
+ epin.pin.terminal? # => true
198
+ epin.pin.first_player? # => true
199
+ epin.pin.enhanced? # => true
200
+ epin.pin.letter # => "P"
201
+ epin.pin.prefix # => "+"
202
+ epin.pin.suffix # => "^"
203
+
204
+ # EPIN queries (style)
205
+ epin.derived? # => true
206
+ epin.native? # => false
207
+
208
+ # Compare EPINs
209
+ other = Sashite::Epin.parse("+P^")
210
+ epin.pin.same_type?(other.pin) # => true (both P)
211
+ epin.pin.same_state?(other.pin) # => true (both enhanced)
212
+ epin.same_derivation?(other) # => false (different derivation)
209
213
  ```
210
214
 
211
215
  ## API Reference
212
216
 
213
- ### Main Module Methods
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
214
225
 
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
226
+ # Validate string
227
+ Sashite::Epin.valid?(epin_string) # => Boolean
228
+ ```
218
229
 
219
230
  ### Identifier Class
220
231
 
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
232
+ #### Core Methods (6 total)
283
233
 
284
234
  ```ruby
285
- # Understanding native vs. foreign pieces
286
- # In a Chess vs. Shōgi match:
287
- # - First player native style: Chess
288
- # - Second player native style: Shōgi
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
289
241
 
290
- native_chess_king = Sashite::Epin.parse("K") # First player native (Chess king)
291
- foreign_shogi_king = Sashite::Epin.parse("K'") # First player foreign (Shōgi king)
242
+ # Serialization
243
+ epin.to_s # => "K^'" or "K^"
292
244
 
293
- native_shogi_king = Sashite::Epin.parse("k") # Second player native (Shōgi king)
294
- foreign_chess_king = Sashite::Epin.parse("k'") # Second player foreign (Chess king)
245
+ # PIN replacement
246
+ epin.with_pin(new_pin) # New EPIN with different PIN
295
247
 
296
- # Style queries
297
- native_chess_king.native? # => true
298
- foreign_shogi_king.derived? # => true
299
- native_shogi_king.native? # => true
300
- foreign_chess_king.derived? # => true
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
301
252
  ```
302
253
 
303
- ### Immutable Transformations
254
+ #### Convenience Queries
255
+
304
256
  ```ruby
305
- # All transformations return new instances
306
- original = Sashite::Epin.identifier(:K, :first, :normal, true)
307
- enhanced = original.enhance
308
- derived = original.derive
309
- flipped = original.flip
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.
310
268
 
311
- # Original piece is never modified
312
- puts original # => "K"
313
- puts enhanced # => "+K"
314
- puts derived # => "K'"
315
- puts flipped # => "k"
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
316
279
 
317
- # Transformations can be chained
318
- result = original.flip.derive.enhance.with_type(:Q)
319
- puts result # => "+q'"
280
+ ### Grammar (BNF)
281
+ ```bnf
282
+ <epin> ::= <pin> | <pin> "'"
283
+
284
+ <pin> ::= ["+" | "-"] <letter> ["^"]
285
+ <letter> ::= "A" | ... | "Z" | "a" | ... | "z"
320
286
  ```
321
287
 
322
- ### Cross-Style Game State Management
288
+ ### Regular Expression
323
289
  ```ruby
324
- class CrossStyleGameBoard
325
- def initialize(first_style, second_style)
326
- @first_style = first_style
327
- @second_style = second_style
328
- @pieces = {}
329
- end
290
+ /\A[-+]?[A-Za-z]\^?'?\z/
291
+ ```
330
292
 
331
- def place(square, piece)
332
- @pieces[square] = piece
333
- end
293
+ ## Examples
334
294
 
335
- def capture_with_style_change(from_square, to_square, new_type = nil)
336
- captured = @pieces[to_square]
337
- capturing = @pieces.delete(from_square)
295
+ ### Basic Identifiers
338
296
 
339
- return nil unless captured && capturing
297
+ ```ruby
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
+ ```
340
318
 
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
319
+ ### Cross-Style Scenarios
344
320
 
345
- @pieces[to_square] = capturing
346
- mutated # Return mutated captured piece for hand
347
- end
321
+ Assume first player uses Chess style, second player uses Makruk style:
348
322
 
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
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)
356
330
 
357
- # Usage
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
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)
362
334
 
363
- analysis = board.pieces_by_style_derivation
364
- puts analysis[:native].size # => 2
365
- puts analysis[:foreign].size # => 1
335
+ makruk_king.native? # => true
336
+ chess_pawn.derived? # => true
366
337
  ```
367
338
 
368
- ### PIN Compatibility Layer
339
+ ### Component Manipulation
340
+
369
341
  ```ruby
370
- # EPIN is fully backward compatible with PIN
371
- def convert_pin_to_epin(pin_string)
372
- # All PIN strings are valid EPIN strings (native pieces)
373
- Sashite::Epin.parse(pin_string)
374
- end
342
+ # Start with native king
343
+ epin = Sashite::Epin.parse("K^")
375
344
 
376
- def convert_epin_to_pin(epin_identifier)
377
- # Only native EPIN pieces can be converted to PIN
378
- return nil unless epin_identifier.native?
345
+ # Convert to derived
346
+ derived = epin.mark_derived
347
+ derived.to_s # => "K^'"
379
348
 
380
- "#{epin_identifier.prefix}#{epin_identifier.letter}"
381
- end
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
382
387
 
383
- # Usage
384
- pin_pieces = %w[K Q +R -P k q r p]
385
- epin_pieces = pin_pieces.map { |pin| convert_pin_to_epin(pin) }
388
+ epin.with_pin(complex_pin).to_s # => "-q'"
389
+ ```
390
+
391
+ ### Immutability
392
+
393
+ ```ruby
394
+ original = Sashite::Epin.parse("K^")
386
395
 
387
- epin_pieces.all?(&:native?) # => true
388
- epin_pieces.map { |p| convert_epin_to_pin(p) } # => ["K", "Q", "+R", "-P", "k", "q", "r", "p"]
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
389
409
  ```
390
410
 
391
- ### Move Validation Example
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
+
392
429
  ```ruby
393
- def can_promote_in_style?(piece, target_rank, style_rules)
394
- return false unless piece.normal? # Already promoted pieces can't promote again
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
430
+ # EPIN is just PIN + derived flag
431
+ class Identifier
432
+ def initialize(pin, derived: false)
433
+ @pin = pin
434
+ @derived = !!derived
407
435
  end
408
436
  end
437
+ ```
409
438
 
410
- # Usage
411
- shogi_pawn = Sashite::Epin.identifier(:P, :first, :normal, false)
439
+ ### 2. Absolute Minimal API
412
440
 
413
- style_rules = { native: :chess, foreign: :shogi }
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
414
448
 
415
- puts can_promote_in_style?(chess_pawn, 8, style_rules) # => true (chess pawn on 8th rank)
416
- puts can_promote_in_style?(shogi_pawn, 8, style_rules) # => true (shogi pawn on 8th rank)
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
417
467
  ```
418
468
 
419
- ## Implementation Architecture
469
+ ### 4. Backward Compatibility
420
470
 
421
- This gem uses **composition over inheritance** by building upon the proven [sashite-pin](https://github.com/sashite/pin.rb) gem:
471
+ Every valid PIN is a valid EPIN (without derivation marker):
422
472
 
423
- - **PIN Foundation**: All type, side, and state logic is handled by an internal `Pin::Identifier` object
424
- - **EPIN Extension**: Only the derivation (`native`) attribute and related methods are added
425
- - **Delegation Pattern**: Core PIN methods are delegated to the internal PIN identifier
426
- - **Immutability**: All transformations return new instances, maintaining functional programming principles
473
+ ```ruby
474
+ # All PIN identifiers work as EPIN
475
+ pin_tokens = ["K", "+R", "-p", "K^", "+R^"]
476
+ pin_tokens.each do |token|
477
+ epin = Sashite::Epin.parse(token)
478
+ epin.native? # => true
479
+ epin.to_s == token # => true
480
+ end
481
+ ```
482
+
483
+ ### 5. Immutability
427
484
 
428
- This architecture ensures:
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
485
+ All instances frozen. Transformations return new instances:
433
486
 
434
- ## Protocol Mapping
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
+ ```
435
494
 
436
- Following the [Game Protocol](https://sashite.dev/game-protocol/):
495
+ ## Semantics
437
496
 
438
- | Protocol Attribute | EPIN Encoding | Examples | Notes |
439
- |-------------------|--------------|----------|-------|
440
- | **Type** | ASCII letter choice | `K`/`k` = King, `P`/`p` = Pawn | Type is always stored as uppercase symbol (`:K`, `:P`) |
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 | |
497
+ ### Native vs Derived
444
498
 
445
- **Style Derivation Logic**:
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)
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
448
506
 
449
- **Canonical principle**: Identical pieces must have identical EPIN representations.
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
+ ```
450
512
 
451
- ## Properties
513
+ ### Style Derivation Logic
452
514
 
453
- * **PIN Compatible**: All valid PIN strings are valid EPIN strings
454
- * **Style Aware**: Distinguishes pieces by their style origin through derivation markers
455
- * **ASCII Compatible**: Maximum portability across systems
456
- * **Rule-Agnostic**: Independent of specific game mechanics
457
- * **Compact Format**: Minimal character usage (1-3 characters per piece)
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
515
+ ```ruby
516
+ # Assume: first=Chess, second=Makruk
462
517
 
463
- ## Implementation Notes
518
+ epin = Sashite::Epin.parse("P")
519
+ # Piece Side: first
520
+ # Style: Chess (first's native) → Native piece
464
521
 
465
- ### Style Derivation Convention
522
+ epin = Sashite::Epin.parse("P'")
523
+ # Piece Side: first
524
+ # Style: Makruk (second's native) → Derived piece
466
525
 
467
- EPIN follows a strict style derivation convention:
526
+ epin = Sashite::Epin.parse("p")
527
+ # Piece Side: second
528
+ # Style: Makruk (second's native) → Native piece
468
529
 
469
- 1. **Native pieces** (no suffix): Use the current side's native style
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
530
+ epin = Sashite::Epin.parse("p'")
531
+ # Piece Side: second
532
+ # Style: Chess (first's native) Derived piece
533
+ ```
473
534
 
474
- ### Example Flow
535
+ ## Error Handling
475
536
 
476
537
  ```ruby
477
- # Match context: First player=Chess, Second player=Shōgi
478
- # Input: "K'" (first player foreign)
479
- # ↓ Parsing
480
- # type: :K, side: :first, state: :normal, native: false
481
- # Style resolution
482
- # Effective style: Shōgi (second player's native style)
483
- # ↓ Display
484
- # EPIN: "K'" (first player king with foreign/Shōgi style)
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
485
558
  ```
486
559
 
487
- This ensures that `parse(epin).to_s == epin` for all valid EPIN strings while enabling cross-style gameplay.
560
+ ## Performance Considerations
488
561
 
489
- ## System Constraints
562
+ ### Efficient Composition
490
563
 
491
- - **Maximum 26 piece types** per game system (one per ASCII letter)
492
- - **Exactly 2 players** (uppercase/lowercase distinction)
493
- - **3 state levels** (enhanced, normal, diminished)
494
- - **2 style derivations** (native, foreign)
495
- - **Style context dependency**: Requires match-level side-style associations
564
+ ```ruby
565
+ # PIN component created once
566
+ pin = Sashite::Pin.parse("K^")
567
+ epin = Sashite::Epin.new(pin, derived: true)
496
568
 
497
- ## Related Specifications
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
+ ```
498
576
 
499
- - [PIN](https://sashite.dev/specs/pin/1.0.0/) - Piece Identifier Notation (style-agnostic base)
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
577
+ ### Transformation Patterns
504
578
 
505
- ## Documentation
579
+ ```ruby
580
+ epin = Sashite::Epin.parse("K^'")
506
581
 
507
- - [Official EPIN Specification v1.0.0](https://sashite.dev/specs/epin/1.0.0/)
508
- - [EPIN Examples Documentation](https://sashite.dev/specs/epin/1.0.0/examples/)
509
- - [Game Protocol Foundation](https://sashite.dev/game-protocol/)
510
- - [API Documentation](https://rubydoc.info/github/sashite/epin.rb/main)
582
+ # Pattern 1: Change derivation only
583
+ epin.unmark_native
511
584
 
512
- ## Development
585
+ # Pattern 2: Change PIN only
586
+ epin.with_pin(epin.pin.with_type(:Q))
513
587
 
514
- ```sh
515
- # Clone the repository
516
- git clone https://github.com/sashite/epin.rb.git
517
- cd epin.rb
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
+ ```
518
599
 
519
- # Install dependencies
520
- bundle install
600
+ ## Comparison with PIN
521
601
 
522
- # Run tests
523
- ruby test.rb
602
+ ### What EPIN Adds
524
603
 
525
- # Generate documentation
526
- yard doc
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)
527
619
  ```
528
620
 
529
- ## Contributing
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
+ ## Related Specifications
530
646
 
531
- 1. Fork the repository
532
- 2. Create a feature branch (`git checkout -b feature/new-feature`)
533
- 3. Add tests for your changes
534
- 4. Ensure all tests pass (`ruby test.rb`)
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
647
+ - [EPIN Specification v1.0.0](https://sashite.dev/specs/epin/1.0.0/) - Technical specification
648
+ - [EPIN Examples](https://sashite.dev/specs/epin/1.0.0/examples/) - Usage examples
649
+ - [PIN Specification v1.0.0](https://sashite.dev/specs/pin/1.0.0/) - Base component
650
+ - [Sashité Game Protocol](https://sashite.dev/game-protocol/) - Foundation
538
651
 
539
652
  ## License
540
653