Pin.rb

Version Yard documentation Ruby License

PIN (Piece Identifier Notation) implementation for the Ruby language.

What is PIN?

PIN (Piece Identifier Notation) provides an ASCII-based format for representing pieces in abstract strategy board games. PIN translates piece attributes from the Game Protocol into a compact, portable notation system.

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

Installation

# In your Gemfile
gem "sashite-pin"

Or install manually:

gem install sashite-pin

Usage

require "sashite/pin"

# Parse PIN strings into identifier objects
identifier = Sashite::Pin.parse("K")          # => #<Pin::Identifier type=:K side=:first state=:normal>
identifier.to_s                               # => "K"
identifier.type                               # => :K
identifier.side                               # => :first
identifier.state                              # => :normal

# Create identifiers directly
identifier = Sashite::Pin.identifier(:K, :first, :normal)    # => #<Pin::Identifier type=:K side=:first state=:normal>
identifier = Sashite::Pin::Identifier.new(:R, :second, :enhanced)  # => #<Pin::Identifier type=:R side=:second state=:enhanced>

# Validate PIN strings
Sashite::Pin.valid?("K")                 # => true
Sashite::Pin.valid?("+R")                # => true
Sashite::Pin.valid?("invalid")           # => false

# State manipulation (returns new immutable instances)
enhanced = identifier.enhance                 # => #<Pin::Identifier type=:K side=:first state=:enhanced>
enhanced.to_s                                 # => "+K"
diminished = identifier.diminish              # => #<Pin::Identifier type=:K side=:first state=:diminished>
diminished.to_s                               # => "-K"

# Side manipulation
flipped = identifier.flip                     # => #<Pin::Identifier type=:K side=:second state=:normal>
flipped.to_s                                  # => "k"

# Type manipulation
queen = identifier.with_type(:Q)              # => #<Pin::Identifier type=:Q side=:first state=:normal>
queen.to_s                                    # => "Q"

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

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

# Attribute access
identifier.letter                             # => "K"
enhanced.prefix                               # => "+"
identifier.prefix                             # => ""

# Type and side comparison
king1 = Sashite::Pin.parse("K")
king2 = Sashite::Pin.parse("k")
queen = Sashite::Pin.parse("Q")

king1.same_type?(king2)                       # => true (both kings)
king1.same_side?(queen)                       # => true (both first player)
king1.same_type?(queen)                       # => false (different types)

# Functional transformations can be chained
pawn = Sashite::Pin.parse("P")
enemy_promoted = pawn.flip.enhance            # => "+p" (second player promoted pawn)

Format Specification

Structure

[<state>]<letter>

Components

  • Letter (A-Z, a-z): Represents piece type and side

    • Uppercase: First player pieces
    • Lowercase: Second player pieces
  • State (optional prefix):

    • +: Enhanced state (promoted, upgraded, empowered)
    • -: Diminished state (weakened, restricted, temporary)
    • No prefix: Normal state

Regular Expression

/\A[-+]?[A-Za-z]\z/

Examples

  • K - First player king (normal state)
  • k - Second player king (normal state)
  • +R - First player rook (enhanced state)
  • -p - Second player pawn (diminished state)

API Reference

Main Module Methods

  • Sashite::Pin.valid?(pin_string) - Check if string is valid PIN notation
  • Sashite::Pin.parse(pin_string) - Parse PIN string into Identifier object
  • Sashite::Pin.identifier(type, side, state = :normal) - Create identifier instance directly

Identifier Class

Creation and Parsing

  • Sashite::Pin::Identifier.new(type, side, state = :normal) - Create identifier instance
  • Sashite::Pin::Identifier.parse(pin_string) - Parse PIN string (same as module method)
  • Sashite::Pin::Identifier.valid?(pin_string) - Validate PIN string (class method)

Attribute Access

  • #type - Get piece type (symbol :A to :Z, always uppercase)
  • #side - Get player side (:first or :second)
  • #state - Get state (:normal, :enhanced, or :diminished)
  • #letter - Get letter representation (string, case determined by side)
  • #prefix - Get state prefix (string: "+", "-", or "")
  • #to_s - Convert to PIN string representation

Type and Case Handling

Important: The type attribute is always stored as an uppercase symbol (:A to :Z), regardless of the input case when parsing. The display case in #letter and #to_s is determined by the side attribute:

# Both create the same internal type representation
identifier1 = Sashite::Pin.parse("K")  # type: :K, side: :first
identifier2 = Sashite::Pin.parse("k")  # type: :K, side: :second

identifier1.type    # => :K (uppercase symbol)
identifier2.type    # => :K (same uppercase symbol)

identifier1.letter  # => "K" (uppercase display)
identifier2.letter  # => "k" (lowercase display)

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 identifier
  • #second_player? - Check if second player identifier

State Transformations (immutable - return new instances)

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

Attribute Transformations (immutable - return new instances)

  • #with_type(new_type) - Create identifier with different type
  • #with_side(new_side) - Create identifier with different side
  • #with_state(new_state) - Create identifier with different state

Comparison Methods

  • #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::Pin::Identifier::PIN_PATTERN - Regular expression for PIN validation (internal use)

Advanced Usage

Type Normalization Examples

# Parsing different cases results in same type
white_king = Sashite::Pin.parse("K")
black_king = Sashite::Pin.parse("k")

# Types are normalized to uppercase
white_king.type  # => :K
black_king.type  # => :K (same type!)

# Sides are different
white_king.side  # => :first
black_king.side  # => :second

# Display follows side convention
white_king.letter # => "K"
black_king.letter # => "k"

# Same type, different sides
white_king.same_type?(black_king)  # => true
white_king.same_side?(black_king)  # => false

Immutable Transformations

# All transformations return new instances
original = Sashite::Pin.piece(:K, :first, :normal)
enhanced = original.enhance
diminished = original.diminish

# Original piece is never modified
puts original.to_s    # => "K"
puts enhanced.to_s    # => "+K"
puts diminished.to_s  # => "-K"

# Transformations can be chained
result = original.flip.enhance.with_type(:Q)
puts result.to_s      # => "+q"

Game State Management

class GameBoard
  def initialize
    @pieces = {}
  end

  def place(square, piece)
    @pieces[square] = piece
  end

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

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

  def capture(from_square, to_square)
    captured = @pieces[to_square]
    @pieces[to_square] = @pieces.delete(from_square)
    captured
  end

  def pieces_by_side(side)
    @pieces.select { |_, piece| piece.side == side }
  end

  def promoted_pieces
    @pieces.select { |_, piece| piece.enhanced? }
  end
end

# Usage
board = GameBoard.new
board.place("e1", Sashite::Pin.piece(:K, :first, :normal))
board.place("e8", Sashite::Pin.piece(:K, :second, :normal))
board.place("a7", Sashite::Pin.piece(:P, :first, :normal))

# Promote pawn
board.promote("a7", :Q)
promoted = board.promoted_pieces
puts promoted.values.first.to_s  # => "+Q"

Piece Analysis

def analyze_pieces(pins)
  pieces = pins.map { |pin| Sashite::Pin.parse(pin) }

  {
    total: pieces.size,
    by_side: pieces.group_by(&:side),
    by_type: pieces.group_by(&:type),
    by_state: pieces.group_by(&:state),
    promoted: pieces.count(&:enhanced?),
    weakened: pieces.count(&:diminished?)
  }
end

pins = %w[K Q +R B N P k q r +b n -p]
analysis = analyze_pieces(pins)
puts analysis[:by_side][:first].size  # => 6
puts analysis[:promoted]              # => 2

Move Validation Example

def can_promote?(piece, target_rank)
  return false unless piece.normal?  # Already promoted pieces can't promote again

  case piece.type
  when :P  # Pawn
    (piece.first_player? && target_rank == 8) ||
    (piece.second_player? && target_rank == 1)
  when :R, :B, :S, :N, :L  # Shōgi pieces that can promote
    true
  else
    false
  end
end

pawn = Sashite::Pin.piece(:P, :first, :normal)
puts can_promote?(pawn, 8)          # => true

promoted_pawn = pawn.enhance
puts can_promote?(promoted_pawn, 8) # => false (already promoted)

Protocol Mapping

Following the Game Protocol:

Protocol Attribute PIN Encoding Examples Notes
Type ASCII letter choice K/k = King, P/p = Pawn Type is always stored as uppercase symbol (:K, :P)
Side Letter case in display K = First player, k = Second player Case is determined by side during rendering
State Optional prefix +K = Enhanced, -K = Diminished, K = Normal

Type Convention: All piece types are internally represented as uppercase symbols (:A to :Z). The display case is determined by the side attribute: first player pieces display as uppercase, second player pieces as lowercase.

Canonical principle: Identical pieces must have identical PIN representations.

Note: PIN does not represent the Style attribute from the Game Protocol. For style-aware piece notation, see Piece Name Notation (PNN).

Properties

  • ASCII Compatible: Maximum portability across systems
  • Rule-Agnostic: Independent of specific game mechanics
  • Compact Format: 1-2 characters per piece
  • Visual Distinction: Clear player differentiation through case
  • Type Normalization: Consistent uppercase type representation internally
  • Protocol Compliant: Direct implementation of Sashité piece attributes
  • Immutable: All piece instances are frozen and transformations return new objects
  • Functional: Pure functions with no side effects

Implementation Notes

Type Normalization Convention

PIN follows a strict type normalization convention:

  1. Internal Storage: All piece types are stored as uppercase symbols (:A to :Z)
  2. Input Flexibility: Both "K" and "k" are valid input during parsing
  3. Case Semantics: Input case determines the side attribute, not the type
  4. Display Logic: Output case is computed from side during rendering

This design ensures:

  • Consistent internal representation regardless of input format
  • Clear separation between piece identity (type) and ownership (side)
  • Predictable behavior when comparing pieces of the same type

Example Flow

# Input: "k" (lowercase)
# ↓ Parsing
# type: :K (normalized to uppercase)
# side: :second (inferred from lowercase input)
# ↓ Display
# letter: "k" (computed from type + side)
# PIN: "k" (final representation)

This ensures that parse(pin).to_s == pin for all valid PIN strings while maintaining internal consistency.

System Constraints

  • Maximum 26 piece types per game system (one per ASCII letter)
  • Exactly 2 players (uppercase/lowercase distinction)
  • 3 state levels (enhanced, normal, diminished)
  • Game Protocol - Conceptual foundation for abstract strategy board games
  • PNN - Piece Name Notation (style-aware piece representation)
  • CELL - Board position coordinates
  • HAND - Reserve location notation
  • PMN - Portable Move Notation

Documentation

Development

# Clone the repository
git clone https://github.com/sashite/pin.rb.git
cd pin.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.