Skip to content
Closed
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
237 changes: 148 additions & 89 deletions crates/chess/src/move_generation.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,16 @@
use crate::{
attacks,
bitboard::Bitboard,
bitboard_helpers,
board::Board,
move_generation::{self, enumerate::enumerate_moves, move_filter::MoveFilter},
move_list::MoveList,
moves::Move,
moves::{Move, MoveFlag},
pieces::Piece,
rank::Rank,
rays,
side::Side,
square::{self, Square},
square::Square,
};

pub mod castling;
Expand Down Expand Up @@ -180,103 +181,161 @@ fn get_piece_moves(piece: Piece, board: &Board, move_list: &mut MoveList, move_f
}
}

/// The promotion flags emitted, in queen-first order, whenever a pawn move
/// lands on the promotion rank.
const PROMOTION_FLAGS: [MoveFlag; 4] = [
MoveFlag::PromotionQueen,
MoveFlag::PromotionRook,
MoveFlag::PromotionBishop,
MoveFlag::PromotionKnight,
];

/// Emit pawn moves for a set of destination squares generated by a single
/// directional shift.
///
/// Each destination's origin is recovered by the inverse shift
/// `(inv_file, inv_rank)`, which is exact because every destination came from
/// shifting a real pawn (see [`Square::offset_unchecked`]). Destinations on the
/// promotion rank expand into the four [`PROMOTION_FLAGS`]; others use
/// `quiet_flag`. `emit_promotions` / `emit_quiets` gate the two cases so a single
/// target set can serve different [`MoveFilter`]s.
#[inline]
fn emit_pawn_targets(
move_list: &mut MoveList,
targets: Bitboard,
inv_file: i8,
inv_rank: i8,
quiet_flag: MoveFlag,
promotion_rank: Rank,
emit_promotions: bool,
emit_quiets: bool,
) {
for to in targets {
let from = to.offset_unchecked(inv_file, inv_rank);
if to.rank() == promotion_rank {
if emit_promotions {
for flag in PROMOTION_FLAGS {
move_list.push(Move::new(from, to, flag));
}
}
} else if emit_quiets {
move_list.push(Move::new(from, to, quiet_flag));
}
}
}

#[cfg_attr(not(debug_assertions), inline(always))]
#[cfg_attr(debug_assertions, inline(never))]
fn get_pawn_moves(board: &Board, move_list: &mut MoveList, move_filter: MoveFilter) {
let us = board.side_to_move();
let them = us.opposite();
let their_pieces = board.pieces(them);
let occupancy = board.all_pieces();
let empty = !occupancy;
let pawns_bb = board.piece_bitboard(Piece::Pawn, us);

// loop through all the pawns for us
for from_square in pawns_bb {
let attack_bb = attacks::pawn(from_square, us);

let mut bb_moves = Bitboard::default();
let to_square = from_square.forward_unchecked(us);

// pawn non-capture moves
if matches!(
move_filter,
MoveFilter::All | MoveFilter::Quiets | MoveFilter::Tacticals
) {
let bb_push = Bitboard::from(to_square);
let bb_single_push = bb_push & empty;
let can_double_push = match us {
Side::White => square::is_square_on_rank(from_square, Rank::R2),
Side::Black => square::is_square_on_rank(from_square, Rank::R7),
};

let double_push_square = if can_double_push {
to_square.forward(us)
} else {
None
};

// note that the single push square has to be empty in addition to the double push square being empty
let is_double_push_unobstructed = if let Some(push_square) = double_push_square {
!occupancy.is_square_occupied(to_square)
&& !occupancy.is_square_occupied(push_square)
} else {
false
};

let bb_double_push = if can_double_push
&& is_double_push_unobstructed
&& let Some(dbl_push_sq) = double_push_square
{
Bitboard::from(dbl_push_sq) & empty
} else {
Bitboard::default()
};

// Tacticals include only promotion-rank pushes; Quiets exclude them.
let promo_rank_bb = Rank::promotion_rank(us).to_bitboard();
let pushes = bb_single_push | bb_double_push;
let filtered_pushes = match move_filter {
MoveFilter::Tacticals => pushes & promo_rank_bb,
MoveFilter::Quiets => pushes & !promo_rank_bb,
_ => pushes,
};
bb_moves |= filtered_pushes;
}
let pawns = board.piece_bitboard(Piece::Pawn, us);
if pawns.is_empty() {
return;
}

// pawn captures
if matches!(
move_filter,
MoveFilter::All | MoveFilter::Captures | MoveFilter::Tacticals
) {
let bb_capture = attack_bb & their_pieces;
// En passant
let bb_en_passant = match board.en_passant_square() {
Some(en_passant_square) => {
// We only want to add the en passant square if it is within range of the pawn.
// This means that the en passant square is within 1 rank of the pawn and the en passant square
// is in the pawn's attack table.
let en_passant_bb = Bitboard::from(en_passant_square);
let result = en_passant_bb & !(attack_bb);
let is_in_range = result == 0;
if is_in_range {
en_passant_bb
} else {
Bitboard::default()
}
}
None => Bitboard::default(),
};
bb_moves |= bb_capture | bb_en_passant;
let enemies = board.pieces(them);
let empty = !board.all_pieces();
let promotion_rank = Rank::promotion_rank(us);

// Non-promotion pushes, promotion pushes, and captures are gated independently
// so each `MoveFilter` selects exactly the same set the per-pawn generator did:
// Quiets = non-promo pushes; Captures = all captures/EP; Tacticals = promo
// pushes + all captures; All = everything.
let want_quiet_pushes = matches!(move_filter, MoveFilter::All | MoveFilter::Quiets);
let want_promo_pushes = matches!(move_filter, MoveFilter::All | MoveFilter::Tacticals);
let want_captures = matches!(
move_filter,
MoveFilter::All | MoveFilter::Captures | MoveFilter::Tacticals
);

// Forward shift and the third rank (where a legal single push from the start
// rank lands) depend on the side; captures always shift toward the lower file
// ("left") and higher file ("right").
let (push, capture_left, capture_right, third_rank): (
fn(Bitboard) -> Bitboard,
fn(Bitboard) -> Bitboard,
fn(Bitboard) -> Bitboard,
Bitboard,
) = match us {
Side::White => (
bitboard_helpers::north,
bitboard_helpers::north_west,
bitboard_helpers::north_east,
Rank::R3.to_bitboard(),
),
Side::Black => (
bitboard_helpers::south,
bitboard_helpers::south_west,
bitboard_helpers::south_east,
Rank::R6.to_bitboard(),
),
};
// Inverse rank delta of one forward push (used to recover a move's origin).
let back = -us.forward_delta();

// Pushes: the single-push set carries both quiet and promotion pushes.
if want_quiet_pushes || want_promo_pushes {
let single = push(pawns) & empty;
emit_pawn_targets(
move_list,
single,
0,
back,
MoveFlag::Standard,
promotion_rank,
want_promo_pushes,
want_quiet_pushes,
);

// A double push exists only where the intermediate single-push square is
// empty (so it must be in `single`) and the pawn started on its home rank.
if want_quiet_pushes {
let double = push(single & third_rank) & empty;
emit_pawn_targets(
move_list,
double,
0,
2 * back,
MoveFlag::DoublePush,
promotion_rank,
false,
true,
);
}
}

enumerate::enumerate_moves(
&bb_moves,
from_square,
Piece::Pawn,
board,
move_filter,
// Captures (including capture-promotions) and en passant.
if want_captures {
emit_pawn_targets(
move_list,
capture_left(pawns) & enemies,
1,
back,
MoveFlag::Standard,
promotion_rank,
true,
true,
);
emit_pawn_targets(
move_list,
capture_right(pawns) & enemies,
-1,
back,
MoveFlag::Standard,
promotion_rank,
true,
true,
);

if let Some(ep_square) = board.en_passant_square() {
// Our pawns that attack the EP square are exactly those a `them` pawn
// on the EP square would attack.
let ep_attackers = pawns & attacks::pawn(ep_square, them);
for from in ep_attackers {
move_list.push(Move::new(from, ep_square, MoveFlag::EnPassant));
}
}
}
}

Expand Down
Loading