mirror of
https://github.com/OMGeeky/hcsalmon1-Chess-Engine-Test.git
synced 2025-12-26 16:27:23 +01:00
Create Algorithm explanation.txt
This commit is contained in:
310
Algorithm explanation.txt
Normal file
310
Algorithm explanation.txt
Normal file
@@ -0,0 +1,310 @@
|
||||
bitwise operations:
|
||||
| - OR - will set a bit to 1 if either bit in two integers is 1
|
||||
& - AND - will set a bit to 1 if both bits in two integers is 1
|
||||
~ - NOT - will invert all bits to the opposite
|
||||
>> - SHIFT RIGHT - will move all bits to the right. Right means a lower value.
|
||||
<< - SHIFT LEFT - will move all bits to the left. Left means a higher value.
|
||||
^ - XOR - If only one bit is set in two integers, the bit is set. If both are set in two integers, it is 0.
|
||||
|
||||
__Adding and removing bits__
|
||||
|
||||
All pieces are represented by an unsigned 64 bit integer.
|
||||
|
||||
unsigned long long bitboard_array[12];
|
||||
const int WP = 0, WN = 1, WB = 2, WR = 3, WQ = 4, WK = 5, BP = 6, BN = 7, BB = 8, BR = 9, BQ = 10, BK = 11;
|
||||
|
||||
You can add a bit to a bitboard using a bitwise or: |
|
||||
|
||||
unsigned long long bitboard = 0;
|
||||
bitboard |= 1ULL << 50; //places 1 bit in square 50, c2
|
||||
|
||||
The bitshift operator will move all bits by that amount. 1 shifted by 50 simply puts one bit in "square" 50.
|
||||
To avoid bitshifting I mostly use the SQUARE_BBS array. This simply indexes all of those values in a constant.
|
||||
So instead we write:
|
||||
|
||||
unsigned long long bitboard = 0;
|
||||
bitboard |= SQUARE_BBS[50]; //places 1 bit in square 50, c2
|
||||
|
||||
These are the same. To make this a bit more human I also made square constants:
|
||||
|
||||
const int A8 = 0, B8 = 1, C8 = 2 etc.
|
||||
|
||||
To remove a bit we need to set that bit on a ulong and then invert the bits:
|
||||
|
||||
unsigned long long bitboard = 0;
|
||||
bitboard |= SQUARE_BBS[50]; //places 1 bit in square 50, c2
|
||||
bitboard &= ~SQUARE_BBS[50]; //bitboard now = 0 again
|
||||
|
||||
SQUARE_BBS[50] =
|
||||
0 0 0 0 0 0 0 0
|
||||
0 0 0 0 0 0 0 0
|
||||
0 0 0 0 0 0 0 0
|
||||
0 0 0 0 0 0 0 0
|
||||
0 0 0 0 0 0 0 0
|
||||
0 0 0 0 0 0 0 0
|
||||
0 0 1 0 0 0 0 0
|
||||
0 0 0 0 0 0 0 0
|
||||
|
||||
~SQUARE_BBS[50] =
|
||||
1 1 1 1 1 1 1 1
|
||||
1 1 1 1 1 1 1 1
|
||||
1 1 1 1 1 1 1 1
|
||||
1 1 1 1 1 1 1 1
|
||||
1 1 1 1 1 1 1 1
|
||||
1 1 1 1 1 1 1 1
|
||||
1 1 0 1 1 1 1 1
|
||||
1 1 1 1 1 1 1 1
|
||||
|
||||
If you bitwise AND (&) with the inverted bitboard it will keep all bits the same
|
||||
except the empty one.
|
||||
Let's say we want to move a pawn:
|
||||
|
||||
unsigned long long black_pawns = 65280;
|
||||
in bits =
|
||||
0 0 0 0 0 0 0 0
|
||||
1 1 1 1 1 1 1 1
|
||||
0 0 0 0 0 0 0 0
|
||||
0 0 0 0 0 0 0 0
|
||||
0 0 0 0 0 0 0 0
|
||||
0 0 0 0 0 0 0 0
|
||||
0 0 0 0 0 0 0 0
|
||||
0 0 0 0 0 0 0 0
|
||||
|
||||
We need to remove the bit we want and place it somewhere else. Let's d7 to d5:
|
||||
|
||||
black_pawns |= SQUARE_BBS[D5]; //add the bit
|
||||
0 0 0 0 0 0 0 0
|
||||
1 1 1 1 1 1 1 1
|
||||
0 0 0 0 0 0 0 0
|
||||
0 0 0 0 1 0 0 0
|
||||
0 0 0 0 0 0 0 0
|
||||
0 0 0 0 0 0 0 0
|
||||
0 0 0 0 0 0 0 0
|
||||
0 0 0 0 0 0 0 0
|
||||
|
||||
This puts a bit on d5. Now we remove d7 like so:
|
||||
|
||||
black_pawns &= ~SQUARE_BBS[D7]; //remove the bit
|
||||
0 0 0 0 0 0 0 0
|
||||
1 1 1 1 0 1 1 1
|
||||
0 0 0 0 0 0 0 0
|
||||
0 0 0 0 1 0 0 0
|
||||
0 0 0 0 0 0 0 0
|
||||
0 0 0 0 0 0 0 0
|
||||
0 0 0 0 0 0 0 0
|
||||
0 0 0 0 0 0 0 0
|
||||
|
||||
That's how we move pieces.
|
||||
|
||||
__Generate moves__
|
||||
|
||||
__Occupancies__
|
||||
|
||||
We track white and black pieces together using occupancies. One approach is to make them global:
|
||||
|
||||
unsigned long long occupancies[3];
|
||||
const int WHITE_OCCUPANCIES = 0, BLACK_OCCUPANCIES = 1, COMBINED_OCCUPANCIES = 2;
|
||||
|
||||
The fastest approach is to update the occupancies with every move. However this gave me the most headaches
|
||||
imaginable with debugging. For example Castling:
|
||||
|
||||
case TAG_W_CASTLE_KS: //it's a castle move
|
||||
bitboard_array_global[WK] |= Constants.SQUARE_BBS[G1]; //move the white king to G1
|
||||
bitboard_array_global[WK] &= ~Constants.SQUARE_BBS[E1];
|
||||
bitboard_array_global[WR] |= Constants.SQUARE_BBS[F1]; //move the white rook to F1
|
||||
bitboard_array_global[WR] &= ~Constants.SQUARE_BBS[H1];
|
||||
//We do the same for the occupancies
|
||||
occupancies[WHITE_OCCUPANCIES] |= Constants.SQUARE_BBS[G1];
|
||||
occupancies[WHITE_OCCUPANCIES] &= ~Constants.SQUARE_BBS[E1];
|
||||
occupancies[WHITE_OCCUPANCIES] |= Constants.SQUARE_BBS[F1];
|
||||
occupancies[WHITE_OCCUPANCIES] &= ~Constants.SQUARE_BBS[H1];
|
||||
occupancies[COMBINED_OCCUPANCIES] |= Constants.SQUARE_BBS[G1];
|
||||
occupancies[COMBINED_OCCUPANCIES] &= ~Constants.SQUARE_BBS[E1];
|
||||
occupancies[COMBINED_OCCUPANCIES] |= Constants.SQUARE_BBS[F1];
|
||||
occupancies[COMBINED_OCCUPANCIES] &= ~Constants.SQUARE_BBS[H1];
|
||||
|
||||
castle_rights_global[WKS_CASTLE_RIGHTS] = false;
|
||||
castle_rights_global[WQS_CASTLE_RIGHTS] = false;
|
||||
ep_global = NO_SQUARE;
|
||||
|
||||
The amount of bug potential this creates is crazy. The safest thing to do is bitwise OR
|
||||
the boards together for each position:
|
||||
|
||||
const unsigned long long WHITE_OCCUPANCIES = bitboard_array[0] |
|
||||
bitboard_array[1] |
|
||||
bitboard_array[2] |
|
||||
bitboard_array[3] |
|
||||
bitboard_array[4] |
|
||||
bitboard_array[5];
|
||||
|
||||
const unsigned long long BLACK_OCCUPANCIES = bitboard_array[6] |
|
||||
bitboard_array[7] |
|
||||
bitboard_array[8] |
|
||||
bitboard_array[9] |
|
||||
bitboard_array[10] |
|
||||
bitboard_array[11];
|
||||
|
||||
const unsigned long long COMBINED_OCCUPANCIES = WHITE_OCCUPANCIES | BLACK_OCCUPANCIES;
|
||||
const unsigned long long EMPTY_OCCUPANCIES = ~COMBINED_OCCUPANCIES;
|
||||
|
||||
This simply joins the white and black pieces together respectively. We can then use the occupancies for moves.
|
||||
To find white capture moves we bitwise AND the attacks with BLACK_OCCUPANCIES and viceversa for black occupancies.
|
||||
To get regular moves we bitwise AND attacks with EMPTY_OCCUPANCIES.
|
||||
|
||||
__Pins and check__
|
||||
|
||||
First we need to work out pins and checks. The tutorial I followed didn't do this
|
||||
and simply played each move and unmade the move if the king was attacked. This is
|
||||
a lot slower and I found an explanation online of how to work this out.
|
||||
|
||||
First we get the king position, let's say it's white to play:
|
||||
|
||||
const int whiteKingPosition = BitscanForward(bitboard_array[BK]);
|
||||
|
||||
From the king square we need to use the piece moves to see if there is a check or pin:
|
||||
|
||||
int whiteKingCheckCount = 0; //We track this for double check and castling
|
||||
|
||||
//pawns
|
||||
tempBitboard = bitboard_array[BP] & WHITE_PAWN_ATTACKS[whiteKingPosition]; //Here we are checking if there is a pawn diagonal from the king
|
||||
|
||||
if (tempBitboard != 0) //if it's not zero then there is a pawn
|
||||
{
|
||||
int pawnSquare = (DEBRUIJN64[MAGIC * (tempBitboard ^ (tempBitboard - 1)) >> 58]); //This inlines BitscanForward
|
||||
|
||||
if (checkBitboard == 0) { //probably an unnecessary check
|
||||
checkBitboard = SQUARE_BBS[pawnSquare]; //We then set the checkbitboard with that square
|
||||
}
|
||||
whiteKingCheckCount++;
|
||||
}
|
||||
|
||||
For checks all we simply do is bitwise AND any potential moves with the checkbitboard. If there is no check then
|
||||
we set checkBitboard to a MAX_ULONG which simply means all bits are set.
|
||||
|
||||
if (whiteKingCheckCount == 0) {
|
||||
checkBitboard = MAX_ULONG;
|
||||
}
|
||||
|
||||
This just avoids a conditional. So if there is a pawn attacking the white king then only moves that hit that square
|
||||
will be stored.
|
||||
|
||||
tempBitboard = bitboard_array[WN];
|
||||
|
||||
while (tempBitboard != 0) { //if there are still knights
|
||||
const int knightSquare = BitscanForward(tempBitboard);
|
||||
ulong knightAttacks = (KNIGHT_ATTACKS[knightSquare] & BLACK_OCCUPANCIES) & checkBitboard;
|
||||
//if the knight attacks from this square intersect with a black piece.
|
||||
//If there is a piece attacking the king, does this move attack that piece?
|
||||
|
||||
while (knightAttacks != 0) {
|
||||
//save the move
|
||||
}
|
||||
}
|
||||
|
||||
For pins it's a bit more complicated
|
||||
|
||||
int pinArray[8][2] =
|
||||
{
|
||||
{ -1, -1 },
|
||||
{ -1, -1 },
|
||||
{ -1, -1 },
|
||||
{ -1, -1 },
|
||||
{ -1, -1 },
|
||||
{ -1, -1 },
|
||||
{ -1, -1 },
|
||||
{ -1, -1 },
|
||||
};
|
||||
|
||||
We create a pin array. The first index is the square being pinned. The second index is piece that is pinning the square.
|
||||
|
||||
//bishops
|
||||
//From the white king, get the bishop moves but only including the black occupancies
|
||||
const unsigned long long bishopAttacksChecks = GetBishopAttacksFast(whiteKingPosition, BLACK_OCCUPANCIES_LOCAL);
|
||||
|
||||
temp_bitboard = bitboard_array_global[BB] & bishopAttacksChecks; //See if there is a black bishop there
|
||||
while (temp_bitboard != 0) //if there is a black bishop there
|
||||
{
|
||||
const int piece_square = (DEBRUIJN64[MAGIC * (temp_bitboard ^ (temp_bitboard - 1)) >> 58]); //find the square
|
||||
temp_pin_bitboard = INBETWEEN_BITBOARDS[whiteKingPosition][piece_square] & WHITE_OCCUPANCIES_LOCAL;
|
||||
|
||||
if (temp_pin_bitboard == 0)
|
||||
{
|
||||
if (check_bitboard == 0)
|
||||
{
|
||||
check_bitboard = INBETWEEN_BITBOARDS[whiteKingPosition][piece_square];
|
||||
}
|
||||
whiteKingCheckCount++;
|
||||
}
|
||||
else
|
||||
{
|
||||
const int pinned_square = (DEBRUIJN64[MAGIC * (temp_pin_bitboard ^ (temp_pin_bitboard - 1)) >> 58]);
|
||||
temp_pin_bitboard &= temp_pin_bitboard - 1;
|
||||
|
||||
if (temp_pin_bitboard == 0)
|
||||
{
|
||||
pinArray[pinNumber][PINNED_SQUARE_INDEX] = pinned_square;
|
||||
pinArray[pinNumber][PINNING_PIECE_INDEX] = piece_square;
|
||||
pinNumber++;
|
||||
}
|
||||
}
|
||||
temp_bitboard &= temp_bitboard - 1; //remove the bit to stop infinitive loop
|
||||
}
|
||||
|
||||
__Finding Pieces__
|
||||
|
||||
I didn't write BitscanForward. It uses the DEBRUJN method to get "the least significant bit".
|
||||
That means the first smallest bit you find. There is a very simple way to do this but it's slower:
|
||||
|
||||
int BitScanForwardSlow(const unsigned long long bitboard)
|
||||
{
|
||||
if (bitboard == 0) { //no bits
|
||||
return -1;
|
||||
}
|
||||
for (size_t i = 0; i < 64; ++i) { //loop through the squares
|
||||
if ((bitboard & SQUARE_BBS[i]) != 0) { //We compare the bit for that square, != 0 means it is 1 in that square
|
||||
return i;
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
The original looks like this:
|
||||
|
||||
inline int BitScanForward(unsigned long long bitboard)
|
||||
{
|
||||
return (DEBRUIJN64[MAGIC * (bitboard ^ (bitboard - 1)) >> 58]);
|
||||
}
|
||||
|
||||
const unsigned long long MAGIC = 0x03f79d71b4cb0a89;
|
||||
const int DEBRUIJN64[64] =
|
||||
{
|
||||
0, 47, 1, 56, 48, 27, 2, 60,
|
||||
57, 49, 41, 37, 28, 16, 3, 61,
|
||||
54, 58, 35, 52, 50, 42, 21, 44,
|
||||
38, 32, 29, 23, 17, 11, 4, 62,
|
||||
46, 55, 26, 59, 40, 36, 15, 53,
|
||||
34, 51, 20, 43, 31, 22, 10, 45,
|
||||
25, 39, 14, 33, 19, 30, 9, 24,
|
||||
13, 18, 8, 12, 7, 6, 5, 63
|
||||
};
|
||||
|
||||
I can't explain this algorithm to you. I just copied the code but it's faster
|
||||
than looping the squares. There is an integer overflow in this algorithm when you multiply so it
|
||||
will cause problems in some languages.
|
||||
|
||||
To remove the least significant bit we use this method:
|
||||
|
||||
tempBitboard &= tempBitboard - 1;
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user