Skip to content
/ qpi.rb Public

QPI (Qualified Piece Identifier) implementation for Ruby - board game piece identification.

License

Notifications You must be signed in to change notification settings

sashite/qpi.rb

Repository files navigation

Gan.rb

Version Yard documentation Ruby License

GAN (General Actor Notation) implementation for the Ruby language.

What is GAN?

GAN (General Actor Notation) provides a rule-agnostic format for identifying game actors in abstract strategy board games by combining Style Name Notation (SNN) and Piece Identifier Notation (PIN) with a colon separator and consistent case encoding.

GAN represents all four fundamental piece attributes from the Game Protocol:

  • Type → PIN component (ASCII letter choice)
  • Side → Consistent case encoding across both SNN and PIN components
  • State → PIN component (optional prefix modifier)
  • Style → SNN component (explicit style identifier)

This gem implements the GAN Specification v1.0.0, providing a modern Ruby interface with immutable actor objects and functional programming principles built upon the sashite-snn and sashite-pin gems.

Installation

# In your Gemfile
gem "sashite-gan"

Or install manually:

gem install sashite-gan

Usage

require "sashite/gan"

# Parse GAN strings into actor objects
actor = Sashite::Gan.parse("CHESS:K")          # => #<Gan::Actor name=:Chess type=:K side=:first state=:normal>
actor.to_s                                     # => "CHESS:K"
actor.name                                     # => :Chess
actor.type                                     # => :K
actor.side                                     # => :first
actor.state                                    # => :normal

# Extract individual components
actor.to_snn                                   # => "CHESS"
actor.to_pin                                   # => "K"

# Create actors directly
actor = Sashite::Gan.actor(:Chess, :K, :first, :normal) # => #<Gan::Actor name=:Chess type=:K side=:first state=:normal>
actor = Sashite::Gan::Actor.new(:Shogi, :P, :second, :enhanced) # => #<Gan::Actor name=:Shogi type=:P side=:second state=:enhanced>

# Validate GAN strings
Sashite::Gan.valid?("CHESS:K")                 # => true
Sashite::Gan.valid?("shogi:+p")                # => true
Sashite::Gan.valid?("Chess:K")                 # => false (mixed case)
Sashite::Gan.valid?("CHESS")                   # => false (missing piece)

# Class-level validation (same as module method)
Sashite::Gan::Actor.valid?("CHESS:K")          # => true
Sashite::Gan::Actor.valid?("chess:k")          # => true
Sashite::Gan::Actor.valid?("Chess:K")          # => false (mixed case)
Sashite::Gan::Actor.valid?("CHESS:k")          # => false (case mismatch)

# State manipulation (returns new immutable instances)
enhanced = actor.enhance                       # => #<Gan::Actor name=:Chess type=:K side=:first state=:enhanced>
enhanced.to_s                                  # => "CHESS:+K"
enhanced.to_pin                                # => "+K"
diminished = actor.diminish                    # => #<Gan::Actor name=:Chess type=:K side=:first state=:diminished>
diminished.to_s                                # => "CHESS:-K"
diminished.to_pin                              # => "-K"

# Side manipulation
flipped = actor.flip                           # => #<Gan::Actor name=:Chess type=:K side=:second state=:normal>
flipped.to_s                                   # => "chess:k"
flipped.to_snn                                 # => "chess"
flipped.to_pin                                 # => "k"

# Style manipulation
shogi_actor = actor.with_name(:Shogi)          # => #<Gan::Actor name=:Shogi type=:K side=:first state=:normal>
shogi_actor.to_s                               # => "SHOGI:K"
shogi_actor.to_snn                             # => "SHOGI"

# Type manipulation
queen = actor.with_type(:Q)                    # => #<Gan::Actor name=:Chess type=:Q side=:first state=:normal>
queen.to_s                                     # => "CHESS:Q"
queen.to_pin                                   # => "Q"

# State queries
actor.normal?                                  # => true
enhanced.enhanced?                             # => true
diminished.diminished?                         # => true

# Side queries
actor.first_player?                            # => true
flipped.second_player?                         # => true

# Component comparison
chess1 = Sashite::Gan.parse("CHESS:K")
chess2 = Sashite::Gan.parse("chess:k")
shogi = Sashite::Gan.parse("SHOGI:K")

chess1.same_name?(chess2)                      # => true (both chess)
chess1.same_side?(shogi)                       # => true (both first player)
chess1.same_type?(chess2)                      # => true (both kings)
chess1.same_name?(shogi)                       # => false (different styles)

# Functional transformations can be chained
black_promoted = Sashite::Gan.parse("CHESS:P").flip.enhance
black_promoted.to_s                            # => "chess:+p"
black_promoted.to_snn                          # => "chess"
black_promoted.to_pin                          # => "+p"

Format Specification

Structure

<snn>:<pin>

Components

  • SNN Component (Style Name Notation): Style identifier with case-based side encoding
    • Uppercase: First player styles (CHESS, SHOGI, XIANGQI)
    • Lowercase: Second player styles (chess, shogi, xiangqi)
  • Colon Separator: Literal : character
  • PIN Component (Piece Identifier Notation): Piece with optional state and case-based ownership
    • Letter case matches SNN case (case consistency requirement)
    • Optional state prefix: + (enhanced), - (diminished)

Case Consistency Requirement

Critical Rule: The case of the SNN component must match the case of the PIN component:

# ✅ Valid combinations
Sashite::Gan.valid?("CHESS:K")     # => true (both uppercase = first player)
Sashite::Gan.valid?("chess:k")     # => true (both lowercase = second player)
Sashite::Gan.valid?("SHOGI:+R")    # => true (both uppercase = first player)
Sashite::Gan.valid?("xiangqi:-g")  # => true (both lowercase = second player)

# ❌ Invalid combinations
Sashite::Gan.valid?("CHESS:k")     # => false (case mismatch)
Sashite::Gan.valid?("chess:K")     # => false (case mismatch)
Sashite::Gan.valid?("SHOGI:+r")    # => false (case mismatch)

Validation Architecture

GAN validation delegates to the underlying components for maximum consistency:

  • SNN validation: Uses Sashite::Snn::Style::SNN_PATTERN for style validation
  • PIN validation: Uses Sashite::Pin::Piece::PIN_PATTERN for piece validation
  • Case consistency: Ensures matching case between SNN and PIN components

This modular approach avoids code duplication and ensures that GAN validation automatically inherits improvements from the underlying SNN and PIN libraries.

Examples

  • CHESS:K - First player chess king
  • chess:k - Second player chess king
  • SHOGI:+P - First player enhanced shōgi pawn
  • xiangqi:-g - Second player diminished xiangqi general

Game Examples

Traditional Same-Style Games

In traditional games where both players use the same piece style:

# Chess pieces
white_king = Sashite::Gan.parse("CHESS:K")
black_king = Sashite::Gan.parse("chess:k")
white_queen = Sashite::Gan.parse("CHESS:Q")
black_queen = Sashite::Gan.parse("chess:q")

# Shōgi pieces
sente_king = Sashite::Gan.parse("SHOGI:K")
gote_king = Sashite::Gan.parse("shogi:k")
sente_gold = Sashite::Gan.parse("SHOGI:G")
gote_gold = Sashite::Gan.parse("shogi:g")

# Enhanced states for special conditions
castling_rook = Sashite::Gan.parse("CHESS:+R") # Castling-eligible rook
vulnerable_pawn = Sashite::Gan.parse("CHESS:-P")   # En passant vulnerable pawn
promoted_pawn = Sashite::Gan.parse("SHOGI:+P")     # Tokin (promoted pawn)

Cross-Style Games

GAN's explicit style naming enables games where players use different piece traditions:

# Chess vs Shōgi
chess_king = Sashite::Gan.parse("CHESS:K")
shogi_king = Sashite::Gan.parse("shogi:k")

# Makruk vs Xiangqi
makruk_queen = Sashite::Gan.parse("MAKRUK:M") # Met (Makruk queen)
xiangqi_general = Sashite::Gan.parse("xiangqi:g") # Xiangqi general

# Multi-tradition setup
def create_cross_style_game
  [
    Sashite::Gan.parse("CHESS:K"),     # First player uses chess
    Sashite::Gan.parse("CHESS:Q"),
    Sashite::Gan.parse("shogi:k"),     # Second player uses shōgi
    Sashite::Gan.parse("shogi:g")
  ]
end

Capture Mechanics Examples

GAN can represent the different capture mechanics described in the specification:

# Chess vs Chess (traditional capture)
def chess_capture(captured_piece)
  # In chess, captured pieces retain their identity but become inactive
  captured_piece # GAN remains unchanged: chess:p stays chess:p
end

# Shōgi vs Shōgi (side-changing capture)
def shogi_capture(captured_piece)
  # In shōgi, captured pieces change sides and lose promotions
  captured_piece.flip.normalize # shogi:+p becomes SHOGI:P
end

# Cross-style capture (style transformation)
def cross_style_capture(captured_piece, capturing_style)
  # Captured piece transforms to capturing player's style
  captured_piece.flip.with_name(capturing_style).normalize
  # chess:q captured by Ōgi player becomes OGI:P
end

API Reference

Main Module Methods

  • Sashite::Gan.valid?(gan_string) - Check if string is valid GAN notation
  • Sashite::Gan.parse(gan_string) - Parse GAN string into Actor object
  • Sashite::Gan.actor(name, type, side, state = :normal) - Create actor instance directly

Actor Class

Creation and Parsing

  • Sashite::Gan::Actor.new(name, type, side, state = :normal) - Create actor instance
  • Sashite::Gan::Actor.parse(gan_string) - Parse GAN string (same as module method)
  • Sashite::Gan::Actor.valid?(gan_string) - Validate GAN string (class method)

Attribute Access

  • #name - Get style name (symbol with proper capitalization)
  • #type - Get piece type (symbol :A to :Z, always uppercase)
  • #side - Get player side (:first or :second)
  • #state - Get piece state (:normal, :enhanced, or :diminished)
  • #to_s - Convert to GAN string representation
  • #to_pin - Convert to PIN string representation (piece component only)
  • #to_snn - Convert to SNN string representation (style component only)

Component Extraction

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

actor = Sashite::Gan.parse("CHESS:+K")

# Full GAN representation
actor.to_s # => "CHESS:+K"

# Individual components
actor.to_snn      # => "CHESS" (style component)
actor.to_pin      # => "+K"    (piece component)

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

# State manipulation example
normalized = actor.normalize
normalized.to_s   # => "CHESS:K"
normalized.to_pin # => "K"      (state modifier removed)
normalized.to_snn # => "CHESS"  (style unchanged)

Component Handling

Important: Following PIN and SNN conventions:

  • Style names are stored with proper capitalization (:Chess, :Shogi)
  • Piece types are stored as uppercase symbols (:K, :P)
  • Display case is determined by side during rendering
# Both create the same internal representation
actor1 = Sashite::Gan.parse("CHESS:K")  # name: :Chess, type: :K, side: :first
actor2 = Sashite::Gan.parse("chess:k")  # name: :Chess, type: :K, side: :second

actor1.name        # => :Chess (proper capitalization)
actor2.name        # => :Chess (same style name)
actor1.type        # => :K (uppercase type)
actor2.type        # => :K (same type)

actor1.to_s        # => "CHESS:K" (uppercase display)
actor2.to_s        # => "chess:k" (lowercase display)
actor1.to_snn      # => "CHESS" (uppercase style)
actor2.to_snn      # => "chess" (lowercase style)
actor1.to_pin      # => "K" (uppercase piece)
actor2.to_pin      # => "k" (lowercase piece)

State Queries

  • #normal? - Check if normal state (no modifiers)
  • #enhanced? - Check if enhanced state
  • #diminished? - Check if diminished state

Side Queries

  • #first_player? - Check if first player actor
  • #second_player? - Check if second player actor

State Transformations (immutable - return new instances)

  • #enhance - Create enhanced version
  • #diminish - Create diminished version
  • #normalize - Remove all state modifiers
  • #flip - Switch player (change side)

Attribute Transformations (immutable - return new instances)

  • #with_name(new_name) - Create actor with different style name
  • #with_type(new_type) - Create actor with different piece type
  • #with_side(new_side) - Create actor with different side
  • #with_state(new_state) - Create actor with different state

Comparison Methods

  • #same_name?(other) - Check if same style name
  • #same_type?(other) - Check if same piece type
  • #same_side?(other) - Check if same side
  • #same_state?(other) - Check if same state
  • #==(other) - Full equality comparison

Constants

  • Sashite::Gan::Actor::SEPARATOR - Colon separator character

Advanced Usage

Component Extraction and Manipulation

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

# Extract and manipulate components
actor = Sashite::Gan.parse("SHOGI:+P")

# Component extraction
style_str = actor.to_snn    # => "SHOGI"
piece_str = actor.to_pin    # => "+P"

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

# Cross-component analysis
actors = [
  Sashite::Gan.parse("CHESS:K"),
  Sashite::Gan.parse("SHOGI:K"),
  Sashite::Gan.parse("chess:k")
]

# Group by style component
by_style = actors.group_by(&:to_snn)
# => {"CHESS" => [...], "SHOGI" => [...], "chess" => [...]}

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

# Component-based filtering
uppercase_styles = actors.select { |a| a.to_snn == a.to_snn.upcase }
enhanced_pieces = actors.select { |a| a.to_pin.start_with?("+") }

Component Reconstruction Patterns

# Template-based reconstruction
def apply_style_template(actors, new_style)
  actors.map do |actor|
    pin_part = actor.to_pin
    side = actor.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::Gan.parse("#{new_style_str}:#{pin_part}")
  end
end

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

shogi_pieces = apply_style_template(chess_pieces, :Shogi)
# => [SHOGI:K, shogi:+q]

# Component swapping
def swap_components(actor1, actor2)
  [
    Sashite::Gan.parse("#{actor1.to_snn}:#{actor2.to_pin}"),
    Sashite::Gan.parse("#{actor2.to_snn}:#{actor1.to_pin}")
  ]
end

chess_king = Sashite::Gan.parse("CHESS:K")
shogi_pawn = Sashite::Gan.parse("shogi:p")

swapped = swap_components(chess_king, shogi_pawn)
# => [CHESS:p, shogi:K]

Immutable Transformations

# All transformations return new instances
original = Sashite::Gan.parse("CHESS:P")
enhanced = original.enhance
cross_style = original.with_name(:Shogi)
enemy = original.flip

# Original actor is never modified
puts original     # => "CHESS:P"
puts enhanced     # => "CHESS:+P"
puts cross_style  # => "SHOGI:P"
puts enemy        # => "chess:p"

# Component extraction shows changes
puts enhanced.to_pin # => "+P" (state changed)
puts cross_style.to_snn # => "SHOGI" (style changed)
puts enemy.to_snn      # => "chess" (case changed)
puts enemy.to_pin      # => "p" (case changed)

# Transformations can be chained
result = original.flip.with_name(:Xiangqi).enhance
puts result # => "xiangqi:+p"
puts result.to_snn     # => "xiangqi"
puts result.to_pin     # => "+p"

Multi-Style Game Management

class CrossStyleGame
  def initialize
    @actors = []
    @style_assignments = {}
  end

  def assign_style(player, style)
    side = player == :white ? :first : :second
    @style_assignments[player] = { style: style, side: side }
  end

  def create_actor(player, type, state = :normal)
    assignment = @style_assignments[player]
    Sashite::Gan::Actor.new(assignment[:style], type, assignment[:side], state)
  end

  def valid_combination?
    return true if @style_assignments.size < 2

    sides = @style_assignments.values.map { |a| a[:side] }
    sides.uniq.size == 2 # Must have different sides
  end

  def get_player_style_string(player)
    actor = create_actor(player, :K) # Use king as reference
    actor.to_snn
  end
end

# Usage
game = CrossStyleGame.new
game.assign_style(:white, :Chess)
game.assign_style(:black, :Shogi)

white_king = game.create_actor(:white, :K)
black_king = game.create_actor(:black, :K)

puts white_king # => "CHESS:K"
puts white_king.to_snn # => "CHESS"
puts black_king # => "shogi:k"
puts black_king.to_snn # => "shogi"
puts game.valid_combination? # => true

Validation and Error Handling

# Comprehensive validation with both module and class methods
def safe_parse(gan_string)
  # You can use either method for validation
  return nil unless Sashite::Gan.valid?(gan_string)

  # Alternative: return nil unless Sashite::Gan::Actor.valid?(gan_string)

  Sashite::Gan.parse(gan_string)
rescue ArgumentError => e
  puts "Parse error: #{e.message}"
  nil
end

# Batch validation with component extraction
gan_strings = ["CHESS:K", "Chess:K", "SHOGI:+p", "invalid"]
valid_actors = gan_strings.filter_map { |s| safe_parse(s) }

puts "Valid actors with components:"
valid_actors.each do |actor|
  puts "  #{actor} -> style: #{actor.to_snn}, piece: #{actor.to_pin}"
end

# Module-level validation
Sashite::Gan.valid?("CHESS:K")           # => true
Sashite::Gan.valid?("chess:k")           # => true
Sashite::Gan.valid?("Chess:K")           # => false (mixed case)
Sashite::Gan.valid?("CHESS")             # => false (missing piece)

# Class-level validation (equivalent to module method)
Sashite::Gan::Actor.valid?("CHESS:K")    # => true
Sashite::Gan::Actor.valid?("chess:k")    # => true
Sashite::Gan::Actor.valid?("Chess:K")    # => false (mixed case)
Sashite::Gan::Actor.valid?("CHESS:k")    # => false (case mismatch)

Collection Operations

# Working with actor collections
actors = [
  Sashite::Gan.parse("CHESS:K"),
  Sashite::Gan.parse("CHESS:Q"),
  Sashite::Gan.parse("shogi:k"),
  Sashite::Gan.parse("shogi:g"),
  Sashite::Gan.parse("XIANGQI:G")
]

# Group by various attributes
by_style = actors.group_by(&:name)
by_side = actors.group_by(&:side)
by_type = actors.group_by(&:type)

# Group by string components
by_style_string = actors.group_by(&:to_snn)
by_piece_string = actors.group_by(&:to_pin)

puts "By style string: #{by_style_string.keys}"  # => ["CHESS", "shogi", "XIANGQI"]
puts "By piece string: #{by_piece_string.keys}"  # => ["K", "Q", "k", "g", "G"]

# Filter operations
first_player_actors = actors.select(&:first_player?)
chess_actors = actors.select { |a| a.name == :Chess }
kings = actors.select { |a| a.type == :K }
uppercase_styles = actors.select { |a| a.to_snn == a.to_snn.upcase }

# Transform collections immutably
enhanced_actors = actors.map(&:enhance)
enemy_actors = actors.map(&:flip)

# Show component changes
puts "Enhanced actors:"
enhanced_actors.each { |a| puts "  #{a} (pin: #{a.to_pin})" }

puts "Enemy actors:"
enemy_actors.each { |a| puts "  #{a} (snn: #{a.to_snn}, pin: #{a.to_pin})" }

# Complex queries
cross_style_pairs = actors.combination(2).select do |a1, a2|
  a1.name != a2.name && a1.side != a2.side
end

puts "Cross-style pairs: #{cross_style_pairs.size}"

Protocol Mapping

GAN encodes piece attributes by combining SNN and PIN information:

Protocol Attribute GAN Encoding Examples Notes
Type PIN letter choice CHESS:K = King, SHOGI:P = Pawn Type stored as uppercase symbol (:K, :P)
Side Unified case across components CHESS:K = First player, chess:k = Second player Case consistency enforced
State PIN prefix modifier SHOGI:+P = Enhanced, CHESS:-P = Diminished
Style SNN identifier CHESS:K = Chess style, SHOGI:K = Shōgi style Style stored with proper capitalization (:Chess, :Shogi)

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 SNN and PIN components accessible via to_snn and to_pin
  • Unified Case Encoding: Consistent case across both components for side identification
  • Protocol Compliance: Direct implementation of Sashité piece attributes
  • Immutable Design: All operations return new instances, ensuring thread safety
  • Compositional Architecture: Built on independent SNN and PIN specifications
  • Modular Validation: Delegates validation to underlying components for consistency

Implementation Notes

Validation Architecture

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

  1. Component Splitting: GAN strings are split on the colon separator
  2. Individual Validation: Each component is validated using its specific regex:
    • SNN component: Sashite::Snn::Style::SNN_PATTERN
    • PIN component: Sashite::Pin::Piece::PIN_PATTERN
  3. Case Consistency: Additional validation ensures matching case between components

This approach:

  • Avoids Code Duplication: No need to maintain a separate GAN regex
  • Maintains Consistency: Automatically inherits validation improvements from SNN and PIN
  • Provides Clear Error Messages: Component-specific validation failures are more informative
  • Enables Modularity: Each library maintains its own validation logic

Component Handling Convention

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

  1. Style Names: Always stored with proper capitalization (:Chess, :Shogi)
  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.

System Constraints

  • Case Consistency: SNN and PIN components must have matching case
  • Exactly 2 players: Distinguished through consistent case encoding
  • Style Assignment: Fixed throughout a game (first/second player styles remain constant)
  • Component Validation: Both SNN and PIN components must be individually valid

Use Cases

GAN 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
  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 GAN and individual SNN/PIN representations

Dependencies

This gem depends on:

Related Specifications

Documentation

Development

# Clone the repository
git clone https://github.com/sashite/gan.rb.git
cd gan.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 - board game piece identification.

Topics

Resources

License

Code of conduct

Stars

Watchers

Forks