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 +7 -0
- data/LICENSE.md +21 -0
- data/README.md +547 -0
- data/lib/sashite/epin/identifier.rb +441 -0
- data/lib/sashite/epin.rb +69 -0
- data/lib/sashite-epin.rb +14 -0
- metadata +72 -0
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
|
+
[](https://github.com/sashite/epin.rb/tags)
|
4
|
+
[](https://rubydoc.info/github/sashite/epin.rb/main)
|
5
|
+

|
6
|
+
[](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
|
data/lib/sashite/epin.rb
ADDED
@@ -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
|
data/lib/sashite-epin.rb
ADDED
@@ -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: []
|