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

Skip to content
/ ggn.rb Public

Ruby implementation of General Gameplay Notation.

License

sashite/ggn.rb

Repository files navigation

Ggn.rb

Version Yard documentation Ruby License

GGN (General Gameplay Notation) implementation for Ruby — a pure, functional library for evaluating movement possibilities in abstract strategy board games.


What is GGN?

GGN (General Gameplay Notation) is a rule-agnostic format for describing pseudo-legal moves in abstract strategy board games. GGN serves as a movement possibility oracle: given a movement context (piece and source location) plus a destination location, it determines if the movement is feasible under specified pre-conditions.

This gem implements the GGN Specification v1.0.0, providing complete movement possibility evaluation with environmental constraint checking.

Core Philosophy

GGN answers the fundamental question:

Can this piece, currently at this location, reach that location?

It encodes:

  • Which piece (via QPI format)
  • From where (source location using CELL or HAND)
  • To where (destination location using CELL or HAND)
  • Which environmental pre-conditions must hold (must)
  • Which environmental pre-conditions must not hold (deny)
  • What changes occur if executed (diff in STN format)

Installation

# In your Gemfile
gem "sashite-ggn"

Or install manually:

gem install sashite-ggn

Dependencies

GGN builds upon foundational Sashité specifications:

gem "sashite-cell"  # Coordinate Encoding for Layered Locations
gem "sashite-hand"  # Hold And Notation Designator
gem "sashite-lcn"   # Location Condition Notation
gem "sashite-qpi"   # Qualified Piece Identifier
gem "sashite-stn"   # State Transition Notation

Quick Start

require "sashite/ggn"

# Define GGN data structure
ggn_data = {
  "C:P" => {
    "e2" => {
      "e4" => [
        {
          "must" => { "e3" => "empty", "e4" => "empty" },
          "deny" => {},
          "diff" => {
            "board" => { "e2" => nil, "e4" => "C:P" },
            "toggle" => true
          }
        }
      ]
    }
  }
}

# Validate GGN structure
Sashite::Ggn.valid?(ggn_data) # => true

# Parse into ruleset
ruleset = Sashite::Ggn.parse(ggn_data)

# Query movement possibility through method chaining
source = ruleset.select("C:P")
destination = source.from("e2")
engine = destination.to("e4")

# Evaluate against position
active_side = :first
squares = {
  "e2" => "C:P",
  "e3" => nil,
  "e4" => nil
}

transitions = engine.where(active_side, squares)
transitions.any? # => true

API Reference

Module Functions

Sashite::Ggn.parse(data) → Ruleset

Parses GGN data structure into an immutable Ruleset object.

ruleset = Sashite::Ggn.parse(ggn_data)

Parameters:

  • data (Hash): GGN data structure conforming to specification

Returns: Ruleset — Immutable ruleset object

Raises: ArgumentError — If data structure is invalid


Sashite::Ggn.valid?(data) → Boolean

Validates GGN data structure against specification.

Sashite::Ggn.valid?(ggn_data) # => true

Parameters:

  • data (Hash): Data structure to validate

Returns: Boolean — True if valid, false otherwise


Sashite::Ggn::Ruleset Class

Immutable container for GGN movement rules.

#select(piece) → Source

Selects movement rules for a specific piece type.

source = ruleset.select("C:K")

Parameters:

  • piece (String): QPI piece identifier

Returns: Source — Source selector object

Raises: KeyError — If piece not found in ruleset


#piece?(piece) → Boolean

Checks if ruleset contains movement rules for specified piece.

ruleset.piece?("C:K") # => true

Parameters:

  • piece (String): QPI piece identifier

Returns: Boolean


#pieces → Array<String>

Returns all piece identifiers in ruleset.

ruleset.pieces # => ["C:K", "C:Q", "C:P", ...]

Returns: Array<String> — QPI piece identifiers


Sashite::Ggn::Ruleset::Source Class

Represents movement possibilities for a piece type.

#from(source) → Destination

Specifies the source location for the piece.

destination = source.from("e1")

Parameters:

  • source (String): Source location (CELL coordinate or HAND "*")

Returns: Destination — Destination selector object

Raises: KeyError — If source not found for this piece


#sources → Array<String>

Returns all valid source locations for this piece.

source.sources # => ["e1", "d1", "*"]

Returns: Array<String> — Source locations


#source?(location) → Boolean

Checks if location is a valid source for this piece.

source.source?("e1") # => true

Parameters:

  • location (String): Source location

Returns: Boolean


Sashite::Ggn::Ruleset::Source::Destination Class

Represents movement possibilities from a specific source.

#to(destination) → Engine

Specifies the destination location.

engine = destination.to("e2")

Parameters:

  • destination (String): Destination location (CELL coordinate or HAND "*")

Returns: Engine — Movement evaluation engine

Raises: KeyError — If destination not found from this source


#destinations → Array<String>

Returns all valid destinations from this source.

destination.destinations # => ["d1", "d2", "e2", "f2", "f1"]

Returns: Array<String> — Destination locations


#destination?(location) → Boolean

Checks if location is a valid destination from this source.

destination.destination?("e2") # => true

Parameters:

  • location (String): Destination location

Returns: Boolean


Sashite::Ggn::Ruleset::Source::Destination::Engine Class

Evaluates movement possibility under given position conditions.

#where(active_side, squares) → Array<Transition>

Evaluates movement against position and returns valid transitions.

active_side = :first
squares = {
  "e2" => "C:P",  # White pawn on e2
  "e3" => nil,    # Empty square
  "e4" => nil     # Empty square
}

transitions = engine.where(active_side, squares)

Parameters:

  • active_side (Symbol): Active player side (:first or :second)
  • squares (Hash): Board state where keys are CELL coordinates and values are QPI identifiers or nil for empty squares

Returns: Array<Sashite::Stn::Transition> — Valid state transitions (may be empty)


GGN Format

Structure

{
  "<qpi-piece>" => {
    "<source-location>" => {
      "<destination-location>" => [
        {
          "must" => { /* LCN format */ },
          "deny" => { /* LCN format */ },
          "diff" => { /* STN format */ }
        }
      ]
    }
  }
}

Field Specifications

Field Type Description
Piece String (QPI) Piece identifier (e.g., "C:K", "s:+p")
Source String (CELL/HAND) Origin location (e.g., "e2", "*")
Destination String (CELL/HAND) Target location (e.g., "e4", "*")
must Hash (LCN) Pre-conditions that must be satisfied
deny Hash (LCN) Pre-conditions that must not be satisfied
diff Hash (STN) State transition specification

Note (normative): To preserve GGN's board-reachability scope, entries where source="*" and destination="*" (direct HAND→HAND) are forbidden by the specification.


Usage Examples

Method Chaining

# Query specific movement
active_side = :first
squares = {
  "e2" => "C:P",
  "e3" => nil,
  "e4" => nil
}

transitions = ruleset
  .select("C:P")
  .from("e2")
  .to("e4")
  .where(active_side, squares)

transitions.size # => 1
transitions.first.board_changes # => { "e2" => nil, "e4" => "C:P" }

Building Board State

# Example: Build squares hash from FEEN position
require "sashite/feen"

feen = "+rnbq+kbn+r/+p+p+p+p+p+p+p+p/8/8/8/8/+P+P+P+P+P+P+P+P/+RNBQ+KBN+R / C/c"
position = Sashite::Feen.parse(feen)

# Extract active player side
active_side = position.styles.active.side # => :first

# Build squares hash from placement
squares = {}
position.placement.ranks.each_with_index do |rank, rank_idx|
  rank.each_with_index do |piece, file_idx|
    # Convert rank_idx and file_idx to CELL coordinate
    cell = Sashite::Cell.from_indices(file_idx, 7 - rank_idx)
    squares[cell] = piece&.to_s
  end
end

# Use with GGN
transitions = engine.where(active_side, squares)

Capture Validation

# Check capture possibility
active_side = :first
squares = {
  "e4" => "C:P",  # White pawn
  "d5" => "c:p",  # Black pawn (enemy)
  "f5" => "c:p"   # Black pawn (enemy)
}

# Pawn can capture diagonally
capture_engine = ruleset.select("C:P").from("e4").to("d5")
transitions = capture_engine.where(active_side, squares)

transitions.any? # => true if capture is allowed

Existence Checks

# Check if piece exists in ruleset
ruleset.piece?("C:K") # => true

# Check valid sources
source = ruleset.select("C:K")
source.source?("e1") # => true

# Check valid destinations
destination = source.from("e1")
destination.destination?("e2") # => true

Introspection

# List all pieces
ruleset.pieces # => ["C:K", "C:Q", "C:R", ...]

# List sources for a piece
source.sources # => ["e1", "d1", "f1", ...]

# List destinations from a source
destination.destinations # => ["d1", "d2", "e2", "f2", "f1"]

Design Properties

  • Functional: Pure functions with no side effects
  • Immutable: All data structures frozen and unchangeable
  • Composable: Clean method chaining for natural query flow
  • Minimal API: Only exposes what's necessary
  • Type-safe: Strict validation of all inputs
  • Lightweight: Minimal dependencies, no unnecessary parsing
  • Spec-compliant: Strictly follows GGN v1.0.0 specification

Error Handling

# Handle missing piece
begin
  source = ruleset.select("INVALID:X")
rescue KeyError => e
  puts "Piece not found: #{e.message}"
end

# Handle missing source
begin
  destination = source.from("z9")
rescue KeyError => e
  puts "Source not found: #{e.message}"
end

# Handle missing destination
begin
  engine = destination.to("z9")
rescue KeyError => e
  puts "Destination not found: #{e.message}"
end

# Safe validation before parsing
if Sashite::Ggn.valid?(data)
  ruleset = Sashite::Ggn.parse(data)
else
  puts "Invalid GGN structure"
end

Related Specifications


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

Ruby implementation of General Gameplay Notation.

Resources

License

Code of conduct

Stars

Watchers

Forks