sashite-qpi 1.0.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.
data/README.md CHANGED
@@ -9,9 +9,29 @@
9
9
 
10
10
  ## What is QPI?
11
11
 
12
- QPI (Qualified Piece Identifier) provides a rule-agnostic format for identifying game pieces in abstract strategy board games by combining [Style Identifier Notation (SIN)](https://sashite.dev/specs/sin/1.0.0/) and [Piece Identifier Notation (PIN)](https://sashite.dev/specs/pin/1.0.0/) primitives with a colon separator.
12
+ QPI (Qualified Piece Identifier) provides complete piece identification by combining two primitive notations:
13
+ - [SIN](https://sashite.dev/specs/sin/1.0.0/) (Style Identifier Notation) — identifies the piece style
14
+ - [PIN](https://sashite.dev/specs/pin/1.0.0/) (Piece Identifier Notation) — identifies the piece attributes
13
15
 
14
- This gem implements the [QPI Specification v1.0.0](https://sashite.dev/specs/qpi/1.0.0/) exactly, providing complete piece identification with all four fundamental attributes: **Family**, **Type**, **Side**, and **State**.
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.
15
35
 
16
36
  ## Installation
17
37
 
@@ -28,171 +48,209 @@ gem install sashite-qpi
28
48
 
29
49
  ## Dependencies
30
50
 
31
- QPI builds upon two foundational primitive specifications:
32
-
33
51
  ```ruby
34
52
  gem "sashite-sin" # Style Identifier Notation
35
53
  gem "sashite-pin" # Piece Identifier Notation
36
54
  ```
37
55
 
38
- ## Usage
39
-
40
- ### Basic Operations
56
+ ## Quick Start
41
57
 
42
58
  ```ruby
43
59
  require "sashite/qpi"
44
60
 
45
- # Parse QPI strings
46
- identifier = Sashite::Qpi.parse("C:K") # Chess king, first player
47
- identifier.to_s # => "C:K"
61
+ # Parse a QPI string
62
+ qpi = Sashite::Qpi.parse("C:K^")
63
+ qpi.to_s # => "C:K^"
48
64
 
49
- # Create identifiers from parameters (strict validation)
50
- identifier = Sashite::Qpi.identifier(:C, :K, :first, :normal)
51
- identifier = Sashite::Qpi::Identifier.new(:S, :R, :first, :enhanced)
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)
52
71
 
53
- # Validate QPI strings
54
- Sashite::Qpi.valid?("C:K") # => true
55
- Sashite::Qpi.valid?("s:+p") # => true
56
- Sashite::Qpi.valid?("C:k") # => false (semantic mismatch)
72
+ # Components are full SIN and PIN instances
73
+ qpi.sin.first_player? # => true
74
+ qpi.pin.enhanced? # => false
57
75
  ```
58
76
 
59
- ### Strict Parameter Validation
77
+ ## Basic Usage
78
+
79
+ ### Creating Identifiers
80
+
81
+ ```ruby
82
+ # Parse from string
83
+ qpi = Sashite::Qpi.parse("C:K^")
84
+
85
+ # Create from components
86
+ sin = Sashite::Sin.parse("C")
87
+ pin = Sashite::Pin.parse("K^")
88
+ qpi = Sashite::Qpi.new(sin, pin)
89
+
90
+ # Validate
91
+ Sashite::Qpi.valid?("C:K^") # => true
92
+ Sashite::Qpi.valid?("C:k") # => false (side mismatch)
93
+ ```
60
94
 
61
- **Important**: QPI enforces the same strict validation as its underlying SIN and PIN primitives:
95
+ ### Accessing Components
62
96
 
63
97
  ```ruby
64
- # Valid - uppercase symbols only for family and type parameters
65
- Sashite::Qpi.identifier(:C, :K, :first, :normal) # => "C:K"
66
- Sashite::Qpi.identifier(:C, :K, :second, :normal) # => "c:k"
98
+ qpi = Sashite::Qpi.parse("S:+R^")
99
+
100
+ # Get components
101
+ qpi.sin # => #<Sin::Identifier family=:S side=:first>
102
+ qpi.pin # => #<Pin::Identifier type=:R state=:enhanced terminal=true>
67
103
 
68
- # Invalid - lowercase symbols rejected with ArgumentError
69
- Sashite::Qpi.identifier(:c, :K, :first, :normal) # => ArgumentError
70
- Sashite::Qpi.identifier(:C, :k, :first, :normal) # => ArgumentError
104
+ # Serialize components
105
+ qpi.sin.to_s # => "S"
106
+ qpi.pin.to_s # => "+R^"
107
+ qpi.to_s # => "S:+R^"
71
108
  ```
72
109
 
73
- **Key principle**: Input parameters must use uppercase symbols (`:A` to `:Z`). The `side` parameter determines the display case, not the input case.
110
+ ### Five Fundamental Attributes
74
111
 
75
- ### Attribute Access
112
+ All attributes come directly from the components:
76
113
 
77
114
  ```ruby
78
- identifier = Sashite::Qpi.parse("S:+R")
115
+ qpi = Sashite::Qpi.parse("S:+R^")
79
116
 
80
- # Four fundamental piece attributes
81
- identifier.family # => :S
82
- identifier.type # => :R
83
- identifier.side # => :first
84
- identifier.state # => :enhanced
117
+ # From SIN component
118
+ qpi.sin.family # => :S (Piece Style)
119
+ qpi.sin.side # => :first (Piece Side)
85
120
 
86
- # Component extraction
87
- identifier.to_sin # => "S"
88
- identifier.to_pin # => "+R"
89
- identifier.sin_component # => #<Sashite::Sin::Identifier>
90
- identifier.pin_component # => #<Sashite::Pin::Identifier>
121
+ # From PIN component
122
+ qpi.pin.type # => :R (Piece Name)
123
+ qpi.pin.state # => :enhanced (Piece State)
124
+ qpi.pin.terminal? # => true (Terminal Status)
91
125
  ```
92
126
 
93
- ### Transformations
127
+ ## Transformations
128
+
129
+ All transformations return new immutable QPI instances:
130
+
131
+ ### Replace Components
94
132
 
95
133
  ```ruby
96
- # All transformations return new immutable instances
97
- identifier = Sashite::Qpi.parse("C:K")
134
+ qpi = Sashite::Qpi.parse("C:K^")
98
135
 
99
- # State transformations
100
- enhanced = identifier.enhance # => "C:+K"
101
- diminished = identifier.diminish # => "C:-K"
102
- normalized = identifier.normalize # => "C:K"
136
+ # Replace SIN component
137
+ new_sin = Sashite::Sin.parse("S")
138
+ qpi.with_sin(new_sin) # => "S:K^"
103
139
 
104
- # Attribute transformations
105
- different_type = identifier.with_type(:Q) # => "C:Q"
106
- different_side = identifier.with_side(:second) # => "c:k"
107
- different_state = identifier.with_state(:enhanced) # => "C:+K"
108
- different_family = identifier.with_family(:S) # => "S:K"
140
+ # Replace PIN component
141
+ new_pin = Sashite::Pin.parse("Q^")
142
+ qpi.with_pin(new_pin) # => "C:Q^"
109
143
 
110
- # Player assignment flip
111
- flipped = identifier.flip # => "c:k"
144
+ # Transform both
145
+ qpi.with_sin(new_sin).with_pin(new_pin) # => "S:Q^"
146
+ ```
112
147
 
113
- # Chain transformations
114
- result = identifier.flip.enhance.with_type(:Q) # => "c:+q"
148
+ ### Flip (Only Convenience Method)
149
+
150
+ ```ruby
151
+ qpi = Sashite::Qpi.parse("C:K^")
152
+
153
+ # Flip both components (change player)
154
+ qpi.flip # => "c:k^"
115
155
  ```
116
156
 
117
- ### State and Comparison Queries
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
+ ### Transform via Components
118
160
 
119
161
  ```ruby
120
- identifier = Sashite::Qpi.parse("S:+P")
162
+ qpi = Sashite::Qpi.parse("C:K^")
121
163
 
122
- # State queries
123
- identifier.normal? # => false
124
- identifier.enhanced? # => true
125
- identifier.diminished? # => false
126
- identifier.first_player? # => true
127
- identifier.second_player? # => false
164
+ # Transform SIN via component
165
+ qpi.with_sin(qpi.sin.with_family(:S)) # => "S:K^"
128
166
 
129
- # Comparison methods
130
- other = Sashite::Qpi.parse("C:+P")
131
- identifier.same_family?(other) # => false (S vs C)
132
- identifier.same_type?(other) # => true (both P)
133
- identifier.same_side?(other) # => true (both first player)
134
- identifier.same_state?(other) # => true (both enhanced)
135
- identifier.cross_family?(other) # => true (different families)
167
+ # 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"
171
+
172
+ # Chain transformations
173
+ qpi
174
+ .flip
175
+ .with_sin(qpi.sin.with_family(:S))
176
+ .with_pin(qpi.pin.with_type(:Q)) # => "s:q^"
177
+ ```
178
+
179
+ ## Component Queries
180
+
181
+ Since QPI is just a composition, use the component APIs directly:
182
+
183
+ ```ruby
184
+ qpi = Sashite::Qpi.parse("S:+P^")
185
+
186
+ # 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"
191
+
192
+ # 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
202
+ 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)
136
207
  ```
137
208
 
138
209
  ## API Reference
139
210
 
140
- ### Main Module Methods
211
+ ### Main Module
141
212
 
142
- - `Sashite::Qpi.parse(qpi_string)` - Parse QPI string into Identifier object
143
- - `Sashite::Qpi.identifier(family, type, side, state = :normal)` - Create identifier from parameters (strict validation)
144
- - `Sashite::Qpi.valid?(qpi_string)` - Check if string is valid QPI notation
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
+ ```
145
223
 
146
224
  ### Identifier Class
147
225
 
148
- #### Creation and Parsing
149
- - `Sashite::Qpi::Identifier.new(family, type, side, state = :normal)` - Create from parameters (strict validation)
150
- - `Sashite::Qpi::Identifier.parse(qpi_string)` - Parse QPI string
151
-
152
- #### Parameter Validation
153
- **Strict validation enforced**:
154
- - `family` parameter: Must be symbol `:A` to `:Z` (uppercase only)
155
- - `type` parameter: Must be symbol `:A` to `:Z` (uppercase only)
156
- - `side` parameter: Must be `:first` or `:second`
157
- - `state` parameter: Must be `:normal`, `:enhanced`, or `:diminished`
158
-
159
- #### Attribute Access
160
- - `#family` - Get style family (symbol `:A` to `:Z`)
161
- - `#type` - Get piece type (symbol `:A` to `:Z`)
162
- - `#side` - Get player side (`:first` or `:second`)
163
- - `#state` - Get piece state (`:normal`, `:enhanced`, or `:diminished`)
164
- - `#to_s` - Convert to QPI string representation
165
-
166
- #### Component Access
167
- - `#to_sin` - Get SIN string representation
168
- - `#to_pin` - Get PIN string representation
169
- - `#sin_component` - Get SIN identifier object
170
- - `#pin_component` - Get PIN identifier object
171
-
172
- #### State Queries
173
- - `#normal?` - Check if normal state
174
- - `#enhanced?` - Check if enhanced state
175
- - `#diminished?` - Check if diminished state
176
- - `#first_player?` - Check if first player
177
- - `#second_player?` - Check if second player
178
-
179
- #### Transformations (immutable - return new instances)
180
- - `#enhance` - Create enhanced version
181
- - `#diminish` - Create diminished version
182
- - `#normalize` - Remove state modifiers
183
- - `#with_type(new_type)` - Change piece type
184
- - `#with_side(new_side)` - Change player side
185
- - `#with_state(new_state)` - Change piece state
186
- - `#with_family(new_family)` - Change style family
187
- - `#flip` - Switch player assignment for both components
188
-
189
- #### Comparison Methods
190
- - `#same_family?(other)` - Check if same style family
191
- - `#same_type?(other)` - Check if same piece type
192
- - `#same_side?(other)` - Check if same player side
193
- - `#same_state?(other)` - Check if same piece state
194
- - `#cross_family?(other)` - Check if different style families
195
- - `#==(other)` - Full equality comparison
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.
196
254
 
197
255
  ## Format Specification
198
256
 
@@ -204,96 +262,371 @@ identifier.cross_family?(other) # => true (different families)
204
262
  ### Grammar (BNF)
205
263
  ```bnf
206
264
  <qpi> ::= <uppercase-qpi> | <lowercase-qpi>
265
+
207
266
  <uppercase-qpi> ::= <uppercase-letter> ":" <uppercase-pin>
208
267
  <lowercase-qpi> ::= <lowercase-letter> ":" <lowercase-pin>
209
- <uppercase-pin> ::= ["+" | "-"] <uppercase-letter>
210
- <lowercase-pin> ::= ["+" | "-"] <lowercase-letter>
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)
211
289
  ```
212
290
 
213
291
  ### Regular Expression
214
292
  ```ruby
215
- /\A([A-Z]:[-+]?[A-Z]|[a-z]:[-+]?[a-z])\z/
293
+ /\A([A-Z]:[-+]?[A-Z]\^?|[a-z]:[-+]?[a-z]\^?)\z/
216
294
  ```
217
295
 
218
- ### Examples
296
+ ## Examples
219
297
 
220
- - `C:K` - Chess-style king, first player
221
- - `c:k` - Chess-style king, second player
222
- - `S:+R` - Shogi-style enhanced rook, first player
223
- - `x:-s` - Xiangqi-style diminished soldier, second player
298
+ ### Basic Identifiers
224
299
 
225
- ## Semantic Consistency
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
+ ```
226
319
 
227
- QPI enforces semantic consistency: the style and piece components must represent the same player. Both components use case to indicate player assignment, and these must align.
320
+ ### Cross-Style Scenarios
228
321
 
229
- **Valid combinations:**
230
322
  ```ruby
231
- Sashite::Qpi.valid?("C:K") # => true (both first player)
232
- Sashite::Qpi.valid?("c:k") # => true (both second player)
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
233
335
  ```
234
336
 
235
- **Invalid combinations:**
337
+ ### Component Manipulation
338
+
236
339
  ```ruby
237
- Sashite::Qpi.valid?("C:k") # => false (family=first, piece=second)
238
- Sashite::Qpi.valid?("c:K") # => false (family=second, piece=first)
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
438
+ def initialize(sin, pin)
439
+ raise unless sin.side == pin.side # Only validation
440
+
441
+ @sin = sin
442
+ @pin = pin
443
+ end
444
+ end
239
445
  ```
240
446
 
241
- ## Parameter Validation
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.
242
457
 
243
- ### Strict Validation Rules
458
+ ### 3. Component Transparency
244
459
 
245
- QPI enforces strict parameter validation consistent with its underlying SIN and PIN primitives:
460
+ Access components directly no wrappers:
246
461
 
247
462
  ```ruby
248
- # Valid parameter examples
249
- Sashite::Qpi.identifier(:C, :K, :first, :normal) # All uppercase symbols
250
- Sashite::Qpi.identifier(:S, :R, :second, :enhanced) # Display case determined by side
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
476
+ ```
477
+
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
251
485
 
252
- # Invalid parameter examples (raise ArgumentError)
253
- Sashite::Qpi.identifier(:c, :K, :first, :normal) # Lowercase family rejected
254
- Sashite::Qpi.identifier(:C, :k, :first, :normal) # Lowercase type rejected
255
- Sashite::Qpi.identifier("C", :K, :first, :normal) # String family rejected
256
- Sashite::Qpi.identifier(:C, "K", :first, :normal) # String type rejected
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))
257
489
  ```
258
490
 
259
- ### Error Handling
491
+ ### 5. Immutability
492
+
493
+ All instances frozen. Transformations return new instances:
494
+
495
+ ```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
501
+ ```
260
502
 
261
- QPI delegates validation to its underlying primitives, ensuring consistent error messages:
503
+ ## Error Handling
262
504
 
263
505
  ```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
512
+
513
+ # Side mismatch between components
514
+ sin = Sashite::Sin.parse("C") # first player
515
+ pin = Sashite::Pin.parse("k") # second player
264
516
  begin
265
- Sashite::Qpi.identifier(:c, :K, :first, :normal)
517
+ Sashite::Qpi.new(sin, pin)
266
518
  rescue ArgumentError => e
267
- # Same error message as Sashite::Sin::Identifier.new(:c, :first)
268
- puts e.message # => "Family must be a symbol from :A to :Z representing Style Family, got: :c"
519
+ e.message # => Semantic consistency error
269
520
  end
270
521
 
522
+ # Component validation errors delegate
271
523
  begin
272
- Sashite::Qpi.identifier(:C, :k, :first, :normal)
524
+ Sashite::Qpi.parse("CC:K")
273
525
  rescue ArgumentError => e
274
- # Same error message as Sashite::Pin::Identifier.new(:k, :first, :normal)
275
- puts e.message # => "Type must be a symbol from :A to :Z, got: :k"
526
+ # SIN validation error
527
+ end
528
+ ```
529
+
530
+ ## Performance Considerations
531
+
532
+ ### Efficient Composition
533
+
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)
539
+
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^")
552
+
553
+ # Pattern 1: Single component transformation
554
+ qpi.with_pin(qpi.pin.with_type(:Q))
555
+
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)
562
+
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)
567
+
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
276
607
  end
277
608
  ```
278
609
 
279
610
  ## Design Properties
280
611
 
281
- - **Rule-agnostic**: Independent of specific game mechanics
282
- - **Complete identification**: All four piece attributes represented
283
- - **Cross-style support**: Enables multi-tradition gaming
284
- - **Semantic validation**: Ensures component consistency
285
- - **Primitive foundation**: Built from SIN and PIN specifications
286
- - **Strict validation**: Consistent parameter validation with underlying primitives
287
- - **Immutable**: All instances frozen, transformations return new objects
288
- - **Functional**: Pure functions with no side effects
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)
289
622
 
290
623
  ## Related Specifications
291
624
 
292
- - [QPI Specification v1.0.0](https://sashite.dev/specs/qpi/1.0.0/) - Complete technical specification
293
- - [QPI Examples](https://sashite.dev/specs/qpi/1.0.0/examples/) - Practical implementation examples
294
- - [SIN Specification v1.0.0](https://sashite.dev/specs/sin/1.0.0/) - Style identification component
295
- - [PIN Specification v1.0.0](https://sashite.dev/specs/pin/1.0.0/) - Piece identification component
296
- - [Sashité Protocol](https://sashite.dev/protocol/) - Conceptual foundation
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
297
630
 
298
631
  ## License
299
632