1pub mod movegen;
2pub mod validation;
3
4use std::fmt::{Debug, Display, Formatter};
5
6use anyhow::{Context, bail};
7
8use arrayvec::ArrayVec;
9use movegen::RAY_BETWEEN;
10use rand::{rngs::ThreadRng, seq::IndexedRandom};
11
12use crate::{
13 chess::{
14 board::movegen::{
15 MoveList, bishop_attacks, king_attacks, knight_attacks, pawn_attacks, rook_attacks,
16 },
17 chessmove::Move,
18 piece::{Black, Col, Colour, Piece, PieceType, White},
19 squareset::SquareSet,
20 types::{CastlingRights, CheckState, File, Rank, Square, Undo},
21 },
22 makemove::{hash_castling, hash_ep, hash_piece, hash_side},
23};
24
25use crate::chess::piecelayout::{PieceLayout, Threats};
26
27#[derive(Clone, Copy, Debug)]
28pub struct MovedPiece {
29 pub from: Square,
30 pub to: Square,
31 pub piece: Piece,
32}
33
34#[derive(Debug, Copy, Clone, PartialEq, Eq)]
36pub struct FeatureUpdate {
37 pub sq: Square,
38 pub piece: Piece,
39}
40
41impl Display for FeatureUpdate {
42 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
43 write!(f, "{piece} on {sq}", piece = self.piece, sq = self.sq)
44 }
45}
46
47#[derive(PartialEq, Eq, Clone, Debug, Default)]
48pub struct UpdateBuffer {
49 add: ArrayVec<FeatureUpdate, 2>,
50 sub: ArrayVec<FeatureUpdate, 2>,
51}
52
53impl UpdateBuffer {
54 pub fn move_piece(&mut self, from: Square, to: Square, piece: Piece) {
55 self.add.push(FeatureUpdate { sq: to, piece });
56 self.sub.push(FeatureUpdate { sq: from, piece });
57 }
58
59 pub fn clear_piece(&mut self, sq: Square, piece: Piece) {
60 self.sub.push(FeatureUpdate { sq, piece });
61 }
62
63 pub fn add_piece(&mut self, sq: Square, piece: Piece) {
64 self.add.push(FeatureUpdate { sq, piece });
65 }
66
67 pub fn adds(&self) -> &[FeatureUpdate] {
68 &self.add[..]
69 }
70
71 pub fn subs(&self) -> &[FeatureUpdate] {
72 &self.sub[..]
73 }
74}
75
76#[derive(Clone, PartialEq, Eq)]
77pub struct Board {
78 pub pieces: PieceLayout,
80 pub piece_array: [Option<Piece>; 64],
82 side: Colour,
84 ep_sq: Option<Square>,
86 castle_perm: CastlingRights,
88 fifty_move_counter: u8,
90 ply: usize,
92
93 key: u64,
95 pawn_key: u64,
97 non_pawn_key: [u64; 2],
99 minor_key: u64,
101 major_key: u64,
103
104 threats: Threats,
106
107 chess960: bool,
109
110 height: usize,
111 history: Vec<Undo>,
112}
113
114impl Debug for Board {
115 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
116 f.debug_struct("Board")
117 .field("piece_array", &self.piece_array)
118 .field("side", &self.side)
119 .field("ep_sq", &self.ep_sq)
120 .field("fifty_move_counter", &self.fifty_move_counter)
121 .field("height", &self.height)
122 .field("ply", &self.ply)
123 .field("key", &self.key)
124 .field("threats", &self.threats)
125 .field("castle_perm", &self.castle_perm)
126 .finish_non_exhaustive()
127 }
128}
129
130impl Board {
131 pub const STARTING_FEN: &'static str =
132 "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1";
133 pub const STARTING_FEN_960: &'static str =
134 "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w AHah - 0 1";
135
136 pub fn new() -> Self {
137 let mut out = Self {
138 pieces: PieceLayout::NULL,
139 piece_array: [None; 64],
140 side: Colour::White,
141 ep_sq: None,
142 fifty_move_counter: 0,
143 height: 0,
144 ply: 0,
145 key: 0,
146 pawn_key: 0,
147 non_pawn_key: [0; 2],
148 minor_key: 0,
149 major_key: 0,
150 threats: Threats::default(),
151 castle_perm: CastlingRights::NONE,
152 history: Vec::new(),
153 chess960: true,
154 };
155 out.reset();
156 out
157 }
158
159 pub fn to_bulletformat(
160 &self,
161 wdl: u8,
162 eval: i16,
163 ) -> Result<bulletformat::ChessBoard, anyhow::Error> {
164 let mut bbs = [0; 8];
165 let piece_layout = &self.pieces;
166 bbs[0] = piece_layout.occupied_co(Colour::White).inner();
167 bbs[1] = piece_layout.occupied_co(Colour::Black).inner();
168 bbs[2] = piece_layout.of_type(PieceType::Pawn).inner();
169 bbs[3] = piece_layout.of_type(PieceType::Knight).inner();
170 bbs[4] = piece_layout.of_type(PieceType::Bishop).inner();
171 bbs[5] = piece_layout.of_type(PieceType::Rook).inner();
172 bbs[6] = piece_layout.of_type(PieceType::Queen).inner();
173 bbs[7] = piece_layout.of_type(PieceType::King).inner();
174 let bulletformat = bulletformat::ChessBoard::from_raw(
175 bbs,
176 (self.turn() != Colour::White).into(),
177 eval,
178 f32::from(wdl) / 2.0,
179 )
180 .map_err(|e| anyhow::anyhow!(e))
181 .with_context(|| "Failed to convert raw components into bulletformat::ChessBoard.")?;
182 Ok(bulletformat)
183 }
184
185 pub const fn ep_sq(&self) -> Option<Square> {
186 self.ep_sq
187 }
188
189 pub fn ep_sq_mut(&mut self) -> &mut Option<Square> {
190 &mut self.ep_sq
191 }
192
193 pub fn turn_mut(&mut self) -> &mut Colour {
194 &mut self.side
195 }
196
197 pub fn halfmove_clock_mut(&mut self) -> &mut u8 {
198 &mut self.fifty_move_counter
199 }
200
201 pub fn set_fullmove_clock(&mut self, fullmove_clock: u16) {
202 self.ply = (fullmove_clock as usize - 1) * 2 + usize::from(self.side == Colour::Black);
203 }
204
205 pub const fn zobrist_key(&self) -> u64 {
206 self.key
207 }
208
209 pub const fn pawn_key(&self) -> u64 {
210 self.pawn_key
211 }
212
213 pub fn non_pawn_key(&self, colour: Colour) -> u64 {
214 self.non_pawn_key[colour]
215 }
216
217 pub const fn minor_key(&self) -> u64 {
218 self.minor_key
219 }
220
221 pub const fn major_key(&self) -> u64 {
222 self.major_key
223 }
224
225 #[cfg(debug_assertions)]
226 pub const fn all_keys(&self) -> (u64, u64, [u64; 2], u64, u64) {
227 (
228 self.key,
229 self.pawn_key,
230 self.non_pawn_key,
231 self.minor_key,
232 self.major_key,
233 )
234 }
235
236 pub fn n_men(&self) -> u8 {
237 #![allow(clippy::cast_possible_truncation)]
238 self.pieces.occupied().count() as u8
239 }
240
241 pub const fn ply(&self) -> usize {
242 self.ply
243 }
244
245 pub const fn threats(&self) -> &Threats {
246 &self.threats
247 }
248
249 pub fn king_sq(&self, side: Colour) -> Square {
250 debug_assert!(side == Colour::White || side == Colour::Black);
251 debug_assert_eq!(self.pieces.king::<White>().count(), 1);
252 debug_assert_eq!(self.pieces.king::<Black>().count(), 1);
253 let sq = match side {
254 Colour::White => self.pieces.king::<White>().first(),
255 Colour::Black => self.pieces.king::<Black>().first(),
256 };
257 debug_assert_eq!(self.pieces.piece_at(sq).unwrap().colour(), side);
258 debug_assert_eq!(
259 self.pieces.piece_at(sq).unwrap().piece_type(),
260 PieceType::King
261 );
262 sq
263 }
264
265 pub const fn in_check(&self) -> bool {
266 self.threats.checkers.non_empty()
267 }
268
269 pub fn zero_height(&mut self) {
270 self.height = 0;
271 }
272
273 pub const fn height(&self) -> usize {
274 self.height
275 }
276
277 pub const fn turn(&self) -> Colour {
278 self.side
279 }
280
281 pub const fn castling_rights(&self) -> CastlingRights {
282 self.castle_perm
283 }
284
285 pub fn castling_rights_mut(&mut self) -> &mut CastlingRights {
286 &mut self.castle_perm
287 }
288
289 pub fn generate_pos_keys(&self) -> (u64, u64, [u64; 2], u64, u64) {
290 let mut key = 0;
291 let mut pawn_key = 0;
292 let mut non_pawn_key = [0; 2];
293 let mut minor_key = 0;
294 let mut major_key = 0;
295 self.pieces.visit_pieces(|sq, piece| {
296 hash_piece(&mut key, piece, sq);
297 if piece.piece_type() == PieceType::Pawn {
298 hash_piece(&mut pawn_key, piece, sq);
299 } else {
300 hash_piece(&mut non_pawn_key[piece.colour()], piece, sq);
301 if piece.piece_type() == PieceType::King {
302 hash_piece(&mut major_key, piece, sq);
303 hash_piece(&mut minor_key, piece, sq);
304 } else if matches!(piece.piece_type(), PieceType::Queen | PieceType::Rook) {
305 hash_piece(&mut major_key, piece, sq);
306 } else {
307 hash_piece(&mut minor_key, piece, sq);
308 }
309 }
310 });
311
312 if self.side == Colour::White {
313 hash_side(&mut key);
314 }
315
316 if let Some(ep_sq) = self.ep_sq {
317 hash_ep(&mut key, ep_sq);
318 }
319
320 hash_castling(&mut key, self.castle_perm);
321
322 debug_assert!(self.fifty_move_counter <= 100);
323
324 (key, pawn_key, non_pawn_key, minor_key, major_key)
325 }
326
327 pub fn regenerate_zobrist(&mut self) {
328 (
329 self.key,
330 self.pawn_key,
331 self.non_pawn_key,
332 self.minor_key,
333 self.major_key,
334 ) = self.generate_pos_keys();
335 }
336
337 pub fn regenerate_threats(&mut self) {
338 self.threats = self.generate_threats(self.side.flip());
339 }
340
341 pub fn generate_threats(&self, side: Colour) -> Threats {
342 if side == Colour::White {
343 self.generate_threats_from::<White>()
344 } else {
345 self.generate_threats_from::<Black>()
346 }
347 }
348
349 pub fn generate_threats_from<C: Col>(&self) -> Threats {
350 let mut threats = SquareSet::EMPTY;
351 let mut checkers = SquareSet::EMPTY;
352
353 let their_pawns = self.pieces.pawns::<C>();
354 let their_knights = self.pieces.knights::<C>();
355 let their_diags = self.pieces.diags::<C>();
356 let their_orthos = self.pieces.orthos::<C>();
357 let their_king = self.king_sq(C::COLOUR);
358 let blockers = self.pieces.occupied();
359
360 threats |= pawn_attacks::<C>(their_pawns);
362
363 for sq in their_knights {
364 threats |= knight_attacks(sq);
365 }
366 for sq in their_diags {
367 threats |= bishop_attacks(sq, blockers);
368 }
369 for sq in their_orthos {
370 threats |= rook_attacks(sq, blockers);
371 }
372
373 threats |= king_attacks(their_king);
374
375 let our_king = self.king_sq(C::Opposite::COLOUR);
377 let king_bb = our_king.as_set();
378 let backwards_from_king = pawn_attacks::<C::Opposite>(king_bb);
379 checkers |= backwards_from_king & their_pawns;
380
381 let knight_attacks = knight_attacks(our_king);
382
383 checkers |= knight_attacks & their_knights;
384
385 let diag_attacks = bishop_attacks(our_king, blockers);
386
387 checkers |= diag_attacks & their_diags;
388
389 let ortho_attacks = rook_attacks(our_king, blockers);
390
391 checkers |= ortho_attacks & their_orthos;
392
393 Threats {
394 all: threats,
395 checkers,
396 }
397 }
398
399 pub fn reset(&mut self) {
400 self.pieces.reset();
401 self.piece_array = [None; 64];
402 self.side = Colour::White;
403 self.ep_sq = None;
404 self.fifty_move_counter = 0;
405 self.height = 0;
406 self.ply = 0;
407 self.castle_perm = CastlingRights::NONE;
408 self.key = 0;
409 self.pawn_key = 0;
410 self.threats = Threats::default();
411 self.chess960 = true;
412 self.history.clear();
413 }
414
415 pub fn set_frc_idx(&mut self, scharnagl: usize) {
416 #![allow(clippy::cast_possible_truncation)]
417 assert!(scharnagl < 960, "scharnagl index out of range");
418 let backrank = Self::get_scharnagl_backrank(scharnagl);
419 self.reset();
420 for (&piece_type, file) in backrank.iter().zip(File::all()) {
421 let sq = Square::from_rank_file(Rank::One, file);
422 self.add_piece(sq, Piece::new(Colour::White, piece_type));
423 }
424 for file in File::all() {
425 let sq = Square::from_rank_file(Rank::Two, file);
427 self.add_piece(sq, Piece::new(Colour::White, PieceType::Pawn));
428 }
429 for (&piece_type, file) in backrank.iter().zip(File::all()) {
430 let sq = Square::from_rank_file(Rank::Eight, file);
431 self.add_piece(sq, Piece::new(Colour::Black, piece_type));
432 }
433 for file in File::all() {
434 let sq = Square::from_rank_file(Rank::Seven, file);
436 self.add_piece(sq, Piece::new(Colour::Black, PieceType::Pawn));
437 }
438 let mut rook_indices = backrank.iter().enumerate().filter_map(|(i, &piece)| {
439 if piece == PieceType::Rook {
440 Some(i)
441 } else {
442 None
443 }
444 });
445 let queenside_file = rook_indices.next().unwrap();
446 let kingside_file = rook_indices.next().unwrap();
447 self.castle_perm = CastlingRights {
448 wk: Some(Square::from_rank_file(
449 Rank::One,
450 File::from_index(kingside_file as u8).unwrap(),
451 )),
452 wq: Some(Square::from_rank_file(
453 Rank::One,
454 File::from_index(queenside_file as u8).unwrap(),
455 )),
456 bk: Some(Square::from_rank_file(
457 Rank::Eight,
458 File::from_index(kingside_file as u8).unwrap(),
459 )),
460 bq: Some(Square::from_rank_file(
461 Rank::Eight,
462 File::from_index(queenside_file as u8).unwrap(),
463 )),
464 };
465 (
466 self.key,
467 self.pawn_key,
468 self.non_pawn_key,
469 self.minor_key,
470 self.major_key,
471 ) = self.generate_pos_keys();
472 self.threats = self.generate_threats(self.side.flip());
473 }
474
475 pub fn set_dfrc_idx(&mut self, scharnagl: usize) {
476 #![allow(clippy::cast_possible_truncation)]
477 assert!(scharnagl < 960 * 960, "double scharnagl index out of range");
478 let white_backrank = Self::get_scharnagl_backrank(scharnagl % 960);
479 let black_backrank = Self::get_scharnagl_backrank(scharnagl / 960);
480 self.reset();
481 for (&piece_type, file) in white_backrank.iter().zip(File::all()) {
482 let sq = Square::from_rank_file(Rank::One, file);
483 self.add_piece(sq, Piece::new(Colour::White, piece_type));
484 }
485 for file in File::all() {
486 let sq = Square::from_rank_file(Rank::Two, file);
488 self.add_piece(sq, Piece::new(Colour::White, PieceType::Pawn));
489 }
490 for (&piece_type, file) in black_backrank.iter().zip(File::all()) {
491 let sq = Square::from_rank_file(Rank::Eight, file);
492 self.add_piece(sq, Piece::new(Colour::Black, piece_type));
493 }
494 for file in File::all() {
495 let sq = Square::from_rank_file(Rank::Seven, file);
497 self.add_piece(sq, Piece::new(Colour::Black, PieceType::Pawn));
498 }
499 let mut white_rook_indices = white_backrank.iter().enumerate().filter_map(|(i, &piece)| {
500 if piece == PieceType::Rook {
501 Some(i)
502 } else {
503 None
504 }
505 });
506 let white_queenside_file = white_rook_indices.next().unwrap();
507 let white_kingside_file = white_rook_indices.next().unwrap();
508 let mut black_rook_indices = black_backrank.iter().enumerate().filter_map(|(i, &piece)| {
509 if piece == PieceType::Rook {
510 Some(i)
511 } else {
512 None
513 }
514 });
515 let black_queenside_file = black_rook_indices.next().unwrap();
516 let black_kingside_file = black_rook_indices.next().unwrap();
517 self.castle_perm = CastlingRights {
518 wk: Some(Square::from_rank_file(
519 Rank::One,
520 File::from_index(white_kingside_file as u8).unwrap(),
521 )),
522 wq: Some(Square::from_rank_file(
523 Rank::One,
524 File::from_index(white_queenside_file as u8).unwrap(),
525 )),
526 bk: Some(Square::from_rank_file(
527 Rank::Eight,
528 File::from_index(black_kingside_file as u8).unwrap(),
529 )),
530 bq: Some(Square::from_rank_file(
531 Rank::Eight,
532 File::from_index(black_queenside_file as u8).unwrap(),
533 )),
534 };
535 (
536 self.key,
537 self.pawn_key,
538 self.non_pawn_key,
539 self.minor_key,
540 self.major_key,
541 ) = self.generate_pos_keys();
542 self.threats = self.generate_threats(self.side.flip());
543 }
544
545 pub fn get_scharnagl_backrank(scharnagl: usize) -> [PieceType; 8] {
546 let mut out = [None; 8];
565 let n = scharnagl;
566 let (n2, b1) = (n / 4, n % 4);
567 match b1 {
568 0 => out[File::B] = Some(PieceType::Bishop),
569 1 => out[File::D] = Some(PieceType::Bishop),
570 2 => out[File::F] = Some(PieceType::Bishop),
571 3 => out[File::H] = Some(PieceType::Bishop),
572 _ => unreachable!(),
573 }
574 let (n3, b2) = (n2 / 4, n2 % 4);
575 match b2 {
576 0 => out[File::A] = Some(PieceType::Bishop),
577 1 => out[File::C] = Some(PieceType::Bishop),
578 2 => out[File::E] = Some(PieceType::Bishop),
579 3 => out[File::G] = Some(PieceType::Bishop),
580 _ => unreachable!(),
581 }
582 let (n4, mut q) = (n3 / 6, n3 % 6);
583 for (idx, &piece) in out.iter().enumerate() {
584 if piece.is_none() {
585 if q == 0 {
586 out[idx] = Some(PieceType::Queen);
587 break;
588 }
589 q -= 1;
590 }
591 }
592 let remaining_slots = out.iter_mut().filter(|piece| piece.is_none());
593 let selection = match n4 {
594 0 => [0, 1],
595 1 => [0, 2],
596 2 => [0, 3],
597 3 => [0, 4],
598 4 => [1, 2],
599 5 => [1, 3],
600 6 => [1, 4],
601 7 => [2, 3],
602 8 => [2, 4],
603 9 => [3, 4],
604 _ => unreachable!(),
605 };
606 for (i, slot) in remaining_slots.enumerate() {
607 if i == selection[0] || i == selection[1] {
608 *slot = Some(PieceType::Knight);
609 }
610 }
611
612 out.iter_mut()
613 .filter(|piece| piece.is_none())
614 .zip([PieceType::Rook, PieceType::King, PieceType::Rook])
615 .for_each(|(slot, piece)| *slot = Some(piece));
616
617 out.map(Option::unwrap)
618 }
619
620 pub fn set_from_fen(&mut self, fen: &str, chess960: bool) -> anyhow::Result<()> {
621 if !fen.is_ascii() {
622 bail!(format!("FEN string is not ASCII: {fen}"));
623 }
624
625 let mut rank = Rank::Eight;
626 let mut file = File::A;
627
628 self.reset();
629
630 self.chess960 = chess960;
631
632 let fen_chars = fen.as_bytes();
633 let split_idx = fen_chars
634 .iter()
635 .position(|&c| c == b' ')
636 .with_context(|| format!("FEN string is missing space: {fen}"))?;
637 let (board_part, info_part) = fen_chars.split_at(split_idx);
638
639 for &c in board_part {
640 let mut count = 1;
641 let piece;
642 match c {
643 b'P' => piece = Some(Piece::WP),
644 b'R' => piece = Some(Piece::WR),
645 b'N' => piece = Some(Piece::WN),
646 b'B' => piece = Some(Piece::WB),
647 b'Q' => piece = Some(Piece::WQ),
648 b'K' => piece = Some(Piece::WK),
649 b'p' => piece = Some(Piece::BP),
650 b'r' => piece = Some(Piece::BR),
651 b'n' => piece = Some(Piece::BN),
652 b'b' => piece = Some(Piece::BB),
653 b'q' => piece = Some(Piece::BQ),
654 b'k' => piece = Some(Piece::BK),
655 b'1'..=b'8' => {
656 piece = None;
657 count = c - b'0';
658 }
659 b'/' => {
660 rank = rank.sub(1).unwrap();
661 file = File::A;
662 continue;
663 }
664 c => {
665 bail!(
666 "FEN string is invalid, got unexpected character: \"{}\"",
667 c as char
668 );
669 }
670 }
671
672 for _ in 0..count {
673 let sq = Square::from_rank_file(rank, file);
674 if let Some(piece) = piece {
675 self.add_piece(sq, piece);
677 }
678 file = file.add(1).unwrap_or(File::H);
679 }
680 }
681
682 let mut info_parts = info_part[1..].split(|&c| c == b' ');
683
684 self.set_side(info_parts.next())?;
685 self.set_castling(info_parts.next())?;
686 self.set_ep(info_parts.next())?;
687 self.set_halfmove(info_parts.next())?;
688 self.set_fullmove(info_parts.next())?;
689
690 (
691 self.key,
692 self.pawn_key,
693 self.non_pawn_key,
694 self.minor_key,
695 self.major_key,
696 ) = self.generate_pos_keys();
697 self.threats = self.generate_threats(self.side.flip());
698
699 Ok(())
700 }
701
702 pub fn set_startpos(&mut self, chess960: bool) {
703 let starting_fen = if self.chess960 {
704 Self::STARTING_FEN_960
705 } else {
706 Self::STARTING_FEN
707 };
708 self.set_from_fen(starting_fen, chess960)
709 .expect("for some reason, STARTING_FEN is now broken.");
710 }
711
712 #[cfg(test)]
713 pub fn from_fen(fen: &str, chess960: bool) -> anyhow::Result<Self> {
714 let mut out = Self::new();
715 out.set_from_fen(fen, chess960)?;
716 Ok(out)
717 }
718
719 #[cfg(test)]
720 pub fn from_frc_idx(scharnagl: usize) -> Self {
721 let mut out = Self::new();
722 out.set_frc_idx(scharnagl);
723 out
724 }
725
726 #[cfg(test)]
727 pub fn from_dfrc_idx(scharnagl: usize) -> Self {
728 let mut out = Self::new();
729 out.set_dfrc_idx(scharnagl);
730 out
731 }
732
733 fn set_side(&mut self, side_part: Option<&[u8]>) -> anyhow::Result<()> {
734 self.side = match side_part {
735 Some([b'w']) => Colour::White,
736 Some([b'b']) => Colour::Black,
737 Some(other) => {
738 bail!(format!(
739 "FEN string is invalid, expected side to be 'w' or 'b', got \"{}\"",
740 std::str::from_utf8(other).unwrap_or("<invalid utf8>")
741 ))
742 }
743 None => bail!("FEN string is invalid, expected side part."),
744 };
745 Ok(())
746 }
747
748 fn set_castling(&mut self, castling_part: Option<&[u8]>) -> anyhow::Result<()> {
749 match castling_part {
750 None => bail!("FEN string is invalid, expected castling part."),
751 Some(b"-") => self.castle_perm = CastlingRights::NONE,
752 Some(castling) if !self.chess960 => {
753 for &c in castling {
754 match c {
755 b'K' => self.castle_perm.wk = Some(Square::H1),
756 b'Q' => self.castle_perm.wq = Some(Square::A1),
757 b'k' => self.castle_perm.bk = Some(Square::H8),
758 b'q' => self.castle_perm.bq = Some(Square::A8),
759 _ => {
760 bail!(format!(
761 "FEN string is invalid, expected castling part to be of the form 'KQkq', got \"{}\"",
762 std::str::from_utf8(castling).unwrap_or("<invalid utf8>")
763 ))
764 }
765 }
766 }
767 }
768 Some(shredder_castling) => {
769 let white_king = self.king_sq(Colour::White);
771 let black_king = self.king_sq(Colour::Black);
772 if white_king.rank() != Rank::One
773 && shredder_castling.iter().any(u8::is_ascii_uppercase)
774 {
775 bail!(format!(
776 "FEN string is invalid, white king is not on the back rank, but got uppercase castling characters, implying present castling rights, got \"{}\"",
777 std::str::from_utf8(shredder_castling).unwrap_or("<invalid utf8>")
778 ));
779 }
780 if black_king.rank() != Rank::Eight
781 && shredder_castling.iter().any(u8::is_ascii_lowercase)
782 {
783 bail!(format!(
784 "FEN string is invalid, black king is not on the back rank, but got lowercase castling characters, implying present castling rights, got \"{}\"",
785 std::str::from_utf8(shredder_castling).unwrap_or("<invalid utf8>")
786 ));
787 }
788 for &c in shredder_castling {
789 match c {
790 b'K' => self.castle_perm.wk = Some(Square::H1),
791 b'Q' => self.castle_perm.wq = Some(Square::A1),
792 b'k' => self.castle_perm.bk = Some(Square::H8),
793 b'q' => self.castle_perm.bq = Some(Square::A8),
794 c if c.is_ascii_uppercase() => {
795 let file = File::from_index(c - b'A').unwrap();
796 let king_file = white_king.file();
797 if file == king_file {
798 bail!(format!(
799 "FEN string is invalid, white king is on file {:?}, but got castling rights on that file - got \"{}\"",
800 king_file,
801 std::str::from_utf8(shredder_castling)
802 .unwrap_or("<invalid utf8>")
803 ));
804 }
805 let sq = Square::from_rank_file(Rank::One, file);
806 if file > king_file {
807 self.castle_perm.wk = Some(sq);
809 } else {
810 self.castle_perm.wq = Some(sq);
812 }
813 }
814 c if c.is_ascii_lowercase() => {
815 let file = File::from_index(c - b'a').unwrap();
816 let king_file = black_king.file();
817 if file == king_file {
818 bail!(format!(
819 "FEN string is invalid, black king is on file {:?}, but got castling rights on that file - got \"{}\"",
820 king_file,
821 std::str::from_utf8(shredder_castling)
822 .unwrap_or("<invalid utf8>")
823 ));
824 }
825 let sq = Square::from_rank_file(Rank::Eight, file);
826 if file > king_file {
827 self.castle_perm.bk = Some(sq);
829 } else {
830 self.castle_perm.bq = Some(sq);
832 }
833 }
834 _ => {
835 bail!(format!(
836 "FEN string is invalid, expected castling part to be of the form 'AHah', 'Bd', or '-', got \"{}\"",
837 std::str::from_utf8(shredder_castling).unwrap_or("<invalid utf8>")
838 ));
839 }
840 }
841 }
842 }
843 }
844
845 Ok(())
846 }
847
848 fn set_ep(&mut self, ep_part: Option<&[u8]>) -> anyhow::Result<()> {
849 match ep_part {
850 None => bail!("FEN string is invalid, expected en passant part.".to_string()),
851 Some([b'-']) => self.ep_sq = None,
852 Some(ep_sq) => {
853 if ep_sq.len() != 2 {
854 bail!(format!(
855 "FEN string is invalid, expected en passant part to be of the form 'a1', got \"{}\"",
856 std::str::from_utf8(ep_sq).unwrap_or("<invalid utf8>")
857 ));
858 }
859 let file = ep_sq[0] - b'a';
860 let rank = ep_sq[1] - b'1';
861 let file = File::from_index(file);
862 let rank = Rank::from_index(rank);
863 if !(file.is_some() && rank.is_some()) {
864 bail!(format!(
865 "FEN string is invalid, expected en passant part to be of the form 'a1', got \"{}\"",
866 std::str::from_utf8(ep_sq).unwrap_or("<invalid utf8>")
867 ));
868 }
869 self.ep_sq = Some(Square::from_rank_file(rank.unwrap(), file.unwrap()));
870 }
871 }
872
873 Ok(())
874 }
875
876 fn set_halfmove(&mut self, halfmove_part: Option<&[u8]>) -> anyhow::Result<()> {
877 match halfmove_part {
878 None => bail!("FEN string is invalid, expected halfmove clock part.".to_string()),
879 Some(halfmove_clock) => {
880 self.fifty_move_counter = std::str::from_utf8(halfmove_clock)
881 .with_context(|| "FEN string is invalid, expected halfmove clock part to be valid UTF-8")?
882 .parse::<u8>()
883 .with_context(|| {
884 format!(
885 "FEN string is invalid, expected halfmove clock part to be a number, got \"{}\"",
886 std::str::from_utf8(halfmove_clock).unwrap_or("<invalid utf8>")
887 )
888 })?;
889 }
890 }
891
892 Ok(())
893 }
894
895 fn set_fullmove(&mut self, fullmove_part: Option<&[u8]>) -> anyhow::Result<()> {
896 match fullmove_part {
897 None => bail!("FEN string is invalid, expected fullmove number part.".to_string()),
898 Some(fullmove_number) => {
899 let fullmove_number = std::str::from_utf8(fullmove_number)
900 .with_context(
901 || "FEN string is invalid, expected fullmove number part to be valid UTF-8",
902 )?
903 .parse::<usize>()
904 .with_context(
905 || "FEN string is invalid, expected fullmove number part to be a number",
906 )?;
907 self.ply = (fullmove_number - 1) * 2;
908 if self.side == Colour::Black {
909 self.ply += 1;
910 }
911 }
912 }
913
914 Ok(())
915 }
916
917 pub fn sq_attacked(&self, sq: Square, side: Colour) -> bool {
919 if side == Colour::White {
920 self.sq_attacked_by::<White>(sq)
921 } else {
922 self.sq_attacked_by::<Black>(sq)
923 }
924 }
925
926 pub fn sq_attacked_by<C: Col>(&self, sq: Square) -> bool {
927 if C::WHITE == (self.side == Colour::Black) {
936 return self.threats.all.contains_square(sq);
937 }
938
939 let sq_bb = sq.as_set();
940 let our_pawns = self.pieces.pawns::<C>();
941 let our_knights = self.pieces.knights::<C>();
942 let our_diags = self.pieces.diags::<C>();
943 let our_orthos = self.pieces.orthos::<C>();
944 let our_king = self.pieces.king::<C>();
945 let blockers = self.pieces.occupied();
946
947 let attacks = pawn_attacks::<C>(our_pawns);
949 if (attacks & sq_bb).non_empty() {
950 return true;
951 }
952
953 let knight_attacks_from_this_square = movegen::knight_attacks(sq);
955 if (our_knights & knight_attacks_from_this_square).non_empty() {
956 return true;
957 }
958
959 let diag_attacks_from_this_square = movegen::bishop_attacks(sq, blockers);
961 if (our_diags & diag_attacks_from_this_square).non_empty() {
962 return true;
963 }
964
965 let ortho_attacks_from_this_square = movegen::rook_attacks(sq, blockers);
967 if (our_orthos & ortho_attacks_from_this_square).non_empty() {
968 return true;
969 }
970
971 let king_attacks_from_this_square = movegen::king_attacks(sq);
973 if (our_king & king_attacks_from_this_square).non_empty() {
974 return true;
975 }
976
977 false
978 }
979
980 pub fn is_pseudo_legal(&self, m: Move) -> bool {
983 let from = m.from();
984 let to = m.to();
985
986 let moved_piece = self.piece_at(from);
987 let captured_piece = if m.is_castle() {
988 None
989 } else {
990 self.piece_at(to)
991 };
992 let is_pawn_double_push = self.is_double_pawn_push(m);
993
994 let Some(moved_piece) = moved_piece else {
995 return false;
996 };
997
998 if moved_piece.colour() != self.side {
999 return false;
1000 }
1001
1002 if let Some(captured_piece) = captured_piece
1003 && captured_piece.colour() == self.side
1004 {
1005 return false;
1006 }
1007
1008 if moved_piece.piece_type() != PieceType::Pawn
1009 && (is_pawn_double_push || m.is_ep() || m.is_promo())
1010 {
1011 return false;
1012 }
1013
1014 if moved_piece.piece_type() != PieceType::King && m.is_castle() {
1015 return false;
1016 }
1017
1018 if captured_piece.is_some() && is_pawn_double_push {
1019 return false;
1020 }
1021
1022 if m.is_castle() {
1023 return self.is_pseudo_legal_castling(m);
1024 }
1025
1026 if moved_piece.piece_type() == PieceType::Pawn {
1027 let should_be_promoting = to > Square::H7 || to < Square::A2;
1028 if should_be_promoting && !m.is_promo() {
1029 return false;
1030 }
1031 if m.is_ep() {
1032 return Some(to) == self.ep_sq;
1033 } else if is_pawn_double_push {
1034 if from.relative_to(self.side).rank() != Rank::Two {
1035 return false;
1036 }
1037 let Some(one_forward) = from.pawn_push(self.side) else {
1038 return false;
1039 };
1040 return self.piece_at(one_forward).is_none()
1041 && Some(to) == one_forward.pawn_push(self.side);
1042 } else if captured_piece.is_none() {
1043 return Some(to) == from.pawn_push(self.side);
1044 }
1045 if self.side == Colour::White {
1047 return (pawn_attacks::<White>(from.as_set()) & to.as_set()).non_empty();
1048 }
1049 return (pawn_attacks::<Black>(from.as_set()) & to.as_set()).non_empty();
1050 }
1051
1052 (to.as_set()
1053 & movegen::attacks_by_type(moved_piece.piece_type(), from, self.pieces.occupied()))
1054 .non_empty()
1055 }
1056
1057 pub fn is_pseudo_legal_castling(&self, m: Move) -> bool {
1058 let Some(moved) = self.piece_at(m.from()) else {
1067 return false;
1068 };
1069 if moved.piece_type() != PieceType::King {
1070 return false;
1071 }
1072 let home_rank = if self.side == Colour::White {
1073 SquareSet::RANK_1
1074 } else {
1075 SquareSet::RANK_8
1076 };
1077 if (m.to().as_set() & home_rank).is_empty() {
1078 return false;
1079 }
1080 if (m.from().as_set() & home_rank).is_empty() {
1081 return false;
1082 }
1083 let (king_dst, rook_dst) = if m.to() > m.from() {
1084 if Some(m.to())
1086 != (if self.side == Colour::Black {
1087 self.castle_perm.bk
1088 } else {
1089 self.castle_perm.wk
1090 })
1091 {
1092 return false;
1095 }
1096 if self.side == Colour::Black {
1097 (Square::G8, Square::F8)
1098 } else {
1099 (Square::G1, Square::F1)
1100 }
1101 } else {
1102 if Some(m.to())
1104 != (if self.side == Colour::Black {
1105 self.castle_perm.bq
1106 } else {
1107 self.castle_perm.wq
1108 })
1109 {
1110 return false;
1113 }
1114 if self.side == Colour::Black {
1115 (Square::C8, Square::D8)
1116 } else {
1117 (Square::C1, Square::D1)
1118 }
1119 };
1120
1121 let king_path = RAY_BETWEEN[m.from()][king_dst];
1123 let rook_path = RAY_BETWEEN[m.from()][m.to()];
1125 let castle_occ = self.pieces.occupied() ^ m.from().as_set() ^ m.to().as_set();
1127
1128 (castle_occ & (king_path | rook_path | king_dst.as_set() | rook_dst.as_set())).is_empty()
1129 && !self.any_attacked(king_path | m.from().as_set(), self.side.flip())
1130 }
1131
1132 pub fn any_attacked(&self, squares: SquareSet, by: Colour) -> bool {
1133 if by == self.side.flip() {
1134 (squares & self.threats.all).non_empty()
1135 } else {
1136 for sq in squares {
1137 if self.sq_attacked(sq, by) {
1138 return true;
1139 }
1140 }
1141 false
1142 }
1143 }
1144
1145 pub fn add_piece(&mut self, sq: Square, piece: Piece) {
1146 self.pieces.set_piece_at(sq, piece);
1147 *self.piece_at_mut(sq) = Some(piece);
1148 }
1149
1150 pub fn moved_piece(&self, m: Move) -> Option<Piece> {
1152 let idx = m.from();
1153 self.piece_array[idx]
1154 }
1155
1156 pub fn captured_piece(&self, m: Move) -> Option<Piece> {
1158 if m.is_castle() {
1159 return None;
1160 }
1161 let idx = m.to();
1162 self.piece_array[idx]
1163 }
1164
1165 pub fn is_capture(&self, m: Move) -> bool {
1167 self.captured_piece(m).is_some()
1168 }
1169
1170 pub fn is_double_pawn_push(&self, m: Move) -> bool {
1172 let from_bb = m.from().as_set();
1173 if (from_bb & (SquareSet::RANK_2 | SquareSet::RANK_7)).is_empty() {
1174 return false;
1175 }
1176 let to_bb = m.to().as_set();
1177 if (to_bb & (SquareSet::RANK_4 | SquareSet::RANK_5)).is_empty() {
1178 return false;
1179 }
1180 let Some(piece_moved) = self.moved_piece(m) else {
1181 return false;
1182 };
1183 piece_moved.piece_type() == PieceType::Pawn
1184 }
1185
1186 pub fn is_tactical(&self, m: Move) -> bool {
1188 m.is_promo() || m.is_ep() || self.is_capture(m)
1189 }
1190
1191 pub fn piece_at(&self, sq: Square) -> Option<Piece> {
1193 self.piece_array[sq]
1194 }
1195
1196 pub fn piece_at_mut(&mut self, sq: Square) -> &mut Option<Piece> {
1198 &mut self.piece_array[sq]
1199 }
1200
1201 pub fn make_move_simple(&mut self, m: Move) -> bool {
1202 self.make_move_base(m, &mut UpdateBuffer::default())
1203 }
1204
1205 #[allow(clippy::cognitive_complexity, clippy::too_many_lines)]
1206 pub fn make_move_base(&mut self, m: Move, update_buffer: &mut UpdateBuffer) -> bool {
1207 #[cfg(debug_assertions)]
1208 self.check_validity().unwrap();
1209
1210 let from = m.from();
1211 let mut to = m.to();
1212 let side = self.side;
1213 let Some(piece) = self.moved_piece(m) else {
1214 return false;
1215 };
1216 let captured = self.captured_piece(m);
1217
1218 let saved_state = Undo {
1219 castle_perm: self.castle_perm,
1220 ep_square: self.ep_sq,
1221 fifty_move_counter: self.fifty_move_counter,
1222 threats: self.threats,
1223 piece_layout: self.pieces,
1224 piece_array: self.piece_array,
1225 key: self.key,
1226 pawn_key: self.pawn_key,
1227 non_pawn_key: self.non_pawn_key,
1228 minor_key: self.minor_key,
1229 major_key: self.major_key,
1230 };
1231
1232 if !m.is_castle() {
1236 if m.is_promo() {
1237 update_buffer.clear_piece(from, piece);
1239 } else {
1240 update_buffer.move_piece(from, to, piece);
1241 }
1242 }
1243
1244 if m.is_ep() {
1245 let clear_at = if side == Colour::White {
1246 to.sub(8)
1247 } else {
1248 to.add(8)
1249 }
1250 .unwrap();
1251 let to_clear = Piece::new(side.flip(), PieceType::Pawn);
1252 self.pieces.clear_piece_at(clear_at, to_clear);
1253 update_buffer.clear_piece(clear_at, to_clear);
1254 } else if m.is_castle() {
1255 self.pieces.clear_piece_at(from, piece);
1256 let (rook_from, rook_to) = match to {
1257 _ if Some(to) == self.castle_perm.wk => {
1258 to = Square::G1;
1259 (self.castle_perm.wk.unwrap(), Square::F1)
1260 }
1261 _ if Some(to) == self.castle_perm.wq => {
1262 to = Square::C1;
1263 (self.castle_perm.wq.unwrap(), Square::D1)
1264 }
1265 _ if Some(to) == self.castle_perm.bk => {
1266 to = Square::G8;
1267 (self.castle_perm.bk.unwrap(), Square::F8)
1268 }
1269 _ if Some(to) == self.castle_perm.bq => {
1270 to = Square::C8;
1271 (self.castle_perm.bq.unwrap(), Square::D8)
1272 }
1273 _ => {
1274 panic!(
1275 "Invalid castle move, to: {}, castle_perm: {}",
1276 to,
1277 self.castle_perm.display(false)
1278 );
1279 }
1280 };
1281 if from != to {
1282 update_buffer.move_piece(from, to, piece);
1283 }
1284 if rook_from != rook_to {
1285 let rook = Piece::new(side, PieceType::Rook);
1286 self.pieces.move_piece(rook_from, rook_to, rook);
1287 update_buffer.move_piece(rook_from, rook_to, rook);
1288 }
1289 }
1290
1291 self.ep_sq = None;
1292
1293 self.fifty_move_counter += 1;
1294
1295 if let Some(captured) = captured {
1296 self.fifty_move_counter = 0;
1297 self.pieces.clear_piece_at(to, captured);
1298 update_buffer.clear_piece(to, captured);
1299 }
1300
1301 if piece.piece_type() == PieceType::Pawn {
1302 self.fifty_move_counter = 0;
1303 if self.is_double_pawn_push(m)
1304 && (m.to().as_set().west_one() | m.to().as_set().east_one())
1305 & self.pieces.all_pawns()
1306 & self.pieces.occupied_co(side.flip())
1307 != SquareSet::EMPTY
1308 {
1309 if side == Colour::White {
1310 self.ep_sq = from.add(8);
1311 debug_assert!(self.ep_sq.unwrap().rank() == Rank::Three);
1312 } else {
1313 self.ep_sq = from.sub(8);
1314 debug_assert!(self.ep_sq.unwrap().rank() == Rank::Six);
1315 }
1316 }
1317 }
1318
1319 if let Some(promo) = m.promotion_type() {
1320 let promo = Piece::new(side, promo);
1321 debug_assert!(promo.piece_type().legal_promo());
1322 self.pieces.clear_piece_at(from, piece);
1323 self.pieces.set_piece_at(to, promo);
1324 update_buffer.add_piece(to, promo);
1325 } else if m.is_castle() {
1326 self.pieces.set_piece_at(to, piece); } else {
1328 self.pieces.move_piece(from, to, piece);
1329 }
1330
1331 self.side = self.side.flip();
1332
1333 if self.sq_attacked(self.king_sq(self.side.flip()), self.side) {
1335 let Undo {
1339 ep_square,
1340 fifty_move_counter,
1341 piece_layout,
1342 ..
1343 } = saved_state;
1344
1345 self.side = self.side.flip();
1348 self.ep_sq = ep_square;
1356 self.fifty_move_counter = fifty_move_counter;
1357 self.pieces = piece_layout;
1359 return false;
1361 }
1362
1363 let mut key = self.key;
1364 let mut pawn_key = self.pawn_key;
1365 let mut non_pawn_key = self.non_pawn_key;
1366 let mut minor_key = self.minor_key;
1367 let mut major_key = self.major_key;
1368
1369 if let Some(ep_sq) = saved_state.ep_square {
1371 hash_ep(&mut key, ep_sq);
1372 }
1373
1374 hash_castling(&mut key, self.castle_perm);
1376
1377 let mut new_rights = self.castle_perm;
1379 if piece == Piece::WR {
1380 if Some(from) == self.castle_perm.wk {
1381 new_rights.wk = None;
1382 } else if Some(from) == self.castle_perm.wq {
1383 new_rights.wq = None;
1384 }
1385 } else if piece == Piece::BR {
1386 if Some(from) == self.castle_perm.bk {
1387 new_rights.bk = None;
1388 } else if Some(from) == self.castle_perm.bq {
1389 new_rights.bq = None;
1390 }
1391 } else if piece == Piece::WK {
1392 new_rights.wk = None;
1393 new_rights.wq = None;
1394 } else if piece == Piece::BK {
1395 new_rights.bk = None;
1396 new_rights.bq = None;
1397 }
1398 new_rights.remove(to);
1399 self.castle_perm = new_rights;
1400
1401 if let Some(ep_sq) = self.ep_sq {
1403 hash_ep(&mut key, ep_sq);
1404 }
1405 hash_side(&mut key);
1406 for &FeatureUpdate { sq, piece } in update_buffer.subs() {
1407 self.piece_array[sq] = None;
1408 hash_piece(&mut key, piece, sq);
1409 if piece.piece_type() == PieceType::Pawn {
1410 hash_piece(&mut pawn_key, piece, sq);
1411 } else {
1412 hash_piece(&mut non_pawn_key[piece.colour()], piece, sq);
1413 if piece.piece_type() == PieceType::King {
1414 hash_piece(&mut major_key, piece, sq);
1415 hash_piece(&mut minor_key, piece, sq);
1416 } else if matches!(piece.piece_type(), PieceType::Queen | PieceType::Rook) {
1417 hash_piece(&mut major_key, piece, sq);
1418 } else {
1419 hash_piece(&mut minor_key, piece, sq);
1420 }
1421 }
1422 }
1423 for &FeatureUpdate { sq, piece } in update_buffer.adds() {
1424 self.piece_array[sq] = Some(piece);
1425 hash_piece(&mut key, piece, sq);
1426 if piece.piece_type() == PieceType::Pawn {
1427 hash_piece(&mut pawn_key, piece, sq);
1428 } else {
1429 hash_piece(&mut non_pawn_key[piece.colour()], piece, sq);
1430 if piece.piece_type() == PieceType::King {
1431 hash_piece(&mut major_key, piece, sq);
1432 hash_piece(&mut minor_key, piece, sq);
1433 } else if matches!(piece.piece_type(), PieceType::Queen | PieceType::Rook) {
1434 hash_piece(&mut major_key, piece, sq);
1435 } else {
1436 hash_piece(&mut minor_key, piece, sq);
1437 }
1438 }
1439 }
1440 hash_castling(&mut key, self.castle_perm);
1442 self.key = key;
1443 self.pawn_key = pawn_key;
1444 self.non_pawn_key = non_pawn_key;
1445 self.minor_key = minor_key;
1446 self.major_key = major_key;
1447
1448 self.ply += 1;
1449 self.height += 1;
1450
1451 self.threats = self.generate_threats(self.side.flip());
1452
1453 self.history.push(saved_state);
1454
1455 #[cfg(debug_assertions)]
1456 self.check_validity().unwrap();
1457
1458 true
1459 }
1460
1461 pub fn unmake_move_base(&mut self) {
1462 let undo = self.history.last().expect("No move to unmake!");
1471
1472 let Undo {
1473 castle_perm,
1474 ep_square,
1475 fifty_move_counter,
1476 threats,
1477 piece_layout,
1478 piece_array,
1479 key,
1480 pawn_key,
1481 non_pawn_key,
1482 minor_key,
1483 major_key,
1484 ..
1485 } = undo;
1486
1487 self.height -= 1;
1488 self.ply -= 1;
1489 self.side = self.side.flip();
1490 self.key = *key;
1491 self.pawn_key = *pawn_key;
1492 self.non_pawn_key = *non_pawn_key;
1493 self.minor_key = *minor_key;
1494 self.major_key = *major_key;
1495 self.castle_perm = *castle_perm;
1496 self.ep_sq = *ep_square;
1497 self.fifty_move_counter = *fifty_move_counter;
1498 self.threats = *threats;
1499 self.pieces = *piece_layout;
1500 self.piece_array = *piece_array;
1501
1502 self.history.pop();
1503
1504 #[cfg(debug_assertions)]
1505 self.check_validity().unwrap();
1506 }
1507
1508 pub fn make_nullmove(&mut self) {
1509 #[cfg(debug_assertions)]
1510 self.check_validity().unwrap();
1511 debug_assert!(!self.in_check());
1512
1513 self.history.push(Undo {
1514 ep_square: self.ep_sq,
1515 threats: self.threats,
1516 key: self.key,
1517 ..Default::default()
1518 });
1519
1520 let mut key = self.key;
1521 if let Some(ep_sq) = self.ep_sq {
1522 hash_ep(&mut key, ep_sq);
1523 }
1524 hash_side(&mut key);
1525 self.key = key;
1526
1527 self.ep_sq = None;
1528 self.side = self.side.flip();
1529 self.ply += 1;
1530 self.height += 1;
1531
1532 self.threats = self.generate_threats(self.side.flip());
1533
1534 #[cfg(debug_assertions)]
1535 self.check_validity().unwrap();
1536 }
1537
1538 pub fn unmake_nullmove(&mut self) {
1539 #[cfg(debug_assertions)]
1540 self.check_validity().unwrap();
1541
1542 self.height -= 1;
1543 self.ply -= 1;
1544 self.side = self.side.flip();
1545
1546 let Undo {
1547 ep_square,
1548 threats,
1549 key,
1550 ..
1551 } = self.history.last().expect("No move to unmake!");
1552
1553 self.ep_sq = *ep_square;
1554 self.threats = *threats;
1555 self.key = *key;
1556
1557 self.history.pop();
1558
1559 #[cfg(debug_assertions)]
1560 self.check_validity().unwrap();
1561 }
1562
1563 pub fn last_move_was_nullmove(&self) -> bool {
1564 if let Some(Undo { piece_layout, .. }) = self.history.last() {
1565 piece_layout.all_kings().is_empty()
1566 } else {
1567 false
1568 }
1569 }
1570
1571 pub fn key_after(&self, m: Move) -> u64 {
1574 let src = m.from();
1575 let tgt = m.to();
1576 let piece = self.moved_piece(m).unwrap();
1577 let captured = self.piece_at(tgt);
1578
1579 let mut new_key = self.key;
1580 hash_side(&mut new_key);
1581 hash_piece(&mut new_key, piece, src);
1582 hash_piece(&mut new_key, piece, tgt);
1583
1584 if let Some(captured) = captured {
1585 hash_piece(&mut new_key, captured, tgt);
1586 }
1587
1588 new_key
1589 }
1590
1591 pub fn key_after_null_move(&self) -> u64 {
1592 let mut new_key = self.key;
1593 hash_side(&mut new_key);
1594 new_key
1595 }
1596
1597 pub fn parse_uci(&self, uci: &str) -> anyhow::Result<Move> {
1599 let san_bytes = uci.as_bytes();
1600 if !(4..=5).contains(&san_bytes.len()) {
1601 bail!("invalid length: {}", san_bytes.len());
1602 }
1603 if !(b'a'..=b'h').contains(&san_bytes[0]) {
1604 bail!("invalid from_square file: {}", san_bytes[0] as char);
1605 }
1606 if !(b'1'..=b'8').contains(&san_bytes[1]) {
1607 bail!("invalid from_square rank: {}", san_bytes[1] as char);
1608 }
1609 if !(b'a'..=b'h').contains(&san_bytes[2]) {
1610 bail!("invalid to_square file: {}", san_bytes[2] as char);
1611 }
1612 if !(b'1'..=b'8').contains(&san_bytes[3]) {
1613 bail!("invalid to_square rank: {}", san_bytes[3] as char);
1614 }
1615 if san_bytes.len() == 5 && ![b'n', b'b', b'r', b'q', b'k'].contains(&san_bytes[4]) {
1616 bail!("invalid promotion piece: {}", san_bytes[4] as char);
1617 }
1618
1619 let from = Square::from_rank_file(
1620 Rank::from_index(san_bytes[1] - b'1').context("unknown")?,
1621 File::from_index(san_bytes[0] - b'a').context("unknown")?,
1622 );
1623 let to = Square::from_rank_file(
1624 Rank::from_index(san_bytes[3] - b'1').context("unknown")?,
1625 File::from_index(san_bytes[2] - b'a').context("unknown")?,
1626 );
1627
1628 let mut list = MoveList::new();
1629 self.generate_moves(&mut list);
1630
1631 let frc_cleanup = !self.chess960;
1632
1633 list.iter_moves()
1634 .copied()
1635 .find(|&m| {
1636 let m_to = if frc_cleanup && m.is_castle() {
1637 match m.to() {
1640 Square::A1 => Square::C1,
1641 Square::H1 => Square::G1,
1642 Square::A8 => Square::C8,
1643 Square::H8 => Square::G8,
1644 _ => m.to(),
1645 }
1646 } else {
1647 m.to()
1648 };
1649 m.from() == from
1650 && m_to == to
1651 && (san_bytes.len() == 4
1652 || m.promotion_type().and_then(PieceType::promo_char).unwrap()
1653 == san_bytes[4] as char)
1654 })
1655 .with_context(|| format!("illegal move: {}", uci))
1656 }
1657
1658 pub fn san(&mut self, m: Move) -> Option<String> {
1659 let check_char = match self.gives(m) {
1660 CheckState::None => "",
1661 CheckState::Check => "+",
1662 CheckState::Checkmate => "#",
1663 };
1664 if m.is_castle() {
1665 match () {
1666 () if m.to() > m.from() => return Some(format!("O-O{check_char}")),
1667 () if m.to() < m.from() => return Some(format!("O-O-O{check_char}")),
1668 () => unreachable!(),
1669 }
1670 }
1671 let to_sq = m.to();
1672 let moved_piece = self.piece_at(m.from())?;
1673 let is_capture = self.is_capture(m)
1674 || (moved_piece.piece_type() == PieceType::Pawn && Some(to_sq) == self.ep_sq);
1675 let piece_prefix = match moved_piece.piece_type() {
1676 PieceType::Pawn if !is_capture => "",
1677 PieceType::Pawn => &"abcdefgh"[m.from().file() as usize..=m.from().file() as usize],
1678 PieceType::Knight => "N",
1679 PieceType::Bishop => "B",
1680 PieceType::Rook => "R",
1681 PieceType::Queen => "Q",
1682 PieceType::King => "K",
1683 };
1684 let possible_ambiguous_attackers = if moved_piece.piece_type() == PieceType::Pawn {
1685 SquareSet::EMPTY
1686 } else {
1687 movegen::attacks_by_type(moved_piece.piece_type(), to_sq, self.pieces.occupied())
1688 & self.pieces.piece_bb(moved_piece)
1689 };
1690 let needs_disambiguation =
1691 possible_ambiguous_attackers.count() > 1 && moved_piece.piece_type() != PieceType::Pawn;
1692 let from_file = SquareSet::FILES[m.from().file()];
1693 let from_rank = SquareSet::RANKS[m.from().rank()];
1694 let can_be_disambiguated_by_file = (possible_ambiguous_attackers & from_file).count() == 1;
1695 let can_be_disambiguated_by_rank = (possible_ambiguous_attackers & from_rank).count() == 1;
1696 let needs_both = !can_be_disambiguated_by_file && !can_be_disambiguated_by_rank;
1697 let must_be_disambiguated_by_file = needs_both || can_be_disambiguated_by_file;
1698 let must_be_disambiguated_by_rank =
1699 needs_both || (can_be_disambiguated_by_rank && !can_be_disambiguated_by_file);
1700 let disambiguator1 = if needs_disambiguation && must_be_disambiguated_by_file {
1701 &"abcdefgh"[m.from().file() as usize..=m.from().file() as usize]
1702 } else {
1703 ""
1704 };
1705 let disambiguator2 = if needs_disambiguation && must_be_disambiguated_by_rank {
1706 &"12345678"[m.from().rank() as usize..=m.from().rank() as usize]
1707 } else {
1708 ""
1709 };
1710 let capture_sigil = if is_capture { "x" } else { "" };
1711 let promo_str = match m.promotion_type() {
1712 Some(PieceType::Knight) => "=N",
1713 Some(PieceType::Bishop) => "=B",
1714 Some(PieceType::Rook) => "=R",
1715 Some(PieceType::Queen) => "=Q",
1716 None => "",
1717 _ => unreachable!(),
1718 };
1719 let san = format!(
1720 "{piece_prefix}{disambiguator1}{disambiguator2}{capture_sigil}{to_sq}{promo_str}{check_char}"
1721 );
1722 Some(san)
1723 }
1724
1725 pub fn gives(&mut self, m: Move) -> CheckState {
1726 if !self.make_move_simple(m) {
1727 return CheckState::None;
1728 }
1729 let gives_check = self.in_check();
1730 if gives_check {
1731 let mut ml = MoveList::new();
1732 self.generate_moves(&mut ml);
1733 for &m in ml.iter_moves() {
1734 if !self.make_move_simple(m) {
1735 continue;
1736 }
1737 self.unmake_move_base();
1739 self.unmake_move_base();
1740 return CheckState::Check;
1741 }
1742 self.unmake_move_base();
1745 return CheckState::Checkmate;
1746 }
1747 self.unmake_move_base();
1748 CheckState::None
1749 }
1750
1751 pub fn is_repetition(&self) -> bool {
1753 let mut counter = 0;
1754 let moves_since_zeroing = self.fifty_move_counter() as usize;
1756 for (dist_back, u) in self
1758 .history
1759 .iter()
1760 .rev()
1761 .enumerate()
1762 .take(moves_since_zeroing)
1763 .skip(3)
1764 .step_by(2)
1765 {
1766 if u.key == self.key {
1767 if dist_back < self.height {
1769 return true;
1770 }
1771 counter += 1;
1773 if counter >= 2 {
1774 return true;
1775 }
1776 }
1777 }
1778 false
1779 }
1780
1781 pub fn is_draw(&self) -> bool {
1783 (self.fifty_move_counter >= 100 || self.is_repetition()) && self.height != 0
1784 }
1785
1786 pub fn legal_moves(&mut self) -> Vec<Move> {
1787 let mut move_list = MoveList::new();
1788 self.generate_moves(&mut move_list);
1789 let mut legal_moves = Vec::new();
1790 for &m in move_list.iter_moves() {
1791 if self.make_move_simple(m) {
1792 self.unmake_move_base();
1793 legal_moves.push(m);
1794 }
1795 }
1796 legal_moves
1797 }
1798
1799 pub const fn fifty_move_counter(&self) -> u8 {
1800 self.fifty_move_counter
1801 }
1802
1803 pub fn has_insufficient_material<C: Col>(&self) -> bool {
1804 if (self.pieces.pawns::<C>() | self.pieces.rooks::<C>() | self.pieces.queens::<C>())
1805 .non_empty()
1806 {
1807 return false;
1808 }
1809
1810 if self.pieces.knights::<C>().non_empty() {
1811 let kings = self.pieces.all_kings();
1815 let queens = self.pieces.all_queens();
1816 return self.pieces.our_pieces::<C>().count() <= 2
1817 && (self.pieces.their_pieces::<C>() & !kings & !queens).is_empty();
1818 }
1819
1820 if self.pieces.bishops::<C>().non_empty() {
1821 let bishops = self.pieces.all_bishops();
1822 let same_color = (bishops & SquareSet::DARK_SQUARES).is_empty()
1823 || (bishops & SquareSet::LIGHT_SQUARES).is_empty();
1824 let pawns = self.pieces.all_pawns();
1825 let knights = self.pieces.all_knights();
1826 return same_color && pawns.is_empty() && knights.is_empty();
1827 }
1828
1829 true
1830 }
1831
1832 pub const fn full_move_number(&self) -> usize {
1833 self.ply / 2 + 1
1834 }
1835
1836 pub fn make_random_move(&mut self, rng: &mut ThreadRng) -> Option<Move> {
1837 let mut ml = MoveList::new();
1838 self.generate_moves(&mut ml);
1839 let self::movegen::MoveListEntry { mov, .. } = ml.choose(rng)?;
1840 self.make_move_simple(*mov);
1841 Some(*mov)
1842 }
1843
1844 pub fn is_insufficient_material(&self) -> bool {
1845 self.has_insufficient_material::<White>() && self.has_insufficient_material::<Black>()
1846 }
1847
1848 pub fn outcome(&mut self) -> GameOutcome {
1849 if self.fifty_move_counter >= 100 {
1850 return GameOutcome::Draw(DrawType::FiftyMoves);
1851 }
1852 let mut reps = 1;
1853 for undo in self.history.iter().rev().skip(1).step_by(2) {
1854 if undo.key == self.key {
1855 reps += 1;
1856 if reps == 3 {
1857 return GameOutcome::Draw(DrawType::Repetition);
1858 }
1859 }
1860 if undo.fifty_move_counter == 0 {
1862 break;
1863 }
1864 }
1865 if self.is_insufficient_material() {
1866 return GameOutcome::Draw(DrawType::InsufficientMaterial);
1867 }
1868 let mut move_list = MoveList::new();
1869 self.generate_moves(&mut move_list);
1870 let mut legal_moves = false;
1871 for &m in move_list.iter_moves() {
1872 if self.make_move_simple(m) {
1873 self.unmake_move_base();
1874 legal_moves = true;
1875 break;
1876 }
1877 }
1878 if legal_moves {
1879 GameOutcome::Ongoing
1880 } else if self.in_check() {
1881 match self.side {
1882 Colour::White => GameOutcome::BlackWin(WinType::Mate),
1883 Colour::Black => GameOutcome::WhiteWin(WinType::Mate),
1884 }
1885 } else {
1886 GameOutcome::Draw(DrawType::Stalemate)
1887 }
1888 }
1889
1890 #[cfg(debug_assertions)]
1891 pub fn assert_mated(&mut self) {
1892 assert!(self.in_check());
1893 let mut move_list = MoveList::new();
1894 self.generate_moves(&mut move_list);
1895 for &mv in move_list.iter_moves() {
1896 assert!(!self.make_move_simple(mv));
1897 }
1898 }
1899
1900 #[allow(clippy::identity_op)]
1901 pub fn material_count(&self) -> u32 {
1902 let pawn_material = 1 * self.pieces.all_pawns().count();
1903 let knight_material = 3 * self.pieces.all_knights().count();
1904 let bishop_material = 3 * self.pieces.all_bishops().count();
1905 let rook_material = 5 * self.pieces.all_rooks().count();
1906 let queen_material = 9 * self.pieces.all_queens().count();
1907
1908 pawn_material + knight_material + bishop_material + rook_material + queen_material
1909 }
1910}
1911
1912#[allow(dead_code)]
1913#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
1914pub enum GameOutcome {
1915 WhiteWin(WinType),
1916 BlackWin(WinType),
1917 Draw(DrawType),
1918 Ongoing,
1919}
1920
1921#[allow(dead_code)]
1922#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
1923pub enum WinType {
1924 Mate,
1925 TB,
1926 Adjudication,
1927}
1928
1929#[allow(dead_code)]
1930#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
1931pub enum DrawType {
1932 TB,
1933 FiftyMoves,
1934 Repetition,
1935 Stalemate,
1936 InsufficientMaterial,
1937 Adjudication,
1938}
1939
1940impl GameOutcome {
1941 pub const fn as_packed_u8(self) -> u8 {
1942 match self {
1944 Self::WhiteWin(_) => 2,
1945 Self::BlackWin(_) => 0,
1946 Self::Draw(_) => 1,
1947 Self::Ongoing => panic!("Game is not over!"),
1948 }
1949 }
1950}
1951
1952impl Default for Board {
1953 fn default() -> Self {
1954 let mut out = Self::new();
1955 out.set_startpos(true);
1956 out
1957 }
1958}
1959
1960impl Display for Board {
1961 fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), std::fmt::Error> {
1962 let mut counter = 0;
1963 for rank in Rank::all().rev() {
1964 for file in File::all() {
1965 let sq = Square::from_rank_file(rank, file);
1966 let piece = self.piece_at(sq);
1967 if let Some(piece) = piece {
1968 if counter != 0 {
1969 write!(f, "{counter}")?;
1970 }
1971 counter = 0;
1972 write!(f, "{piece}")?;
1973 } else {
1974 counter += 1;
1975 }
1976 }
1977 if counter != 0 {
1978 write!(f, "{counter}")?;
1979 }
1980 counter = 0;
1981 if rank != Rank::One {
1982 write!(f, "/")?;
1983 }
1984 }
1985
1986 if self.side == Colour::White {
1987 write!(f, " w")?;
1988 } else {
1989 write!(f, " b")?;
1990 }
1991 write!(f, " ")?;
1992 if self.castle_perm == CastlingRights::NONE {
1993 write!(f, "-")?;
1994 } else {
1995 for (_, ch) in [
1996 self.castle_perm.wk,
1997 self.castle_perm.wq,
1998 self.castle_perm.bk,
1999 self.castle_perm.bq,
2000 ]
2001 .into_iter()
2002 .zip("KQkq".chars())
2003 .filter(|(m, _)| m.is_some())
2004 {
2005 write!(f, "{ch}")?;
2006 }
2007 }
2008 if let Some(ep_sq) = self.ep_sq {
2009 write!(f, " {ep_sq}")?;
2010 } else {
2011 write!(f, " -")?;
2012 }
2013 write!(f, " {}", self.fifty_move_counter)?;
2014 write!(f, " {}", self.ply / 2 + 1)?;
2015
2016 Ok(())
2017 }
2018}
2019
2020impl std::fmt::UpperHex for Board {
2021 fn fmt(&self, f: &mut Formatter) -> Result<(), std::fmt::Error> {
2022 for rank in Rank::all().rev() {
2023 write!(f, "{} ", rank as u8 + 1)?;
2024 for file in File::all() {
2025 let sq = Square::from_rank_file(rank, file);
2026 if let Some(piece) = self.piece_at(sq) {
2027 write!(f, "{piece} ")?;
2028 } else {
2029 write!(f, ". ")?;
2030 }
2031 }
2032 writeln!(f)?;
2033 }
2034
2035 writeln!(f, " a b c d e f g h")?;
2036 writeln!(f, "FEN: {self}")?;
2037
2038 Ok(())
2039 }
2040}
2041
2042mod tests {
2043 #[test]
2044 fn read_fen_validity() {
2045 use super::Board;
2046
2047 let mut board_1 = Board::new();
2048 board_1
2049 .set_from_fen(Board::STARTING_FEN, true)
2050 .expect("setfen failed.");
2051 board_1.check_validity().unwrap();
2052
2053 let board_2 = Board::from_fen(Board::STARTING_FEN, true).expect("setfen failed.");
2054 board_2.check_validity().unwrap();
2055
2056 assert_eq!(board_1, board_2);
2057 }
2058
2059 #[test]
2060 fn game_end_states() {
2061 use super::Board;
2062 use super::{DrawType, GameOutcome};
2063 use crate::{chess::chessmove::Move, chess::types::Square};
2064
2065 let mut fiftymove_draw = Board::from_fen(
2066 "rnbqkb1r/pppppppp/5n2/8/3N4/8/PPPPPPPP/RNBQKB1R b KQkq - 100 2",
2067 false,
2068 )
2069 .unwrap();
2070 assert_eq!(
2071 fiftymove_draw.outcome(),
2072 GameOutcome::Draw(DrawType::FiftyMoves)
2073 );
2074 let mut draw_repetition = Board::default();
2075 assert_eq!(draw_repetition.outcome(), GameOutcome::Ongoing);
2076 draw_repetition.make_move_simple(Move::new(Square::G1, Square::F3));
2077 draw_repetition.make_move_simple(Move::new(Square::B8, Square::C6));
2078 assert_eq!(draw_repetition.outcome(), GameOutcome::Ongoing);
2079 draw_repetition.make_move_simple(Move::new(Square::F3, Square::G1));
2080 draw_repetition.make_move_simple(Move::new(Square::C6, Square::B8));
2081 assert_eq!(draw_repetition.outcome(), GameOutcome::Ongoing);
2082 draw_repetition.make_move_simple(Move::new(Square::G1, Square::F3));
2083 draw_repetition.make_move_simple(Move::new(Square::B8, Square::C6));
2084 assert_eq!(draw_repetition.outcome(), GameOutcome::Ongoing);
2085 draw_repetition.make_move_simple(Move::new(Square::F3, Square::G1));
2086 draw_repetition.make_move_simple(Move::new(Square::C6, Square::B8));
2087 assert_eq!(
2088 draw_repetition.outcome(),
2089 GameOutcome::Draw(DrawType::Repetition)
2090 );
2091 let mut stalemate = Board::from_fen("7k/8/6Q1/8/8/8/8/K7 b - - 0 1", true).unwrap();
2092 assert_eq!(stalemate.outcome(), GameOutcome::Draw(DrawType::Stalemate));
2093 let mut insufficient_material_bare_kings =
2094 Board::from_fen("8/8/5k2/8/8/2K5/8/8 b - - 0 1", true).unwrap();
2095 assert_eq!(
2096 insufficient_material_bare_kings.outcome(),
2097 GameOutcome::Draw(DrawType::InsufficientMaterial)
2098 );
2099 let mut insufficient_material_knights =
2100 Board::from_fen("8/8/5k2/8/2N5/2K2N2/8/8 b - - 0 1", true).unwrap();
2101 assert_eq!(
2102 insufficient_material_knights.outcome(),
2103 GameOutcome::Ongoing
2104 );
2105 }
2107
2108 #[test]
2109 fn fen_round_trip() {
2110 use crate::chess::board::Board;
2111 use std::{
2112 fs::File,
2113 io::{BufRead, BufReader},
2114 };
2115
2116 let fens = BufReader::new(File::open("epds/perftsuite.epd").unwrap())
2117 .lines()
2118 .map(|l| l.unwrap().split_once(';').unwrap().0.trim().to_owned())
2119 .collect::<Vec<_>>();
2120 let mut board = Board::new();
2121 for fen in fens {
2122 board.set_from_fen(&fen, true).expect("setfen failed.");
2123 let fen_2 = board.to_string();
2124 assert_eq!(fen, fen_2);
2125 }
2126 }
2127
2128 #[test]
2129 fn scharnagl_backrank_works() {
2130 use super::Board;
2131 use crate::chess::piece::PieceType;
2132 let normal_chess_arrangement = Board::get_scharnagl_backrank(518);
2133 assert_eq!(
2134 normal_chess_arrangement,
2135 [
2136 PieceType::Rook,
2137 PieceType::Knight,
2138 PieceType::Bishop,
2139 PieceType::Queen,
2140 PieceType::King,
2141 PieceType::Bishop,
2142 PieceType::Knight,
2143 PieceType::Rook
2144 ]
2145 );
2146 }
2147
2148 #[test]
2149 fn scharnagl_full_works() {
2150 #![allow(clippy::similar_names)]
2151 use super::Board;
2152 let normal = Board::from_fen(Board::STARTING_FEN, true).unwrap();
2153 let frc = Board::from_frc_idx(518);
2154 let dfrc = Board::from_dfrc_idx(518 * 960 + 518);
2155 assert_eq!(normal, frc);
2156 assert_eq!(normal, dfrc);
2157 }
2158
2159 #[test]
2160 fn castling_pseudolegality() {
2161 use super::Board;
2162 use crate::chess::chessmove::{Move, MoveFlags};
2163 use crate::chess::types::Square;
2164 let board = Board::from_fen(
2165 "1r2k2r/2pb1pp1/2pp4/p1n5/2P4p/PP2P2P/1qB2PP1/R2QKN1R w KQk - 0 20",
2166 false,
2167 )
2168 .unwrap();
2169 let kingside_castle = Move::new_with_flags(Square::E1, Square::H1, MoveFlags::Castle);
2170 assert!(!board.is_pseudo_legal(kingside_castle));
2171 }
2172
2173 #[test]
2174 fn threat_generation_white() {
2175 use super::Board;
2176 use crate::chess::squareset::SquareSet;
2177
2178 let board = Board::from_fen("3k4/8/8/5N2/8/1P6/8/K1Q1RB2 b - - 0 1", true).unwrap();
2179 assert_eq!(
2180 board.threats.all,
2181 SquareSet::from_inner(0x1454_9d56_bddd_5f3f)
2182 );
2183 }
2184
2185 #[test]
2186 fn threat_generation_black() {
2187 use super::Board;
2188 use crate::chess::squareset::SquareSet;
2189
2190 let board = Board::from_fen("2br1q1k/8/6p1/8/2n5/8/8/4K3 w - - 0 1", true).unwrap();
2191 assert_eq!(
2192 board.threats.all,
2193 SquareSet::from_inner(0xfcfa_bbbd_6ab9_2a28)
2194 );
2195 }
2196
2197 #[test]
2198 fn key_after_works_for_simple_moves() {
2199 use super::Board;
2200 use crate::chess::chessmove::Move;
2201 use crate::chess::types::Square;
2202 let mut board = Board::default();
2203 let mv = Move::new(Square::E2, Square::E3);
2204 let key = board.key_after(mv);
2205 board.make_move_simple(mv);
2206 assert_eq!(board.key, key);
2207 }
2208
2209 #[test]
2210 fn key_after_works_for_captures() {
2211 use super::Board;
2212 use crate::chess::chessmove::Move;
2213 use crate::chess::types::Square;
2214 let mut board = Board::from_fen(
2215 "r1bqkb1r/ppp2ppp/2n5/3np1N1/2B5/8/PPPP1PPP/RNBQK2R w KQkq - 0 6",
2216 false,
2217 )
2218 .unwrap();
2219 let mv = Move::new(Square::G5, Square::F7);
2221 let key = board.key_after(mv);
2222 board.make_move_simple(mv);
2223 assert_eq!(board.key, key);
2224 }
2225
2226 #[test]
2227 fn key_after_works_for_nullmove() {
2228 use super::Board;
2229 let mut board = Board::default();
2230 let key = board.key_after_null_move();
2231 board.make_nullmove();
2232 assert_eq!(board.key, key);
2233 }
2234
2235 #[test]
2236 fn ep_square_edge_case() {
2237 use super::Board;
2238 use crate::chess::chessmove::Move;
2239 use crate::chess::piece::Piece;
2240 use crate::chess::types::Square;
2241 use crate::makemove::{hash_ep, hash_piece, hash_side};
2242 let mut not_ep_capturable = Board::from_fen(
2243 "rnbqkbnr/ppppp1pp/8/5p2/4P3/5N2/PPPP1PPP/RNBQKB1R b KQkq - 1 2",
2244 false,
2245 )
2246 .unwrap();
2247 let mut ep_capturable = Board::from_fen(
2248 "rnbqkbnr/ppppp1pp/8/4Pp2/8/8/PPPP1PPP/RNBQKBNR b KQkq - 0 2",
2249 false,
2250 )
2251 .unwrap();
2252 let d5 = Move::new(Square::D7, Square::D5);
2253 let mut not_ep_capturable_key = not_ep_capturable.key;
2254 let mut ep_capturable_key = ep_capturable.key;
2255 hash_side(&mut not_ep_capturable_key);
2256 hash_side(&mut ep_capturable_key);
2257 hash_piece(&mut not_ep_capturable_key, Piece::BP, Square::D7);
2258 hash_piece(&mut ep_capturable_key, Piece::BP, Square::D7);
2259 hash_piece(&mut not_ep_capturable_key, Piece::BP, Square::D5);
2260 hash_piece(&mut ep_capturable_key, Piece::BP, Square::D5);
2261
2262 hash_ep(&mut ep_capturable_key, Square::D6);
2263
2264 assert!(not_ep_capturable.make_move_simple(d5));
2265 assert!(ep_capturable.make_move_simple(d5));
2266
2267 assert_eq!(not_ep_capturable.ep_sq, None);
2268 assert_eq!(ep_capturable.ep_sq, Some(Square::D6));
2269
2270 assert_eq!(not_ep_capturable.key, not_ep_capturable_key);
2271 assert_eq!(ep_capturable.key, ep_capturable_key);
2272 }
2273
2274 #[test]
2275 fn other_ep_edge_case() {
2276 use super::Board;
2277 use crate::chess::chessmove::Move;
2278 use crate::chess::types::Square;
2279 let mut board = Board::from_fen(
2280 "rnbqkbnr/1ppppppp/p7/P7/8/8/1PPPPPPP/RNBQKBNR b KQkq - 0 2",
2281 false,
2282 )
2283 .unwrap();
2284 assert!(board.make_move_simple(Move::new(Square::B7, Square::B5)));
2285 assert_eq!(board.ep_sq, Some(Square::B6));
2286 }
2287}