Skip to content
/ qpi.rb Public

QPI (Qualified Piece Identifier) implementation for Ruby with immutable identifier objects.

License

Notifications You must be signed in to change notification settings

sashite/qpi.rb

Repository files navigation

Qpi.rb

Version Yard documentation Ruby License

QPI (Qualified Piece Identifier) implementation for the Ruby language.

What is QPI?

QPI (Qualified Piece Identifier) provides a rule-agnostic format for identifying game pieces in abstract strategy board games by combining Style Identifier Notation (SIN) and Piece Identifier Notation (PIN) with a colon separator.

QPI represents all four fundamental piece attributes from the Sashité Protocol:

  • Type → PIN component (ASCII letter choice)
  • Side → PIN component (letter case)
  • State → PIN component (optional prefix modifier)
  • Style → SIN component (style identifier)

Unlike Extended Piece Identifier Notation (EPIN) which uses derivation markers, QPI explicitly names the style for unambiguous identification.

This gem implements the QPI Specification v1.0.0, providing a modern Ruby interface with immutable identifier objects and functional programming principles.

Installation

# In your Gemfile
gem "sashite-qpi"

Or install manually:

gem install sashite-qpi

Usage

Basic Operations

require "sashite/qpi"

# Parse QPI strings into identifier objects
identifier = Sashite::Qpi.parse("C:K")         # => #<Qpi::Identifier sin=:C pin=:K>
identifier.to_s                                # => "C:K"
identifier.sin                                 # => :C
identifier.pin                                 # => :K
identifier.style                               # => :C
identifier.type                                # => :K
identifier.side                                # => :first
identifier.state                               # => :normal

# Create identifiers directly
identifier = Sashite::Qpi.identifier("C", "K")              # => #<Qpi::Identifier sin=:C pin=:K>
identifier = Sashite::Qpi::Identifier.new("S", "+R")        # => #<Qpi::Identifier sin=:S pin=:+R>

# Validate QPI strings
Sashite::Qpi.valid?("C:K")                    # => true
Sashite::Qpi.valid?("s:+p")                   # => true
Sashite::Qpi.valid?("invalid")                # => false
Sashite::Qpi.valid?("C:k")                    # => false (semantic mismatch)

# Access all four piece attributes
chess_king = Sashite::Qpi.parse("C:K")
chess_king.type                                # => :K
chess_king.side                                # => :first
chess_king.state                               # => :normal
chess_king.style                               # => :C

shogi_promoted = Sashite::Qpi.parse("s:+r")
shogi_promoted.type                            # => :R
shogi_promoted.side                            # => :second
shogi_promoted.state                           # => :enhanced
shogi_promoted.style                           # => :s

# Extract individual components
chess_king.to_sin                              # => "C"
chess_king.to_pin                              # => "K"
shogi_promoted.to_sin                          # => "s"
shogi_promoted.to_pin                          # => "+r"

Single-Style Games

# Western Chess
white_king = Sashite::Qpi.parse("C:K")        # Chess king, first player
black_queen = Sashite::Qpi.parse("c:q")       # Chess queen, second player
castling_rook = Sashite::Qpi.parse("C:+R")    # Chess rook, castling eligible

# Japanese Shōgi
sente_king = Sashite::Qpi.parse("S:K")        # Shōgi king, sente
gote_promoted_rook = Sashite::Qpi.parse("s:+r") # Shōgi dragon king, gote
promoted_pawn = Sashite::Qpi.parse("S:+P")    # Shōgi tokin, sente

# Chinese Xiangqi
red_general = Sashite::Qpi.parse("X:G")       # Xiangqi general, red
black_cannon = Sashite::Qpi.parse("x:c")      # Xiangqi cannon, black

Cross-Style Scenarios

# Chess vs. Shōgi match
chess_player = Sashite::Qpi.parse("C:K")      # First player uses Chess
shogi_player = Sashite::Qpi.parse("s:k")      # Second player uses Shōgi

# Ōgi vs. Makruk match
ogi_king = Sashite::Qpi.parse("O:K")          # First player uses Ōgi
makruk_queen = Sashite::Qpi.parse("m:m")      # Second player uses Makruk

# Verify cross-style combinations
chess_player.cross_style?(shogi_player)       # => true
chess_player.same_style?(shogi_player)        # => false

Identifier Transformations

# All transformations return new immutable instances
identifier = Sashite::Qpi.parse("C:K")

# Transform PIN component (piece attributes)
enhanced = identifier.enhance                  # => "C:+K"
different_type = identifier.with_type(:Q)     # => "C:Q"
flipped_side = identifier.flip_side           # => "c:k"

# Transform SIN component (style)
different_style = identifier.with_style(:S)   # => "S:K"
flipped_style = identifier.flip_style         # => "c:K"

# Chain transformations
result = identifier.flip_style.enhance.with_type(:Q)  # => "c:+Q"

# Original identifier remains unchanged
identifier.to_s                               # => "C:K"

Component Extraction

QPI provides methods to extract individual notation components:

# Extract and manipulate components
identifier = Sashite::Qpi.parse("S:+P")

# Component extraction
style_str = identifier.to_sin               # => "S"
piece_str = identifier.to_pin               # => "+P"

# Reconstruct from components
reconstructed = "#{style_str}:#{piece_str}" # => "S:+P"

# Cross-component analysis
identifiers = [
  Sashite::Qpi.parse("C:K"),
  Sashite::Qpi.parse("S:K"),
  Sashite::Qpi.parse("c:k")
]

# Group by style component
by_style = identifiers.group_by(&:to_sin)
# => {"C" => [...], "S" => [...], "c" => [...]}

# Group by piece component
by_piece = identifiers.group_by(&:to_pin)
# => {"K" => [...], "k" => [...]}

# Component-based filtering
uppercase_styles = identifiers.select { |i| i.to_sin == i.to_sin.upcase }
enhanced_pieces = identifiers.select { |i| i.to_pin.start_with?("+") }

Validation and Constraints

# Semantic validation - style and side must match
Sashite::Qpi.valid?("C:K")                    # => true (first player Chess with first player piece)
Sashite::Qpi.valid?("c:k")                    # => true (second player Chess with second player piece)
Sashite::Qpi.valid?("C:k")                    # => false (first player Chess with second player piece)
Sashite::Qpi.valid?("c:K")                    # => false (second player Chess with first player piece)

# Syntactic validation
Sashite::Qpi.valid?("C:")                     # => false (missing PIN)
Sashite::Qpi.valid?(":K")                     # => false (missing SIN)
Sashite::Qpi.valid?("CC:K")                   # => false (invalid SIN)
Sashite::Qpi.valid?("C:KK")                   # => false (invalid PIN)

Modular Validation Architecture

QPI validation delegates to the underlying components for maximum consistency:

# QPI validation follows a three-step process:
# 1. Component Splitting: QPI strings are split on the colon separator
# 2. Individual Validation: Each component validated using its specific pattern:
#    - SIN component: Uses Sashite::Sin::Identifier::SIN_PATTERN
#    - PIN component: Uses Sashite::Pin::Identifier::PIN_PATTERN
# 3. Cross-Reference Constraint: Ensures matching player assignment

# This modular approach:
# - Avoids Code Duplication: No separate QPI regex needed
# - Maintains Consistency: Inherits validation improvements from SIN and PIN
# - Provides Clear Error Messages: Component-specific failures are informative
# - Enables Modularity: Each library maintains its own validation logic

def demonstrate_validation_delegation
  qpi_string = "C:+K"

  # QPI splits and delegates validation
  sin_part, pin_part = qpi_string.split(':')

  sin_valid = Sashite::Sin.valid?(sin_part)    # => true
  pin_valid = Sashite::Pin.valid?(pin_part)    # => true

  # Plus semantic consistency check
  sin_side = sin_part == sin_part.upcase ? :first : :second
  pin_side = pin_part.match(/[A-Z]/) ? :first : :second
  sides_match = sin_side == pin_side           # => true

  overall_valid = sin_valid && pin_valid && sides_match

  puts "SIN valid: #{sin_valid}, PIN valid: #{pin_valid}, Sides match: #{sides_match}"
  puts "Overall valid: #{overall_valid}"
end

Format Specification

Structure

<sin>:<pin>

Grammar (BNF)

<qpi> ::= <uppercase-qpi> | <lowercase-qpi>

<uppercase-qpi> ::= <uppercase-letter> <colon> <uppercase-pin>
<lowercase-qpi> ::= <lowercase-letter> <colon> <lowercase-pin>

<colon> ::= ":"

<uppercase-pin> ::= <uppercase-letter> | <state-modifier> <uppercase-letter>
<lowercase-pin> ::= <lowercase-letter> | <state-modifier> <lowercase-letter>

<state-modifier> ::= "+" | "-"
<uppercase-letter> ::= "A" | "B" | "C" | ... | "Z"
<lowercase-letter> ::= "a" | "b" | "c" | ... | "z"

Regular Expression

/\A([A-Z]:[-+]?[A-Z]|[a-z]:[-+]?[a-z])\z/

Component Mapping

Piece Attribute QPI Encoding Examples
Type PIN letter choice C:K = King, C:P = Pawn
Side PIN case C:K = First player, c:k = Second player
State PIN prefix modifier S:+P = Enhanced, C:-P = Diminished
Style SIN identifier C:K = Chess style, S:K = Shōgi style

API Reference

Main Module Methods

  • Sashite::Qpi.valid?(qpi_string) - Check if string is valid QPI notation
  • Sashite::Qpi.parse(qpi_string) - Parse QPI string into Identifier object
  • Sashite::Qpi.identifier(sin, pin) - Create identifier instance from components

Identifier Class

Creation and Parsing

  • Sashite::Qpi::Identifier.new(sin, pin) - Create identifier from SIN and PIN strings
  • Sashite::Qpi::Identifier.parse(qpi_string) - Parse QPI string

Attribute Access

  • #sin - Get SIN component (style identifier as symbol)
  • #pin - Get PIN component (piece identifier as symbol)
  • #style - Get style (alias for #sin)
  • #type - Get piece type (from PIN component)
  • #side - Get player side (from PIN component)
  • #state - Get piece state (from PIN component)
  • #to_s - Convert to QPI string representation
  • #to_sin - Convert to SIN string representation (style component only)
  • #to_pin - Convert to PIN string representation (piece component only)

Component Access

  • #sin_component - Get parsed SIN identifier object
  • #pin_component - Get parsed PIN identifier object

Component Extraction

The to_sin and to_pin methods allow extraction of individual notation components:

identifier = Sashite::Qpi.parse("C:+K")

# Full QPI representation
identifier.to_s # => "C:+K"

# Individual components
identifier.to_sin    # => "C"  (style component)
identifier.to_pin    # => "+K" (piece component)

# Component transformation example
flipped = identifier.flip
flipped.to_s         # => "c:+k"
flipped.to_sin       # => "c"  (lowercase for second player)
flipped.to_pin       # => "+k" (lowercase with state preserved)

# State manipulation example
normalized = identifier.normalize
normalized.to_s      # => "C:K"
normalized.to_sin    # => "C"  (style unchanged)
normalized.to_pin    # => "K"  (state modifier removed)

Validation Queries

  • #valid? - Check if identifier has semantic consistency
  • #same_style?(other) - Check if same style
  • #cross_style?(other) - Check if different styles
  • #same_side?(other) - Check if same side
  • #same_type?(other) - Check if same type
  • #same_state?(other) - Check if same state

Transformations (immutable - return new instances)

PIN Component Transformations:

  • #enhance - Create enhanced version
  • #diminish - Create diminished version
  • #normalize - Remove all state modifiers
  • #with_type(new_type) - Change piece type
  • #flip_side - Switch player side

SIN Component Transformations:

  • #with_style(new_style) - Change style
  • #flip_style - Switch style player assignment

Combined Transformations:

  • #flip - Flip both style and side assignments
  • #with_components(sin, pin) - Create with different components

State Queries

  • #normal? - Check if normal state
  • #enhanced? - Check if enhanced state
  • #diminished? - Check if diminished state
  • #first_player? - Check if first player piece
  • #second_player? - Check if second player piece

Advanced Usage

Component Extraction and Manipulation

The to_sin and to_pin methods enable powerful component-based operations:

# Extract and manipulate components
identifier = Sashite::Qpi.parse("S:+P")

# Component extraction
style_str = identifier.to_sin    # => "S"
piece_str = identifier.to_pin    # => "+P"

# Reconstruct from components
reconstructed = "#{style_str}:#{piece_str}" # => "S:+P"

# Cross-component analysis
identifiers = [
  Sashite::Qpi.parse("C:K"),
  Sashite::Qpi.parse("S:K"),
  Sashite::Qpi.parse("c:k")
]

# Group by style component
by_style = identifiers.group_by(&:to_sin)
# => {"C" => [...], "S" => [...], "c" => [...]}

# Group by piece component
by_piece = identifiers.group_by(&:to_pin)
# => {"K" => [...], "k" => [...]}

# Component-based filtering
uppercase_styles = identifiers.select { |i| i.to_sin == i.to_sin.upcase }
enhanced_pieces = identifiers.select { |i| i.to_pin.start_with?("+") }

Component Reconstruction Patterns

# Template-based reconstruction
def apply_style_template(identifiers, new_style)
  identifiers.map do |identifier|
    pin_part = identifier.to_pin
    side = identifier.side

    # Apply new style while preserving piece and side
    new_style_str = side == :first ? new_style.to_s.upcase : new_style.to_s.downcase
    Sashite::Qpi.parse("#{new_style_str}:#{pin_part}")
  end
end

# Convert chess pieces to shōgi style
chess_pieces = [
  Sashite::Qpi.parse("C:K"),
  Sashite::Qpi.parse("c:+q")
]

shogi_pieces = apply_style_template(chess_pieces, :S)
# => [S:K, s:+q]

# Component swapping
def swap_components(identifier1, identifier2)
  [
    Sashite::Qpi.parse("#{identifier1.to_sin}:#{identifier2.to_pin}"),
    Sashite::Qpi.parse("#{identifier2.to_sin}:#{identifier1.to_pin}")
  ]
end

chess_king = Sashite::Qpi.parse("C:K")
shogi_pawn = Sashite::Qpi.parse("s:p")

swapped = swap_components(chess_king, shogi_pawn)
# => [C:p, s:K]

Cross-Style Game Management

class CrossStyleMatch
  def initialize
    @pieces = {}
  end

  def place(square, qpi_string)
    identifier = Sashite::Qpi.parse(qpi_string)
    @pieces[square] = identifier
  end

  def pieces_by_style(style)
    @pieces.select { |_, piece| piece.style.to_s.upcase == style.to_s.upcase }
  end

  def cross_style_pieces
    styles = @pieces.values.map { |p| p.style.to_s.upcase }.uniq
    styles.size > 1
  end

  def promote(square, new_type = :Q)
    piece = @pieces[square]
    return nil unless piece&.normal?

    @pieces[square] = piece.with_type(new_type).enhance
  end
end

# Usage
match = CrossStyleMatch.new
match.place("e1", "C:K")    # Chess king
match.place("e8", "s:k")    # Shōgi king
match.place("a1", "C:R")    # Chess rook
match.place("a9", "s:l")    # Shōgi lance

chess_pieces = match.pieces_by_style(:C)
shogi_pieces = match.pieces_by_style(:S)

puts "Cross-style match: #{match.cross_style_pieces}" # => true
puts "Chess pieces: #{chess_pieces.size}"             # => 2
puts "Shōgi pieces: #{shogi_pieces.size}"             # => 2

Capture Mechanics Simulation

def simulate_capture(attacker_qpi, defender_qpi, game_rules)
  attacker = Sashite::Qpi.parse(attacker_qpi)
  defender = Sashite::Qpi.parse(defender_qpi)

  case game_rules
  when :chess
    # Chess: captured piece becomes inactive
    captured = defender  # Piece retains identity but becomes inactive

  when :shogi
    # Shōgi: captured piece changes side and loses promotion
    captured = defender.flip_side.normalize

  when :ogi_transformation
    # Ōgi: captured piece transforms completely
    captured = attacker.with_type(:P).normalize  # Becomes pawn of capturing side

  else
    captured = defender
  end

  {
    original: defender.to_s,
    captured: captured.to_s,
    attacker_style: attacker.style,
    transformation: defender.to_s != captured.to_s
  }
end

# Chess capture
chess_result = simulate_capture("C:Q", "c:p", :chess)
puts chess_result  # => { original: "c:p", captured: "c:p", ... }

# Shōgi capture
shogi_result = simulate_capture("S:R", "s:+p", :shogi)
puts shogi_result  # => { original: "s:+p", captured: "S:P", ... }

# Ōgi transformation
ogi_result = simulate_capture("O:K", "c:q", :ogi_transformation)
puts ogi_result    # => { original: "c:q", captured: "O:P", ... }

Piece Analysis

def analyze_position(qpi_strings)
  pieces = qpi_strings.map { |qpi| Sashite::Qpi.parse(qpi) }

  {
    total: pieces.size,
    by_style: pieces.group_by(&:style),
    by_side: pieces.group_by(&:side),
    by_type: pieces.group_by(&:type),
    by_state: pieces.group_by(&:state),
    cross_style: pieces.map(&:style).uniq.size > 1,
    promoted: pieces.count(&:enhanced?),
    weakened: pieces.count(&:diminished?)
  }
end

position = %w[C:K C:Q C:+R c:k c:q s:+r S:G s:+p]
analysis = analyze_position(position)

puts "Cross-style position: #{analysis[:cross_style]}"  # => true
puts "Styles present: #{analysis[:by_style].keys}"     # => [:C, :c, :s, :S]
puts "Promoted pieces: #{analysis[:promoted]}"         # => 3

Validation Patterns

class QpiValidator
  def self.validate_match_consistency(qpi_strings)
    pieces = qpi_strings.map { |qpi| Sashite::Qpi.parse(qpi) }
    errors = []

    # Check for semantic consistency
    pieces.each do |piece|
      unless piece.valid?
        errors << "Invalid piece: #{piece}"
      end
    end

    # Check for duplicate pieces at same location (if positions provided)
    # Check for impossible combinations, etc.

    errors.empty? ? :valid : errors
  end

  def self.cross_style_rules_check(qpi1, qpi2)
    piece1 = Sashite::Qpi.parse(qpi1)
    piece2 = Sashite::Qpi.parse(qpi2)

    {
      same_style: piece1.same_style?(piece2),
      cross_style: piece1.cross_style?(piece2),
      compatible_interaction: compatible_styles?(piece1.style, piece2.style)
    }
  end

  private

  def self.compatible_styles?(style1, style2)
    # Implementation depends on game rules
    # This is a placeholder for actual compatibility logic
    true
  end
end

System Constraints

  • Semantic Consistency: SIN and PIN components must have matching player assignments
  • Component Independence: Each component validated according to its own specification
  • Cross-Style Support: Enables multi-tradition gaming environments
  • Complete Attribute Coverage: All four fundamental piece attributes represented

Use Cases

QPI is particularly useful for:

  1. Multi-Style Environments: Positions involving pieces from multiple style traditions
  2. Cross-Style Games: Games combining elements from different piece traditions
  3. Component Analysis: Extracting and analyzing style and piece information separately using to_sin and to_pin
  4. Game Engine Development: Engines needing unambiguous piece identification
  5. Database Systems: Storing game data without naming conflicts
  6. Hybrid Analysis: Comparing strategic elements across different traditions
  7. Functional Programming: Immutable game state representations
  8. Format Conversion: Converting between QPI and individual SIN/PIN representations
  9. Validation Systems: Leveraging modular validation for robust error checking

Component Dependencies

QPI builds upon two foundational specifications:

Both dependencies are automatically managed:

# Dependencies are resolved automatically
qpi = Sashite::Qpi.parse("C:+K")

# Access underlying components
sin_component = qpi.sin_component  # => Sashite::Sin::Identifier instance
pin_component = qpi.pin_component  # => Sashite::Pin::Identifier instance

# Component methods are available
sin_component.first_player?        # => true
pin_component.enhanced?            # => true

Design Properties

  • Rule-Agnostic: Independent of specific game mechanics
  • Complete Identification: Explicit representation of all four piece attributes
  • Cross-Style Support: Enables multi-tradition gaming environments
  • Component Clarity: Clear separation between style context and piece identity
  • Component Extraction: Individual SIN and PIN components accessible via to_sin and to_pin
  • Semantic Validation: Ensures consistency between style and piece ownership
  • Modular Validation: Delegates validation to underlying components for consistency
  • Immutable: All instances are frozen and transformations return new objects
  • Functional: Pure functions with no side effects

Implementation Notes

Validation Architecture

QPI follows a modular validation approach that leverages the underlying component libraries:

  1. Component Splitting: QPI strings are split on the colon separator
  2. Individual Validation: Each component is validated using its specific pattern:
    • SIN component: Sashite::Sin::Identifier::SIN_PATTERN
    • PIN component: Sashite::Pin::Identifier::PIN_PATTERN
  3. Cross-Reference Constraint: Additional validation ensures matching player assignment between components

This approach:

  • Avoids Code Duplication: No need to maintain a separate QPI regex
  • Maintains Consistency: Automatically inherits validation improvements from SIN and PIN
  • Provides Clear Error Messages: Component-specific validation failures are more informative
  • Enables Modularity: Each library maintains its own validation logic
# Example of validation delegation in practice
qpi_string = "C:+K"

# QPI internally splits and validates each component
sin_part, pin_part = qpi_string.split(':')

# Delegates to component validation
sin_valid = Sashite::Sin.valid?(sin_part)    # => true
pin_valid = Sashite::Pin.valid?(pin_part)    # => true

# Plus semantic consistency check
sin_identifier = Sashite::Sin.parse(sin_part)
pin_identifier = Sashite::Pin.parse(pin_part)
sides_match = sin_identifier.side == pin_identifier.side  # => true

overall_valid = sin_valid && pin_valid && sides_match

Component Handling Convention

QPI follows the same internal representation conventions as its constituent libraries:

  1. Style Letters: Stored as symbols with case preserved (:C, :c, :S, :s)
  2. Piece Types: Always stored as uppercase symbols (:K, :P)
  3. Display Logic: Case is computed from side during string rendering

This ensures predictable behavior and consistency across the entire Sashité ecosystem.

Related Specifications

Documentation

Development

# Clone the repository
git clone https://github.com/sashite/qpi.rb.git
cd qpi.rb

# Install dependencies
bundle install

# Run tests
ruby test.rb

# Generate documentation
yard doc

Contributing

  1. Fork the repository
  2. Create a feature branch (git checkout -b feature/new-feature)
  3. Add tests for your changes
  4. Ensure all tests pass (ruby test.rb)
  5. Commit your changes (git commit -am 'Add new feature')
  6. Push to the branch (git push origin feature/new-feature)
  7. Create a Pull Request

License

Available as open source under the MIT License.

About

Maintained by Sashité — promoting chess variants and sharing the beauty of board game cultures.

About

QPI (Qualified Piece Identifier) implementation for Ruby with immutable identifier objects.

Topics

Resources

License

Code of conduct

Stars

Watchers

Forks