Skip to content

sashite/pin.rb

Repository files navigation

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)

Related Specifications

  • 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.

About

PIN (Piece Identifier Notation) implementation for Ruby with immutable piece objects.

Resources

License

Code of conduct

Stars

Watchers

Forks