Skip to content

Commit

Permalink
Merge pull request #28 from na2hiro/add-doc-comments-and-tests
Browse files Browse the repository at this point in the history
Add doc comments & tests. Some refactoring
  • Loading branch information
sugyan authored Oct 13, 2023
2 parents f4be403 + 5776c78 commit 51c2378
Show file tree
Hide file tree
Showing 5 changed files with 181 additions and 87 deletions.
68 changes: 60 additions & 8 deletions src/bitboard.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,19 @@ pub(crate) trait Occupied
where
Self: Sized,
{
/// Shift left (South)
fn shl(&self) -> Self;
/// Shift right (North)
fn shr(&self) -> Self;
/// Slide consecutively to the positive: that is South.
fn sliding_positive_consecutive(&self, mask: &Self) -> Self;
/// Slide consecutively to the negative: that is North.
fn sliding_negative_consecutive(&self, mask: &Self) -> Self;
/// Slide for 2 directions to the positive. Positive is further West, or further South if it's on the same file.
fn sliding_positives(&self, masks: &[Self; 2]) -> Self;
/// Slide for 2 directions to the negative. Negative is further East, or further North if it's on the same file.
fn sliding_negatives(&self, masks: &[Self; 2]) -> Self;
/// Vacant files
fn vacant_files(&self) -> Self;
}

Expand Down Expand Up @@ -154,25 +161,70 @@ mod tests {

#[test]
fn sliding_positives() {
let bb = Bitboard::single(SQ_8C) | Bitboard::single(SQ_8G);
// Imagine there's a bishop at 6E
let bb = to_bb(vec![SQ_8C, SQ_8G]);
assert_eq!(
bb | Bitboard::single(SQ_7D) | Bitboard::single(SQ_7F),
bb | to_bb(vec![SQ_7D, SQ_7F]),
bb.sliding_positives(&[
Bitboard::single(SQ_7D) | Bitboard::single(SQ_8C) | Bitboard::single(SQ_9B),
Bitboard::single(SQ_7F) | Bitboard::single(SQ_8G) | Bitboard::single(SQ_9H),
to_bb(vec![SQ_7D, SQ_8C, SQ_9B]),
to_bb(vec![SQ_7F, SQ_8G, SQ_9H]),
])
);

// Imagine there's a rook at 6F
let bb = to_bb(vec![SQ_6H, SQ_8F]);
assert_eq!(
bb | to_bb(vec![SQ_6G, SQ_7F]),
bb.sliding_positives(&[
to_bb(vec![SQ_6G, SQ_6H, SQ_6I]),
to_bb(vec![SQ_7F, SQ_8F, SQ_9F]),
])
);
}

#[test]
fn sliding_negatives() {
let bb = Bitboard::single(SQ_2C) | Bitboard::single(SQ_2G);
// Imagine there's a bishop at 4E
let bb = to_bb(vec![SQ_2C, SQ_2G]);
assert_eq!(
bb | to_bb(vec![SQ_3D, SQ_3F]),
bb.sliding_negatives(&[
to_bb(vec![SQ_3D, SQ_2C, SQ_1B]),
to_bb(vec![SQ_3F, SQ_2G, SQ_1H]),
])
);
// Imagine there's a rook at 4D
let bb = to_bb(vec![SQ_2D, SQ_4B]);
assert_eq!(
bb | Bitboard::single(SQ_3D) | Bitboard::single(SQ_3F),
bb | to_bb(vec![SQ_3D, SQ_4C]),
bb.sliding_negatives(&[
Bitboard::single(SQ_3D) | Bitboard::single(SQ_2C) | Bitboard::single(SQ_1B),
Bitboard::single(SQ_3F) | Bitboard::single(SQ_2G) | Bitboard::single(SQ_1H),
to_bb(vec![SQ_3D, SQ_2D, SQ_1D]),
to_bb(vec![SQ_4C, SQ_4B, SQ_4A]),
])
);
}

#[test]
fn vacant_files() {
assert_eq!(!Bitboard::empty(), Bitboard::empty().vacant_files());
let all_files = to_bb(vec![
SQ_1A, SQ_2B, SQ_3C, SQ_4D, SQ_5E, SQ_6F, SQ_7G, SQ_8H, SQ_9I,
])
.vacant_files();
assert_eq!(Bitboard::empty(), all_files);

let odd_files = to_bb(vec![SQ_1A, SQ_3A, SQ_5A, SQ_7A, SQ_9A]).vacant_files();
let odd_files2 = to_bb(vec![SQ_1I, SQ_3I, SQ_5I, SQ_7I, SQ_9I]).vacant_files();
assert_eq!(odd_files, odd_files2);

let even_files = to_bb(vec![SQ_2A, SQ_4A, SQ_6A, SQ_8A]).vacant_files();
assert_eq!(Bitboard::empty(), odd_files & even_files);
assert_eq!(!Bitboard::empty(), odd_files | even_files);
}

fn to_bb(squares: Vec<Square>) -> Bitboard {
squares
.iter()
.fold(Bitboard::empty(), |acc, e| (acc | Bitboard::single(*e)))
}
}
14 changes: 14 additions & 0 deletions src/bitboard/core.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ use super::Occupied;
pub(crate) use shogi_core::Bitboard;
use shogi_core::Square;

/// Note the alignment of the bitboard: 18 bits and 63 bits out of 2 64-bit int are used
const VACANT_MASK_VALUE: u128 = 0x0002_0100_4020_1008_0402_0100;
const VACANT_MASK: Bitboard = unsafe { Bitboard::from_u128_unchecked(VACANT_MASK_VALUE) };
const BB_1A: Bitboard = Bitboard::single(Square::SQ_1A);
Expand All @@ -18,12 +19,20 @@ const MASKED_BBS: [Bitboard; Square::NUM + 2] = {
bbs
};

/// # Arguments
///
/// * `bb` - The occupied bitboard
/// * `mask` - The potential attacks
#[inline(always)]
fn sliding_positive(bb: &Bitboard, mask: &Bitboard) -> Bitboard {
let tz = (*bb & mask | BB_9I).to_u128().trailing_zeros();
*mask & MASKED_BBS[tz as usize + 1]
}

/// # Arguments
///
/// * `bb` - The occupied bitboard
/// * `mask` - The potential attacks
#[inline(always)]
fn sliding_negative(bb: &Bitboard, mask: &Bitboard) -> Bitboard {
let lz = (*bb & mask | BB_1A).to_u128().leading_zeros();
Expand Down Expand Up @@ -57,6 +66,11 @@ impl Occupied for Bitboard {
}
#[inline(always)]
fn vacant_files(&self) -> Self {
// Following happens in parallel for each file:
// 1. The highest bit of (0b100000000 - self) is 1 iff the file is vacant thanks to borrowing.
// 2. Shift it by 8 bit to get the flag. Results in either 0b000000000 or 0b000000001
// 3. 0b100000000 - the value from 2. Results in either 0b100000000 or 0b011111111
// 4. XOR with 0b100000000. Results in either 0b000000000 or 0b111111111
let bb = unsafe { Self::from_u128_unchecked(VACANT_MASK_VALUE - self.to_u128()) };
VACANT_MASK
^ unsafe { Self::from_u128_unchecked(VACANT_MASK_VALUE - bb.shift_up(8).to_u128()) }
Expand Down
111 changes: 68 additions & 43 deletions src/movegen.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ impl Position {
}
av
}
/// Generate moves.
fn generate_all(&self, av: &mut ArrayVec<Move, MAX_LEGAL_MOVES>) {
let target = !self.player_bitboard(self.side_to_move());
self.generate_for_fu(av, &target);
Expand All @@ -39,52 +40,48 @@ impl Position {
self.generate_for_ry(av, &target);
self.generate_drop(av, &(!self.occupied_bitboard() & !Bitboard::empty()));
}
/// Generate moves to evade check, optimized using AttackInfo.
fn generate_evasions(&self, av: &mut ArrayVec<Move, MAX_LEGAL_MOVES>) {
let c = self.side_to_move();
if let Some(king) = self.king_position(c) {
let mut checkers_attacks = Bitboard::empty();
let mut checkers_count = 0;
for ch in self.checkers() {
if let Some(p) = self.piece_at(ch) {
let pk = p.piece_kind();
// 龍が斜め位置から王手している場合のみ、他の駒の裏に逃がれることができる可能性がある
if pk == PieceKind::ProRook
&& ch.file() != king.file()
&& ch.rank() != king.rank()
{
checkers_attacks |= ATTACK_TABLE.hi.attack(ch, &self.occupied_bitboard());
} else {
checkers_attacks |= ATTACK_TABLE.pseudo_attack(pk, ch, c.flip());
}
}
checkers_count += 1;
}
for to in ATTACK_TABLE.ou.attack(king, c) & !self.player_bitboard(c) & !checkers_attacks
{
av.push(Move::Normal {
from: king,
to,
promote: false,
});
}
// 両王手の場合は玉が逃げるしかない
if checkers_count > 1 {
return;
}
if let Some(ch) = self.checkers().into_iter().next() {
let target_drop = BETWEEN_TABLE[ch.array_index()][king.array_index()];
let target_move = target_drop | self.checkers();
self.generate_for_fu(av, &target_move);
self.generate_for_ky(av, &target_move);
self.generate_for_ke(av, &target_move);
self.generate_for_gi(av, &target_move);
self.generate_for_ka(av, &target_move);
self.generate_for_hi(av, &target_move);
self.generate_for_ki(av, &target_move);
self.generate_for_um(av, &target_move);
self.generate_for_ry(av, &target_move);
self.generate_drop(av, &target_drop);
let king = self.king_position(c).unwrap();
let mut checkers_attacks = Bitboard::empty();
let mut checkers_count = 0;
for ch in self.checkers() {
let pk = self.piece_at(ch).unwrap().piece_kind();
// 龍が斜め位置から王手している場合のみ、他の駒の裏に逃がれることができる可能性がある
if pk == PieceKind::ProRook && ch.file() != king.file() && ch.rank() != king.rank() {
checkers_attacks |= ATTACK_TABLE.hi.attack(ch, &self.occupied_bitboard());
} else {
checkers_attacks |= ATTACK_TABLE.pseudo_attack(pk, ch, c.flip());
}
checkers_count += 1;
}
for to in ATTACK_TABLE.ou.attack(king, c) & !self.player_bitboard(c) & !checkers_attacks {
av.push(Move::Normal {
from: king,
to,
promote: false,
});
}
// 両王手の場合は玉が逃げるしかない
if checkers_count > 1 {
return;
}
let ch = self.checkers().into_iter().next().unwrap();
let target_drop = BETWEEN_TABLE[ch.array_index()][king.array_index()];
let target_move = target_drop | self.checkers();
self.generate_for_fu(av, &target_move);
self.generate_for_ky(av, &target_move);
self.generate_for_ke(av, &target_move);
self.generate_for_gi(av, &target_move);
self.generate_for_ka(av, &target_move);
self.generate_for_hi(av, &target_move);
self.generate_for_ki(av, &target_move);
self.generate_for_um(av, &target_move);
self.generate_for_ry(av, &target_move);
if !target_drop.is_empty() {
// No need to exclude occupied bitboard: Existence of cells between attacker and king is given.
self.generate_drop(av, &target_drop);
}
}
fn generate_for_fu(&self, av: &mut ArrayVec<Move, MAX_LEGAL_MOVES>, target: &Bitboard) {
Expand Down Expand Up @@ -231,6 +228,7 @@ impl Position {
}
}
}
// Generate moves of pieces which moves like KI
fn generate_for_ki(&self, av: &mut ArrayVec<Move, MAX_LEGAL_MOVES>, target: &Bitboard) {
let c = self.side_to_move();
for from in (self.piece_kind_bitboard(PieceKind::Gold)
Expand Down Expand Up @@ -322,6 +320,7 @@ impl Position {
}
}
}
// Checks if the move isn't illegal: king's suicidal moves and moving pinned piece away.
fn is_legal(&self, m: Move) -> bool {
if let Some(from) = m.from() {
let c = self.side_to_move();
Expand Down Expand Up @@ -390,6 +389,7 @@ impl Position {
| (ATTACK_TABLE.ki.attack(to, opp) & (self.piece_kind_bitboard(PieceKind::Gold) | self.piece_kind_bitboard(PieceKind::ProPawn) | self.piece_kind_bitboard(PieceKind::ProLance) | self.piece_kind_bitboard(PieceKind::ProKnight) | self.piece_kind_bitboard(PieceKind::ProSilver) | self.piece_kind_bitboard(PieceKind::ProBishop) | self.piece_kind_bitboard(PieceKind::King)))
) & self.player_bitboard(c)
}
/// Attackers except for king, lance & pawn, which are not applicable to evade check by pawn
#[rustfmt::skip]
fn attackers_to_except_klp(&self, c: Color, to: Square) -> Bitboard {
let opp = c.flip();
Expand Down Expand Up @@ -466,6 +466,31 @@ mod tests {
assert_eq!(593, pos.legal_moves().len());
}

#[test]
fn evasion_moves() {
// TODO: add more cases
// Behind RY
// P1 * * * * * * * * *
// P2 * * * * * * * * *
// P3 * * * * * * * * *
// P4 * * * * * * * * *
// P5 * * * * * * * * *
// P6 * * * * * * * -FU *
// P7 * * * * * * * -RY *
// P8 * * * * * * +OU+KE *
// P9 * * * * -OU * +GI * *
// P+00FU
// P-00AL
// +
let pos = Position::new(
PartialPosition::from_usi("sfen 9/9/9/9/9/7p1/7+r1/6KN1/4k1S2 b Pr2b4g3s3n4l16p 1")
.expect("failed to parse"),
);
let moves = pos.legal_moves();
assert_eq!(1, moves.len());
assert_eq!(Square::SQ_2I, moves[0].to());
}

#[test]
fn pawn_drop() {
{
Expand Down
Loading

0 comments on commit 51c2378

Please sign in to comment.