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.
- checksums.yaml +4 -4
- data/LICENSE +201 -0
- data/README.md +208 -491
- data/lib/sashite/qpi/constants.rb +13 -0
- data/lib/sashite/qpi/errors/argument/messages.rb +23 -0
- data/lib/sashite/qpi/errors/argument.rb +16 -0
- data/lib/sashite/qpi/errors.rb +3 -0
- data/lib/sashite/qpi/identifier.rb +156 -153
- data/lib/sashite/qpi/parser.rb +122 -0
- data/lib/sashite/qpi.rb +57 -100
- data/lib/sashite-qpi.rb +0 -11
- metadata +18 -17
- data/LICENSE.md +0 -22
data/README.md
CHANGED
|
@@ -1,37 +1,21 @@
|
|
|
1
|
-
#
|
|
1
|
+
# qpi.rb
|
|
2
2
|
|
|
3
3
|
[](https://github.com/sashite/qpi.rb/tags)
|
|
4
4
|
[](https://rubydoc.info/github/sashite/qpi.rb/main)
|
|
5
|
-
](https://github.com/sashite/qpi.rb/raw/main/LICENSE
|
|
5
|
+
[](https://github.com/sashite/qpi.rb/actions)
|
|
6
|
+
[](https://github.com/sashite/qpi.rb/raw/main/LICENSE)
|
|
7
7
|
|
|
8
|
-
> **QPI** (Qualified Piece Identifier) implementation for
|
|
8
|
+
> **QPI** (Qualified Piece Identifier) implementation for Ruby.
|
|
9
9
|
|
|
10
|
-
##
|
|
10
|
+
## Overview
|
|
11
11
|
|
|
12
|
-
|
|
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
|
|
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
|
-
##
|
|
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
|
-
#
|
|
49
|
+
# Standard parsing (raises on error)
|
|
62
50
|
qpi = Sashite::Qpi.parse("C:K^")
|
|
63
|
-
qpi.to_s
|
|
51
|
+
qpi.to_s # => "C:K^"
|
|
64
52
|
|
|
65
|
-
# Access the five
|
|
66
|
-
qpi.sin.
|
|
67
|
-
qpi.pin.type
|
|
68
|
-
qpi.
|
|
69
|
-
qpi.pin.state
|
|
70
|
-
qpi.pin.terminal?
|
|
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?
|
|
74
|
-
qpi.pin.enhanced?
|
|
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
|
-
|
|
68
|
+
### Formatting (Identifier → String)
|
|
78
69
|
|
|
79
|
-
|
|
70
|
+
Convert an `Identifier` back to a QPI string.
|
|
80
71
|
|
|
81
72
|
```ruby
|
|
82
|
-
#
|
|
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
|
-
#
|
|
91
|
-
Sashite::
|
|
92
|
-
Sashite::
|
|
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
|
|
102
|
-
qpi.pin
|
|
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
|
|
106
|
-
qpi.pin.to_s
|
|
107
|
-
qpi.to_s
|
|
107
|
+
qpi.sin.to_s # => "S"
|
|
108
|
+
qpi.pin.to_s # => "+R^"
|
|
109
|
+
qpi.to_s # => "S:+R^"
|
|
108
110
|
```
|
|
109
111
|
|
|
110
|
-
### Five
|
|
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.
|
|
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
|
|
123
|
-
qpi.pin.
|
|
124
|
-
qpi.pin.
|
|
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
|
-
|
|
129
|
+
### Native and Derived Relationship
|
|
130
|
+
|
|
131
|
+
QPI defines a deterministic relationship based on case comparison between SIN and PIN letters.
|
|
128
132
|
|
|
129
|
-
|
|
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
|
-
|
|
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)
|
|
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)
|
|
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)
|
|
146
|
-
```
|
|
166
|
+
qpi.with_sin(new_sin).with_pin(new_pin).to_s # => "S:+Q^"
|
|
147
167
|
|
|
148
|
-
|
|
168
|
+
# Flip both components (change player)
|
|
169
|
+
qpi.flip.to_s # => "c:k^"
|
|
149
170
|
|
|
150
|
-
|
|
151
|
-
qpi = Sashite::Qpi.parse("C:
|
|
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
|
-
|
|
154
|
-
qpi.
|
|
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.
|
|
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))
|
|
169
|
-
qpi.with_pin(qpi.pin.with_state(:enhanced))
|
|
170
|
-
qpi.with_pin(qpi.pin.with_terminal(false))
|
|
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
|
-
|
|
198
|
+
### Component Queries
|
|
180
199
|
|
|
181
|
-
Since QPI is
|
|
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.
|
|
188
|
-
qpi.sin.side
|
|
189
|
-
qpi.sin.first_player?
|
|
190
|
-
qpi.sin.letter
|
|
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
|
|
194
|
-
qpi.pin.state
|
|
195
|
-
qpi.pin.terminal?
|
|
196
|
-
qpi.pin.enhanced?
|
|
197
|
-
qpi.pin.letter
|
|
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.
|
|
204
|
-
qpi.pin.same_type?(other.pin)
|
|
205
|
-
qpi.sin.same_side?(other.sin)
|
|
206
|
-
qpi.pin.same_state?(other.pin)
|
|
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
|
-
###
|
|
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
|
-
#
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
#
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
#
|
|
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
|
-
|
|
442
|
-
|
|
443
|
-
|
|
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
|
-
###
|
|
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
|
-
#
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
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
|
-
###
|
|
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
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
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
|
-
|
|
292
|
+
### Transformations
|
|
504
293
|
|
|
505
294
|
```ruby
|
|
506
|
-
#
|
|
507
|
-
|
|
508
|
-
|
|
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
|
-
#
|
|
514
|
-
|
|
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
|
-
#
|
|
523
|
-
|
|
524
|
-
|
|
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
|
-
|
|
307
|
+
### Errors
|
|
531
308
|
|
|
532
|
-
|
|
309
|
+
All parsing and validation errors raise `ArgumentError` with descriptive messages:
|
|
533
310
|
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
qpi.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
- **
|
|
613
|
-
- **
|
|
614
|
-
- **
|
|
615
|
-
- **
|
|
616
|
-
- **
|
|
617
|
-
- **
|
|
618
|
-
- **
|
|
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
|
-
- [
|
|
626
|
-
- [QPI
|
|
627
|
-
- [
|
|
628
|
-
- [
|
|
629
|
-
- [
|
|
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 [
|
|
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).
|