11-add-attack-detection #15
@@ -530,6 +530,50 @@ int gen_pseudo_moves(const struct Board *board, struct Move *moves, bool capture
|
||||
return count;
|
||||
}
|
||||
|
||||
uint64_t attackers_to(const struct Board *board, int sq, enum Color by) {
|
||||
uint64_t occ = board->occ[BOTH];
|
||||
|
||||
uint64_t pawns = (by == WHITE) ? board->pieces[P] : board->pieces[p];
|
||||
uint64_t knights = (by == WHITE) ? board->pieces[N] : board->pieces[n];
|
||||
uint64_t bishops = (by == WHITE) ? board->pieces[B] : board->pieces[b];
|
||||
uint64_t rooks = (by == WHITE) ? board->pieces[R] : board->pieces[r];
|
||||
uint64_t queens = (by == WHITE) ? board->pieces[Q] : board->pieces[q];
|
||||
uint64_t kings = (by == WHITE) ? board->pieces[K] : board->pieces[k];
|
||||
|
||||
uint64_t attacks = 0;
|
||||
|
||||
attacks |= PAWN_ATTACKS[by ^ 1][sq] & pawns;
|
||||
attacks |= KNIGHT_ATTACKS[sq] & knights;
|
||||
attacks |= KING_ATTACKS[sq] & kings;
|
||||
|
||||
// Squares than can see sq given the occupancy of both sides but intersect
|
||||
// with the specified color pieces.
|
||||
attacks |= bishop_attacks(sq, occ) & (bishops | queens);
|
||||
attacks |= rook_attacks(sq, occ) & (rooks | queens);
|
||||
|
||||
return attacks;
|
||||
}
|
||||
|
||||
bool in_check(const struct Board *board, enum Color side) {
|
||||
uint64_t king_bb = (side == WHITE) ? board->pieces[K] : board->pieces[k];
|
||||
|
||||
// Do we need this check here?
|
||||
// This would be an illegal position.
|
||||
if (!king_bb) return false;
|
||||
|
||||
int king_sq = pop_lsb_index(&king_bb);
|
||||
|
||||
// Is the king square in the attack map of opposite side?
|
||||
return attackers_to(board, king_sq, side ^ 1) != 0;
|
||||
}
|
||||
|
||||
bool square_attacked(const struct Board *board, int sq, enum Color by) {
|
||||
// Should we have another way to calculate this and have
|
||||
// earlier exits in the code? For example, we could iterate
|
||||
// each piece type and return boolean sooner and skip the
|
||||
// ray scanning.
|
||||
return attackers_to(board, sq, by) != 0;
|
||||
}
|
||||
|
||||
void print_board(const struct Board *board) {
|
||||
const char PIECE_CH[12] = {
|
||||
|
||||
@@ -14,8 +14,8 @@ class ChessLibTestBase(unittest.TestCase):
|
||||
init_attack_caches()
|
||||
|
||||
cls.KNIGHT_ATTACKS = KNIGHT_ATTACKS
|
||||
cls.KING_ATTACKS = KING_ATTACKS
|
||||
cls.PAWN_ATTACKS = PAWN_ATTACKS
|
||||
cls.KING_ATTACKS = KING_ATTACKS
|
||||
cls.PAWN_ATTACKS = PAWN_ATTACKS
|
||||
|
||||
|
||||
def setUp(self):
|
||||
|
||||
@@ -87,6 +87,16 @@ gen_queen_moves = _bind_opt("gen_queen_moves", *PIECE_SIG)
|
||||
gen_king_moves = _bind_opt("gen_king_moves", *PIECE_SIG)
|
||||
|
||||
|
||||
# Attack checks.
|
||||
ATTACKED_SIG = (C.POINTER(Board), C.c_int, C.c_int)
|
||||
INCHECK_ARGS = (C.POINTER(Board), C.c_int)
|
||||
ATTACKERS_TO = (C.POINTER(Board), C.c_int, C.c_int)
|
||||
|
||||
square_attacked = _bind_opt("square_attacked", ATTACKED_SIG, C.c_bool)
|
||||
in_check = _bind_opt("in_check", INCHECK_ARGS, C.c_bool)
|
||||
attackers_to = _bind_opt("attackers_to", ATTACKERS_TO, C.c_uint64)
|
||||
|
||||
|
||||
# Attack cache tables.
|
||||
KnightArr = (C.c_uint64 * 64)
|
||||
KingArr = (C.c_uint64 * 64)
|
||||
@@ -102,8 +112,8 @@ except ValueError:
|
||||
|
||||
def init_attack_caches():
|
||||
if create_knight_attack_cache: create_knight_attack_cache()
|
||||
if create_king_attack_cache: create_king_attack_cache()
|
||||
if create_pawn_attack_cache: create_pawn_attack_cache()
|
||||
if create_king_attack_cache: create_king_attack_cache()
|
||||
if create_pawn_attack_cache: create_pawn_attack_cache()
|
||||
|
||||
|
||||
def load_fen(board, fen):
|
||||
@@ -116,6 +126,18 @@ def gen_moves(board, captures_only=False, cap=256):
|
||||
return buf, n
|
||||
|
||||
|
||||
def is_square_attacked(board, sq, by):
|
||||
return bool(square_attacked(C.byref(board), int(sq), int(by)))
|
||||
|
||||
|
||||
def is_in_check(board, side):
|
||||
return bool(in_check(C.byref(board), int(side)))
|
||||
|
||||
|
||||
def get_attackers_to(board, sq, by):
|
||||
return int(attackers_to(C.byref(board), int(sq), int(by)))
|
||||
|
||||
|
||||
def sq_to_coord(sq):
|
||||
return chr(ord('a') + (sq % 8)) + chr(ord('1') + (sq // 8))
|
||||
|
||||
@@ -137,7 +159,7 @@ def popcount(x: int) -> int:
|
||||
return x.bit_count()
|
||||
|
||||
|
||||
def draw_bb(mask: int, origin: int | None = None) -> str:
|
||||
def draw_bb(mask, origin=None):
|
||||
print("\n")
|
||||
lines = []
|
||||
for r in range(7, -1, -1):
|
||||
|
||||
138
test/test_attack_to.py
Normal file
138
test/test_attack_to.py
Normal file
@@ -0,0 +1,138 @@
|
||||
import ctypes
|
||||
from test.base import ChessLibTestBase
|
||||
from test.chess_ffi import get_attackers_to
|
||||
from test.chess_ffi import is_square_attacked
|
||||
from test.chess_ffi import is_in_check
|
||||
from test.chess_ffi import sq
|
||||
from test.chess_ffi import Board
|
||||
from test.chess_ffi import BLACK
|
||||
from test.chess_ffi import WHITE
|
||||
from test.chess_ffi import draw_bb
|
||||
|
||||
|
||||
class TestAttackers(ChessLibTestBase):
|
||||
def test_square_attacked(self):
|
||||
cases = [
|
||||
# Pawns: bp d5 attacks c4/e4; not d4
|
||||
("8/8/8/3p4/8/8/8/8 w - - 0 1", "c4", BLACK, True, "bp d5 -> c4"),
|
||||
("8/8/8/3p4/8/8/8/8 w - - 0 1", "e4", BLACK, True, "bp d5 -> e4"),
|
||||
("8/8/8/3p4/8/8/8/8 w - - 0 1", "d4", BLACK, False, "bp d5 not d4"),
|
||||
|
||||
# Knights
|
||||
("8/8/8/3n4/8/8/8/8 w - - 0 1", "f4", BLACK, True, "bn d5 -> f4"),
|
||||
("8/8/8/3n4/8/8/8/8 w - - 0 1", "e7", BLACK, True, "bn d5 -> e7"),
|
||||
("8/8/8/3n4/8/8/8/8 w - - 0 1", "e5", BLACK, False, "bn d5 not e5"),
|
||||
|
||||
# Bishops (wB d4)
|
||||
("8/8/8/8/3B4/8/8/8 w - - 0 1", "c5", WHITE, True, "wB d4 -> c5"),
|
||||
("8/8/8/8/3B4/8/8/8 w - - 0 1", "e5", WHITE, True, "wB d4 -> e5"),
|
||||
|
||||
# Rooks (wR e4)
|
||||
("8/8/8/8/4R3/8/8/8 w - - 0 1", "e8", WHITE, True, "wR e4 -> e8 clear"),
|
||||
("4k3/4p3/8/8/4R3/8/8/8 w - - 0 1", "e8", WHITE, False, "wR e4 blocked by pe7"),
|
||||
("4k3/4p3/8/8/4R3/8/8/8 w - - 0 1", "e7", WHITE, True, "wR e4 blocked can attack pawn on e7"),
|
||||
("8/8/8/8/4R3/8/8/8 w - - 0 1", "a4", WHITE, True, "wR e4 -> a4"),
|
||||
|
||||
# Queens (wQ d4)
|
||||
("8/8/8/8/3Q4/8/8/8 w - - 0 1", "h8", WHITE, True, "wQ d4 -> h8"),
|
||||
("8/8/8/8/3Q4/8/8/8 w - - 0 1", "d8", WHITE, True, "wQ d4 -> d8"),
|
||||
|
||||
# Kings (bk e4)
|
||||
("8/8/8/8/4k3/8/8/4K3 w - - 0 1", "e3", BLACK, True, "bk e4 -> e3"),
|
||||
("8/8/8/8/4k3/8/8/4K3 w - - 0 1", "g4", BLACK, False, "bk e4 not g4"),
|
||||
|
||||
# Edge knights (bn a1)
|
||||
("8/8/8/8/8/8/8/n7 w - - 0 1", "b3", BLACK, True, "bn a1 -> b3"),
|
||||
("8/8/8/8/8/8/8/n7 w - - 0 1", "c2", BLACK, True, "bn a1 -> c2"),
|
||||
("8/8/8/8/8/8/8/n7 w - - 0 1", "a2", BLACK, False, "bn a1 not a2"),
|
||||
]
|
||||
|
||||
for fen, sq_str, by, expected, msg in cases:
|
||||
with self.subTest(msg=msg, fen=fen, sq=sq_str, by=by):
|
||||
b = Board()
|
||||
self.load_fen(fen, board=b)
|
||||
got = bool(is_square_attacked(b, sq(sq_str), by))
|
||||
self.assertEqual(expected, got, msg)
|
||||
|
||||
|
||||
def test_in_check(self):
|
||||
cases = [
|
||||
# Unblocked slider (Qe2 checks e8)
|
||||
("4k3/8/8/8/8/8/4Q3/4K3 b - - 0 1", BLACK, True, "Qe2 -> e8"),
|
||||
# Blocked by pe7
|
||||
("4k3/4p3/8/8/8/8/4Q3/4K3 b - - 0 1", BLACK, False, "Qe2 blocked by pe7"),
|
||||
|
||||
# Knight check (Nc7 checks e8)
|
||||
("4k3/2N5/8/8/8/8/8/4K3 b - - 0 1", BLACK, True, "Nc7 -> e8"),
|
||||
|
||||
# Pawn check: bp e5 checks f4 (WK on f4)
|
||||
("4k3/8/8/4p3/5K2/8/8/8 b - - 0 1", WHITE, True, "bp e5 -> f4"),
|
||||
# Not checking WK e1
|
||||
("4k3/8/8/4p3/8/8/8/4K3 b - - 0 1", WHITE, False, "bp e5 not e1"),
|
||||
|
||||
# King adjacency (illegal but detected)
|
||||
# Ensure that this case does not make it through a legality filter.
|
||||
("8/8/8/8/8/8/4k3/4K3 w - - 0 1", WHITE, True, "BK e2 checks WK e1"),
|
||||
|
||||
# Double check (Qe2 + Nc7 vs BK e8)
|
||||
("4k3/2N5/8/8/8/8/4Q3/4K3 b - - 0 1", BLACK, True, "double check still true"),
|
||||
]
|
||||
|
||||
for fen, side, expected, msg in cases:
|
||||
with self.subTest(msg=msg, fen=fen, side=side):
|
||||
b = Board()
|
||||
self.load_fen(fen, board=b)
|
||||
actual = bool(is_in_check(b, side))
|
||||
self.assertEqual(expected, actual, msg)
|
||||
|
||||
|
||||
def test_attackers_to_popcount(self):
|
||||
cases = [
|
||||
# Pawns (original correction): only c5 & e5 attack d4
|
||||
("8/8/8/2p1p3/3B4/2p1p3/8/8 w - - 0 1", "d4", BLACK, 2, "black pawns c5,e5 attack d4; c3,e3 do not"),
|
||||
|
||||
# 0 attackers on empty board
|
||||
("8/8/8/8/8/8/8/8 w - - 0 1", "d4", WHITE, 0, "no pieces -> no attackers"),
|
||||
|
||||
# Knights: b5 and f5 both hit d4
|
||||
("8/8/8/1n3n2/8/8/8/8 w - - 0 1", "d4", BLACK, 2, "two black knights b5,f5 attack d4"),
|
||||
|
||||
# White pawns from below (c3,e3) attack d4
|
||||
("8/8/8/8/8/2P1P3/8/8 w - - 0 1", "d4", WHITE, 2, "white pawns c3,e3 attack d4"),
|
||||
|
||||
# Rooks on the file: d8 and d1 both see d4
|
||||
("3R4/8/8/8/8/8/8/3R4 w - - 0 1", "d4", WHITE, 2, "two white rooks d8,d1 attack d4"),
|
||||
|
||||
# Bishops on diagonals: b6 and f2 see d4
|
||||
("8/8/1B6/8/8/8/5B2/8 w - - 0 1", "d4", WHITE, 2, "two white bishops b6,f2 attack d4"),
|
||||
|
||||
# Mixed sliders: Ba7 and Rd1 both hit d4
|
||||
("8/B7/8/8/8/8/8/3R4 w - - 0 1", "d4", WHITE, 2, "white bishop a7 and rook d1 attack d4"),
|
||||
|
||||
# Rook blocked by own pawn -> no attack up the file
|
||||
("8/8/8/8/8/8/3P4/3R4 w - - 0 1", "d4", WHITE, 0, "rook d1 blocked by own pawn d2"),
|
||||
|
||||
# Two queens (different lines) both attack d4
|
||||
("3Q4/8/8/8/7Q/8/8/8 w - - 0 1", "d4", WHITE, 2, "queens d8 (file) and h4 (rank) attack d4"),
|
||||
|
||||
# Double-check-style: Qe2 and Nc7 attack e8
|
||||
("4k3/2N5/8/8/8/8/4Q3/4K3 w - - 0 1", "e8", WHITE, 2, "white queen e2 and knight c7 attack e8"),
|
||||
|
||||
# Blocked slider by enemy pawn: rook e4 cannot reach e8
|
||||
("4k3/4p3/8/8/4R3/8/8/4K3 w - - 0 1", "e8", WHITE, 0, "white rook e4 blocked by black pawn e7"),
|
||||
|
||||
# Three independent attackers to e4: Nf6, Be3 (from b1 path), Re1
|
||||
("8/8/5N2/8/8/8/8/1B2R3 w - - 0 1", "e4", WHITE, 3, "knight f6, bishop b1 path, rook e1 all attack e4"),
|
||||
|
||||
# King adjacency (illegal in play but functionally one attacker)
|
||||
# We need to ensure that the legal move filter catches this case.
|
||||
("8/8/8/8/8/8/4k3/4K3 w - - 0 1", "e1", BLACK, 1, "black king e2 attacks e1"),
|
||||
]
|
||||
|
||||
for fen, sq_str, by, expected_cnt, msg in cases:
|
||||
with self.subTest(msg=msg, fen=fen, sq=sq_str, by=by):
|
||||
b = Board()
|
||||
self.load_fen(fen, board=b)
|
||||
|
||||
mask = get_attackers_to(b, sq(sq_str), by)
|
||||
self.assertEqual(expected_cnt, int(mask).bit_count())
|
||||
Reference in New Issue
Block a user