Sven Schüle wrote:wgarvin wrote:Better if it can be done without if-tests though. Using a table indexed by source square and destination square, to mask off the appropriate rights, is branchless, but it uses a couple L1 cache lines. For bitboard programs, the "bitboard of squares that don't have their castling right" is also branchless and extremely lightweight. When generating the castling moves, you just have to test one byte of the bitboard with a mask containing the two bits (one for a rook, one for a king) and if either of them is set you know the move is not legal anymore. So its cheap in makemove and in the move generation.
You describe how simple *using* that bitboard would be. Agreed, but the classical way of maintaining four bits indicating availability of the four types of castling is even simpler.
Code: Select all
if (moveIsWhiteOO(move) && (castlingRights & WhiteOORight) && "F1, G1 are empty")
generateMove(move);
if (moveIsWhiteOOO(move) && (castlingRights & WhiteOOORight) && "D1, C1, B1 are empty")
generateMove(move);
if (moveIsBlackOO(move) && (castlingRights & BlackOORight) && "F8, G8 are empty")
generateMove(move);
if (moveIsBlackOOO(move) && (castlingRights & BlackOOORight) && "D8, C8, B8 are empty")
generateMove(move);
And you don't describe how simple *updating* that bitboard would be, or did I miss that? If I understand correctly this includes performing copy-and-update, and therefore keeping one instance of that bitboard per ply, since updating the bitboard by setting the 'from' and 'to' bits is not reversible (you don't know when to set bits back to 0) so make+unmake would not work. 8 extra bytes per ply on the stack, instead of just 4 bits (needing 1 byte), is the price.
Is this correct?
Sven
More or less. Its simple to update the bitboard because you just do this:
Code: Select all
board.noCastleBB |= (BITBOARD(1)<<fromSq) | (BITBOARD(1)<<toSq);
Notice that in a bitboard engine you'll have to compute (BITBOARD(1)<<fromSq) and (BITBOARD(1)<<toSq) anyway so that you can update your bitboards for the moving piece. So this update is as cheap as anything I can imagine.
Of course it adds 8 bytes to the board representation instead of just 4 bits. I was assuming that you back up the board struct in makemove (by copying its contents into an array for example) and restore it when undoing the move, so yes you'd have to copy the 8 bytes instead of 4 bits. Given what Dr. Hyatt pointed out about predictability of the branches, maybe its simpler to just use 4 bits and run a few extra instructions during makemove. I just like the idea because the update in makemove is extremely cheap.
As for "using" the bitboard (in the generation of moves), its exactly as simple as using the 4 bits would be.
instead of something like this:
Code: Select all
struct Board
{
U8 castlingRights;
} board;
if (sideToMove == WHITE)
{
if (board.castlingRights & 0x01) GenerateMove(WHITE_CASTLE_KS);
if (board.castlingRights & 0x02) GenerateMove(WHITE_CASTLE_QS);
if (board.castlingRights & 0x04) GenerateMove(BLACK_CASTLE_KS);
if (board.castlingRights & 0x08) GenerateMove(BLACK_CASTLE_QS);
}
You end up with something like this:
Code: Select all
struct Board
{
union
{
U8 u;
BITBOARD bb;
} noCastle;
} board;
if (sideToMove == WHITE)
{
if ((board.noCastle.u[0] & KS_MASK) != 0) GenerateMove(WHITE_CASTLE_KS);
if ((board.noCastle.u[0] & QS_MASK) != 0) GenerateMove(WHITE_CASTLE_QS);
if ((board.noCastle.u[7] & KS_MASK) != 0) GenerateMove(BLACK_CASTLE_KS);
if ((board.noCastle.u[7] & QS_MASK) != 0) GenerateMove(BLACK_CASTLE_QS);
}
Of course that union is funky because which end of the bitboard you access for White and for Black is going to depend on whether you're compiling for a big-endian or little-endian machine. If you already have #ifdefs for this, you can use those. Or if you want to keep it simple, you can just use a normal 64-bit masks and let the compiler worry about optimizing it.