use shogi_core::{Color, Piece, PieceKind, Square};
use crate::bitboard::{Bitboard, file_of, index_of, on_board, rank_of};
const PAWN_STEPS: [(i8, i8); 1] = [(0, -1)];
const KNIGHT_STEPS: [(i8, i8); 2] = [(-1, -2), (1, -2)];
const SILVER_STEPS: [(i8, i8); 5] = [(-1, -1), (0, -1), (1, -1), (-1, 1), (1, 1)];
const GOLD_STEPS: [(i8, i8); 6] = [(-1, -1), (0, -1), (1, -1), (-1, 0), (1, 0), (0, 1)];
const ORTHOGONAL_STEPS: [(i8, i8); 4] = [(0, -1), (0, 1), (-1, 0), (1, 0)];
const DIAGONAL_STEPS: [(i8, i8); 4] = [(-1, -1), (1, -1), (-1, 1), (1, 1)];
const KING_STEPS: [(i8, i8); 8] = [
(-1, -1),
(0, -1),
(1, -1),
(-1, 0),
(1, 0),
(-1, 1),
(0, 1),
(1, 1),
];
const fn step_table(steps: &[(i8, i8)]) -> [Bitboard; 81] {
let mut table = [Bitboard::EMPTY; 81];
let mut index = 0;
while index < 81 {
let (file, rank) = (file_of(index), rank_of(index));
let mut bits = 0u128;
let mut i = 0;
while i < steps.len() {
let (file_delta, rank_delta) = steps[i];
let (to_file, to_rank) = (file + file_delta, rank + rank_delta);
if on_board(to_file, to_rank) {
bits |= 1 << index_of(to_file, to_rank);
}
i += 1;
}
table[index] = Bitboard::from_bits(bits);
index += 1;
}
table
}
const fn flip_steps<const N: usize>(steps: [(i8, i8); N]) -> [(i8, i8); N] {
let mut flipped = steps;
let mut i = 0;
while i < N {
let (file_delta, rank_delta) = steps[i];
flipped[i] = (-file_delta, -rank_delta);
i += 1;
}
flipped
}
const fn step_table_both<const N: usize>(steps: [(i8, i8); N]) -> [[Bitboard; 81]; 2] {
[step_table(&steps), step_table(&flip_steps(steps))]
}
const fn ray_table(steps: &[(i8, i8)]) -> [Bitboard; 81] {
let mut table = [Bitboard::EMPTY; 81];
let mut index = 0;
while index < 81 {
let mut bits = 0u128;
let mut i = 0;
while i < steps.len() {
let (file_delta, rank_delta) = steps[i];
let (mut file, mut rank) = (file_of(index), rank_of(index));
loop {
file += file_delta;
rank += rank_delta;
if !on_board(file, rank) {
break;
}
bits |= 1 << index_of(file, rank);
}
i += 1;
}
table[index] = Bitboard::from_bits(bits);
index += 1;
}
table
}
const fn ray_table_both<const N: usize>(steps: [(i8, i8); N]) -> [[Bitboard; 81]; 2] {
[ray_table(&steps), ray_table(&flip_steps(steps))]
}
const fn box_table(file_radius: i8, rank_radius: i8) -> [Bitboard; 81] {
let mut table = [Bitboard::EMPTY; 81];
let mut index = 0;
while index < 81 {
let (file, rank) = (file_of(index), rank_of(index));
let mut bits = 0u128;
let mut file_delta = -file_radius;
while file_delta <= file_radius {
let mut rank_delta = -rank_radius;
while rank_delta <= rank_radius {
let (to_file, to_rank) = (file + file_delta, rank + rank_delta);
if on_board(to_file, to_rank) {
bits |= 1 << index_of(to_file, to_rank);
}
rank_delta += 1;
}
file_delta += 1;
}
table[index] = Bitboard::from_bits(bits);
index += 1;
}
table
}
static PAWN_ATTACKS: [[Bitboard; 81]; 2] = step_table_both(PAWN_STEPS);
static KNIGHT_ATTACKS: [[Bitboard; 81]; 2] = step_table_both(KNIGHT_STEPS);
static SILVER_ATTACKS: [[Bitboard; 81]; 2] = step_table_both(SILVER_STEPS);
static GOLD_ATTACKS: [[Bitboard; 81]; 2] = step_table_both(GOLD_STEPS);
static KING_ATTACKS: [Bitboard; 81] = step_table(&KING_STEPS);
static ORTHOGONAL_ATTACKS: [Bitboard; 81] = step_table(&ORTHOGONAL_STEPS);
static DIAGONAL_ATTACKS: [Bitboard; 81] = step_table(&DIAGONAL_STEPS);
static ROOK_RAYS: [Bitboard; 81] = ray_table(&ORTHOGONAL_STEPS);
static BISHOP_RAYS: [Bitboard; 81] = ray_table(&DIAGONAL_STEPS);
static LANCE_RAYS: [[Bitboard; 81]; 2] = ray_table_both(PAWN_STEPS);
static STEP_ATTACKS: [[Bitboard; 81]; 32] = step_attacks_table();
const fn step_attacks_table() -> [[Bitboard; 81]; 32] {
let mut table = [[Bitboard::EMPTY; 81]; 32];
let pawn = step_table_both(PAWN_STEPS);
let knight = step_table_both(KNIGHT_STEPS);
let silver = step_table_both(SILVER_STEPS);
let gold = step_table_both(GOLD_STEPS);
let king = step_table(&KING_STEPS);
let orthogonal = step_table(&ORTHOGONAL_STEPS);
let diagonal = step_table(&DIAGONAL_STEPS);
let mut color = 0;
while color < 2 {
let base = color * 16;
table[base + PieceKind::Pawn as usize] = pawn[color];
table[base + PieceKind::Knight as usize] = knight[color];
table[base + PieceKind::Silver as usize] = silver[color];
table[base + PieceKind::Gold as usize] = gold[color];
table[base + PieceKind::King as usize] = king;
table[base + PieceKind::ProPawn as usize] = gold[color];
table[base + PieceKind::ProLance as usize] = gold[color];
table[base + PieceKind::ProKnight as usize] = gold[color];
table[base + PieceKind::ProSilver as usize] = gold[color];
table[base + PieceKind::ProBishop as usize] = orthogonal;
table[base + PieceKind::ProRook as usize] = diagonal;
color += 1;
}
table
}
const SLIDER_KINDS: u32 = (1 << PieceKind::Lance as u32)
| (1 << PieceKind::Bishop as u32)
| (1 << PieceKind::Rook as u32)
| (1 << PieceKind::ProBishop as u32)
| (1 << PieceKind::ProRook as u32);
static STEP_ATTACKER_ZONE: [Bitboard; 81] = box_table(2, 3);
const fn slider_attacker_zone(steps: &[(i8, i8)]) -> [Bitboard; 81] {
let rays = ray_table(steps);
let mut table = [Bitboard::EMPTY; 81];
let mut index = 0;
while index < 81 {
let (file, rank) = (file_of(index), rank_of(index));
let mut bits = 0u128;
let mut i = 0;
while i < KING_STEPS.len() {
let (file_delta, rank_delta) = KING_STEPS[i];
let (to_file, to_rank) = (file + file_delta, rank + rank_delta);
if on_board(to_file, to_rank) {
bits |= rays[index_of(to_file, to_rank)].bits();
}
i += 1;
}
table[index] = Bitboard::from_bits(bits);
index += 1;
}
table
}
static ORTHOGONAL_ATTACKER_ZONE: [Bitboard; 81] = slider_attacker_zone(&ORTHOGONAL_STEPS);
static DIAGONAL_ATTACKER_ZONE: [Bitboard; 81] = slider_attacker_zone(&DIAGONAL_STEPS);
static BETWEEN: [[Bitboard; 81]; 81] = between_table();
const fn between_table() -> [[Bitboard; 81]; 81] {
let mut table = [[Bitboard::EMPTY; 81]; 81];
let mut from = 0;
while from < 81 {
let mut step = 0;
while step < KING_STEPS.len() {
let (file_delta, rank_delta) = KING_STEPS[step];
let (mut file, mut rank) = (file_of(from), rank_of(from));
let mut between = 0u128;
loop {
file += file_delta;
rank += rank_delta;
if !on_board(file, rank) {
break;
}
let to = index_of(file, rank);
table[from][to] = Bitboard::from_bits(between);
between |= 1 << to;
}
step += 1;
}
from += 1;
}
table
}
pub(crate) fn pawn_attacks(color: Color, square: Square) -> Bitboard {
PAWN_ATTACKS[color.array_index()][square.array_index()]
}
pub(crate) fn knight_attacks(color: Color, square: Square) -> Bitboard {
KNIGHT_ATTACKS[color.array_index()][square.array_index()]
}
pub(crate) fn silver_attacks(color: Color, square: Square) -> Bitboard {
SILVER_ATTACKS[color.array_index()][square.array_index()]
}
pub(crate) fn gold_attacks(color: Color, square: Square) -> Bitboard {
GOLD_ATTACKS[color.array_index()][square.array_index()]
}
pub(crate) fn king_attacks(square: Square) -> Bitboard {
KING_ATTACKS[square.array_index()]
}
#[inline(always)]
pub(crate) fn step_attacker_zone(king: Square) -> Bitboard {
STEP_ATTACKER_ZONE[king.array_index()]
}
#[inline(always)]
pub(crate) fn orthogonal_attacker_zone(king: Square) -> Bitboard {
ORTHOGONAL_ATTACKER_ZONE[king.array_index()]
}
#[inline(always)]
pub(crate) fn diagonal_attacker_zone(king: Square) -> Bitboard {
DIAGONAL_ATTACKER_ZONE[king.array_index()]
}
pub(crate) fn orthogonal_attacks(square: Square) -> Bitboard {
ORTHOGONAL_ATTACKS[square.array_index()]
}
pub(crate) fn diagonal_attacks(square: Square) -> Bitboard {
DIAGONAL_ATTACKS[square.array_index()]
}
#[inline(always)]
pub(crate) fn rook_rays(square: Square) -> Bitboard {
ROOK_RAYS[square.array_index()]
}
#[inline(always)]
pub(crate) fn bishop_rays(square: Square) -> Bitboard {
BISHOP_RAYS[square.array_index()]
}
#[inline(always)]
pub(crate) fn lance_rays(color: Color, square: Square) -> Bitboard {
LANCE_RAYS[color.array_index()][square.array_index()]
}
static LINE: [[Bitboard; 81]; 81] = line_table();
const AXES: [(i8, i8); 4] = [(1, 0), (0, 1), (1, 1), (1, -1)];
const fn line_table() -> [[Bitboard; 81]; 81] {
let mut table = [[Bitboard::EMPTY; 81]; 81];
let mut a = 0;
while a < 81 {
let mut axis = 0;
while axis < 4 {
let (file_delta, rank_delta) = AXES[axis];
let mut line = 1u128 << a;
let mut direction = 0;
while direction < 2 {
let sign: i8 = if direction == 0 { 1 } else { -1 };
let (mut file, mut rank) = (file_of(a), rank_of(a));
loop {
file += sign * file_delta;
rank += sign * rank_delta;
if !on_board(file, rank) {
break;
}
line |= 1 << index_of(file, rank);
}
direction += 1;
}
let mut rest = line & !(1 << a);
while rest != 0 {
let b = rest.trailing_zeros() as usize;
table[a][b] = Bitboard::from_bits(line);
rest &= rest - 1;
}
axis += 1;
}
a += 1;
}
table
}
pub(crate) fn between(a: Square, b: Square) -> Bitboard {
BETWEEN[a.array_index()][b.array_index()]
}
#[inline(always)]
pub(crate) fn line(a: Square, b: Square) -> Bitboard {
LINE[a.array_index()][b.array_index()]
}
const fn relative_rank_mask(color: usize, max_relative_rank: i8) -> Bitboard {
let mut bits = 0u128;
let mut index = 0;
while index < 81 {
let rank = rank_of(index);
let relative = if color == 0 { rank } else { 10 - rank };
if relative <= max_relative_rank {
bits |= 1 << index;
}
index += 1;
}
Bitboard::from_bits(bits)
}
const fn relative_rank_masks(max_relative_rank: i8) -> [Bitboard; 2] {
[
relative_rank_mask(0, max_relative_rank),
relative_rank_mask(1, max_relative_rank),
]
}
static PROMOTION_ZONE: [Bitboard; 2] = relative_rank_masks(3);
static LAST_RANK: [Bitboard; 2] = relative_rank_masks(1);
static LAST_TWO_RANKS: [Bitboard; 2] = relative_rank_masks(2);
static PROMOTION_MASK: [[Bitboard; 81]; 2] = promotion_mask_table();
const fn promotion_mask_table() -> [[Bitboard; 81]; 2] {
let mut table = [[Bitboard::EMPTY; 81]; 2];
let mut color = 0;
while color < 2 {
let zone = PROMOTION_ZONE[color];
let mut index = 0;
while index < 81 {
table[color][index] = if zone.bits() & (1 << index) != 0 {
Bitboard::ALL
} else {
zone
};
index += 1;
}
color += 1;
}
table
}
#[inline(always)]
pub(crate) fn promotion_mask(color: Color, from: Square) -> Bitboard {
PROMOTION_MASK[color.array_index()][from.array_index()]
}
pub(crate) const PROMOTABLE_KINDS: u32 = (1 << PieceKind::Pawn as u32)
| (1 << PieceKind::Lance as u32)
| (1 << PieceKind::Knight as u32)
| (1 << PieceKind::Silver as u32)
| (1 << PieceKind::Bishop as u32)
| (1 << PieceKind::Rook as u32);
static FORCED_PROMOTION: [Bitboard; 32] = forced_promotion_table();
const fn forced_promotion_table() -> [Bitboard; 32] {
let mut table = [Bitboard::EMPTY; 32];
let mut color = 0;
while color < 2 {
let base = color * 16;
table[base + PieceKind::Pawn as usize] = LAST_RANK[color];
table[base + PieceKind::Lance as usize] = LAST_RANK[color];
table[base + PieceKind::Knight as usize] = LAST_TWO_RANKS[color];
color += 1;
}
table
}
#[inline(always)]
pub(crate) fn forced_promotion_zone(piece: Piece) -> Bitboard {
FORCED_PROMOTION[(piece.as_u8() & 31) as usize]
}
pub(crate) use crate::sliders::active::{bishop_attacks, rook_attacks};
pub(crate) use crate::sliders::lance_attacks;
#[inline]
pub(crate) fn attacks_of(piece: Piece, square: Square, occupied: Bitboard) -> Bitboard {
let index = (piece.as_u8() & 31) as usize;
if SLIDER_KINDS >> (index & 15) & 1 == 0 {
return STEP_ATTACKS[index][square.array_index()];
}
let (piece_kind, color) = piece.to_parts();
match piece_kind {
PieceKind::Lance => lance_attacks(color, square, occupied),
PieceKind::Bishop => bishop_attacks(square, occupied),
PieceKind::Rook => rook_attacks(square, occupied),
PieceKind::ProBishop => {
bishop_attacks(square, occupied) | STEP_ATTACKS[index][square.array_index()]
}
_ => rook_attacks(square, occupied) | STEP_ATTACKS[index][square.array_index()],
}
}
#[cfg(test)]
mod tests {
use super::*;
fn sq(file: u8, rank: u8) -> Square {
Square::new(file, rank).unwrap()
}
fn step_table_reference(steps: &[(i8, i8)]) -> [Bitboard; 81] {
let mut table = [Bitboard::EMPTY; 81];
for square in Square::all() {
for &(file_delta, rank_delta) in steps {
if let Some(to) = square.shift(file_delta, rank_delta) {
table[square.array_index()] |= Bitboard::single(to);
}
}
}
table
}
fn step_table_both_reference(steps: &[(i8, i8)]) -> [[Bitboard; 81]; 2] {
let flipped: Vec<(i8, i8)> = steps
.iter()
.map(|&(file_delta, rank_delta)| (-file_delta, -rank_delta))
.collect();
[step_table_reference(steps), step_table_reference(&flipped)]
}
#[test]
fn const_index_arithmetic_matches_square() {
for square in Square::all() {
let index = square.array_index();
assert_eq!(file_of(index), square.file() as i8);
assert_eq!(rank_of(index), square.rank() as i8);
assert_eq!(index_of(square.file() as i8, square.rank() as i8), index);
}
}
#[test]
fn const_step_tables_match_reference() {
for (name, table, steps) in [
("king", &KING_ATTACKS, &KING_STEPS[..]),
("orthogonal", &ORTHOGONAL_ATTACKS, &ORTHOGONAL_STEPS[..]),
("diagonal", &DIAGONAL_ATTACKS, &DIAGONAL_STEPS[..]),
] {
assert_eq!(*table, step_table_reference(steps), "{name}");
}
for (name, table, steps) in [
("pawn", &PAWN_ATTACKS, &PAWN_STEPS[..]),
("knight", &KNIGHT_ATTACKS, &KNIGHT_STEPS[..]),
("silver", &SILVER_ATTACKS, &SILVER_STEPS[..]),
("gold", &GOLD_ATTACKS, &GOLD_STEPS[..]),
] {
assert_eq!(*table, step_table_both_reference(steps), "{name}");
}
}
#[test]
fn const_between_table_matches_reference() {
let mut reference = Box::new([[Bitboard::EMPTY; 81]; 81]);
for from in Square::all() {
for (file_delta, rank_delta) in KING_STEPS {
let mut between = Bitboard::EMPTY;
let mut current = from;
while let Some(next) = current.shift(file_delta, rank_delta) {
reference[from.array_index()][next.array_index()] = between;
between |= Bitboard::single(next);
current = next;
}
}
}
assert_eq!(BETWEEN, *reference);
}
#[test]
fn const_line_table_matches_reference() {
let mut reference = Box::new([[Bitboard::EMPTY; 81]; 81]);
for from in Square::all() {
for (file_delta, rank_delta) in AXES {
let mut line = Bitboard::single(from);
for sign in [1, -1] {
let mut current = from;
while let Some(next) = current.shift(sign * file_delta, sign * rank_delta) {
line |= Bitboard::single(next);
current = next;
}
}
for to in line {
if to != from {
reference[from.array_index()][to.array_index()] = line;
}
}
}
}
assert_eq!(LINE, *reference);
}
#[test]
fn line_supports_the_pin_mask() {
for a in Square::all() {
assert!(line(a, a).is_empty(), "LINE[a][a] must be empty");
for b in Square::all() {
let aligned = a != b
&& (a.file() == b.file()
|| a.rank() == b.rank()
|| (a.file() as i8 - b.file() as i8).abs()
== (a.rank() as i8 - b.rank() as i8).abs());
let line = line(a, b);
assert_eq!(aligned, !line.is_empty(), "alignment vs LINE: {a:?} {b:?}");
if !aligned {
continue;
}
assert!(
line.contains(a) && line.contains(b),
"endpoints on the line"
);
assert_eq!(line, self::line(b, a), "LINE must be symmetric");
assert_eq!(between(a, b) & !line, Bitboard::EMPTY, "between ⊆ line");
for c in line {
if c != a {
assert_eq!(self::line(a, c), line, "{a:?} {b:?} {c:?}");
}
}
}
}
}
#[test]
fn step_attacker_zone_covers_every_step_piece() {
let step_kinds = [
PieceKind::Pawn,
PieceKind::Knight,
PieceKind::Silver,
PieceKind::Gold,
PieceKind::King,
PieceKind::ProPawn,
PieceKind::ProLance,
PieceKind::ProKnight,
PieceKind::ProSilver,
];
let mut covered = 0;
for king in Square::all() {
let zone = step_attacker_zone(king);
for to in king_attacks(king) {
for from in Square::all() {
for color in Color::all() {
for piece_kind in step_kinds {
let piece = Piece::new(piece_kind, color);
if attacks_of(piece, from, Bitboard::EMPTY).contains(to) {
assert!(
zone.contains(from),
"{piece:?} on {from:?} attacks {to:?} next to king {king:?}, \
but {from:?} is outside the zone"
);
covered += 1;
}
}
}
}
}
}
assert!(covered > 10_000, "test exercised only {covered} attacks");
}
#[test]
fn slider_attacker_zones_cover_every_slider() {
let slider_kinds = [
PieceKind::Lance,
PieceKind::Bishop,
PieceKind::Rook,
PieceKind::ProBishop,
PieceKind::ProRook,
];
let mut occupancies = vec![Bitboard::EMPTY, Bitboard::ALL];
let mut state = 0x0053_4855_4e53_4149u64;
for _ in 0..64 {
let mut bits = 0u128;
for _ in 0..2 {
state = state
.wrapping_mul(6364136223846793005)
.wrapping_add(1442695040888963407);
bits = (bits << 64) | state as u128;
}
occupancies.push(Bitboard::ALL & Bitboard::from_bits(bits & ((1 << 81) - 1)));
}
for &piece_kind in &slider_kinds {
for color in Color::all() {
let piece = Piece::new(piece_kind, color);
for from in Square::all() {
let unblocked = attacks_of(piece, from, Bitboard::EMPTY);
for &occupied in &occupancies {
assert_eq!(
attacks_of(piece, from, occupied) & !unblocked,
Bitboard::EMPTY,
"{piece:?} on {from:?} attacks more with blockers than without"
);
}
}
}
}
let mut step_only = 0;
let mut covered = 0;
for king in Square::all() {
let steps = step_attacker_zone(king);
let orthogonal = steps | orthogonal_attacker_zone(king);
let diagonal = steps | diagonal_attacker_zone(king);
for to in king_attacks(king) {
for from in Square::all() {
for color in Color::all() {
for piece_kind in slider_kinds {
let piece = Piece::new(piece_kind, color);
if !attacks_of(piece, from, Bitboard::EMPTY).contains(to) {
continue;
}
let (zone, rays) = match piece_kind {
PieceKind::Bishop | PieceKind::ProBishop => {
(diagonal, diagonal_attacker_zone(king))
}
_ => (orthogonal, orthogonal_attacker_zone(king)),
};
assert!(
zone.contains(from),
"{piece:?} on {from:?} attacks {to:?} next to king {king:?}, \
but {from:?} is outside the zone"
);
if !rays.contains(from) {
step_only += 1;
}
covered += 1;
}
}
}
}
}
assert!(covered > 10_000, "test exercised only {covered} attacks");
assert!(
step_only > 0,
"no slider was caught by the step zone alone; the step term looks removable"
);
}
#[test]
fn step_attacker_zone_is_bounded() {
assert_eq!(step_attacker_zone(sq(5, 5)).count(), 35);
assert_eq!(step_attacker_zone(sq(1, 1)).count(), 3 * 4);
for king in Square::all() {
assert!(step_attacker_zone(king).contains(king));
assert!(step_attacker_zone(king).count() < 81);
}
}
#[test]
fn slider_attacker_zones_are_bounded() {
let (mut orthogonal, mut diagonal) = (0, 0);
for king in Square::all() {
let (o, d) = (orthogonal_attacker_zone(king), diagonal_attacker_zone(king));
assert!(o.contains(king) && d.contains(king), "{king:?}");
assert!(
o.count() < 81 && d.count() < 81,
"{king:?} admits the board"
);
orthogonal += o.count() as usize;
diagonal += d.count() as usize;
}
assert_eq!(orthogonal, 3425);
assert_eq!(diagonal, 3617);
assert_eq!(orthogonal_attacker_zone(sq(5, 5)).count(), 45);
assert_eq!(orthogonal_attacker_zone(sq(1, 1)).count(), 32);
assert_eq!(diagonal_attacker_zone(sq(5, 5)).count(), 65);
assert_eq!(diagonal_attacker_zone(sq(1, 1)).count(), 26);
}
#[test]
fn pawn_moves_forward() {
assert_eq!(
pawn_attacks(Color::Black, sq(5, 5)),
Bitboard::single(sq(5, 4))
);
assert_eq!(
pawn_attacks(Color::White, sq(5, 5)),
Bitboard::single(sq(5, 6))
);
assert!(pawn_attacks(Color::Black, sq(5, 1)).is_empty());
assert!(pawn_attacks(Color::White, sq(5, 9)).is_empty());
}
#[test]
fn knight_edges() {
let attacks = knight_attacks(Color::Black, sq(5, 5));
assert_eq!(
attacks,
Bitboard::single(sq(4, 3)) | Bitboard::single(sq(6, 3))
);
assert_eq!(
knight_attacks(Color::Black, sq(1, 5)),
Bitboard::single(sq(2, 3))
);
assert_eq!(
knight_attacks(Color::White, sq(9, 5)),
Bitboard::single(sq(8, 7))
);
}
#[test]
fn step_counts_center() {
assert_eq!(silver_attacks(Color::Black, sq(5, 5)).count(), 5);
assert_eq!(gold_attacks(Color::Black, sq(5, 5)).count(), 6);
assert_eq!(king_attacks(sq(5, 5)).count(), 8);
assert_eq!(king_attacks(sq(1, 1)).count(), 3);
}
#[test]
fn color_symmetry() {
for square in Square::all() {
let flipped = square.flip();
for (black, white) in [
(
silver_attacks(Color::Black, square),
silver_attacks(Color::White, flipped),
),
(
gold_attacks(Color::Black, square),
gold_attacks(Color::White, flipped),
),
] {
let mirrored =
white.fold(Bitboard::EMPTY, |acc, s| acc | Bitboard::single(s.flip()));
assert_eq!(black, mirrored);
}
}
}
#[test]
fn sliders_empty_board() {
assert_eq!(rook_attacks(sq(5, 5), Bitboard::EMPTY).count(), 16);
assert_eq!(bishop_attacks(sq(5, 5), Bitboard::EMPTY).count(), 16);
assert_eq!(bishop_attacks(sq(1, 1), Bitboard::EMPTY).count(), 8);
assert_eq!(
lance_attacks(Color::Black, sq(5, 9), Bitboard::EMPTY).count(),
8
);
assert!(lance_attacks(Color::Black, sq(5, 1), Bitboard::EMPTY).is_empty());
}
#[test]
fn empty_board_rays_match_the_naive_backend() {
use crate::sliders::naive;
for square in Square::all() {
assert_eq!(
rook_rays(square),
naive::rook_attacks(square, Bitboard::EMPTY),
"rook rays at {square:?}"
);
assert_eq!(
bishop_rays(square),
naive::bishop_attacks(square, Bitboard::EMPTY),
"bishop rays at {square:?}"
);
for color in Color::all() {
assert_eq!(
lance_rays(color, square),
naive::lance_attacks(color, square, Bitboard::EMPTY),
"lance rays for {color:?} at {square:?}"
);
}
}
}
#[test]
fn sliders_stop_at_blockers() {
let occupied = Bitboard::single(sq(5, 3));
let attacks = rook_attacks(sq(5, 7), occupied);
assert!(attacks.contains(sq(5, 3)));
assert!(!attacks.contains(sq(5, 2)));
assert!(attacks.contains(sq(5, 6)));
}
#[test]
fn between_pairs() {
assert_eq!(between(sq(1, 1), sq(9, 1)).count(), 7);
assert_eq!(between(sq(1, 1), sq(9, 9)).count(), 7);
assert!(between(sq(1, 1), sq(1, 2)).is_empty());
assert!(between(sq(1, 1), sq(5, 4)).is_empty());
assert!(between(sq(2, 3), sq(3, 5)).is_empty());
for &(a, b) in &[(sq(2, 2), sq(8, 8)), (sq(3, 9), sq(3, 1))] {
assert_eq!(between(a, b), between(b, a));
}
}
#[test]
fn promoted_slider_attacks() {
let horse = attacks_of(
Piece::new(PieceKind::ProBishop, Color::Black),
sq(5, 5),
Bitboard::EMPTY,
);
assert_eq!(horse.count(), 16 + 4);
let dragon = attacks_of(
Piece::new(PieceKind::ProRook, Color::White),
sq(5, 5),
Bitboard::EMPTY,
);
assert_eq!(dragon.count(), 16 + 4);
}
#[test]
fn piece_index_matches_as_u8() {
let mut seen = [None; 32];
for piece in Piece::all() {
let (piece_kind, color) = piece.to_parts();
let expected = 16 * color.array_index() + piece_kind as usize;
let index = (piece.as_u8() & 31) as usize;
assert_eq!(index, expected, "index arithmetic disagrees for {piece:?}");
assert_eq!(index, piece.as_u8() as usize, "the mask dropped a bit");
assert!(
seen[index].replace(piece).is_none(),
"index {index} is shared by two pieces"
);
assert_eq!(index & 15, piece_kind as usize);
}
}
#[test]
fn promotion_tables_match_the_rules() {
for piece in Piece::all() {
let (piece_kind, color) = piece.to_parts();
assert_eq!(
PROMOTABLE_KINDS >> (piece.as_u8() & 15) & 1 != 0,
piece_kind.promote().is_some(),
"PROMOTABLE_KINDS disagrees for {piece:?}"
);
let forced = match piece_kind {
PieceKind::Pawn | PieceKind::Lance => LAST_RANK[color.array_index()],
PieceKind::Knight => LAST_TWO_RANKS[color.array_index()],
_ => Bitboard::EMPTY,
};
assert_eq!(
forced_promotion_zone(piece),
forced,
"forced_promotion_zone disagrees for {piece:?}"
);
for from in Square::all() {
let zone = PROMOTION_ZONE[color.array_index()];
let expected = if zone.contains(from) {
Bitboard::ALL
} else {
zone
};
assert_eq!(
promotion_mask(color, from),
expected,
"promotion_mask disagrees for {color:?} from {from:?}"
);
}
}
}
#[test]
fn attacks_of_matches_the_per_kind_tables() {
fn reference(piece: Piece, square: Square, occupied: Bitboard) -> Bitboard {
let (piece_kind, color) = piece.to_parts();
match piece_kind {
PieceKind::Pawn => pawn_attacks(color, square),
PieceKind::Lance => lance_attacks(color, square, occupied),
PieceKind::Knight => knight_attacks(color, square),
PieceKind::Silver => silver_attacks(color, square),
PieceKind::Gold
| PieceKind::ProPawn
| PieceKind::ProLance
| PieceKind::ProKnight
| PieceKind::ProSilver => gold_attacks(color, square),
PieceKind::King => king_attacks(square),
PieceKind::Bishop => bishop_attacks(square, occupied),
PieceKind::Rook => rook_attacks(square, occupied),
PieceKind::ProBishop => {
bishop_attacks(square, occupied) | orthogonal_attacks(square)
}
PieceKind::ProRook => rook_attacks(square, occupied) | diagonal_attacks(square),
}
}
let mut occupancies = vec![Bitboard::EMPTY, Bitboard::ALL];
let mut state = 0x9e3779b97f4a7c15u64;
for _ in 0..64 {
let mut bits = 0u128;
for _ in 0..2 {
state = state.wrapping_mul(6364136223846793005).wrapping_add(1);
bits = (bits << 64) | state as u128;
}
occupancies.push(Bitboard::ALL & Bitboard::from_bits(bits & ((1 << 81) - 1)));
}
for piece in Piece::all() {
for square in Square::all() {
for &occupied in &occupancies {
assert_eq!(
attacks_of(piece, square, occupied),
reference(piece, square, occupied),
"{piece:?} on {square:?} with {occupied:?}"
);
}
}
}
}
}