QPI (Qualified Piece Identifier) implementation for the Ruby language.
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.
# In your Gemfile
gem "sashite-qpi"
Or install manually:
gem install sashite-qpi
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"
# 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
# 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
# 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"
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?("+") }
# 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)
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
<sin>:<pin>
<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"
/\A([A-Z]:[-+]?[A-Z]|[a-z]:[-+]?[a-z])\z/
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 |
Sashite::Qpi.valid?(qpi_string)
- Check if string is valid QPI notationSashite::Qpi.parse(qpi_string)
- Parse QPI string into Identifier objectSashite::Qpi.identifier(sin, pin)
- Create identifier instance from components
Sashite::Qpi::Identifier.new(sin, pin)
- Create identifier from SIN and PIN stringsSashite::Qpi::Identifier.parse(qpi_string)
- Parse QPI string
#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)
#sin_component
- Get parsed SIN identifier object#pin_component
- Get parsed PIN identifier object
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)
#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
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
#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
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?("+") }
# 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]
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
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", ... }
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
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
- 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
QPI is particularly useful for:
- Multi-Style Environments: Positions involving pieces from multiple style traditions
- Cross-Style Games: Games combining elements from different piece traditions
- Component Analysis: Extracting and analyzing style and piece information separately using
to_sin
andto_pin
- Game Engine Development: Engines needing unambiguous piece identification
- Database Systems: Storing game data without naming conflicts
- Hybrid Analysis: Comparing strategic elements across different traditions
- Functional Programming: Immutable game state representations
- Format Conversion: Converting between QPI and individual SIN/PIN representations
- Validation Systems: Leveraging modular validation for robust error checking
QPI builds upon two foundational specifications:
- SIN (Style Identifier Notation): Style identification with player assignment
- PIN (Piece Identifier Notation): Piece type, side, and state representation
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
- 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
andto_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
QPI follows a modular validation approach that leverages the underlying component libraries:
- Component Splitting: QPI strings are split on the colon separator
- Individual Validation: Each component is validated using its specific pattern:
- SIN component:
Sashite::Sin::Identifier::SIN_PATTERN
- PIN component:
Sashite::Pin::Identifier::PIN_PATTERN
- SIN component:
- 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
QPI follows the same internal representation conventions as its constituent libraries:
- Style Letters: Stored as symbols with case preserved (
:C
,:c
,:S
,:s
) - Piece Types: Always stored as uppercase symbols (
:K
,:P
) - Display Logic: Case is computed from
side
during string rendering
This ensures predictable behavior and consistency across the entire Sashité ecosystem.
- QPI Specification v1.0.0 - Complete technical specification
- QPI Examples - Practical implementation examples
- SIN Specification v1.0.0 - Style identification component
- PIN Specification v1.0.0 - Piece identification component
- EPIN Specification v1.0.0 - Alternative with derivation markers
- Sashité Protocol - Conceptual foundation for abstract strategy board games
- Official QPI Specification v1.0.0
- QPI Examples Documentation
- Sashité Protocol Foundation
- API Documentation
# 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
- Fork the repository
- Create a feature branch (
git checkout -b feature/new-feature
) - Add tests for your changes
- Ensure all tests pass (
ruby test.rb
) - Commit your changes (
git commit -am 'Add new feature'
) - Push to the branch (
git push origin feature/new-feature
) - Create a Pull Request
Available as open source under the MIT License.
Maintained by Sashité — promoting chess variants and sharing the beauty of board game cultures.