1use super::{Action, Board, Team};
59use crate::state::{
60 MASK_COL_A, MASK_COL_B, MASK_COL_G, MASK_COL_H, MASK_ROW_1, MASK_ROW_2, MASK_ROW_7, MASK_ROW_8,
61 MASK_ROW_PROMOTIONS,
62};
63
64const LEFT_RAY: [u64; 64] = {
68 let mut rays = [0u64; 64];
69 let mut sq = 0;
70 while sq < 64 {
71 let col = sq % 8;
72 let row_start = sq - col;
73 let mut mask = 0u64;
75 let mut c = 0;
76 while c < col {
77 mask |= 1u64 << (row_start + c);
78 c += 1;
79 }
80 rays[sq] = mask;
81 sq += 1;
82 }
83 rays
84};
85
86const RIGHT_RAY: [u64; 64] = {
88 let mut rays = [0u64; 64];
89 let mut sq = 0;
90 while sq < 64 {
91 let col = sq % 8;
92 let row_start = sq - col;
93 let mut mask = 0u64;
95 let mut c = col + 1;
96 while c < 8 {
97 mask |= 1u64 << (row_start + c);
98 c += 1;
99 }
100 rays[sq] = mask;
101 sq += 1;
102 }
103 rays
104};
105
106const UP_RAY: [u64; 64] = {
108 let mut rays = [0u64; 64];
109 let mut sq = 0;
110 while sq < 64 {
111 let col = sq % 8;
112 let row = sq / 8;
113 let mut mask = 0u64;
115 let mut r = row + 1;
116 while r < 8 {
117 mask |= 1u64 << (r * 8 + col);
118 r += 1;
119 }
120 rays[sq] = mask;
121 sq += 1;
122 }
123 rays
124};
125
126const DOWN_RAY: [u64; 64] = {
128 let mut rays = [0u64; 64];
129 let mut sq = 0;
130 while sq < 64 {
131 let col = sq % 8;
132 let row = sq / 8;
133 let mut mask = 0u64;
135 let mut r: i32 = row as i32 - 1;
136 while r >= 0 {
137 mask |= 1u64 << (r as usize * 8 + col);
138 r -= 1;
139 }
140 rays[sq] = mask;
141 sq += 1;
142 }
143 rays
144};
145
146#[allow(clippy::large_const_arrays)]
150const RANK_ATTACKS: [[u64; 64]; 64] = {
151 let mut table = [[0u64; 64]; 64];
152 let mut sq = 0usize;
153 while sq < 64 {
154 let col = sq % 8;
155 let row_start = sq - col;
156
157 let mut occ6 = 0usize;
158 while occ6 < 64 {
159 let occ = (occ6 as u64) << 1;
161 let mut attacks = 0u64;
162
163 let mut c = col as i8 - 1;
165 while c >= 0 {
166 attacks |= 1u64 << (row_start + c as usize);
167 if (occ & (1u64 << c)) != 0 {
168 break;
169 }
170 c -= 1;
171 }
172
173 c = col as i8 + 1;
175 while c < 8 {
176 attacks |= 1u64 << (row_start + c as usize);
177 if (occ & (1u64 << c)) != 0 {
178 break;
179 }
180 c += 1;
181 }
182
183 table[sq][occ6] = attacks;
184 occ6 += 1;
185 }
186 sq += 1;
187 }
188 table
189};
190
191#[allow(clippy::large_const_arrays)]
195const FILE_ATTACKS: [[u64; 64]; 64] = {
196 let mut table = [[0u64; 64]; 64];
197 let mut sq = 0usize;
198 while sq < 64 {
199 let col = sq % 8;
200 let row = sq / 8;
201
202 let mut occ6 = 0usize;
203 while occ6 < 64 {
204 let occ = (occ6 as u64) << 1;
206 let mut attacks = 0u64;
207
208 let mut r = row as i8 - 1;
210 while r >= 0 {
211 attacks |= 1u64 << (r as usize * 8 + col);
212 if (occ & (1u64 << r)) != 0 {
213 break;
214 }
215 r -= 1;
216 }
217
218 r = row as i8 + 1;
220 while r < 8 {
221 attacks |= 1u64 << (r as usize * 8 + col);
222 if (occ & (1u64 << r)) != 0 {
223 break;
224 }
225 r += 1;
226 }
227
228 table[sq][occ6] = attacks;
229 occ6 += 1;
230 }
231 sq += 1;
232 }
233 table
234};
235
236const RANK_OCC_MASK: [u64; 64] = {
238 let mut masks = [0u64; 64];
239 let mut sq = 0;
240 while sq < 64 {
241 let col = sq % 8;
242 let row_start = sq - col;
243 masks[sq] = 0x7Eu64 << row_start;
245 sq += 1;
246 }
247 masks
248};
249
250const WHITE_PAWN_MOVES: [u64; 64] = {
251 let mut moves = [0u64; 64];
252 let mut src_index = 0;
253 while src_index < 64 {
254 let src_mask = 1u64 << src_index;
255 moves[src_index] = (src_mask & !MASK_COL_A) >> 1u8 | (src_mask & !MASK_COL_H) << 1u8 | (src_mask & !MASK_ROW_8) << 8u8; src_index += 1;
259 }
260 moves
261};
262
263const BLACK_PAWN_MOVES: [u64; 64] = {
264 let mut moves = [0u64; 64];
265 let mut src_index = 0;
266 while src_index < 64 {
267 let src_mask = 1u64 << src_index;
268 moves[src_index] = (src_mask & !MASK_COL_A) >> 1u8 | (src_mask & !MASK_COL_H) << 1u8 | (src_mask & !MASK_ROW_1) >> 8u8; src_index += 1;
272 }
273 moves
274};
275
276impl Board {
277 #[must_use]
279 #[inline]
280 pub fn actions(&self) -> Vec<Action> {
281 let mut actions = Vec::with_capacity(32);
282 self.actions_into(&mut actions);
283 actions
284 }
285
286 #[inline]
291 pub fn actions_into(&self, actions: &mut Vec<Action>) {
292 actions.clear();
293 if self.turn == Team::White {
294 self.generate_captures::<0>(actions);
295
296 if actions.is_empty() {
297 self.generate_moves::<0>(actions);
298 }
299 } else {
300 self.generate_captures::<1>(actions);
301
302 if actions.is_empty() {
303 self.generate_moves::<1>(actions);
304 }
305 }
306 }
307
308 #[inline]
314 pub fn count_actions(&self, scratch: &mut Vec<Action>) -> u64 {
315 if self.turn == Team::White {
316 let capture_count = self.count_captures::<0>(scratch);
317 if capture_count > 0 {
318 capture_count
319 } else {
320 self.count_moves::<0>()
321 }
322 } else {
323 let capture_count = self.count_captures::<1>(scratch);
324 if capture_count > 0 {
325 capture_count
326 } else {
327 self.count_moves::<1>()
328 }
329 }
330 }
331
332 fn count_captures<const TEAM_INDEX: usize>(&self, scratch: &mut Vec<Action>) -> u64 {
334 let may_have_pawn_captures = self.has_any_pawn_captures::<TEAM_INDEX>();
335 let has_friendly_kings = (self.friendly_pieces() & self.state.kings) != 0;
336
337 if !may_have_pawn_captures && !has_friendly_kings {
338 return 0;
339 }
340
341 scratch.clear();
344 let mut max_length: u32 = 0;
345 let mut board = *self;
346
347 if has_friendly_kings {
348 Self::generate_king_captures_with_board::<TEAM_INDEX>(
349 &mut board,
350 scratch,
351 &mut max_length,
352 );
353 }
354
355 if may_have_pawn_captures {
356 Self::generate_pawn_captures_with_board::<TEAM_INDEX>(
357 &mut board,
358 scratch,
359 &mut max_length,
360 );
361 }
362
363 scratch.len() as u64
364 }
365
366 fn count_moves<const TEAM_INDEX: usize>(&self) -> u64 {
368 let empty = self.state.empty();
369 self.count_king_moves::<TEAM_INDEX>(empty) + self.count_pawn_moves::<TEAM_INDEX>(empty)
370 }
371
372 #[inline]
374 fn count_pawn_moves<const TEAM_INDEX: usize>(&self, empty: u64) -> u64 {
375 let friendly_pawns = self.friendly_pieces() & !self.state.kings;
376 let mut count = 0u64;
377 let mut pawns = friendly_pawns;
378
379 while pawns != 0 {
380 let src_mask = pawns & pawns.wrapping_neg();
381 let src_index = src_mask.trailing_zeros() as usize;
382 let moves = if TEAM_INDEX == 0 {
383 WHITE_PAWN_MOVES[src_index] & empty
384 } else {
385 BLACK_PAWN_MOVES[src_index] & empty
386 };
387 count += moves.count_ones() as u64;
388 pawns ^= src_mask;
389 }
390 count
391 }
392
393 #[inline]
396 fn count_king_moves<const TEAM_INDEX: usize>(&self, empty: u64) -> u64 {
397 let occupied = !empty;
398 let mut friendly_kings = self.friendly_pieces() & self.state.kings;
399 let mut count = 0u64;
400
401 while friendly_kings != 0 {
402 let src_mask = friendly_kings & friendly_kings.wrapping_neg();
403 let sq = src_mask.trailing_zeros() as usize;
404
405 let attacks = Self::king_attacks_lut(sq, occupied);
407 count += (attacks & empty).count_ones() as u64;
408
409 friendly_kings ^= src_mask;
410 }
411 count
412 }
413
414 #[inline(always)]
417 fn king_attacks_lut(sq: usize, occupied: u64) -> u64 {
418 let rank_occ = (occupied & RANK_OCC_MASK[sq]) >> (sq - sq % 8 + 1);
420 let rank_occ6 = rank_occ as usize & 0x3F;
421
422 let col = sq % 8;
425 let file_bits = (occupied >> col) & 0x0101_0101_0101_0101u64;
426 let file_occ6 = ((file_bits.wrapping_mul(0x0002_0408_1020_4080u64)) >> 57) as usize & 0x3F;
428
429 RANK_ATTACKS[sq][rank_occ6] | FILE_ATTACKS[sq][file_occ6]
431 }
432
433 const fn has_any_pawn_captures<const TEAM_INDEX: usize>(&self) -> bool {
439 let friendly_pawns = self.friendly_pieces() & !self.state.kings;
440 if friendly_pawns == 0 {
441 return false;
442 }
443
444 let hostile = self.hostile_pieces();
445 let empty = self.state.empty();
446
447 {
449 let eligible = friendly_pawns & !(MASK_COL_A | MASK_COL_B);
450 let adjacent_hostile = (eligible >> 1) & hostile; let landing_clear = (eligible >> 2) & empty; if (landing_clear << 1) & adjacent_hostile != 0 {
454 return true;
455 }
456 }
457
458 {
460 let eligible = friendly_pawns & !(MASK_COL_G | MASK_COL_H);
461 let adjacent_hostile = (eligible << 1) & hostile; let landing_clear = (eligible << 2) & empty; if (landing_clear >> 1) & adjacent_hostile != 0 {
465 return true;
466 }
467 }
468
469 if TEAM_INDEX == 0 {
471 let eligible = friendly_pawns & !(MASK_ROW_7 | MASK_ROW_8);
473 let adjacent_hostile = (eligible << 8) & hostile;
474 let landing_clear = (eligible << 16) & empty;
475 if (landing_clear >> 8) & adjacent_hostile != 0 {
476 return true;
477 }
478 } else {
479 let eligible = friendly_pawns & !(MASK_ROW_1 | MASK_ROW_2);
481 let adjacent_hostile = (eligible >> 8) & hostile;
482 let landing_clear = (eligible >> 16) & empty;
483 if (landing_clear << 8) & adjacent_hostile != 0 {
484 return true;
485 }
486 }
487
488 false
489 }
490
491 #[inline]
492 fn generate_captures<const TEAM_INDEX: usize>(&self, actions: &mut Vec<Action>) {
493 let may_have_pawn_captures = self.has_any_pawn_captures::<TEAM_INDEX>();
497 let has_friendly_kings = (self.friendly_pieces() & self.state.kings) != 0;
498
499 if !may_have_pawn_captures && !has_friendly_kings {
500 return;
501 }
502
503 let mut max_length: u32 = 0;
505
506 let mut board = *self;
508
509 if has_friendly_kings {
511 Self::generate_king_captures_with_board::<TEAM_INDEX>(
512 &mut board,
513 actions,
514 &mut max_length,
515 );
516 }
517
518 if may_have_pawn_captures {
520 Self::generate_pawn_captures_with_board::<TEAM_INDEX>(
521 &mut board,
522 actions,
523 &mut max_length,
524 );
525 }
526 }
527
528 #[inline(always)]
530 fn push_capture_action<const TEAM_INDEX: usize>(
531 actions: &mut Vec<Action>,
532 max_length: &mut u32,
533 action: Action,
534 ) {
535 let length = action.delta.pieces[1 - TEAM_INDEX].count_ones();
536 if length > *max_length {
537 actions.clear();
539 *max_length = length;
540 actions.push(action);
541 } else if length == *max_length {
542 actions.push(action);
544 }
545 }
547
548 #[inline]
549 fn generate_pawn_captures_with_board<const TEAM_INDEX: usize>(
550 board: &mut Self,
551 actions: &mut Vec<Action>,
552 max_length: &mut u32,
553 ) {
554 let mut friendly_pawns = board.friendly_pieces() & !board.state.kings;
555
556 while friendly_pawns != 0u64 {
557 let src_mask = friendly_pawns & friendly_pawns.wrapping_neg(); Self::generate_pawn_captures_at::<TEAM_INDEX, 0>(
561 board,
562 actions,
563 max_length,
564 src_mask,
565 Action::EMPTY,
566 );
567
568 friendly_pawns ^= src_mask; }
570 }
571
572 #[inline]
586 fn generate_pawn_captures_at<const TEAM_INDEX: usize, const PREVIOUS_DIRECTION: i8>(
587 board: &mut Self,
588 actions: &mut Vec<Action>,
589 max_length: &mut u32,
590 src_mask: u64,
591 previous_action: Action,
592 ) {
593 let mut has_more_captures = false;
594
595 let hostile_pieces = board.hostile_pieces();
596 let empty = board.state.empty();
597
598 if PREVIOUS_DIRECTION != 1 {
601 let left_capture_mask =
602 ((src_mask & !(MASK_COL_A | MASK_COL_B)) >> 1u8) & hostile_pieces;
603 let left_dest_mask = ((src_mask & !(MASK_COL_A | MASK_COL_B)) >> 2u8) & empty;
604 if left_capture_mask != 0u64 && left_dest_mask != 0u64 {
605 has_more_captures = true;
606
607 let capture_action = Action::new_capture_as_pawn::<TEAM_INDEX>(
608 src_mask,
609 left_dest_mask,
610 left_capture_mask,
611 board.state.kings,
612 );
613
614 board.apply_(&capture_action);
616
617 Self::generate_pawn_captures_at::<TEAM_INDEX, -1>(
618 board,
619 actions,
620 max_length,
621 left_dest_mask,
622 previous_action.combine(&capture_action),
623 );
624
625 board.apply_(&capture_action);
627 }
628 }
629
630 if PREVIOUS_DIRECTION != -1 {
633 let right_capture_mask =
634 ((src_mask & !(MASK_COL_G | MASK_COL_H)) << 1u8) & hostile_pieces;
635 let right_dest_mask = ((src_mask & !(MASK_COL_G | MASK_COL_H)) << 2u8) & empty;
636 if right_capture_mask != 0u64 && right_dest_mask != 0u64 {
637 has_more_captures = true;
638
639 let capture_action = Action::new_capture_as_pawn::<TEAM_INDEX>(
640 src_mask,
641 right_dest_mask,
642 right_capture_mask,
643 board.state.kings,
644 );
645
646 board.apply_(&capture_action);
648
649 Self::generate_pawn_captures_at::<TEAM_INDEX, 1>(
650 board,
651 actions,
652 max_length,
653 right_dest_mask,
654 previous_action.combine(&capture_action),
655 );
656
657 board.apply_(&capture_action);
659 }
660 }
661
662 let (vert_capture_mask, vert_dest_mask): (u64, u64) = if TEAM_INDEX == 0 {
666 let mask = !(MASK_ROW_7 | MASK_ROW_8);
667 (
668 ((src_mask & mask) << 8u8) & hostile_pieces,
669 ((src_mask & mask) << 16u8) & empty,
670 )
671 } else {
672 let mask = !(MASK_ROW_1 | MASK_ROW_2);
673 (
674 ((src_mask & mask) >> 8u8) & hostile_pieces,
675 ((src_mask & mask) >> 16u8) & empty,
676 )
677 };
678
679 if vert_capture_mask != 0u64 && vert_dest_mask != 0u64 {
682 has_more_captures = true;
683 let capture_action = Action::new_capture_as_pawn::<TEAM_INDEX>(
684 src_mask,
685 vert_dest_mask,
686 vert_capture_mask,
687 board.state.kings,
688 );
689
690 board.apply_(&capture_action);
691
692 if TEAM_INDEX == 0 {
694 Self::generate_pawn_captures_at::<TEAM_INDEX, 8>(
695 board,
696 actions,
697 max_length,
698 vert_dest_mask,
699 previous_action.combine(&capture_action),
700 );
701 } else {
702 Self::generate_pawn_captures_at::<TEAM_INDEX, -8>(
703 board,
704 actions,
705 max_length,
706 vert_dest_mask,
707 previous_action.combine(&capture_action),
708 );
709 }
710
711 board.apply_(&capture_action);
712 }
713
714 if !has_more_captures && !previous_action.is_empty() {
715 let mut final_action = previous_action;
718 let promotion_mask = MASK_ROW_PROMOTIONS[TEAM_INDEX];
719 if src_mask & promotion_mask != 0 {
720 final_action.delta.kings ^= src_mask;
722 }
723 Self::push_capture_action::<TEAM_INDEX>(actions, max_length, final_action);
724 }
725 }
726
727 #[inline]
728 fn generate_king_captures_with_board<const TEAM_INDEX: usize>(
729 board: &mut Self,
730 actions: &mut Vec<Action>,
731 max_length: &mut u32,
732 ) {
733 let mut friendly_kings = board.friendly_pieces() & board.state.kings;
734
735 while friendly_kings != 0u64 {
736 let src_mask = friendly_kings & friendly_kings.wrapping_neg(); Self::generate_king_captures_at::<TEAM_INDEX, 0i8>(
739 board,
740 actions,
741 max_length,
742 src_mask,
743 Action::EMPTY,
744 );
745
746 friendly_kings ^= src_mask; }
748 }
749
750 #[inline]
751 fn generate_king_captures_at<const TEAM_INDEX: usize, const PREVIOUS_DIRECTION: i8>(
752 board: &mut Self,
753 actions: &mut Vec<Action>,
754 max_length: &mut u32,
755 src_mask: u64,
756 previous_action: Action,
757 ) {
758 let mut has_more_captures = false;
759
760 let friendly_pieces = board.friendly_pieces();
761 let hostile_pieces = board.hostile_pieces();
762
763 let src_index = src_mask.trailing_zeros() as usize;
764
765 if PREVIOUS_DIRECTION != 1 && (hostile_pieces & LEFT_RAY[src_index]) != 0 {
771 board
772 .gen_inner::<-1i8, PREVIOUS_DIRECTION, TEAM_INDEX, { !(MASK_COL_A | MASK_COL_B) }>(
773 src_mask,
774 src_index as u8,
775 friendly_pieces,
776 hostile_pieces,
777 &mut has_more_captures,
778 actions,
779 max_length,
780 previous_action,
781 );
782 }
783
784 if PREVIOUS_DIRECTION != -1 && (hostile_pieces & RIGHT_RAY[src_index]) != 0 {
786 board.gen_inner::<1i8, PREVIOUS_DIRECTION, TEAM_INDEX, { !(MASK_COL_G | MASK_COL_H) }>(
787 src_mask,
788 src_index as u8,
789 friendly_pieces,
790 hostile_pieces,
791 &mut has_more_captures,
792 actions,
793 max_length,
794 previous_action,
795 );
796 }
797
798 if PREVIOUS_DIRECTION != -8 && (hostile_pieces & UP_RAY[src_index]) != 0 {
800 board.gen_inner::<8i8, PREVIOUS_DIRECTION, TEAM_INDEX, { !(MASK_ROW_7 | MASK_ROW_8) }>(
801 src_mask,
802 src_index as u8,
803 friendly_pieces,
804 hostile_pieces,
805 &mut has_more_captures,
806 actions,
807 max_length,
808 previous_action,
809 );
810 }
811
812 if PREVIOUS_DIRECTION != 8 && (hostile_pieces & DOWN_RAY[src_index]) != 0 {
814 board
815 .gen_inner::<-8i8, PREVIOUS_DIRECTION, TEAM_INDEX, { !(MASK_ROW_1 | MASK_ROW_2) }>(
816 src_mask,
817 src_index as u8,
818 friendly_pieces,
819 hostile_pieces,
820 &mut has_more_captures,
821 actions,
822 max_length,
823 previous_action,
824 );
825 }
826
827 if !has_more_captures && !previous_action.is_empty() {
828 Self::push_capture_action::<TEAM_INDEX>(actions, max_length, previous_action);
829 }
830 }
831
832 #[allow(clippy::too_many_arguments)] #[inline(always)]
834 fn gen_inner<
835 const DIRECTION: i8,
836 const PREVIOUS_DIRECTION: i8,
837 const TEAM_INDEX: usize,
838 const CHECKMASK: u64,
839 >(
840 &mut self,
841 src_mask: u64,
842 src_index: u8,
843 friendly_pieces: u64,
844 hostile_pieces: u64,
845 has_more_captures: &mut bool,
846 actions: &mut Vec<Action>,
847 max_length: &mut u32,
848 previous_action: Action,
849 ) {
850 if (CHECKMASK >> src_index) & 1u64 != 1u64 {
855 return;
856 }
857
858 let mut temp_index = src_index as i8 + DIRECTION;
859 const NO_CAPTURE: u8 = 255; let mut capture_index: u8 = NO_CAPTURE;
861 let mut possible_move_indices: [u8; 7] = [0; 7]; let mut possible_move_count: usize = 0;
863
864 #[allow(clippy::manual_range_contains)]
866 while temp_index >= 0 && temp_index <= 63 {
867 let temp_index_mask = 1u64 << temp_index;
868 if friendly_pieces & temp_index_mask != 0 {
869 break;
871 }
872 if hostile_pieces & temp_index_mask != 0 {
873 if capture_index != NO_CAPTURE {
874 break;
877 }
878 capture_index = temp_index as u8;
880 } else if capture_index != NO_CAPTURE {
881 #[allow(clippy::cast_sign_loss)] {
884 possible_move_indices[possible_move_count] = temp_index as u8;
885 }
886 possible_move_count += 1;
887 }
888
889 if DIRECTION == 1 && temp_index % 8 == 7 {
892 break; }
894 if DIRECTION == -1 && temp_index % 8 == 0 {
895 break; }
897
898 temp_index += DIRECTION;
899 }
900
901 if possible_move_count == 0 {
902 return;
903 }
904 let capture_index_mask = 1u64 << capture_index;
908
909 for &possible_move_index in &possible_move_indices[..possible_move_count] {
910 let dest_mask = 1u64 << possible_move_index;
911 *has_more_captures = true;
912
913 let capture_action = Action::new_capture_as_king::<TEAM_INDEX>(
914 src_mask,
915 dest_mask,
916 capture_index_mask,
917 self.state.kings,
918 );
919
920 self.apply_(&capture_action);
922
923 Self::generate_king_captures_at::<TEAM_INDEX, DIRECTION>(
924 self,
925 actions,
926 max_length,
927 dest_mask,
928 previous_action.combine(&capture_action),
929 );
930
931 self.apply_(&capture_action);
933 }
934 }
935
936 #[inline]
937 fn generate_moves<const TEAM_INDEX: usize>(&self, actions: &mut Vec<Action>) {
938 let empty = self.state.empty();
939 self.generate_king_moves::<TEAM_INDEX>(actions, empty);
940 self.generate_pawn_moves::<TEAM_INDEX>(actions, empty);
941 }
942
943 #[inline]
944 fn generate_pawn_moves<const TEAM_INDEX: usize>(&self, actions: &mut Vec<Action>, empty: u64) {
945 let mut friendly_pawns = self.friendly_pieces() & !self.state.kings;
946 while friendly_pawns != 0u64 {
947 let src_mask = friendly_pawns & friendly_pawns.wrapping_neg(); let src_index: usize = src_mask.trailing_zeros() as usize;
949 let mut possible_dest_masks = if TEAM_INDEX == 0 {
950 WHITE_PAWN_MOVES[src_index] & empty
951 } else {
952 BLACK_PAWN_MOVES[src_index] & empty
953 };
954
955 while possible_dest_masks != 0u64 {
956 let dest_mask = possible_dest_masks & possible_dest_masks.wrapping_neg(); actions.push(Action::new_move_as_pawn::<TEAM_INDEX>(src_mask, dest_mask));
958 possible_dest_masks ^= dest_mask; }
960
961 friendly_pawns ^= src_mask; }
963 }
964
965 #[inline]
968 fn generate_king_moves<const TEAM_INDEX: usize>(&self, actions: &mut Vec<Action>, empty: u64) {
969 let occupied = !empty;
970 let mut friendly_kings = self.friendly_pieces() & self.state.kings;
971
972 while friendly_kings != 0u64 {
973 let src_mask = friendly_kings & friendly_kings.wrapping_neg();
974 let sq = src_mask.trailing_zeros() as usize;
975
976 let attacks = Self::king_attacks_lut(sq, occupied);
978 let mut moves = attacks & empty;
979
980 while moves != 0u64 {
982 let dest_mask = moves & moves.wrapping_neg();
983 actions.push(Action::new_move_as_king::<TEAM_INDEX>(src_mask, dest_mask));
984 moves ^= dest_mask;
985 }
986
987 friendly_kings ^= src_mask;
988 }
989 }
990}
991
992#[cfg(test)]
993mod tests {
994 use super::*;
995 use crate::game_status::GameStatus;
996 use crate::Square;
997
998 #[test]
1001 fn initial_position_action_count() {
1002 let board = Board::new_default();
1003 let actions = board.actions();
1004 assert!(!actions.is_empty(), "Initial position should have moves");
1009 assert!(
1011 !actions.is_empty(),
1012 "Should have moves from initial position"
1013 );
1014 }
1015
1016 #[test]
1017 fn initial_position_only_moves_no_captures() {
1018 let board = Board::new_default();
1019 let actions = board.actions();
1020 for action in &actions {
1022 assert_eq!(
1024 action.delta.pieces[Team::Black.to_usize()],
1025 0,
1026 "Initial position should only have moves, not captures"
1027 );
1028 }
1029 }
1030
1031 #[test]
1034 fn forced_capture_rule() {
1035 let board = Board::from_squares(Team::White, &[Square::D4], &[Square::D5, Square::H8], &[]);
1038 let actions = board.actions();
1039
1040 assert_eq!(actions.len(), 1, "Should have exactly one capture");
1041 assert_ne!(
1043 actions[0].delta.pieces[Team::Black.to_usize()],
1044 0,
1045 "Action should be a capture"
1046 );
1047 }
1048
1049 #[test]
1050 fn multiple_capture_options() {
1051 let board = Board::from_squares(
1053 Team::White,
1054 &[Square::D4],
1055 &[Square::C4, Square::E4, Square::D5],
1056 &[],
1057 );
1058 let actions = board.actions();
1059
1060 assert_eq!(actions.len(), 3, "Should have three capture options");
1061 for action in &actions {
1062 assert_ne!(
1063 action.delta.pieces[Team::Black.to_usize()],
1064 0,
1065 "All actions should be captures"
1066 );
1067 }
1068 }
1069
1070 #[test]
1073 fn maximum_capture_rule_prefers_longer_chain() {
1074 let board = Board::from_squares(
1078 Team::White,
1079 &[Square::A4],
1080 &[Square::B4, Square::A5, Square::B6],
1081 &[],
1082 );
1083 let actions = board.actions();
1084
1085 for action in &actions {
1087 let capture_count = action.delta.pieces[Team::Black.to_usize()].count_ones();
1088 assert_eq!(
1089 capture_count, 2,
1090 "Should only return maximum length captures (2)"
1091 );
1092 }
1093 }
1094
1095 #[test]
1096 fn maximum_capture_multiple_equal_length() {
1097 let board = Board::from_squares(Team::White, &[Square::D4], &[Square::D5, Square::E4], &[]);
1099 let actions = board.actions();
1100
1101 assert_eq!(
1103 actions.len(),
1104 2,
1105 "Should have two equal-length capture options"
1106 );
1107 }
1108
1109 #[test]
1112 fn king_can_slide_multiple_squares() {
1113 let board = Board::from_squares(
1115 Team::White,
1116 &[Square::D4],
1117 &[],
1118 &[Square::D4], );
1120 let actions = board.actions();
1121
1122 assert_eq!(actions.len(), 14, "King at D4 should have 14 moves");
1129 }
1130
1131 #[test]
1132 fn king_blocked_by_friendly_piece() {
1133 let board = Board::from_squares(
1135 Team::White,
1136 &[Square::D4, Square::D6],
1137 &[],
1138 &[Square::D4], );
1140 let actions = board.actions();
1141
1142 let king_up_moves: Vec<_> = actions
1146 .iter()
1147 .filter(|a| {
1148 let dest = a.delta.pieces[Team::White.to_usize()] & !Square::D4.to_mask();
1149 dest == Square::D5.to_mask()
1150 })
1151 .collect();
1152 assert_eq!(
1153 king_up_moves.len(),
1154 1,
1155 "King should only be able to move to D5 going up"
1156 );
1157 }
1158
1159 #[test]
1162 fn king_can_capture_from_distance() {
1163 let board = Board::from_squares(
1165 Team::White,
1166 &[Square::A4],
1167 &[Square::D4],
1168 &[Square::A4], );
1170 let actions = board.actions();
1171
1172 assert_eq!(
1174 actions.len(),
1175 4,
1176 "King should have 4 landing options after capture"
1177 );
1178 for action in &actions {
1179 assert_eq!(
1180 action.delta.pieces[Team::Black.to_usize()],
1181 Square::D4.to_mask(),
1182 "Should capture D4"
1183 );
1184 }
1185 }
1186
1187 #[test]
1188 fn king_multi_direction_capture() {
1189 let board = Board::from_squares(
1191 Team::White,
1192 &[Square::D4],
1193 &[Square::B4, Square::D6],
1194 &[Square::D4],
1195 );
1196 let actions = board.actions();
1197
1198 assert!(
1200 actions.len() >= 2,
1201 "King should be able to capture in multiple directions"
1202 );
1203 }
1204
1205 #[test]
1208 fn white_pawn_moves_forward_and_sideways() {
1209 let board = Board::from_squares(Team::White, &[Square::D4], &[], &[]);
1210 let actions = board.actions();
1211
1212 assert_eq!(actions.len(), 3, "Pawn at D4 should have 3 moves");
1214 }
1215
1216 #[test]
1217 fn black_pawn_moves_forward_and_sideways() {
1218 let board = Board::from_squares(Team::Black, &[], &[Square::D5], &[]);
1219 let actions = board.actions();
1220
1221 assert_eq!(actions.len(), 3, "Black pawn at D5 should have 3 moves");
1223 }
1224
1225 #[test]
1226 fn pawn_at_edge_has_fewer_moves() {
1227 let board = Board::from_squares(Team::White, &[Square::A4], &[], &[]);
1228 let actions = board.actions();
1229
1230 assert_eq!(actions.len(), 2, "Pawn at A4 should have 2 moves");
1232 }
1233
1234 #[test]
1237 fn pawn_promotes_on_last_rank() {
1238 let board = Board::from_squares(Team::White, &[Square::D7], &[], &[]);
1239 let actions = board.actions();
1240
1241 let promotion_action = actions
1243 .iter()
1244 .find(|a| a.delta.pieces[Team::White.to_usize()] & Square::D8.to_mask() != 0);
1245
1246 assert!(promotion_action.is_some(), "Should be able to move to D8");
1247 let action = promotion_action.unwrap();
1248 assert_ne!(
1249 action.delta.kings & Square::D8.to_mask(),
1250 0,
1251 "Pawn should promote to king at D8"
1252 );
1253 }
1254
1255 #[test]
1256 fn pawn_promotes_during_capture() {
1257 let board = Board::from_squares(Team::White, &[Square::B6], &[Square::B7], &[]);
1260 let actions = board.actions();
1261
1262 assert_eq!(actions.len(), 1, "Should have one capture");
1263 let action = &actions[0];
1264 assert_ne!(
1265 action.delta.kings & Square::B8.to_mask(),
1266 0,
1267 "Pawn should promote after capture landing on B8"
1268 );
1269 }
1270
1271 #[test]
1274 fn no_pieces_no_actions() {
1275 let board = Board::from_squares(Team::White, &[], &[Square::D4], &[]);
1276 let actions = board.actions();
1277 assert!(actions.is_empty(), "No friendly pieces means no actions");
1278 }
1279
1280 #[test]
1281 fn completely_blocked_pawn_can_capture() {
1282 let board = Board::from_squares(
1285 Team::White,
1286 &[Square::B2],
1287 &[Square::A2, Square::B3, Square::C2],
1288 &[],
1289 );
1290 let actions = board.actions();
1291 assert!(
1293 !actions.is_empty(),
1294 "Surrounded pawn can capture adjacent enemies"
1295 );
1296 for action in &actions {
1297 assert_ne!(
1298 action.delta.pieces[Team::Black.to_usize()],
1299 0,
1300 "Actions should be captures"
1301 );
1302 }
1303 }
1304
1305 #[test]
1306 fn truly_blocked_pawn() {
1307 let board = Board::from_squares(
1309 Team::White,
1310 &[Square::B2, Square::A2, Square::B3, Square::C2],
1311 &[Square::H8],
1312 &[],
1313 );
1314 let actions = board.actions();
1315 let b2_moves: Vec<_> = actions
1321 .iter()
1322 .filter(|a| {
1323 let delta = a.delta.pieces[Team::White.to_usize()];
1324 delta & Square::B2.to_mask() != 0
1325 })
1326 .collect();
1327 assert!(b2_moves.is_empty(), "B2 pawn should have no moves");
1328 }
1329
1330 #[test]
1333 fn pawn_chain_capture_two_pieces() {
1334 let board = Board::from_squares(Team::White, &[Square::A4], &[Square::A5, Square::B6], &[]);
1337 let actions = board.actions();
1338
1339 assert!(!actions.is_empty(), "Should have capture chain");
1341 for action in &actions {
1342 let capture_count = action.delta.pieces[Team::Black.to_usize()].count_ones();
1343 assert_eq!(capture_count, 2, "Should capture 2 pieces in chain");
1344 }
1345 }
1346
1347 #[test]
1348 fn pawn_chain_capture_three_pieces() {
1349 let board = Board::from_squares(
1352 Team::White,
1353 &[Square::A2],
1354 &[Square::A3, Square::B4, Square::C5],
1355 &[],
1356 );
1357 let actions = board.actions();
1358
1359 assert!(!actions.is_empty(), "Should have capture chain");
1360 for action in &actions {
1361 let capture_count = action.delta.pieces[Team::Black.to_usize()].count_ones();
1362 assert_eq!(capture_count, 3, "Should capture 3 pieces in chain");
1363 }
1364 }
1365
1366 #[test]
1367 fn king_chain_capture() {
1368 let board = Board::from_squares(
1371 Team::White,
1372 &[Square::A4],
1373 &[Square::C4, Square::E6],
1374 &[Square::A4],
1375 );
1376 let actions = board.actions();
1377
1378 assert!(!actions.is_empty(), "King should have capture options");
1380 let max_captures = actions
1381 .iter()
1382 .map(|a| a.delta.pieces[Team::Black.to_usize()].count_ones())
1383 .max()
1384 .unwrap();
1385 assert_eq!(max_captures, 2, "King should capture 2 pieces in chain");
1386 }
1387
1388 #[test]
1391 fn king_cannot_pass_through_friendly() {
1392 let board = Board::from_squares(
1395 Team::White,
1396 &[Square::A4, Square::C4],
1397 &[Square::H8],
1398 &[Square::A4],
1399 );
1400 let actions = board.actions();
1401
1402 let right_moves: Vec<_> = actions
1404 .iter()
1405 .filter(|a| {
1406 let src = Square::A4.to_mask();
1407 let delta = a.delta.pieces[Team::White.to_usize()];
1408 delta & src != 0 && {
1410 let dest = delta & !src;
1411 let dest_sq = unsafe { Square::from_mask(dest) };
1414 dest_sq.row() == 3 && dest_sq.column() > 0
1415 }
1416 })
1417 .collect();
1418
1419 assert_eq!(
1420 right_moves.len(),
1421 1,
1422 "King should only reach B4 going right"
1423 );
1424 }
1425
1426 #[test]
1427 fn king_cannot_jump_over_hostile_without_capturing() {
1428 let board = Board::from_squares(Team::White, &[Square::A4], &[Square::C4], &[Square::A4]);
1431 let actions = board.actions();
1432
1433 for action in &actions {
1435 let dest = action.delta.pieces[Team::White.to_usize()] & !Square::A4.to_mask();
1436 if dest != 0 {
1437 let dest_sq = unsafe { Square::from_mask(dest) };
1439 if dest_sq.row() == 3 && dest_sq.column() >= 3 {
1440 assert_ne!(
1442 action.delta.pieces[Team::Black.to_usize()],
1443 0,
1444 "Moving past hostile must be a capture"
1445 );
1446 }
1447 }
1448 }
1449 }
1450
1451 #[test]
1454 fn black_pawn_captures() {
1455 let board = Board::from_squares(Team::Black, &[Square::A1], &[Square::D5], &[]);
1456 let actions = board.actions();
1457
1458 assert_eq!(actions.len(), 3, "Black pawn at D5 should have 3 moves");
1461 }
1462
1463 #[test]
1464 fn black_king_movement() {
1465 let board = Board::from_squares(Team::Black, &[Square::A1], &[Square::D4], &[Square::D4]);
1466 let actions = board.actions();
1467
1468 assert_eq!(actions.len(), 14, "Black king at D4 should have 14 moves");
1470 }
1471
1472 #[test]
1475 fn multiple_pieces_all_can_move() {
1476 let board = Board::from_squares(Team::White, &[Square::A4, Square::H4], &[Square::D8], &[]);
1477 let actions = board.actions();
1478
1479 assert_eq!(actions.len(), 4, "Both pawns should contribute moves");
1483 }
1484
1485 #[test]
1486 fn multiple_pieces_one_must_capture() {
1487 let board = Board::from_squares(Team::White, &[Square::A4, Square::H4], &[Square::A5], &[]);
1490 let actions = board.actions();
1491
1492 assert_eq!(actions.len(), 1, "Only capture should be returned");
1493 assert_ne!(
1494 actions[0].delta.pieces[Team::Black.to_usize()],
1495 0,
1496 "Action should be a capture"
1497 );
1498 }
1499
1500 #[test]
1503 fn black_pawn_promotes_on_row_1() {
1504 let board = Board::from_squares(Team::Black, &[Square::H8], &[Square::D2], &[]);
1505 let actions = board.actions();
1506
1507 let promotion = actions
1509 .iter()
1510 .find(|a| a.delta.pieces[Team::Black.to_usize()] & Square::D1.to_mask() != 0);
1511
1512 assert!(promotion.is_some(), "Should be able to move to D1");
1513 assert_ne!(
1514 promotion.unwrap().delta.kings & Square::D1.to_mask(),
1515 0,
1516 "Black pawn should promote at D1"
1517 );
1518 }
1519
1520 #[test]
1521 fn pawn_capture_leads_to_promotion() {
1522 let board = Board::from_squares(Team::White, &[Square::B6], &[Square::B7], &[]);
1524 let actions = board.actions();
1525
1526 assert_eq!(actions.len(), 1, "Should have one capture");
1527 let action = &actions[0];
1528
1529 assert_eq!(
1531 action.delta.pieces[Team::Black.to_usize()],
1532 Square::B7.to_mask(),
1533 "Should capture B7"
1534 );
1535
1536 assert_ne!(
1538 action.delta.kings & Square::B8.to_mask(),
1539 0,
1540 "Pawn should promote at B8"
1541 );
1542 }
1543
1544 #[test]
1547 fn pawn_in_corner() {
1548 let board = Board::from_squares(
1552 Team::Black,
1553 &[Square::A8],
1554 &[Square::H1],
1555 &[Square::H1], );
1557 let actions = board.actions();
1558
1559 assert_eq!(actions.len(), 14, "King at H1 should have 14 moves");
1564 }
1565
1566 #[test]
1575 fn king_cannot_reverse_up_down_during_capture() {
1576 let board = Board::from_squares(
1612 Team::White,
1613 &[Square::D5],
1614 &[Square::D3, Square::D7],
1615 &[Square::D5], );
1617 let actions = board.actions();
1618
1619 for action in &actions {
1624 let capture_count = action.delta.pieces[Team::Black.to_usize()].count_ones();
1625 assert_eq!(
1626 capture_count, 1,
1627 "King should NOT chain captures that require 180° turn (up then down)"
1628 );
1629 }
1630 }
1631
1632 #[test]
1633 fn king_cannot_reverse_left_right_during_capture() {
1634 let board = Board::from_squares(
1639 Team::White,
1640 &[Square::D4],
1641 &[Square::B4, Square::F4],
1642 &[Square::D4], );
1644 let actions = board.actions();
1645
1646 for action in &actions {
1648 let capture_count = action.delta.pieces[Team::Black.to_usize()].count_ones();
1649 assert_eq!(
1650 capture_count, 1,
1651 "King should NOT chain captures that require 180° turn (left then right)"
1652 );
1653 }
1654 }
1655
1656 #[test]
1657 fn king_can_turn_90_degrees_during_capture() {
1658 let board = Board::from_squares(
1663 Team::White,
1664 &[Square::D4],
1665 &[Square::D6, Square::F7],
1666 &[Square::D4],
1667 );
1668 let actions = board.actions();
1669
1670 let max_captures = actions
1672 .iter()
1673 .map(|a| a.delta.pieces[Team::Black.to_usize()].count_ones())
1674 .max()
1675 .unwrap_or(0);
1676
1677 assert_eq!(
1678 max_captures, 2,
1679 "King should be able to chain captures with 90° turn"
1680 );
1681 }
1682
1683 #[test]
1684 fn king_180_restriction_complex_scenario() {
1685 let board = Board::from_squares(
1691 Team::White,
1692 &[Square::A1],
1693 &[Square::A3, Square::C5, Square::E3],
1694 &[Square::A1],
1695 );
1696 let actions = board.actions();
1697
1698 let max_captures = actions
1700 .iter()
1701 .map(|a| a.delta.pieces[Team::Black.to_usize()].count_ones())
1702 .max()
1703 .unwrap_or(0);
1704
1705 assert_eq!(
1706 max_captures, 3,
1707 "King should chain 3 captures with 90° turns"
1708 );
1709 }
1710
1711 #[test]
1714 fn king_180_turn_prohibited_vertical() {
1715 let board = Board::from_squares(
1716 Team::Black,
1717 &[Square::D2, Square::D6],
1718 &[Square::D4],
1719 &[Square::D4, Square::D6],
1720 );
1721
1722 let actions = board.actions();
1723
1724 assert!(!actions.is_empty());
1725 for action in &actions {
1726 assert_eq!(
1727 action.capture_count(Team::Black),
1728 1,
1729 "180° turn should prevent chaining North->South or South->North captures"
1730 );
1731 }
1732 }
1733
1734 #[test]
1737 fn king_180_turn_prohibited_horizontal() {
1738 let board = Board::from_squares(
1739 Team::Black,
1740 &[Square::B4, Square::F4],
1741 &[Square::D4],
1742 &[Square::D4, Square::F4],
1743 );
1744
1745 let actions = board.actions();
1746
1747 assert!(!actions.is_empty());
1748 for action in &actions {
1749 assert_eq!(
1750 action.capture_count(Team::Black),
1751 1,
1752 "180° turn should prevent chaining East->West or West->East captures"
1753 );
1754 }
1755 }
1756
1757 #[test]
1761 fn can_cross_captured_square_pawn() {
1762 let board = Board::from_squares(Team::White, &[Square::A2], &[Square::A3, Square::B4], &[]);
1777 let actions = board.actions();
1778
1779 let max_captures = actions
1781 .iter()
1782 .map(|a| a.delta.pieces[Team::Black.to_usize()].count_ones())
1783 .max()
1784 .unwrap_or(0);
1785
1786 assert_eq!(max_captures, 2, "Pawn should chain 2 captures");
1787 }
1788
1789 #[test]
1790 fn king_can_cross_captured_square() {
1791 let board = Board::from_squares(
1797 Team::White,
1798 &[Square::A4],
1799 &[Square::C4, Square::E4],
1800 &[Square::A4],
1801 );
1802 let actions = board.actions();
1803
1804 let max_captures = actions
1806 .iter()
1807 .map(|a| a.delta.pieces[Team::Black.to_usize()].count_ones())
1808 .max()
1809 .unwrap_or(0);
1810
1811 assert_eq!(
1812 max_captures, 2,
1813 "King should be able to chain captures (immediate removal allows path)"
1814 );
1815 }
1816
1817 #[test]
1818 fn king_complex_crossing_pattern() {
1819 let board = Board::from_squares(
1848 Team::White,
1849 &[Square::A4],
1850 &[Square::C4, Square::E4, Square::G4],
1851 &[Square::A4],
1852 );
1853 let actions = board.actions();
1854
1855 let max_captures = actions
1860 .iter()
1861 .map(|a| a.delta.pieces[Team::Black.to_usize()].count_ones())
1862 .max()
1863 .unwrap_or(0);
1864
1865 assert_eq!(max_captures, 3, "King should chain 3 captures in a line");
1866 }
1867
1868 #[test]
1872 fn pawn_promotes_at_end_of_capture_sequence() {
1873 let board = Board::from_squares(Team::White, &[Square::D6], &[Square::D7], &[]);
1879 let actions = board.actions();
1880
1881 assert_eq!(actions.len(), 1, "Should have one capture");
1882 let action = &actions[0];
1883
1884 let dest = action.delta.pieces[Team::White.to_usize()] & !Square::D6.to_mask();
1886 assert_eq!(dest, Square::D8.to_mask(), "Should land on D8");
1887 assert_ne!(
1888 action.delta.kings & Square::D8.to_mask(),
1889 0,
1890 "Should promote to king at D8"
1891 );
1892 }
1893
1894 #[test]
1895 fn pawn_captures_and_promotes_when_ending_on_back_row() {
1896 let board = Board::from_squares(Team::White, &[Square::D6], &[Square::D7], &[]);
1901 let actions = board.actions();
1902
1903 assert_eq!(actions.len(), 1, "Should have one capture");
1904 let action = &actions[0];
1905
1906 let dest = action.delta.pieces[Team::White.to_usize()] & !Square::D6.to_mask();
1908 assert_eq!(dest, Square::D8.to_mask(), "Should land on D8");
1909 assert_ne!(
1910 action.delta.kings & Square::D8.to_mask(),
1911 0,
1912 "Should promote to king at D8"
1913 );
1914 }
1915
1916 #[test]
1917 fn pawn_continues_capturing_from_promotion_row() {
1918 let board = Board::from_squares(Team::White, &[Square::D6], &[Square::D7, Square::C8], &[]);
1927 let actions = board.actions();
1928
1929 assert_eq!(actions.len(), 1, "Should have one 2-capture action");
1932 let action = &actions[0];
1933
1934 let captures = action.delta.pieces[Team::Black.to_usize()];
1936 assert_eq!(
1937 captures,
1938 Square::D7.to_mask() | Square::C8.to_mask(),
1939 "Should capture both D7 and C8"
1940 );
1941
1942 let final_pos = action.delta.pieces[Team::White.to_usize()] & !Square::D6.to_mask();
1944 assert_eq!(final_pos, Square::B8.to_mask(), "Should end on B8");
1945
1946 assert_ne!(
1948 action.delta.kings & Square::B8.to_mask(),
1949 0,
1950 "Should promote to king at B8"
1951 );
1952 }
1953
1954 #[test]
1955 fn pawn_chain_capture_ending_on_promotion_row() {
1956 let board = Board::from_squares(Team::White, &[Square::A4], &[Square::A5, Square::B6], &[]);
1962 let actions = board.actions();
1963
1964 let max_captures = actions
1965 .iter()
1966 .map(|a| a.delta.pieces[Team::Black.to_usize()].count_ones())
1967 .max()
1968 .unwrap_or(0);
1969
1970 assert_eq!(max_captures, 2, "Should capture both in chain");
1971 }
1972
1973 #[test]
1982 fn max_capture_three_vs_two() {
1983 let board = Board::from_squares(
1989 Team::White,
1990 &[Square::A2],
1991 &[Square::A3, Square::B4, Square::C5],
1992 &[],
1993 );
1994 let actions = board.actions();
1995
1996 for action in &actions {
1998 let count = action.delta.pieces[Team::Black.to_usize()].count_ones();
1999 assert_eq!(count, 3, "Only 3-capture sequences should be returned");
2000 }
2001 }
2002
2003 #[test]
2004 fn max_capture_equal_length_all_returned() {
2005 let board = Board::from_squares(
2010 Team::White,
2011 &[Square::D4],
2012 &[Square::D5, Square::C4, Square::E4],
2013 &[],
2014 );
2015 let actions = board.actions();
2016
2017 assert_eq!(
2019 actions.len(),
2020 3,
2021 "Should have 3 equal-length capture options"
2022 );
2023 for action in &actions {
2024 let count = action.delta.pieces[Team::Black.to_usize()].count_ones();
2025 assert_eq!(count, 1, "All captures should be length 1");
2026 }
2027 }
2028
2029 #[test]
2030 fn max_capture_king_vs_pawn_count_equally() {
2031 let board = Board::from_squares(
2037 Team::White,
2038 &[Square::D4],
2039 &[Square::D5, Square::C4, Square::B5],
2040 &[Square::D5], );
2042 let actions = board.actions();
2043
2044 let max_captures = actions
2045 .iter()
2046 .map(|a| a.delta.pieces[Team::Black.to_usize()].count_ones())
2047 .max()
2048 .unwrap_or(0);
2049
2050 assert_eq!(
2051 max_captures, 2,
2052 "Should prefer 2-pawn capture over 1-king capture"
2053 );
2054 }
2055
2056 #[test]
2060 fn white_pawn_cannot_move_backward() {
2061 let board = Board::from_squares(Team::White, &[Square::D4], &[Square::H8], &[]);
2064 let actions = board.actions();
2065
2066 let backward_move = actions.iter().find(|a| {
2067 let dest = a.delta.pieces[Team::White.to_usize()] & !Square::D4.to_mask();
2068 dest == Square::D3.to_mask()
2069 });
2070
2071 assert!(
2072 backward_move.is_none(),
2073 "White pawn should not move backward to D3"
2074 );
2075 }
2076
2077 #[test]
2078 fn black_pawn_cannot_move_backward() {
2079 let board = Board::from_squares(Team::Black, &[Square::H1], &[Square::D5], &[]);
2082 let actions = board.actions();
2083
2084 let backward_move = actions.iter().find(|a| {
2085 let dest = a.delta.pieces[Team::Black.to_usize()] & !Square::D5.to_mask();
2086 dest == Square::D6.to_mask()
2087 });
2088
2089 assert!(
2090 backward_move.is_none(),
2091 "Black pawn should not move backward to D6"
2092 );
2093 }
2094
2095 #[test]
2096 fn pawn_cannot_move_diagonally() {
2097 let board = Board::from_squares(Team::White, &[Square::D4], &[Square::H8], &[]);
2099 let actions = board.actions();
2100
2101 let diagonal_squares = [Square::C3, Square::C5, Square::E3, Square::E5];
2102 for sq in &diagonal_squares {
2103 let diag_move = actions.iter().find(|a| {
2104 let dest = a.delta.pieces[Team::White.to_usize()] & !Square::D4.to_mask();
2105 dest == sq.to_mask()
2106 });
2107 assert!(
2108 diag_move.is_none(),
2109 "Pawn should not move diagonally to {sq:?}"
2110 );
2111 }
2112 }
2113
2114 #[test]
2115 fn pawn_moves_exactly_one_square() {
2116 let board = Board::from_squares(Team::White, &[Square::D4], &[Square::H8], &[]);
2118 let actions = board.actions();
2119
2120 let valid_dests = [Square::C4, Square::E4, Square::D5];
2123 let invalid_dests = [Square::B4, Square::F4, Square::D6];
2124
2125 for sq in &valid_dests {
2126 let found = actions.iter().any(|a| {
2127 let dest = a.delta.pieces[Team::White.to_usize()] & !Square::D4.to_mask();
2128 dest == sq.to_mask()
2129 });
2130 assert!(found, "Pawn should be able to move to {sq:?}");
2131 }
2132
2133 for sq in &invalid_dests {
2134 let found = actions.iter().any(|a| {
2135 let dest = a.delta.pieces[Team::White.to_usize()] & !Square::D4.to_mask();
2136 dest == sq.to_mask()
2137 });
2138 assert!(!found, "Pawn should NOT move 2 squares to {sq:?}");
2139 }
2140 }
2141
2142 #[test]
2146 fn king_moves_multiple_squares_all_directions() {
2147 let board = Board::from_squares(Team::White, &[Square::D4], &[Square::H8], &[Square::D4]);
2149 let actions = board.actions();
2150
2151 assert_eq!(actions.len(), 14, "King at D4 should have 14 moves");
2155 }
2156
2157 #[test]
2158 fn king_cannot_move_diagonally() {
2159 let board = Board::from_squares(Team::White, &[Square::D4], &[Square::H8], &[Square::D4]);
2160 let actions = board.actions();
2161
2162 let diagonal_squares = [
2163 Square::A1,
2164 Square::B2,
2165 Square::C3,
2166 Square::E5,
2167 Square::F6,
2168 Square::G7,
2169 Square::A7,
2170 Square::B6,
2171 Square::C5,
2172 Square::E3,
2173 Square::F2,
2174 Square::G1,
2175 ];
2176
2177 for sq in &diagonal_squares {
2178 let diag_move = actions.iter().find(|a| {
2179 let dest = a.delta.pieces[Team::White.to_usize()] & !Square::D4.to_mask();
2180 dest == sq.to_mask()
2181 });
2182 assert!(
2183 diag_move.is_none(),
2184 "King should not move diagonally to {sq:?}"
2185 );
2186 }
2187 }
2188
2189 #[test]
2193 fn white_pawn_cannot_capture_backward() {
2194 let board = Board::from_squares(
2197 Team::White,
2198 &[Square::D4],
2199 &[Square::D3, Square::D5], &[],
2201 );
2202 let actions = board.actions();
2203
2204 assert_eq!(actions.len(), 1, "Should have only 1 capture");
2206 assert_eq!(
2207 actions[0].delta.pieces[Team::Black.to_usize()],
2208 Square::D5.to_mask(),
2209 "Should capture D5 (forward), not D3 (backward)"
2210 );
2211 }
2212
2213 #[test]
2214 fn black_pawn_cannot_capture_backward() {
2215 let board = Board::from_squares(
2217 Team::Black,
2218 &[Square::D6, Square::D4], &[Square::D5],
2220 &[],
2221 );
2222 let actions = board.actions();
2223
2224 assert_eq!(actions.len(), 1, "Should have only 1 capture");
2226 assert_eq!(
2227 actions[0].delta.pieces[Team::White.to_usize()],
2228 Square::D4.to_mask(),
2229 "Should capture D4 (forward for black), not D6 (backward)"
2230 );
2231 }
2232
2233 #[test]
2234 fn pawn_cannot_capture_diagonally() {
2235 let board = Board::from_squares(
2237 Team::White,
2238 &[Square::D4],
2239 &[Square::C3, Square::C5, Square::E3, Square::E5],
2240 &[],
2241 );
2242 let actions = board.actions();
2243
2244 for action in &actions {
2247 assert_eq!(
2248 action.delta.pieces[Team::Black.to_usize()],
2249 0,
2250 "Should not capture diagonal enemies"
2251 );
2252 }
2253 }
2254
2255 #[test]
2259 fn king_capture_from_distance() {
2260 let board = Board::from_squares(Team::White, &[Square::A4], &[Square::E4], &[Square::A4]);
2263 let actions = board.actions();
2264
2265 assert_eq!(actions.len(), 3, "King should have 3 landing options");
2267
2268 for action in &actions {
2269 assert_eq!(
2270 action.delta.pieces[Team::Black.to_usize()],
2271 Square::E4.to_mask(),
2272 "All captures should take E4"
2273 );
2274 }
2275 }
2276
2277 #[test]
2278 fn king_cannot_capture_two_in_line() {
2279 let board = Board::from_squares(
2282 Team::White,
2283 &[Square::A4],
2284 &[Square::C4, Square::E4],
2285 &[Square::A4],
2286 );
2287 let actions = board.actions();
2288
2289 let max_captures = actions
2293 .iter()
2294 .map(|a| a.delta.pieces[Team::Black.to_usize()].count_ones())
2295 .max()
2296 .unwrap_or(0);
2297
2298 assert_eq!(
2299 max_captures, 2,
2300 "Should capture both in chain, not single jump"
2301 );
2302 }
2303
2304 #[test]
2308 fn must_capture_when_available() {
2309 let board = Board::from_squares(Team::White, &[Square::D4], &[Square::D5], &[]);
2312 let actions = board.actions();
2313
2314 for action in &actions {
2316 assert_ne!(
2317 action.delta.pieces[Team::Black.to_usize()],
2318 0,
2319 "Must capture when capture is available"
2320 );
2321 }
2322 }
2323
2324 #[test]
2325 fn moves_allowed_when_no_capture() {
2326 let board = Board::from_squares(
2328 Team::White,
2329 &[Square::D4],
2330 &[Square::H8], &[],
2332 );
2333 let actions = board.actions();
2334
2335 for action in &actions {
2337 assert_eq!(
2338 action.delta.pieces[Team::Black.to_usize()],
2339 0,
2340 "Should be moves, not captures"
2341 );
2342 }
2343 assert!(!actions.is_empty(), "Should have moves available");
2344 }
2345
2346 #[test]
2350 fn one_piece_each_is_draw() {
2351 let board = Board::from_squares(Team::White, &[Square::A1], &[Square::H8], &[]);
2353 assert_eq!(
2354 board.status(),
2355 GameStatus::Draw,
2356 "One piece each should be draw"
2357 );
2358 }
2359
2360 #[test]
2361 fn one_king_each_is_draw() {
2362 let board = Board::from_squares(
2363 Team::White,
2364 &[Square::A1],
2365 &[Square::H8],
2366 &[Square::A1, Square::H8],
2367 );
2368 assert_eq!(
2369 board.status(),
2370 GameStatus::Draw,
2371 "One king each should be draw"
2372 );
2373 }
2374
2375 #[test]
2376 fn king_vs_pawn_is_draw() {
2377 let board = Board::from_squares(
2379 Team::White,
2380 &[Square::A1],
2381 &[Square::H8],
2382 &[Square::A1], );
2384 assert_eq!(
2385 board.status(),
2386 GameStatus::Draw,
2387 "King vs pawn (1v1) is draw in current implementation"
2388 );
2389 }
2390
2391 #[test]
2394 fn no_pieces_means_loss() {
2395 let board = Board::from_squares(Team::White, &[], &[Square::D4], &[]);
2396 assert_eq!(
2397 board.status(),
2398 GameStatus::Won(Team::Black),
2399 "No white pieces means black wins"
2400 );
2401 }
2402
2403 #[test]
2404 fn blocked_means_loss() {
2405 let board = Board::from_squares(
2409 Team::White,
2410 &[Square::A2, Square::A3],
2411 &[
2412 Square::A4, Square::A5, Square::B2, Square::B3, Square::C2, Square::C3, ],
2419 &[],
2420 );
2421 assert_eq!(
2425 board.status(),
2426 GameStatus::Won(Team::Black),
2427 "Completely blocked white should lose"
2428 );
2429 }
2430
2431 #[test]
2434 fn pawn_four_capture_chain() {
2435 let board = Board::from_squares(
2438 Team::White,
2439 &[Square::A2],
2440 &[Square::A3, Square::B4, Square::C5, Square::D6],
2441 &[],
2442 );
2443 let actions = board.actions();
2444
2445 let max_captures = actions
2446 .iter()
2447 .map(|a| a.delta.pieces[Team::Black.to_usize()].count_ones())
2448 .max()
2449 .unwrap_or(0);
2450
2451 assert_eq!(max_captures, 4, "Should capture 4 pieces in chain");
2452 }
2453
2454 #[test]
2455 fn king_five_capture_chain() {
2456 let board = Board::from_squares(
2465 Team::White,
2466 &[Square::A2],
2467 &[Square::A4, Square::C5, Square::D3, Square::F2],
2468 &[Square::A2],
2469 );
2470 let actions = board.actions();
2471
2472 let max_captures = actions
2473 .iter()
2474 .map(|a| a.delta.pieces[Team::Black.to_usize()].count_ones())
2475 .max()
2476 .unwrap_or(0);
2477
2478 assert!(max_captures >= 3, "King should capture at least 3 in chain");
2480 }
2481
2482 #[test]
2485 fn pawn_on_edge_limited_moves() {
2486 let board = Board::from_squares(Team::White, &[Square::A4], &[Square::H8], &[]);
2488 let actions = board.actions();
2489
2490 assert_eq!(actions.len(), 2, "Edge pawn should have 2 moves");
2492 }
2493
2494 #[test]
2495 fn pawn_in_corner_very_limited() {
2496 let board = Board::from_squares(Team::White, &[Square::H3], &[Square::A8], &[]);
2499 let actions = board.actions();
2500
2501 assert_eq!(actions.len(), 2, "Corner-area pawn should have 2 moves");
2503 }
2504
2505 #[test]
2506 fn king_in_corner_moves() {
2507 let board = Board::from_squares(Team::White, &[Square::A1], &[Square::H8], &[Square::A1]);
2509 let actions = board.actions();
2510
2511 assert_eq!(actions.len(), 14, "Corner king should have 14 moves");
2513 }
2514
2515 #[test]
2518 fn pawn_blocked_by_friendly() {
2519 let board = Board::from_squares(Team::White, &[Square::D4, Square::D5], &[Square::H8], &[]);
2521 let actions = board.actions();
2522
2523 let d4_moves: Vec<_> = actions
2527 .iter()
2528 .filter(|a| {
2529 let delta = a.delta.pieces[Team::White.to_usize()];
2530 delta & Square::D4.to_mask() != 0
2531 })
2532 .collect();
2533
2534 let d4_to_d5 = d4_moves.iter().find(|a| {
2535 let dest = a.delta.pieces[Team::White.to_usize()] & !Square::D4.to_mask();
2536 dest == Square::D5.to_mask()
2537 });
2538
2539 assert!(
2540 d4_to_d5.is_none(),
2541 "D4 should not be able to move to D5 (blocked)"
2542 );
2543 }
2544
2545 #[test]
2546 fn king_blocked_by_friendly_cannot_pass() {
2547 let board = Board::from_squares(
2549 Team::White,
2550 &[Square::A4, Square::C4],
2551 &[Square::H8],
2552 &[Square::A4],
2553 );
2554 let actions = board.actions();
2555
2556 let past_c4 = actions.iter().find(|a| {
2558 let delta = a.delta.pieces[Team::White.to_usize()];
2559 if delta & Square::A4.to_mask() != 0 {
2560 let dest = delta & !Square::A4.to_mask() & !Square::C4.to_mask();
2561 dest & (Square::D4.to_mask()
2563 | Square::E4.to_mask()
2564 | Square::F4.to_mask()
2565 | Square::G4.to_mask()
2566 | Square::H4.to_mask())
2567 != 0
2568 } else {
2569 false
2570 }
2571 });
2572
2573 assert!(
2574 past_c4.is_none(),
2575 "King should not pass through friendly piece"
2576 );
2577 }
2578
2579 #[test]
2582 fn initial_position_piece_count() {
2583 let board = Board::new_default();
2584 assert_eq!(
2585 board.friendly_pieces().count_ones(),
2586 16,
2587 "White should have 16 pieces"
2588 );
2589 assert_eq!(
2590 board.hostile_pieces().count_ones(),
2591 16,
2592 "Black should have 16 pieces"
2593 );
2594 }
2595
2596 #[test]
2597 fn initial_position_no_kings() {
2598 let board = Board::new_default();
2599 assert_eq!(board.state.kings, 0, "No kings at start");
2600 }
2601
2602 #[test]
2603 fn initial_position_white_to_move() {
2604 let board = Board::new_default();
2605 assert_eq!(board.turn, Team::White, "White moves first");
2606 }
2607
2608 #[test]
2614 fn king_10_capture_path_no_180_turns() {
2615 let board = Board::from_squares(
2616 Team::White,
2617 &[Square::C2, Square::G2, Square::H4],
2618 &[
2619 Square::C1,
2620 Square::E2,
2621 Square::C3,
2622 Square::B4,
2623 Square::D4,
2624 Square::A5,
2625 Square::E5,
2626 Square::B6,
2627 Square::D6,
2628 Square::H6,
2629 Square::C7,
2630 Square::H7,
2631 ],
2632 &[
2633 Square::C1,
2634 Square::C2,
2635 Square::D4,
2636 Square::B6,
2637 Square::D6,
2638 Square::C7,
2639 ],
2640 );
2641
2642 let actions = board.actions();
2643 assert_eq!(actions.len(), 9, "Expected 9 maximum-capture actions");
2644
2645 for action in &actions {
2646 assert_eq!(
2647 action.capture_count(Team::White),
2648 10,
2649 "All actions should capture 10 pieces"
2650 );
2651
2652 let detailed = action.to_detailed(board.turn, &board.state);
2653 let path = detailed.path();
2654 let mut prev_dir: Option<(i8, i8)> = None;
2655
2656 for i in 1..path.len() {
2657 let from = path[i - 1];
2658 let to = path[i];
2659
2660 let dcol = (to.column() as i8 - from.column() as i8).signum();
2661 let drow = (to.row() as i8 - from.row() as i8).signum();
2662
2663 if let Some((pcol, prow)) = prev_dir {
2664 assert!(
2665 !(dcol == -pcol && drow == -prow && (dcol != 0 || drow != 0)),
2666 "180° turn detected in path: {} -> {}",
2667 from,
2668 to
2669 );
2670 }
2671 prev_dir = Some((dcol, drow));
2672 }
2673 }
2674 }
2675}