1use crate::attacks;
2use crate::bitboard::*;
3use crate::types::*;
4use std::error::Error;
5use std::fmt;
6
7#[derive(Debug)]
9pub enum FenError {
10 TooShort {
12 parts: usize,
14 },
15 WrongRankCount {
17 expected: u8,
19 got: usize,
21 },
22 InvalidSideToMove(String),
24 InvalidCastling(String),
26 InvalidEpSquare(String),
28 ParseInt(String),
30}
31
32impl fmt::Display for FenError {
33 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
34 match self {
35 FenError::TooShort { parts } => {
36 write!(f, "FEN too short: expected at least 4 parts, got {parts}")
37 }
38 FenError::WrongRankCount { expected, got } => {
39 write!(f, "Expected {expected} ranks in FEN, got {got}")
40 }
41 FenError::InvalidSideToMove(v) => write!(f, "Invalid side to move: {v}"),
42 FenError::InvalidCastling(v) => write!(f, "Invalid castling rights: {v}"),
43 FenError::InvalidEpSquare(v) => write!(f, "Invalid en-passant square: {v}"),
44 FenError::ParseInt(v) => write!(f, "Failed to parse integer: {v}"),
45 }
46 }
47}
48
49impl Error for FenError {}
50
51fn char_to_piece(c: char) -> Option<Piece> {
52 match c {
53 'P' => Some(W_PAWN),
54 'N' => Some(W_KNIGHT),
55 'B' => Some(W_BISHOP),
56 'R' => Some(W_ROOK),
57 'Q' => Some(W_QUEEN),
58 'C' => Some(W_COMMONER),
59 'K' => Some(W_COMMONER),
60 'p' => Some(B_PAWN),
61 'n' => Some(B_KNIGHT),
62 'b' => Some(B_BISHOP),
63 'r' => Some(B_ROOK),
64 'q' => Some(B_QUEEN),
65 'c' => Some(B_COMMONER),
66 'k' => Some(B_COMMONER),
67 _ => None,
68 }
69}
70
71#[derive(Debug, Clone, Copy)]
76pub struct StateInfo {
77 pub checkers: Bitboard,
79 pub pinned: Bitboard,
80 pub commoners_count: u32,
81 pub them_commoners_count: u32,
82
83 pub castling_rights: u8,
85 pub ep_square: Option<Square>,
86 pub rule50: u8,
87 pub captured_count: u8,
88 pub captured: [(Square, Piece); 9],
89 pub cap_sq: Option<Square>,
90 pub cap_piece: Piece,
91}
92
93impl Default for StateInfo {
94 fn default() -> Self {
95 Self::new()
96 }
97}
98
99impl StateInfo {
100 pub fn new() -> Self {
102 StateInfo {
103 checkers: Bitboard::EMPTY,
104 pinned: Bitboard::EMPTY,
105 commoners_count: 0,
106 them_commoners_count: 0,
107 castling_rights: 0,
108 ep_square: None,
109 rule50: 0,
110 captured_count: 0,
111 captured: [(Square::NONE, NO_PIECE); 9],
112 cap_sq: None,
113 cap_piece: NO_PIECE,
114 }
115 }
116}
117
118#[derive(Debug, Clone)]
124pub struct Board {
125 squares: [Piece; 64],
126 by_color: [Bitboard; 2],
127 by_type: [Bitboard; 6],
128 side_to_move: Color,
129 castling_rights: u8,
130 ep_square: Option<Square>,
131 rule50: u8,
132 game_ply: u16,
133}
134
135pub(crate) const WK_CASTLE: u8 = 1;
136pub(crate) const WQ_CASTLE: u8 = 2;
137pub(crate) const BK_CASTLE: u8 = 4;
138pub(crate) const BQ_CASTLE: u8 = 8;
139
140impl Default for Board {
141 fn default() -> Self {
142 Self::new()
143 }
144}
145
146impl Board {
147 pub fn new() -> Self {
149 let fen = "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1";
150 Board::from_fen(fen).expect("Failed to create starting position")
151 }
152
153 pub fn from_fen(fen: &str) -> Result<Self, FenError> {
159 let parts: Vec<&str> = fen.split_whitespace().collect();
160 if parts.len() < 4 {
161 return Err(FenError::TooShort {
162 parts: parts.len(),
163 });
164 }
165
166 let mut squares = [NO_PIECE; 64];
167 let mut by_color = [Bitboard::EMPTY; 2];
168 let mut by_type = [Bitboard::EMPTY; 6];
169
170 let rows: Vec<&str> = parts[0].split('/').collect();
173 if rows.len() != 8 {
174 return Err(FenError::WrongRankCount {
175 expected: 8,
176 got: rows.len(),
177 });
178 }
179 for (ri, row) in rows.iter().enumerate() {
180 let rank_idx = 7 - ri; let mut col = 0usize;
182 for c in row.chars() {
183 if c.is_ascii_digit() {
184 col += c.to_digit(10).unwrap() as usize;
185 } else if let Some(piece) = char_to_piece(c) {
186 let sq_idx = rank_idx * 8 + col;
187 if sq_idx < 64 {
188 squares[sq_idx] = piece;
189 let sq = Square::from_u8(sq_idx as u8);
190 let bb = Bitboard::square_bb(sq);
191 by_color[piece.color() as usize] = by_color[piece.color() as usize] | bb;
192 by_type[piece.type_of() as usize] = by_type[piece.type_of() as usize] | bb;
193 }
194 col += 1;
195 }
196 }
197 }
198
199 let side_to_move = match parts[1] {
200 "w" => Color::White,
201 "b" => Color::Black,
202 _ => {
203 return Err(FenError::InvalidSideToMove(parts[1].to_string()));
204 }
205 };
206
207 let mut castling_rights = 0u8;
208 for c in parts[2].chars() {
209 match c {
210 'K' => castling_rights |= WK_CASTLE,
211 'Q' => castling_rights |= WQ_CASTLE,
212 'k' => castling_rights |= BK_CASTLE,
213 'q' => castling_rights |= BQ_CASTLE,
214 '-' => break,
215 _ => {
216 return Err(FenError::InvalidCastling(c.to_string()));
217 }
218 }
219 }
220
221 let ep_square = if parts[3] == "-" {
222 None
223 } else {
224 let sq = crate::types::parse_sq(parts[3]);
225 if sq == Square::A1 && parts[3] != "a1" {
226 return Err(FenError::InvalidEpSquare(parts[3].to_string()));
227 }
228 Some(sq)
229 };
230
231 let rule50 = if parts.len() > 4 {
232 parts[4].parse::<u8>().map_err(|e| FenError::ParseInt(e.to_string()))?
233 } else {
234 0
235 };
236
237 let game_ply = if parts.len() > 5 {
238 parts[5].parse::<u16>().map_err(|e| FenError::ParseInt(e.to_string()))?
239 } else {
240 1
241 };
242
243 Ok(Board {
244 squares,
245 by_color,
246 by_type,
247 side_to_move,
248 castling_rights,
249 ep_square,
250 rule50,
251 game_ply,
252 })
253 }
254
255 #[must_use]
257 pub fn fen(&self) -> String {
258 let mut fen = String::new();
259 for rank in (0..8).rev() {
260 let mut empty = 0;
261 for file in 0..8 {
262 let idx = rank * 8 + file;
263 if self.squares[idx] == NO_PIECE {
264 empty += 1;
265 } else {
266 if empty > 0 {
267 fen.push_str(&empty.to_string());
268 empty = 0;
269 }
270 fen.push(self.squares[idx].ascii_char());
271 }
272 }
273 if empty > 0 {
274 fen.push_str(&empty.to_string());
275 }
276 if rank > 0 {
277 fen.push('/');
278 }
279 }
280
281 fen.push(' ');
282 fen.push(match self.side_to_move {
283 Color::White => 'w',
284 Color::Black => 'b',
285 });
286
287 fen.push(' ');
288 let mut has_castling = false;
289 if self.castling_rights & WK_CASTLE != 0 {
290 fen.push('K');
291 has_castling = true;
292 }
293 if self.castling_rights & WQ_CASTLE != 0 {
294 fen.push('Q');
295 has_castling = true;
296 }
297 if self.castling_rights & BK_CASTLE != 0 {
298 fen.push('k');
299 has_castling = true;
300 }
301 if self.castling_rights & BQ_CASTLE != 0 {
302 fen.push('q');
303 has_castling = true;
304 }
305 if !has_castling {
306 fen.push('-');
307 }
308
309 fen.push(' ');
310 match self.ep_square {
311 Some(sq) => fen.push_str(&crate::types::sq_str(sq)),
312 None => fen.push('-'),
313 }
314
315 fen.push(' ');
316 fen.push_str(&self.rule50.to_string());
317 fen.push(' ');
318 fen.push_str(&self.game_ply.to_string());
319
320 fen
321 }
322
323 #[inline(always)]
325 #[must_use]
326 pub fn piece_on(&self, sq: Square) -> Piece {
327 self.squares[sq as usize]
328 }
329
330 #[must_use]
332 pub fn empty(&self, sq: Square) -> bool {
333 self.squares[sq as usize] == NO_PIECE
334 }
335
336 #[inline(always)]
338 #[must_use]
339 pub fn occupied(&self) -> Bitboard {
340 self.by_color[0] | self.by_color[1]
341 }
342
343 #[inline(always)]
345 pub fn pieces_color(&self, c: Color) -> Bitboard {
346 self.by_color[c as usize]
347 }
348
349 #[inline(always)]
351 pub fn pieces_pt(&self, pt: PieceType) -> Bitboard {
352 self.by_type[pt as usize]
353 }
354
355 #[inline(always)]
357 pub fn pieces_color_pt(&self, c: Color, pt: PieceType) -> Bitboard {
358 self.by_color[c as usize] & self.by_type[pt as usize]
359 }
360
361 #[must_use]
363 pub fn side_to_move(&self) -> Color {
364 self.side_to_move
365 }
366
367 #[must_use]
369 pub fn castling_rights(&self) -> u8 {
370 self.castling_rights
371 }
372
373 #[must_use]
375 pub fn ep_square(&self) -> Option<Square> {
376 self.ep_square
377 }
378
379 #[inline(always)]
381 #[must_use]
382 pub fn commoners(&self, c: Color) -> Bitboard {
383 self.pieces_color_pt(c, PieceType::Commoner)
384 }
385
386 #[must_use]
391 pub fn attackers_to(&self, sq: Square, occupied: Bitboard) -> Bitboard {
392 let mut attackers = Bitboard::EMPTY;
393
394 let white_pawn_attacks = attacks::pawn_attacks(Color::White, sq);
396 attackers =
397 attackers | (white_pawn_attacks & self.pieces_color_pt(Color::Black, PieceType::Pawn));
398 let black_pawn_attacks = attacks::pawn_attacks(Color::Black, sq);
399 attackers =
400 attackers | (black_pawn_attacks & self.pieces_color_pt(Color::White, PieceType::Pawn));
401
402 let knight_atk = attacks::knight_attacks(sq);
404 attackers = attackers | (knight_atk & (self.by_type[PieceType::Knight as usize]));
405
406 let bishop_atk = attacks::bishop_attacks(sq, occupied);
408 attackers = attackers
409 | (bishop_atk
410 & (self.by_type[PieceType::Bishop as usize]
411 | self.by_type[PieceType::Queen as usize]));
412
413 let rook_atk = attacks::rook_attacks(sq, occupied);
415 attackers = attackers
416 | (rook_atk
417 & (self.by_type[PieceType::Rook as usize]
418 | self.by_type[PieceType::Queen as usize]));
419
420 let king_atk = attacks::king_attacks(sq);
422 attackers = attackers | (king_atk & self.by_type[PieceType::Commoner as usize]);
423
424 attackers
425 }
426
427 pub(crate) fn compute_checkers(&self, us: Color) -> Bitboard {
428 let them = us.flip();
429 let commoners = self.commoners(us);
430 if commoners.is_empty() {
431 return Bitboard::EMPTY;
432 }
433
434 let mut checkers = Bitboard::EMPTY;
435 let occupied = self.occupied();
436
437 let them_bb = self.pieces_color(them);
438 let mut c = commoners;
439 while !c.is_empty() {
440 let ksq = c.pop_lsb();
441 let rook_atk = attacks::rook_attacks(ksq, occupied);
442 let bishop_atk = attacks::bishop_attacks(ksq, occupied);
443 let queen_atk = rook_atk | bishop_atk;
444 checkers = checkers
445 | (rook_atk & self.by_type[PieceType::Rook as usize] & them_bb)
446 | (bishop_atk & self.by_type[PieceType::Bishop as usize] & them_bb)
447 | (queen_atk & self.by_type[PieceType::Queen as usize] & them_bb)
448 | (attacks::knight_attacks(ksq)
449 & self.by_type[PieceType::Knight as usize]
450 & them_bb)
451 | (attacks::pawn_attacks(us, ksq)
452 & self.by_type[PieceType::Pawn as usize]
453 & them_bb);
454 }
455
456 let them_commoners = self.commoners(them);
458 if !them_commoners.is_empty() && !commoners.is_empty() {
459 let mut tc = them_commoners;
460 while !tc.is_empty() {
461 let tksq = tc.pop_lsb();
462 if attacks::king_attacks(tksq) & commoners != Bitboard::EMPTY {
463 checkers = checkers | Bitboard::square_bb(tksq);
464 }
465 }
466 }
467
468 checkers
469 }
470
471 #[must_use]
473 pub fn checkers(&self) -> Bitboard {
474 self.compute_checkers(self.side_to_move)
475 }
476
477 pub(crate) fn compute_pinned(&self, us: Color) -> Bitboard {
478 let mut pinned = Bitboard::EMPTY;
479 let commoners = self.commoners(us);
480 let them = us.flip();
481 let occupied = self.occupied();
482
483 let mut c_iter = commoners;
484 while !c_iter.is_empty() {
485 let ksq = c_iter.pop_lsb();
486
487 let mut snipers = ((self.by_type[PieceType::Rook as usize]
488 | self.by_type[PieceType::Queen as usize])
489 & attacks::rook_attacks(ksq, Bitboard::EMPTY))
490 | ((self.by_type[PieceType::Bishop as usize]
491 | self.by_type[PieceType::Queen as usize])
492 & attacks::bishop_attacks(ksq, Bitboard::EMPTY));
493 snipers = snipers & self.pieces_color(them);
494
495 let mut s = snipers;
496 while !s.is_empty() {
497 let sniper_sq = s.pop_lsb();
498 let between = between_bb(ksq, sniper_sq) & occupied;
499 if between.count() == 1 {
500 pinned = pinned | between;
501 }
502 }
503 }
504
505 pinned
506 }
507
508 #[must_use]
511 pub fn pinned(&self, c: Color) -> Bitboard {
512 self.compute_pinned(c)
513 }
514
515 pub fn populate_state(&self, state: &mut StateInfo) {
518 state.checkers = self.compute_checkers(self.side_to_move);
519 state.pinned = self.compute_pinned(self.side_to_move);
520 state.commoners_count = self.commoners(self.side_to_move).count();
521 state.them_commoners_count = self.commoners(self.side_to_move.flip()).count();
522 }
523
524 pub fn do_move(&mut self, m: Move, state: &mut StateInfo) {
529 state.castling_rights = self.castling_rights;
530 state.ep_square = self.ep_square;
531 state.rule50 = self.rule50;
532 state.captured_count = 0;
533 state.cap_sq = None;
534 state.cap_piece = NO_PIECE;
535
536 let us = self.side_to_move;
537 let them = us.flip();
538 let from = m.from_sq();
539 let to = m.to_sq();
540 let piece = self.squares[from as usize];
541 let pt = piece.type_of();
542 let is_capture = !self.empty(to);
543
544 if m.move_type() == MoveType::Castling {
545 let (kfrom, kto, rfrom, rto) = castling_squares(us, to > from);
546 self.move_piece(kfrom, kto);
547 self.move_piece(rfrom, rto);
548 self.castling_rights &= match us {
549 Color::White => !(WK_CASTLE | WQ_CASTLE),
550 Color::Black => !(BK_CASTLE | BQ_CASTLE),
551 };
552 self.ep_square = None;
553 self.rule50 += 1;
554 self.side_to_move = them;
555 self.game_ply += 1;
556 return;
557 }
558
559 if m.move_type() == MoveType::EnPassant {
560 let ep_cap = match us {
561 Color::White => Square::from_index(to as i8 - 8),
562 Color::Black => Square::from_index(to as i8 + 8),
563 };
564 let cap_piece = self.squares[ep_cap as usize];
565 state.captured[state.captured_count as usize] = (ep_cap, cap_piece);
566 state.captured_count += 1;
567 self.remove_piece(ep_cap);
568 } else if is_capture {
569 let cap_piece = self.squares[to as usize];
570 state.cap_sq = Some(to);
571 state.cap_piece = cap_piece;
572 self.remove_piece(to);
573 }
574
575 if m.move_type() == MoveType::Promotion {
576 let prom_pt = m.promotion_type();
577 let prom_piece = make_piece(us, prom_pt);
578 self.remove_piece(from);
579 self.squares[to as usize] = prom_piece;
580 self.by_color[us as usize] = self.by_color[us as usize] | Bitboard::square_bb(to);
581 self.by_type[prom_pt as usize] =
582 self.by_type[prom_pt as usize] | Bitboard::square_bb(to);
583 } else {
584 self.move_piece(from, to);
585 }
586
587 if is_capture || m.move_type() == MoveType::EnPassant {
589 let blast_zone = attacks::king_attacks(to) & !self.by_type[PieceType::Pawn as usize];
591 let mut to_blast = blast_zone & self.occupied();
592
593 to_blast = to_blast | Bitboard::square_bb(to);
595
596 let mut b = to_blast;
597 while !b.is_empty() {
598 let bsq = b.pop_lsb();
599 let bpiece = self.squares[bsq as usize];
600 if bpiece != NO_PIECE {
601 state.captured[state.captured_count as usize] = (bsq, bpiece);
602 state.captured_count += 1;
603 self.remove_piece(bsq);
604 }
605 }
606 }
607
608 self.update_castling_rights(from, to, us);
609
610 if is_capture || m.move_type() == MoveType::EnPassant {
611 if self.castling_rights & WK_CASTLE != 0
613 && self.squares[Square::H1 as usize] != make_piece(Color::White, PieceType::Rook)
614 {
615 self.castling_rights &= !WK_CASTLE;
616 }
617 if self.castling_rights & WQ_CASTLE != 0
619 && self.squares[Square::A1 as usize] != make_piece(Color::White, PieceType::Rook)
620 {
621 self.castling_rights &= !WQ_CASTLE;
622 }
623 if self.castling_rights & BK_CASTLE != 0
625 && self.squares[Square::H8 as usize] != make_piece(Color::Black, PieceType::Rook)
626 {
627 self.castling_rights &= !BK_CASTLE;
628 }
629 if self.castling_rights & BQ_CASTLE != 0
631 && self.squares[Square::A8 as usize] != make_piece(Color::Black, PieceType::Rook)
632 {
633 self.castling_rights &= !BQ_CASTLE;
634 }
635 }
636
637 if pt == PieceType::Pawn && (to as i8 - from as i8).abs() == 16 {
638 self.ep_square = Some(match us {
639 Color::White => Square::from_index(from as i8 + 8),
640 Color::Black => Square::from_index(from as i8 - 8),
641 });
642 } else {
643 self.ep_square = None;
644 }
645
646 if pt == PieceType::Pawn || is_capture {
647 self.rule50 = 0;
648 } else {
649 self.rule50 += 1;
650 }
651
652 self.side_to_move = them;
653 self.game_ply += 1;
654
655 self.populate_state(state);
656 }
657
658 pub fn undo_move(&mut self, m: Move, state: &StateInfo) {
662 self.castling_rights = state.castling_rights;
663 self.ep_square = state.ep_square;
664 self.rule50 = state.rule50;
665
666 self.side_to_move = self.side_to_move.flip();
667 let us = self.side_to_move;
668 let from = m.from_sq();
669 let to = m.to_sq();
670
671 if m.move_type() == MoveType::Castling {
672 let (kfrom, kto, rfrom, rto) = castling_squares(us, to > from);
673 self.move_piece(kto, kfrom);
674 self.move_piece(rto, rfrom);
675 self.game_ply -= 1;
676 return;
677 }
678
679 let mut i = state.captured_count;
680 while i > 0 {
681 i -= 1;
682 let (sq, piece) = state.captured[i as usize];
683 self.place_piece(piece, sq);
684 }
685
686 if m.move_type() == MoveType::Promotion {
687 let pawn = make_piece(us, PieceType::Pawn);
688 self.remove_piece(to);
689 self.place_piece(pawn, from);
690 } else {
691 self.move_piece(to, from);
692 }
693
694 if let Some(sq) = state.cap_sq {
695 self.place_piece(state.cap_piece, sq);
696 }
697
698 self.game_ply -= 1;
699 }
700
701 fn move_piece(&mut self, from: Square, to: Square) {
702 let piece = self.squares[from as usize];
703 debug_assert!(piece != NO_PIECE);
704 self.squares[to as usize] = piece;
705 self.squares[from as usize] = NO_PIECE;
706
707 let from_bb = Bitboard::square_bb(from);
708 let to_bb = Bitboard::square_bb(to);
709
710 let c = piece.color();
711 let pt = piece.type_of();
712 self.by_color[c as usize] = (self.by_color[c as usize] ^ from_bb) | to_bb;
713 self.by_type[pt as usize] = (self.by_type[pt as usize] ^ from_bb) | to_bb;
714 }
715
716 fn remove_piece(&mut self, sq: Square) {
717 let piece = self.squares[sq as usize];
718 if piece == NO_PIECE {
719 return;
720 }
721 self.squares[sq as usize] = NO_PIECE;
722 let sq_bb = Bitboard::square_bb(sq);
723 self.by_color[piece.color() as usize] = self.by_color[piece.color() as usize] ^ sq_bb;
724 self.by_type[piece.type_of() as usize] = self.by_type[piece.type_of() as usize] ^ sq_bb;
725 }
726
727 fn place_piece(&mut self, piece: Piece, sq: Square) {
728 debug_assert!(self.squares[sq as usize] == NO_PIECE);
729 self.squares[sq as usize] = piece;
730 let sq_bb = Bitboard::square_bb(sq);
731 self.by_color[piece.color() as usize] = self.by_color[piece.color() as usize] | sq_bb;
732 self.by_type[piece.type_of() as usize] = self.by_type[piece.type_of() as usize] | sq_bb;
733 }
734
735 fn update_castling_rights(&mut self, from: Square, to: Square, _us: Color) {
736 if from == Square::E1 || from == Square::H1 || to == Square::H1 {
738 self.castling_rights &= !WK_CASTLE;
739 }
740 if from == Square::E1 || from == Square::A1 || to == Square::A1 {
741 self.castling_rights &= !WQ_CASTLE;
742 }
743 if from == Square::E8 || from == Square::H8 || to == Square::H8 {
744 self.castling_rights &= !BK_CASTLE;
745 }
746 if from == Square::E8 || from == Square::A8 || to == Square::A8 {
747 self.castling_rights &= !BQ_CASTLE;
748 }
749 }
750
751 #[must_use]
758 pub fn legal(&self, m: Move, state: &StateInfo) -> bool {
759 let from = m.from_sq();
760 let to = m.to_sq();
761 let us = self.side_to_move;
762 let them = us.flip();
763
764 let piece = self.piece_on(from);
765 if piece == NO_PIECE {
766 return false;
767 }
768
769 let is_capture = m.move_type() != MoveType::Castling
770 && (m.move_type() == MoveType::EnPassant || self.piece_on(to) != NO_PIECE);
771
772 if m.move_type() == MoveType::Castling {
773 let ksq = from;
774 let occupied = self.occupied();
775 let pass_through = if to > ksq {
776 [ksq, Square::from_index(ksq as i8 + 1)]
777 } else {
778 [Square::from_index(ksq as i8 - 1), ksq]
779 };
780 for &sq in &pass_through {
781 let adjacent_enemy_commoners = self.commoners(them) & attacks::king_attacks(sq);
782 if adjacent_enemy_commoners.is_empty() {
783 let atk = attackers_to(self, sq, occupied, self.by_color[them as usize])
784 | (attacks::king_attacks(sq)
785 & self.by_type[PieceType::Commoner as usize]
786 & self.by_color[them as usize]);
787 if atk != Bitboard::EMPTY {
788 return false;
789 }
790 }
791 }
792 }
793
794 let mut occupied = self.occupied() ^ from;
795 let mut kto = to;
796
797 if m.move_type() == MoveType::Castling {
798 let (_, kto_actual, rfrom, rto) = castling_squares(us, to > from);
799 kto = kto_actual;
800 occupied = occupied ^ Bitboard::square_bb(rfrom);
801 occupied = occupied | Bitboard::square_bb(rto);
802 } else if m.move_type() == MoveType::EnPassant {
803 let capsq = match us {
804 Color::White => Square::from_index(to as i8 - 8),
805 Color::Black => Square::from_index(to as i8 + 8),
806 };
807 occupied = occupied & !Bitboard::square_bb(capsq);
808 }
809
810 occupied = occupied | Bitboard::square_bb(kto);
811
812 if is_capture {
813 let pre_pawns = self.by_type[PieceType::Pawn as usize];
814 let pre_non_pawns = self.occupied() ^ pre_pawns;
815 let blast_adjacent = attacks::king_attacks(kto) & pre_non_pawns;
816 occupied = occupied & !(blast_adjacent | Bitboard::square_bb(kto));
817 }
818
819 let mut our_commoners = self.commoners(us) & occupied;
820
821 if m.move_type() == MoveType::Castling {
822 our_commoners = our_commoners | Bitboard::square_bb(kto);
823 } else if !is_capture {
824 let moving_piece = self.piece_on(from);
825 if moving_piece != NO_PIECE && moving_piece.type_of() == PieceType::Commoner {
826 our_commoners = our_commoners | Bitboard::square_bb(kto);
827 }
828 }
829
830 if our_commoners.is_empty() {
831 return false;
832 }
833
834 let our_pr_count = state.commoners_count as usize;
835 let them_pr_count = state.them_commoners_count as usize;
836
837 if our_pr_count <= 1 {
838 let enemy_pr_destroyed =
839 them_pr_count <= 1 && (self.commoners(them) & occupied).is_empty();
840
841 if !enemy_pr_destroyed {
842 let them_commoners = self.commoners(them);
843 let enemy_survivors = self.by_color[them as usize] & occupied;
844
845 let mut c = our_commoners;
846 while !c.is_empty() {
847 let ksq = c.pop_lsb();
848 let adjacent_enemy = them_commoners & attacks::king_attacks(ksq);
849 if adjacent_enemy.is_empty()
850 && attackers_to(self, ksq, occupied, enemy_survivors) != Bitboard::EMPTY
851 {
852 return false;
853 }
854 }
855 }
856 }
857
858 true
859 }
860}
861
862#[inline(always)]
868fn attackers_to(board: &Board, sq: Square, occupied: Bitboard, enemy_bb: Bitboard) -> Bitboard {
869 let rook_atk = attacks::rook_attacks(sq, occupied);
870 let bishop_atk = attacks::bishop_attacks(sq, occupied);
871 let queen_atk = rook_atk | bishop_atk;
872 rook_atk & board.by_type[PieceType::Rook as usize] & enemy_bb
873 | bishop_atk & board.by_type[PieceType::Bishop as usize] & enemy_bb
874 | queen_atk & board.by_type[PieceType::Queen as usize] & enemy_bb
875 | attacks::knight_attacks(sq) & board.by_type[PieceType::Knight as usize] & enemy_bb
876 | attacks::pawn_attacks(board.side_to_move, sq)
877 & board.by_type[PieceType::Pawn as usize]
878 & enemy_bb
879}
880
881fn castling_squares(us: Color, kingside: bool) -> (Square, Square, Square, Square) {
883 match (us, kingside) {
884 (Color::White, true) => (Square::E1, Square::G1, Square::H1, Square::F1),
885 (Color::White, false) => (Square::E1, Square::C1, Square::A1, Square::D1),
886 (Color::Black, true) => (Square::E8, Square::G8, Square::H8, Square::F8),
887 (Color::Black, false) => (Square::E8, Square::C8, Square::A8, Square::D8),
888 }
889}
890
891#[inline(always)]
898pub(crate) fn is_move_trivially_legal(board: &Board, m: Move, state: &StateInfo) -> bool {
899 if !state.checkers.is_empty() {
900 return false;
901 }
902 if state.commoners_count == 0 {
903 return false;
904 }
905
906 let from = m.from_sq();
907 let pt = board.piece_on(from).type_of();
908 if pt == PieceType::Commoner {
909 return false;
910 }
911
912 let mt = m.move_type();
913 if mt == MoveType::EnPassant {
914 return false;
915 }
916
917 let is_capture = mt != MoveType::Castling && board.piece_on(m.to_sq()) != NO_PIECE;
919 if is_capture {
920 return false;
921 }
922
923 if mt == MoveType::Castling {
926 return false;
927 }
928
929 if (state.pinned & Bitboard::square_bb(from)) != Bitboard::EMPTY {
931 return false;
932 }
933
934 true
935}
936
937impl fmt::Display for Board {
938 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
939 writeln!(f)?;
940 for rank in (0..8).rev() {
941 write!(f, "{} ", rank + 1)?;
942 for file in 0..8 {
943 let wrapped_sq = make_square(
944 match file {
945 0 => File::A,
946 1 => File::B,
947 2 => File::C,
948 3 => File::D,
949 4 => File::E,
950 5 => File::F,
951 6 => File::G,
952 7 => File::H,
953 _ => unreachable!(),
954 },
955 match rank {
956 0 => Rank::R1,
957 1 => Rank::R2,
958 2 => Rank::R3,
959 3 => Rank::R4,
960 4 => Rank::R5,
961 5 => Rank::R6,
962 6 => Rank::R7,
963 7 => Rank::R8,
964 _ => unreachable!(),
965 },
966 );
967 let wrapped_idx = wrapped_sq as usize;
968 if self.squares[wrapped_idx] == NO_PIECE {
969 write!(f, " .")?;
970 } else {
971 write!(f, " {}", self.squares[wrapped_idx].ascii_char())?;
972 }
973 }
974 writeln!(f)?;
975 }
976 writeln!(f, " a b c d e f g h")?;
977 writeln!(f, "Side to move: {:?}", self.side_to_move)?;
978 writeln!(f, "Castling: {:b}", self.castling_rights)?;
979 writeln!(f, "EP: {:?}", self.ep_square)?;
980 writeln!(f, "FEN: {}", self.fen())?;
981 Ok(())
982 }
983}
984
985impl Square {
986 pub fn from_index(idx: i8) -> Square {
989 if (0..64).contains(&idx) {
990 crate::types::SQUARES[idx as usize]
991 } else {
992 Square::NONE
993 }
994 }
995
996 pub fn from_u8(idx: u8) -> Square {
998 Square::from_index(idx as i8)
999 }
1000}
1001
1002#[cfg(test)]
1003mod tests {
1004 use super::*;
1005
1006 #[test]
1007 fn test_starting_position() {
1008 let board = Board::new();
1009 assert_eq!(board.piece_on(Square::E1), W_COMMONER);
1010 assert_eq!(board.piece_on(Square::D1), W_QUEEN);
1011 assert_eq!(board.piece_on(Square::E8), B_COMMONER);
1012 assert_eq!(board.side_to_move(), Color::White);
1013 assert_eq!(
1014 board.castling_rights,
1015 WK_CASTLE | WQ_CASTLE | BK_CASTLE | BQ_CASTLE
1016 );
1017 }
1018
1019 #[test]
1020 fn test_fen_roundtrip() {
1021 let board = Board::new();
1022 let fen = board.fen();
1023 let board2 = Board::from_fen(&fen).unwrap();
1024 assert_eq!(board.fen(), board2.fen());
1025 }
1026
1027 #[test]
1028 fn test_custom_fen() {
1029 let fen = "8/8/8/8/8/8/8/4K3 w - - 0 1";
1030 let board = Board::from_fen(fen).unwrap();
1031 assert_eq!(board.piece_on(Square::E1), W_COMMONER);
1032 }
1033
1034 #[test]
1035 fn test_checkers() {
1036 let fen = "4k3/8/8/8/8/8/8/4Q2K b - - 0 1";
1038 let board = Board::from_fen(fen).unwrap();
1039 let checkers = board.checkers();
1040 assert!(!checkers.is_empty(), "Expected checkers, got empty");
1041 }
1042
1043 #[test]
1044 fn test_pinned() {
1045 let fen = "4k3/8/8/8/4r3/4P3/4K3/8 w - - 0 1";
1047 let board = Board::from_fen(fen).unwrap();
1048 let pinned = board.pinned(Color::White);
1049 assert!(!pinned.is_empty(), "Expected pinned pieces, got empty");
1050 }
1051
1052 #[test]
1053 fn test_do_undo_restores_state() {
1054 let fen = "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1";
1055 let mut board = Board::from_fen(fen).unwrap();
1056 let orig_fen = board.fen();
1057 let mut state = StateInfo::new();
1058
1059 let m = Move::make_move(Square::E2, Square::E4);
1061 board.do_move(m, &mut state);
1062 board.undo_move(m, &state);
1063
1064 assert_eq!(board.fen(), orig_fen);
1065 }
1066
1067 #[test]
1068 fn test_do_undo_capture_restores() {
1069 let fen2 = "rnbqkbnr/ppp1pppp/8/3p4/4P3/8/PPPP1PPP/RNBQKBNR w KQkq d6 0 2";
1070 let mut board2 = Board::from_fen(fen2).unwrap();
1071 let orig_fen = board2.fen();
1072 let mut state2 = StateInfo::new();
1073
1074 let m = Move::make_move(Square::E4, Square::D5);
1075 board2.do_move(m, &mut state2);
1076 board2.undo_move(m, &state2);
1077
1078 assert_eq!(board2.fen(), orig_fen);
1079 }
1080
1081 #[test]
1082 fn test_self_explosion_legal_with_surviving_commoner() {
1083 let fen = "4k3/8/8/3R4/3p4/3C4/8/4K3 w - - 0 1";
1089 let board = Board::from_fen(fen).unwrap();
1090 let mut moves = MoveList::new();
1091 crate::movegen::generate_legal(&board, &mut moves);
1092 let has_rook_d4 = moves
1094 .as_slice()
1095 .iter()
1096 .any(|&m| m.from_sq() == Square::D5 && m.to_sq() == Square::D4);
1097 assert!(
1098 has_rook_d4,
1099 "rook capture on d4 should be legal (e1 commoner survives)"
1100 );
1101 }
1102
1103 #[test]
1104 fn test_self_explosion_illegal_last_commoner() {
1105 let fen = "4k3/8/8/3R4/3p4/3C4/8/8 w - - 0 1";
1109 let board = Board::from_fen(fen).unwrap();
1110 let mut moves = MoveList::new();
1111 crate::movegen::generate_legal(&board, &mut moves);
1112 for &m in moves.as_slice() {
1113 assert!(
1114 m.from_sq() != Square::D5 || m.to_sq() != Square::D4,
1115 "rook capture on d4 should be illegal (last commoner destroyed)"
1116 );
1117 }
1118 }
1119
1120 #[test]
1121 fn test_blast_zone_removes_pieces() {
1122 let fen = "4k3/8/8/4np2/4R3/8/8/4K3 w - - 0 1";
1126 let mut board = Board::from_fen(fen).unwrap();
1127 let mut state = StateInfo::new();
1128 let m = Move::make_move(Square::E4, Square::E5);
1129 board.do_move(m, &mut state);
1130 assert!(
1132 board.piece_on(Square::E4) == NO_PIECE,
1133 "rook at e4 should be gone"
1134 );
1135 assert!(
1136 board.piece_on(Square::E5) == NO_PIECE,
1137 "knight at e5 should be gone"
1138 );
1139 assert!(
1140 board.piece_on(Square::F5) == B_PAWN,
1141 "pawn at f5 should survive"
1142 );
1143 }
1144
1145 #[test]
1146 fn test_pinned_piece_capture_explodes_pinner() {
1147 let fen = "4k3/8/8/4r3/4p3/4R3/8/4K3 w - - 0 1";
1153 let board = Board::from_fen(fen).unwrap();
1154 let mut moves = MoveList::new();
1155 crate::movegen::generate_legal(&board, &mut moves);
1156 let has_rook_e4 = moves
1157 .as_slice()
1158 .iter()
1159 .any(|&m| m.from_sq() == Square::E3 && m.to_sq() == Square::E4);
1160 assert!(
1161 has_rook_e4,
1162 "rook capture on e4 should be legal (blast removes pinning rook)"
1163 );
1164 }
1165
1166 #[test]
1167 fn test_en_passant_blast() {
1168 let fen2 = "4k3/8/8/2Pp4/8/8/8/4K3 w KQkq d6 0 2";
1171 let mut board2 = Board::from_fen(fen2).unwrap();
1172 let mut state2 = StateInfo::new();
1173 let m = Move::make_enpassant(Square::C5, Square::D6);
1174 board2.do_move(m, &mut state2);
1175 assert!(board2.piece_on(Square::C5) == NO_PIECE);
1178 assert!(board2.piece_on(Square::D5) == NO_PIECE);
1179 }
1180}