sashite-qpi 1.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 +7 -0
- data/LICENSE.md +22 -0
- data/README.md +304 -0
- data/lib/sashite/qpi/identifier.rb +424 -0
- data/lib/sashite/qpi.rb +220 -0
- data/lib/sashite-qpi.rb +14 -0
- metadata +87 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 3f707276efe5619994fb1f4f3204b3b7bf32654cff14bae674d9ad75764afce3
|
4
|
+
data.tar.gz: 8a19a611d441d8e50362449d202e7f49c1298790388d802d9be90b4119ffd237
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 84cbe2ee48fa8ef3e3c2e5b3146447f17f9c0f9755fd2376da25baae3e6ee1a7242144682f3ff0de371ee9a2a618d0051f043888c05d6e92bb4dad393c44077d
|
7
|
+
data.tar.gz: 12dfafe49bf08c75c915f72f1c9fc4484bd9d1e125fd653c6387d290ce761ed6e7d91233f40728bf9ffb61af4e25b3e2fe524f7bca1433048a889fd3588cbd1e
|
data/LICENSE.md
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2014-2025 Sashite
|
2
|
+
|
3
|
+
MIT License
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
6
|
+
a copy of this software and associated documentation files (the
|
7
|
+
"Software"), to deal in the Software without restriction, including
|
8
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
9
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
10
|
+
permit persons to whom the Software is furnished to do so, subject to
|
11
|
+
the following conditions:
|
12
|
+
|
13
|
+
The above copyright notice and this permission notice shall be
|
14
|
+
included in all copies or substantial portions of the Software.
|
15
|
+
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
19
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
20
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
21
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
22
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,304 @@
|
|
1
|
+
# Qpi.rb
|
2
|
+
|
3
|
+
[](https://github.com/sashite/qpi.rb/tags)
|
4
|
+
[](https://rubydoc.info/github/sashite/qpi.rb/main)
|
5
|
+

|
6
|
+
[](https://github.com/sashite/qpi.rb/raw/main/LICENSE.md)
|
7
|
+
|
8
|
+
> **QPI** (Qualified Piece Identifier) implementation for the Ruby language.
|
9
|
+
|
10
|
+
## What is QPI?
|
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.
|
13
|
+
|
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**.
|
15
|
+
|
16
|
+
## Installation
|
17
|
+
|
18
|
+
```ruby
|
19
|
+
# In your Gemfile
|
20
|
+
gem "sashite-qpi"
|
21
|
+
```
|
22
|
+
|
23
|
+
Or install manually:
|
24
|
+
|
25
|
+
```sh
|
26
|
+
gem install sashite-qpi
|
27
|
+
```
|
28
|
+
|
29
|
+
## Dependencies
|
30
|
+
|
31
|
+
QPI builds upon two foundational primitive specifications:
|
32
|
+
|
33
|
+
```ruby
|
34
|
+
gem "sashite-sin" # Style Identifier Notation
|
35
|
+
gem "sashite-pin" # Piece Identifier Notation
|
36
|
+
```
|
37
|
+
|
38
|
+
## Usage
|
39
|
+
|
40
|
+
### Basic Operations
|
41
|
+
|
42
|
+
```ruby
|
43
|
+
require "sashite/qpi"
|
44
|
+
|
45
|
+
# Parse QPI strings
|
46
|
+
identifier = Sashite::Qpi.parse("C:K") # Chess king, first player
|
47
|
+
identifier.to_s # => "C:K"
|
48
|
+
|
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)
|
52
|
+
|
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)
|
57
|
+
```
|
58
|
+
|
59
|
+
### Strict Parameter Validation
|
60
|
+
|
61
|
+
**Important**: QPI enforces the same strict validation as its underlying SIN and PIN primitives:
|
62
|
+
|
63
|
+
```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"
|
67
|
+
|
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
|
71
|
+
```
|
72
|
+
|
73
|
+
**Key principle**: Input parameters must use uppercase symbols (`:A` to `:Z`). The `side` parameter determines the display case, not the input case.
|
74
|
+
|
75
|
+
### Attribute Access
|
76
|
+
|
77
|
+
```ruby
|
78
|
+
identifier = Sashite::Qpi.parse("S:+R")
|
79
|
+
|
80
|
+
# Four fundamental piece attributes
|
81
|
+
identifier.family # => :S
|
82
|
+
identifier.type # => :R
|
83
|
+
identifier.side # => :first
|
84
|
+
identifier.state # => :enhanced
|
85
|
+
|
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>
|
91
|
+
```
|
92
|
+
|
93
|
+
### Transformations
|
94
|
+
|
95
|
+
```ruby
|
96
|
+
# All transformations return new immutable instances
|
97
|
+
identifier = Sashite::Qpi.parse("C:K")
|
98
|
+
|
99
|
+
# State transformations
|
100
|
+
enhanced = identifier.enhance # => "C:+K"
|
101
|
+
diminished = identifier.diminish # => "C:-K"
|
102
|
+
normalized = identifier.normalize # => "C:K"
|
103
|
+
|
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"
|
109
|
+
|
110
|
+
# Player assignment flip
|
111
|
+
flipped = identifier.flip # => "c:k"
|
112
|
+
|
113
|
+
# Chain transformations
|
114
|
+
result = identifier.flip.enhance.with_type(:Q) # => "c:+q"
|
115
|
+
```
|
116
|
+
|
117
|
+
### State and Comparison Queries
|
118
|
+
|
119
|
+
```ruby
|
120
|
+
identifier = Sashite::Qpi.parse("S:+P")
|
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
|
+
```
|
137
|
+
|
138
|
+
## API Reference
|
139
|
+
|
140
|
+
### Main Module Methods
|
141
|
+
|
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
|
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
|
+
```
|
203
|
+
|
204
|
+
### Grammar (BNF)
|
205
|
+
```bnf
|
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>
|
211
|
+
```
|
212
|
+
|
213
|
+
### Regular Expression
|
214
|
+
```ruby
|
215
|
+
/\A([A-Z]:[-+]?[A-Z]|[a-z]:[-+]?[a-z])\z/
|
216
|
+
```
|
217
|
+
|
218
|
+
### Examples
|
219
|
+
|
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
|
224
|
+
|
225
|
+
## Semantic Consistency
|
226
|
+
|
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.
|
228
|
+
|
229
|
+
**Valid combinations:**
|
230
|
+
```ruby
|
231
|
+
Sashite::Qpi.valid?("C:K") # => true (both first player)
|
232
|
+
Sashite::Qpi.valid?("c:k") # => true (both second player)
|
233
|
+
```
|
234
|
+
|
235
|
+
**Invalid combinations:**
|
236
|
+
```ruby
|
237
|
+
Sashite::Qpi.valid?("C:k") # => false (family=first, piece=second)
|
238
|
+
Sashite::Qpi.valid?("c:K") # => false (family=second, piece=first)
|
239
|
+
```
|
240
|
+
|
241
|
+
## Parameter Validation
|
242
|
+
|
243
|
+
### Strict Validation Rules
|
244
|
+
|
245
|
+
QPI enforces strict parameter validation consistent with its underlying SIN and PIN primitives:
|
246
|
+
|
247
|
+
```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
|
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
|
257
|
+
```
|
258
|
+
|
259
|
+
### Error Handling
|
260
|
+
|
261
|
+
QPI delegates validation to its underlying primitives, ensuring consistent error messages:
|
262
|
+
|
263
|
+
```ruby
|
264
|
+
begin
|
265
|
+
Sashite::Qpi.identifier(:c, :K, :first, :normal)
|
266
|
+
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"
|
269
|
+
end
|
270
|
+
|
271
|
+
begin
|
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
|
+
```
|
278
|
+
|
279
|
+
## Design Properties
|
280
|
+
|
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
|
289
|
+
|
290
|
+
## Related Specifications
|
291
|
+
|
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
|
297
|
+
|
298
|
+
## License
|
299
|
+
|
300
|
+
Available as open source under the [MIT License](https://opensource.org/licenses/MIT).
|
301
|
+
|
302
|
+
## About
|
303
|
+
|
304
|
+
Maintained by [Sashité](https://sashite.com/) — promoting chess variants and sharing the beauty of board game cultures.
|
@@ -0,0 +1,424 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "sashite/pin"
|
4
|
+
require "sashite/sin"
|
5
|
+
|
6
|
+
module Sashite
|
7
|
+
module Qpi
|
8
|
+
# Represents an identifier in QPI (Qualified Piece Identifier) format.
|
9
|
+
#
|
10
|
+
# A QPI identifier combines style and piece attributes into a unified representation:
|
11
|
+
# - Family: Style family from SIN component (:A to :Z only)
|
12
|
+
# - Type: Piece type (:A to :Z) from PIN component
|
13
|
+
# - Side: Player assignment (:first or :second) from both components
|
14
|
+
# - State: Piece state (:normal, :enhanced, :diminished) from PIN component
|
15
|
+
# - Semantic constraint: SIN and PIN components must represent the same player
|
16
|
+
#
|
17
|
+
# All instances are immutable - transformation methods return new instances.
|
18
|
+
# This follows the QPI Specification v1.0.0 with strict parameter validation
|
19
|
+
# consistent with the underlying SIN and PIN primitive specifications.
|
20
|
+
#
|
21
|
+
# ## Strict Parameter Validation
|
22
|
+
#
|
23
|
+
# QPI enforces the same strict validation as its underlying primitives:
|
24
|
+
# - Family parameter must be a symbol from :A to :Z (not :a to :z)
|
25
|
+
# - Type parameter must be a symbol from :A to :Z (delegated to PIN)
|
26
|
+
# - Side parameter determines the display case, not the input parameters
|
27
|
+
#
|
28
|
+
# This ensures consistency with SIN and PIN behavior where lowercase symbols
|
29
|
+
# are rejected with ArgumentError.
|
30
|
+
#
|
31
|
+
# @example Strict parameter validation
|
32
|
+
# # Valid - uppercase symbols only
|
33
|
+
# Sashite::Qpi::Identifier.new(:C, :K, :first, :normal) # => "C:K"
|
34
|
+
# Sashite::Qpi::Identifier.new(:C, :K, :second, :normal) # => "c:k"
|
35
|
+
#
|
36
|
+
# # Invalid - lowercase symbols rejected
|
37
|
+
# Sashite::Qpi::Identifier.new(:c, :K, :second, :normal) # => ArgumentError
|
38
|
+
# Sashite::Qpi::Identifier.new(:C, :k, :second, :normal) # => ArgumentError
|
39
|
+
#
|
40
|
+
# @see https://sashite.dev/specs/qpi/1.0.0/ QPI Specification v1.0.0
|
41
|
+
class Identifier
|
42
|
+
# Component separator for string representation
|
43
|
+
SEPARATOR = ":"
|
44
|
+
|
45
|
+
# Error messages
|
46
|
+
ERROR_INVALID_QPI = "Invalid QPI string: %s"
|
47
|
+
ERROR_SEMANTIC_MISMATCH = "Family and side must represent the same player: family=%s (side=%s), side=%s"
|
48
|
+
ERROR_MISSING_SEPARATOR = "QPI string must contain exactly one colon separator: %s"
|
49
|
+
|
50
|
+
# @return [Symbol] the style family (:A to :Z based on SIN component)
|
51
|
+
def family
|
52
|
+
@sin_identifier.family
|
53
|
+
end
|
54
|
+
|
55
|
+
# @return [Symbol] the piece type (:A to :Z)
|
56
|
+
def type
|
57
|
+
@pin_identifier.type
|
58
|
+
end
|
59
|
+
|
60
|
+
# @return [Symbol] the player side (:first or :second)
|
61
|
+
def side
|
62
|
+
@pin_identifier.side
|
63
|
+
end
|
64
|
+
|
65
|
+
# @return [Symbol] the piece state (:normal, :enhanced, or :diminished)
|
66
|
+
def state
|
67
|
+
@pin_identifier.state
|
68
|
+
end
|
69
|
+
|
70
|
+
# Create a new identifier instance
|
71
|
+
#
|
72
|
+
# @param family [Symbol] style family identifier (:A to :Z only)
|
73
|
+
# @param type [Symbol] piece type (:A to :Z only)
|
74
|
+
# @param side [Symbol] player side (:first or :second)
|
75
|
+
# @param state [Symbol] piece state (:normal, :enhanced, or :diminished)
|
76
|
+
# @raise [ArgumentError] if parameters are invalid or semantically inconsistent
|
77
|
+
#
|
78
|
+
# @example Create identifiers with strict parameter validation
|
79
|
+
# # Valid - uppercase symbols only
|
80
|
+
# chess_king = Sashite::Qpi::Identifier.new(:C, :K, :first, :normal) # => "C:K"
|
81
|
+
# chess_pawn = Sashite::Qpi::Identifier.new(:C, :P, :second, :normal) # => "c:p"
|
82
|
+
#
|
83
|
+
# # Invalid - lowercase symbols rejected
|
84
|
+
# # Sashite::Qpi::Identifier.new(:c, :K, :first, :normal) # => ArgumentError
|
85
|
+
# # Sashite::Qpi::Identifier.new(:C, :k, :first, :normal) # => ArgumentError
|
86
|
+
def initialize(family, type, side, state = Pin::Identifier::NORMAL_STATE)
|
87
|
+
# Strict validation - delegate to underlying primitives for consistency
|
88
|
+
Sin::Identifier.validate_family(family)
|
89
|
+
Pin::Identifier.validate_type(type)
|
90
|
+
Pin::Identifier.validate_side(side)
|
91
|
+
Pin::Identifier.validate_state(state)
|
92
|
+
|
93
|
+
# Create PIN component
|
94
|
+
@pin_identifier = Pin::Identifier.new(type, side, state)
|
95
|
+
|
96
|
+
# Create SIN component - pass family directly without normalization
|
97
|
+
@sin_identifier = Sin::Identifier.new(family, side)
|
98
|
+
|
99
|
+
# Validate semantic consistency
|
100
|
+
validate_semantic_consistency
|
101
|
+
|
102
|
+
freeze
|
103
|
+
end
|
104
|
+
|
105
|
+
# Parse a QPI string into an Identifier object
|
106
|
+
#
|
107
|
+
# @param qpi_string [String] QPI notation string (format: sin:pin)
|
108
|
+
# @return [Identifier] new identifier instance
|
109
|
+
# @raise [ArgumentError] if the QPI string is invalid
|
110
|
+
#
|
111
|
+
# @example Parse QPI strings with automatic component separation
|
112
|
+
# Sashite::Qpi::Identifier.parse("C:K") # => #<Qpi::Identifier family=:C type=:K side=:first state=:normal>
|
113
|
+
# Sashite::Qpi::Identifier.parse("s:+r") # => #<Qpi::Identifier family=:S type=:R side=:second state=:enhanced>
|
114
|
+
# Sashite::Qpi::Identifier.parse("X:-S") # => #<Qpi::Identifier family=:X type=:S side=:first state=:diminished>
|
115
|
+
def self.parse(qpi_string)
|
116
|
+
string_value = String(qpi_string)
|
117
|
+
sin_part, pin_part = split_components(string_value)
|
118
|
+
|
119
|
+
# Parse components
|
120
|
+
sin_identifier = Sin::Identifier.parse(sin_part)
|
121
|
+
pin_identifier = Pin::Identifier.parse(pin_part)
|
122
|
+
|
123
|
+
# Validate semantic consistency BEFORE creating new instance
|
124
|
+
unless sin_identifier.side == pin_identifier.side
|
125
|
+
raise ::ArgumentError, format(ERROR_SEMANTIC_MISMATCH,
|
126
|
+
sin_part, sin_identifier.side, pin_identifier.side)
|
127
|
+
end
|
128
|
+
|
129
|
+
# Extract parameters and create new instance
|
130
|
+
new(sin_identifier.family, pin_identifier.type, pin_identifier.side, pin_identifier.state)
|
131
|
+
end
|
132
|
+
|
133
|
+
# Check if a string is a valid QPI notation
|
134
|
+
#
|
135
|
+
# @param qpi_string [String] the string to validate
|
136
|
+
# @return [Boolean] true if valid QPI, false otherwise
|
137
|
+
#
|
138
|
+
# @example Validate QPI strings with semantic checking
|
139
|
+
# Sashite::Qpi::Identifier.valid?("C:K") # => true
|
140
|
+
# Sashite::Qpi::Identifier.valid?("s:+r") # => true
|
141
|
+
# Sashite::Qpi::Identifier.valid?("C:k") # => false (semantic mismatch)
|
142
|
+
# Sashite::Qpi::Identifier.valid?("Chess") # => false (no separator)
|
143
|
+
def self.valid?(qpi_string)
|
144
|
+
return false unless qpi_string.is_a?(::String)
|
145
|
+
|
146
|
+
# Split components and validate each part
|
147
|
+
sin_part, pin_part = split_components(qpi_string)
|
148
|
+
return false unless Sashite::Sin.valid?(sin_part) && Sashite::Pin.valid?(pin_part)
|
149
|
+
|
150
|
+
# Semantic consistency check
|
151
|
+
sin_identifier = Sashite::Sin.parse(sin_part)
|
152
|
+
pin_identifier = Sashite::Pin.parse(pin_part)
|
153
|
+
sin_identifier.side == pin_identifier.side
|
154
|
+
rescue ArgumentError
|
155
|
+
false
|
156
|
+
end
|
157
|
+
|
158
|
+
# Convert the identifier to its QPI string representation
|
159
|
+
#
|
160
|
+
# @return [String] QPI notation string (format: sin:pin)
|
161
|
+
# @example Display QPI identifiers
|
162
|
+
# identifier.to_s # => "C:K"
|
163
|
+
def to_s
|
164
|
+
"#{@sin_identifier}#{SEPARATOR}#{@pin_identifier}"
|
165
|
+
end
|
166
|
+
|
167
|
+
# Convert to SIN string representation (style component only)
|
168
|
+
#
|
169
|
+
# @return [String] SIN notation string
|
170
|
+
# @example Extract style component
|
171
|
+
# identifier.to_sin # => "C"
|
172
|
+
def to_sin
|
173
|
+
@sin_identifier.to_s
|
174
|
+
end
|
175
|
+
|
176
|
+
# Convert to PIN string representation (piece component only)
|
177
|
+
#
|
178
|
+
# @return [String] PIN notation string
|
179
|
+
# @example Extract piece component
|
180
|
+
# identifier.to_pin # => "+K"
|
181
|
+
def to_pin
|
182
|
+
@pin_identifier.to_s
|
183
|
+
end
|
184
|
+
|
185
|
+
# Get the parsed SIN identifier object
|
186
|
+
#
|
187
|
+
# @return [Sashite::Sin::Identifier] SIN component as identifier object
|
188
|
+
def sin_component
|
189
|
+
@sin_identifier
|
190
|
+
end
|
191
|
+
|
192
|
+
# Get the parsed PIN identifier object
|
193
|
+
#
|
194
|
+
# @return [Sashite::Pin::Identifier] PIN component as identifier object
|
195
|
+
def pin_component
|
196
|
+
@pin_identifier
|
197
|
+
end
|
198
|
+
|
199
|
+
# Create a new identifier with enhanced state
|
200
|
+
#
|
201
|
+
# @return [Identifier] new identifier with enhanced PIN component
|
202
|
+
def enhance
|
203
|
+
return self if enhanced?
|
204
|
+
|
205
|
+
self.class.new(family, type, side, Pin::Identifier::ENHANCED_STATE)
|
206
|
+
end
|
207
|
+
|
208
|
+
# Create a new identifier with diminished state
|
209
|
+
#
|
210
|
+
# @return [Identifier] new identifier with diminished PIN component
|
211
|
+
def diminish
|
212
|
+
return self if diminished?
|
213
|
+
|
214
|
+
self.class.new(family, type, side, Pin::Identifier::DIMINISHED_STATE)
|
215
|
+
end
|
216
|
+
|
217
|
+
# Create a new identifier with normal state (no modifiers)
|
218
|
+
#
|
219
|
+
# @return [Identifier] new identifier with normalized PIN component
|
220
|
+
def normalize
|
221
|
+
return self if normal?
|
222
|
+
|
223
|
+
self.class.new(family, type, side, Pin::Identifier::NORMAL_STATE)
|
224
|
+
end
|
225
|
+
|
226
|
+
# Create a new identifier with different piece type
|
227
|
+
#
|
228
|
+
# @param new_type [Symbol] new piece type (:A to :Z)
|
229
|
+
# @return [Identifier] new identifier with different type
|
230
|
+
def with_type(new_type)
|
231
|
+
return self if type == new_type
|
232
|
+
|
233
|
+
self.class.new(family, new_type, side, state)
|
234
|
+
end
|
235
|
+
|
236
|
+
# Create a new identifier with different side
|
237
|
+
#
|
238
|
+
# @param new_side [Symbol] new player side (:first or :second)
|
239
|
+
# @return [Identifier] new identifier with different side
|
240
|
+
def with_side(new_side)
|
241
|
+
return self if side == new_side
|
242
|
+
|
243
|
+
self.class.new(family, type, new_side, state)
|
244
|
+
end
|
245
|
+
|
246
|
+
# Create a new identifier with different state
|
247
|
+
#
|
248
|
+
# @param new_state [Symbol] new piece state (:normal, :enhanced, or :diminished)
|
249
|
+
# @return [Identifier] new identifier with different state
|
250
|
+
def with_state(new_state)
|
251
|
+
return self if state == new_state
|
252
|
+
|
253
|
+
self.class.new(family, type, side, new_state)
|
254
|
+
end
|
255
|
+
|
256
|
+
# Create a new identifier with different family
|
257
|
+
#
|
258
|
+
# @param new_family [Symbol] new style family identifier (:A to :Z)
|
259
|
+
# @return [Identifier] new identifier with different family
|
260
|
+
def with_family(new_family)
|
261
|
+
return self if family == new_family
|
262
|
+
|
263
|
+
self.class.new(new_family, type, side, state)
|
264
|
+
end
|
265
|
+
|
266
|
+
# Create a new identifier with opposite player assignment
|
267
|
+
#
|
268
|
+
# Changes the player assignment (side) while preserving the family and piece attributes.
|
269
|
+
# This maintains semantic consistency between the components.
|
270
|
+
#
|
271
|
+
# @return [Identifier] new identifier with opposite side but same family
|
272
|
+
#
|
273
|
+
# @example Flip player assignment while preserving family and attributes
|
274
|
+
# chess_first = Sashite::Qpi::Identifier.parse("C:K") # Chess king, first player
|
275
|
+
# chess_second = chess_first.flip # => "c:k" (Chess king, second player)
|
276
|
+
#
|
277
|
+
# shogi_first = Sashite::Qpi::Identifier.parse("S:+R") # Shogi enhanced rook, first player
|
278
|
+
# shogi_second = shogi_first.flip # => "s:+r" (Shogi enhanced rook, second player)
|
279
|
+
def flip
|
280
|
+
self.class.new(family, type, opposite_side, state)
|
281
|
+
end
|
282
|
+
|
283
|
+
# Check if the identifier has normal state
|
284
|
+
#
|
285
|
+
# @return [Boolean] true if normal state
|
286
|
+
def normal?
|
287
|
+
@pin_identifier.normal?
|
288
|
+
end
|
289
|
+
|
290
|
+
# Check if the identifier has enhanced state
|
291
|
+
#
|
292
|
+
# @return [Boolean] true if enhanced state
|
293
|
+
def enhanced?
|
294
|
+
@pin_identifier.enhanced?
|
295
|
+
end
|
296
|
+
|
297
|
+
# Check if the identifier has diminished state
|
298
|
+
#
|
299
|
+
# @return [Boolean] true if diminished state
|
300
|
+
def diminished?
|
301
|
+
@pin_identifier.diminished?
|
302
|
+
end
|
303
|
+
|
304
|
+
# Check if the identifier belongs to the first player
|
305
|
+
#
|
306
|
+
# @return [Boolean] true if first player
|
307
|
+
def first_player?
|
308
|
+
@pin_identifier.first_player?
|
309
|
+
end
|
310
|
+
|
311
|
+
# Check if the identifier belongs to the second player
|
312
|
+
#
|
313
|
+
# @return [Boolean] true if second player
|
314
|
+
def second_player?
|
315
|
+
@pin_identifier.second_player?
|
316
|
+
end
|
317
|
+
|
318
|
+
# Check if this identifier has the same family as another
|
319
|
+
#
|
320
|
+
# @param other [Identifier] identifier to compare with
|
321
|
+
# @return [Boolean] true if same family (case-insensitive)
|
322
|
+
def same_family?(other)
|
323
|
+
return false unless other.is_a?(self.class)
|
324
|
+
|
325
|
+
@sin_identifier.same_family?(other.sin_component)
|
326
|
+
end
|
327
|
+
|
328
|
+
# Check if this identifier has different family from another
|
329
|
+
#
|
330
|
+
# @param other [Identifier] identifier to compare with
|
331
|
+
# @return [Boolean] true if different families
|
332
|
+
def cross_family?(other)
|
333
|
+
return false unless other.is_a?(self.class)
|
334
|
+
|
335
|
+
!same_family?(other)
|
336
|
+
end
|
337
|
+
|
338
|
+
# Check if this identifier has the same side as another
|
339
|
+
#
|
340
|
+
# @param other [Identifier] identifier to compare with
|
341
|
+
# @return [Boolean] true if same side
|
342
|
+
def same_side?(other)
|
343
|
+
return false unless other.is_a?(self.class)
|
344
|
+
|
345
|
+
@pin_identifier.same_side?(other.pin_component)
|
346
|
+
end
|
347
|
+
|
348
|
+
# Check if this identifier has the same type as another
|
349
|
+
#
|
350
|
+
# @param other [Identifier] identifier to compare with
|
351
|
+
# @return [Boolean] true if same type
|
352
|
+
def same_type?(other)
|
353
|
+
return false unless other.is_a?(self.class)
|
354
|
+
|
355
|
+
@pin_identifier.same_type?(other.pin_component)
|
356
|
+
end
|
357
|
+
|
358
|
+
# Check if this identifier has the same state as another
|
359
|
+
#
|
360
|
+
# @param other [Identifier] identifier to compare with
|
361
|
+
# @return [Boolean] true if same state
|
362
|
+
def same_state?(other)
|
363
|
+
return false unless other.is_a?(self.class)
|
364
|
+
|
365
|
+
@pin_identifier.same_state?(other.pin_component)
|
366
|
+
end
|
367
|
+
|
368
|
+
# Custom equality comparison
|
369
|
+
#
|
370
|
+
# @param other [Object] object to compare with
|
371
|
+
# @return [Boolean] true if identifiers are equal
|
372
|
+
def ==(other)
|
373
|
+
return false unless other.is_a?(self.class)
|
374
|
+
|
375
|
+
@sin_identifier == other.sin_component && @pin_identifier == other.pin_component
|
376
|
+
end
|
377
|
+
|
378
|
+
# Alias for == to ensure Set functionality works correctly
|
379
|
+
alias eql? ==
|
380
|
+
|
381
|
+
# Custom hash implementation for use in collections
|
382
|
+
#
|
383
|
+
# @return [Integer] hash value
|
384
|
+
def hash
|
385
|
+
[self.class, @sin_identifier, @pin_identifier].hash
|
386
|
+
end
|
387
|
+
|
388
|
+
private
|
389
|
+
|
390
|
+
# Split QPI string into SIN and PIN components
|
391
|
+
#
|
392
|
+
# @param qpi_string [String] QPI string to split
|
393
|
+
# @return [Array<String>] array containing [sin_part, pin_part]
|
394
|
+
def self.split_components(qpi_string)
|
395
|
+
parts = qpi_string.split(SEPARATOR, 2)
|
396
|
+
raise ::ArgumentError, format(ERROR_MISSING_SEPARATOR, qpi_string) unless parts.size == 2
|
397
|
+
|
398
|
+
parts
|
399
|
+
end
|
400
|
+
|
401
|
+
private_class_method :split_components
|
402
|
+
|
403
|
+
# Validate semantic consistency between SIN and PIN components
|
404
|
+
#
|
405
|
+
# @raise [ArgumentError] if family case doesn't match side
|
406
|
+
def validate_semantic_consistency
|
407
|
+
expected_side = @sin_identifier.side
|
408
|
+
actual_side = @pin_identifier.side
|
409
|
+
|
410
|
+
return if expected_side == actual_side
|
411
|
+
|
412
|
+
raise ::ArgumentError, format(ERROR_SEMANTIC_MISMATCH,
|
413
|
+
@sin_identifier.letter, expected_side, actual_side)
|
414
|
+
end
|
415
|
+
|
416
|
+
# Get the opposite player side
|
417
|
+
#
|
418
|
+
# @return [Symbol] the opposite side
|
419
|
+
def opposite_side
|
420
|
+
first_player? ? Pin::Identifier::SECOND_PLAYER : Pin::Identifier::FIRST_PLAYER
|
421
|
+
end
|
422
|
+
end
|
423
|
+
end
|
424
|
+
end
|
data/lib/sashite/qpi.rb
ADDED
@@ -0,0 +1,220 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "qpi/identifier"
|
4
|
+
|
5
|
+
module Sashite
|
6
|
+
# QPI (Qualified Piece Identifier) implementation for Ruby
|
7
|
+
#
|
8
|
+
# Provides a rule-agnostic format for identifying game pieces in abstract strategy board games
|
9
|
+
# by combining Style Identifier Notation (SIN) and Piece Identifier Notation (PIN) primitives
|
10
|
+
# with a colon separator. This combination enables complete piece identification across different
|
11
|
+
# game styles and contexts.
|
12
|
+
#
|
13
|
+
# ## Concept
|
14
|
+
#
|
15
|
+
# QPI addresses the fundamental need to uniquely identify game pieces across different style
|
16
|
+
# systems while maintaining complete attribute information. By combining SIN and PIN primitives,
|
17
|
+
# QPI provides explicit representation of all four fundamental piece attributes from the
|
18
|
+
# Sashité Protocol.
|
19
|
+
#
|
20
|
+
# ## Four Fundamental Attributes
|
21
|
+
#
|
22
|
+
# QPI represents all four piece attributes through primitive combination:
|
23
|
+
# - **Family**: Style identification from SIN component
|
24
|
+
# - **Type**: Piece type from PIN component
|
25
|
+
# - **Side**: Player assignment from both components (must be consistent)
|
26
|
+
# - **State**: Piece state from PIN component
|
27
|
+
#
|
28
|
+
# ## Format Structure
|
29
|
+
#
|
30
|
+
# A QPI identifier consists of two primitive components separated by a colon:
|
31
|
+
# - **SIN component**: Style identification with player assignment
|
32
|
+
# - **PIN component**: Piece identification with type, side, and state
|
33
|
+
# - **Separator**: Colon (:) provides clear delimitation
|
34
|
+
#
|
35
|
+
# The components must maintain semantic consistency: both SIN and PIN must represent
|
36
|
+
# the same player (first or second) through their respective case encodings.
|
37
|
+
#
|
38
|
+
# ## Semantic Consistency Constraint
|
39
|
+
#
|
40
|
+
# QPI enforces a critical constraint: the style identified by the SIN component must be
|
41
|
+
# associated with the same player as indicated by the PIN component. This ensures that
|
42
|
+
# piece ownership and style ownership remain aligned, preventing impossible combinations
|
43
|
+
# like a first player style with a second player piece.
|
44
|
+
#
|
45
|
+
# Examples of semantic consistency:
|
46
|
+
# - SIN "C" (first player) + PIN "K" (first player) = Valid
|
47
|
+
# - SIN "c" (second player) + PIN "k" (second player) = Valid
|
48
|
+
# - SIN "C" (first player) + PIN "k" (second player) = Invalid
|
49
|
+
# - SIN "c" (second player) + PIN "K" (first player) = Invalid
|
50
|
+
#
|
51
|
+
# ## Cross-Style Gaming Support
|
52
|
+
#
|
53
|
+
# QPI enables cross-style gaming scenarios where different players use different game
|
54
|
+
# traditions. The explicit style identification allows pieces from different systems
|
55
|
+
# to coexist while maintaining clear attribution to their respective players.
|
56
|
+
#
|
57
|
+
# ## Format Specification
|
58
|
+
#
|
59
|
+
# Structure: `<sin>:<pin>`
|
60
|
+
#
|
61
|
+
# Grammar (BNF):
|
62
|
+
# <qpi> ::= <uppercase-qpi> | <lowercase-qpi>
|
63
|
+
# <uppercase-qpi> ::= <uppercase-letter> ":" <uppercase-pin>
|
64
|
+
# <lowercase-qpi> ::= <lowercase-letter> ":" <lowercase-pin>
|
65
|
+
# <uppercase-pin> ::= ["+" | "-"] <uppercase-letter>
|
66
|
+
# <lowercase-pin> ::= ["+" | "-"] <lowercase-letter>
|
67
|
+
#
|
68
|
+
# Regular Expression: `/\A([A-Z]:[-+]?[A-Z]|[a-z]:[-+]?[a-z])\z/`
|
69
|
+
#
|
70
|
+
# ## Attribute Mapping
|
71
|
+
#
|
72
|
+
# QPI encodes piece attributes through primitive combination:
|
73
|
+
#
|
74
|
+
# | Piece Attribute | QPI Encoding | Examples |
|
75
|
+
# |-----------------|--------------|----------|
|
76
|
+
# | **Family** | SIN component | `C:K` = Chess family, `O:K` = Ogi family |
|
77
|
+
# | **Type** | PIN letter choice | `C:K` = King, `C:P` = Pawn |
|
78
|
+
# | **Side** | Component cases | `C:K` = First player, `c:k` = Second player |
|
79
|
+
# | **State** | PIN prefix modifier | `O:+P` = Enhanced, `C:-P` = Diminished |
|
80
|
+
#
|
81
|
+
# ## System Constraints
|
82
|
+
#
|
83
|
+
# - **Semantic Consistency**: SIN and PIN components must represent the same player
|
84
|
+
# - **Component Validation**: Each component must be valid according to its specification
|
85
|
+
# - **Complete Attribution**: All four fundamental piece attributes explicitly represented
|
86
|
+
# - **Cross-Style Support**: Enables multi-tradition gaming environments
|
87
|
+
#
|
88
|
+
# ## Examples
|
89
|
+
#
|
90
|
+
# ### Single-Style Games
|
91
|
+
#
|
92
|
+
# # Chess (both players use Chess style)
|
93
|
+
# white_king = Sashite::Qpi.parse("C:K") # Chess king, first player
|
94
|
+
# black_king = Sashite::Qpi.parse("c:k") # Chess king, second player
|
95
|
+
#
|
96
|
+
# # Ogi (both players use Ogi style)
|
97
|
+
# sente_king = Sashite::Qpi.parse("O:K") # Ogi king, first player (sente)
|
98
|
+
# gote_rook = Sashite::Qpi.parse("o:+r") # Ogi promoted rook, second player (gote)
|
99
|
+
#
|
100
|
+
# ### Cross-Style Games
|
101
|
+
#
|
102
|
+
# # Chess vs. Ogi match
|
103
|
+
# chess_player = Sashite::Qpi.parse("C:K") # First player uses Chess
|
104
|
+
# ogi_player = Sashite::Qpi.parse("o:k") # Second player uses Ogi
|
105
|
+
#
|
106
|
+
# # Verify cross-style scenario
|
107
|
+
# chess_player.cross_family?(ogi_player) # => true
|
108
|
+
#
|
109
|
+
# ### Attribute Access and Manipulation
|
110
|
+
#
|
111
|
+
# identifier = Sashite::Qpi.parse("O:+R")
|
112
|
+
#
|
113
|
+
# # Four fundamental attributes
|
114
|
+
# identifier.family # => :O
|
115
|
+
# identifier.type # => :R
|
116
|
+
# identifier.side # => :first
|
117
|
+
# identifier.state # => :enhanced
|
118
|
+
#
|
119
|
+
# # Component extraction
|
120
|
+
# identifier.to_sin # => "O"
|
121
|
+
# identifier.to_pin # => "+R"
|
122
|
+
#
|
123
|
+
# # Immutable transformations
|
124
|
+
# flipped = identifier.flip # => "o:+r"
|
125
|
+
# different_type = identifier.with_type(:Q) # => "O:+Q"
|
126
|
+
# different_family = identifier.with_family(:C) # => "C:+R"
|
127
|
+
#
|
128
|
+
# ## Design Properties
|
129
|
+
#
|
130
|
+
# - **Rule-agnostic**: Independent of specific game mechanics
|
131
|
+
# - **Complete identification**: Explicit representation of all four piece attributes
|
132
|
+
# - **Cross-style support**: Enables multi-tradition gaming environments
|
133
|
+
# - **Semantic validation**: Ensures consistency between style and piece ownership
|
134
|
+
# - **Primitive foundation**: Built from foundational SIN and PIN building blocks
|
135
|
+
# - **Extension-ready**: Can be enhanced by human-readable naming systems
|
136
|
+
# - **Context-flexible**: Adaptable to various identification needs
|
137
|
+
# - **Immutable**: All instances are frozen and transformations return new objects
|
138
|
+
# - **Functional**: Pure functions with no side effects
|
139
|
+
#
|
140
|
+
# @see https://sashite.dev/specs/qpi/1.0.0/ QPI Specification v1.0.0
|
141
|
+
# @see https://sashite.dev/specs/qpi/1.0.0/examples/ QPI Examples
|
142
|
+
# @see https://sashite.dev/specs/sin/1.0.0/ Style Identifier Notation (SIN)
|
143
|
+
# @see https://sashite.dev/specs/pin/1.0.0/ Piece Identifier Notation (PIN)
|
144
|
+
module Qpi
|
145
|
+
# Check if a string is a valid QPI notation
|
146
|
+
#
|
147
|
+
# Validates the string format and semantic consistency between SIN and PIN components.
|
148
|
+
# Both components must be individually valid and represent the same player through
|
149
|
+
# their respective case encodings.
|
150
|
+
#
|
151
|
+
# @param qpi_string [String] the string to validate
|
152
|
+
# @return [Boolean] true if valid QPI, false otherwise
|
153
|
+
#
|
154
|
+
# @example Validate various QPI formats
|
155
|
+
# Sashite::Qpi.valid?("C:K") # => true (Chess king, first player)
|
156
|
+
# Sashite::Qpi.valid?("c:k") # => true (Chess king, second player)
|
157
|
+
# Sashite::Qpi.valid?("O:+P") # => true (Ogi enhanced pawn, first player)
|
158
|
+
# Sashite::Qpi.valid?("o:-r") # => true (Ogi diminished rook, second player)
|
159
|
+
# Sashite::Qpi.valid?("C:k") # => false (semantic mismatch: first player style, second player piece)
|
160
|
+
# Sashite::Qpi.valid?("c:K") # => false (semantic mismatch: second player style, first player piece)
|
161
|
+
# Sashite::Qpi.valid?("CHESS:K") # => false (multi-character SIN component)
|
162
|
+
# Sashite::Qpi.valid?("C") # => false (missing PIN component)
|
163
|
+
def self.valid?(qpi_string)
|
164
|
+
Identifier.valid?(qpi_string)
|
165
|
+
end
|
166
|
+
|
167
|
+
# Parse a QPI string into an Identifier object
|
168
|
+
#
|
169
|
+
# Creates a new QPI identifier by parsing the string into SIN and PIN components,
|
170
|
+
# validating each component, and ensuring semantic consistency between them.
|
171
|
+
#
|
172
|
+
# @param qpi_string [String] QPI notation string (format: sin:pin)
|
173
|
+
# @return [Qpi::Identifier] parsed identifier object with family, type, side, and state attributes
|
174
|
+
# @raise [ArgumentError] if the QPI string is invalid or semantically inconsistent
|
175
|
+
#
|
176
|
+
# @example Parse different QPI formats with complete attribute access
|
177
|
+
# Sashite::Qpi.parse("C:K") # => #<Qpi::Identifier family=:C type=:K side=:first state=:normal>
|
178
|
+
# Sashite::Qpi.parse("c:k") # => #<Qpi::Identifier family=:C type=:K side=:second state=:normal>
|
179
|
+
# Sashite::Qpi.parse("O:+R") # => #<Qpi::Identifier family=:O type=:R side=:first state=:enhanced>
|
180
|
+
# Sashite::Qpi.parse("x:-s") # => #<Qpi::Identifier family=:X type=:S side=:second state=:diminished>
|
181
|
+
#
|
182
|
+
# @example Traditional game styles
|
183
|
+
# chess_king = Sashite::Qpi.parse("C:K") # Chess king, first player
|
184
|
+
# ogi_rook = Sashite::Qpi.parse("o:+r") # Ogi promoted rook, second player
|
185
|
+
# xiongqi_king = Sashite::Qpi.parse("X:K") # Xiongqi king, first player
|
186
|
+
def self.parse(qpi_string)
|
187
|
+
Identifier.parse(qpi_string)
|
188
|
+
end
|
189
|
+
|
190
|
+
# Create a new identifier instance with explicit parameters
|
191
|
+
#
|
192
|
+
# Constructs a QPI identifier by directly specifying all four fundamental attributes.
|
193
|
+
# This method provides parameter-based construction as an alternative to string parsing,
|
194
|
+
# enabling immediate validation and clearer API usage.
|
195
|
+
#
|
196
|
+
# @param family [Symbol] style family identifier (single ASCII letter as symbol)
|
197
|
+
# @param type [Symbol] piece type (:A to :Z)
|
198
|
+
# @param side [Symbol] player side (:first or :second)
|
199
|
+
# @param state [Symbol] piece state (:normal, :enhanced, or :diminished)
|
200
|
+
# @return [Qpi::Identifier] new immutable identifier instance
|
201
|
+
# @raise [ArgumentError] if parameters are invalid or semantically inconsistent
|
202
|
+
#
|
203
|
+
# @example Create identifiers with explicit parameters
|
204
|
+
# Sashite::Qpi.identifier(:C, :K, :first, :normal) # => "C:K"
|
205
|
+
# Sashite::Qpi.identifier(:c, :K, :second, :normal) # => "c:k"
|
206
|
+
# Sashite::Qpi.identifier(:O, :R, :first, :enhanced) # => "O:+R"
|
207
|
+
# Sashite::Qpi.identifier(:x, :S, :second, :diminished) # => "x:-s"
|
208
|
+
#
|
209
|
+
# @example Cross-style game setup
|
210
|
+
# chess_player = Sashite::Qpi.identifier(:C, :K, :first, :normal) # Chess king, first player
|
211
|
+
# ogi_player = Sashite::Qpi.identifier(:o, :K, :second, :normal) # Ogi king, second player
|
212
|
+
#
|
213
|
+
# chess_player.cross_family?(ogi_player) # => true (different families)
|
214
|
+
# chess_player.same_type?(ogi_player) # => true (both kings)
|
215
|
+
# chess_player.same_side?(ogi_player) # => false (different players)
|
216
|
+
def self.identifier(family, type, side, state = Pin::Identifier::NORMAL_STATE)
|
217
|
+
Identifier.new(family, type, side, state)
|
218
|
+
end
|
219
|
+
end
|
220
|
+
end
|
data/lib/sashite-qpi.rb
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "sashite/qpi"
|
4
|
+
|
5
|
+
# Sashité namespace for board game notation libraries
|
6
|
+
#
|
7
|
+
# Sashité provides a collection of libraries for representing and manipulating
|
8
|
+
# board game concepts according to the Game Protocol specifications.
|
9
|
+
#
|
10
|
+
# @see https://sashite.dev/game-protocol/ Game Protocol Foundation
|
11
|
+
# @see https://sashite.dev/specs/ Sashité Specifications
|
12
|
+
# @author Sashité
|
13
|
+
module Sashite
|
14
|
+
end
|
metadata
ADDED
@@ -0,0 +1,87 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: sashite-qpi
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 1.0.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Cyril Kato
|
8
|
+
bindir: bin
|
9
|
+
cert_chain: []
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
11
|
+
dependencies:
|
12
|
+
- !ruby/object:Gem::Dependency
|
13
|
+
name: sashite-pin
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
15
|
+
requirements:
|
16
|
+
- - "~>"
|
17
|
+
- !ruby/object:Gem::Version
|
18
|
+
version: '3.1'
|
19
|
+
type: :runtime
|
20
|
+
prerelease: false
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
22
|
+
requirements:
|
23
|
+
- - "~>"
|
24
|
+
- !ruby/object:Gem::Version
|
25
|
+
version: '3.1'
|
26
|
+
- !ruby/object:Gem::Dependency
|
27
|
+
name: sashite-sin
|
28
|
+
requirement: !ruby/object:Gem::Requirement
|
29
|
+
requirements:
|
30
|
+
- - "~>"
|
31
|
+
- !ruby/object:Gem::Version
|
32
|
+
version: '2.1'
|
33
|
+
type: :runtime
|
34
|
+
prerelease: false
|
35
|
+
version_requirements: !ruby/object:Gem::Requirement
|
36
|
+
requirements:
|
37
|
+
- - "~>"
|
38
|
+
- !ruby/object:Gem::Version
|
39
|
+
version: '2.1'
|
40
|
+
description: |
|
41
|
+
QPI (Qualified Piece Identifier) provides a rule-agnostic format for identifying game pieces
|
42
|
+
in abstract strategy board games by combining Style Identifier Notation (SIN) and Piece
|
43
|
+
Identifier Notation (PIN) primitives. This gem implements the QPI Specification v1.0.0 with
|
44
|
+
a modern Ruby interface featuring immutable identifier objects and functional programming
|
45
|
+
principles. QPI enables complete piece identification with all four fundamental attributes
|
46
|
+
(family, type, side, state) while supporting cross-style gaming environments. Perfect for
|
47
|
+
multi-tradition board games, hybrid gaming systems, and game engines requiring comprehensive
|
48
|
+
piece identification across different game styles and traditions.
|
49
|
+
email: contact@cyril.email
|
50
|
+
executables: []
|
51
|
+
extensions: []
|
52
|
+
extra_rdoc_files: []
|
53
|
+
files:
|
54
|
+
- LICENSE.md
|
55
|
+
- README.md
|
56
|
+
- lib/sashite-qpi.rb
|
57
|
+
- lib/sashite/qpi.rb
|
58
|
+
- lib/sashite/qpi/identifier.rb
|
59
|
+
homepage: https://github.com/sashite/qpi.rb
|
60
|
+
licenses:
|
61
|
+
- MIT
|
62
|
+
metadata:
|
63
|
+
bug_tracker_uri: https://github.com/sashite/qpi.rb/issues
|
64
|
+
documentation_uri: https://rubydoc.info/github/sashite/qpi.rb/main
|
65
|
+
homepage_uri: https://github.com/sashite/qpi.rb
|
66
|
+
source_code_uri: https://github.com/sashite/qpi.rb
|
67
|
+
specification_uri: https://sashite.dev/specs/qpi/1.0.0/
|
68
|
+
rubygems_mfa_required: 'true'
|
69
|
+
rdoc_options: []
|
70
|
+
require_paths:
|
71
|
+
- lib
|
72
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
73
|
+
requirements:
|
74
|
+
- - ">="
|
75
|
+
- !ruby/object:Gem::Version
|
76
|
+
version: 3.2.0
|
77
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
78
|
+
requirements:
|
79
|
+
- - ">="
|
80
|
+
- !ruby/object:Gem::Version
|
81
|
+
version: '0'
|
82
|
+
requirements: []
|
83
|
+
rubygems_version: 3.7.1
|
84
|
+
specification_version: 4
|
85
|
+
summary: QPI (Qualified Piece Identifier) implementation for Ruby with immutable identifier
|
86
|
+
objects
|
87
|
+
test_files: []
|