Pnn.rb

Version Yard documentation Ruby License

PNN (Piece Name Notation) implementation for the Ruby language.

What is PNN?

PNN (Piece Name Notation) extends PIN (Piece Identifier Notation) to provide style-aware piece representation in abstract strategy board games. PNN adds a derivation marker that distinguishes pieces by their style origin, enabling cross-style game scenarios and piece origin tracking.

This gem implements the PNN Specification v1.0.0, providing a modern Ruby interface with immutable piece objects and full backward compatibility with PIN while adding style differentiation capabilities.

Installation

# In your Gemfile
gem "sashite-pnn"

Or install manually:

gem install sashite-pnn

Usage

require "sashite/pnn"

# Parse PNN strings into piece objects
piece = Sashite::Pnn.parse("K")          # => #<Pnn::Piece type=:K side=:first state=:normal native=true>
piece.to_s                               # => "K"
piece.type                               # => :K
piece.side                               # => :first
piece.state                              # => :normal
piece.native?                            # => true

# Create pieces directly
piece = Sashite::Pnn.piece(:K, :first) # => #<Pnn::Piece type=:K side=:first state=:normal native=true>
piece = Sashite::Pnn::Piece.new(:R, :second, :enhanced, false) # => #<Pnn::Piece type=:R side=:second state=:enhanced native=false>

# Validate PNN strings
Sashite::Pnn.valid?("K")                 # => true
Sashite::Pnn.valid?("+R'")               # => true
Sashite::Pnn.valid?("invalid")           # => false

# Style derivation with apostrophe suffix
native_king = Sashite::Pnn.parse("K")    # => #<Pnn::Piece type=:K side=:first state=:normal native=true>
foreign_king = Sashite::Pnn.parse("K'")  # => #<Pnn::Piece type=:K side=:first state=:normal native=false>

native_king.to_s                         # => "K"
foreign_king.to_s                        # => "K'"

# State manipulation (returns new immutable instances)
enhanced = piece.enhance                 # => #<Pnn::Piece type=:K side=:first state=:enhanced native=true>
enhanced.to_s                            # => "+K"
diminished = piece.diminish              # => #<Pnn::Piece type=:K side=:first state=:diminished native=true>
diminished.to_s                          # => "-K"

# Style derivation manipulation
foreign_piece = piece.derive             # => #<Pnn::Piece type=:K side=:first state=:normal native=false>
foreign_piece.to_s                       # => "K'"
back_to_native = foreign_piece.underive  # => #<Pnn::Piece type=:K side=:first state=:normal native=true>
back_to_native.to_s                      # => "K"

# Side manipulation
flipped = piece.flip                     # => #<Pnn::Piece type=:K side=:second state=:normal native=true>
flipped.to_s                             # => "k"

# Type manipulation
queen = piece.with_type(:Q)              # => #<Pnn::Piece type=:Q side=:first state=:normal native=true>
queen.to_s                               # => "Q"

# Style queries
piece.native?                            # => true
foreign_king.derived?                    # => true

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

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

# Attribute access
piece.letter                             # => "K"
enhanced.prefix                          # => "+"
foreign_king.suffix                      # => "'"
piece.suffix                             # => ""

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

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

# Style comparison
native_king = Sashite::Pnn.parse("K")
foreign_king = Sashite::Pnn.parse("K'")

native_king.same_style?(foreign_king) # => false (different derivation)

# Functional transformations can be chained
pawn = Sashite::Pnn.parse("P")
enemy_foreign_promoted = pawn.flip.derive.enhance # => "+p'" (second player foreign promoted pawn)

Format Specification

Structure

<pin>[<suffix>]

Components

  • PIN part ([<state>]<letter>): Standard PIN notation

    • 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
  • Derivation suffix (optional):

    • ': Foreign style (piece has opposite side's native style)
    • No suffix: Native style (piece has current side's native style)

Regular Expression

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

Examples

  • K - First player king (native style, normal state)
  • k' - Second player king (foreign style, normal state)
  • +R' - First player rook (foreign style, enhanced state)
  • -p - Second player pawn (native style, diminished state)

Game Examples

Cross-Style Chess vs. Shōgi

# Match setup: First player uses Chess, Second player uses Shōgi
# Native styles: first=Chess, second=Shōgi

# Native pieces (no derivation suffix)
white_king = Sashite::Pnn.piece(:K, :first)          # => "K" (Chess king)
black_king = Sashite::Pnn.piece(:K, :second)         # => "k" (Shōgi king)

# Foreign pieces (with derivation suffix)
white_shogi_king = Sashite::Pnn.piece(:K, :first, :normal, false)   # => "K'" (Shōgi king for white)
black_chess_king = Sashite::Pnn.piece(:K, :second, :normal, false)  # => "k'" (Chess king for black)

# Promoted pieces in cross-style context
white_promoted_rook = Sashite::Pnn.parse("+R'")  # White shōgi rook promoted to Dragon King
black_promoted_pawn = Sashite::Pnn.parse("+p")   # Black shōgi pawn promoted to Tokin

white_promoted_rook.enhanced?                     # => true
white_promoted_rook.derived?                      # => true
black_promoted_pawn.enhanced?                     # => true
black_promoted_pawn.native?                       # => true

Single-Style Games (PIN Compatibility)

# Traditional Chess (both players use Chess style)
# All pieces are native, so PNN behaves exactly like PIN

white_pieces = %w[K Q +R B N P].map { |pin| Sashite::Pnn.parse(pin) }
black_pieces = %w[k q +r b n p].map { |pin| Sashite::Pnn.parse(pin) }

white_pieces.all?(&:native?)                     # => true
black_pieces.all?(&:native?)                     # => true

# PNN strings match PIN strings for native pieces
white_pieces.map(&:to_s)                         # => ["K", "Q", "+R", "B", "N", "P"]
black_pieces.map(&:to_s)                         # => ["k", "q", "+r", "b", "n", "p"]

Style Mutation During Gameplay

# Simulate capture with style change (Ōgi rules)
chess_queen = Sashite::Pnn.parse("q'") # Black chess queen (foreign for shōgi player)
captured = chess_queen.flip.with_type(:P).underive # Becomes white native pawn

chess_queen.to_s                                 # => "q'" (black foreign queen)
captured.to_s                                    # => "P" (white native pawn)

# Style derivation changes during gameplay
shogi_piece = Sashite::Pnn.parse("r")           # Black shōgi rook (native)
foreign_piece = shogi_piece.derive              # Convert to foreign style
foreign_piece.to_s                              # => "r'" (black foreign rook)

API Reference

Main Module Methods

  • Sashite::Pnn.valid?(pnn_string) - Check if string is valid PNN notation
  • Sashite::Pnn.parse(pnn_string) - Parse PNN string into Piece object
  • Sashite::Pnn.piece(type, side, state = :normal, native = true) - Create piece instance directly

Piece Class

Creation and Parsing

  • Sashite::Pnn::Piece.new(type, side, state = :normal, native = true) - Create piece instance
  • Sashite::Pnn::Piece.parse(pnn_string) - Parse PNN string (same as module method)
  • Sashite::Pnn::Piece.valid?(pnn_string) - Validate PNN 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)
  • #native - Get style derivation (true for native, false for foreign)
  • #letter - Get letter representation (string, case determined by side)
  • #prefix - Get state prefix (string: "+", "-", or "")
  • #suffix - Get derivation suffix (string: "'" or "")
  • #to_s - Convert to PNN string representation

Style Queries

  • #native? - Check if native style (current side's native style)
  • #derived? - Check if foreign style (opposite side's native style)
  • #foreign? - Alias for #derived?

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

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

Style Transformations (immutable - return new instances)

  • #derive - Convert to foreign style (add derivation suffix)
  • #underive - Convert to native style (remove derivation suffix)
  • #flip - Switch player (change side)

Attribute Transformations (immutable - return new instances)

  • #with_type(new_type) - Create piece with different type
  • #with_side(new_side) - Create piece with different side
  • #with_state(new_state) - Create piece with different state
  • #with_derivation(native) - Create piece with different derivation

Comparison Methods

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

Constants

  • Sashite::Pnn::Piece::NATIVE - Constant for native style (true)
  • Sashite::Pnn::Piece::FOREIGN - Constant for foreign style (false)
  • Sashite::Pnn::Piece::FOREIGN_SUFFIX - Derivation suffix for foreign pieces ("'")
  • Sashite::Pnn::Piece::NATIVE_SUFFIX - Derivation suffix for native pieces ("")

Note: PNN validation leverages the existing Sashite::Pin::Piece::PIN_PATTERN for the PIN component, with additional logic for the optional derivation suffix.

Advanced Usage

Style Derivation Examples

# Understanding native vs. foreign pieces
# In a Chess vs. Shōgi match:
# - First player native style: Chess
# - Second player native style: Shōgi

native_chess_king = Sashite::Pnn.parse("K")      # First player native (Chess king)
foreign_shogi_king = Sashite::Pnn.parse("K'")    # First player foreign (Shōgi king)

native_shogi_king = Sashite::Pnn.parse("k")      # Second player native (Shōgi king)
foreign_chess_king = Sashite::Pnn.parse("k'")    # Second player foreign (Chess king)

# Style queries
native_chess_king.native?                        # => true
foreign_shogi_king.derived?                      # => true
native_shogi_king.native?                        # => true
foreign_chess_king.derived?                      # => true

Immutable Transformations

# All transformations return new instances
original = Sashite::Pnn.piece(:K, :first)
enhanced = original.enhance
derived = original.derive
flipped = original.flip

# Original piece is never modified
puts original    # => "K"
puts enhanced    # => "+K"
puts derived     # => "K'"
puts flipped     # => "k"

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

Cross-Style Game State Management

class CrossStyleGameBoard
  def initialize(first_style, second_style)
    @first_style = first_style
    @second_style = second_style
    @pieces = {}
  end

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

  def capture_with_style_change(from_square, to_square, new_type = nil)
    captured = @pieces[to_square]
    capturing = @pieces.delete(from_square)

    return nil unless captured && capturing

    # Style mutation: captured piece becomes native to capturing side
    mutated = captured.flip.underive
    mutated = mutated.with_type(new_type) if new_type

    @pieces[to_square] = capturing
    mutated # Return mutated captured piece for hand
  end

  def pieces_by_style_derivation
    {
      native:  @pieces.select { |_, piece| piece.native? },
      foreign: @pieces.select { |_, piece| piece.derived? }
    }
  end
end

# Usage
board = CrossStyleGameBoard.new(:chess, :shogi)
board.place("e1", Sashite::Pnn.piece(:K, :first))               # Chess king
board.place("e8", Sashite::Pnn.piece(:K, :second))              # Shōgi king
board.place("d4", Sashite::Pnn.piece(:Q, :first, :normal, false)) # Chess queen using Shōgi style

analysis = board.pieces_by_style_derivation
puts analysis[:native].size    # => 2
puts analysis[:foreign].size   # => 1

PIN Compatibility Layer

# PNN is fully backward compatible with PIN
def convert_pin_to_pnn(pin_string)
  # All PIN strings are valid PNN strings (native pieces)
  Sashite::Pnn.parse(pin_string)
end

def convert_pnn_to_pin(pnn_piece)
  # Only native PNN pieces can be converted to PIN
  return nil unless pnn_piece.native?

  "#{pnn_piece.prefix}#{pnn_piece.letter}"
end

# Usage
pin_pieces = %w[K Q +R -P k q r p]
pnn_pieces = pin_pieces.map { |pin| convert_pin_to_pnn(pin) }

pnn_pieces.all?(&:native?)                    # => true
pnn_pieces.map { |p| convert_pnn_to_pin(p) }  # => ["K", "Q", "+R", "-P", "k", "q", "r", "p"]

Move Validation Example

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

  case [piece.type, piece.native? ? style_rules[:native] : style_rules[:foreign]]
  when %i[P chess]  # Chess pawn
    (piece.first_player? && target_rank == 8) ||
      (piece.second_player? && target_rank == 1)
  when %i[P shogi]  # Shōgi pawn
    (piece.first_player? && target_rank >= 7) ||
      (piece.second_player? && target_rank <= 3)
  when %i[R shogi], %i[B shogi] # Shōgi major pieces
    true
  else
    false
  end
end

# Usage
chess_pawn = Sashite::Pnn.piece(:P, :first)
shogi_pawn = Sashite::Pnn.piece(:P, :first, :normal, false)

style_rules = { native: :chess, foreign: :shogi }

puts can_promote_in_style?(chess_pawn, 8, style_rules)  # => true (chess pawn on 8th rank)
puts can_promote_in_style?(shogi_pawn, 8, style_rules)  # => true (shogi pawn on 8th rank)

Implementation Architecture

This gem uses composition over inheritance by building upon the proven sashite-pin gem:

  • PIN Foundation: All type, side, and state logic is handled by an internal Pin::Piece object
  • PNN Extension: Only the derivation (native) attribute and related methods are added
  • Delegation Pattern: Core PIN methods are delegated to the internal PIN piece
  • Immutability: All transformations return new instances, maintaining functional programming principles

This architecture ensures:

  • Reliability: Reuses battle-tested PIN logic
  • Maintainability: PIN updates automatically benefit PNN
  • Consistency: PIN and PNN pieces behave identically for shared attributes
  • Performance: Minimal overhead over pure PIN implementation

Protocol Mapping

Following the Game Protocol:

Protocol Attribute PNN 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
Style Derivation suffix K = Native style, K' = Foreign style

Style Derivation Logic:

  • No suffix: Piece has the native style of its current side
  • Apostrophe suffix ('): Piece has the foreign style (opposite side's native style)

Canonical principle: Identical pieces must have identical PNN representations.

Properties

  • PIN Compatible: All valid PIN strings are valid PNN strings
  • Style Aware: Distinguishes pieces by their style origin through derivation markers
  • ASCII Compatible: Maximum portability across systems
  • Rule-Agnostic: Independent of specific game mechanics
  • Compact Format: Minimal character usage (1-3 characters per piece)
  • Visual Distinction: Clear player and style differentiation
  • Protocol Compliant: Complete 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

Style Derivation Convention

PNN follows a strict style derivation convention:

  1. Native pieces (no suffix): Use the current side's native style
  2. Foreign pieces (' suffix): Use the opposite side's native style
  3. Match context: Each side has a defined native style for the entire match
  4. Style mutations: Pieces can change derivation through gameplay mechanics

Example Flow

# Match context: First player=Chess, Second player=Shōgi
# Input: "K'" (first player foreign)
# ↓ Parsing
# type: :K, side: :first, state: :normal, native: false
# ↓ Style resolution
# Effective style: Shōgi (second player's native style)
# ↓ Display
# PNN: "K'" (first player king with foreign/Shōgi style)

This ensures that parse(pnn).to_s == pnn for all valid PNN strings while enabling cross-style gameplay.

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)
  • 2 style derivations (native, foreign)
  • Style context dependency: Requires match-level side-style associations
  • PIN - Piece Identifier Notation (style-agnostic base)
  • Game Protocol - Conceptual foundation for abstract strategy board games
  • SNN - Style Name Notation
  • GAN - General Actor Notation (alternative style-aware format)
  • CELL - Board position coordinates
  • HAND - Reserve location notation
  • PMN - Portable Move Notation

Documentation

Development

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