Skip to content

sashite/feen.ex

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

1 Commit
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Sashite.Feen

Hex.pm Docs License

FEEN (Field Expression Encoding Notation) implementation for Elixir.

What is FEEN?

FEEN (Field Expression Encoding Notation) is a rule-agnostic position encoding for two-player, turn-based board games built on the Sashité Game Protocol.

A FEEN string encodes exactly:

  1. Board occupancy (which Pieces are on which Squares)
  2. Hands (multisets of off-board Pieces held by each Player)
  3. Side styles and the Active Player

This library implements the FEEN Specification v1.0.0.

Installation

Add sashite_feen to your list of dependencies in mix.exs:

def deps do
  [
    {:sashite_feen, "~> 1.0"}
  ]
end

This will also install sashite_epin and sashite_sin as transitive dependencies.

Quick Start

# Parse a FEEN string (Western Chess starting position)
feen_string = "+rnbq+k^bn+r/+p+p+p+p+p+p+p+p/8/8/8/8/+P+P+P+P+P+P+P+P/+RNBQ+K^BN+R / C/c"

{:ok, position} = Sashite.Feen.parse(feen_string)

# Access position components
position.piece_placement   # Board state
position.hands             # Captured pieces
position.style_turn        # Active player and styles

# Serialize back to FEEN string
Sashite.Feen.to_string(position)  # => "+rnbq+k^bn+r/..."

# Validate a FEEN string
Sashite.Feen.valid?(feen_string)  # => true

Format Overview

A FEEN string consists of three fields separated by single ASCII spaces:

<PIECE-PLACEMENT> <HANDS> <STYLE-TURN>

Example: Chess Starting Position

+rnbq+k^bn+r/+p+p+p+p+p+p+p+p/8/8/8/8/+P+P+P+P+P+P+P+P/+RNBQ+K^BN+R / C/c
|----------------------------------------------------| |-| |---|
                   Piece Placement                    Hands Style-Turn

Example: Shogi Starting Position

lnsgk^gsnl/1r5b1/ppppppppp/9/9/9/PPPPPPPPP/1B5R1/LNSGK^GSNL / S/s

Example: Position with Captured Pieces

r1bqk^b1r/+p+p+p+p1+p+p+p/2n2n2/4p3/2B1P3/5N2/+P+P+P+P1+P+P+P/RNBQK^2R 2P/p C/c
                                                              |--| |-|
                                                              First Second
                                                              hand  hand

Usage

Parsing FEEN Strings

# Parse with error handling
case Sashite.Feen.parse(feen_string) do
  {:ok, position} -> # use position
  {:error, reason} -> # handle error
end

# Bang version (raises on invalid input)
position = Sashite.Feen.parse!(feen_string)

# Validate without parsing
Sashite.Feen.valid?(feen_string)  # => true/false

Accessing Position Components

position = Sashite.Feen.parse!("+rnbq+k^bn+r/+p+p+p+p+p+p+p+p/8/8/8/8/+P+P+P+P+P+P+P+P/+RNBQ+K^BN+R / C/c")

# Piece Placement (board state)
position.piece_placement
# => %{squares: [[...], [...], ...], separators: [1, 1, 1, 1, 1, 1, 1]}

# Hands (captured pieces)
position.hands
# => %{first: [], second: []}

# Style-Turn (active player and styles)
position.style_turn
# => %{active: %Sashite.Sin{style: :C, side: :first}, inactive: %Sashite.Sin{style: :C, side: :second}}

Working with Piece Placement

The piece_placement field contains:

  • squares — List of segments, each segment is a list of squares (nil for empty, %Sashite.Epin{} for piece)
  • separators — List of separator counts between consecutive segments
position = Sashite.Feen.parse!("8/8/8/8/8/8/8/8 / C/c")

# Access segments
position.piece_placement.squares
# => [[nil, nil, nil, nil, nil, nil, nil, nil], ...]

# Access separator structure (for multi-dimensional boards)
position.piece_placement.separators
# => [1, 1, 1, 1, 1, 1, 1]

# Iterate over a segment
Enum.each(hd(position.piece_placement.squares), fn
  nil -> IO.puts("Empty square")
  %Sashite.Epin{} = epin -> IO.puts("Piece: #{epin}")
end)

Working with Hands

The hands field contains:

  • first — List of %Sashite.Epin{} structs held by first player
  • second — List of %Sashite.Epin{} structs held by second player
position = Sashite.Feen.parse!("8/8/8/8/8/8/8/8 2P/p C/c")

# Access first player's hand
position.hands.first
# => [%Sashite.Epin{...}, %Sashite.Epin{...}]

length(position.hands.first)  # => 2

# Access second player's hand
position.hands.second
# => [%Sashite.Epin{...}]

Working with Style-Turn

The style_turn field contains:

  • active%Sashite.Sin{} of the active player (to move)
  • inactive%Sashite.Sin{} of the inactive player
position = Sashite.Feen.parse!("8/8/8/8/8/8/8/8 / C/c")

# Get active player's style
position.style_turn.active
# => %Sashite.Sin{style: :C, side: :first}

# Get inactive player's style
position.style_turn.inactive
# => %Sashite.Sin{style: :C, side: :second}

# Check who is to move
Sashite.Sin.first_player?(position.style_turn.active)   # => true
Sashite.Sin.second_player?(position.style_turn.active)  # => false

Serialization

# Convert position back to FEEN string
feen_string = Sashite.Feen.to_string(position)

# String.Chars protocol is implemented
"Position: #{position}"

# The output is always canonical

Field Specifications

Field 1 — Piece Placement

Encodes board occupancy as a stream of tokens organized into segments separated by /:

  • Empty-count tokens: integers representing runs of empty squares (e.g., 8 = 8 empty squares)
  • Piece tokens: valid EPIN tokens (e.g., +K^, p, R')
# 8x8 board with pieces
"+rnbq+k^bn+r/+p+p+p+p+p+p+p+p/8/8/8/8/+P+P+P+P+P+P+P+P/+RNBQ+K^BN+R"

# Multi-dimensional board (3D Raumschach uses //)
"+rn+k^n+r/+p+p+p+p+p/5/5/5//buqbu/+p+p+p+p+p/5/5/5//..."

Field 2 — Hands

Encodes pieces held by each player, separated by /:

<FIRST-HAND>/<SECOND-HAND>
  • Each hand is a concatenation of [count]<piece> items
  • Count is optional (absent = 1, present ≥ 2)
  • Empty hands are represented as empty strings
"/"        # Both hands empty
"2P/p"     # First has 2 pawns, second has 1 pawn
"3P2B/2p"  # First has 3 pawns + 2 bishops, second has 2 pawns

Field 3 — Style-Turn

Encodes native styles and active player:

<ACTIVE-STYLE>/<INACTIVE-STYLE>
  • Each style is a valid SIN token (single ASCII letter)
  • Uppercase = Side first, lowercase = Side second
  • Position determines who is active
"C/c"  # First player (Chess-style) to move
"c/C"  # Second player (Chess-style) to move
"S/s"  # First player (Shogi-style) to move
"M/c"  # First player (Makruk-style) vs second player (Chess-style), first to move

Game Examples

Western Chess

# Starting position
"+rnbq+k^bn+r/+p+p+p+p+p+p+p+p/8/8/8/8/+P+P+P+P+P+P+P+P/+RNBQ+K^BN+R / C/c"

# After 1.e4
"+rnbq+k^bn+r/+p+p+p+p+p+p+p+p/8/8/4P3/8/+P+P+P+P1+P+P+P/+RNBQ+K^BN+R / c/C"

# After 1.e4 c5 (Sicilian Defense)
"+rnbq+k^bn+r/+p+p1+p+p+p+p+p/8/2p5/4P3/8/+P+P+P+P1+P+P+P/+RNBQ+K^BN+R / C/c"

Japanese Shogi

# Starting position
"lnsgk^gsnl/1r5b1/ppppppppp/9/9/9/PPPPPPPPP/1B5R1/LNSGK^GSNL / S/s"

# After 1.P-7f
"lnsgk^gsnl/1r5b1/ppppppppp/9/9/2P6/PP1PPPPPP/1B5R1/LNSGK^GSNL / s/S"

Chinese Xiangqi

# Starting position
"rheag^aehr/9/1c5c1/p1p1p1p1p/9/9/P1P1P1P1P/1C5C1/9/RHEAG^AEHR / X/x"

Thai Makruk

# Starting position
"rnsmk^snr/8/pppppppp/8/8/PPPPPPPP/8/RNSK^MSNR / M/m"

Cross-Style Games

# Chess vs Makruk hybrid
"rnsmk^snr/8/pppppppp/8/8/8/+P+P+P+P+P+P+P+P/+RNBQ+K^BN+R / C/m"

API Reference

Main Module

# Parse FEEN string
Sashite.Feen.parse(feen_string)   # => {:ok, %Sashite.Feen{}} | {:error, reason}
Sashite.Feen.parse!(feen_string)  # => %Sashite.Feen{} | raises ArgumentError

# Validate string
Sashite.Feen.valid?(feen_string)  # => boolean

# Serialize position
Sashite.Feen.to_string(position)  # => String.t()

Data Structure

%Sashite.Feen{
  piece_placement: %{
    squares: [[Sashite.Epin.t() | nil]],  # List of segments
    separators: [pos_integer()]            # Separator counts between segments
  },
  hands: %{
    first: [Sashite.Epin.t()],   # First player's hand
    second: [Sashite.Epin.t()]   # Second player's hand
  },
  style_turn: %{
    active: Sashite.Sin.t(),     # Active player's style
    inactive: Sashite.Sin.t()    # Inactive player's style
  }
}

Canonical Form

FEEN output is always canonical:

  • Empty-count tokens use minimal base-10 integers (no leading zeros)
  • Hand items are aggregated and sorted deterministically:
    1. By multiplicity (descending)
    2. By EPIN base letter (case-insensitive alphabetical)
    3. By EPIN letter case (uppercase before lowercase)
    4. By EPIN state modifier (- before + before none)
    5. By EPIN terminal marker (absent before present)
    6. By EPIN derivation marker (absent before present)
# Non-canonical input is accepted and normalized on output
{:ok, pos} = Sashite.Feen.parse("8/8/8/8/8/8/8/8 PpP/p C/c")
Sashite.Feen.to_string(pos)
# => "8/8/8/8/8/8/8/8 2Pp/p C/c"

Dependencies

FEEN builds on these Sashité specifications:

  • EPIN — Extended Piece Identifier Notation for piece tokens
  • SIN — Style Identifier Notation for style-turn encoding

Design Properties

FEEN is designed to be:

  • Rule-agnostic — No game-specific rules embedded
  • Protocol-aligned — Compatible with the Game Protocol's Position model
  • Compact — Run-length encoding for empty squares, multiplicities for hands
  • Multi-dimensional friendly — Separator groups preserve higher-dimensional boundaries
  • Canonical — Single canonical string for each position
  • Engine-friendly — Suitable for hashing, caching, and repetition detection

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

FEEN support for the Elixir language.

Resources

License

Stars

Watchers

Forks

Packages

No packages published

Languages