qpi.rb
QPI (Qualified Piece Identifier) implementation for Ruby.
Overview
This library implements the QPI Specification v1.0.0.
QPI provides complete piece identification by combining two primitive notations:
- SIN (Style Identifier Notation) — identifies the piece style
- PIN (Piece Identifier Notation) — identifies the piece attributes
A QPI identifier is a pair of (SIN, PIN) that encodes complete Piece Identity.
Installation
# In your Gemfile
gem "sashite-qpi"
Or install manually:
gem install sashite-qpi
Dependencies
gem "sashite-sin" # Style Identifier Notation
gem "sashite-pin" # Piece Identifier Notation
Usage
Parsing (String → Identifier)
Convert a QPI string into an Identifier object.
require "sashite/qpi"
# Standard parsing (raises on error)
qpi = Sashite::Qpi.parse("C:K^")
qpi.to_s # => "C:K^"
# Access the five Piece Identity attributes through components
qpi.sin.abbr # => :C (Piece Style)
qpi.pin.abbr # => :K (Piece Name)
qpi.pin.side # => :first (Piece Side)
qpi.pin.state # => :normal (Piece State)
qpi.pin.terminal? # => true (Terminal Status)
# Components are full SIN and PIN instances
qpi.sin.first_player? # => true
qpi.pin.enhanced? # => false
# Invalid input raises ArgumentError
Sashite::Qpi.parse("invalid") # => raises ArgumentError
Formatting (Identifier → String)
Convert an Identifier back to a QPI string.
# From components
sin = Sashite::Sin.parse("C")
pin = Sashite::Pin.parse("K^")
qpi = Sashite::Qpi::Identifier.new(sin, pin)
qpi.to_s # => "C:K^"
# With attributes
sin = Sashite::Sin.parse("s")
pin = Sashite::Pin.parse("+r")
qpi = Sashite::Qpi::Identifier.new(sin, pin)
qpi.to_s # => "s:+r"
Validation
# Boolean check
Sashite::Qpi.valid?("C:K^") # => true
Sashite::Qpi.valid?("s:+r") # => true
Sashite::Qpi.valid?("invalid") # => false
Sashite::Qpi.valid?("C:") # => false
Sashite::Qpi.valid?(":K") # => false
Accessing Components
qpi = Sashite::Qpi.parse("S:+R^")
# Get components
qpi.sin # => #<Sashite::Sin::Identifier S>
qpi.pin # => #<Sashite::Pin::Identifier +R^>
# Serialize components
qpi.sin.to_s # => "S"
qpi.pin.to_s # => "+R^"
qpi.to_s # => "S:+R^"
Five Piece Identity Attributes
All attributes come directly from the components:
qpi = Sashite::Qpi.parse("S:+R^")
# From SIN component
qpi.sin.abbr # => :S (Piece Style)
# From PIN component
qpi.pin.abbr # => :R (Piece Name)
qpi.pin.side # => :first (Piece Side)
qpi.pin.state # => :enhanced (Piece State)
qpi.pin.terminal? # => true (Terminal Status)
Native and Derived Relationship
QPI defines a deterministic relationship based on case comparison between SIN and PIN letters.
qpi = Sashite::Qpi.parse("C:K^")
# Access the relationship
qpi.sin.side # => :first (derived from SIN letter case)
qpi.native? # => true (sin.side == pin.side)
qpi.derived? # => false
# Native: SIN case matches PIN case
Sashite::Qpi.parse("C:K").native? # => true (both uppercase/first)
Sashite::Qpi.parse("c:k").native? # => true (both lowercase/second)
# Derived: SIN case differs from PIN case
Sashite::Qpi.parse("C:k").derived? # => true (uppercase vs lowercase)
Sashite::Qpi.parse("c:K").derived? # => true (lowercase vs uppercase)
Transformations
All transformations return new immutable instances.
qpi = Sashite::Qpi.parse("C:K^")
# Replace SIN component
new_sin = Sashite::Sin.parse("S")
qpi.with_sin(new_sin).to_s # => "S:K^"
# Replace PIN component
new_pin = Sashite::Pin.parse("+Q^")
qpi.with_pin(new_pin).to_s # => "C:+Q^"
# Transform both
qpi.with_sin(new_sin).with_pin(new_pin).to_s # => "S:+Q^"
# Flip PIN side (SIN unchanged)
qpi.flip.to_s # => "C:k^"
# Native/Derived transformations (modify PIN only)
qpi = Sashite::Qpi.parse("C:r")
qpi.native.to_s # => "C:R" (PIN case aligned with SIN case)
qpi.derive.to_s # => "C:r" (already derived, unchanged)
qpi = Sashite::Qpi.parse("C:R")
qpi.native.to_s # => "C:R" (already native, unchanged)
qpi.derive.to_s # => "C:r" (PIN case differs from SIN case)
Transform via Components
qpi = Sashite::Qpi.parse("C:K^")
# Transform PIN via component
qpi.with_pin(qpi.pin.with_abbr(:Q)).to_s # => "C:Q^"
qpi.with_pin(qpi.pin.with_state(:enhanced)).to_s # => "C:+K^"
qpi.with_pin(qpi.pin.with_terminal(false)).to_s # => "C:K"
# Replace SIN with new instance
qpi.with_sin(Sashite::Sin.parse("S")).to_s # => "S:K^"
# Chain transformations
qpi.flip.with_sin(Sashite::Sin.parse("c")).to_s # => "c:k^"
Component Queries
Since QPI is a composition, use the component APIs directly:
qpi = Sashite::Qpi.parse("S:+P^")
# SIN queries (style and side)
qpi.sin.abbr # => :S
qpi.sin.side # => :first
qpi.sin.first_player? # => true
qpi.sin.to_s # => "S"
# PIN queries (abbr, state, terminal)
qpi.pin.abbr # => :P
qpi.pin.state # => :enhanced
qpi.pin.terminal? # => true
qpi.pin.enhanced? # => true
qpi.pin.letter # => "P"
qpi.pin.prefix # => "+"
qpi.pin.suffix # => "^"
# Compare QPIs via components
other = Sashite::Qpi.parse("C:+P^")
qpi.sin.same_abbr?(other.sin) # => false (S vs C)
qpi.pin.same_abbr?(other.pin) # => true (both P)
qpi.sin.same_side?(other.sin) # => true (both first)
qpi.pin.same_state?(other.pin) # => true (both enhanced)
API Reference
Types
# Identifier represents a parsed QPI with complete Piece Identity.
class Sashite::Qpi::Identifier
# Creates an Identifier from SIN and PIN components.
# Raises ArgumentError if components are invalid.
#
# @param sin [Sashite::Sin::Identifier] Style component
# @param pin [Sashite::Pin::Identifier] Piece component
# @return [Identifier]
def initialize(sin, pin)
# Returns the SIN component.
#
# @return [Sashite::Sin::Identifier]
def sin
# Returns the PIN component.
#
# @return [Sashite::Pin::Identifier]
def pin
# Returns true if sin.side equals pin.side (Native relationship).
#
# @return [Boolean]
def native?
# Returns true if sin.side differs from pin.side (Derived relationship).
#
# @return [Boolean]
def derived?
# Returns the QPI string representation.
#
# @return [String]
def to_s
end
Parsing
# Parses a QPI string into an Identifier.
# Raises ArgumentError if the string is not valid.
#
# @param string [String] QPI string
# @return [Identifier]
# @raise [ArgumentError] if invalid
def Sashite::Qpi.parse(string)
Validation
# Reports whether string is a valid QPI.
#
# @param string [String] QPI string
# @return [Boolean]
def Sashite::Qpi.valid?(string)
Transformations
# Component replacement (return new Identifier)
def with_sin(new_sin) # => Identifier with different SIN
def with_pin(new_pin) # => Identifier with different PIN
# Flip transformation (modifies PIN only)
def flip # => Identifier with PIN side flipped
# Native/Derived transformations (modify PIN only)
def native # => Identifier with PIN case aligned to SIN case
def derive # => Identifier with PIN case opposite to SIN case
Errors
All parsing and validation errors raise ArgumentError with descriptive messages:
| Message | Cause |
|---|---|
"empty input" |
String length is 0 |
"missing colon separator" |
No : found in string |
"missing SIN component" |
Nothing before : |
"missing PIN component" |
Nothing after : |
"invalid SIN component: ..." |
SIN parsing failed |
"invalid PIN component: ..." |
PIN parsing failed |
Piece Identity Mapping
QPI encodes complete Piece Identity as defined in the Glossary:
| Piece Attribute | QPI Access | Encoding |
|---|---|---|
| Piece Style | qpi.sin.abbr |
SIN letter (case-insensitive identity) |
| Piece Name | qpi.pin.abbr |
PIN letter (case-insensitive identity) |
| Piece Side | qpi.pin.side |
PIN letter case (uppercase = first, lowercase = second) |
| Piece State | qpi.pin.state |
PIN modifier (+ = enhanced, - = diminished) |
| Terminal Status | qpi.pin.terminal? |
PIN marker (^ = terminal) |
Additionally, QPI provides a Native/Derived relationship via native?, derived?, native, and derive.
Design Principles
- Pure composition: QPI composes SIN and PIN without reimplementing features
- Minimal API: Core methods (
sin,pin,native?,derived?,native,derive,to_s) plus transformations - Component transparency: Access components directly, no wrapper methods
- QPI-specific conveniences:
flip,native,derive(operations that modify PIN only, per spec) - Immutable identifiers: Frozen instances prevent mutation
- Ruby idioms:
valid?predicate,to_sconversion,ArgumentErrorfor invalid input - No duplication: Delegates to
sashite-sinandsashite-pin
Related Specifications
- Game Protocol — Conceptual foundation
- QPI Specification — Official specification
- QPI Examples — Usage examples
- SIN Specification — Style component
- PIN Specification — Piece component
License
Available as open source under the Apache License 2.0.