FEEN (Field Expression Encoding Notation) implementation for Elixir.
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:
- Board occupancy (which Pieces are on which Squares)
- Hands (multisets of off-board Pieces held by each Player)
- Side styles and the Active Player
This library implements the FEEN Specification v1.0.0.
Add sashite_feen to your list of dependencies in mix.exs:
def deps do
[
{:sashite_feen, "~> 1.0"}
]
endThis will also install sashite_epin and sashite_sin as transitive dependencies.
# 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) # => trueA FEEN string consists of three fields separated by single ASCII spaces:
<PIECE-PLACEMENT> <HANDS> <STYLE-TURN>
+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
lnsgk^gsnl/1r5b1/ppppppppp/9/9/9/PPPPPPPPP/1B5R1/LNSGK^GSNL / S/s
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
# 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
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}}The piece_placement field contains:
squares— List of segments, each segment is a list of squares (nilfor 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)The hands field contains:
first— List of%Sashite.Epin{}structs held by first playersecond— 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{...}]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# Convert position back to FEEN string
feen_string = Sashite.Feen.to_string(position)
# String.Chars protocol is implemented
"Position: #{position}"
# The output is always canonicalEncodes 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//..."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 pawnsEncodes native styles and active player:
<ACTIVE-STYLE>/<INACTIVE-STYLE>
- Each style is a valid SIN token (single ASCII letter)
- Uppercase = Side
first, lowercase = Sidesecond - 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# 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"# 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"# Starting position
"rheag^aehr/9/1c5c1/p1p1p1p1p/9/9/P1P1P1P1P/1C5C1/9/RHEAG^AEHR / X/x"# Starting position
"rnsmk^snr/8/pppppppp/8/8/PPPPPPPP/8/RNSK^MSNR / M/m"# 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"# 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()%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
}
}FEEN output is always canonical:
- Empty-count tokens use minimal base-10 integers (no leading zeros)
- Hand items are aggregated and sorted deterministically:
- By multiplicity (descending)
- By EPIN base letter (case-insensitive alphabetical)
- By EPIN letter case (uppercase before lowercase)
- By EPIN state modifier (
-before+before none) - By EPIN terminal marker (absent before present)
- 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"FEEN builds on these Sashité specifications:
- EPIN — Extended Piece Identifier Notation for piece tokens
- SIN — Style Identifier Notation for style-turn encoding
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
- FEEN Specification v1.0.0 — Technical specification
- FEEN Examples — Comprehensive examples
- Game Protocol — Conceptual foundation
- EPIN Specification — Piece encoding
- SIN Specification — Style encoding
Available as open source under the MIT License.
Maintained by Sashité — promoting chess variants and sharing the beauty of board game cultures.