sashite-epin 2.0.0 → 2.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
data/lib/sashite/epin.rb CHANGED
@@ -1,224 +1,301 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative "epin/identifier"
3
+ require "sashite/pin"
4
4
 
5
5
  module Sashite
6
- # EPIN (Extended Piece Identifier Notation) implementation for Ruby
6
+ # EPIN (Extended Piece Identifier Notation) implementation for Ruby.
7
7
  #
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 (').
8
+ # EPIN extends PIN by adding a **derivation marker** to track piece style
9
+ # in cross-style games.
10
10
  #
11
- # ## Core Concept
11
+ # **EPIN is simply: PIN + optional style derivation marker (`'`)**
12
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 ')
13
+ # == Format
16
14
  #
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).
15
+ # <pin-token>[<derivation-marker>]
19
16
  #
20
- # ## Pure Composition
17
+ # Where +<pin-token>+ is a valid PIN token and +<derivation-marker>+ is
18
+ # an optional trailing apostrophe (<tt>'</tt>).
21
19
  #
22
- # EPIN doesn't reimplement PIN - it's pure composition:
20
+ # == Five Fundamental Attributes
23
21
  #
24
- # EPIN = PIN + derived flag
22
+ # EPIN exposes all five attributes from the Sashité Game Protocol:
25
23
  #
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).
24
+ # - *Piece Name* â†' +epin.pin.type+
25
+ # - *Piece Side* â†' +epin.pin.side+
26
+ # - *Piece State* â†' +epin.pin.state+
27
+ # - *Terminal Status* â†' +epin.pin.terminal+
28
+ # - *Piece Style* â†' +epin.derived+ (native vs derived)
28
29
  #
29
- # ## Minimal API
30
+ # == Examples
30
31
  #
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
32
+ # epin = Sashite::Epin.parse("K^'")
33
+ # epin.pin.type # => :K
34
+ # epin.pin.terminal # => true
35
+ # epin.derived # => true
35
36
  #
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
37
  # 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
38
+ # epin = Sashite::Epin.new(pin, derived: true)
39
+ # epin.to_s # => "K^'"
128
40
  #
129
- # chess_king.native? # => true (uses Chess style)
130
- # makruk_pawn.derived? # => true (uses Makruk style)
41
+ # Sashite::Epin.valid?("K^'") # => true
42
+ # Sashite::Epin.valid?("K'^") # => false
131
43
  #
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)
147
- module Epin
148
- # Check if a string is a valid EPIN notation
149
- #
150
- # Validates both the EPIN format and the underlying PIN component.
151
- #
152
- # @param epin_string [String] the string to validate
153
- # @return [Boolean] true if valid EPIN, false otherwise
154
- #
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)
162
- def self.valid?(epin_string)
163
- Identifier.valid?(epin_string)
44
+ # See the EPIN Specification (https://sashite.dev/specs/epin/1.0.0/) for details.
45
+ class Epin
46
+ # Pattern for validating EPIN strings
47
+ EPIN_PATTERN = /\A(?<pin>[-+]?[A-Za-z]\^?)(?<derived>')?\z/
48
+
49
+ # @return [Sashite::Pin] The underlying PIN component
50
+ attr_reader :pin
51
+
52
+ # @return [Boolean] Derivation status (true = derived, false = native)
53
+ attr_reader :derived
54
+
55
+ # ========================================================================
56
+ # Creation and Parsing
57
+ # ========================================================================
58
+
59
+ # Creates a new EPIN instance from a PIN component.
60
+ #
61
+ # @param pin [Sashite::Pin] The underlying PIN instance
62
+ # @param derived [Boolean] Derivation status (default: false)
63
+ # @return [Epin] A new frozen Epin instance
64
+ #
65
+ # @example
66
+ # pin = Sashite::Pin.parse("K^")
67
+ # Sashite::Epin.new(pin)
68
+ # # => #<Sashite::Epin K^>
69
+ #
70
+ # Sashite::Epin.new(pin, derived: true)
71
+ # # => #<Sashite::Epin K^'>
72
+ def initialize(pin, derived: false)
73
+ raise ArgumentError, "Expected a Sashite::Pin instance, got: #{pin.inspect}" unless pin.is_a?(Pin)
74
+
75
+ @pin = pin
76
+ @derived = !!derived
77
+
78
+ freeze
164
79
  end
165
80
 
166
- # Parse an EPIN string into an Identifier object
167
- #
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
174
- # @raise [ArgumentError] if the EPIN string is invalid
175
- #
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)
81
+ # Parses an EPIN string into an Epin instance.
82
+ #
83
+ # @param epin_string [String] The EPIN string to parse
84
+ # @return [Epin] A new Epin instance
85
+ # @raise [ArgumentError] If the string is not a valid EPIN
86
+ #
87
+ # @example
88
+ # Sashite::Epin.parse("K")
89
+ # # => #<Sashite::Epin K>
90
+ #
91
+ # Sashite::Epin.parse("K'")
92
+ # # => #<Sashite::Epin K'>
93
+ #
94
+ # Sashite::Epin.parse("+R^'")
95
+ # # => #<Sashite::Epin +R^'>
96
+ #
97
+ # Sashite::Epin.parse("invalid")
98
+ # # => ArgumentError: Invalid EPIN string: invalid
190
99
  def self.parse(epin_string)
191
- Identifier.parse(epin_string)
100
+ raise ArgumentError, "Invalid EPIN string: #{epin_string.inspect}" unless epin_string.is_a?(String)
101
+
102
+ match = EPIN_PATTERN.match(epin_string)
103
+ raise ArgumentError, "Invalid EPIN string: #{epin_string}" unless match
104
+
105
+ pin_string = match[:pin]
106
+ derived_marker = match[:derived]
107
+
108
+ pin = Pin.parse(pin_string)
109
+ derived = derived_marker == "'"
110
+
111
+ new(pin, derived: derived)
192
112
  end
193
113
 
194
- # Create a new identifier from a PIN component and derivation flag
114
+ # Checks if a string is a valid EPIN notation.
195
115
  #
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).
116
+ # @param epin_string [String] The string to validate
117
+ # @return [Boolean] true if valid, false otherwise
199
118
  #
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
119
+ # @example
120
+ # Sashite::Epin.valid?("K") # => true
121
+ # Sashite::Epin.valid?("K'") # => true
122
+ # Sashite::Epin.valid?("+R^'") # => true
123
+ # Sashite::Epin.valid?("K'^") # => false
124
+ # Sashite::Epin.valid?("K''") # => false
125
+ # Sashite::Epin.valid?("invalid") # => false
126
+ def self.valid?(epin_string)
127
+ return false unless epin_string.is_a?(String)
128
+
129
+ EPIN_PATTERN.match?(epin_string)
130
+ end
131
+
132
+ # ========================================================================
133
+ # Conversion
134
+ # ========================================================================
135
+
136
+ # Converts the Epin to its string representation.
137
+ #
138
+ # @return [String] The EPIN string
204
139
  #
205
- # @example Create identifiers from PIN components
140
+ # @example
206
141
  # pin = Sashite::Pin.parse("K^")
207
- # native = Sashite::Epin.new(pin, derived: false)
208
- # native.to_s # => "K^"
142
+ # Sashite::Epin.new(pin).to_s
143
+ # # => "K^"
144
+ #
145
+ # Sashite::Epin.new(pin, derived: true).to_s
146
+ # # => "K^'"
147
+ def to_s
148
+ "#{pin}#{derivation_suffix}"
149
+ end
150
+
151
+ # ========================================================================
152
+ # Transformations
153
+ # ========================================================================
154
+
155
+ # Returns a new Epin with a different PIN component.
156
+ #
157
+ # @param new_pin [Sashite::Pin] The new PIN component
158
+ # @return [Epin] A new Epin with the specified PIN
159
+ #
160
+ # @example
161
+ # epin = Sashite::Epin.parse("K^'")
162
+ # new_pin = epin.pin.with_type(:Q)
163
+ # epin.with_pin(new_pin).to_s
164
+ # # => "Q^'"
165
+ def with_pin(new_pin)
166
+ return self if pin == new_pin
167
+
168
+ self.class.new(new_pin, derived: derived)
169
+ end
170
+
171
+ # Returns a new Epin with a different derivation status.
172
+ #
173
+ # @param new_derived [Boolean] The new derivation status
174
+ # @return [Epin] A new Epin with the specified derivation status
175
+ #
176
+ # @example
177
+ # epin = Sashite::Epin.parse("K^")
178
+ # epin.with_derived(true).to_s
179
+ # # => "K^'"
180
+ #
181
+ # epin = Sashite::Epin.parse("K^'")
182
+ # epin.with_derived(false).to_s
183
+ # # => "K^"
184
+ def with_derived(new_derived)
185
+ return self if derived == !!new_derived
186
+
187
+ self.class.new(pin, derived: !!new_derived)
188
+ end
189
+
190
+ # Returns a new Epin marked as derived.
191
+ #
192
+ # @return [Epin] A new Epin with derived: true
193
+ #
194
+ # @example
195
+ # epin = Sashite::Epin.parse("K^")
196
+ # epin.mark_derived.derived
197
+ # # => true
198
+ def mark_derived
199
+ return self if derived
200
+
201
+ self.class.new(pin, derived: true)
202
+ end
203
+
204
+ # Returns a new Epin marked as native (not derived).
205
+ #
206
+ # @return [Epin] A new Epin with derived: false
207
+ #
208
+ # @example
209
+ # epin = Sashite::Epin.parse("K^'")
210
+ # epin.unmark_derived.derived
211
+ # # => false
212
+ def unmark_derived
213
+ return self unless derived
214
+
215
+ self.class.new(pin, derived: false)
216
+ end
217
+
218
+ # ========================================================================
219
+ # Queries
220
+ # ========================================================================
221
+
222
+ # Checks if the Epin is derived (uses opponent's style).
223
+ #
224
+ # @return [Boolean] true if derived
225
+ #
226
+ # @example
227
+ # Sashite::Epin.parse("K^'").derived? # => true
228
+ # Sashite::Epin.parse("K^").derived? # => false
229
+ def derived?
230
+ derived
231
+ end
232
+
233
+ # Checks if the Epin is native (uses own side's style).
234
+ #
235
+ # @return [Boolean] true if native
236
+ #
237
+ # @example
238
+ # Sashite::Epin.parse("K^").native? # => true
239
+ # Sashite::Epin.parse("K^'").native? # => false
240
+ def native?
241
+ !derived
242
+ end
243
+
244
+ # Checks if two Epins have the same derivation status.
245
+ #
246
+ # @param other [Epin] The other Epin to compare
247
+ # @return [Boolean] true if same derivation status
248
+ #
249
+ # @example
250
+ # epin1 = Sashite::Epin.parse("K^'")
251
+ # epin2 = Sashite::Epin.parse("Q'")
252
+ # epin1.same_derived?(epin2)
253
+ # # => true
254
+ #
255
+ # epin3 = Sashite::Epin.parse("K^")
256
+ # epin1.same_derived?(epin3)
257
+ # # => false
258
+ def same_derived?(other)
259
+ derived == other.derived
260
+ end
261
+
262
+ # ========================================================================
263
+ # Comparison
264
+ # ========================================================================
265
+
266
+ # Checks equality with another Epin.
209
267
  #
210
- # derived = Sashite::Epin.new(pin, derived: true)
211
- # derived.to_s # => "K^'"
268
+ # @param other [Object] The object to compare
269
+ # @return [Boolean] true if equal
270
+ def ==(other)
271
+ return false unless other.is_a?(self.class)
272
+
273
+ pin == other.pin && derived == other.derived
274
+ end
275
+
276
+ alias eql? ==
277
+
278
+ # Returns a hash code for the Epin.
212
279
  #
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)
280
+ # @return [Integer] Hash code
281
+ def hash
282
+ [pin, derived].hash
283
+ end
284
+
285
+ # Returns an inspect string for the Epin.
286
+ #
287
+ # @return [String] Inspect representation
288
+ def inspect
289
+ "#<#{self.class} #{self}>"
290
+ end
291
+
292
+ private
293
+
294
+ # Returns the derivation suffix for string representation.
217
295
  #
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)
296
+ # @return [String] "'" if derived, "" otherwise
297
+ def derivation_suffix
298
+ derived ? "'" : ""
222
299
  end
223
300
  end
224
301
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: sashite-epin
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.0.0
4
+ version: 2.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Cyril Kato
@@ -15,22 +15,18 @@ dependencies:
15
15
  requirements:
16
16
  - - "~>"
17
17
  - !ruby/object:Gem::Version
18
- version: 3.2.0
18
+ version: 3.3.0
19
19
  type: :runtime
20
20
  prerelease: false
21
21
  version_requirements: !ruby/object:Gem::Requirement
22
22
  requirements:
23
23
  - - "~>"
24
24
  - !ruby/object:Gem::Version
25
- version: 3.2.0
25
+ version: 3.3.0
26
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.
27
+ EPIN (Extended Piece Identifier Notation) implementation for Ruby.
28
+ Extends PIN by adding a derivation marker to track piece style in cross-style
29
+ abstract strategy board games with a minimal compositional API.
34
30
  email: contact@cyril.email
35
31
  executables: []
36
32
  extensions: []
@@ -40,7 +36,6 @@ files:
40
36
  - README.md
41
37
  - lib/sashite-epin.rb
42
38
  - lib/sashite/epin.rb
43
- - lib/sashite/epin/identifier.rb
44
39
  homepage: https://github.com/sashite/epin.rb
45
40
  licenses:
46
41
  - MIT