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.
- checksums.yaml +4 -4
- data/README.md +502 -169
- data/lib/sashite/qpi/identifier.rb +115 -298
- data/lib/sashite/qpi.rb +73 -158
- metadata +6 -6
data/README.md
CHANGED
|
@@ -9,9 +9,29 @@
|
|
|
9
9
|
|
|
10
10
|
## What is QPI?
|
|
11
11
|
|
|
12
|
-
QPI (Qualified Piece Identifier) provides
|
|
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
|
-
|
|
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
|
-
##
|
|
39
|
-
|
|
40
|
-
### Basic Operations
|
|
56
|
+
## Quick Start
|
|
41
57
|
|
|
42
58
|
```ruby
|
|
43
59
|
require "sashite/qpi"
|
|
44
60
|
|
|
45
|
-
# Parse QPI
|
|
46
|
-
|
|
47
|
-
|
|
61
|
+
# Parse a QPI string
|
|
62
|
+
qpi = Sashite::Qpi.parse("C:K^")
|
|
63
|
+
qpi.to_s # => "C:K^"
|
|
48
64
|
|
|
49
|
-
#
|
|
50
|
-
|
|
51
|
-
|
|
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
|
-
#
|
|
54
|
-
|
|
55
|
-
|
|
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
|
-
|
|
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
|
-
|
|
95
|
+
### Accessing Components
|
|
62
96
|
|
|
63
97
|
```ruby
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
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
|
-
#
|
|
69
|
-
|
|
70
|
-
|
|
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
|
-
|
|
110
|
+
### Five Fundamental Attributes
|
|
74
111
|
|
|
75
|
-
|
|
112
|
+
All attributes come directly from the components:
|
|
76
113
|
|
|
77
114
|
```ruby
|
|
78
|
-
|
|
115
|
+
qpi = Sashite::Qpi.parse("S:+R^")
|
|
79
116
|
|
|
80
|
-
#
|
|
81
|
-
|
|
82
|
-
|
|
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
|
-
#
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
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
|
-
|
|
127
|
+
## Transformations
|
|
128
|
+
|
|
129
|
+
All transformations return new immutable QPI instances:
|
|
130
|
+
|
|
131
|
+
### Replace Components
|
|
94
132
|
|
|
95
133
|
```ruby
|
|
96
|
-
|
|
97
|
-
identifier = Sashite::Qpi.parse("C:K")
|
|
134
|
+
qpi = Sashite::Qpi.parse("C:K^")
|
|
98
135
|
|
|
99
|
-
#
|
|
100
|
-
|
|
101
|
-
|
|
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
|
-
#
|
|
105
|
-
|
|
106
|
-
|
|
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
|
-
#
|
|
111
|
-
|
|
144
|
+
# Transform both
|
|
145
|
+
qpi.with_sin(new_sin).with_pin(new_pin) # => "S:Q^"
|
|
146
|
+
```
|
|
112
147
|
|
|
113
|
-
|
|
114
|
-
|
|
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
|
-
|
|
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
|
-
|
|
162
|
+
qpi = Sashite::Qpi.parse("C:K^")
|
|
121
163
|
|
|
122
|
-
#
|
|
123
|
-
|
|
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
|
-
#
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
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
|
|
211
|
+
### Main Module
|
|
141
212
|
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
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
|
-
####
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
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
|
-
|
|
210
|
-
<
|
|
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]
|
|
293
|
+
/\A([A-Z]:[-+]?[A-Z]\^?|[a-z]:[-+]?[a-z]\^?)\z/
|
|
216
294
|
```
|
|
217
295
|
|
|
218
|
-
|
|
296
|
+
## Examples
|
|
219
297
|
|
|
220
|
-
|
|
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
|
-
|
|
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
|
-
|
|
320
|
+
### Cross-Style Scenarios
|
|
228
321
|
|
|
229
|
-
**Valid combinations:**
|
|
230
322
|
```ruby
|
|
231
|
-
|
|
232
|
-
Sashite::Qpi.
|
|
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
|
-
|
|
337
|
+
### Component Manipulation
|
|
338
|
+
|
|
236
339
|
```ruby
|
|
237
|
-
|
|
238
|
-
Sashite::Qpi.
|
|
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
|
-
|
|
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
|
-
###
|
|
458
|
+
### 3. Component Transparency
|
|
244
459
|
|
|
245
|
-
|
|
460
|
+
Access components directly — no wrappers:
|
|
246
461
|
|
|
247
462
|
```ruby
|
|
248
|
-
#
|
|
249
|
-
|
|
250
|
-
|
|
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
|
-
#
|
|
253
|
-
|
|
254
|
-
|
|
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
|
-
###
|
|
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
|
-
|
|
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.
|
|
517
|
+
Sashite::Qpi.new(sin, pin)
|
|
266
518
|
rescue ArgumentError => e
|
|
267
|
-
#
|
|
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.
|
|
524
|
+
Sashite::Qpi.parse("CC:K")
|
|
273
525
|
rescue ArgumentError => e
|
|
274
|
-
#
|
|
275
|
-
|
|
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
|
|
282
|
-
- **Complete identification**: All
|
|
283
|
-
- **Cross-style support**:
|
|
284
|
-
- **
|
|
285
|
-
- **
|
|
286
|
-
- **
|
|
287
|
-
- **Immutable**:
|
|
288
|
-
- **
|
|
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/) -
|
|
293
|
-
- [QPI Examples](https://sashite.dev/specs/qpi/1.0.0/examples/) -
|
|
294
|
-
- [SIN Specification v1.0.0](https://sashite.dev/specs/sin/1.0.0/) - Style
|
|
295
|
-
- [PIN Specification v1.0.0](https://sashite.dev/specs/pin/1.0.0/) - Piece
|
|
296
|
-
- [Sashité Protocol](https://sashite.dev/protocol/) -
|
|
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
|
|