sashite-epin 1.2.0 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +509 -396
- data/lib/sashite/epin/identifier.rb +168 -463
- data/lib/sashite/epin.rb +195 -54
- metadata +1 -1
data/lib/sashite/epin.rb
CHANGED
|
@@ -5,79 +5,220 @@ require_relative "epin/identifier"
|
|
|
5
5
|
module Sashite
|
|
6
6
|
# EPIN (Extended Piece Identifier Notation) implementation for Ruby
|
|
7
7
|
#
|
|
8
|
-
#
|
|
9
|
-
#
|
|
10
|
-
#
|
|
11
|
-
#
|
|
12
|
-
#
|
|
13
|
-
#
|
|
14
|
-
# -
|
|
15
|
-
# -
|
|
16
|
-
#
|
|
17
|
-
#
|
|
18
|
-
#
|
|
19
|
-
#
|
|
20
|
-
#
|
|
21
|
-
#
|
|
22
|
-
#
|
|
23
|
-
#
|
|
24
|
-
#
|
|
25
|
-
#
|
|
26
|
-
#
|
|
27
|
-
#
|
|
8
|
+
# Extends PIN (Piece Identifier Notation) with a derivation marker to track piece style
|
|
9
|
+
# in cross-style games. EPIN is simply: PIN + optional style derivation marker (').
|
|
10
|
+
#
|
|
11
|
+
# ## Core Concept
|
|
12
|
+
#
|
|
13
|
+
# EPIN addresses the need to distinguish between:
|
|
14
|
+
# - **Native pieces**: Using their own side's native style (no marker)
|
|
15
|
+
# - **Derived pieces**: Using the opponent's native style (marked with ')
|
|
16
|
+
#
|
|
17
|
+
# This distinction is essential for cross-style games where different players use
|
|
18
|
+
# different game traditions (e.g., Chess vs Makruk, Chess vs Shogi).
|
|
19
|
+
#
|
|
20
|
+
# ## Pure Composition
|
|
21
|
+
#
|
|
22
|
+
# EPIN doesn't reimplement PIN - it's pure composition:
|
|
23
|
+
#
|
|
24
|
+
# EPIN = PIN + derived flag
|
|
25
|
+
#
|
|
26
|
+
# All piece attributes (name, side, state, terminal) come from the PIN component.
|
|
27
|
+
# EPIN adds only the 5th attribute: piece style (native vs derived).
|
|
28
|
+
#
|
|
29
|
+
# ## Minimal API
|
|
30
|
+
#
|
|
31
|
+
# Module-level methods (3 total):
|
|
32
|
+
# 1. valid?(epin_string) - validate EPIN string
|
|
33
|
+
# 2. parse(epin_string) - parse into Identifier
|
|
34
|
+
# 3. new(pin, derived: false) - create from PIN component
|
|
35
|
+
#
|
|
36
|
+
# ## Five Fundamental Attributes
|
|
37
|
+
#
|
|
38
|
+
# EPIN represents all five piece attributes from the Sashité Game Protocol:
|
|
39
|
+
#
|
|
40
|
+
# From PIN component (4 attributes):
|
|
41
|
+
# - **Piece Name**: epin.pin.type
|
|
42
|
+
# - **Piece Side**: epin.pin.side
|
|
43
|
+
# - **Piece State**: epin.pin.state
|
|
44
|
+
# - **Terminal Status**: epin.pin.terminal?
|
|
45
|
+
#
|
|
46
|
+
# From EPIN (5th attribute):
|
|
47
|
+
# - **Piece Style**: epin.derived? (native vs derived)
|
|
48
|
+
#
|
|
49
|
+
# ## Format Structure
|
|
50
|
+
#
|
|
51
|
+
# Structure: `<pin>[']`
|
|
52
|
+
#
|
|
53
|
+
# Grammar (BNF):
|
|
54
|
+
# <epin> ::= <pin> | <pin> "'"
|
|
55
|
+
# <pin> ::= ["+" | "-"] <letter> ["^"]
|
|
56
|
+
# <letter> ::= "A" | ... | "Z" | "a" | ... | "z"
|
|
57
|
+
#
|
|
58
|
+
# Regular Expression: `/\A[-+]?[A-Za-z]\^?'?\z/`
|
|
59
|
+
#
|
|
60
|
+
# ## Semantics
|
|
61
|
+
#
|
|
62
|
+
# ### Native vs Derived
|
|
63
|
+
#
|
|
64
|
+
# In cross-style games (e.g., Chess vs Makruk):
|
|
65
|
+
# - First player's native style: Chess
|
|
66
|
+
# - Second player's native style: Makruk
|
|
67
|
+
#
|
|
68
|
+
# Then:
|
|
69
|
+
# - "K" = First player king in Chess style (native)
|
|
70
|
+
# - "K'" = First player king in Makruk style (derived from opponent)
|
|
71
|
+
# - "k" = Second player king in Makruk style (native)
|
|
72
|
+
# - "k'" = Second player king in Chess style (derived from opponent)
|
|
73
|
+
#
|
|
74
|
+
# ### Backward Compatibility
|
|
75
|
+
#
|
|
76
|
+
# Every valid PIN token is a valid EPIN token:
|
|
77
|
+
# - "K" is valid PIN and valid EPIN (native)
|
|
78
|
+
# - "+R^" is valid PIN and valid EPIN (native)
|
|
79
|
+
# - All PIN semantics preserved
|
|
80
|
+
#
|
|
81
|
+
# EPIN extends PIN by adding the optional derivation marker:
|
|
82
|
+
# - "K'" is valid EPIN (derived)
|
|
83
|
+
# - "+R^'" is valid EPIN (enhanced, terminal, derived)
|
|
84
|
+
#
|
|
85
|
+
# ## Examples
|
|
86
|
+
#
|
|
87
|
+
# ### Basic Usage
|
|
88
|
+
#
|
|
89
|
+
# # Parse EPIN strings
|
|
90
|
+
# native = Sashite::Epin.parse("K^") # Native king
|
|
91
|
+
# derived = Sashite::Epin.parse("K^'") # Derived king
|
|
92
|
+
#
|
|
93
|
+
# # Access attributes via PIN component
|
|
94
|
+
# native.pin.type # => :K
|
|
95
|
+
# native.pin.terminal? # => true
|
|
96
|
+
# native.derived? # => false
|
|
97
|
+
#
|
|
98
|
+
# # Create from PIN component
|
|
99
|
+
# pin = Sashite::Pin.parse("K^")
|
|
100
|
+
# epin = Sashite::Epin.new(pin, derived: false)
|
|
101
|
+
# epin.to_s # => "K^"
|
|
102
|
+
#
|
|
103
|
+
# ### Transformations
|
|
104
|
+
#
|
|
105
|
+
# epin = Sashite::Epin.parse("K^")
|
|
106
|
+
#
|
|
107
|
+
# # Mark as derived
|
|
108
|
+
# derived = epin.mark_derived
|
|
109
|
+
# derived.to_s # => "K^'"
|
|
110
|
+
#
|
|
111
|
+
# # Transform PIN component
|
|
112
|
+
# queen = epin.with_pin(epin.pin.with_type(:Q))
|
|
113
|
+
# queen.to_s # => "Q^"
|
|
114
|
+
#
|
|
115
|
+
# # Transform both
|
|
116
|
+
# derived_queen = epin
|
|
117
|
+
# .with_pin(epin.pin.with_type(:Q))
|
|
118
|
+
# .mark_derived
|
|
119
|
+
# derived_queen.to_s # => "Q^'"
|
|
120
|
+
#
|
|
121
|
+
# ### Cross-Style Games
|
|
122
|
+
#
|
|
123
|
+
# # Chess vs Makruk match
|
|
124
|
+
# # First player = Chess, Second player = Makruk
|
|
125
|
+
#
|
|
126
|
+
# chess_king = Sashite::Epin.parse("K^") # Native Chess king
|
|
127
|
+
# makruk_pawn = Sashite::Epin.parse("P'") # Derived Makruk pawn
|
|
128
|
+
#
|
|
129
|
+
# chess_king.native? # => true (uses Chess style)
|
|
130
|
+
# makruk_pawn.derived? # => true (uses Makruk style)
|
|
131
|
+
#
|
|
132
|
+
# ## Design Properties
|
|
133
|
+
#
|
|
134
|
+
# - **Rule-agnostic**: Independent of game mechanics
|
|
135
|
+
# - **Pure composition**: Extends PIN minimally (PIN + derived flag)
|
|
136
|
+
# - **Minimal API**: Only 3 module methods, 6 instance methods
|
|
137
|
+
# - **Component transparency**: Direct PIN access via epin.pin
|
|
138
|
+
# - **Backward compatible**: All PIN tokens are valid EPIN tokens
|
|
139
|
+
# - **Immutable**: All instances frozen, transformations return new objects
|
|
140
|
+
# - **Type-safe**: Full PIN type preservation
|
|
141
|
+
# - **Style-aware**: Tracks native vs derived pieces
|
|
142
|
+
# - **Compact**: Single character overhead for style information
|
|
143
|
+
#
|
|
144
|
+
# @see https://sashite.dev/specs/epin/1.0.0/ EPIN Specification v1.0.0
|
|
145
|
+
# @see https://sashite.dev/specs/epin/1.0.0/examples/ EPIN Examples
|
|
146
|
+
# @see https://sashite.dev/specs/pin/1.0.0/ PIN Specification (base component)
|
|
28
147
|
module Epin
|
|
29
148
|
# Check if a string is a valid EPIN notation
|
|
30
149
|
#
|
|
31
|
-
#
|
|
150
|
+
# Validates both the EPIN format and the underlying PIN component.
|
|
151
|
+
#
|
|
152
|
+
# @param epin_string [String] the string to validate
|
|
32
153
|
# @return [Boolean] true if valid EPIN, false otherwise
|
|
33
154
|
#
|
|
34
|
-
# @example
|
|
35
|
-
# Sashite::Epin.valid?("K")
|
|
36
|
-
# Sashite::Epin.valid?("K^")
|
|
37
|
-
# Sashite::Epin.valid?("+R'") # => true
|
|
38
|
-
# Sashite::Epin.valid?("
|
|
39
|
-
# Sashite::Epin.valid?("
|
|
40
|
-
# Sashite::Epin.valid?("
|
|
41
|
-
# Sashite::Epin.valid?("++K") # => false
|
|
42
|
-
# Sashite::Epin.valid?("K'^") # => false (wrong order)
|
|
155
|
+
# @example Validate EPIN strings
|
|
156
|
+
# Sashite::Epin.valid?("K^") # => true (valid PIN, native)
|
|
157
|
+
# Sashite::Epin.valid?("K^'") # => true (valid PIN with derivation)
|
|
158
|
+
# Sashite::Epin.valid?("+R'") # => true (enhanced derived rook)
|
|
159
|
+
# Sashite::Epin.valid?("K^''") # => false (multiple markers)
|
|
160
|
+
# Sashite::Epin.valid?("KK'") # => false (invalid PIN part)
|
|
161
|
+
# Sashite::Epin.valid?("invalid") # => false (invalid format)
|
|
43
162
|
def self.valid?(epin_string)
|
|
44
163
|
Identifier.valid?(epin_string)
|
|
45
164
|
end
|
|
46
165
|
|
|
47
166
|
# Parse an EPIN string into an Identifier object
|
|
48
167
|
#
|
|
49
|
-
#
|
|
50
|
-
#
|
|
168
|
+
# Creates a new EPIN identifier by parsing the string, extracting the PIN part
|
|
169
|
+
# and derivation marker, validating the PIN component, and creating an identifier
|
|
170
|
+
# with the appropriate derivation status.
|
|
171
|
+
#
|
|
172
|
+
# @param epin_string [String] EPIN notation string (format: <pin>['])
|
|
173
|
+
# @return [Epin::Identifier] parsed identifier with PIN component and derivation flag
|
|
51
174
|
# @raise [ArgumentError] if the EPIN string is invalid
|
|
52
175
|
#
|
|
53
|
-
# @example
|
|
54
|
-
# Sashite::Epin.parse("K")
|
|
55
|
-
# Sashite::Epin.parse("K^")
|
|
56
|
-
# Sashite::Epin.parse("+R
|
|
57
|
-
# Sashite::Epin.parse("+
|
|
58
|
-
# Sashite::Epin.parse("-p")
|
|
176
|
+
# @example Parse different EPIN formats
|
|
177
|
+
# Sashite::Epin.parse("K^") # => Native king, terminal
|
|
178
|
+
# Sashite::Epin.parse("K^'") # => Derived king, terminal
|
|
179
|
+
# Sashite::Epin.parse("+R") # => Native rook, enhanced
|
|
180
|
+
# Sashite::Epin.parse("+R'") # => Derived rook, enhanced
|
|
181
|
+
# Sashite::Epin.parse("-p") # => Native pawn, diminished
|
|
182
|
+
#
|
|
183
|
+
# @example Access all five attributes
|
|
184
|
+
# epin = Sashite::Epin.parse("+R^'")
|
|
185
|
+
# epin.pin.type # => :R (Piece Name)
|
|
186
|
+
# epin.pin.side # => :first (Piece Side)
|
|
187
|
+
# epin.pin.state # => :enhanced (Piece State)
|
|
188
|
+
# epin.pin.terminal? # => true (Terminal Status)
|
|
189
|
+
# epin.derived? # => true (Piece Style)
|
|
59
190
|
def self.parse(epin_string)
|
|
60
191
|
Identifier.parse(epin_string)
|
|
61
192
|
end
|
|
62
193
|
|
|
63
|
-
# Create a new identifier
|
|
194
|
+
# Create a new identifier from a PIN component and derivation flag
|
|
195
|
+
#
|
|
196
|
+
# Constructs an EPIN identifier by combining a PIN component (which provides
|
|
197
|
+
# the four base attributes: name, side, state, terminal) with a derivation flag
|
|
198
|
+
# (which provides the fifth attribute: style).
|
|
199
|
+
#
|
|
200
|
+
# @param pin [Pin::Identifier] PIN component providing base attributes
|
|
201
|
+
# @param derived [Boolean] whether the piece uses derived style (default: false)
|
|
202
|
+
# @return [Epin::Identifier] new immutable identifier instance
|
|
203
|
+
# @raise [ArgumentError] if pin is not a Pin::Identifier
|
|
204
|
+
#
|
|
205
|
+
# @example Create identifiers from PIN components
|
|
206
|
+
# pin = Sashite::Pin.parse("K^")
|
|
207
|
+
# native = Sashite::Epin.new(pin, derived: false)
|
|
208
|
+
# native.to_s # => "K^"
|
|
209
|
+
#
|
|
210
|
+
# derived = Sashite::Epin.new(pin, derived: true)
|
|
211
|
+
# derived.to_s # => "K^'"
|
|
64
212
|
#
|
|
65
|
-
# @
|
|
66
|
-
#
|
|
67
|
-
#
|
|
68
|
-
#
|
|
69
|
-
# @param terminal [Boolean] whether the piece is a terminal piece
|
|
70
|
-
# @return [Epin::Identifier] new identifier instance
|
|
71
|
-
# @raise [ArgumentError] if parameters are invalid
|
|
213
|
+
# @example Cross-style game setup
|
|
214
|
+
# # First player uses Chess style, second uses Makruk style
|
|
215
|
+
# chess_king = Sashite::Epin.new(Sashite::Pin.parse("K^"), derived: false)
|
|
216
|
+
# makruk_pawn = Sashite::Epin.new(Sashite::Pin.parse("P"), derived: true)
|
|
72
217
|
#
|
|
73
|
-
#
|
|
74
|
-
#
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
# Sashite::Epin.identifier(:K, :first, :enhanced, false, terminal: true) # => "+K^'"
|
|
78
|
-
# Sashite::Epin.identifier(:P, :second, :diminished, true) # => "-p"
|
|
79
|
-
def self.identifier(type, side, state, native, terminal: false)
|
|
80
|
-
Identifier.new(type, side, state, native, terminal: terminal)
|
|
218
|
+
# chess_king.native? # => true (uses own Chess style)
|
|
219
|
+
# makruk_pawn.derived? # => true (uses opponent's Makruk style)
|
|
220
|
+
def self.new(pin, derived: false)
|
|
221
|
+
Identifier.new(pin, derived: derived)
|
|
81
222
|
end
|
|
82
223
|
end
|
|
83
224
|
end
|