sashite-epin 1.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.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 8d4832b1fa452d2b0c5fb78091435ee12a125efd9dcf66c68de291fdc840adf3
4
+ data.tar.gz: c847fd7927b983c57cf019514a2a4e798cd04a0cda2ed8ef7859a725eee418ba
5
+ SHA512:
6
+ metadata.gz: 6b5cdff7dc99229e4e72175c284eb9af3baa94196b73923b642ec223028dde914ab9f4f4709a4f4f5c12c04ae53b70e5358864f92045c62b814f0c79c83c22e8
7
+ data.tar.gz: 21ebf13ebecdfc8ffa165d98788daaf6e3730d082d8fa5cdab0c8ae71da8c386673741b067231407fb5cbb45d1dd85b12b70ca859ff759b434dcf2eb1e0aa39e
data/LICENSE.md ADDED
@@ -0,0 +1,21 @@
1
+ # The MIT License
2
+
3
+ Copyright (c) 2025 Sashité
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,547 @@
1
+ # Epin.rb
2
+
3
+ [![Version](https://img.shields.io/github/v/tag/sashite/epin.rb?label=Version&logo=github)](https://github.com/sashite/epin.rb/tags)
4
+ [![Yard documentation](https://img.shields.io/badge/Yard-documentation-blue.svg?logo=github)](https://rubydoc.info/github/sashite/epin.rb/main)
5
+ ![Ruby](https://github.com/sashite/epin.rb/actions/workflows/main.yml/badge.svg?branch=main)
6
+ [![License](https://img.shields.io/github/license/sashite/epin.rb?label=License&logo=github)](https://github.com/sashite/epin.rb/raw/main/LICENSE.md)
7
+
8
+ > **EPIN** (Extended Piece Identifier Notation) implementation for the Ruby language.
9
+
10
+ ## What is EPIN?
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.
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.
15
+
16
+ ## Installation
17
+
18
+ ```ruby
19
+ # In your Gemfile
20
+ gem "sashite-epin"
21
+ ```
22
+
23
+ Or install manually:
24
+
25
+ ```sh
26
+ gem install sashite-epin
27
+ ```
28
+
29
+ ## Usage
30
+
31
+ ```ruby
32
+ require "sashite/epin"
33
+
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(:K, :first) # => #<Epin::Identifier type=:K side=:first state=:normal native=true>
44
+ identifier = Sashite::Epin::Identifier.new(:R, :second, :enhanced, false) # => #<Epin::Identifier type=:R side=:second state=:enhanced native=false>
45
+
46
+ # Validate EPIN strings
47
+ Sashite::Epin.valid?("K") # => true
48
+ Sashite::Epin.valid?("+R'") # => true
49
+ Sashite::Epin.valid?("invalid") # => false
50
+
51
+ # Style derivation with apostrophe suffix
52
+ native_king = Sashite::Epin.parse("K") # => #<Epin::Identifier type=:K side=:first state=:normal native=true>
53
+ foreign_king = Sashite::Epin.parse("K'") # => #<Epin::Identifier type=:K side=:first state=:normal native=false>
54
+
55
+ native_king.to_s # => "K"
56
+ foreign_king.to_s # => "K'"
57
+
58
+ # State manipulation (returns new immutable instances)
59
+ enhanced = identifier.enhance # => #<Epin::Identifier type=:K side=:first state=:enhanced native=true>
60
+ enhanced.to_s # => "+K"
61
+ diminished = identifier.diminish # => #<Epin::Identifier type=:K side=:first state=:diminished native=true>
62
+ diminished.to_s # => "-K"
63
+
64
+ # Style derivation manipulation
65
+ foreign_piece = identifier.derive # => #<Epin::Identifier type=:K side=:first state=:normal native=false>
66
+ foreign_piece.to_s # => "K'"
67
+ back_to_native = foreign_piece.underive # => #<Epin::Identifier type=:K side=:first state=:normal native=true>
68
+ back_to_native.to_s # => "K"
69
+
70
+ # Side manipulation
71
+ flipped = identifier.flip # => #<Epin::Identifier type=:K side=:second state=:normal native=true>
72
+ flipped.to_s # => "k"
73
+
74
+ # Type manipulation
75
+ queen = identifier.with_type(:Q) # => #<Epin::Identifier type=:Q side=:first state=:normal native=true>
76
+ queen.to_s # => "Q"
77
+
78
+ # Style queries
79
+ identifier.native? # => true
80
+ foreign_king.derived? # => true
81
+ foreign_king.foreign? # => true (alias for derived?)
82
+
83
+ # State queries
84
+ identifier.normal? # => true
85
+ enhanced.enhanced? # => true
86
+ diminished.diminished? # => true
87
+
88
+ # Side queries
89
+ identifier.first_player? # => true
90
+ flipped.second_player? # => true
91
+
92
+ # Attribute access
93
+ identifier.letter # => "K"
94
+ enhanced.prefix # => "+"
95
+ foreign_king.suffix # => "'"
96
+ identifier.suffix # => ""
97
+
98
+ # Type and side comparison
99
+ king1 = Sashite::Epin.parse("K")
100
+ king2 = Sashite::Epin.parse("k")
101
+ queen = Sashite::Epin.parse("Q")
102
+
103
+ king1.same_type?(king2) # => true (both kings)
104
+ king1.same_side?(queen) # => true (both first player)
105
+ king1.same_type?(queen) # => false (different types)
106
+
107
+ # Style comparison
108
+ native_king = Sashite::Epin.parse("K")
109
+ foreign_king = Sashite::Epin.parse("K'")
110
+
111
+ native_king.same_style?(foreign_king) # => false (different derivation)
112
+
113
+ # Functional transformations can be chained
114
+ pawn = Sashite::Epin.parse("P")
115
+ enemy_foreign_promoted = pawn.flip.derive.enhance # => "+p'" (second player foreign promoted pawn)
116
+ ```
117
+
118
+ ## Format Specification
119
+
120
+ ### Structure
121
+ ```
122
+ <pin>[<suffix>]
123
+ ```
124
+
125
+ Where `<pin>` follows the PIN format: `[<state>]<letter>`
126
+
127
+ ### Components
128
+
129
+ - **PIN part** (`[<state>]<letter>`): Standard PIN notation
130
+ - **Letter** (`A-Z`, `a-z`): Represents piece type and side
131
+ - Uppercase: First player pieces
132
+ - Lowercase: Second player pieces
133
+ - **State** (optional prefix):
134
+ - `+`: Enhanced state (promoted, upgraded, empowered)
135
+ - `-`: Diminished state (weakened, restricted, temporary)
136
+ - No prefix: Normal state
137
+
138
+ - **Derivation suffix** (optional):
139
+ - `'`: Foreign style (piece has opposite side's native style)
140
+ - No suffix: Native style (piece has current side's native style)
141
+
142
+ ### Regular Expression
143
+ ```ruby
144
+ /\A[-+]?[A-Za-z]'?\z/
145
+ ```
146
+
147
+ ### Examples
148
+ - `K` - First player king (native style, normal state)
149
+ - `k'` - Second player king (foreign style, normal state)
150
+ - `+R'` - First player rook (foreign style, enhanced state)
151
+ - `-p` - Second player pawn (native style, diminished state)
152
+
153
+ ## Game Examples
154
+
155
+ ### Cross-Style Chess vs. Shōgi
156
+
157
+ ```ruby
158
+ # Match setup: First player uses Chess, Second player uses Shōgi
159
+ # Native styles: first=Chess, second=Shōgi
160
+
161
+ # Native pieces (no derivation suffix)
162
+ white_king = Sashite::Epin.identifier(:K, :first) # => "K" (Chess king)
163
+ black_king = Sashite::Epin.identifier(:K, :second) # => "k" (Shōgi king)
164
+
165
+ # Foreign pieces (with derivation suffix)
166
+ white_shogi_king = Sashite::Epin.identifier(:K, :first, :normal, false) # => "K'" (Shōgi king for white)
167
+ black_chess_king = Sashite::Epin.identifier(:K, :second, :normal, false) # => "k'" (Chess king for black)
168
+
169
+ # Promoted pieces in cross-style context
170
+ white_promoted_rook = Sashite::Epin.parse("+R'") # White shōgi rook promoted to Dragon King
171
+ black_promoted_pawn = Sashite::Epin.parse("+p") # Black shōgi pawn promoted to Tokin
172
+
173
+ white_promoted_rook.enhanced? # => true
174
+ white_promoted_rook.derived? # => true
175
+ black_promoted_pawn.enhanced? # => true
176
+ black_promoted_pawn.native? # => true
177
+ ```
178
+
179
+ ### Single-Style Games (PIN Compatibility)
180
+
181
+ ```ruby
182
+ # Traditional Chess (both players use Chess style)
183
+ # All pieces are native, so EPIN behaves exactly like PIN
184
+
185
+ white_pieces = %w[K Q +R B N P].map { |pin| Sashite::Epin.parse(pin) }
186
+ black_pieces = %w[k q +r b n p].map { |pin| Sashite::Epin.parse(pin) }
187
+
188
+ white_pieces.all?(&:native?) # => true
189
+ black_pieces.all?(&:native?) # => true
190
+
191
+ # EPIN strings match PIN strings for native pieces
192
+ white_pieces.map(&:to_s) # => ["K", "Q", "+R", "B", "N", "P"]
193
+ black_pieces.map(&:to_s) # => ["k", "q", "+r", "b", "n", "p"]
194
+ ```
195
+
196
+ ### Style Mutation During Gameplay
197
+
198
+ ```ruby
199
+ # Simulate capture with style change (Ōgi rules)
200
+ chess_queen = Sashite::Epin.parse("q'") # Black chess queen (foreign for shōgi player)
201
+ captured = chess_queen.flip.with_type(:P).underive # Becomes white native pawn
202
+
203
+ chess_queen.to_s # => "q'" (black foreign queen)
204
+ captured.to_s # => "P" (white native pawn)
205
+
206
+ # Style derivation changes during gameplay
207
+ shogi_piece = Sashite::Epin.parse("r") # Black shōgi rook (native)
208
+ foreign_piece = shogi_piece.derive # Convert to foreign style
209
+ foreign_piece.to_s # => "r'" (black foreign rook)
210
+ ```
211
+
212
+ ## API Reference
213
+
214
+ ### Main Module Methods
215
+
216
+ - `Sashite::Epin.valid?(epin_string)` - Check if string is valid EPIN notation
217
+ - `Sashite::Epin.parse(epin_string)` - Parse EPIN string into Identifier object
218
+ - `Sashite::Epin.identifier(type, side, state = :normal, native = true)` - Create identifier instance directly
219
+
220
+ ### Identifier Class
221
+
222
+ #### Creation and Parsing
223
+ - `Sashite::Epin::Identifier.new(type, side, state = :normal, native = true)` - Create identifier instance
224
+ - `Sashite::Epin::Identifier.parse(epin_string)` - Parse EPIN string (same as module method)
225
+ - `Sashite::Epin::Identifier.valid?(epin_string)` - Validate EPIN string (class method)
226
+
227
+ #### Attribute Access
228
+ - `#type` - Get piece type (symbol :A to :Z, always uppercase)
229
+ - `#side` - Get player side (:first or :second)
230
+ - `#state` - Get state (:normal, :enhanced, or :diminished)
231
+ - `#native` - Get style derivation (true for native, false for foreign)
232
+ - `#letter` - Get letter representation (string, case determined by side)
233
+ - `#prefix` - Get state prefix (string: "+", "-", or "")
234
+ - `#suffix` - Get derivation suffix (string: "'" or "")
235
+ - `#to_s` - Convert to EPIN string representation
236
+
237
+ #### Style Queries
238
+ - `#native?` - Check if native style (current side's native style)
239
+ - `#derived?` - Check if foreign style (opposite side's native style)
240
+ - `#foreign?` - Alias for `#derived?`
241
+
242
+ #### State Queries
243
+ - `#normal?` - Check if normal state (no modifiers)
244
+ - `#enhanced?` - Check if enhanced state
245
+ - `#diminished?` - Check if diminished state
246
+
247
+ #### Side Queries
248
+ - `#first_player?` - Check if first player identifier
249
+ - `#second_player?` - Check if second player identifier
250
+
251
+ #### State Transformations (immutable - return new instances)
252
+ - `#enhance` - Create enhanced version
253
+ - `#unenhance` - Remove enhanced state
254
+ - `#diminish` - Create diminished version
255
+ - `#undiminish` - Remove diminished state
256
+ - `#normalize` - Remove all state modifiers
257
+
258
+ #### Style Transformations (immutable - return new instances)
259
+ - `#derive` - Convert to foreign style (add derivation suffix)
260
+ - `#underive` - Convert to native style (remove derivation suffix)
261
+ - `#flip` - Switch player (change side)
262
+
263
+ #### Attribute Transformations (immutable - return new instances)
264
+ - `#with_type(new_type)` - Create identifier with different type
265
+ - `#with_side(new_side)` - Create identifier with different side
266
+ - `#with_state(new_state)` - Create identifier with different state
267
+ - `#with_derivation(native)` - Create identifier with different derivation
268
+
269
+ #### Comparison Methods
270
+ - `#same_type?(other)` - Check if same piece type
271
+ - `#same_side?(other)` - Check if same side
272
+ - `#same_state?(other)` - Check if same state
273
+ - `#same_style?(other)` - Check if same style derivation
274
+ - `#==(other)` - Full equality comparison
275
+
276
+ ### Constants
277
+ - `Sashite::Epin::Identifier::NATIVE` - Constant for native style (`true`)
278
+ - `Sashite::Epin::Identifier::FOREIGN` - Constant for foreign style (`false`)
279
+ - `Sashite::Epin::Identifier::DERIVATION_SUFFIX` - Derivation suffix for foreign pieces (`"'"`)
280
+
281
+ ## Advanced Usage
282
+
283
+ ### Style Derivation Examples
284
+
285
+ ```ruby
286
+ # Understanding native vs. foreign pieces
287
+ # In a Chess vs. Shōgi match:
288
+ # - First player native style: Chess
289
+ # - Second player native style: Shōgi
290
+
291
+ native_chess_king = Sashite::Epin.parse("K") # First player native (Chess king)
292
+ foreign_shogi_king = Sashite::Epin.parse("K'") # First player foreign (Shōgi king)
293
+
294
+ native_shogi_king = Sashite::Epin.parse("k") # Second player native (Shōgi king)
295
+ foreign_chess_king = Sashite::Epin.parse("k'") # Second player foreign (Chess king)
296
+
297
+ # Style queries
298
+ native_chess_king.native? # => true
299
+ foreign_shogi_king.derived? # => true
300
+ native_shogi_king.native? # => true
301
+ foreign_chess_king.derived? # => true
302
+ ```
303
+
304
+ ### Immutable Transformations
305
+ ```ruby
306
+ # All transformations return new instances
307
+ original = Sashite::Epin.identifier(:K, :first)
308
+ enhanced = original.enhance
309
+ derived = original.derive
310
+ flipped = original.flip
311
+
312
+ # Original piece is never modified
313
+ puts original # => "K"
314
+ puts enhanced # => "+K"
315
+ puts derived # => "K'"
316
+ puts flipped # => "k"
317
+
318
+ # Transformations can be chained
319
+ result = original.flip.derive.enhance.with_type(:Q)
320
+ puts result # => "+q'"
321
+ ```
322
+
323
+ ### Cross-Style Game State Management
324
+ ```ruby
325
+ class CrossStyleGameBoard
326
+ def initialize(first_style, second_style)
327
+ @first_style = first_style
328
+ @second_style = second_style
329
+ @pieces = {}
330
+ end
331
+
332
+ def place(square, piece)
333
+ @pieces[square] = piece
334
+ end
335
+
336
+ def capture_with_style_change(from_square, to_square, new_type = nil)
337
+ captured = @pieces[to_square]
338
+ capturing = @pieces.delete(from_square)
339
+
340
+ return nil unless captured && capturing
341
+
342
+ # Style mutation: captured piece becomes native to capturing side
343
+ mutated = captured.flip.underive
344
+ mutated = mutated.with_type(new_type) if new_type
345
+
346
+ @pieces[to_square] = capturing
347
+ mutated # Return mutated captured piece for hand
348
+ end
349
+
350
+ def pieces_by_style_derivation
351
+ {
352
+ native: @pieces.select { |_, piece| piece.native? },
353
+ foreign: @pieces.select { |_, piece| piece.derived? }
354
+ }
355
+ end
356
+ end
357
+
358
+ # Usage
359
+ board = CrossStyleGameBoard.new(:chess, :shogi)
360
+ board.place("e1", Sashite::Epin.identifier(:K, :first)) # Chess king
361
+ board.place("e8", Sashite::Epin.identifier(:K, :second)) # Shōgi king
362
+ board.place("d4", Sashite::Epin.identifier(:Q, :first, :normal, false)) # Chess queen using Shōgi style
363
+
364
+ analysis = board.pieces_by_style_derivation
365
+ puts analysis[:native].size # => 2
366
+ puts analysis[:foreign].size # => 1
367
+ ```
368
+
369
+ ### PIN Compatibility Layer
370
+ ```ruby
371
+ # EPIN is fully backward compatible with PIN
372
+ def convert_pin_to_epin(pin_string)
373
+ # All PIN strings are valid EPIN strings (native pieces)
374
+ Sashite::Epin.parse(pin_string)
375
+ end
376
+
377
+ def convert_epin_to_pin(epin_identifier)
378
+ # Only native EPIN pieces can be converted to PIN
379
+ return nil unless epin_identifier.native?
380
+
381
+ "#{epin_identifier.prefix}#{epin_identifier.letter}"
382
+ end
383
+
384
+ # Usage
385
+ pin_pieces = %w[K Q +R -P k q r p]
386
+ epin_pieces = pin_pieces.map { |pin| convert_pin_to_epin(pin) }
387
+
388
+ epin_pieces.all?(&:native?) # => true
389
+ epin_pieces.map { |p| convert_epin_to_pin(p) } # => ["K", "Q", "+R", "-P", "k", "q", "r", "p"]
390
+ ```
391
+
392
+ ### Move Validation Example
393
+ ```ruby
394
+ def can_promote_in_style?(piece, target_rank, style_rules)
395
+ return false unless piece.normal? # Already promoted pieces can't promote again
396
+
397
+ case [piece.type, piece.native? ? style_rules[:native] : style_rules[:foreign]]
398
+ when %i[P chess] # Chess pawn
399
+ (piece.first_player? && target_rank == 8) ||
400
+ (piece.second_player? && target_rank == 1)
401
+ when %i[P shogi] # Shōgi pawn
402
+ (piece.first_player? && target_rank >= 7) ||
403
+ (piece.second_player? && target_rank <= 3)
404
+ when %i[R shogi], %i[B shogi] # Shōgi major pieces
405
+ true
406
+ else
407
+ false
408
+ end
409
+ end
410
+
411
+ # Usage
412
+ chess_pawn = Sashite::Epin.identifier(:P, :first)
413
+ shogi_pawn = Sashite::Epin.identifier(:P, :first, :normal, false)
414
+
415
+ style_rules = { native: :chess, foreign: :shogi }
416
+
417
+ puts can_promote_in_style?(chess_pawn, 8, style_rules) # => true (chess pawn on 8th rank)
418
+ puts can_promote_in_style?(shogi_pawn, 8, style_rules) # => true (shogi pawn on 8th rank)
419
+ ```
420
+
421
+ ## Implementation Architecture
422
+
423
+ This gem uses **composition over inheritance** by building upon the proven [sashite-pin](https://github.com/sashite/pin.rb) gem:
424
+
425
+ - **PIN Foundation**: All type, side, and state logic is handled by an internal `Pin::Identifier` object
426
+ - **EPIN Extension**: Only the derivation (`native`) attribute and related methods are added
427
+ - **Delegation Pattern**: Core PIN methods are delegated to the internal PIN identifier
428
+ - **Immutability**: All transformations return new instances, maintaining functional programming principles
429
+
430
+ This architecture ensures:
431
+ - **Reliability**: Reuses battle-tested PIN logic
432
+ - **Maintainability**: PIN updates automatically benefit EPIN
433
+ - **Consistency**: PIN and EPIN identifiers behave identically for shared attributes
434
+ - **Performance**: Minimal overhead over pure PIN implementation
435
+
436
+ ## Protocol Mapping
437
+
438
+ Following the [Game Protocol](https://sashite.dev/game-protocol/):
439
+
440
+ | Protocol Attribute | EPIN Encoding | Examples | Notes |
441
+ |-------------------|--------------|----------|-------|
442
+ | **Type** | ASCII letter choice | `K`/`k` = King, `P`/`p` = Pawn | Type is always stored as uppercase symbol (`:K`, `:P`) |
443
+ | **Side** | Letter case in display | `K` = First player, `k` = Second player | Case is determined by side during rendering |
444
+ | **State** | Optional prefix | `+K` = Enhanced, `-K` = Diminished, `K` = Normal | |
445
+ | **Style** | Derivation suffix | `K` = Native style, `K'` = Foreign style | |
446
+
447
+ **Style Derivation Logic**:
448
+ - **No suffix**: Piece has the **native style** of its current side
449
+ - **Apostrophe suffix (`'`)**: Piece has the **foreign style** (opposite side's native style)
450
+
451
+ **Canonical principle**: Identical pieces must have identical EPIN representations.
452
+
453
+ ## Properties
454
+
455
+ * **PIN Compatible**: All valid PIN strings are valid EPIN strings
456
+ * **Style Aware**: Distinguishes pieces by their style origin through derivation markers
457
+ * **ASCII Compatible**: Maximum portability across systems
458
+ * **Rule-Agnostic**: Independent of specific game mechanics
459
+ * **Compact Format**: Minimal character usage (1-3 characters per piece)
460
+ * **Visual Distinction**: Clear player and style differentiation
461
+ * **Protocol Compliant**: Complete implementation of Sashité piece attributes
462
+ * **Immutable**: All identifier instances are frozen and transformations return new objects
463
+ * **Functional**: Pure functions with no side effects
464
+
465
+ ## Implementation Notes
466
+
467
+ ### Style Derivation Convention
468
+
469
+ EPIN follows a strict style derivation convention:
470
+
471
+ 1. **Native pieces** (no suffix): Use the current side's native style
472
+ 2. **Foreign pieces** (`'` suffix): Use the opposite side's native style
473
+ 3. **Match context**: Each side has a defined native style for the entire match
474
+ 4. **Style mutations**: Pieces can change derivation through gameplay mechanics
475
+
476
+ ### Example Flow
477
+
478
+ ```ruby
479
+ # Match context: First player=Chess, Second player=Shōgi
480
+ # Input: "K'" (first player foreign)
481
+ # ↓ Parsing
482
+ # type: :K, side: :first, state: :normal, native: false
483
+ # ↓ Style resolution
484
+ # Effective style: Shōgi (second player's native style)
485
+ # ↓ Display
486
+ # EPIN: "K'" (first player king with foreign/Shōgi style)
487
+ ```
488
+
489
+ This ensures that `parse(epin).to_s == epin` for all valid EPIN strings while enabling cross-style gameplay.
490
+
491
+ ## System Constraints
492
+
493
+ - **Maximum 26 piece types** per game system (one per ASCII letter)
494
+ - **Exactly 2 players** (uppercase/lowercase distinction)
495
+ - **3 state levels** (enhanced, normal, diminished)
496
+ - **2 style derivations** (native, foreign)
497
+ - **Style context dependency**: Requires match-level side-style associations
498
+
499
+ ## Related Specifications
500
+
501
+ - [PIN](https://sashite.dev/specs/pin/1.0.0/) - Piece Identifier Notation (style-agnostic base)
502
+ - [Game Protocol](https://sashite.dev/game-protocol/) - Conceptual foundation for abstract strategy board games
503
+ - [CELL](https://sashite.dev/specs/cell/) - Board position coordinates
504
+ - [HAND](https://sashite.dev/specs/hand/) - Reserve location notation
505
+ - [PMN](https://sashite.dev/specs/pmn/) - Portable Move Notation
506
+
507
+ ## Documentation
508
+
509
+ - [Official EPIN Specification v1.0.0](https://sashite.dev/specs/epin/1.0.0/)
510
+ - [EPIN Examples Documentation](https://sashite.dev/specs/epin/1.0.0/examples/)
511
+ - [Game Protocol Foundation](https://sashite.dev/game-protocol/)
512
+ - [API Documentation](https://rubydoc.info/github/sashite/epin.rb/main)
513
+
514
+ ## Development
515
+
516
+ ```sh
517
+ # Clone the repository
518
+ git clone https://github.com/sashite/epin.rb.git
519
+ cd epin.rb
520
+
521
+ # Install dependencies
522
+ bundle install
523
+
524
+ # Run tests
525
+ ruby test.rb
526
+
527
+ # Generate documentation
528
+ yard doc
529
+ ```
530
+
531
+ ## Contributing
532
+
533
+ 1. Fork the repository
534
+ 2. Create a feature branch (`git checkout -b feature/new-feature`)
535
+ 3. Add tests for your changes
536
+ 4. Ensure all tests pass (`ruby test.rb`)
537
+ 5. Commit your changes (`git commit -am 'Add new feature'`)
538
+ 6. Push to the branch (`git push origin feature/new-feature`)
539
+ 7. Create a Pull Request
540
+
541
+ ## License
542
+
543
+ Available as open source under the [MIT License](https://opensource.org/licenses/MIT).
544
+
545
+ ## About
546
+
547
+ Maintained by [Sashité](https://sashite.com/) — promoting chess variants and sharing the beauty of board game cultures.
@@ -0,0 +1,441 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "sashite/pin"
4
+
5
+ module Sashite
6
+ module Epin
7
+ # Represents an identifier in EPIN (Extended Piece Identifier Notation) format.
8
+ #
9
+ # An identifier consists of a PIN component with an optional derivation marker:
10
+ # - PIN component: [<state>]<letter> (from PIN specification)
11
+ # - Derivation marker: "'" (foreign style) or none (native style)
12
+ #
13
+ # The case of the letter determines ownership:
14
+ # - Uppercase (A-Z): first player
15
+ # - Lowercase (a-z): second player
16
+ #
17
+ # Style derivation logic:
18
+ # - No suffix: piece has the native style of its current side
19
+ # - Apostrophe suffix: piece has the foreign style (opposite side's native style)
20
+ #
21
+ # All instances are immutable - state manipulation methods return new instances.
22
+ # This extends the Game Protocol's piece model with Style support through derivation.
23
+ class Identifier
24
+ # Valid derivation suffixes
25
+ DERIVATION_SUFFIX = "'"
26
+ NATIVE_SUFFIX = ""
27
+
28
+ # Derivation constants
29
+ NATIVE = true
30
+ FOREIGN = false
31
+
32
+ # Valid derivations
33
+ VALID_DERIVATIONS = [NATIVE, FOREIGN].freeze
34
+
35
+ # Error messages
36
+ ERROR_INVALID_EPIN = "Invalid EPIN string: %s"
37
+ ERROR_INVALID_DERIVATION = "Derivation must be true (native) or false (foreign), got: %s"
38
+
39
+ # @return [Symbol] the piece type (:A to :Z)
40
+ def type
41
+ @pin_identifier.type
42
+ end
43
+
44
+ # @return [Symbol] the player side (:first or :second)
45
+ def side
46
+ @pin_identifier.side
47
+ end
48
+
49
+ # @return [Symbol] the piece state (:normal, :enhanced, or :diminished)
50
+ def state
51
+ @pin_identifier.state
52
+ end
53
+
54
+ # @return [Boolean] the style derivation (true for native, false for foreign)
55
+ attr_reader :native
56
+
57
+ # Create a new identifier instance
58
+ #
59
+ # @param type [Symbol] piece type (:A to :Z)
60
+ # @param side [Symbol] player side (:first or :second)
61
+ # @param state [Symbol] piece state (:normal, :enhanced, or :diminished)
62
+ # @param native [Boolean] style derivation (true for native, false for foreign)
63
+ # @raise [ArgumentError] if parameters are invalid
64
+ # @example
65
+ # Identifier.new(:K, :first, :normal, true)
66
+ # Identifier.new(:P, :second, :enhanced, false)
67
+ def initialize(type, side, state = Pin::Identifier::NORMAL_STATE, native = NATIVE)
68
+ # Validate using PIN class methods for type, side, and state
69
+ Pin::Identifier.validate_type(type)
70
+ Pin::Identifier.validate_side(side)
71
+ Pin::Identifier.validate_state(state)
72
+ self.class.validate_derivation(native)
73
+
74
+ @pin_identifier = Pin::Identifier.new(type, side, state)
75
+ @native = native
76
+
77
+ freeze
78
+ end
79
+
80
+ # Parse an EPIN string into an Identifier object
81
+ #
82
+ # @param epin_string [String] EPIN notation string
83
+ # @return [Identifier] new identifier instance
84
+ # @raise [ArgumentError] if the EPIN string is invalid
85
+ # @example
86
+ # Epin::Identifier.parse("k") # => #<Epin::Identifier type=:K side=:second state=:normal native=true>
87
+ # Epin::Identifier.parse("+R'") # => #<Epin::Identifier type=:R side=:first state=:enhanced native=false>
88
+ # Epin::Identifier.parse("-p") # => #<Epin::Identifier type=:P side=:second state=:diminished native=true>
89
+ def self.parse(epin_string)
90
+ string_value = String(epin_string)
91
+
92
+ # Check for derivation suffix
93
+ if string_value.end_with?(DERIVATION_SUFFIX)
94
+ pin_part = string_value[0...-1] # Remove the apostrophe
95
+ foreign = true
96
+ else
97
+ pin_part = string_value
98
+ foreign = false
99
+ end
100
+
101
+ # Validate and parse the PIN part using existing PIN logic
102
+ raise ::ArgumentError, format(ERROR_INVALID_EPIN, string_value) unless Pin::Identifier.valid?(pin_part)
103
+
104
+ pin_identifier = Pin::Identifier.parse(pin_part)
105
+ identifier_native = !foreign
106
+
107
+ new(pin_identifier.type, pin_identifier.side, pin_identifier.state, identifier_native)
108
+ end
109
+
110
+ # Check if a string is a valid EPIN notation
111
+ #
112
+ # @param epin_string [String] The string to validate
113
+ # @return [Boolean] true if valid EPIN, false otherwise
114
+ #
115
+ # @example
116
+ # Sashite::Epin::Identifier.valid?("K") # => true
117
+ # Sashite::Epin::Identifier.valid?("+R'") # => true
118
+ # Sashite::Epin::Identifier.valid?("-p") # => true
119
+ # Sashite::Epin::Identifier.valid?("KK") # => false
120
+ # Sashite::Epin::Identifier.valid?("++K") # => false
121
+ def self.valid?(epin_string)
122
+ return false unless epin_string.is_a?(::String)
123
+ return false if epin_string.empty?
124
+
125
+ # Check for derivation suffix
126
+ if epin_string.end_with?(DERIVATION_SUFFIX)
127
+ pin_part = epin_string[0...-1] # Remove the apostrophe
128
+ return false if pin_part.empty? # Can't have just an apostrophe
129
+ else
130
+ pin_part = epin_string
131
+ end
132
+
133
+ # Validate the PIN part using existing PIN validation
134
+ Pin::Identifier.valid?(pin_part)
135
+ end
136
+
137
+ # Convert the identifier to its EPIN string representation
138
+ #
139
+ # @return [String] EPIN notation string
140
+ # @example
141
+ # identifier.to_s # => "+R'"
142
+ # identifier.to_s # => "-p"
143
+ # identifier.to_s # => "K"
144
+ def to_s
145
+ "#{prefix}#{letter}#{suffix}"
146
+ end
147
+
148
+ # Get the letter representation (inherited from PIN logic)
149
+ #
150
+ # @return [String] letter representation combining type and side
151
+ def letter
152
+ @pin_identifier.letter
153
+ end
154
+
155
+ # Get the prefix representation (inherited from PIN logic)
156
+ #
157
+ # @return [String] prefix representing the state
158
+ def prefix
159
+ @pin_identifier.prefix
160
+ end
161
+
162
+ # Get the suffix representation
163
+ #
164
+ # @return [String] suffix representing the derivation
165
+ def suffix
166
+ native? ? NATIVE_SUFFIX : DERIVATION_SUFFIX
167
+ end
168
+
169
+ # Create a new identifier with enhanced state
170
+ #
171
+ # @return [Identifier] new identifier instance with enhanced state
172
+ # @example
173
+ # identifier.enhance # (:K, :first, :normal, true) => (:K, :first, :enhanced, true)
174
+ def enhance
175
+ return self if enhanced?
176
+
177
+ self.class.new(type, side, Pin::Identifier::ENHANCED_STATE, native)
178
+ end
179
+
180
+ # Create a new identifier without enhanced state
181
+ #
182
+ # @return [Identifier] new identifier instance without enhanced state
183
+ # @example
184
+ # identifier.unenhance # (:K, :first, :enhanced, true) => (:K, :first, :normal, true)
185
+ def unenhance
186
+ return self unless enhanced?
187
+
188
+ self.class.new(type, side, Pin::Identifier::NORMAL_STATE, native)
189
+ end
190
+
191
+ # Create a new identifier with diminished state
192
+ #
193
+ # @return [Identifier] new identifier instance with diminished state
194
+ # @example
195
+ # identifier.diminish # (:K, :first, :normal, true) => (:K, :first, :diminished, true)
196
+ def diminish
197
+ return self if diminished?
198
+
199
+ self.class.new(type, side, Pin::Identifier::DIMINISHED_STATE, native)
200
+ end
201
+
202
+ # Create a new identifier without diminished state
203
+ #
204
+ # @return [Identifier] new identifier instance without diminished state
205
+ # @example
206
+ # identifier.undiminish # (:K, :first, :diminished, true) => (:K, :first, :normal, true)
207
+ def undiminish
208
+ return self unless diminished?
209
+
210
+ self.class.new(type, side, Pin::Identifier::NORMAL_STATE, native)
211
+ end
212
+
213
+ # Create a new identifier with normal state (no modifiers)
214
+ #
215
+ # @return [Identifier] new identifier instance with normal state
216
+ # @example
217
+ # identifier.normalize # (:K, :first, :enhanced, true) => (:K, :first, :normal, true)
218
+ def normalize
219
+ return self if normal?
220
+
221
+ self.class.new(type, side, Pin::Identifier::NORMAL_STATE, native)
222
+ end
223
+
224
+ # Create a new identifier with opposite side
225
+ #
226
+ # @return [Identifier] new identifier instance with opposite side
227
+ # @example
228
+ # identifier.flip # (:K, :first, :normal, true) => (:K, :second, :normal, true)
229
+ def flip
230
+ self.class.new(type, opposite_side, state, native)
231
+ end
232
+
233
+ # Create a new identifier with foreign style (derivation marker)
234
+ #
235
+ # @return [Identifier] new identifier instance with foreign style
236
+ # @example
237
+ # identifier.derive # (:K, :first, :normal, true) => (:K, :first, :normal, false)
238
+ def derive
239
+ return self if derived?
240
+
241
+ self.class.new(type, side, state, FOREIGN)
242
+ end
243
+
244
+ # Create a new identifier with native style (no derivation marker)
245
+ #
246
+ # @return [Identifier] new identifier instance with native style
247
+ # @example
248
+ # identifier.underive # (:K, :first, :normal, false) => (:K, :first, :normal, true)
249
+ def underive
250
+ return self if native?
251
+
252
+ self.class.new(type, side, state, NATIVE)
253
+ end
254
+
255
+ # Create a new identifier with a different type (keeping same side, state, and derivation)
256
+ #
257
+ # @param new_type [Symbol] new type (:A to :Z)
258
+ # @return [Identifier] new identifier instance with different type
259
+ # @example
260
+ # identifier.with_type(:Q) # (:K, :first, :normal, true) => (:Q, :first, :normal, true)
261
+ def with_type(new_type)
262
+ Pin::Identifier.validate_type(new_type)
263
+ return self if type == new_type
264
+
265
+ self.class.new(new_type, side, state, native)
266
+ end
267
+
268
+ # Create a new identifier with a different side (keeping same type, state, and derivation)
269
+ #
270
+ # @param new_side [Symbol] :first or :second
271
+ # @return [Identifier] new identifier instance with different side
272
+ # @example
273
+ # identifier.with_side(:second) # (:K, :first, :normal, true) => (:K, :second, :normal, true)
274
+ def with_side(new_side)
275
+ Pin::Identifier.validate_side(new_side)
276
+ return self if side == new_side
277
+
278
+ self.class.new(type, new_side, state, native)
279
+ end
280
+
281
+ # Create a new identifier with a different state (keeping same type, side, and derivation)
282
+ #
283
+ # @param new_state [Symbol] :normal, :enhanced, or :diminished
284
+ # @return [Identifier] new identifier instance with different state
285
+ # @example
286
+ # identifier.with_state(:enhanced) # (:K, :first, :normal, true) => (:K, :first, :enhanced, true)
287
+ def with_state(new_state)
288
+ Pin::Identifier.validate_state(new_state)
289
+ return self if state == new_state
290
+
291
+ self.class.new(type, side, new_state, native)
292
+ end
293
+
294
+ # Create a new identifier with a different derivation (keeping same type, side, and state)
295
+ #
296
+ # @param new_native [Boolean] true for native, false for foreign
297
+ # @return [Identifier] new identifier instance with different derivation
298
+ # @example
299
+ # identifier.with_derivation(false) # (:K, :first, :normal, true) => (:K, :first, :normal, false)
300
+ def with_derivation(new_native)
301
+ self.class.validate_derivation(new_native)
302
+ return self if native == new_native
303
+
304
+ self.class.new(type, side, state, new_native)
305
+ end
306
+
307
+ # Check if the identifier has enhanced state
308
+ #
309
+ # @return [Boolean] true if enhanced
310
+ def enhanced?
311
+ @pin_identifier.enhanced?
312
+ end
313
+
314
+ # Check if the identifier has diminished state
315
+ #
316
+ # @return [Boolean] true if diminished
317
+ def diminished?
318
+ @pin_identifier.diminished?
319
+ end
320
+
321
+ # Check if the identifier has normal state (no modifiers)
322
+ #
323
+ # @return [Boolean] true if no modifiers are present
324
+ def normal?
325
+ @pin_identifier.normal?
326
+ end
327
+
328
+ # Check if the identifier belongs to the first player
329
+ #
330
+ # @return [Boolean] true if first player
331
+ def first_player?
332
+ @pin_identifier.first_player?
333
+ end
334
+
335
+ # Check if the identifier belongs to the second player
336
+ #
337
+ # @return [Boolean] true if second player
338
+ def second_player?
339
+ @pin_identifier.second_player?
340
+ end
341
+
342
+ # Check if the identifier has native style (no derivation marker)
343
+ #
344
+ # @return [Boolean] true if native style
345
+ def native?
346
+ native == NATIVE
347
+ end
348
+
349
+ # Check if the identifier has foreign style (derivation marker)
350
+ #
351
+ # @return [Boolean] true if foreign style
352
+ def derived?
353
+ native == FOREIGN
354
+ end
355
+
356
+ # Alias for derived? to match the specification terminology
357
+ alias foreign? derived?
358
+
359
+ # Check if this identifier is the same type as another (ignoring side, state, and derivation)
360
+ #
361
+ # @param other [Identifier] identifier to compare with
362
+ # @return [Boolean] true if same type
363
+ # @example
364
+ # king1.same_type?(king2) # (:K, :first, :normal, true) and (:K, :second, :enhanced, false) => true
365
+ def same_type?(other)
366
+ return false unless other.is_a?(self.class)
367
+
368
+ @pin_identifier.same_type?(other.instance_variable_get(:@pin_identifier))
369
+ end
370
+
371
+ # Check if this identifier belongs to the same side as another
372
+ #
373
+ # @param other [Identifier] identifier to compare with
374
+ # @return [Boolean] true if same side
375
+ def same_side?(other)
376
+ return false unless other.is_a?(self.class)
377
+
378
+ @pin_identifier.same_side?(other.instance_variable_get(:@pin_identifier))
379
+ end
380
+
381
+ # Check if this identifier has the same state as another
382
+ #
383
+ # @param other [Identifier] identifier to compare with
384
+ # @return [Boolean] true if same state
385
+ def same_state?(other)
386
+ return false unless other.is_a?(self.class)
387
+
388
+ @pin_identifier.same_state?(other.instance_variable_get(:@pin_identifier))
389
+ end
390
+
391
+ # Check if this identifier has the same style derivation as another
392
+ #
393
+ # @param other [Identifier] identifier to compare with
394
+ # @return [Boolean] true if same style derivation
395
+ def same_style?(other)
396
+ return false unless other.is_a?(self.class)
397
+
398
+ native == other.native
399
+ end
400
+
401
+ # Custom equality comparison
402
+ #
403
+ # @param other [Object] object to compare with
404
+ # @return [Boolean] true if identifiers are equal
405
+ def ==(other)
406
+ return false unless other.is_a?(self.class)
407
+
408
+ @pin_identifier == other.instance_variable_get(:@pin_identifier) && native == other.native
409
+ end
410
+
411
+ # Alias for == to ensure Set functionality works correctly
412
+ alias eql? ==
413
+
414
+ # Custom hash implementation for use in collections
415
+ #
416
+ # @return [Integer] hash value
417
+ def hash
418
+ [self.class, @pin_identifier, native].hash
419
+ end
420
+
421
+ # Validate that the derivation is a valid boolean
422
+ #
423
+ # @param derivation [Boolean] the derivation to validate
424
+ # @raise [ArgumentError] if invalid
425
+ def self.validate_derivation(derivation)
426
+ return if VALID_DERIVATIONS.include?(derivation)
427
+
428
+ raise ::ArgumentError, format(ERROR_INVALID_DERIVATION, derivation.inspect)
429
+ end
430
+
431
+ private
432
+
433
+ # Get the opposite side of the current identifier
434
+ #
435
+ # @return [Symbol] :first if current side is :second, :second if current side is :first
436
+ def opposite_side
437
+ @pin_identifier.send(:opposite_side)
438
+ end
439
+ end
440
+ end
441
+ end
@@ -0,0 +1,69 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "epin/identifier"
4
+
5
+ module Sashite
6
+ # EPIN (Extended Piece Identifier Notation) implementation for Ruby
7
+ #
8
+ # Provides style-aware ASCII-based format for representing pieces in abstract strategy board games.
9
+ # EPIN extends PIN by adding derivation markers that distinguish pieces by their style origin,
10
+ # enabling cross-style game scenarios and piece origin tracking.
11
+ #
12
+ # Format: [<state>]<letter>[<derivation>]
13
+ # - State modifier: "+" (enhanced), "-" (diminished), or none (normal)
14
+ # - Letter: A-Z (first player), a-z (second player)
15
+ # - Derivation marker: "'" (foreign style), or none (native style)
16
+ #
17
+ # Examples:
18
+ # "K" - First player king (native style, normal state)
19
+ # "k'" - Second player king (foreign style, normal state)
20
+ # "+R'" - First player rook (foreign style, enhanced state)
21
+ # "-p" - Second player pawn (native style, diminished state)
22
+ #
23
+ # See: https://sashite.dev/specs/epin/1.0.0/
24
+ module Epin
25
+ # Check if a string is a valid EPIN notation
26
+ #
27
+ # @param epin_string [String] The string to validate
28
+ # @return [Boolean] true if valid EPIN, false otherwise
29
+ #
30
+ # @example
31
+ # Sashite::Epin.valid?("K") # => true
32
+ # Sashite::Epin.valid?("+R'") # => true
33
+ # Sashite::Epin.valid?("-p") # => true
34
+ # Sashite::Epin.valid?("KK") # => false
35
+ # Sashite::Epin.valid?("++K") # => false
36
+ def self.valid?(epin_string)
37
+ Identifier.valid?(epin_string)
38
+ end
39
+
40
+ # Parse an EPIN string into an Identifier object
41
+ #
42
+ # @param epin_string [String] EPIN notation string
43
+ # @return [Epin::Identifier] new identifier instance
44
+ # @raise [ArgumentError] if the EPIN string is invalid
45
+ # @example
46
+ # Sashite::Epin.parse("K") # => #<Epin::Identifier type=:K side=:first state=:normal native=true>
47
+ # Sashite::Epin.parse("+R'") # => #<Epin::Identifier type=:R side=:first state=:enhanced native=false>
48
+ # Sashite::Epin.parse("-p") # => #<Epin::Identifier type=:P side=:second state=:diminished native=true>
49
+ def self.parse(epin_string)
50
+ Identifier.parse(epin_string)
51
+ end
52
+
53
+ # Create a new identifier instance
54
+ #
55
+ # @param type [Symbol] piece type (:A to :Z)
56
+ # @param side [Symbol] player side (:first or :second)
57
+ # @param state [Symbol] piece state (:normal, :enhanced, or :diminished)
58
+ # @param native [Boolean] style derivation (true for native, false for foreign)
59
+ # @return [Epin::Identifier] new identifier instance
60
+ # @raise [ArgumentError] if parameters are invalid
61
+ # @example
62
+ # Sashite::Epin.identifier(:K, :first, :normal, true) # => #<Epin::Identifier type=:K side=:first state=:normal native=true>
63
+ # Sashite::Epin.identifier(:R, :first, :enhanced, false) # => #<Epin::Identifier type=:R side=:first state=:enhanced native=false>
64
+ # Sashite::Epin.identifier(:P, :second, :diminished, true) # => #<Epin::Identifier type=:P side=:second state=:diminished native=true>
65
+ def self.identifier(type, side, state = Sashite::Pin::Identifier::NORMAL_STATE, native = Identifier::NATIVE)
66
+ Identifier.new(type, side, state, native)
67
+ end
68
+ end
69
+ end
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "sashite/epin"
4
+
5
+ # Sashité namespace for board game notation libraries
6
+ #
7
+ # Sashité provides a collection of libraries for representing and manipulating
8
+ # board game concepts according to the Game Protocol specifications.
9
+ #
10
+ # @see https://sashite.dev/game-protocol/ Game Protocol Foundation
11
+ # @see https://sashite.dev/specs/ Sashité Specifications
12
+ # @author Sashité
13
+ module Sashite
14
+ end
metadata ADDED
@@ -0,0 +1,72 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: sashite-epin
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.0
5
+ platform: ruby
6
+ authors:
7
+ - Cyril Kato
8
+ bindir: bin
9
+ cert_chain: []
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
+ dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: sashite-pin
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - "~>"
17
+ - !ruby/object:Gem::Version
18
+ version: 3.0.0
19
+ type: :runtime
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - "~>"
24
+ - !ruby/object:Gem::Version
25
+ version: 3.0.0
26
+ description: |
27
+ EPIN (Extended Piece Identifier Notation) extends PIN to provide style-aware piece representation
28
+ in abstract strategy board games. This gem implements the EPIN Specification v1.0.0 with
29
+ a modern Ruby interface featuring immutable identifier objects and functional programming
30
+ principles. EPIN adds derivation markers to PIN that distinguish pieces by their style
31
+ origin, enabling cross-style game scenarios and piece origin tracking. Represents all
32
+ four Game Protocol piece attributes with full PIN backward compatibility. Perfect for
33
+ game engines, cross-tradition tournaments, and hybrid board game environments.
34
+ email: contact@cyril.email
35
+ executables: []
36
+ extensions: []
37
+ extra_rdoc_files: []
38
+ files:
39
+ - LICENSE.md
40
+ - README.md
41
+ - lib/sashite-epin.rb
42
+ - lib/sashite/epin.rb
43
+ - lib/sashite/epin/identifier.rb
44
+ homepage: https://github.com/sashite/epin.rb
45
+ licenses:
46
+ - MIT
47
+ metadata:
48
+ bug_tracker_uri: https://github.com/sashite/epin.rb/issues
49
+ documentation_uri: https://rubydoc.info/github/sashite/epin.rb/main
50
+ homepage_uri: https://github.com/sashite/epin.rb
51
+ source_code_uri: https://github.com/sashite/epin.rb
52
+ specification_uri: https://sashite.dev/specs/epin/1.0.0/
53
+ rubygems_mfa_required: 'true'
54
+ rdoc_options: []
55
+ require_paths:
56
+ - lib
57
+ required_ruby_version: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: 3.2.0
62
+ required_rubygems_version: !ruby/object:Gem::Requirement
63
+ requirements:
64
+ - - ">="
65
+ - !ruby/object:Gem::Version
66
+ version: '0'
67
+ requirements: []
68
+ rubygems_version: 3.6.9
69
+ specification_version: 4
70
+ summary: EPIN (Extended Piece Identifier Notation) implementation for Ruby extending
71
+ PIN with style derivation markers.
72
+ test_files: []