schachmatt 0.3.0

A chess library
Documentation
use crate::{
    CLASSIC_RULESET, Columns, Field,
    Fields::{FIELD_A1, FIELD_E1},
    Piece, PieceType, PlayerColor, Position, Rows, Turn,
};

use pest::{Parser, iterators::Pair};

use super::{San, SanParserError};

#[derive(Parser)]
#[grammar = "parser/standard_algebraic_notation/standard_algebraic_notation.pest"]
struct SanStruct;

impl San {
    /// Converts a string in SAN representation to a `Turn` object.
    /// - `raw` - The raw san string
    /// - `current_position` - The position the turn was played in
    /// - `returns` - The `Turn` as an object
    pub fn import(raw: &str, current_position: &Position) -> Result<Turn, SanParserError> {
        // Cut potential "+" from raw string data as it doesnt convey any needed information

        let mut san_data = raw;
        if let Some(index) = san_data.find('+') {
            san_data = &san_data[0..index];
        }

        // Parse SAN data
        let Ok(mut parsed_data) = SanStruct::parse(Rule::turn, san_data) else {
            return Err(SanParserError::InvalidData(san_data.to_string()));
        };

        if let Some(turn_type) = parsed_data.next().unwrap().into_inner().next() {
            return match turn_type.as_rule() {
                Rule::pawn_move => Self::import_pawn_movement(turn_type, current_position),
                Rule::castling => Ok(Self::import_handle_castling(&turn_type, current_position)),
                Rule::piece_move_full => Self::import_piece_move_full(turn_type, current_position),
                _ => unreachable!(),
            };
        }
        unreachable!()
    }

    /// Converts the full piece move into a turn
    /// - `san_data` - The pest parsed san data
    /// - `position` - The current game position
    /// - `returns` - The resulting turn
    fn import_piece_move_full(
        san_data: Pair<Rule>,
        position: &Position,
    ) -> Result<Turn, SanParserError> {
        let possible_moves = CLASSIC_RULESET.get_possible_turns(position);
        let raw_turn = san_data.as_str().to_string();

        let mut piece_type: Option<Piece> = None;
        let mut target_field: Option<Field> = None;
        let mut from_column: Option<u8> = None;
        let mut from_row: Option<u8> = None;

        for parts in san_data.into_inner() {
            match parts.as_rule() {
                Rule::piece_symbol => {
                    let letter = parts.as_str().as_bytes()[0] as char;
                    let piece = PieceType::import_piecetype(letter.to_ascii_lowercase());

                    if let Some(piece) = piece {
                        piece_type = Some(Piece::new(piece, position.get_active_color()));
                    } else {
                        piece_type = None;
                    }
                }
                Rule::piece_move => {
                    let (target, column, row) = Self::import_piece_move(parts);
                    target_field = Some(target);
                    from_column = column;
                    from_row = row;
                }
                _ => return Err(SanParserError::InvalidData(raw_turn)),
            }
        }

        for turn in possible_moves {
            if target_field.unwrap() == turn.target
                && position.get_field_occupation(&turn.current) == piece_type
            {
                let mut ok: bool = true;
                if let Some(column_value) = from_column {
                    if turn.current.get_column() != column_value {
                        ok = false;
                    }
                }
                if let Some(row_value) = from_row {
                    if turn.current.get_row() != row_value {
                        ok = false;
                    }
                }
                if ok {
                    return Ok(turn);
                }
            }
        }
        Err(SanParserError::InvalidMove(raw_turn))
    }

    /// Converts a simple piece move
    /// - `san_data` - The pest parsed san data
    /// - `returns` - The target field
    fn import_piece_move(san_data: Pair<Rule>) -> (Field, Option<u8>, Option<u8>) {
        let mut from_column: Option<u8> = None;
        let mut from_row: Option<u8> = None;

        for part in san_data.into_inner() {
            match part.as_rule() {
                Rule::to_field => {
                    return (
                        Field::new_from_string(part.as_str()).unwrap(),
                        from_column,
                        from_row,
                    );
                }
                Rule::from_field => {
                    let data = part.as_str().as_bytes();
                    if data[0] >= b'a' && data[0] <= b'h' {
                        from_column = Some(data[0] - b'a');
                        if data.len() > 1 {
                            from_row = Some(data[1] - b'1');
                        }
                    } else {
                        from_row = Some(data[0] - b'1');
                    }
                }
                _ => unreachable!(),
            }
        }
        unreachable!();
    }

    /// Converts the san castling moves into turns
    /// - `san_data` - The pest parsed turn data
    /// - `position` - The position in which the turn was played
    /// - `returns` - The resulting `Turn`
    fn import_handle_castling(san_data: &Pair<Rule>, position: &Position) -> Turn {
        let possible_moves = CLASSIC_RULESET.get_possible_turns(position);
        let player_color = position.get_active_color();

        // Initiate with row for white
        let mut target_field = FIELD_A1;
        let mut starting_field = FIELD_E1;

        // Change row if color is black
        if player_color == PlayerColor::Black {
            starting_field.set_row(Rows::ROW_8);
            target_field.set_row(Rows::ROW_8);
        }

        // Check if castle is king or queenside
        match san_data.as_str() {
            "O-O" | "0-0" => {
                target_field.set_column(Columns::COLUMN_G);
            }
            "O-O-O" | "0-0-0" => {
                target_field.set_column(Columns::COLUMN_C);
            }
            _ => unreachable!(),
        };

        possible_moves
            .into_iter()
            .find(|&turn| turn.target == target_field && turn.current == starting_field)
            .unwrap()
    }

    /// Convert the san pawn moves into turns
    fn import_pawn_movement(
        san_data: Pair<Rule>,
        position: &Position,
    ) -> Result<Turn, SanParserError> {
        let possible_moves = CLASSIC_RULESET.get_possible_turns(position);
        let raw_turn = san_data.as_str().to_string();

        let mut target_field: Option<Field> = None;
        let mut promotion_piece: Option<PieceType> = None;
        let mut from_column: Option<u8> = None;
        let mut from_row: Option<u8> = None;

        // Create target field and promotion target
        for pawn_push in san_data.into_inner() {
            match pawn_push.as_rule() {
                Rule::to_field => target_field = Field::new_from_string(pawn_push.as_str()),
                Rule::promotion_piece => {
                    let letter = (pawn_push.as_str().as_bytes()[0] as char).to_ascii_lowercase();
                    let piece_type = PieceType::import_piecetype(letter).unwrap();
                    promotion_piece = Some(piece_type);
                }
                Rule::from_field => {
                    let data = pawn_push.as_str().as_bytes();
                    from_column = Some(data[0] - b'a');
                    if data.len() > 1 {
                        from_row = Some(data[1] - 1);
                    }
                }
                _ => unreachable!(),
            }
        }

        for turn in possible_moves {
            let from_occupation = position.get_field_occupation(&turn.current);
            let Some(moving_piece) = from_occupation else {
                return Err(SanParserError::InvalidMove(raw_turn));
            };
            if target_field.unwrap() == turn.target
                && promotion_piece == turn.promotion
                && moving_piece.get_type() == PieceType::Pawn
            {
                match from_column {
                    Some(column) => {
                        // Is a capture move
                        match from_row {
                            Some(row) => {
                                if column == turn.current.get_column()
                                    && row == turn.current.get_row()
                                {
                                    return Ok(turn);
                                }
                            }
                            None => {
                                if column == turn.current.get_column() {
                                    return Ok(turn);
                                }
                            }
                        }
                    }
                    None => return Ok(turn),
                }
            }
        }
        Err(SanParserError::InvalidMove(raw_turn))
    }
}