Module: Sashite::Lcn

Defined in:
lib/sashite/lcn.rb,
lib/sashite/lcn/conditions.rb

Overview

LCN (Location Condition Notation) implementation for Ruby

Provides a rule-agnostic format for describing location conditions in abstract strategy board games. LCN provides a standardized way to express constraints on board locations, defining what states specific locations should have.

## Concept

LCN is a foundational specification designed to be used by other formats that require location state definitions. It expresses environmental constraints as key-value pairs where keys are CELL coordinates and values are state values.

## State Value Types

LCN supports two categories of state values:

### Reserved Keywords

  • ‘“empty”`: Location should be unoccupied

  • ‘“enemy”`: Location should contain a piece from the opposing side (context-dependent)

### QPI Piece Identifiers

  • Exact piece requirements using QPI format (e.g., “C:K”, “c:p”, “S:+P”)

## Context Dependency

The ‘“enemy”` keyword is context-dependent and requires a reference piece for interpretation. The consuming specification must provide:

  1. Reference piece identification (which piece determines the perspective)

  2. Side determination logic (how the reference piece’s side is established)

  3. Enemy evaluation rules (how opposing pieces are identified)

## Format Structure

LCN uses a simple JSON object structure: “‘json

"<cell-coordinate>": "<state-value>"

“‘

Where:

  • Keys: CELL coordinates (string, conforming to CELL specification)

  • Values: State values (reserved keywords or QPI identifiers)

## Examples

### Basic Conditions

# Empty location requirement
conditions = Sashite::Lcn.parse({ "e4" => "empty" })
conditions[:e4]  # => "empty"

# Enemy piece requirement (context-dependent)
conditions = Sashite::Lcn.parse({ "f5" => "enemy" })
conditions[:f5]  # => "enemy"

### Specific Piece Requirements

conditions = Sashite::Lcn.parse({
  "h1" => "C:+R",    # Chess rook with castling rights
  "e1" => "C:+K",    # Chess king with castling rights
  "f5" => "c:-p"     # Enemy pawn vulnerable to en passant
})

### Complex Path Conditions

# Path clearance for movement
path_conditions = Sashite::Lcn.parse({
  "b2" => "empty",
  "c3" => "empty",
  "d4" => "empty"
})

# Castling requirements
castling = Sashite::Lcn.parse({
  "f1" => "empty",
  "g1" => "empty",
  "h1" => "C:+R"
})

### Empty Conditions

no_conditions = Sashite::Lcn.parse({})
no_conditions.empty?  # => true

## Design Properties

  • Rule-agnostic: Independent of specific game mechanics

  • Context-neutral: Provides format without imposing evaluation logic

  • Composable: Can be combined by consuming specifications

  • Type-safe: Clear distinction between keywords and piece identifiers

  • **Minimal syntax**: Clean key-value pairs without nesting

  • Functional: Pure functions with no side effects

  • Immutable: All instances are frozen for thread safety

Defined Under Namespace

Classes: Conditions

Constant Summary collapse

EMPTY_KEYWORD =

Reserved keywords for common location states

"empty"
ENEMY_KEYWORD =
"enemy"
RESERVED_KEYWORDS =
[EMPTY_KEYWORD, ENEMY_KEYWORD].freeze

Class Method Summary collapse

Class Method Details

.parse(data) ⇒ Lcn::Conditions

Parse LCN data into a Conditions object

Creates a new LCN Conditions instance by parsing and validating the input data. Accepts both string and symbol keys for compatibility, but internally normalizes to symbol keys for Ruby idiomatic usage.

Examples:

Parse different LCN formats

# String keys (as per specification)
conditions = Sashite::Lcn.parse({ "e4" => "empty", "f5" => "enemy" })
conditions[:e4]  # => "empty"

# Symbol keys (Ruby idiomatic)
conditions = Sashite::Lcn.parse({ e4: "empty", f5: "enemy" })
conditions[:e4]  # => "empty"

# Mixed QPI and keywords
conditions = Sashite::Lcn.parse({
  "f1" => "empty",
  "g1" => "empty",
  "h1" => "C:+R"
})

Error handling

Sashite::Lcn.parse({ "invalid" => "empty" })  # => ArgumentError: Invalid CELL coordinate: invalid
Sashite::Lcn.parse({ "e4" => "unknown" })     # => ArgumentError: Invalid state value: unknown
Sashite::Lcn.parse("not a hash")              # => ArgumentError: LCN data must be a Hash

Parameters:

  • data (Hash)

    LCN conditions data with CELL coordinates as keys

Returns:

Raises:

  • (ArgumentError)

    if the data is invalid (not a Hash, invalid keys, or invalid values)



168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
# File 'lib/sashite/lcn.rb', line 168

def self.parse(data)
  raise ArgumentError, "LCN data must be a Hash" unless data.is_a?(Hash)

  validated_conditions = {}

  data.each do |location, state_value|
    # Convert location to string for validation, then to symbol for storage
    location_str = location.to_s

    raise ArgumentError, "Invalid CELL coordinate: #{location_str}" unless valid_location?(location_str)

    raise ArgumentError, "Invalid state value: #{state_value}" unless valid_state_value?(state_value)

    # Store with symbol key for Ruby idiomatic access
    validated_conditions[location_str.to_sym] = state_value
  end

  Conditions.new(**validated_conditions)
end

.valid?(data) ⇒ Boolean

Check if data represents valid LCN conditions

Validates that the data is a Hash with:

  • Keys: Valid CELL coordinates

  • Values: Reserved keywords (“empty”, “enemy”) or valid QPI identifiers

Examples:

Validate various LCN formats

Sashite::Lcn.valid?({ "e4" => "empty" })          # => true
Sashite::Lcn.valid?({ "f5" => "enemy" })          # => true
Sashite::Lcn.valid?({ "h1" => "C:+R" })           # => true
Sashite::Lcn.valid?({ "invalid" => "empty" })     # => false (invalid CELL)
Sashite::Lcn.valid?({ "e4" => "unknown" })        # => false (invalid state)
Sashite::Lcn.valid?("not a hash")                 # => false
Sashite::Lcn.valid?({})                           # => true (empty conditions)

Parameters:

  • data (Object)

    the data to validate

Returns:

  • (Boolean)

    true if valid LCN data, false otherwise



130
131
132
133
134
135
136
# File 'lib/sashite/lcn.rb', line 130

def self.valid?(data)
  return false unless data.is_a?(Hash)

  data.all? do |location, state_value|
    valid_location?(location) && valid_state_value?(state_value)
  end
end