///|
/// Everything that can be reported while reading a FEN document.
pub(all) suberror FenError {
Empty
FieldCount(Int32)
BoardRankCount(Int32)
BoardRankWidth(Int32, Int32)
InvalidPiece(Char)
InvalidSideToMove(String)
InvalidCastling(String)
InvalidEnPassant(String)
InvalidHalfmoveClock(String)
InvalidFullmoveNumber(String)
MissingKing(Color)
TooManyKings(Color)
} derive(@debug.Debug)
///|
pub extend FenError with @debug.Debug::{to_repr}
///|
/// A human readable explanation of the error.
pub fn FenError::message(self : FenError) -> String {
match self {
Empty => "the FEN document is empty"
FieldCount(count) =>
"expected between 4 and 6 FEN fields, found " + count.to_string()
BoardRankCount(count) =>
"expected 8 board ranks, found " + count.to_string()
BoardRankWidth(rank, width) =>
"board rank " +
(rank + 1).to_string() +
" describes " +
width.to_string() +
" files instead of 8"
InvalidPiece(c) => "unknown piece letter '" + c.to_string() + "'"
InvalidSideToMove(text) => "invalid side to move '" + text + "'"
InvalidCastling(text) => "invalid castling rights '" + text + "'"
InvalidEnPassant(text) => "invalid en passant square '" + text + "'"
InvalidHalfmoveClock(text) => "invalid halfmove clock '" + text + "'"
InvalidFullmoveNumber(text) => "invalid move number '" + text + "'"
MissingKing(color) => "the " + color.name() + " king is missing"
TooManyKings(color) =>
"the position holds more than one " + color.name() + " king"
}
}
///|
/// Splits a FEN document on spaces, dropping empty fields.
fn fen_fields(fen : String) -> Array[String] {
let out : Array[String] = []
for part in fen.split(" ") {
if part.length() > 0 {
out.push(part.to_owned())
}
}
out
}
///|
fn split_on(text : String, separator : String) -> Array[String] {
let out : Array[String] = []
for part in text.split(separator) {
out.push(part.to_owned())
}
out
}
///|
fn parse_board(bits : FixedArray[UInt64], text : String) -> Unit raise FenError {
let ranks = split_on(text, "/")
if ranks.length() != 8 {
raise BoardRankCount(ranks.length())
}
for row = 0; row < 8; row = row + 1 {
let rank = 7 - row
let mut file = 0
for c in ranks[row].to_array() {
if c >= '1' && c <= '9' {
file = file + (c.to_int() - '1'.to_int() + 1)
} else {
match Piece::from_fen_char(c) {
Some(piece) => {
if file >= 8 {
raise BoardRankWidth(rank, file + 1)
}
let index = piece_index(piece.color(), piece.kind())
bits[index] = bits[index] | (1UL << (rank * 8 + file))
file = file + 1
}
None => raise InvalidPiece(c)
}
}
}
if file != 8 {
raise BoardRankWidth(rank, file)
}
}
}
///|
fn check_kings(bits : FixedArray[UInt64]) -> Unit raise FenError {
for color in [White, Black] {
let kings = bits[piece_index(color, King)].popcnt()
if kings == 0 {
raise MissingKing(color)
}
if kings > 1 {
raise TooManyKings(color)
}
}
}
///|
fn parse_turn(text : String) -> Color raise FenError {
match text.get_char(0) {
Some(c) if text.length() == 1 =>
match Color::from_fen_char(c) {
Some(color) => color
None => raise InvalidSideToMove(text)
}
_ => raise InvalidSideToMove(text)
}
}
///|
fn parse_castling(text : String) -> CastlingRights raise FenError {
if text == "-" {
return CastlingRights::none()
}
let mut rights = CastlingRights::none()
for c in text.to_array() {
match c {
'K' => rights = rights.with_kingside(White, true)
'Q' => rights = rights.with_queenside(White, true)
'k' => rights = rights.with_kingside(Black, true)
'q' => rights = rights.with_queenside(Black, true)
_ => raise InvalidCastling(text)
}
}
rights
}
///|
fn parse_en_passant(text : String) -> Square? raise FenError {
if text == "-" {
return None
}
match Square::parse(text) {
Some(square) =>
if square.rank() == 2 || square.rank() == 5 {
Some(square)
} else {
raise InvalidEnPassant(text)
}
None => raise InvalidEnPassant(text)
}
}
///|
fn decimal(text : String) -> Int32? {
if text.length() == 0 {
return None
}
let mut value = 0
for c in text.to_array() {
if c >= '0' && c <= '9' {
value = value * 10 + (c.to_int() - '0'.to_int())
} else {
return None
}
}
Some(value)
}
///|
fn parse_halfmove(text : String) -> Int32 raise FenError {
match decimal(text) {
Some(value) => value
None => raise InvalidHalfmoveClock(text)
}
}
///|
fn parse_fullmove(text : String) -> Int32 raise FenError {
match decimal(text) {
Some(value) if value >= 1 => value
_ => raise InvalidFullmoveNumber(text)
}
}
///|
/// Parses a FEN document.
///
/// Four to six fields are accepted. When the clock fields are omitted the
/// halfmove clock defaults to `0` and the move number to `1`. The board must
/// hold exactly one king per colour.
pub fn Position::from_fen(fen : String) -> Position raise FenError {
let fields = fen_fields(fen)
if fields.length() == 0 {
raise Empty
}
if fields.length() < 4 || fields.length() > 6 {
raise FieldCount(fields.length())
}
let bits = FixedArray::make(12, 0UL)
parse_board(bits, fields[0])
check_kings(bits)
let turn = parse_turn(fields[1])
let castling = parse_castling(fields[2])
let ep_square = parse_en_passant(fields[3])
let halfmove_clock = if fields.length() >= 5 {
parse_halfmove(fields[4])
} else {
0
}
let fullmove_number = if fields.length() >= 6 {
parse_fullmove(fields[5])
} else {
1
}
Position::new(
bits, turn, castling, ep_square, halfmove_clock, fullmove_number,
)
}
///|
/// Parses a FEN document and returns the error instead of raising it.
pub fn Position::try_from_fen(fen : String) -> Result[Position, FenError] {
Ok(Position::from_fen(fen)) catch {
error => Err(error)
}
}
///|
/// Renders the position as a FEN document with all six fields.
pub fn Position::to_fen(self : Position) -> String {
let buf = StringBuilder()
for row = 0; row < 8; row = row + 1 {
let rank = 7 - row
let mut empty = 0
for file = 0; file < 8; file = file + 1 {
match self.piece_at(Square::make(file, rank)) {
Some(piece) => {
if empty > 0 {
buf.write_char(digit_char(empty))
empty = 0
}
buf.write_char(piece.fen_char())
}
None => empty = empty + 1
}
}
if empty > 0 {
buf.write_char(digit_char(empty))
}
if row < 7 {
buf.write_char('/')
}
}
buf.write_char(' ')
buf.write_char(self.turn.fen_char())
buf.write_char(' ')
buf.write_string(self.castling.to_fen())
buf.write_char(' ')
match self.ep_square {
Some(square) => buf.write_string(square.to_string())
None => buf.write_char('-')
}
buf.write_char(' ')
buf.write_string(self.halfmove_clock.to_string())
buf.write_char(' ')
buf.write_string(self.fullmove_number.to_string())
buf.to_string()
}
///|
/// The `0`..`8` digit for a small count.
fn digit_char(value : Int32) -> Char {
('0'.to_int() + value).to_char().unwrap()
}