GGN (General Gameplay Notation) implementation for Ruby — a pure, functional library for evaluating movement possibilities in abstract strategy board games.
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.
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 (
diffin STN format)
# In your Gemfile
gem "sashite-ggn"Or install manually:
gem install sashite-ggnGGN 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 Notationrequire "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? # => trueParses 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
Validates GGN data structure against specification.
Sashite::Ggn.valid?(ggn_data) # => trueParameters:
data(Hash): Data structure to validate
Returns: Boolean — True if valid, false otherwise
Immutable container for GGN movement rules.
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
Checks if ruleset contains movement rules for specified piece.
ruleset.piece?("C:K") # => trueParameters:
piece(String): QPI piece identifier
Returns: Boolean
Returns all piece identifiers in ruleset.
ruleset.pieces # => ["C:K", "C:Q", "C:P", ...]Returns: Array<String> — QPI piece identifiers
Represents movement possibilities for a piece type.
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
Returns all valid source locations for this piece.
source.sources # => ["e1", "d1", "*"]Returns: Array<String> — Source locations
Checks if location is a valid source for this piece.
source.source?("e1") # => trueParameters:
location(String): Source location
Returns: Boolean
Represents movement possibilities from a specific source.
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
Returns all valid destinations from this source.
destination.destinations # => ["d1", "d2", "e2", "f2", "f1"]Returns: Array<String> — Destination locations
Checks if location is a valid destination from this source.
destination.destination?("e2") # => trueParameters:
location(String): Destination location
Returns: Boolean
Evaluates movement possibility under given position conditions.
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 (:firstor:second)squares(Hash): Board state where keys are CELL coordinates and values are QPI identifiers ornilfor empty squares
Returns: Array<Sashite::Stn::Transition> — Valid state transitions (may be empty)
{
"<qpi-piece>" => {
"<source-location>" => {
"<destination-location>" => [
{
"must" => { /* LCN format */ },
"deny" => { /* LCN format */ },
"diff" => { /* STN format */ }
}
]
}
}
}| 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.
# 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" }# 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)# 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# 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# 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"]- 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
# 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- GGN v1.0.0 — General Gameplay Notation specification
- CELL v1.0.0 — Coordinate encoding
- HAND v1.0.0 — Reserve notation
- LCN v1.0.0 — Location conditions
- QPI v1.0.0 — Piece identification
- STN v1.0.0 — State transitions
Available as open source under the MIT License.
Maintained by Sashité — promoting chess variants and sharing the beauty of board game cultures.