0.0
No release in over 3 years
Low commit activity in last 3 years
QPI (Qualified Piece Identifier) implementation for Ruby. Provides a rule-agnostic format for complete piece identification in abstract strategy board games by combining SIN and PIN primitives, with Native/Derived relationship support.
2005
2006
2007
2008
2009
2010
2011
2012
2013
2014
2015
2016
2017
2018
2019
2020
2021
2022
2023
2024
2025
2026
 Dependencies

Runtime

~> 4.1.0
~> 3.1.0
 Project Readme

qpi.rb

Version Yard documentation CI License

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_s conversion, ArgumentError for invalid input
  • No duplication: Delegates to sashite-sin and sashite-pin

Related Specifications

License

Available as open source under the Apache License 2.0.