PIN (Piece Identifier Notation) implementation for the Ruby language.
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.
# In your Gemfile
gem "sashite-pin"
Or install manually:
gem install sashite-pin
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)
[<state>]<letter>
-
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
/\A[-+]?[A-Za-z]\z/
K
- First player king (normal state)k
- Second player king (normal state)+R
- First player rook (enhanced state)-p
- Second player pawn (diminished state)
Sashite::Pin.valid?(pin_string)
- Check if string is valid PIN notationSashite::Pin.parse(pin_string)
- Parse PIN string into Identifier objectSashite::Pin.identifier(type, side, state = :normal)
- Create identifier instance directly
Sashite::Pin::Identifier.new(type, side, state = :normal)
- Create identifier instanceSashite::Pin::Identifier.parse(pin_string)
- Parse PIN string (same as module method)Sashite::Pin::Identifier.valid?(pin_string)
- Validate PIN string (class method)
#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
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)
#normal?
- Check if normal state (no modifiers)#enhanced?
- Check if enhanced state#diminished?
- Check if diminished state
#first_player?
- Check if first player identifier#second_player?
- Check if second player identifier
#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)
#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
#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
Sashite::Pin::Identifier::PIN_PATTERN
- Regular expression for PIN validation (internal use)
# 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
# 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"
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"
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
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)
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).
- 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
PIN follows a strict type normalization convention:
- Internal Storage: All piece types are stored as uppercase symbols (
:A
to:Z
) - Input Flexibility: Both
"K"
and"k"
are valid input during parsing - Case Semantics: Input case determines the
side
attribute, not thetype
- 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
# 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.
- 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
- Official PIN Specification v1.0.0
- PIN Examples Documentation
- Game Protocol Foundation
- API Documentation
# 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
- 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.