use crate::engine::attacks;
use crate::engine::board::Position;
use crate::engine::types::{Bitboard, Color, Move, MoveFlags, PieceType, Square};
pub fn legal_moves(pos: &Position) -> Vec<Move> {
let mut pseudo = Vec::with_capacity(256);
generate_pseudo_legal(pos, &mut pseudo);
let mut legal = Vec::with_capacity(pseudo.len());
for mv in pseudo {
let mut copy = pos.clone();
copy.make_move(mv);
let us = !copy.side_to_move;
if !copy.is_square_attacked(copy.king_sq(us), copy.side_to_move) {
legal.push(mv);
}
}
legal
}
pub fn legal_moves_from(pos: &Position, from: Square) -> Vec<Move> {
legal_moves(pos)
.into_iter()
.filter(|m| m.from == from)
.collect()
}
pub fn legal_captures(pos: &Position) -> Vec<Move> {
let mut pseudo = Vec::with_capacity(64);
generate_pseudo_legal(pos, &mut pseudo);
let mut captures = Vec::with_capacity(pseudo.len());
for mv in pseudo {
if !mv.flags.is_capture() && mv.promotion != Some(PieceType::Queen) {
continue;
}
let mut copy = pos.clone();
copy.make_move(mv);
let us = !copy.side_to_move;
if !copy.is_square_attacked(copy.king_sq(us), copy.side_to_move) {
captures.push(mv);
}
}
captures
}
fn generate_pseudo_legal(pos: &Position, moves: &mut Vec<Move>) {
let us = pos.side_to_move;
generate_pawn_moves(pos, us, moves);
generate_knight_moves(pos, us, moves);
generate_king_moves(pos, us, moves);
generate_slider_moves(pos, us, PieceType::Bishop, moves);
generate_slider_moves(pos, us, PieceType::Rook, moves);
generate_slider_moves(pos, us, PieceType::Queen, moves);
generate_castling_moves(pos, us, moves);
}
fn generate_pawn_moves(pos: &Position, us: Color, moves: &mut Vec<Move>) {
let t = attacks::tables();
let pawns = pos.bb(us, PieceType::Pawn);
let enemy = pos.occupied[(!us).index()];
let empty = !pos.all_occupied;
let (push_dir, start_rank, promo_rank): (i8, u8, u8) = match us {
Color::White => (8, 1, 6), Color::Black => (-8, 6, 1), };
for from in pawns.iter() {
let from_rank = from.rank();
let to_idx = (from.0 as i8 + push_dir) as u8;
let to = Square(to_idx);
if (Bitboard::from_square(to) & empty).is_not_empty() {
if from_rank == promo_rank {
add_promotions(from, to, MoveFlags::NONE, moves);
} else {
moves.push(Move::new(from, to));
}
if from_rank == start_rank {
let to2_idx = (from.0 as i8 + push_dir * 2) as u8;
let to2 = Square(to2_idx);
if (Bitboard::from_square(to2) & empty).is_not_empty() {
moves.push(Move::with_flags(from, to2, MoveFlags::DOUBLE_PUSH));
}
}
}
let attack_bb = t.pawn_attacks(us, from) & enemy;
for to in attack_bb.iter() {
if from_rank == promo_rank {
add_promotions(from, to, MoveFlags::CAPTURE, moves);
} else {
moves.push(Move::with_flags(from, to, MoveFlags::CAPTURE));
}
}
if let Some(ep_sq) = pos.en_passant
&& t.pawn_attacks(us, from).is_set(ep_sq)
{
moves.push(Move::with_flags(
from,
ep_sq,
MoveFlags::CAPTURE | MoveFlags::EN_PASSANT,
));
}
}
}
fn add_promotions(from: Square, to: Square, extra_flags: MoveFlags, moves: &mut Vec<Move>) {
for &promo in &[
PieceType::Queen,
PieceType::Rook,
PieceType::Bishop,
PieceType::Knight,
] {
moves.push(Move::with_promotion(from, to, promo, extra_flags));
}
}
fn generate_knight_moves(pos: &Position, us: Color, moves: &mut Vec<Move>) {
let t = attacks::tables();
let knights = pos.bb(us, PieceType::Knight);
let friendly = pos.occupied[us.index()];
let enemy = pos.occupied[(!us).index()];
for from in knights.iter() {
let targets = t.knight_attacks(from) & !friendly;
for to in targets.iter() {
let flags = if (Bitboard::from_square(to) & enemy).is_not_empty() {
MoveFlags::CAPTURE
} else {
MoveFlags::NONE
};
moves.push(Move::with_flags(from, to, flags));
}
}
}
fn generate_king_moves(pos: &Position, us: Color, moves: &mut Vec<Move>) {
let t = attacks::tables();
let king_sq = pos.king_sq(us);
let friendly = pos.occupied[us.index()];
let enemy = pos.occupied[(!us).index()];
let targets = t.king_attacks(king_sq) & !friendly;
for to in targets.iter() {
let flags = if (Bitboard::from_square(to) & enemy).is_not_empty() {
MoveFlags::CAPTURE
} else {
MoveFlags::NONE
};
moves.push(Move::with_flags(king_sq, to, flags));
}
}
fn generate_slider_moves(pos: &Position, us: Color, piece: PieceType, moves: &mut Vec<Move>) {
let t = attacks::tables();
let pieces = pos.bb(us, piece);
let friendly = pos.occupied[us.index()];
let enemy = pos.occupied[(!us).index()];
let occ = pos.all_occupied;
for from in pieces.iter() {
let attacks = match piece {
PieceType::Bishop => t.bishop_attacks(from, occ),
PieceType::Rook => t.rook_attacks(from, occ),
PieceType::Queen => t.queen_attacks(from, occ),
_ => unreachable!(),
};
let targets = attacks & !friendly;
for to in targets.iter() {
let flags = if (Bitboard::from_square(to) & enemy).is_not_empty() {
MoveFlags::CAPTURE
} else {
MoveFlags::NONE
};
moves.push(Move::with_flags(from, to, flags));
}
}
}
fn generate_castling_moves(pos: &Position, us: Color, moves: &mut Vec<Move>) {
let them = !us;
let king_sq = pos.king_sq(us);
if pos.is_square_attacked(king_sq, them) {
return;
}
let (ks_right, qs_right, rank_base) = match us {
Color::White => (
CastlingRights::WHITE_KINGSIDE,
CastlingRights::WHITE_QUEENSIDE,
0u8,
),
Color::Black => (
CastlingRights::BLACK_KINGSIDE,
CastlingRights::BLACK_QUEENSIDE,
56u8,
),
};
if pos.castling_rights.has(ks_right) {
let f_sq = Square(rank_base + 5);
let g_sq = Square(rank_base + 6);
if !pos.all_occupied.is_set(f_sq)
&& !pos.all_occupied.is_set(g_sq)
&& !pos.is_square_attacked(f_sq, them)
&& !pos.is_square_attacked(g_sq, them)
{
moves.push(Move::with_flags(king_sq, g_sq, MoveFlags::CASTLING));
}
}
if pos.castling_rights.has(qs_right) {
let b_sq = Square(rank_base + 1);
let c_sq = Square(rank_base + 2);
let d_sq = Square(rank_base + 3);
if !pos.all_occupied.is_set(b_sq)
&& !pos.all_occupied.is_set(c_sq)
&& !pos.all_occupied.is_set(d_sq)
&& !pos.is_square_attacked(c_sq, them)
&& !pos.is_square_attacked(d_sq, them)
{
moves.push(Move::with_flags(king_sq, c_sq, MoveFlags::CASTLING));
}
}
}
use crate::engine::types::CastlingRights;
#[cfg(test)]
mod tests {
use super::*;
fn sq(name: &str) -> Square {
Square::from_algebraic(name).unwrap()
}
fn pos(fen: &str) -> Position {
Position::from_fen(fen).unwrap()
}
fn count_legal(fen: &str) -> usize {
legal_moves(&pos(fen)).len()
}
#[test]
fn starting_position_has_20_moves() {
assert_eq!(
count_legal("rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1"),
20
);
}
#[test]
fn starting_position_after_e4() {
assert_eq!(
count_legal("rnbqkbnr/pppppppp/8/8/4P3/8/PPPP1PPP/RNBQKBNR b KQkq e3 0 1"),
20
);
}
#[test]
fn pawn_single_push() {
let p = pos("4k3/8/8/8/8/8/4P3/4K3 w - - 0 1");
let moves = legal_moves(&p);
let pawn_moves: Vec<_> = moves.iter().filter(|m| m.from == sq("e2")).collect();
assert_eq!(pawn_moves.len(), 2);
}
#[test]
fn pawn_blocked() {
let p = pos("4k3/8/8/8/8/4p3/4P3/4K3 w - - 0 1");
let moves = legal_moves(&p);
let pawn_moves: Vec<_> = moves.iter().filter(|m| m.from == sq("e2")).collect();
assert_eq!(pawn_moves.len(), 0);
}
#[test]
fn pawn_capture() {
let p = pos("4k3/8/8/8/3p4/8/4P3/4K3 w - - 0 1");
let moves = legal_moves(&p);
let pawn_moves: Vec<_> = moves.iter().filter(|m| m.from == sq("e2")).collect();
assert_eq!(pawn_moves.len(), 2); }
#[test]
fn pawn_promotion() {
let p = pos("7k/4P3/8/8/8/8/8/4K3 w - - 0 1");
let moves = legal_moves(&p);
let promo_moves: Vec<_> = moves.iter().filter(|m| m.from == sq("e7")).collect();
assert_eq!(promo_moves.len(), 4);
assert!(promo_moves.iter().all(|m| m.promotion.is_some()));
}
#[test]
fn en_passant_move_generated() {
let p = pos("rnbqkbnr/ppp1p1pp/8/3pPp2/8/8/PPPP1PPP/RNBQKBNR w KQkq f6 0 3");
let moves = legal_moves(&p);
let ep_moves: Vec<_> = moves.iter().filter(|m| m.flags.is_en_passant()).collect();
assert_eq!(ep_moves.len(), 1);
assert_eq!(ep_moves[0].to, sq("f6"));
}
#[test]
fn castling_both_sides() {
let p = pos("r3k2r/pppppppp/8/8/8/8/PPPPPPPP/R3K2R w KQkq - 0 1");
let moves = legal_moves(&p);
let castle_moves: Vec<_> = moves.iter().filter(|m| m.flags.is_castling()).collect();
assert_eq!(castle_moves.len(), 2);
}
#[test]
fn castling_blocked() {
let p = pos("r3k2r/pppppppp/8/8/8/8/PPPPPPPP/RN2K1NR w KQkq - 0 1");
let moves = legal_moves(&p);
let castle_moves: Vec<_> = moves.iter().filter(|m| m.flags.is_castling()).collect();
assert_eq!(castle_moves.len(), 0);
}
#[test]
fn castling_through_check_forbidden() {
let p = pos("4kr2/8/8/8/8/8/8/R3K2R w KQ - 0 1");
let moves = legal_moves(&p);
let castle_moves: Vec<_> = moves.iter().filter(|m| m.flags.is_castling()).collect();
assert_eq!(castle_moves.len(), 1);
assert_eq!(castle_moves[0].to, sq("c1"));
}
#[test]
fn no_castling_while_in_check() {
let p = pos("4k3/8/8/8/8/8/8/R3K2r w Q - 0 1");
let moves = legal_moves(&p);
let castle_moves: Vec<_> = moves.iter().filter(|m| m.flags.is_castling()).collect();
assert_eq!(castle_moves.len(), 0);
}
#[test]
fn must_escape_check() {
let p = pos("4k3/8/8/8/8/8/8/R3K2q w Q - 0 1");
let moves = legal_moves(&p);
for mv in &moves {
let mut copy = p.clone();
copy.make_move(*mv);
let us = Color::White;
assert!(
!copy.is_square_attacked(copy.king_sq(us), !us),
"move {} leaves king in check",
mv
);
}
}
#[test]
fn kiwipete_48_moves() {
assert_eq!(
count_legal("r3k2r/p1ppqpb1/bn2pnp1/3PN3/1p2P3/2N2Q1p/PPPBBPPP/R3K2R w KQkq - 0 1"),
48
);
}
#[test]
fn position_3_14_moves() {
assert_eq!(count_legal("8/2p5/3p4/KP5r/1R3p1k/8/4P1P1/8 w - - 0 1"), 14);
}
#[test]
fn position_4_6_moves() {
assert_eq!(
count_legal("r3k2r/Pppp1ppp/1b3nbN/nP6/BBP1P3/q4N2/Pp1P2PP/R2Q1RK1 w kq - 0 1"),
6
);
}
#[test]
fn position_5_44_moves() {
assert_eq!(
count_legal("rnbq1k1r/pp1Pbppp/2p5/8/2B5/8/PPP1NnPP/RNBQK2R w KQ - 1 8"),
44
);
}
#[test]
fn make_undo_preserves_hash() {
let p = Position::starting();
let original_hash = p.zobrist_hash;
let all = legal_moves(&p);
for mv in all {
let mut copy = p.clone();
let undo = copy.make_move(mv);
copy.undo_move(mv, &undo);
assert_eq!(
copy.zobrist_hash, original_hash,
"hash mismatch after make/undo of {mv}"
);
}
}
#[test]
fn make_undo_preserves_fen() {
let fen = "r3k2r/p1ppqpb1/bn2pnp1/3PN3/1p2P3/2N2Q1p/PPPBBPPP/R3K2R w KQkq - 0 1";
let p = pos(fen);
let original_hash = p.zobrist_hash;
for mv in legal_moves(&p) {
let mut copy = p.clone();
let undo = copy.make_move(mv);
copy.undo_move(mv, &undo);
assert_eq!(copy.to_fen(), fen, "FEN mismatch after make/undo of {mv}");
assert_eq!(copy.zobrist_hash, original_hash);
}
}
#[test]
fn legal_moves_from_e2() {
let p = Position::starting();
let pawn_moves = legal_moves_from(&p, sq("e2"));
assert_eq!(pawn_moves.len(), 2); }
#[test]
fn legal_moves_from_empty_square() {
let p = Position::starting();
let moves = legal_moves_from(&p, sq("e4"));
assert_eq!(moves.len(), 0);
}
}