1use crate::bitboard::*;
6use crate::board::*;
7use crate::lookup;
8use crate::position::*;
9
10use alloc::string::String;
11
12#[derive(Clone, PartialEq, Eq, PartialOrd, Ord)]
31pub struct Setup {
32 bitboards: [Bitboard; 5],
33}
34
35impl Setup {
36 #[inline]
38 pub fn new() -> Self {
39 Self::from_bitboards_inner([Bitboard(0); 5])
40 }
41
42 #[inline]
45 pub fn from_text_record(record: &str) -> Result<Self, ParseRecordError> {
46 Self::from_ascii_record(record.as_bytes())
47 }
48
49 pub fn from_ascii_record(record: &[u8]) -> Result<Self, ParseRecordError> {
51 let mut s = record.iter().copied().peekable();
52 (|| {
53 let mut setup = Setup::new();
54 let mut accept_empty_square = true;
55 let mut rank: u8 = 7;
56 let mut file: u8 = 0;
57 for c in s.by_ref() {
58 if c == b'/' {
59 (file == 8).then_some(())?;
60 rank = rank.checked_sub(1)?;
61 file = 0;
62 accept_empty_square = true;
63 } else if (b'1'..=b'8').contains(&c) && accept_empty_square {
64 file = file + c - b'0';
65 (file <= 8).then_some(())?;
66 accept_empty_square = false;
67 } else if c == b' ' {
68 break;
69 } else {
70 let role = Role::from_ascii(c)?;
71 let color = match c.is_ascii_uppercase() {
72 true => Color::White,
73 false => Color::Black,
74 };
75 setup = setup.set(
76 Square::from_coords(File::new(file)?, Rank::new(rank)?),
77 Some(Piece { role, color }),
78 );
79 file += 1;
80 accept_empty_square = true;
81 }
82 }
83 (rank == 0).then_some(())?;
84 (file == 8).then_some(())?;
85
86 setup = match s.next()? {
87 b'w' => setup.set_turn(Color::White),
88 b'b' => setup.set_turn(Color::Black),
89 _ => return None,
90 };
91 (s.next()? == b' ').then_some(())?;
92
93 if s.next_if_eq(&b'-').is_none() {
94 if s.next_if_eq(&b'K').is_some() {
95 setup = setup.set_castling_rights(Color::White, Castle::Short, true);
96 }
97 if s.next_if_eq(&b'Q').is_some() {
98 setup = setup.set_castling_rights(Color::White, Castle::Long, true);
99 }
100 if s.next_if_eq(&b'k').is_some() {
101 setup = setup.set_castling_rights(Color::Black, Castle::Short, true);
102 }
103 if s.next_if_eq(&b'q').is_some() {
104 setup = setup.set_castling_rights(Color::Black, Castle::Long, true);
105 }
106 }
107 (s.next()? == b' ').then_some(())?;
108
109 match s.next()? {
110 b'-' => (),
111 file => {
112 setup = setup.set_en_passant_target_square(Some(Square::from_coords(
113 File::from_ascii(file)?,
114 Rank::from_ascii(s.next()?)?,
115 )));
116 }
117 }
118 s.next().is_none().then_some(())?;
119
120 Some(setup)
121 })()
122 .ok_or_else(|| ParseRecordError {
123 byte: record.len() - s.len(),
124 })
125 }
126
127 #[inline]
129 pub fn set(&self, square: Square, piece: Option<Piece>) -> Self {
130 let mask = square.bitboard();
131 let [mut a, mut b, mut c, mut d, e] = self.bitboards();
132 if let Some(piece) = piece {
133 match piece.role {
134 Role::Pawn => {
135 a |= mask;
136 b &= !mask;
137 c &= !mask;
138 }
139 Role::Knight => {
140 a &= !mask;
141 b |= mask;
142 c &= !mask;
143 }
144 Role::Bishop => {
145 a |= mask;
146 b |= mask;
147 c &= !mask;
148 }
149 Role::Rook => {
150 a &= !mask;
151 b &= !mask;
152 c |= mask;
153 }
154 Role::Queen => {
155 a |= mask;
156 b &= !mask;
157 c |= mask;
158 }
159 Role::King => {
160 a &= !mask;
161 b |= mask;
162 c |= mask;
163 }
164 }
165 match piece.color {
166 Color::White => d &= !mask,
167 Color::Black => d |= mask,
168 }
169 }
170 Self::from_bitboards_inner([a, b, c, d, e])
171 }
172
173 #[inline]
175 pub fn set_turn(&self, color: Color) -> Self {
176 let [a, b, c, d, mut e] = self.bitboards();
177 e.0 &= !2;
178 e.0 |= (color as u8 as u64) << 1;
179 Self::from_bitboards_inner([a, b, c, d, e])
180 }
181
182 #[inline]
184 pub fn switch_turn(&self) -> Self {
185 let [a, b, c, d, mut e] = self.bitboards();
186 e.0 ^= 2;
187 Self::from_bitboards_inner([a, b, c, d, e])
188 }
189
190 #[inline]
192 pub fn set_castling_rights(&self, color: Color, castle: Castle, value: bool) -> Self {
193 let [a, b, c, d, mut e] = self.bitboards();
194 let square = match (color, castle) {
195 (Color::White, Castle::Short) => Square::H1,
196 (Color::White, Castle::Long) => Square::A1,
197 (Color::Black, Castle::Short) => Square::H8,
198 (Color::Black, Castle::Long) => Square::A8,
199 };
200 match value {
201 false => e.remove(square),
202 true => e.insert(square),
203 }
204 Self::from_bitboards_inner([a, b, c, d, e])
205 }
206
207 #[inline]
209 pub fn set_en_passant_target_square(&self, square: Option<Square>) -> Self {
210 let [a, b, c, d, mut e] = self.bitboards();
211 let mask = Rank::Third.bitboard() | Rank::Sixth.bitboard();
212 e &= !mask;
213 if let Some(square) = square {
214 e |= mask & square.bitboard();
215 }
216 Self::from_bitboards_inner([a, b, c, d, e])
217 }
218
219 #[inline]
228 pub fn mirror(&self) -> Self {
229 let mask = Square::B1.bitboard();
230 let [a, b, c, d, e] = self.bitboards();
231 let [a, b, c, d] = [a, b, c, d].map(Bitboard::mirror);
232 let turn = (e & mask) ^ mask;
233 let e = (e & !mask).mirror() | turn;
234 Self::from_bitboards_inner([a, b, c, (a | b | c) ^ d, e])
235 }
236
237 pub fn illegal_reasons(&self) -> IllegalPositionReasons {
239 let mut reasons = IllegalPositionReasons::new();
240
241 let [a, b, c, _, _] = self.bitboards();
242 let blockers = a | b | c;
243 let pieces = self.pieces();
244
245 if Color::all()
246 .into_iter()
247 .any(|color| pieces.get(color).king().is_empty())
248 {
249 reasons.insert(IllegalPositionReason::MissingKing);
250 }
251
252 if pieces.get(!self.turn()).king().any(|enemy_king| {
253 let pieces = pieces.get(self.turn());
254 !(lookup::king(enemy_king) & pieces.king()
255 | lookup::bishop(enemy_king, blockers) & (pieces.queen() | pieces.bishop())
256 | lookup::rook(enemy_king, blockers) & (pieces.queen() | pieces.rook())
257 | lookup::knight(enemy_king) & pieces.knight()
258 | lookup::pawn_attack(!self.turn(), enemy_king) & pieces.pawn())
259 .is_empty()
260 }) {
261 reasons.insert(IllegalPositionReason::HangingKing);
262 }
263
264 if Color::all().into_iter().any(|color| {
265 !(pieces.get(color).pawn() & (Rank::First.bitboard() | Rank::Eighth.bitboard()))
266 .is_empty()
267 }) {
268 reasons.insert(IllegalPositionReason::PawnOnBackRank);
269 }
270
271 if Color::all().into_iter().any(|color| {
272 let pieces = pieces.get(color);
273 (pieces.pawn().len()
274 + pieces.queen().len().saturating_sub(1)
275 + pieces.bishop().len().saturating_sub(2)
276 + pieces.knight().len().saturating_sub(2)
277 + pieces.rook().len().saturating_sub(2))
278 .saturating_sub(8)
279 + pieces.king().len().saturating_sub(1)
280 > 0
281 }) {
282 reasons.insert(IllegalPositionReason::TooMuchMaterial);
283 }
284
285 if Color::all().into_iter().any(|color| {
286 Castle::all().into_iter().any(|castle| {
287 self.castling_rights(color, castle)
288 && !(pieces
289 .get(color)
290 .king()
291 .contains(Square::from_coords(File::E, color.home_rank()))
292 && pieces.get(color).rook().contains(Square::from_coords(
293 castle.rook_origin_file(),
294 color.home_rank(),
295 )))
296 })
297 }) {
298 reasons.insert(IllegalPositionReason::InvalidCastlingRights);
299 }
300
301 if self.en_passant_target_square().is_some_and(|en_passant| {
302 let (target_rank, pawn_rank) = match self.turn() {
303 Color::White => (Rank::Sixth, Rank::Fifth),
304 Color::Black => (Rank::Third, Rank::Fourth),
305 };
306 let pawn_square = Square::from_coords(en_passant.file(), pawn_rank);
307 en_passant.rank() != target_rank
308 || blockers.contains(en_passant)
309 || !pieces.get(!self.turn()).pawn().contains(pawn_square)
310 }) {
311 reasons.insert(IllegalPositionReason::InvalidEnPassant);
312 }
313
314 if self.en_passant_target_square().is_some_and(|en_passant| {
315 let blockers = blockers
316 & !en_passant.bitboard().trans(match self.turn() {
317 Color::White => Direction::South,
318 Color::Black => Direction::North,
319 });
320 pieces
321 .get(self.turn())
322 .king()
323 .first()
324 .is_some_and(|king_square| {
325 !(lookup::bishop(king_square, blockers)
326 & (pieces.get(!self.turn()).queen() | pieces.get(!self.turn()).bishop()))
327 .is_empty()
328 })
329 }) {
330 reasons.insert(IllegalPositionReason::ImpossibleEnPassantPin);
331 }
332
333 reasons
334 }
335
336 #[inline]
340 pub fn into_position(self) -> Result<Position, IllegalPosition> {
341 let reasons = self.illegal_reasons();
342 if reasons.is_empty() {
343 Ok(Position::from_setup_inner(self))
344 } else {
345 Err(IllegalPosition {
346 setup: self,
347 reasons,
348 })
349 }
350 }
351
352 #[inline]
356 pub unsafe fn into_position_unchecked(self) -> Position {
357 Position::from_setup_inner(self)
358 }
359
360 #[inline]
363 pub fn from_bitboards(bitboards: [Bitboard; 5]) -> Option<Self> {
364 sanity_check(bitboards).then_some(Self { bitboards })
365 }
366
367 #[inline]
373 pub unsafe fn from_bitboards_unchecked(bitboards: [Bitboard; 5]) -> Self {
374 Self::from_bitboards_inner(bitboards)
375 }
376
377 #[inline]
378 pub(crate) fn from_bitboards_inner(bitboards: [Bitboard; 5]) -> Self {
379 debug_assert!(sanity_check(bitboards));
380 Self { bitboards }
381 }
382
383 #[inline]
384 pub(crate) fn role(&self, square: Square) -> Option<Role> {
385 let mask = square.bitboard();
386 let [a, b, c, _, _] = self.bitboards();
387 let bit0 = (a & mask).0 >> square as u8;
388 let bit1 = (b & mask).0 >> square as u8;
389 let bit2 = (c & mask).0 >> square as u8;
390 Role::new((bit0 | bit1 << 1 | bit2 << 2) as u8)
391 }
392
393 #[inline]
394 pub(crate) fn color(&self, square: Square) -> Color {
395 let [_, _, _, x, _] = self.bitboards();
396 match x.contains(square) {
397 false => Color::White,
398 true => Color::Black,
399 }
400 }
401}
402
403#[inline]
404pub(crate) fn sanity_check(bitboards: [Bitboard; 5]) -> bool {
405 let [a, b, c, d, e] = bitboards;
406 let mut res = true;
407 res &= (a & b & c).is_empty();
408 res &= (d & !(a | b | c)).is_empty();
409 res &= (e & !Bitboard(0x8100FF0000FF0083)).is_empty();
410 res &= (e & Bitboard(0x0000FF0000FF0000)).len() <= 1;
411 res
412}
413
414pub(crate) struct TextRecord<'a>(pub(crate) &'a Setup);
415impl<'a> core::fmt::Display for TextRecord<'a> {
416 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
417 self.0.write_text_record(f)
418 }
419}
420impl<'a> core::fmt::Debug for TextRecord<'a> {
421 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
422 use core::fmt::Write;
423 f.write_char('"')?;
424 self.0.write_text_record(f)?;
425 f.write_char('"')?;
426 Ok(())
427 }
428}
429
430impl core::fmt::Debug for Setup {
431 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> Result<(), core::fmt::Error> {
432 f.debug_tuple("Setup").field(&TextRecord(&self)).finish()
433 }
434}
435
436impl Default for Setup {
437 #[inline]
438 fn default() -> Self {
439 Self::new()
440 }
441}
442
443#[derive(Debug)]
447pub struct IllegalPosition {
448 pub setup: Setup,
449 pub reasons: IllegalPositionReasons,
450}
451impl core::fmt::Display for IllegalPosition {
452 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
453 use core::fmt::Write;
454 let setup = &self.setup;
455 write!(f, "`{}` is illegal:", &TextRecord(setup))?;
456 let mut first = true;
457 for reason in self.reasons {
458 if !first {
459 f.write_char(',')?;
460 }
461 first = false;
462 f.write_char(' ')?;
463 f.write_str(reason.to_str())?;
464 }
465 Ok(())
466 }
467}
468impl core::error::Error for IllegalPosition {}
469
470#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
472pub struct IllegalPositionReasons {
473 set: u8,
474}
475
476impl core::fmt::Debug for IllegalPositionReasons {
477 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
478 f.debug_list().entries(*self).finish()
479 }
480}
481
482impl IllegalPositionReasons {
483 pub fn contains(&self, reason: IllegalPositionReason) -> bool {
485 self.set & (1 << reason as u8) != 0
486 }
487
488 pub fn is_empty(&self) -> bool {
490 self.set == 0
491 }
492
493 fn new() -> Self {
494 Self { set: 0 }
495 }
496
497 fn insert(&mut self, reason: IllegalPositionReason) {
498 self.set |= 1 << reason as u8;
499 }
500}
501
502impl Iterator for IllegalPositionReasons {
503 type Item = IllegalPositionReason;
504 fn next(&mut self) -> Option<Self::Item> {
505 if self.set == 0 {
506 None
507 } else {
508 let i = self.set.trailing_zeros();
509 self.set &= !(1 << i);
510 IllegalPositionReason::new(i as u8)
511 }
512 }
513}
514
515#[test]
516fn illegal_position_reasons() {
517 let mut reasons = IllegalPositionReasons { set: 64 };
518 assert_eq!(
519 reasons.next(),
520 Some(IllegalPositionReason::ImpossibleEnPassantPin)
521 );
522 assert_eq!(reasons.next(), None);
523 assert_eq!(IllegalPositionReasons { set: 63 }.count(), 6);
524}
525
526#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
528pub enum IllegalPositionReason {
529 MissingKing,
531 HangingKing,
533 PawnOnBackRank,
535 InvalidCastlingRights,
537 InvalidEnPassant,
542 TooMuchMaterial,
547 ImpossibleEnPassantPin,
553}
554
555impl IllegalPositionReason {
556 fn new(index: u8) -> Option<Self> {
557 let reason = match index {
558 0 => Self::MissingKing,
559 1 => Self::HangingKing,
560 2 => Self::PawnOnBackRank,
561 3 => Self::InvalidCastlingRights,
562 4 => Self::InvalidEnPassant,
563 5 => Self::TooMuchMaterial,
564 6 => Self::ImpossibleEnPassantPin,
565 _ => return None,
566 };
567 debug_assert!(reason as u8 == index);
568 Some(reason)
569 }
570
571 fn to_str(self) -> &'static str {
572 match self {
573 Self::MissingKing => "missing king",
574 Self::HangingKing => "hanging king",
575 Self::PawnOnBackRank => "pawn on back rank",
576 Self::InvalidCastlingRights => "invalid castling rights",
577 Self::InvalidEnPassant => "invalid en passant",
578 Self::TooMuchMaterial => "too much material",
579 Self::ImpossibleEnPassantPin => "illegal en passant",
580 }
581 }
582}
583
584impl core::fmt::Display for IllegalPositionReason {
585 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
586 f.write_str(self.to_str())
587 }
588}
589
590#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
592pub struct ParseRecordError {
593 pub byte: usize,
595}
596impl core::fmt::Display for ParseRecordError {
597 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
598 write!(f, "invalid text record (at byte {})", self.byte)
599 }
600}
601impl core::error::Error for ParseRecordError {}
602
603pub trait AsSetup {
605 fn as_setup(&self) -> &Setup;
606 #[inline]
620 fn bitboards(&self) -> [Bitboard; 5] {
621 self.as_setup().bitboards
622 }
623 #[inline]
625 fn get(&self, square: Square) -> Option<Piece> {
626 let setup = self.as_setup();
627 Some(Piece {
628 role: setup.role(square)?,
629 color: setup.color(square),
630 })
631 }
632 #[inline]
634 fn turn(&self) -> Color {
635 let [_, _, _, _, x] = self.bitboards();
636 Color::new(((x.0 >> 1) & 1) as u8).unwrap()
637 }
638 #[inline]
640 fn castling_rights(&self, color: Color, castle: Castle) -> bool {
641 let [_, _, _, _, x] = self.bitboards();
642 let square = match (color, castle) {
643 (Color::White, Castle::Short) => Square::H1,
644 (Color::White, Castle::Long) => Square::A1,
645 (Color::Black, Castle::Short) => Square::H8,
646 (Color::Black, Castle::Long) => Square::A8,
647 };
648 x.contains(square)
649 }
650 #[inline]
652 fn en_passant_target_square(&self) -> Option<Square> {
653 let [_, _, _, _, x] = self.bitboards();
654 (x & (Rank::Third.bitboard() | Rank::Sixth.bitboard())).first()
655 }
656 #[inline]
658 fn pieces(&self) -> ByColor<ByRole<Bitboard>> {
659 let [a, b, c, d, _] = self.bitboards();
660 let t = [
661 a & !b & !c,
662 !a & b & !c,
663 a & b & !c,
664 !a & !b & c,
665 a & !b & c,
666 !a & b & c,
667 ];
668 ByColor([ByRole(t.map(|x| x & !d)), ByRole(t.map(|x| x & d))])
669 }
670 fn to_text_record(&self) -> String {
673 let mut record = String::with_capacity(81);
674 self.write_text_record(&mut record).unwrap();
675 record
676 }
677 #[inline]
679 fn write_text_record<W: core::fmt::Write>(&self, w: &mut W) -> core::fmt::Result {
680 for rank in Rank::all().into_iter().rev() {
681 let mut count = 0;
682 for file in File::all() {
683 match self.get(Square::from_coords(file, rank)) {
684 Some(piece) => {
685 if count > 0 {
686 w.write_char(char::from_u32('0' as u32 + count).unwrap())?;
687 }
688 count = 0;
689 w.write_char(match piece.color {
690 Color::White => piece.role.to_char_uppercase(),
691 Color::Black => piece.role.to_char_lowercase(),
692 })?;
693 }
694 None => {
695 count += 1;
696 }
697 }
698 }
699 if count > 0 {
700 w.write_char(char::from_u32('0' as u32 + count).unwrap())?;
701 }
702 if rank != Rank::First {
703 w.write_char('/')?;
704 }
705 }
706
707 w.write_char(' ')?;
708
709 w.write_char(match self.turn() {
710 Color::White => 'w',
711 Color::Black => 'b',
712 })?;
713
714 w.write_char(' ')?;
715
716 let mut no_castle_available = true;
717 if self.castling_rights(Color::White, Castle::Short) {
718 w.write_char('K')?;
719 no_castle_available = false;
720 }
721 if self.castling_rights(Color::White, Castle::Long) {
722 w.write_char('Q')?;
723 no_castle_available = false;
724 }
725 if self.castling_rights(Color::Black, Castle::Short) {
726 w.write_char('k')?;
727 no_castle_available = false;
728 }
729 if self.castling_rights(Color::Black, Castle::Long) {
730 w.write_char('q')?;
731 no_castle_available = false;
732 }
733 if no_castle_available {
734 w.write_char('-')?;
735 }
736
737 w.write_char(' ')?;
738
739 match self.en_passant_target_square() {
740 Some(sq) => {
741 w.write_char(sq.file().to_char())?;
742 w.write_char(sq.rank().to_char())?;
743 }
744 None => {
745 w.write_char('-')?;
746 }
747 }
748
749 Ok(())
750 }
751}
752
753impl AsSetup for Setup {
754 #[inline]
755 fn as_setup(&self) -> &Setup {
756 self
757 }
758}