sashite-qpi 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/README.md CHANGED
@@ -1,37 +1,21 @@
1
- # Qpi.rb
1
+ # qpi.rb
2
2
 
3
3
  [![Version](https://img.shields.io/github/v/tag/sashite/qpi.rb?label=Version&logo=github)](https://github.com/sashite/qpi.rb/tags)
4
4
  [![Yard documentation](https://img.shields.io/badge/Yard-documentation-blue.svg?logo=github)](https://rubydoc.info/github/sashite/qpi.rb/main)
5
- ![Ruby](https://github.com/sashite/qpi.rb/actions/workflows/main.yml/badge.svg?branch=main)
6
- [![License](https://img.shields.io/github/license/sashite/qpi.rb?label=License&logo=github)](https://github.com/sashite/qpi.rb/raw/main/LICENSE.md)
5
+ [![CI](https://github.com/sashite/qpi.rb/actions/workflows/ruby.yml/badge.svg?branch=main)](https://github.com/sashite/qpi.rb/actions)
6
+ [![License](https://img.shields.io/github/license/sashite/qpi.rb?label=License&logo=github)](https://github.com/sashite/qpi.rb/raw/main/LICENSE)
7
7
 
8
- > **QPI** (Qualified Piece Identifier) implementation for the Ruby language.
8
+ > **QPI** (Qualified Piece Identifier) implementation for Ruby.
9
9
 
10
- ## What is QPI?
10
+ ## Overview
11
11
 
12
- QPI (Qualified Piece Identifier) provides complete piece identification by combining two primitive notations:
12
+ This library implements the [QPI Specification v1.0.0](https://sashite.dev/specs/qpi/1.0.0/).
13
+
14
+ QPI provides complete piece identification by combining two primitive notations:
13
15
  - [SIN](https://sashite.dev/specs/sin/1.0.0/) (Style Identifier Notation) — identifies the piece style
14
16
  - [PIN](https://sashite.dev/specs/pin/1.0.0/) (Piece Identifier Notation) — identifies the piece attributes
15
17
 
16
- A QPI identifier is simply a **pair of (SIN, PIN)** with one constraint: both components must represent the same player.
17
-
18
- This gem implements the [QPI Specification v1.0.0](https://sashite.dev/specs/qpi/1.0.0/) with a minimal compositional API.
19
-
20
- ## Core Concept
21
-
22
- ```ruby
23
- # QPI is just composition
24
- qpi = Sashite::Qpi.new(sin_component, pin_component)
25
-
26
- # Serializes as "sin:pin"
27
- qpi.to_s # => "C:K^"
28
-
29
- # Access components directly
30
- qpi.sin # => SIN::Identifier instance
31
- qpi.pin # => PIN::Identifier instance
32
- ```
33
-
34
- **That's it.** All piece attributes come from the components.
18
+ A QPI identifier is a **pair of (SIN, PIN)** that encodes complete **Piece Identity**.
35
19
 
36
20
  ## Installation
37
21
 
@@ -53,43 +37,61 @@ gem "sashite-sin" # Style Identifier Notation
53
37
  gem "sashite-pin" # Piece Identifier Notation
54
38
  ```
55
39
 
56
- ## Quick Start
40
+ ## Usage
41
+
42
+ ### Parsing (String → Identifier)
43
+
44
+ Convert a QPI string into an `Identifier` object.
57
45
 
58
46
  ```ruby
59
47
  require "sashite/qpi"
60
48
 
61
- # Parse a QPI string
49
+ # Standard parsing (raises on error)
62
50
  qpi = Sashite::Qpi.parse("C:K^")
63
- qpi.to_s # => "C:K^"
51
+ qpi.to_s # => "C:K^"
64
52
 
65
- # Access the five fundamental attributes through components
66
- qpi.sin.family # => :C (Piece Style)
67
- qpi.pin.type # => :K (Piece Name)
68
- qpi.sin.side # => :first (Piece Side)
69
- qpi.pin.state # => :normal (Piece State)
70
- qpi.pin.terminal? # => true (Terminal Status)
53
+ # Access the five Piece Identity attributes through components
54
+ qpi.sin.style # => :C (Piece Style)
55
+ qpi.pin.type # => :K (Piece Name)
56
+ qpi.pin.side # => :first (Piece Side)
57
+ qpi.pin.state # => :normal (Piece State)
58
+ qpi.pin.terminal? # => true (Terminal Status)
71
59
 
72
60
  # Components are full SIN and PIN instances
73
- qpi.sin.first_player? # => true
74
- qpi.pin.enhanced? # => false
61
+ qpi.sin.first_player? # => true
62
+ qpi.pin.enhanced? # => false
63
+
64
+ # Invalid input raises ArgumentError
65
+ Sashite::Qpi.parse("invalid") # => raises ArgumentError
75
66
  ```
76
67
 
77
- ## Basic Usage
68
+ ### Formatting (Identifier → String)
78
69
 
79
- ### Creating Identifiers
70
+ Convert an `Identifier` back to a QPI string.
80
71
 
81
72
  ```ruby
82
- # Parse from string
83
- qpi = Sashite::Qpi.parse("C:K^")
84
-
85
- # Create from components
73
+ # From components
86
74
  sin = Sashite::Sin.parse("C")
87
75
  pin = Sashite::Pin.parse("K^")
88
- qpi = Sashite::Qpi.new(sin, pin)
76
+ qpi = Sashite::Qpi::Identifier.new(sin, pin)
77
+ qpi.to_s # => "C:K^"
89
78
 
90
- # Validate
91
- Sashite::Qpi.valid?("C:K^") # => true
92
- Sashite::Qpi.valid?("C:k") # => false (side mismatch)
79
+ # With attributes
80
+ sin = Sashite::Sin.parse("s")
81
+ pin = Sashite::Pin.parse("+r")
82
+ qpi = Sashite::Qpi::Identifier.new(sin, pin)
83
+ qpi.to_s # => "s:+r"
84
+ ```
85
+
86
+ ### Validation
87
+
88
+ ```ruby
89
+ # Boolean check
90
+ Sashite::Qpi.valid?("C:K^") # => true
91
+ Sashite::Qpi.valid?("s:+r") # => true
92
+ Sashite::Qpi.valid?("invalid") # => false
93
+ Sashite::Qpi.valid?("C:") # => false
94
+ Sashite::Qpi.valid?(":K") # => false
93
95
  ```
94
96
 
95
97
  ### Accessing Components
@@ -98,16 +100,16 @@ Sashite::Qpi.valid?("C:k") # => false (side mismatch)
98
100
  qpi = Sashite::Qpi.parse("S:+R^")
99
101
 
100
102
  # Get components
101
- qpi.sin # => #<Sin::Identifier family=:S side=:first>
102
- qpi.pin # => #<Pin::Identifier type=:R state=:enhanced terminal=true>
103
+ qpi.sin # => #<Sashite::Sin::Identifier style=:S side=:first>
104
+ qpi.pin # => #<Sashite::Pin::Identifier type=:R state=:enhanced terminal=true>
103
105
 
104
106
  # Serialize components
105
- qpi.sin.to_s # => "S"
106
- qpi.pin.to_s # => "+R^"
107
- qpi.to_s # => "S:+R^"
107
+ qpi.sin.to_s # => "S"
108
+ qpi.pin.to_s # => "+R^"
109
+ qpi.to_s # => "S:+R^"
108
110
  ```
109
111
 
110
- ### Five Fundamental Attributes
112
+ ### Five Piece Identity Attributes
111
113
 
112
114
  All attributes come directly from the components:
113
115
 
@@ -115,523 +117,238 @@ All attributes come directly from the components:
115
117
  qpi = Sashite::Qpi.parse("S:+R^")
116
118
 
117
119
  # From SIN component
118
- qpi.sin.family # => :S (Piece Style)
119
- qpi.sin.side # => :first (Piece Side)
120
+ qpi.sin.style # => :S (Piece Style)
120
121
 
121
122
  # From PIN component
122
- qpi.pin.type # => :R (Piece Name)
123
- qpi.pin.state # => :enhanced (Piece State)
124
- qpi.pin.terminal? # => true (Terminal Status)
123
+ qpi.pin.type # => :R (Piece Name)
124
+ qpi.pin.side # => :first (Piece Side)
125
+ qpi.pin.state # => :enhanced (Piece State)
126
+ qpi.pin.terminal? # => true (Terminal Status)
125
127
  ```
126
128
 
127
- ## Transformations
129
+ ### Native and Derived Relationship
130
+
131
+ QPI defines a deterministic relationship based on case comparison between SIN and PIN letters.
128
132
 
129
- All transformations return new immutable QPI instances:
133
+ ```ruby
134
+ qpi = Sashite::Qpi.parse("C:K^")
135
+
136
+ # Access the relationship
137
+ qpi.sin.side # => :first (derived from SIN letter case)
138
+ qpi.native? # => true (sin.side == pin.side)
139
+ qpi.derived? # => false
140
+
141
+ # Native: SIN case matches PIN case
142
+ Sashite::Qpi.parse("C:K").native? # => true (both uppercase/first)
143
+ Sashite::Qpi.parse("c:k").native? # => true (both lowercase/second)
144
+
145
+ # Derived: SIN case differs from PIN case
146
+ Sashite::Qpi.parse("C:k").derived? # => true (uppercase vs lowercase)
147
+ Sashite::Qpi.parse("c:K").derived? # => true (lowercase vs uppercase)
148
+ ```
149
+
150
+ ### Transformations
130
151
 
131
- ### Replace Components
152
+ All transformations return new immutable instances.
132
153
 
133
154
  ```ruby
134
155
  qpi = Sashite::Qpi.parse("C:K^")
135
156
 
136
157
  # Replace SIN component
137
158
  new_sin = Sashite::Sin.parse("S")
138
- qpi.with_sin(new_sin) # => "S:K^"
159
+ qpi.with_sin(new_sin).to_s # => "S:K^"
139
160
 
140
161
  # Replace PIN component
141
- new_pin = Sashite::Pin.parse("Q^")
142
- qpi.with_pin(new_pin) # => "C:Q^"
162
+ new_pin = Sashite::Pin.parse("+Q^")
163
+ qpi.with_pin(new_pin).to_s # => "C:+Q^"
143
164
 
144
165
  # Transform both
145
- qpi.with_sin(new_sin).with_pin(new_pin) # => "S:Q^"
146
- ```
166
+ qpi.with_sin(new_sin).with_pin(new_pin).to_s # => "S:+Q^"
147
167
 
148
- ### Flip (Only Convenience Method)
168
+ # Flip both components (change player)
169
+ qpi.flip.to_s # => "c:k^"
149
170
 
150
- ```ruby
151
- qpi = Sashite::Qpi.parse("C:K^")
171
+ # Native/Derived transformations
172
+ qpi = Sashite::Qpi.parse("C:r")
173
+ qpi.native.to_s # => "C:R" (PIN case aligned with SIN case)
174
+ qpi.derive.to_s # => "C:r" (already derived, unchanged)
152
175
 
153
- # Flip both components (change player)
154
- qpi.flip # => "c:k^"
176
+ qpi = Sashite::Qpi.parse("C:R")
177
+ qpi.native.to_s # => "C:R" (already native, unchanged)
178
+ qpi.derive.to_s # => "C:r" (PIN case differs from SIN case)
155
179
  ```
156
180
 
157
- **Why only `flip`?** It's the only transformation that affects **both** SIN and PIN components simultaneously. All other transformations work through component replacement.
158
-
159
181
  ### Transform via Components
160
182
 
161
183
  ```ruby
162
184
  qpi = Sashite::Qpi.parse("C:K^")
163
185
 
164
186
  # Transform SIN via component
165
- qpi.with_sin(qpi.sin.with_family(:S)) # => "S:K^"
187
+ qpi.with_sin(qpi.sin.with_style(:S)).to_s # => "S:K^"
166
188
 
167
189
  # Transform PIN via component
168
- qpi.with_pin(qpi.pin.with_type(:Q)) # => "C:Q^"
169
- qpi.with_pin(qpi.pin.with_state(:enhanced)) # => "C:+K^"
170
- qpi.with_pin(qpi.pin.with_terminal(false)) # => "C:K"
190
+ qpi.with_pin(qpi.pin.with_type(:Q)).to_s # => "C:Q^"
191
+ qpi.with_pin(qpi.pin.with_state(:enhanced)).to_s # => "C:+K^"
192
+ qpi.with_pin(qpi.pin.with_terminal(false)).to_s # => "C:K"
171
193
 
172
194
  # Chain transformations
173
- qpi
174
- .flip
175
- .with_sin(qpi.sin.with_family(:S))
176
- .with_pin(qpi.pin.with_type(:Q)) # => "s:q^"
195
+ qpi.flip.with_sin(qpi.sin.with_style(:S)).to_s # => "s:k^"
177
196
  ```
178
197
 
179
- ## Component Queries
198
+ ### Component Queries
180
199
 
181
- Since QPI is just a composition, use the component APIs directly:
200
+ Since QPI is a composition, use the component APIs directly:
182
201
 
183
202
  ```ruby
184
203
  qpi = Sashite::Qpi.parse("S:+P^")
185
204
 
186
205
  # SIN queries (style and side)
187
- qpi.sin.family # => :S
188
- qpi.sin.side # => :first
189
- qpi.sin.first_player? # => true
190
- qpi.sin.letter # => "S"
206
+ qpi.sin.style # => :S
207
+ qpi.sin.side # => :first
208
+ qpi.sin.first_player? # => true
209
+ qpi.sin.letter # => "S"
191
210
 
192
211
  # PIN queries (type, state, terminal)
193
- qpi.pin.type # => :P
194
- qpi.pin.state # => :enhanced
195
- qpi.pin.terminal? # => true
196
- qpi.pin.enhanced? # => true
197
- qpi.pin.letter # => "P"
198
- qpi.pin.prefix # => "+"
199
- qpi.pin.suffix # => "^"
200
-
201
- # Compare QPIs
212
+ qpi.pin.type # => :P
213
+ qpi.pin.state # => :enhanced
214
+ qpi.pin.terminal? # => true
215
+ qpi.pin.enhanced? # => true
216
+ qpi.pin.letter # => "P"
217
+ qpi.pin.prefix # => "+"
218
+ qpi.pin.suffix # => "^"
219
+
220
+ # Compare QPIs via components
202
221
  other = Sashite::Qpi.parse("C:+P^")
203
- qpi.sin.same_family?(other.sin) # => false (S vs C)
204
- qpi.pin.same_type?(other.pin) # => true (both P)
205
- qpi.sin.same_side?(other.sin) # => true (both first)
206
- qpi.pin.same_state?(other.pin) # => true (both enhanced)
222
+ qpi.sin.same_style?(other.sin) # => false (S vs C)
223
+ qpi.pin.same_type?(other.pin) # => true (both P)
224
+ qpi.sin.same_side?(other.sin) # => true (both first)
225
+ qpi.pin.same_state?(other.pin) # => true (both enhanced)
207
226
  ```
208
227
 
209
228
  ## API Reference
210
229
 
211
- ### Main Module
212
-
213
- ```ruby
214
- # Parse QPI string
215
- Sashite::Qpi.parse(qpi_string) # => Qpi::Identifier
216
-
217
- # Create from components
218
- Sashite::Qpi.new(sin, pin) # => Qpi::Identifier
219
-
220
- # Validate string
221
- Sashite::Qpi.valid?(qpi_string) # => Boolean
222
- ```
223
-
224
- ### Identifier Class
225
-
226
- #### Core Methods (5 total)
227
-
228
- ```ruby
229
- # Creation
230
- Sashite::Qpi.new(sin, pin) # Create from components
231
-
232
- # Component access
233
- qpi.sin # => SIN::Identifier
234
- qpi.pin # => PIN::Identifier
235
-
236
- # Serialization
237
- qpi.to_s # => "C:K^"
238
-
239
- # Component replacement
240
- qpi.with_sin(new_sin) # New QPI with different SIN
241
- qpi.with_pin(new_pin) # New QPI with different PIN
242
-
243
- # Convenience (transforms both components)
244
- qpi.flip # Flip both SIN and PIN sides
245
- ```
246
-
247
- #### Equality
248
-
249
- ```ruby
250
- qpi1 == qpi2 # True if both SIN and PIN equal
251
- ```
252
-
253
- **That's the entire API.** Everything else uses the component APIs directly.
254
-
255
- ## Format Specification
256
-
257
- ### Structure
258
- ```
259
- <sin>:<pin>
260
- ```
261
-
262
- ### Grammar (BNF)
263
- ```bnf
264
- <qpi> ::= <uppercase-qpi> | <lowercase-qpi>
265
-
266
- <uppercase-qpi> ::= <uppercase-letter> ":" <uppercase-pin>
267
- <lowercase-qpi> ::= <lowercase-letter> ":" <lowercase-pin>
268
-
269
- <uppercase-pin> ::= ["+" | "-"] <uppercase-letter> ["^"]
270
- <lowercase-pin> ::= ["+" | "-"] <lowercase-letter> ["^"]
271
- ```
272
-
273
- ### Semantic Constraint
274
-
275
- **Critical**: The SIN and PIN components must represent the **same player**:
276
-
277
- ```ruby
278
- # Valid - both first player
279
- Sashite::Qpi.valid?("C:K") # => true
280
- Sashite::Qpi.valid?("C:+K^") # => true
281
-
282
- # Valid - both second player
283
- Sashite::Qpi.valid?("c:k") # => true
284
- Sashite::Qpi.valid?("c:-p^") # => true
285
-
286
- # Invalid - side mismatch
287
- Sashite::Qpi.valid?("C:k") # => false (first vs second)
288
- Sashite::Qpi.valid?("c:K") # => false (second vs first)
289
- ```
290
-
291
- ### Regular Expression
292
- ```ruby
293
- /\A([A-Z]:[-+]?[A-Z]\^?|[a-z]:[-+]?[a-z]\^?)\z/
294
- ```
295
-
296
- ## Examples
297
-
298
- ### Basic Identifiers
299
-
300
- ```ruby
301
- # Chess pieces
302
- chess_king = Sashite::Qpi.parse("C:K^")
303
- chess_king.sin.family # => :C (Chess style)
304
- chess_king.pin.type # => :K (King)
305
- chess_king.pin.terminal? # => true
306
-
307
- # Shogi pieces
308
- shogi_rook = Sashite::Qpi.parse("S:+R")
309
- shogi_rook.sin.family # => :S (Shogi style)
310
- shogi_rook.pin.type # => :R (Rook)
311
- shogi_rook.pin.enhanced? # => true (promoted)
312
-
313
- # Xiangqi pieces
314
- xiangqi_general = Sashite::Qpi.parse("X:G^")
315
- xiangqi_general.sin.family # => :X (Xiangqi style)
316
- xiangqi_general.pin.type # => :G (General)
317
- xiangqi_general.pin.terminal? # => true
318
- ```
319
-
320
- ### Cross-Style Scenarios
230
+ ### Types
321
231
 
322
232
  ```ruby
323
- # Chess vs Shogi match
324
- chess_player = Sashite::Qpi.parse("C:K^") # First player uses Chess
325
- shogi_player = Sashite::Qpi.parse("s:k^") # Second player uses Shogi
326
-
327
- # Different styles
328
- chess_player.sin.same_family?(shogi_player.sin) # => false
329
-
330
- # Same piece type
331
- chess_player.pin.same_type?(shogi_player.pin) # => true (both kings)
332
-
333
- # Different players
334
- chess_player.sin.same_side?(shogi_player.sin) # => false
335
- ```
336
-
337
- ### Component Manipulation
338
-
339
- ```ruby
340
- # Start with Chess king
341
- qpi = Sashite::Qpi.parse("C:K^")
342
-
343
- # Change to Shogi style (keep same piece)
344
- shogi_king = qpi.with_sin(qpi.sin.with_family(:S))
345
- shogi_king.to_s # => "S:K^"
346
-
347
- # Change to queen (keep same style)
348
- chess_queen = qpi.with_pin(qpi.pin.with_type(:Q))
349
- chess_queen.to_s # => "C:Q^"
350
-
351
- # Enhance piece (keep everything else)
352
- enhanced = qpi.with_pin(qpi.pin.with_state(:enhanced))
353
- enhanced.to_s # => "C:+K^"
354
-
355
- # Remove terminal marker
356
- non_terminal = qpi.with_pin(qpi.pin.with_terminal(false))
357
- non_terminal.to_s # => "C:K"
358
-
359
- # Switch player (flip both components)
360
- opponent = qpi.flip
361
- opponent.to_s # => "c:k^"
362
- ```
363
-
364
- ### Working with Components
365
-
366
- ```ruby
367
- qpi = Sashite::Qpi.parse("S:+R^")
368
-
369
- # Extract and transform SIN
370
- sin = qpi.sin # => "S"
371
- new_sin = sin.with_family(:C) # => "C"
372
- qpi.with_sin(new_sin).to_s # => "C:+R^"
373
-
374
- # Extract and transform PIN
375
- pin = qpi.pin # => "+R^"
376
- new_pin = pin.with_type(:B) # => "+B^"
377
- qpi.with_pin(new_pin).to_s # => "S:+B^"
378
-
379
- # Multiple PIN transformations
380
- new_pin = pin
381
- .with_type(:Q)
382
- .with_state(:normal)
383
- .with_terminal(false)
384
- qpi.with_pin(new_pin).to_s # => "S:Q"
385
-
386
- # Create completely new QPI
387
- new_sin = Sashite::Sin.parse("X")
388
- new_pin = Sashite::Pin.parse("G^")
389
- Sashite::Qpi.new(new_sin, new_pin).to_s # => "X:G^"
390
- ```
391
-
392
- ### Immutability
393
-
394
- ```ruby
395
- original = Sashite::Qpi.parse("C:K^")
396
-
397
- # All transformations return new instances
398
- flipped = original.flip
399
- enhanced = original.with_pin(original.pin.with_state(:enhanced))
400
- different = original.with_sin(original.sin.with_family(:S))
401
-
402
- # Original unchanged
403
- original.to_s # => "C:K^"
404
- flipped.to_s # => "c:k^"
405
- enhanced.to_s # => "C:+K^"
406
- different.to_s # => "S:K^"
407
-
408
- # Components are also immutable
409
- sin = original.sin
410
- pin = original.pin
411
- sin.frozen? # => true
412
- pin.frozen? # => true
413
- ```
414
-
415
- ## Attribute Mapping
416
-
417
- QPI exposes all five fundamental attributes from the Sashité Game Protocol through component delegation:
418
-
419
- | Protocol Attribute | QPI Access | Example |
420
- |-------------------|------------|---------|
421
- | **Piece Style** | `qpi.sin.family` | `:C` (Chess), `:S` (Shogi) |
422
- | **Piece Name** | `qpi.pin.type` | `:K` (King), `:R` (Rook) |
423
- | **Piece Side** | `qpi.sin.side` or `qpi.pin.side` | `:first`, `:second` |
424
- | **Piece State** | `qpi.pin.state` | `:normal`, `:enhanced`, `:diminished` |
425
- | **Terminal Status** | `qpi.pin.terminal?` | `true`, `false` |
426
-
427
- **Note**: `qpi.sin.side` and `qpi.pin.side` are always equal (semantic constraint).
428
-
429
- ## Design Principles
430
-
431
- ### 1. Pure Composition
432
-
433
- QPI doesn't reimplement features — it composes existing primitives:
434
-
435
- ```ruby
436
- # QPI is just a validated pair
437
- class Identifier
233
+ # Identifier represents a parsed QPI with complete Piece Identity.
234
+ class Sashite::Qpi::Identifier
235
+ # Creates an Identifier from SIN and PIN components.
236
+ # Raises ArgumentError if components are invalid.
237
+ #
238
+ # @param sin [Sashite::Sin::Identifier] Style component
239
+ # @param pin [Sashite::Pin::Identifier] Piece component
240
+ # @return [Identifier]
438
241
  def initialize(sin, pin)
439
- raise unless sin.side == pin.side # Only validation
440
242
 
441
- @sin = sin
442
- @pin = pin
443
- end
243
+ # Returns the SIN component.
244
+ #
245
+ # @return [Sashite::Sin::Identifier]
246
+ def sin
247
+
248
+ # Returns the PIN component.
249
+ #
250
+ # @return [Sashite::Pin::Identifier]
251
+ def pin
252
+
253
+ # Returns true if sin.side equals pin.side (Native relationship).
254
+ #
255
+ # @return [Boolean]
256
+ def native?
257
+
258
+ # Returns true if sin.side differs from pin.side (Derived relationship).
259
+ #
260
+ # @return [Boolean]
261
+ def derived?
262
+
263
+ # Returns the QPI string representation.
264
+ #
265
+ # @return [String]
266
+ def to_s
444
267
  end
445
268
  ```
446
269
 
447
- ### 2. Absolute Minimal API
448
-
449
- **5 core methods only:**
450
- 1. `new(sin, pin)` — create from components
451
- 2. `sin` — get SIN component
452
- 3. `pin` — get PIN component
453
- 4. `to_s` — serialize
454
- 5. `flip` — flip both components (only convenience method)
455
-
456
- Everything else uses component APIs directly.
457
-
458
- ### 3. Component Transparency
459
-
460
- Access components directly — no wrappers:
270
+ ### Parsing
461
271
 
462
272
  ```ruby
463
- # Use component APIs directly
464
- qpi.sin.family
465
- qpi.sin.with_family(:S)
466
- qpi.pin.type
467
- qpi.pin.with_type(:Q)
468
- qpi.pin.with_terminal(true)
469
-
470
- # No need for wrapper methods like:
471
- # qpi.family
472
- # qpi.with_family
473
- # qpi.type
474
- # qpi.with_type
475
- # qpi.with_terminal
273
+ # Parses a QPI string into an Identifier.
274
+ # Raises ArgumentError if the string is not valid.
275
+ #
276
+ # @param string [String] QPI string
277
+ # @return [Identifier]
278
+ # @raise [ArgumentError] if invalid
279
+ def Sashite::Qpi.parse(string)
476
280
  ```
477
281
 
478
- ### 4. Single Convenience Method
479
-
480
- Only `flip` is provided as a convenience because it's the **only** transformation that naturally operates on both components:
481
-
482
- ```ruby
483
- # Makes sense as convenience
484
- qpi.flip # Flips both SIN and PIN
485
-
486
- # Would be arbitrary conveniences
487
- # qpi.with_family(:S) # Just use qpi.with_sin(qpi.sin.with_family(:S))
488
- # qpi.with_type(:Q) # Just use qpi.with_pin(qpi.pin.with_type(:Q))
489
- ```
490
-
491
- ### 5. Immutability
492
-
493
- All instances frozen. Transformations return new instances:
282
+ ### Validation
494
283
 
495
284
  ```ruby
496
- qpi1 = Sashite::Qpi.parse("C:K^")
497
- qpi2 = qpi1.flip
498
- qpi1.frozen? # => true
499
- qpi2.frozen? # => true
500
- qpi1.equal?(qpi2) # => false
285
+ # Reports whether string is a valid QPI.
286
+ #
287
+ # @param string [String] QPI string
288
+ # @return [Boolean]
289
+ def Sashite::Qpi.valid?(string)
501
290
  ```
502
291
 
503
- ## Error Handling
292
+ ### Transformations
504
293
 
505
294
  ```ruby
506
- # Invalid QPI string
507
- begin
508
- Sashite::Qpi.parse("invalid")
509
- rescue ArgumentError => e
510
- e.message # => "Invalid QPI string: invalid"
511
- end
295
+ # Component replacement (return new Identifier)
296
+ def with_sin(new_sin) # => Identifier with different SIN
297
+ def with_pin(new_pin) # => Identifier with different PIN
512
298
 
513
- # Side mismatch between components
514
- sin = Sashite::Sin.parse("C") # first player
515
- pin = Sashite::Pin.parse("k") # second player
516
- begin
517
- Sashite::Qpi.new(sin, pin)
518
- rescue ArgumentError => e
519
- e.message # => Semantic consistency error
520
- end
299
+ # Flip transformation (transforms both components)
300
+ def flip # => Identifier with both SIN and PIN flipped
521
301
 
522
- # Component validation errors delegate
523
- begin
524
- Sashite::Qpi.parse("CC:K")
525
- rescue ArgumentError => e
526
- # SIN validation error
527
- end
302
+ # Native/Derived transformations
303
+ def native # => Identifier with PIN case aligned to SIN case
304
+ def derive # => Identifier with PIN case opposite to SIN case
528
305
  ```
529
306
 
530
- ## Performance Considerations
307
+ ### Errors
531
308
 
532
- ### Efficient Composition
309
+ All parsing and validation errors raise `ArgumentError` with descriptive messages:
533
310
 
534
- ```ruby
535
- # Components are created once
536
- sin = Sashite::Sin.parse("C")
537
- pin = Sashite::Pin.parse("K^")
538
- qpi = Sashite::Qpi.new(sin, pin)
311
+ | Message | Cause |
312
+ |---------|-------|
313
+ | `"empty input"` | String length is 0 |
314
+ | `"missing colon separator"` | No `:` found in string |
315
+ | `"missing SIN component"` | Nothing before `:` |
316
+ | `"missing PIN component"` | Nothing after `:` |
317
+ | `"invalid SIN component: ..."` | SIN parsing failed |
318
+ | `"invalid PIN component: ..."` | PIN parsing failed |
539
319
 
540
- # Accessing components is O(1)
541
- qpi.sin # => direct reference
542
- qpi.pin # => direct reference
543
-
544
- # No overhead from method delegation
545
- qpi.sin.family # => direct method call on component
546
- ```
547
-
548
- ### Transformation Patterns
549
-
550
- ```ruby
551
- qpi = Sashite::Qpi.parse("C:K^")
320
+ ## Piece Identity Mapping
552
321
 
553
- # Pattern 1: Single component transformation
554
- qpi.with_pin(qpi.pin.with_type(:Q))
322
+ QPI encodes complete **Piece Identity** as defined in the [Glossary](https://sashite.dev/glossary/):
555
323
 
556
- # Pattern 2: Multiple transformations on same component
557
- new_pin = qpi.pin
558
- .with_type(:Q)
559
- .with_state(:enhanced)
560
- .with_terminal(false)
561
- qpi.with_pin(new_pin)
324
+ | Piece Attribute | QPI Access | Encoding |
325
+ |---------------------|----------------------|--------------------------------------------------------|
326
+ | **Piece Style** | `qpi.sin.style` | SIN letter (case-insensitive identity) |
327
+ | **Piece Name** | `qpi.pin.type` | PIN letter (case-insensitive identity) |
328
+ | **Piece Side** | `qpi.pin.side` | PIN letter case (uppercase = first, lowercase = second)|
329
+ | **Piece State** | `qpi.pin.state` | PIN modifier (`+` = enhanced, `-` = diminished) |
330
+ | **Terminal Status** | `qpi.pin.terminal?` | PIN marker (`^` = terminal) |
562
331
 
563
- # Pattern 3: Transform both components
564
- new_sin = qpi.sin.with_family(:S)
565
- new_pin = qpi.pin.with_type(:R)
566
- Sashite::Qpi.new(new_sin, new_pin)
332
+ Additionally, QPI provides a **Native/Derived relationship** via `native?`, `derived?`, `native`, and `derive`.
567
333
 
568
- # Pattern 4: Flip (convenience)
569
- qpi.flip # Most efficient for switching sides
570
- ```
571
-
572
- ## Comparison with Other Approaches
573
-
574
- ### Why Not More Convenience Methods?
575
-
576
- ```ruby
577
- # ✗ Arbitrary conveniences
578
- qpi.with_family(:S) # Why this...
579
- qpi.with_type(:Q) # ...but not this?
580
- qpi.with_state(:enhanced) # Where do we stop?
581
- qpi.with_terminal(true) # All PIN methods?
582
-
583
- # ✓ Consistent principle: use components
584
- qpi.with_sin(qpi.sin.with_family(:S))
585
- qpi.with_pin(qpi.pin.with_type(:Q))
586
- qpi.with_pin(qpi.pin.with_state(:enhanced))
587
- qpi.with_pin(qpi.pin.with_terminal(true))
588
-
589
- # ✓ Only exception: flip (transforms both)
590
- qpi.flip
591
- ```
592
-
593
- ### Why Composition Over Inheritance?
594
-
595
- ```ruby
596
- # ✗ Bad: QPI inheriting from PIN
597
- class Qpi < Pin
598
- # Problem: QPI is not a specialized PIN
599
- end
600
-
601
- # ✓ Good: QPI composes SIN and PIN
602
- class Qpi
603
- def initialize(sin, pin)
604
- @sin = sin
605
- @pin = pin
606
- end
607
- end
608
- ```
609
-
610
- ## Design Properties
334
+ ## Design Principles
611
335
 
612
- - **Rule-agnostic**: Independent of game mechanics
613
- - **Complete identification**: All five protocol attributes
614
- - **Cross-style support**: Multi-tradition games
615
- - **Absolute minimal API**: Only 5 core methods
616
- - **Pure composition**: Zero feature duplication
617
- - **Component transparency**: Direct primitive access
618
- - **Immutable**: Frozen instances
619
- - **Semantic validation**: Automatic side consistency
620
- - **Type-safe**: Full component type preservation
621
- - **Single convenience**: Only `flip` (multi-component operation)
336
+ - **Pure composition**: QPI composes SIN and PIN without reimplementing features
337
+ - **Minimal API**: Core methods (`sin`, `pin`, `native?`, `derived?`, `native`, `derive`, `to_s`) plus transformations
338
+ - **Component transparency**: Access components directly, no wrapper methods
339
+ - **QPI-specific conveniences**: `flip`, `native`, `derive` (operations that span both components)
340
+ - **Immutable identifiers**: Frozen instances prevent mutation
341
+ - **Ruby idioms**: `valid?` predicate, `to_s` conversion, `ArgumentError` for invalid input
342
+ - **No duplication**: Delegates to `sashite-sin` and `sashite-pin`
622
343
 
623
344
  ## Related Specifications
624
345
 
625
- - [QPI Specification v1.0.0](https://sashite.dev/specs/qpi/1.0.0/) - Technical specification
626
- - [QPI Examples](https://sashite.dev/specs/qpi/1.0.0/examples/) - Usage examples
627
- - [SIN Specification v1.0.0](https://sashite.dev/specs/sin/1.0.0/) - Style component
628
- - [PIN Specification v1.0.0](https://sashite.dev/specs/pin/1.0.0/) - Piece component
629
- - [Sashité Game Protocol](https://sashite.dev/game-protocol/) - Foundation
346
+ - [Game Protocol](https://sashite.dev/game-protocol/) Conceptual foundation
347
+ - [QPI Specification](https://sashite.dev/specs/qpi/1.0.0/) Official specification
348
+ - [QPI Examples](https://sashite.dev/specs/qpi/1.0.0/examples/) Usage examples
349
+ - [SIN Specification](https://sashite.dev/specs/sin/1.0.0/) Style component
350
+ - [PIN Specification](https://sashite.dev/specs/pin/1.0.0/) Piece component
630
351
 
631
352
  ## License
632
353
 
633
- Available as open source under the [MIT License](https://opensource.org/licenses/MIT).
634
-
635
- ## About
636
-
637
- Maintained by [Sashité](https://sashite.com/) — promoting chess variants and sharing the beauty of board game cultures.
354
+ Available as open source under the [Apache License 2.0](https://opensource.org/licenses/Apache-2.0).