check_buddy/
board.rs

1use crate::errors::*;
2use crate::moves::position_move::{
3    Direction, Position, PositionMove, DIRECTION_OFFSETS, KNIGHT_DIRECTION_OFFSETS,
4};
5use crate::piece::{piece_type::*, Piece};
6use crate::piece_color::PieceColor;
7use crate::uci_move::{UciMove, UciMoveType, NON_PAWN_SYMBOLS};
8use anyhow::{anyhow, Result};
9use arrayvec::ArrayVec;
10use std::borrow::BorrowMut;
11use std::cmp::min;
12use std::fmt::{Debug, Formatter};
13use std::ops::{Deref, DerefMut, Sub};
14
15const RANKS: [char; 8] = ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h'];
16const FILES: [char; 8] = ['1', '2', '3', '4', '5', '6', '7', '8'];
17
18#[derive(Clone, Copy)]
19pub struct BoardMap {
20    squares: [[Piece; 8]; 8],
21    active_color: PieceColor,
22    black_king_moved: bool,
23    white_king_moved: bool,
24    black_queenside_rook_moved: bool,
25    black_kingside_rook_moved: bool,
26    white_queenside_rook_moved: bool,
27    white_kingside_rook_moved: bool,
28    white_king_pos: Position,
29    black_king_pos: Position,
30}
31
32impl Default for BoardMap {
33    fn default() -> Self {
34        let squares = [[Piece(0); 8]; 8];
35
36        Self {
37            squares,
38            active_color: PieceColor::White,
39            black_king_moved: false,
40            white_king_moved: false,
41            black_queenside_rook_moved: false,
42            black_kingside_rook_moved: false,
43            white_queenside_rook_moved: false,
44            white_kingside_rook_moved: false,
45            white_king_pos: [0, 0],
46            black_king_pos: [0, 0],
47        }
48    }
49}
50
51impl BoardMap {
52    pub fn empty() -> Self {
53        BoardMap::default()
54    }
55    /// starting position: rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1
56    pub fn starting() -> Self {
57        BoardMap::from_fen("rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1")
58    }
59    pub fn from_fen(fen: impl Into<String>) -> Self {
60        let fen = fen.into();
61        let mut board = Self::default();
62        let sections = fen.split_whitespace().collect::<Vec<_>>();
63        let placement = sections[0].split('/').collect::<Vec<_>>();
64
65        let mut index = 0;
66        placement.iter().for_each(|x| {
67            for mut x in x.chars() {
68                let color = if x.is_uppercase() { WHITE } else { BLACK };
69
70                if !x.is_numeric() {
71                    x.make_ascii_lowercase();
72                    let rank = match x {
73                        'p' => PAWN,
74                        'r' => ROOK,
75                        'b' => BISHOP,
76                        'q' => QUEEN,
77                        'k' => KING,
78                        'n' => KNIGHT,
79                        _ => 0,
80                    };
81                    let pos = [index / 8, index % 8];
82                    board.squares[pos[0]][pos[1]] = Piece(color | rank);
83
84                    // track king position
85                    if rank == KING {
86                        if color == WHITE {
87                            board.white_king_pos = pos;
88                        } else {
89                            board.black_king_pos = pos;
90                        }
91                    }
92
93                    index += 1;
94                } else {
95                    index += x.to_digit(10).unwrap() as usize;
96                }
97            }
98        });
99        board.active_color = if let Some(string) = sections.get(1) {
100            if let Some(c) = string.chars().next() {
101                match c {
102                    'w' => PieceColor::White,
103                    'b' => PieceColor::Black,
104                    _ => unreachable!("FEN incorrect"),
105                }
106            } else {
107                PieceColor::White
108            }
109        } else {
110            PieceColor::White
111        };
112
113        board
114    }
115    pub fn get_fen(&self) -> String {
116        let mut fen = String::new();
117        let squares = self.squares;
118        for row in squares {
119            let mut space = 0;
120            for col in row {
121                if let Some(piece_type) = col.get_type() {
122                    if space != 0 {
123                        fen.push_str(space.to_string().as_str());
124                        space = 0;
125                    }
126
127                    let mut piece_character = match piece_type {
128                        PieceType::Rook => 'r',
129                        PieceType::Pawn(_) => 'p',
130                        PieceType::King => 'k',
131                        PieceType::Queen => 'q',
132                        PieceType::Bishop => 'b',
133                        PieceType::Knight => 'n',
134                    };
135
136                    if col.get_color() == PieceColor::White {
137                        piece_character = piece_character.to_ascii_uppercase();
138                    }
139
140                    fen.push(piece_character);
141                } else {
142                    space += 1;
143                }
144            }
145            if space != 0 {
146                fen.push_str(space.to_string().as_str());
147            }
148            fen.push('/');
149        }
150
151        fen.pop();
152        fen
153    }
154    pub fn parse_uci_to_move(&mut self, mut uci: &str) -> Result<UciMove> {
155        let mate = uci.ends_with('#');
156        if mate {
157            let mut chars = uci.chars();
158            chars.next_back();
159            uci = chars.as_str();
160        }
161
162        let check = uci.ends_with('+');
163        if check {
164            let mut chars = uci.chars();
165            chars.next_back();
166            uci = chars.as_str();
167        }
168
169        let uci_move_type = if uci == "O-O" {
170            UciMoveType::CastleShort {
171                piece_color: self.active_color,
172                take: false,
173                check,
174            }
175        } else if uci == "O-O-O" {
176            UciMoveType::CastleLong {
177                piece_color: self.active_color,
178                take: false,
179                check,
180            }
181        } else if uci.len() == 2 {
182            UciMoveType::Pawn {
183                take: false,
184                check,
185                promotion: None, //TODO PROMOTION
186            }
187        } else if RANKS.contains(&uci.chars().next().expect("Couldnt get next char of uci")) {
188            let specified_file =
189                FILES.contains(&uci.chars().nth(1).expect("Couldnt get second char of uci"));
190            let take = (!specified_file && uci.chars().nth(1) == Some('x'))
191                || (specified_file && uci.chars().nth(2) == Some('x'));
192
193            let promotion_position = uci.len() - 2;
194            let promotion = if uci.chars().nth(promotion_position) == Some('=') {
195                let piece_type = match uci
196                    .chars()
197                    .next_back()
198                    .expect("Couldnt get last char of uci")
199                {
200                    'K' => PieceType::King,
201                    'N' => PieceType::Knight,
202                    'Q' => PieceType::Queen,
203                    'R' => PieceType::Rook,
204                    'B' => PieceType::Bishop,
205                    _ => return Err(PieceError::SymbolNotFound.into()),
206                };
207
208                Some(piece_type)
209            } else {
210                None
211            };
212
213            UciMoveType::Pawn {
214                take,
215                check,
216                promotion,
217            }
218        } else if uci.len() >= 3
219            && NON_PAWN_SYMBOLS.contains(&uci.chars().next().expect("Couldnt get next char of uci"))
220        {
221            // specified rank is always position 1
222            // take can be 1 or 2
223            // to position differs
224            //
225            //         f | r | t | p
226            // Re8     x | x | x | 1
227            // Rxe8    x | x | 1 | 2
228            // R1e8    1 | x | x | 2
229            // Rbe8    x | 1 | x | 2
230            // Rbxe8   x | 1 | 2 | 3
231            // R1xe8   1 | x | 2 | 3
232            let specified_rank = uci.len() > 3
233                && RANKS.contains(&uci.chars().nth(1).expect("couldnt get first char of uci"))
234                && (RANKS.contains(&uci.chars().nth(2).expect("couldnt get second char of uci"))
235                    || RANKS.contains(&uci.chars().nth(3).expect("couldnt get third char of uci"))
236                    || (uci.len() > 4
237                        && RANKS.contains(
238                            &uci.chars().nth(4).expect("couldnt get third char of uci"),
239                        )));
240            let specified_file = uci.len() > 3
241                && (FILES.contains(&uci.chars().nth(1).expect("couldnt get first char of uci"))
242                    || (RANKS
243                        .contains(&uci.chars().nth(1).expect("couldnt get second char of uci"))
244                        && FILES.contains(
245                            &uci.chars().nth(2).expect("couldnt get first char of uci"),
246                        )));
247            let take = uci.chars().nth(1) == Some('x')
248                || uci.chars().nth(2) == Some('x')
249                || uci.chars().nth(3) == Some('x');
250            let piece_type = match uci.chars().next().expect("Couldnt get next char of uci") {
251                'K' => PieceType::King,
252                'N' => PieceType::Knight,
253                'Q' => PieceType::Queen,
254                'R' => PieceType::Rook,
255                'B' => PieceType::Bishop,
256                _ => return Err(PieceError::SymbolNotFound.into()),
257            };
258
259            UciMoveType::Default {
260                specified_rank,
261                specified_file,
262                piece_type,
263                take,
264                check,
265            }
266        } else {
267            return Err(UciMoveError::InvalidUciMoveType.into());
268        };
269
270        let to: Position = match uci_move_type {
271            UciMoveType::Pawn { take, .. } => {
272                let to = if take {
273                    uci.chars().skip(2).take(2).collect::<String>()
274                } else {
275                    uci.chars().take(2).collect::<String>()
276                };
277                self.parse_uci_position_to_file_rank(to)?
278            }
279            UciMoveType::Default {
280                specified_rank,
281                specified_file,
282                take,
283                ..
284            } => {
285                let offset = specified_rank as usize + specified_file as usize + take as usize;
286                let to = uci.chars().skip(offset + 1).take(2).collect::<String>();
287                self.parse_uci_position_to_file_rank(to)?
288            }
289            UciMoveType::CastleLong { .. } => {
290                if self.get_active_color() == &PieceColor::White {
291                    [7, 2]
292                } else {
293                    [0, 2]
294                }
295            }
296            UciMoveType::CastleShort { .. } => {
297                if self.get_active_color() == &PieceColor::White {
298                    [7, 6]
299                } else {
300                    [0, 6]
301                }
302            }
303        };
304
305        let from: Position = match uci_move_type {
306            UciMoveType::Pawn { take, .. } => {
307                if take {
308                    let rank = (uci.chars().next().ok_or(anyhow!("can't parse"))? as usize).sub(97);
309
310                    let shift = match self.get_active_color() {
311                        PieceColor::Black => 1,
312                        PieceColor::White => -1,
313                    };
314                    // t . .
315                    // o f .
316                    let pos1 = [
317                        ((to[0] as i32) - shift) as usize,
318                        ((to[1] as i32) + 1) as usize,
319                    ];
320                    // . . t
321                    // . f o
322                    let pos2 = [
323                        ((to[0] as i32) - shift) as usize,
324                        ((to[1] as i32) - 1) as usize,
325                    ];
326                    // t . .
327                    // . f .
328                    let pos3 = [
329                        ((to[0] as i32) - shift) as usize,
330                        ((to[1] as i32) + 1) as usize,
331                    ];
332                    // . . t
333                    // . f .
334                    let pos4 = [
335                        ((to[0] as i32) - shift) as usize,
336                        ((to[1] as i32) - 1) as usize,
337                    ];
338
339                    self.verify_any_own_position(vec![pos1, pos2, pos3, pos4], Some(rank))?
340                } else {
341                    let shift = match self.get_active_color() {
342                        PieceColor::Black => 1,
343                        PieceColor::White => -1,
344                    };
345                    let pos1 = [((to[0] as i32) - shift) as usize, to[1]];
346                    let pos2 = [((to[0] as i32) - shift * 2) as usize, to[1]];
347                    self.verify_any_own_position(vec![pos1, pos2], None)?
348                }
349            }
350            UciMoveType::Default {
351                piece_type,
352                specified_rank,
353                specified_file,
354                ..
355            } => {
356                let mut possible_positions = self.get_positions_from_type(&piece_type);
357
358                if specified_rank {
359                    let specified_rank =
360                        (uci.chars().nth(1).ok_or(anyhow!("Can't parse rank"))? as usize).sub(97);
361                    possible_positions = possible_positions
362                        .iter()
363                        .filter_map(|&x| {
364                            if x[1] == specified_rank {
365                                return Some(x);
366                            }
367                            None
368                        })
369                        .collect::<Vec<_>>();
370                }
371
372                if specified_file {
373                    let shift = if specified_rank { 2 } else { 1 };
374
375                    let specified_file = 7 - uci
376                        .chars()
377                        .nth(shift)
378                        .ok_or(anyhow!("can't pop last char of uci"))?
379                        .to_string()
380                        .parse::<usize>()?
381                        .sub(1);
382                    possible_positions = possible_positions
383                        .iter()
384                        .filter_map(|&x| {
385                            if x[0] == specified_file {
386                                return Some(x);
387                            }
388                            None
389                        })
390                        .collect::<Vec<_>>();
391                }
392
393                let mut found_position = None;
394                for position in possible_positions.iter() {
395                    let moves = self.gen_legal_positions(*position);
396                    if moves.contains(&to) {
397                        found_position = Some(*position);
398                        break;
399                    }
400                }
401                if let Some(position) = found_position {
402                    position
403                } else {
404                    return Err(anyhow!(
405                        "Couldn't find [from] position for {:?} with [to] {:?}",
406                        uci,
407                        to
408                    ));
409                }
410            }
411            UciMoveType::CastleShort { .. } | UciMoveType::CastleLong { .. } => {
412                if self.get_active_color() == &PieceColor::White {
413                    [7, 4]
414                } else {
415                    [0, 4]
416                }
417            }
418        };
419
420        let en_passant = self.is_en_passant(from, to);
421
422        Ok((
423            uci_move_type,
424            PositionMove {
425                from,
426                to,
427                en_passant,
428                promotion: false,
429                ..Default::default()
430            },
431        ))
432    }
433    pub fn get_piece(&self, pos: Position) -> Piece {
434        self.squares[pos[0]][pos[1]]
435    }
436    pub fn find_piece(&self, piece_color: PieceColor, piece_type: PieceType) -> Vec<Position> {
437        let mut vec = vec![];
438        for (y, row) in self.squares.iter().enumerate() {
439            for (x, p) in row.iter().enumerate() {
440                if let Some(t) = p.get_type() {
441                    if t == piece_type && p.get_color() == piece_color {
442                        vec.push([y, x]);
443                    }
444                }
445            }
446        }
447        vec
448    }
449    /// Find the king of the given color
450    pub fn find_king(&self, piece_color: PieceColor) -> Option<Position> {
451        let pos = match piece_color {
452            PieceColor::White => self.white_king_pos,
453            PieceColor::Black => self.black_king_pos,
454        };
455        Some(pos)
456    }
457    pub fn get_piece_mut(&mut self, pos: Position) -> &mut Piece {
458        self.squares[pos[0]][pos[1]].borrow_mut()
459    }
460    pub fn get_active_color(&self) -> &PieceColor {
461        &self.active_color
462    }
463    pub fn get_active_pieces(&self) -> Vec<Position> {
464        let mut pieces = vec![];
465        for (i, row) in self.squares.iter().enumerate() {
466            for (j, piece) in row.iter().enumerate() {
467                if piece.get_color() == *self.get_active_color() {
468                    pieces.push([i, j]);
469                }
470            }
471        }
472        pieces
473    }
474    pub fn set_piece(&mut self, on: Position, value: u32) {
475        self.squares[on[0]][on[1]] = Piece(value);
476    }
477    /// makes a move with uci info
478    ///
479    /// returns true if move was successful
480    pub fn uci_move_turn(&mut self, uci_move: UciMove) -> Result<()> {
481        if let UciMoveType::CastleShort { .. } = uci_move.0 {
482            // make_move handles rook movement internally when king moves 2 squares
483            self.make_move(uci_move.1);
484        } else if let UciMoveType::CastleLong { .. } = uci_move.0 {
485            // make_move handles rook movement internally when king moves 2 squares
486            self.make_move(uci_move.1);
487        } else {
488            let position_move = uci_move.1;
489
490            let PositionMove { .. } = position_move;
491            self.is_valid_move(position_move)?;
492            self.make_move(position_move);
493
494            match uci_move.0 {
495                UciMoveType::Pawn { promotion, .. } => {
496                    self.handle_convert_to_en_passantable(position_move);
497
498                    if let Some(piece_type) = promotion {
499                        let value = piece_type.to_value() | self.get_active_color().to_value();
500                        self.set_piece(position_move.to, value);
501                    }
502                }
503                UciMoveType::Default { piece_type, .. } => {
504                    if piece_type == PieceType::King {
505                        if self.get_active_color() == &PieceColor::White {
506                            self.white_king_moved = false;
507                        } else {
508                            self.black_king_moved = false;
509                        }
510                    }
511                }
512                _ => {}
513            }
514        }
515
516        self.switch_active_color();
517        Ok(())
518    }
519    /// makes a single move with check
520    ///
521    /// returns true if move was successful
522    pub fn single_move_turn(&mut self, position_move: PositionMove) -> Result<()> {
523        let PositionMove { to, .. } = position_move;
524
525        self.is_valid_move(position_move)?;
526
527        self.make_move(position_move);
528
529        let piece_to = &mut self.get_piece(to);
530
531        let castle = false; //TODO!
532        if castle {
533            if self.active_color == PieceColor::White {
534                self.make_move(PositionMove::new([7, 0], [7, 3]));
535            } else {
536                self.make_move(PositionMove::new([0, 0], [0, 3]));
537            }
538        }
539
540        if let Some(PieceType::Pawn(_)) = piece_to.get_type() {
541            self.handle_convert_to_en_passantable(position_move);
542        }
543
544        self.switch_active_color();
545
546        Ok(())
547    }
548    /// check if move is valid
549    pub fn is_valid_move(&self, piece_move: PositionMove) -> Result<()> {
550        let PositionMove { from, to, .. } = piece_move;
551        let piece_from = self.squares[from[0]][from[1]];
552        let piece_to = self.squares[to[0]][to[1]];
553
554        if !piece_from.is_piece() {
555            return Err(anyhow!(PieceMoveError::EmptySquare));
556        }
557
558        if piece_from.get_color() != self.active_color {
559            return Err(anyhow!(PieceMoveError::NotYourPiece(piece_from, from)));
560        }
561
562        if piece_to.is_piece() && piece_to.get_color() == self.active_color {
563            return Err(anyhow!(PieceMoveError::NotYourPiece(piece_to, to)));
564        }
565
566        let moves = self.gen_legal_positions(from);
567
568        if !moves.contains(&to) {
569            return Err(anyhow!(PieceMoveError::MoveNotFound));
570        }
571
572        Ok(())
573    }
574    pub fn is_hit(&self, pos: Position) -> bool {
575        let piece_on = self.get_piece(pos);
576        piece_on.is_piece() && piece_on.get_color() != self.active_color
577    }
578    /// generate only legal move positions for piece
579    pub fn gen_legal_positions(&self, from: Position) -> Vec<Position> {
580        let mut temp_board = *self;
581        let positions = temp_board.gen_to_positions(from);
582        let mut legal_positions = vec![];
583
584        for to in positions.into_iter() {
585            let en_passant = self.is_en_passant(from, to);
586            let promotion = self.is_promotion(from, to);
587            let last_piece = temp_board.squares[to[0]][to[1]].0;
588
589            let position_move = PositionMove {
590                from,
591                to,
592                en_passant,
593                promotion,
594                ..Default::default()
595            };
596            temp_board.make_move(position_move);
597            let next_moves = temp_board.gen_all_opponent_positions();
598            if !next_moves.iter().any(|m| {
599                let next_piece = temp_board.squares[m[0]][m[1]];
600                next_piece.is_piece()
601                    && next_piece.get_color() == temp_board.active_color
602                    && Some(PieceType::King) == next_piece.get_type()
603            }) {
604                legal_positions.push(to);
605            }
606
607            temp_board.undo_move(position_move, last_piece);
608        }
609        legal_positions
610    }
611    /// generate all possible move position for piece
612    pub fn gen_to_positions(&self, from: Position) -> ArrayVec<Position, 32> {
613        let piece_from = self.squares[from[0]][from[1]];
614        if let Some(piece_type) = piece_from.get_type() {
615            return match piece_type {
616                PieceType::Bishop | PieceType::Rook | PieceType::Queen => {
617                    self.gen_sliding(from, piece_type)
618                }
619                PieceType::Pawn(_) => self.gen_pawn(from),
620                PieceType::King => self.gen_king(from),
621                PieceType::Knight => self.gen_knight(from),
622            };
623        }
624
625        ArrayVec::new()
626    }
627    pub fn gen_sliding(&self, from: Position, piece_type: PieceType) -> ArrayVec<Position, 32> {
628        let piece_from = self.squares[from[0]][from[1]];
629        let piece_color = piece_from.get_color();
630        let mut positions = ArrayVec::new();
631        let start = if piece_type == PieceType::Bishop {
632            4
633        } else {
634            0
635        };
636        let end = if piece_type == PieceType::Rook { 4 } else { 8 };
637        for (direction, offset) in DIRECTION_OFFSETS.iter().enumerate().take(end).skip(start) {
638            for n in 0..self.len_to_edge(from, Direction::from(direction)) {
639                let index = from[0] * 8 + from[1];
640                let target_index = (index as i32 + offset * (n + 1) as i32).clamp(0, 63) as usize;
641                let target_position = [target_index / 8, target_index % 8];
642                let target_piece = self.squares[target_position[0]][target_position[1]];
643
644                if target_piece.is_piece() && target_piece.get_color() == piece_color {
645                    // your own color is in the way
646                    break;
647                }
648                positions.push(target_position);
649                // self.squares[target_move[0]][target_move[1]] = Piece(100);
650
651                if target_piece.is_piece() && target_piece.get_color() != piece_color {
652                    // Enemy piece and capturable
653                    break;
654                }
655            }
656        }
657        positions
658    }
659    pub fn gen_king(&self, from: Position) -> ArrayVec<Position, 32> {
660        let piece_from = self.squares[from[0]][from[1]];
661        let piece_color = piece_from.get_color();
662        let mut positions = ArrayVec::new();
663        for (direction, offset) in DIRECTION_OFFSETS.iter().enumerate() {
664            let index = from[0] * 8 + from[1];
665            let target_index = index as i32 + offset;
666            if !(0..=63).contains(&target_index)
667                || self.len_to_edge(from, Direction::from(direction)) == 0
668            {
669                continue;
670            }
671            let target_move = [target_index as usize / 8, target_index as usize % 8];
672            let target_piece = self.squares[target_move[0]][target_move[1]];
673
674            if target_piece.is_piece() && target_piece.get_color() == piece_color {
675                // your own color is in the way
676                continue;
677            }
678            positions.push(target_move);
679
680            if target_piece.is_piece() && target_piece.get_color() != piece_color {
681                // Enemy piece and capturable
682                continue;
683            }
684        }
685
686        // castling - use piece color, not active color
687        if piece_color == PieceColor::Black {
688            if self.black_can_short_castle() {
689                positions.push([0, 6]);
690            }
691            if self.black_can_long_castle() {
692                positions.push([0, 2]);
693            }
694        } else {
695            if self.white_can_short_castle() {
696                positions.push([7, 6]);
697            }
698            if self.white_can_long_castle() {
699                positions.push([7, 2]);
700            }
701        }
702
703        positions
704    }
705    pub fn gen_pawn(&self, from: Position) -> ArrayVec<Position, 32> {
706        let piece_from = self.squares[from[0]][from[1]];
707        let mut moves = ArrayVec::new();
708        let piece_color = piece_from.get_color();
709        let is_black = piece_color == PieceColor::Black;
710        let shift = if is_black { 1 } else { -1 };
711
712        // piece blocking
713        let vertical = (from[0] as i32 + shift) as usize;
714        if vertical < 8 {
715            let is_blocking = self.squares[vertical][from[1]].is_piece();
716            if !is_blocking {
717                moves.push([(from[0] as i32 + shift) as usize, from[1]]);
718            }
719
720            // hasn't moved yet
721            let vertical = (from[0] as i32 + shift * 2) as usize;
722            if vertical < 8 {
723                let is_blocking = is_blocking || self.squares[vertical][from[1]].is_piece();
724                if ((is_black && from[0] == 1) || (!is_black && from[0] == 6)) && !is_blocking {
725                    moves.push([vertical, from[1]]);
726                }
727            }
728        }
729
730        // takeable pieces on [+1,-1]
731        // x  .  .
732        // .  p  .
733        if from[1] > 0 && from[1] < 8 {
734            let to_top_left_pos = [(from[0] as i32 + shift) as usize, from[1] - 1];
735            if to_top_left_pos[0] < 8 {
736                let to_top_left = self.get_piece(to_top_left_pos);
737                if to_top_left.is_piece() && to_top_left.get_color() != piece_color {
738                    moves.push(to_top_left_pos);
739                }
740
741                // en passant
742                // x  .  .
743                // _  p  .
744                let to_left = self.squares[from[0]][from[1] - 1];
745                if let Some(PieceType::Pawn(en_passantable)) = to_left.get_type() {
746                    if en_passantable && to_left.get_color() != piece_color {
747                        let to_en_passant = [(from[0] as i32 + shift) as usize, from[1] - 1];
748                        moves.push(to_en_passant);
749                    }
750                }
751            }
752        }
753
754        // takeable pieces on [+1,+1]
755        // .  .  x
756        // .  p  .
757        if from[1] < 7 {
758            let to_top_right_pos = [(from[0] as i32 + shift) as usize, from[1] + 1];
759            if to_top_right_pos[0] < 8 {
760                let to_top_right = self.squares[to_top_right_pos[0]][to_top_right_pos[1]];
761                if to_top_right.is_piece() && to_top_right.get_color() != piece_color {
762                    moves.push(to_top_right_pos);
763                }
764
765                // en passant
766                // .  .  x
767                // .  p  _
768                let to_right = self.squares[from[0]][from[1] + 1];
769                if let Some(PieceType::Pawn(en_passantable)) = to_right.get_type() {
770                    if en_passantable && to_right.get_color() != piece_color {
771                        let to_en_passant = [(from[0] as i32 + shift) as usize, from[1] + 1];
772                        moves.push(to_en_passant);
773                    }
774                }
775            }
776        }
777        moves
778    }
779    pub fn gen_knight(&self, from: Position) -> ArrayVec<Position, 32> {
780        let piece_from = self.squares[from[0]][from[1]];
781        let piece_color = piece_from.get_color();
782        KNIGHT_DIRECTION_OFFSETS
783            .iter()
784            .filter_map(|direction| {
785                let new_pos = [
786                    (direction[0] + from[0] as i32) as usize,
787                    (direction[1] + from[1] as i32) as usize,
788                ];
789                if new_pos[0] < 8 && new_pos[1] < 8 {
790                    let target_piece = self.squares[new_pos[0]][new_pos[1]];
791                    if !(target_piece.is_piece() && target_piece.get_color() == piece_color) {
792                        return Some(new_pos);
793                    }
794                }
795                None
796            })
797            .collect()
798    }
799    fn len_to_edge(&self, pos: Position, direction: Direction) -> usize {
800        let (rank, file) = (pos[0], pos[1]);
801        let north = 7 - rank;
802        let south = rank;
803        let west = file;
804        let east = 7 - file;
805
806        match direction {
807            Direction::North => north,
808            Direction::NorthEast => min(north, east),
809            Direction::East => east,
810            Direction::SouthEast => min(south, east),
811            Direction::South => south,
812            Direction::SouthWest => min(south, west),
813            Direction::West => west,
814            Direction::NorthWest => min(north, west),
815        }
816    }
817    /// make a move (without check)
818    pub fn make_move(&mut self, position_move: PositionMove) {
819        let PositionMove {
820            from,
821            to,
822            en_passant,
823            promotion,
824            ..
825        } = position_move;
826
827        let piece = self.get_piece(from);
828        let is_white = piece.get_color() == PieceColor::White;
829        let target_piece = self.get_piece(to);
830
831        if en_passant {
832            let shift = if is_white { -1 } else { 1 };
833            let to_step = [(to[0] as isize - shift) as usize, to[1]];
834            self.set_piece(to_step, 0);
835        }
836
837        // track castling rights
838        // mark kings and rooks as moved
839        if let Some(piece_type) = piece.get_type() {
840            match piece_type {
841                PieceType::King => {
842                    if is_white {
843                        self.white_king_moved = true;
844                        self.white_king_pos = to;
845                    } else {
846                        self.black_king_moved = true;
847                        self.black_king_pos = to;
848                    }
849                }
850                PieceType::Rook => {
851                    // check if rook move
852                    if is_white {
853                        if from == [7, 0] {
854                            self.white_queenside_rook_moved = true;
855                        } else if from == [7, 7] {
856                            self.white_kingside_rook_moved = true;
857                        }
858                    } else if from == [0, 0] {
859                        self.black_queenside_rook_moved = true;
860                    } else if from == [0, 7] {
861                        self.black_kingside_rook_moved = true;
862                    }
863                }
864                _ => {}
865            }
866        }
867
868        // invalidate castling if a rook is captured on its starting square
869        if target_piece.is_piece() {
870            if let Some(PieceType::Rook) = target_piece.get_type() {
871                match to {
872                    [7, 0] => self.white_queenside_rook_moved = true,
873                    [7, 7] => self.white_kingside_rook_moved = true,
874                    [0, 0] => self.black_queenside_rook_moved = true,
875                    [0, 7] => self.black_kingside_rook_moved = true,
876                    _ => {}
877                }
878            }
879        }
880
881        // handle castling rook movement
882        if let Some(PieceType::King) = piece.get_type() {
883            // detect castling by king moving 2 squares horizontally
884            if from[1] == 4 && to[1] == 6 {
885                // kingside castle - move rook from h-file to f-file
886                let rook_from = [from[0], 7];
887                let rook_to = [from[0], 5];
888                let rook_piece = self.get_piece(rook_from).0;
889                self.set_piece(rook_to, rook_piece);
890                self.set_piece(rook_from, 0);
891            } else if from[1] == 4 && to[1] == 2 {
892                // queenside castle - move rook from a-file to d-file
893                let rook_from = [from[0], 0];
894                let rook_to = [from[0], 3];
895                let rook_piece = self.get_piece(rook_from).0;
896                self.set_piece(rook_to, rook_piece);
897                self.set_piece(rook_from, 0);
898            }
899        }
900
901        if promotion {
902            let color = if is_white { WHITE } else { BLACK };
903            self.set_piece(to, position_move.promotion_piece | color);
904        } else {
905            self.set_piece(to, piece.0);
906        }
907        self.set_piece(from, 0);
908
909        //TODO instead of downgrading all pawns EVERY MOVE, just keep track of en passants per "next move"
910        // and clear every move, you can only have one en passant per move anyway.
911        for pos in self.get_piece_positions_by_type(PieceType::Pawn(true)) {
912            if to == pos {
913                continue;
914            }
915            self.get_piece_mut(pos).0 %= 32;
916        }
917
918        // mark pawn as en-passantable if it moved 2 squares
919        self.handle_convert_to_en_passantable(position_move);
920    }
921    pub fn undo_move(&mut self, piece_move: PositionMove, last_piece: u32) {
922        let PositionMove {
923            from,
924            to,
925            en_passant,
926            ..
927        } = piece_move;
928
929        let piece = self.get_piece(to);
930        let is_white = piece.get_color() == PieceColor::White;
931
932        // undo castling rook movement if this was a castling move
933        if let Some(PieceType::King) = piece.get_type() {
934            if from[1] == 4 && to[1] == 6 {
935                // undo kingside castle - move rook back from f-file to h-file
936                let rook_from = [from[0], 5];
937                let rook_to = [from[0], 7];
938                let rook_piece = self.get_piece(rook_from).0;
939                self.set_piece(rook_to, rook_piece);
940                self.set_piece(rook_from, 0);
941            } else if from[1] == 4 && to[1] == 2 {
942                // undo queenside castle - move rook back from d-file to a-file
943                let rook_from = [from[0], 3];
944                let rook_to = [from[0], 0];
945                let rook_piece = self.get_piece(rook_from).0;
946                self.set_piece(rook_to, rook_piece);
947                self.set_piece(rook_from, 0);
948            }
949        }
950
951        self.set_piece(from, piece.0);
952        self.set_piece(to, last_piece);
953
954        // undo en passant - restore the captured pawn
955        if en_passant {
956            let shift = if is_white { -1 } else { 1 };
957            let captured_pawn_pos = [(to[0] as isize - shift) as usize, to[1]];
958            // restore the captured pawn (it was the opponent's pawn with en-passant flag)
959            let opponent_color = if is_white { BLACK } else { WHITE };
960            self.set_piece(captured_pawn_pos, PAWN | opponent_color | 32);
961        }
962    }
963    /// generates all moves based on active color.
964    pub fn gen_all_legal_moves(&self) -> Vec<PositionMove> {
965        let mut legal_moves = vec![];
966        for rank in 0..8 {
967            for file in 0..8 {
968                let piece = self.squares[rank][file];
969                if piece.is_piece() && piece.get_color() == self.active_color {
970                    let from_move = [rank, file];
971                    let to_moves = self.gen_legal_positions([rank, file]);
972                    let moves = to_moves
973                        .iter()
974                        .map(|&to| PositionMove::new(from_move, to))
975                        .collect::<Vec<_>>();
976                    legal_moves.extend(moves);
977                }
978            }
979        }
980        legal_moves
981    }
982    pub fn gen_all_opponent_positions(&self) -> Vec<Position> {
983        let mut opponent_positions = vec![];
984        for rank in 0..8 {
985            for file in 0..8 {
986                let piece = self.squares[rank][file];
987                if piece.is_piece() && piece.get_color() != self.active_color {
988                    let positions = self.gen_to_positions([rank, file]);
989                    opponent_positions.extend(positions);
990                }
991            }
992        }
993        opponent_positions
994    }
995    pub fn is_en_passant(&self, from: Position, to: Position) -> bool {
996        // only en passant moves can be moved diagonally on an empty square
997        if self.get_piece(to).is_piece() {
998            return false;
999        }
1000        let piece = self.get_piece(from);
1001        if let Some(PieceType::Pawn(_)) = piece.get_type() {
1002            let shift = match piece.get_color() {
1003                PieceColor::Black => 1,
1004                PieceColor::White => -1,
1005            };
1006            let step_pos = [(to[0] as isize - shift) as usize, to[1]];
1007            let step_piece = self.get_piece(step_pos);
1008            if step_piece.is_piece() && step_piece.get_color() != piece.get_color() {
1009                if let Some(PieceType::Pawn(_)) = step_piece.get_type() {
1010                    return true;
1011                }
1012            }
1013        }
1014        false
1015    }
1016    pub fn is_promotion(&self, from: Position, to: Position) -> bool {
1017        let piece = self.get_piece(from);
1018        if let Some(piece_type) = piece.get_type() {
1019            return (piece_type == PieceType::Pawn(false) || piece_type == PieceType::Pawn(true))
1020                && (to[0] == 7 || to[0] == 0);
1021        }
1022
1023        false
1024    }
1025
1026    /// Check if a square is attacked by pieces of the given color
1027    pub fn is_square_attacked_by(&self, square: Position, attacking_color: PieceColor) -> bool {
1028        let index = square[0] * 8 + square[1];
1029
1030        // check for attacking pawns
1031        let pawn_direction = if attacking_color == PieceColor::White {
1032            8
1033        } else {
1034            -8
1035        };
1036        for file_offset in [-1, 1] {
1037            let pawn_index = index as i32 + pawn_direction + file_offset;
1038            if (0..64).contains(&pawn_index) {
1039                let pawn_pos = [pawn_index as usize / 8, pawn_index as usize % 8];
1040
1041                // prevent wrapping
1042                if (square[1] as i32 + file_offset) >= 0 && (square[1] as i32 + file_offset) < 8 {
1043                    let attacker = self.get_piece(pawn_pos);
1044                    if attacker.is_piece()
1045                        && attacker.get_color() == attacking_color
1046                        && matches!(attacker.get_type(), Some(PieceType::Pawn(_)))
1047                    {
1048                        return true;
1049                    }
1050                }
1051            }
1052        }
1053
1054        // check for attacking knights
1055        for &offset in KNIGHT_DIRECTION_OFFSETS.iter() {
1056            let knight_rank = square[0] as i32 + offset[0];
1057            let knight_file = square[1] as i32 + offset[1];
1058            if (0..8).contains(&knight_rank) && (0..8).contains(&knight_file) {
1059                let attacker_pos = [knight_rank as usize, knight_file as usize];
1060                let attacker = self.get_piece(attacker_pos);
1061                if attacker.is_piece()
1062                    && attacker.get_color() == attacking_color
1063                    && attacker.get_type() == Some(PieceType::Knight)
1064                {
1065                    return true;
1066                }
1067            }
1068        }
1069
1070        // check for attacking sliding pieces (bishops, rooks, queens) and king
1071        for (dir_idx, &offset) in DIRECTION_OFFSETS.iter().enumerate() {
1072            let direction = Direction::from(dir_idx);
1073            let is_diagonal = dir_idx >= 4; // diagonal directions are indices 4-7
1074            let max_distance = self.len_to_edge(square, direction);
1075
1076            for n in 1..=max_distance {
1077                let target_index = index as i32 + offset * n as i32;
1078                if !(0..64).contains(&target_index) {
1079                    break;
1080                }
1081
1082                let check_pos = [target_index as usize / 8, target_index as usize % 8];
1083                let piece = self.get_piece(check_pos);
1084
1085                if piece.is_piece() {
1086                    if piece.get_color() == attacking_color {
1087                        match piece.get_type() {
1088                            Some(PieceType::Queen) => return true,
1089                            Some(PieceType::Bishop) if is_diagonal => return true,
1090                            Some(PieceType::Rook) if !is_diagonal => return true,
1091                            Some(PieceType::King) if n == 1 => return true,
1092                            _ => {}
1093                        }
1094                    }
1095                    break; // piece blocks further checks in this direction
1096                }
1097            }
1098        }
1099
1100        false
1101    }
1102
1103    // f(p) = 200(K-K')
1104    //        + 9(Q-Q')
1105    //        + 5(R-R')
1106    //        + 3(B-B' + N-N')
1107    //        + 1(P-P')
1108    //        - 0.5(D-D' + S-S' + I-I')
1109    //        + 0.1(M-M') + ...
1110    // KQRBNP = number of kings, queens, rooks, bishops, knights and pawns
1111    //  D,S,I = doubled, blocked and isolated pawns
1112    //  M = Mobility (the number of legal moves)
1113    pub fn get_material_weight(&self) -> i32 {
1114        let mut res = 0;
1115        for row in self.squares.iter() {
1116            for piece in row.iter() {
1117                let mut value = piece.0;
1118                if value >= 32 {
1119                    value %= 32;
1120                } // en passantable pawns
1121                let piece_value =
1122                    if self.active_color == PieceColor::White && value > WHITE && value < BLACK {
1123                        value - WHITE
1124                    } else if self.active_color == PieceColor::Black && value > BLACK {
1125                        value - BLACK
1126                    } else {
1127                        continue;
1128                    };
1129
1130                let mut piece_weight = match piece_value {
1131                    PAWN => 1,
1132                    KNIGHT | BISHOP => 3,
1133                    ROOK => 5,
1134                    QUEEN => 9,
1135                    KING => 200,
1136                    _ => unimplemented!("{} is not a valid piece value", piece.0),
1137                };
1138                if self.active_color != piece.get_color() {
1139                    piece_weight *= -1;
1140                }
1141                res += piece_weight;
1142            }
1143        }
1144        res
1145    }
1146    pub fn get_num_white_pieces(&self) -> i32 {
1147        self.squares.iter().fold(0, |res, row| {
1148            res + row
1149                .iter()
1150                .filter(|&&item| item.0 > WHITE && item.0 < BLACK)
1151                .count()
1152        }) as i32
1153    }
1154    pub fn get_num_black_pieces(&self) -> i32 {
1155        self.squares.iter().fold(0, |res, row| {
1156            res + row.iter().filter(|&&item| item.0 > BLACK).count()
1157        }) as i32
1158    }
1159    fn get_positions_from_type(&self, piece_type: &PieceType) -> Vec<Position> {
1160        let mut possible_positions = vec![];
1161        for (i, row) in self.squares.iter().enumerate() {
1162            for (j, position) in row.iter().enumerate() {
1163                if position.is_piece() {
1164                    if let Some(pt) = position.get_type() {
1165                        if piece_type == &pt && position.get_color() == self.active_color {
1166                            possible_positions.push([i, j]);
1167                        }
1168                    }
1169                }
1170            }
1171        }
1172        possible_positions
1173    }
1174    pub fn switch_active_color(&mut self) {
1175        self.active_color = if self.active_color == PieceColor::Black {
1176            PieceColor::White
1177        } else {
1178            PieceColor::Black
1179        };
1180    }
1181    fn move_should_enable_en_passant(&self, piece_move: PositionMove) -> bool {
1182        let PositionMove { from, to, .. } = piece_move;
1183        let piece = self.get_piece(to);
1184        if let Some(PieceType::Pawn(_)) = piece.get_type() {
1185            if *self.get_active_color() == piece.get_color() {
1186                return match piece.get_color() {
1187                    PieceColor::White => from[0] == 6 && to[0] == 4,
1188                    PieceColor::Black => from[0] == 1 && to[0] == 3,
1189                };
1190            }
1191        }
1192        false
1193    }
1194
1195    fn parse_uci_position_to_file_rank(&self, mut position: String) -> Result<Position> {
1196        let file = 7 - position
1197            .pop()
1198            .ok_or(anyhow!("can't pop last char of uci"))?
1199            .to_string()
1200            .parse::<usize>()?
1201            .sub(1);
1202
1203        let rank = (position
1204            .pop()
1205            .ok_or(anyhow!("can't pop last char of uci"))? as usize)
1206            .sub(97);
1207
1208        Ok([file, rank])
1209    }
1210
1211    fn verify_any_own_position(
1212        &self,
1213        positions: Vec<Position>,
1214        rank: Option<usize>,
1215    ) -> Result<Position> {
1216        for pos in positions.iter() {
1217            if let Some(rank) = rank {
1218                if pos[1] != rank {
1219                    continue;
1220                }
1221            }
1222            if self.own_position(pos) {
1223                return Ok(*pos);
1224            }
1225        }
1226        Err(anyhow!("Couldn't find [from] position for {:?}", positions))
1227    }
1228
1229    fn own_position(&self, pos: &Position) -> bool {
1230        let piece = self.get_piece(*pos);
1231        if piece.is_piece() && piece.get_color() == *self.get_active_color() {
1232            if let Some(PieceType::Pawn(_)) = piece.get_type() {
1233                return true;
1234            }
1235        }
1236        false
1237    }
1238
1239    fn handle_convert_to_en_passantable(&mut self, position_move: PositionMove) {
1240        let PositionMove { to, .. } = position_move;
1241        let should_enable_en_passant = self.move_should_enable_en_passant(position_move);
1242
1243        if should_enable_en_passant && self.get_piece(to).0 < 32 {
1244            self.get_piece_mut(to).0 += 32;
1245        }
1246    }
1247
1248    fn get_piece_positions_by_type(&self, piece_type: PieceType) -> Vec<Position> {
1249        let mut positions = Vec::with_capacity(64);
1250        for (y, row) in self.squares.iter().enumerate() {
1251            for (x, piece) in row.iter().enumerate() {
1252                if piece.get_type() == Some(piece_type) {
1253                    positions.push([y, x]);
1254                }
1255            }
1256        }
1257        positions
1258    }
1259
1260    fn black_can_long_castle(&self) -> bool {
1261        if self.black_king_moved || self.black_queenside_rook_moved {
1262            return false;
1263        }
1264        //TODO can be removed if single move turn supports castling
1265        let possible_king = self.get_piece([0, 4]);
1266        if possible_king.is_piece()
1267            && possible_king.is_black()
1268            && possible_king.get_type().unwrap() == PieceType::King
1269        {
1270            let row = self.squares[0];
1271            if !row[1..4].iter().all(|p| !p.is_piece()) {
1272                return false;
1273            }
1274            // check king doesn't pass through check
1275            // d8 c8 must not be attacked
1276            if self.is_square_attacked_by([0, 4], PieceColor::White)
1277                || self.is_square_attacked_by([0, 3], PieceColor::White)
1278                || self.is_square_attacked_by([0, 2], PieceColor::White)
1279            {
1280                return false;
1281            }
1282            return true;
1283        }
1284        false
1285    }
1286
1287    fn black_can_short_castle(&self) -> bool {
1288        if self.black_king_moved || self.black_kingside_rook_moved {
1289            return false;
1290        }
1291        //TODO can be removed if single move turn supports castling
1292        let possible_king = self.get_piece([0, 4]);
1293        if possible_king.is_piece()
1294            && possible_king.is_black()
1295            && possible_king.get_type().unwrap() == PieceType::King
1296        {
1297            let row = self.squares[0];
1298            if !row[5..7].iter().all(|p| !p.is_piece()) {
1299                return false;
1300            }
1301            // check king doesn't pass through check
1302            // e8 f8 and g8 must not be attacked
1303            if self.is_square_attacked_by([0, 4], PieceColor::White)
1304                || self.is_square_attacked_by([0, 5], PieceColor::White)
1305                || self.is_square_attacked_by([0, 6], PieceColor::White)
1306            {
1307                return false;
1308            }
1309            return true;
1310        }
1311        false
1312    }
1313
1314    fn white_can_long_castle(&self) -> bool {
1315        if self.white_king_moved || self.white_queenside_rook_moved {
1316            return false;
1317        }
1318        //TODO can be removed if single move turn supports castling
1319        let possible_king = self.get_piece([7, 4]);
1320        if possible_king.is_piece()
1321            && possible_king.is_white()
1322            && possible_king.get_type().unwrap() == PieceType::King
1323        {
1324            let row = self.squares[7];
1325            if !row[1..4].iter().all(|p| !p.is_piece()) {
1326                return false;
1327            }
1328            // check king doesn't pass through check
1329            // e1, d1 and c1 must not be attacked
1330            if self.is_square_attacked_by([7, 4], PieceColor::Black)
1331                || self.is_square_attacked_by([7, 3], PieceColor::Black)
1332                || self.is_square_attacked_by([7, 2], PieceColor::Black)
1333            {
1334                return false;
1335            }
1336            return true;
1337        }
1338        false
1339    }
1340
1341    fn white_can_short_castle(&self) -> bool {
1342        if self.white_king_moved || self.white_kingside_rook_moved {
1343            return false;
1344        }
1345        //TODO can be removed if single move turn supports castling
1346        let possible_king = self.get_piece([7, 4]);
1347        if possible_king.is_piece()
1348            && possible_king.is_white()
1349            && possible_king.get_type().unwrap() == PieceType::King
1350        {
1351            let row = self.squares[7];
1352            if !row[5..7].iter().all(|p| !p.is_piece()) {
1353                return false;
1354            }
1355            // check king doesn't pass through check
1356            // e1, f1 and g1 must not be attacked
1357            if self.is_square_attacked_by([7, 4], PieceColor::Black)
1358                || self.is_square_attacked_by([7, 5], PieceColor::Black)
1359                || self.is_square_attacked_by([7, 6], PieceColor::Black)
1360            {
1361                return false;
1362            }
1363            return true;
1364        }
1365        false
1366    }
1367}
1368
1369impl Debug for BoardMap {
1370    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
1371        for i in 0..8 {
1372            writeln!(f, "{} {:?}", 8 - i, self.squares[i]).unwrap();
1373        }
1374        writeln!(f, "    a    b    c    d    e    f    g    h").unwrap();
1375        writeln!(f, "fen: {}", self.get_fen()).unwrap();
1376        writeln!(
1377            f,
1378            "{}'s turn",
1379            match self.active_color {
1380                PieceColor::Black => "black",
1381                PieceColor::White => "white",
1382            }
1383        )
1384        .unwrap();
1385
1386        Ok(())
1387    }
1388}
1389
1390impl Deref for BoardMap {
1391    type Target = [[Piece; 8]; 8];
1392
1393    fn deref(&self) -> &Self::Target {
1394        &self.squares
1395    }
1396}
1397
1398impl DerefMut for BoardMap {
1399    fn deref_mut(&mut self) -> &mut Self::Target {
1400        &mut self.squares
1401    }
1402}