Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add doc comments & tests. Some refactoring #28

Merged
merged 6 commits into from
Oct 13, 2023
Merged
Show file tree
Hide file tree
Changes from 2 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
75 changes: 67 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,77 @@ 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(&[
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wanted to make sure where the positives are

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
126 changes: 78 additions & 48 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,52 @@ 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) {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unwrapped king_position() as this method should be called during evading

let mut checkers_attacks = Bitboard::empty();
let mut checkers_count = 0;
for ch in self.checkers() {
if let Some(p) = self.piece_at(ch) {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unwrapped piece_at(ch) as checkers() should only return positions where pieces exist

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
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()
{
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);
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() {
Copy link
Contributor Author

@na2hiro na2hiro Sep 14, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm guessing generate_drop is somewhat costly/redundant in case target_drop is empty, which is fairly often like in case attacker is not 飛び駒.

// 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 +232,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,15 +324,16 @@ 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();
let king = [Piece::B_K, Piece::W_K][c.array_index()];
// 玉が相手の攻撃範囲内に動いてしまう指し手は除外
if self.piece_at(from) == Some(king)
&& !self
.attackers_to(c.flip(), m.to(), &self.occupied_bitboard())
.is_empty()
.attackers_to(c.flip(), m.to(), &self.occupied_bitboard())
.is_empty()
{
return false;
}
Expand Down Expand Up @@ -390,6 +393,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 All @@ -405,6 +409,7 @@ impl Position {

#[cfg(test)]
mod tests {
use shogi_core::consts::square::SQ_2I;
use super::*;
use shogi_core::PartialPosition;
use shogi_usi_parser::FromUsi;
Expand Down Expand Up @@ -433,7 +438,7 @@ mod tests {
PartialPosition::from_usi(
"sfen lnsgkg1nl/1r5s1/pppppp1pp/6p2/9/2P6/PP1PPPPPP/7R1/LNSGKGSNL b Bb 1",
)
.expect("failed to parse"),
.expect("failed to parse"),
);
assert_eq!(
43,
Expand Down Expand Up @@ -466,6 +471,31 @@ mod tests {
assert_eq!(593, pos.legal_moves().len());
}

#[test]
fn evasion_moves() {
// TODO: add more cases
// Behind RY
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cover the line 57, described as

龍が斜め位置から王手している場合のみ、他の駒の裏に逃がれることができる可能性がある

// 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!(SQ_2I, moves[0].to());
}

#[test]
fn pawn_drop() {
{
Expand All @@ -485,7 +515,7 @@ mod tests {
PartialPosition::from_usi(
"sfen lnsgkgsnl/1r5s1/pppppppp1/9/8L/9/PPPPPPPP1/1B5S1/LNSGKGSN1 w Pp 1",
)
.expect("failed to parse"),
.expect("failed to parse"),
);
let drop_moves = pos
.legal_moves()
Expand Down Expand Up @@ -518,7 +548,7 @@ mod tests {
PartialPosition::from_usi(
"sfen lnsgkgsn1/1r5s1/pppppppp1/9/8l/9/PPPPPPPP1/1B5S1/LNSGKGSN1 b Ppl 1",
)
.expect("failed to parse"),
.expect("failed to parse"),
);
let drop_moves = pos
.legal_moves()
Expand Down Expand Up @@ -642,7 +672,7 @@ mod tests {
PartialPosition::from_usi(
"sfen 6B2/7np/8k/7P1/7G1/9/9/9/9 b P2rb3g4s3n4l15p 1",
)
.expect("failed to parse"),
.expect("failed to parse"),
),
Square::SQ_1D,
true,
Expand Down
Loading