Pan.rb

Version Yard documentation Ruby License

PAN (Portable Action Notation) implementation for the Ruby language.

What is PAN?

PAN (Portable Action Notation) is a human-readable string format for representing atomic actions in abstract strategy board games. PAN provides an intuitive operator-based syntax to describe how pieces move, capture, transform, and interact on game boards.

This gem implements the PAN Specification v1.0.0, providing a pure functional Ruby interface with immutable action objects.

Installation

# In your Gemfile
gem "sashite-pan"

Or install manually:

gem install sashite-pan

Quick Start

require "sashite/pan"

# Validate PAN strings
Sashite::Pan.valid?("e2-e4")      # => true
Sashite::Pan.valid?("d1+f3")      # => true
Sashite::Pan.valid?("...")        # => true
Sashite::Pan.valid?("invalid")    # => false

# Parse PAN strings into action objects
action = Sashite::Pan.parse("e2-e4")
action.type          # => :move
action.source        # => "e2"
action.destination   # => "e4"
action.to_s          # => "e2-e4"

# Create actions programmatically
action = Sashite::Pan::Action.move("e2", "e4")
action.to_s          # => "e2-e4"

promotion = Sashite::Pan::Action.move("e7", "e8", transformation: "Q")
promotion.to_s       # => "e7-e8=Q"

capture = Sashite::Pan::Action.capture("d1", "f3")
capture.to_s         # => "d1+f3"

# Drop actions (shogi-style)
drop = Sashite::Pan::Action.drop("e5", piece: "P")
drop.to_s            # => "P*e5"

# Pass action
pass = Sashite::Pan::Action.pass
pass.to_s            # => "..."

# Query action properties
action.move?         # => true
action.pass?         # => false
capture.capture?     # => true

Format Overview

PAN uses six intuitive operators:

Operator Meaning Example
- Move to empty square e2-e4
+ Capture at destination d1+f3
~ Special move with side effects e1~g1 (castling)
* Drop to empty square P*e5
. Drop with capture L.b4
= Transform piece e4=+P
... Pass turn ...

For complete format details, see the PAN Specification.

API Reference

Module Methods

Validation

Sashite::Pan.valid?(pan_string)

Check if a string represents a valid PAN action.

Parameters:

  • pan_string [String] - The string to validate

Returns: [Boolean] - true if valid PAN, false otherwise

Examples:

Sashite::Pan.valid?("e2-e4")       # => true
Sashite::Pan.valid?("P*d4")        # => true
Sashite::Pan.valid?("...")         # => true
Sashite::Pan.valid?("invalid")     # => false

Parsing

Sashite::Pan.parse(pan_string)

Parse a PAN string into an Action object.

Parameters:

  • pan_string [String] - PAN notation string

Returns: [Pan::Action] - Immutable action object

Raises: [ArgumentError] - If the PAN string is invalid

Examples:

Sashite::Pan.parse("e2-e4")        # => #<Pan::Action type=:move ...>
Sashite::Pan.parse("d1+f3")        # => #<Pan::Action type=:capture ...>
Sashite::Pan.parse("...")          # => #<Pan::Action type=:pass>

Action Class

Creation Methods

All creation methods return immutable Action objects.

Pass Action
Sashite::Pan::Action.pass

Create a pass action (no move, turn ends).

Returns: [Action] - Pass action

Example:

action = Sashite::Pan::Action.pass
action.to_s  # => "..."
Movement Actions
Sashite::Pan::Action.move(source, destination, transformation: nil)

Create a move action to an empty square.

Parameters:

  • source [String] - Source CELL coordinate
  • destination [String] - Destination CELL coordinate
  • transformation [String, nil] - Optional EPIN transformation

Returns: [Action] - Move action

Examples:

Sashite::Pan::Action.move("e2", "e4")
# => "e2-e4"

Sashite::Pan::Action.move("e7", "e8", transformation: "Q")
# => "e7-e8=Q"

Sashite::Pan::Action.move("a7", "a8", transformation: "+R")
# => "a7-a8=+R"

Sashite::Pan::Action.capture(source, destination, transformation: nil)

Create a capture action at destination.

Parameters:

  • source [String] - Source CELL coordinate
  • destination [String] - Destination CELL coordinate (occupied square)
  • transformation [String, nil] - Optional EPIN transformation

Returns: [Action] - Capture action

Examples:

Sashite::Pan::Action.capture("d1", "f3")
# => "d1+f3"

Sashite::Pan::Action.capture("b7", "a8", transformation: "R")
# => "b7+a8=R"

Sashite::Pan::Action.special(source, destination, transformation: nil)

Create a special move action with implicit side effects.

Parameters:

  • source [String] - Source CELL coordinate
  • destination [String] - Destination CELL coordinate
  • transformation [String, nil] - Optional EPIN transformation

Returns: [Action] - Special action

Examples:

Sashite::Pan::Action.special("e1", "g1")
# => "e1~g1" (castling)

Sashite::Pan::Action.special("e5", "f6")
# => "e5~f6" (en passant)
Static Capture
Sashite::Pan::Action.static_capture(square)

Create a static capture action (remove piece without movement).

Parameters:

  • square [String] - CELL coordinate of piece to capture

Returns: [Action] - Static capture action

Example:

Sashite::Pan::Action.static_capture("d4")
# => "+d4"
Drop Actions
Sashite::Pan::Action.drop(destination, piece: nil, transformation: nil)

Create a drop action to empty square.

Parameters:

  • destination [String] - Destination CELL coordinate (empty square)
  • piece [String, nil] - Optional EPIN piece identifier
  • transformation [String, nil] - Optional EPIN transformation

Returns: [Action] - Drop action

Examples:

Sashite::Pan::Action.drop("e5", piece: "P")
# => "P*e5"

Sashite::Pan::Action.drop("d4")
# => "*d4" (piece type inferred from context)

Sashite::Pan::Action.drop("c3", piece: "S", transformation: "+S")
# => "S*c3=+S"

Sashite::Pan::Action.drop_capture(destination, piece: nil, transformation: nil)

Create a drop action with capture.

Parameters:

  • destination [String] - Destination CELL coordinate (occupied square)
  • piece [String, nil] - Optional EPIN piece identifier
  • transformation [String, nil] - Optional EPIN transformation

Returns: [Action] - Drop capture action

Example:

Sashite::Pan::Action.drop_capture("b4", piece: "L")
# => "L.b4"
Modification Action
Sashite::Pan::Action.modify(square, piece)

Create an in-place transformation action.

Parameters:

  • square [String] - CELL coordinate
  • piece [String] - EPIN piece identifier (final state)

Returns: [Action] - Modification action

Examples:

Sashite::Pan::Action.modify("e4", "+P")
# => "e4=+P"

Sashite::Pan::Action.modify("c3", "k'")
# => "c3=k'"

Instance Methods

Attribute Access
action.type

Get the action type.

Returns: [Symbol] - One of: :pass, :move, :capture, :special, :static_capture, :drop, :drop_capture, :modify


action.source

Get the source coordinate (for movement actions).

Returns: [String, nil] - CELL coordinate or nil


action.destination

Get the destination coordinate.

Returns: [String, nil] - CELL coordinate or nil


action.piece

Get the piece identifier (for drop/modify actions).

Returns: [String, nil] - EPIN identifier or nil


action.transformation

Get the transformation piece (for actions with =<piece>).

Returns: [String, nil] - EPIN identifier or nil


action.to_s

Convert action to PAN string representation.

Returns: [String] - PAN notation

Examples:

Sashite::Pan::Action.move("e2", "e4").to_s
# => "e2-e4"

Sashite::Pan::Action.drop("e5", piece: "P").to_s
# => "P*e5"
Type Queries
action.pass?
action.move?
action.capture?
action.special?
action.static_capture?
action.drop?
action.drop_capture?
action.modify?
action.movement?        # true for move, capture, or special
action.drop_action?     # true for drop or drop_capture

Check action type.

Returns: [Boolean]

Examples:

action = Sashite::Pan.parse("e2-e4")
action.move?       # => true
action.movement?   # => true
action.pass?       # => false

pass = Sashite::Pan::Action.pass
pass.pass?         # => true

drop = Sashite::Pan.parse("P*e5")
drop.drop?         # => true
drop.drop_action?  # => true
Comparison
action == other

Check equality between actions.

Parameters:

  • other [Action] - Action to compare with

Returns: [Boolean] - true if actions are identical

Example:

action1 = Sashite::Pan.parse("e2-e4")
action2 = Sashite::Pan::Action.move("e2", "e4")
action1 == action2  # => true

Advanced Usage

Parsing Game Sequences

# Parse a sequence of moves
moves = %w[e2-e4 e7-e5 g1-f3 b8-c6]
actions = moves.map { |move| Sashite::Pan.parse(move) }

# Analyze action types
actions.count(&:move?)     # => 4
actions.all?(&:movement?)  # => true

# Extract coordinates
sources = actions.map(&:source)
destinations = actions.map(&:destination)

Action Type Detection

def describe_action(pan_string)
  action = Sashite::Pan.parse(pan_string)

  case action.type
  when :pass
    "Player passes"
  when :move
    "Move from #{action.source} to #{action.destination}"
  when :capture
    "Capture at #{action.destination}"
  when :special
    "Special move: #{action.source} to #{action.destination}"
  when :drop
    piece_str = action.piece ? "#{action.piece} " : ""
    "Drop #{piece_str}at #{action.destination}"
  when :modify
    "Transform piece at #{action.square} to #{action.piece}"
  end
end

describe_action("e2-e4")   # => "Move from e2 to e4"
describe_action("d1+f3")   # => "Capture at f3"
describe_action("P*e5")    # => "Drop P at e5"
describe_action("...")     # => "Player passes"

Transformation Detection

def has_promotion?(pan_string)
  action = Sashite::Pan.parse(pan_string)
  !action.transformation.nil?
end

has_promotion?("e2-e4")      # => false
has_promotion?("e7-e8=Q")    # => true
has_promotion?("P*e5")       # => false
has_promotion?("S*c3=+S")    # => true

Building Move Generators

class MoveBuilder
  def initialize(source)
    @source = source
  end

  def to(destination)
    Sashite::Pan::Action.move(@source, destination)
  end

  def captures(destination)
    Sashite::Pan::Action.capture(@source, destination)
  end

  def to_promoting(destination, piece)
    Sashite::Pan::Action.move(@source, destination, transformation: piece)
  end
end

# Usage
builder = MoveBuilder.new("e7")
builder.to("e8").to_s                    # => "e7-e8"
builder.to_promoting("e8", "Q").to_s     # => "e7-e8=Q"
builder.captures("d8").to_s              # => "e7+d8"

Validation Before Parsing

def safe_parse(pan_string)
  return nil unless Sashite::Pan.valid?(pan_string)

  Sashite::Pan.parse(pan_string)
rescue ArgumentError
  nil
end

safe_parse("e2-e4")      # => #<Pan::Action ...>
safe_parse("invalid")    # => nil

Pattern Matching (Ruby 3.0+)

def analyze(action)
  case action
  in { type: :move, source:, destination:, transformation: nil }
    "Simple move: #{source} → #{destination}"
  in { type: :move, transformation: piece }
    "Promotion to #{piece}"
  in { type: :capture, source:, destination: }
    "Capture: #{source} takes #{destination}"
  in { type: :drop, piece:, destination: }
    "Drop #{piece} at #{destination}"
  in { type: :pass }
    "Pass"
  else
    "Other action"
  end
end

action = Sashite::Pan.parse("e7-e8=Q")
analyze(action)  # => "Promotion to Q"

Properties

  • Operator-based: Intuitive symbols for different action types
  • Compact notation: Minimal character usage while maintaining readability
  • Game-agnostic: Works across chess, shōgi, xiangqi, and other abstract strategy games
  • CELL integration: Uses CELL coordinates for board positions
  • EPIN integration: Uses EPIN identifiers for piece representation
  • Immutable: All action objects are frozen
  • Functional: Pure functions with no side effects
  • Type-safe: Strong validation and error handling

Documentation

Development

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