sashite-qpi 1.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 +260 -210
- 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 +153 -333
- data/lib/sashite/qpi/parser.rb +122 -0
- data/lib/sashite/qpi.rb +60 -188
- data/lib/sashite-qpi.rb +0 -11
- metadata +18 -17
- data/LICENSE.md +0 -22
data/README.md
CHANGED
|
@@ -1,17 +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
13
|
|
|
14
|
-
|
|
14
|
+
QPI provides complete piece identification by combining two primitive notations:
|
|
15
|
+
- [SIN](https://sashite.dev/specs/sin/1.0.0/) (Style Identifier Notation) — identifies the piece style
|
|
16
|
+
- [PIN](https://sashite.dev/specs/pin/1.0.0/) (Piece Identifier Notation) — identifies the piece attributes
|
|
17
|
+
|
|
18
|
+
A QPI identifier is a **pair of (SIN, PIN)** that encodes complete **Piece Identity**.
|
|
15
19
|
|
|
16
20
|
## Installation
|
|
17
21
|
|
|
@@ -28,8 +32,6 @@ gem install sashite-qpi
|
|
|
28
32
|
|
|
29
33
|
## Dependencies
|
|
30
34
|
|
|
31
|
-
QPI builds upon two foundational primitive specifications:
|
|
32
|
-
|
|
33
35
|
```ruby
|
|
34
36
|
gem "sashite-sin" # Style Identifier Notation
|
|
35
37
|
gem "sashite-pin" # Piece Identifier Notation
|
|
@@ -37,268 +39,316 @@ gem "sashite-pin" # Piece Identifier Notation
|
|
|
37
39
|
|
|
38
40
|
## Usage
|
|
39
41
|
|
|
40
|
-
###
|
|
42
|
+
### Parsing (String → Identifier)
|
|
43
|
+
|
|
44
|
+
Convert a QPI string into an `Identifier` object.
|
|
41
45
|
|
|
42
46
|
```ruby
|
|
43
47
|
require "sashite/qpi"
|
|
44
48
|
|
|
45
|
-
#
|
|
46
|
-
|
|
47
|
-
|
|
49
|
+
# Standard parsing (raises on error)
|
|
50
|
+
qpi = Sashite::Qpi.parse("C:K^")
|
|
51
|
+
qpi.to_s # => "C:K^"
|
|
52
|
+
|
|
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)
|
|
59
|
+
|
|
60
|
+
# Components are full SIN and PIN instances
|
|
61
|
+
qpi.sin.first_player? # => true
|
|
62
|
+
qpi.pin.enhanced? # => false
|
|
63
|
+
|
|
64
|
+
# Invalid input raises ArgumentError
|
|
65
|
+
Sashite::Qpi.parse("invalid") # => raises ArgumentError
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
### Formatting (Identifier → String)
|
|
69
|
+
|
|
70
|
+
Convert an `Identifier` back to a QPI string.
|
|
71
|
+
|
|
72
|
+
```ruby
|
|
73
|
+
# From components
|
|
74
|
+
sin = Sashite::Sin.parse("C")
|
|
75
|
+
pin = Sashite::Pin.parse("K^")
|
|
76
|
+
qpi = Sashite::Qpi::Identifier.new(sin, pin)
|
|
77
|
+
qpi.to_s # => "C:K^"
|
|
78
|
+
|
|
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
|
|
48
87
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
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
|
|
95
|
+
```
|
|
52
96
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
Sashite::Qpi.
|
|
97
|
+
### Accessing Components
|
|
98
|
+
|
|
99
|
+
```ruby
|
|
100
|
+
qpi = Sashite::Qpi.parse("S:+R^")
|
|
101
|
+
|
|
102
|
+
# Get components
|
|
103
|
+
qpi.sin # => #<Sashite::Sin::Identifier style=:S side=:first>
|
|
104
|
+
qpi.pin # => #<Sashite::Pin::Identifier type=:R state=:enhanced terminal=true>
|
|
105
|
+
|
|
106
|
+
# Serialize components
|
|
107
|
+
qpi.sin.to_s # => "S"
|
|
108
|
+
qpi.pin.to_s # => "+R^"
|
|
109
|
+
qpi.to_s # => "S:+R^"
|
|
57
110
|
```
|
|
58
111
|
|
|
59
|
-
###
|
|
112
|
+
### Five Piece Identity Attributes
|
|
60
113
|
|
|
61
|
-
|
|
114
|
+
All attributes come directly from the components:
|
|
62
115
|
|
|
63
116
|
```ruby
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
117
|
+
qpi = Sashite::Qpi.parse("S:+R^")
|
|
118
|
+
|
|
119
|
+
# From SIN component
|
|
120
|
+
qpi.sin.style # => :S (Piece Style)
|
|
67
121
|
|
|
68
|
-
#
|
|
69
|
-
|
|
70
|
-
|
|
122
|
+
# From PIN component
|
|
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)
|
|
71
127
|
```
|
|
72
128
|
|
|
73
|
-
|
|
129
|
+
### Native and Derived Relationship
|
|
74
130
|
|
|
75
|
-
|
|
131
|
+
QPI defines a deterministic relationship based on case comparison between SIN and PIN letters.
|
|
76
132
|
|
|
77
133
|
```ruby
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
#
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
#
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
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)
|
|
91
148
|
```
|
|
92
149
|
|
|
93
150
|
### Transformations
|
|
94
151
|
|
|
152
|
+
All transformations return new immutable instances.
|
|
153
|
+
|
|
95
154
|
```ruby
|
|
96
|
-
|
|
97
|
-
identifier = Sashite::Qpi.parse("C:K")
|
|
155
|
+
qpi = Sashite::Qpi.parse("C:K^")
|
|
98
156
|
|
|
99
|
-
#
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
normalized = identifier.normalize # => "C:K"
|
|
157
|
+
# Replace SIN component
|
|
158
|
+
new_sin = Sashite::Sin.parse("S")
|
|
159
|
+
qpi.with_sin(new_sin).to_s # => "S:K^"
|
|
103
160
|
|
|
104
|
-
#
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
different_state = identifier.with_state(:enhanced) # => "C:+K"
|
|
108
|
-
different_family = identifier.with_family(:S) # => "S:K"
|
|
161
|
+
# Replace PIN component
|
|
162
|
+
new_pin = Sashite::Pin.parse("+Q^")
|
|
163
|
+
qpi.with_pin(new_pin).to_s # => "C:+Q^"
|
|
109
164
|
|
|
110
|
-
#
|
|
111
|
-
|
|
165
|
+
# Transform both
|
|
166
|
+
qpi.with_sin(new_sin).with_pin(new_pin).to_s # => "S:+Q^"
|
|
112
167
|
|
|
113
|
-
#
|
|
114
|
-
|
|
168
|
+
# Flip both components (change player)
|
|
169
|
+
qpi.flip.to_s # => "c:k^"
|
|
170
|
+
|
|
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)
|
|
175
|
+
|
|
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)
|
|
115
179
|
```
|
|
116
180
|
|
|
117
|
-
###
|
|
181
|
+
### Transform via Components
|
|
118
182
|
|
|
119
183
|
```ruby
|
|
120
|
-
|
|
121
|
-
|
|
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
|
|
128
|
-
|
|
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)
|
|
136
|
-
```
|
|
184
|
+
qpi = Sashite::Qpi.parse("C:K^")
|
|
137
185
|
|
|
138
|
-
|
|
186
|
+
# Transform SIN via component
|
|
187
|
+
qpi.with_sin(qpi.sin.with_style(:S)).to_s # => "S:K^"
|
|
139
188
|
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
- `Sashite::Qpi.valid?(qpi_string)` - Check if string is valid QPI notation
|
|
145
|
-
|
|
146
|
-
### Identifier Class
|
|
147
|
-
|
|
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
|
|
196
|
-
|
|
197
|
-
## Format Specification
|
|
198
|
-
|
|
199
|
-
### Structure
|
|
200
|
-
```
|
|
201
|
-
<sin>:<pin>
|
|
202
|
-
```
|
|
189
|
+
# Transform PIN via component
|
|
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"
|
|
203
193
|
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
<qpi> ::= <uppercase-qpi> | <lowercase-qpi>
|
|
207
|
-
<uppercase-qpi> ::= <uppercase-letter> ":" <uppercase-pin>
|
|
208
|
-
<lowercase-qpi> ::= <lowercase-letter> ":" <lowercase-pin>
|
|
209
|
-
<uppercase-pin> ::= ["+" | "-"] <uppercase-letter>
|
|
210
|
-
<lowercase-pin> ::= ["+" | "-"] <lowercase-letter>
|
|
194
|
+
# Chain transformations
|
|
195
|
+
qpi.flip.with_sin(qpi.sin.with_style(:S)).to_s # => "s:k^"
|
|
211
196
|
```
|
|
212
197
|
|
|
213
|
-
###
|
|
198
|
+
### Component Queries
|
|
199
|
+
|
|
200
|
+
Since QPI is a composition, use the component APIs directly:
|
|
201
|
+
|
|
214
202
|
```ruby
|
|
215
|
-
|
|
203
|
+
qpi = Sashite::Qpi.parse("S:+P^")
|
|
204
|
+
|
|
205
|
+
# SIN queries (style and side)
|
|
206
|
+
qpi.sin.style # => :S
|
|
207
|
+
qpi.sin.side # => :first
|
|
208
|
+
qpi.sin.first_player? # => true
|
|
209
|
+
qpi.sin.letter # => "S"
|
|
210
|
+
|
|
211
|
+
# PIN queries (type, state, terminal)
|
|
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
|
|
221
|
+
other = Sashite::Qpi.parse("C:+P^")
|
|
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)
|
|
216
226
|
```
|
|
217
227
|
|
|
218
|
-
|
|
228
|
+
## API Reference
|
|
219
229
|
|
|
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
|
|
230
|
+
### Types
|
|
224
231
|
|
|
225
|
-
|
|
232
|
+
```ruby
|
|
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]
|
|
241
|
+
def initialize(sin, pin)
|
|
242
|
+
|
|
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
|
|
267
|
+
end
|
|
268
|
+
```
|
|
226
269
|
|
|
227
|
-
|
|
270
|
+
### Parsing
|
|
228
271
|
|
|
229
|
-
**Valid combinations:**
|
|
230
272
|
```ruby
|
|
231
|
-
|
|
232
|
-
|
|
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)
|
|
233
280
|
```
|
|
234
281
|
|
|
235
|
-
|
|
282
|
+
### Validation
|
|
283
|
+
|
|
236
284
|
```ruby
|
|
237
|
-
|
|
238
|
-
|
|
285
|
+
# Reports whether string is a valid QPI.
|
|
286
|
+
#
|
|
287
|
+
# @param string [String] QPI string
|
|
288
|
+
# @return [Boolean]
|
|
289
|
+
def Sashite::Qpi.valid?(string)
|
|
239
290
|
```
|
|
240
291
|
|
|
241
|
-
|
|
292
|
+
### Transformations
|
|
242
293
|
|
|
243
|
-
|
|
294
|
+
```ruby
|
|
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
|
|
244
298
|
|
|
245
|
-
|
|
299
|
+
# Flip transformation (transforms both components)
|
|
300
|
+
def flip # => Identifier with both SIN and PIN flipped
|
|
246
301
|
|
|
247
|
-
|
|
248
|
-
#
|
|
249
|
-
|
|
250
|
-
Sashite::Qpi.identifier(:S, :R, :second, :enhanced) # Display case determined by side
|
|
251
|
-
|
|
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
|
|
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
|
|
257
305
|
```
|
|
258
306
|
|
|
259
|
-
###
|
|
307
|
+
### Errors
|
|
260
308
|
|
|
261
|
-
|
|
309
|
+
All parsing and validation errors raise `ArgumentError` with descriptive messages:
|
|
262
310
|
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
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 |
|
|
270
319
|
|
|
271
|
-
|
|
272
|
-
Sashite::Qpi.identifier(:C, :k, :first, :normal)
|
|
273
|
-
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"
|
|
276
|
-
end
|
|
277
|
-
```
|
|
320
|
+
## Piece Identity Mapping
|
|
278
321
|
|
|
279
|
-
|
|
322
|
+
QPI encodes complete **Piece Identity** as defined in the [Glossary](https://sashite.dev/glossary/):
|
|
280
323
|
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
- **Functional**: Pure functions with no side effects
|
|
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) |
|
|
289
331
|
|
|
290
|
-
|
|
332
|
+
Additionally, QPI provides a **Native/Derived relationship** via `native?`, `derived?`, `native`, and `derive`.
|
|
291
333
|
|
|
292
|
-
|
|
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
|
|
334
|
+
## Design Principles
|
|
297
335
|
|
|
298
|
-
|
|
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`
|
|
299
343
|
|
|
300
|
-
|
|
344
|
+
## Related Specifications
|
|
345
|
+
|
|
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
|
|
301
351
|
|
|
302
|
-
##
|
|
352
|
+
## License
|
|
303
353
|
|
|
304
|
-
|
|
354
|
+
Available as open source under the [Apache License 2.0](https://opensource.org/licenses/Apache-2.0).
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Sashite
|
|
4
|
+
module Qpi
|
|
5
|
+
# Constants for QPI (Qualified Piece Identifier).
|
|
6
|
+
#
|
|
7
|
+
# This module defines the structural constants for QPI tokens.
|
|
8
|
+
module Constants
|
|
9
|
+
# Separator between SIN and PIN components.
|
|
10
|
+
SEPARATOR = ":"
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
end
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Sashite
|
|
4
|
+
module Qpi
|
|
5
|
+
module Errors
|
|
6
|
+
class Argument < ::ArgumentError
|
|
7
|
+
# Centralized error messages for QPI parsing and validation.
|
|
8
|
+
#
|
|
9
|
+
# @example
|
|
10
|
+
# raise Errors::Argument, Messages::EMPTY_INPUT
|
|
11
|
+
module Messages
|
|
12
|
+
# Parsing errors
|
|
13
|
+
EMPTY_INPUT = "empty input"
|
|
14
|
+
MISSING_SEPARATOR = "missing colon separator"
|
|
15
|
+
MISSING_SIN = "missing SIN component"
|
|
16
|
+
MISSING_PIN = "missing PIN component"
|
|
17
|
+
INVALID_SIN = "invalid SIN component"
|
|
18
|
+
INVALID_PIN = "invalid PIN component"
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "argument/messages"
|
|
4
|
+
|
|
5
|
+
module Sashite
|
|
6
|
+
module Qpi
|
|
7
|
+
module Errors
|
|
8
|
+
# Error raised when QPI parsing or validation fails.
|
|
9
|
+
#
|
|
10
|
+
# @example
|
|
11
|
+
# raise Argument, Argument::Messages::EMPTY_INPUT
|
|
12
|
+
class Argument < ::ArgumentError
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|