Thanks to visit codestin.com
Credit goes to github.com

Skip to content
/ pmn.rb Public

A Ruby interface for data serialization in PMN format.

License

sashite/pmn.rb

Repository files navigation

Pmn.rb

Version Yard documentation Ruby License

PMN (Portable Move Notation) implementation for the Ruby language.

What is PMN?

PMN (Portable Move Notation) is a rule-agnostic, array-based format for describing the mechanical decomposition of moves in abstract strategy board games. PMN breaks down complex movements into sequences of atomic actions, revealing the underlying mechanics while remaining completely independent of specific game rules, validation logic, or gameplay concepts.

This gem implements the PMN Specification v1.0.0, providing a small, functional Ruby interface for working with mechanical move decomposition across any board game system.

Installation

# In your Gemfile
gem "sashite-pmn"

Or install manually:

gem install sashite-pmn

Dependencies

PMN builds upon three foundational Sashité specifications:

gem "sashite-cell"  # Multi-dimensional coordinate encoding
gem "sashite-hand"  # Reserve location notation
gem "sashite-qpi"   # Qualified Piece Identifier

Usage

Basic Operations

require "sashite/pmn"

# Parse PMN arrays into move objects
move = Sashite::Pmn.parse(["e2", "e4", "C:P"])
move.valid?                                   # => true
move.actions                                  # => [#<Sashite::Pmn::Action ...>]
move.to_a                                     # => ["e2", "e4", "C:P"]

# Validate PMN arrays
Sashite::Pmn.valid?(["e2", "e4", "C:P"])     # => true
Sashite::Pmn.valid?(%w[e2 e4])               # => true (inferred piece)
Sashite::Pmn.valid?([])                      # => true (pass move)
Sashite::Pmn.valid?(["e2"])                  # => false (incomplete)

# Create moves programmatically
move = Sashite::Pmn.from_actions([
                                   Sashite::Pmn::Action.new("e2", "e4", "C:P")
                                 ])

Pass Moves

A pass move is represented by an empty array [], allowing a player to voluntarily conclude their turn without performing any displacement or mutation.

# Parse a pass move
pass = Sashite::Pmn.parse([])
pass.valid?                                   # => true
pass.pass?                                    # => true
pass.empty?                                   # => true
pass.actions                                  # => []
pass.to_a                                     # => []

# Create a pass move directly
pass = Sashite::Pmn::Move.new
pass.pass? # => true

# Validate pass moves
Sashite::Pmn.valid?([]) # => true

Important: According to the Sashité Protocol, a pass move that results in a position identical to a previous position violates the uniqueness constraint. Game engines must enforce this constraint.

Action Decomposition

# Simple move with explicit piece
move = Sashite::Pmn.parse(["e2", "e4", "C:P"])
action = move.actions.first
action.source                                 # => "e2"
action.destination                            # => "e4"
action.piece                                  # => "C:P"
action.piece_specified?                       # => true

# Move with inferred piece
move = Sashite::Pmn.parse(%w[e2 e4])
action = move.actions.first
action.piece                                  # => nil
action.piece_specified?                       # => false
action.inferred?                              # => true

# In-place transformations (source == destination)
transform = Sashite::Pmn.parse(["e4", "e4", "C:+P"])
transform.valid?                              # => true
action = transform.actions.first
action.in_place?                              # => true
action.transformation?                        # => true

# Reserve operations
drop = Sashite::Pmn.parse(["*", "e5", "S:P"]) # Drop from reserve
capture = Sashite::Pmn.parse(["e4", "*"])     # Capture to reserve (inferred piece)

Complex Moves

# Multi-action move (castling)
castling = Sashite::Pmn.parse([
                                "e1", "g1", "C:K",
                                "h1", "f1", "C:R"
                              ])
castling.compound?                            # => true
castling.simple?                              # => false
castling.pass?                                # => false
castling.actions.size                         # => 2

# En passant
en_passant = Sashite::Pmn.parse([
                                  "e5", "f6", "C:P",
                                  "f5", "*", "c:p"
                                ])
en_passant.has_captures?                      # => true
en_passant.board_moves.size                   # => 1

Move Analysis

move = Sashite::Pmn.parse([
                            "e1", "g1", "C:K",
                            "h1", "f1", "C:R"
                          ])

# Structure analysis
move.pass?                                    # => false
move.simple?                                  # => false
move.compound?                                # => true
move.size                                     # => 2
move.empty?                                   # => false

# Drop/capture checks
move.has_drops?                               # => false
move.has_captures?                            # => false
move.board_moves.size                         # => 2

# Extract information
move.sources                                  # => ["e1", "h1"]
move.destinations                             # => ["g1", "f1"]
move.pieces                                   # => ["C:K", "C:R"]
move.has_inferred?                            # => false

Action Predicates

action = move.actions.first

# Location predicates
action.board_to_board?                        # => true
action.from_reserve?                          # => false
action.to_reserve?                            # => false
action.drop?                                  # => false
action.capture?                               # => false
action.board_move?                            # => true
action.in_place?                              # => false (source != destination)
action.transformation?                        # => false (not in-place with state change)

# Validation predicates
action.valid?                                 # => true
action.piece_valid?                           # => true or false depending on piece

Error Handling

# Invalid action built directly raises action-level errors
begin
  Sashite::Pmn::Action.new("invalid", "e4", "C:P")
rescue Sashite::Pmn::Error::Location => e
  puts e.message
end

begin
  Sashite::Pmn::Action.new("e2", "e4", "InvalidPiece")
rescue Sashite::Pmn::Error::Piece => e
  puts e.message
end

# Parsing a move wraps action-level errors as Error::Move
begin
  Sashite::Pmn.parse(["e2"]) # Incomplete action
rescue Sashite::Pmn::Error::Move => e
  puts e.message
end

# Pass moves are always valid structurally
Sashite::Pmn.parse([]).valid? # => true (never raises)

API Reference

Main Module Methods

  • Sashite::Pmn.parse(array) — Parse a PMN array into a Move object. Accepts empty arrays [] for pass moves.
  • Sashite::Pmn.valid?(array) — Check if an array is valid PMN notation (non-raising). Returns true for [].
  • Sashite::Pmn.from_actions(actions) — Build a Move from Action objects. Pass empty array for pass moves.
  • Sashite::Pmn.valid_location?(location) — Check if a location is valid (CELL or "*").
  • Sashite::Pmn.valid_piece?(piece) — Check if a piece is valid QPI.

Move Class

Creation

  • Sashite::Pmn::Move.new(*elements) — Create from PMN elements (variadic). Call with no arguments for pass move.
    • Move.new("e2", "e4", "C:P") — Standard move
    • Move.new — Pass move (empty)
    • Note: Move.new(["e2","e4","C:P"]) is not accepted; pass individual arguments.
  • Sashite::Pmn::Move.from_actions(actions) — Create from Action objects. Pass [] for pass move.

Validation & Data

  • #valid? — Check overall validity.
  • #actions — Ordered array of Action objects (frozen). Empty for pass moves.
  • #pmn_array — Original PMN elements (frozen). Empty for pass moves.
  • #to_a — Copy of the PMN elements. Returns [] for pass moves.

Structure & Queries

  • #size / #length — Number of actions. Returns 0 for pass moves.
  • #empty? — No actions? Returns true for pass moves.
  • #pass? — Is this a pass move? Returns true only for empty moves.
  • #simple? — Exactly one action? Returns false for pass moves.
  • #compound? — Multiple actions? Returns false for pass moves.
  • #first_action / #last_action — Convenience accessors. Return nil for pass moves.
  • #has_drops? / #has_captures? — Presence of drops/captures. Return false for pass moves.
  • #board_moves — Actions that are board-to-board. Returns [] for pass moves.
  • #sources / #destinations / #pieces — Unique lists. Return [] for pass moves.
  • #has_inferred? — Any action with inferred piece? Returns false for pass moves.

Action Class

Creation

  • Sashite::Pmn::Action.new(source, destination, piece = nil)

Data & Conversion

  • #source, #destination, #piece
  • #to_a["src", "dst"] or ["src", "dst", "piece"]
  • #to_h{ source:, destination:, piece: } (piece omitted if nil)

Predicates

  • #inferred?, #piece_specified?, #piece_valid?
  • #from_reserve?, #to_reserve?
  • #reserve_to_board? (drop), #board_to_reserve? (capture), #board_to_board?
  • #drop? (alias), #capture? (alias), #board_move?
  • #in_place? — Source equals destination?
  • #transformation? — In-place with state change?
  • #valid?

Exceptions

  • Sashite::Pmn::Error — Base error class
  • Sashite::Pmn::Error::Move — Invalid PMN sequence / parsing failure
  • Sashite::Pmn::Error::Action — Invalid atomic action
  • Sashite::Pmn::Error::Location — Invalid location (not CELL or HAND)
  • Sashite::Pmn::Error::Piece — Invalid piece (not QPI format)

Format Specification (Summary)

Structure

PMN moves are flat arrays containing action sequences or empty for pass moves:

[]                                            # Pass move
[<element-1>, <element-2>, <element-3>, ...] # Action sequence

Pass Move Format

A pass move is represented by an empty array:

[]

This indicates that the active player concludes their turn without performing any action. The resulting position must respect the position uniqueness constraint defined in the Sashité Protocol.

Action Format

Each action consists of 2 or 3 consecutive elements:

[<source>, <destination>, <piece>?]
  • Source: CELL coordinate or "*" (reserve)
  • Destination: CELL coordinate or "*" (reserve)
  • Piece: QPI string (optional; may be inferred)

Array Length Rules

  • Pass move: 0 elements (empty array)
  • Action moves: Minimum 2 elements (one action with inferred piece)
  • Valid non-empty lengths: multiple of 3, or multiple of 3 plus 2

In-Place Actions

Actions where source == destination are allowed, enabling:

  • In-place transformations with state change (e.g., ["e4", "e4", "C:+P"])
  • Context-dependent mutations specified by game rules

Important: Actions without observable effect (same location, same piece state) should be avoided. Use pass moves [] instead for turn-only actions.

Game Examples

Pass Moves

# Player passes their turn
pass = Sashite::Pmn.parse([])
pass.pass? # => true

# In games where passing is strategic (e.g., Go, some variants)
game_engine.execute_move([]) # Pass turn to opponent

Western Chess

# Pawn move
pawn_move = Sashite::Pmn.parse(["e2", "e4", "C:P"])

# Castling kingside
castling = Sashite::Pmn.parse([
                                "e1", "g1", "C:K",
                                "h1", "f1", "C:R"
                              ])

# En passant
en_passant = Sashite::Pmn.parse([
                                  "e5", "f6", "C:P",
                                  "f5", "*", "c:p"
                                ])

# Promotion
promotion = Sashite::Pmn.parse(["e7", "e8", "C:Q"])

# In-place promotion (variant rule)
in_place_promotion = Sashite::Pmn.parse(["e8", "e8", "C:Q"])

Japanese Shōgi

# Drop piece from hand
drop = Sashite::Pmn.parse(["*", "e5", "S:P"])

# Capture and convert
capture = Sashite::Pmn.parse([
                               "a1", "*", "S:L",
                               "b2", "a1", "S:S"
                             ])

# Promotion
promotion = Sashite::Pmn.parse(["h8", "i8", "S:+S"])

# Pass (uncommon in Shōgi but structurally valid)
pass = Sashite::Pmn.parse([])

Chinese Xiangqi

# General move
general_move = Sashite::Pmn.parse(["e1", "e2", "X:G"])

# Cannon capture (jumping)
cannon_capture = Sashite::Pmn.parse([
                                      "b3", "*", "x:s",
                                      "b1", "b9", "X:C"
                                    ])

Go

# Place stone
place_stone = Sashite::Pmn.parse(["*", "d4", "G:B"])

# Pass (very common in Go)
pass = Sashite::Pmn.parse([])
pass.pass? # => true

# Consecutive passes typically end the game in Go
end_game_and_score if last_move.pass? && current_move.pass?

Advanced Usage

Move Composition

actions = []
actions << Sashite::Pmn::Action.new("e2", "e4", "C:P")
actions << Sashite::Pmn::Action.new("d7", "d5", "c:p")

move = Sashite::Pmn.from_actions(actions)
move.to_a # => ["e2", "e4", "C:P", "d7", "d5", "c:p"]

# Create pass move
pass = Sashite::Pmn.from_actions([])
pass.pass? # => true

Integration with Game Engines

class GameEngine
  def execute_move(pmn_array)
    move = Sashite::Pmn.parse(pmn_array)

    # Handle pass moves
    if move.pass?
      handle_pass_move
      switch_active_player
      return
    end

    # Execute each action
    move.actions.each do |action|
      if action.from_reserve?
        place_piece(action.destination, action.piece)
      elsif action.to_reserve?
        capture_piece(action.source)
      elsif action.in_place?
        transform_piece(action.source, action.piece)
      else
        move_piece(action.source, action.destination, action.piece)
      end
    end

    switch_active_player
  end

  private

  def handle_pass_move
    # Check position uniqueness constraint
    raise "Pass move creates repeated position (protocol violation)" if position_seen_before?(current_position)

    # Record pass in game history
    record_move([])
  end

  # ...
end

Position Tracking with Pass Moves

class PositionTracker
  def initialize
    @seen_positions = Set.new
    @position_history = []
  end

  def record_move(pmn_array, resulting_position)
    move = Sashite::Pmn.parse(pmn_array)

    # For pass moves, verify uniqueness constraint
    if move.pass? && @seen_positions.include?(resulting_position)
      raise "Pass move violates position uniqueness constraint"
    end

    @seen_positions.add(resulting_position)
    @position_history << { move: pmn_array, position: resulting_position }
  end

  def position_seen?(position)
    @seen_positions.include?(position)
  end
end

Design Properties

  • Rule-agnostic: Independent of specific game mechanics
  • Mechanical decomposition: Breaks complex moves into atomic actions
  • Array-based: Simple, interoperable structure
  • Pass move support: Empty arrays [] for voluntary turn conclusion
  • Sequential execution: Actions execute in array order
  • Piece inference: Optional piece specification when context is clear
  • Universal applicability: Works across board game systems
  • Functional design: Immutable data structures
  • Dependency integration: CELL, HAND, and QPI specs

Mechanical Semantics (Recap)

Pass Moves

Pass moves ([]) indicate:

  • No piece displacement occurs
  • No piece mutation occurs
  • The active player voluntarily concludes their turn
  • The resulting position must be unique according to protocol constraints

Action Execution

For non-pass moves, each action applies atomically:

  1. Source state change:

    • CELL → becomes empty
    • HAND "*" → remove piece from reserve
  2. Destination state change:

    • CELL → contains final piece
    • HAND "*" → add piece to reserve
  3. Piece transformation: Final state (specified or inferred)

  4. Atomic commitment: Each action applies atomically

Protocol Compliance

Position Uniqueness Constraint

According to the Sashité Protocol, all positions within a match must be unique. This applies to pass moves:

  • A pass move that results in a position identical to any previous position violates the constraint
  • Game engines must track position history and enforce this rule
  • However, if the position after a pass move is unique (due to time-sensitive state or other factors), the pass is valid

Authorized Operations

PMN supports all protocol-authorized operations:

  • Board-to-board: Standard piece movement
  • Board-to-hand: Captures to reserve
  • Hand-to-board: Drops from reserve
  • In-place transformations: State changes without displacement
  • Pass moves: Voluntary turn conclusion

Prohibited: Hand-to-hand transfers are not allowed by the protocol.

License

Available as open source under the MIT License.

Contributing

Bug reports and pull requests are welcome on GitHub at https://github.com/sashite/pmn.rb.

See Also

About

Maintained by Sashité — promoting chess variants and sharing the beauty of board game cultures.

About

A Ruby interface for data serialization in PMN format.

Topics

Resources

License

Code of conduct

Stars

Watchers

Forks

Contributors 2

  •  
  •