pounce 2.0.2

A mediocre (but trying its best) uci chess engine
Documentation
use std::fmt::{self, Display, Formatter};
use std::num::NonZeroU16;
use std::str::FromStr;

use thiserror::Error;

use crate::chess::board::ParseSquareError;
use crate::chess::piece::ParsePieceError;
use crate::chess::position::CastleRights;
use crate::chess::{Color, File, Position, Rank, Square};

pub const STARTPOS: &str = "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1";

#[derive(Debug, Error)]
pub enum ParseFenError {
    #[error("found `{0}` parts in FEN string, expected 6")]
    InvalidPartCount(usize),
    #[error("too many slashes")]
    TooManySlashesInBoard,
    #[error("could not parse piece character")]
    CouldNotParsePiece(#[from] ParsePieceError),
    #[error("could not parse color: '{0}'")]
    CouldNotParseColor(String),
    #[error("could not parse castling rights: '{0}'")]
    CouldNotParseCastle(String),
    #[error("invalid en-passant square")]
    InvalidEpSquare(#[from] ParseSquareError),
    #[error("invalid halfmove clock")]
    InvalidHalfmoveClock(#[source] std::num::ParseIntError),
    #[error("invalid fullmove number")]
    InvalidFullmoveNumber(#[source] std::num::ParseIntError),
}

type Result<T, E = ParseFenError> = std::result::Result<T, E>;

pub struct Fen(pub Position);

impl Fen {
    pub fn parse(fen: &str) -> Result<Fen> {
        let parts: Vec<&str> = fen.split_whitespace().collect();
        if parts.len() != 6 {
            return Err(ParseFenError::InvalidPartCount(parts.len()));
        }
        let board_str = parts[0];
        let side_str = parts[1];
        let castling_str = parts[2];
        let ep_square_str = parts[3];
        let halfmove_clock_str = parts[4];
        let fullmove_number_str = parts[5];

        let mut position = parse_board_part(board_str)?;
        position.side = parse_side_part(side_str)?;
        position.castling = parse_castle_part(castling_str)?;
        position.ep_square = parse_ep_part(ep_square_str)?;
        position.halfmove_clock = parse_halfmove_clock_part(halfmove_clock_str)?;
        position.fullmove_number = parse_fullmove_number_part(fullmove_number_str)?;

        position.refresh_checks_and_pins();
        position.key = position.zobrist_hash();

        Ok(Fen(position))
    }
}

impl FromStr for Fen {
    type Err = ParseFenError;
    fn from_str(fen: &str) -> Result<Self, Self::Err> {
        Fen::parse(fen)
    }
}

impl Display for Fen {
    fn fmt(&self, f: &mut Formatter) -> fmt::Result {
        let Fen(position) = self;
        write!(f, "{}", position.to_fen())
    }
}

fn parse_board_part(board_str: &str) -> Result<Position> {
    let iter = board_str.chars();
    let mut file = File::A;
    let mut rank = Rank::R8;

    let mut position = Position::new();

    for c in iter {
        match c {
            '/' => {
                rank = rank.down().ok_or(ParseFenError::TooManySlashesInBoard)?;
            }
            '1'..='8' => {
                let n = c.to_digit(10).unwrap() as u8;
                for _ in 0..n {
                    file = file.east_wrapped()
                }
            }
            _ => {
                let piece = c.to_string().parse()?;
                position.set(Square::make(file, rank), piece);
                file = file.east_wrapped();
            }
        }
    }

    Ok(position)
}

fn parse_side_part(side_str: &str) -> Result<Color> {
    match side_str {
        "w" => Ok(Color::White),
        "b" => Ok(Color::Black),
        _ => Err(ParseFenError::CouldNotParseColor(side_str.to_string())),
    }
}

fn parse_castle_part(castle_str: &str) -> Result<CastleRights> {
    let mut castling = CastleRights::empty();
    for c in castle_str.chars() {
        match c {
            'K' => castling.insert(CastleRights::WHITE_KING_SIDE),
            'Q' => castling.insert(CastleRights::WHITE_QUEEN_SIDE),
            'k' => castling.insert(CastleRights::BLACK_KING_SIDE),
            'q' => castling.insert(CastleRights::BLACK_QUEEN_SIDE),
            '-' => castling = CastleRights::empty(),
            _ => return Err(ParseFenError::CouldNotParseCastle(castle_str.to_string())),
        }
    }
    Ok(castling)
}

fn parse_ep_part(ep_str: &str) -> Result<Option<Square>> {
    if ep_str == "-" {
        Ok(None)
    } else {
        let ep_square = ep_str.parse()?;
        Ok(Some(ep_square))
    }
}

fn parse_halfmove_clock_part(halfmove_clock_str: &str) -> Result<u8> {
    halfmove_clock_str
        .parse()
        .map_err(ParseFenError::InvalidHalfmoveClock)
}

fn parse_fullmove_number_part(fullmove_number_str: &str) -> Result<NonZeroU16> {
    fullmove_number_str
        .parse()
        .map_err(ParseFenError::InvalidFullmoveNumber)
}

impl Position {
    pub fn to_fen(&self) -> String {
        let mut fen = String::new();
        for rank in Rank::ALL.iter().rev() {
            let mut empty = 0;
            for file in File::ALL.iter() {
                let square = Square::make(*file, *rank);
                match self.piece_at(square) {
                    Some(piece) => {
                        if empty > 0 {
                            fen.push_str(&empty.to_string());
                            empty = 0;
                        }
                        fen.push_str(&piece.to_string())
                    }
                    None => {
                        empty += 1;
                    }
                }
            }
            if empty > 0 {
                fen.push_str(&empty.to_string());
            }
            if *rank != Rank::R1 {
                fen.push('/');
            }
        }
        format!(
            "{} {} {} {} {} {}",
            fen,
            self.side.to_fen(),
            self.castling.to_fen(),
            self.ep_square
                .map_or_else(|| "-".to_string(), |s| s.to_string()),
            self.halfmove_clock,
            self.fullmove_number.get(),
        )
    }
}

impl Color {
    fn to_fen(self) -> &'static str {
        match self {
            Color::White => "w",
            Color::Black => "b",
        }
    }
}

impl CastleRights {
    fn to_fen(self) -> String {
        if self.is_empty() {
            "-".to_string()
        } else {
            let mut s = String::new();
            if self.contains(CastleRights::WHITE_KING_SIDE) {
                s.push('K');
            }
            if self.contains(CastleRights::WHITE_QUEEN_SIDE) {
                s.push('Q');
            }
            if self.contains(CastleRights::BLACK_KING_SIDE) {
                s.push('k');
            }
            if self.contains(CastleRights::BLACK_QUEEN_SIDE) {
                s.push('q');
            }
            s
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_fen_parse() {
        let fen = "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1";
        let Fen(position) = Fen::parse(fen).unwrap();
        println!("{position:?}");
        assert_eq!(position.side, Color::White);
        assert_eq!(position.castling, CastleRights::all());
        assert_eq!(position.ep_square, None);
        assert_eq!(position.halfmove_clock, 0);
        assert_eq!(position.fullmove_number, NonZeroU16::new(1).unwrap());
    }
}