The easiest way to generate legal moves is to just play out every pseudo-legal move and see if the king in check afterward. The fastest way to check for check (no pun intended) is with the "I see you, you see me" method. For example, to see if the king is in check from a rook, you would generate rook moves from the king's position and see if any of them move to a square with an enemy rook (or a queen).
I recommend doing this before you move on to faster methods if you don't have working legal move generation yet, since you can use this to catch lots of bugs while your code isn't too complicated.
However, this is quite slow as you can imagine, and there is a much faster way to do it. The first thing you do is generate the enemy's attacked squares. This unfortunately is not very fast; you have to do pseudo legal move generation for your opponent as well. You can AND their moves for each piece to an attacks bitboard. At the beginning of your function you will need to start with a variable checkSquare
which will be equal to zero. If the current enemy pieces' move bitboard && your king then make checkSquare
equal to the index of the current enemy piece, unless checkSquare
is already nonzero, in which case you are in double check and can set checkSquare
to say, 64.
IMPORTANT NOTE: make sure to allow your opponent to capture their own pieces in this special move generation, otherwise your program will think taking a defended piece with your king is legal. This can be done by fiddling with the piece color bitboards.
After this, you generate moves based on checkSquare
. If checkSquare
is 64, you are in double check and your only moves will be pseudoLegalKingMoves && !attacks
.
Otherwise, you will have to find all of the pinned pieces. You can find some info about this here.
If there is only one check, you will look at the piece type of the enemy checker. If it is a knight or pawn, the only legal moves will be capturing the checking piece or the king moves as described earlier. If it is a sliding piece, you will need to generate all the squares in between your king and checkSquare (IIRC a lookup table is fastest for this) then AND all your pseudo legal moves with the in-between squares. No pinned piece can move while in check, so you can actually save yourself the effort of generating moves for pinned pieces by testing pins before pseudo-legal move generation.
IMPORTANT NOTE: en-passant can be tricky: for example, a common bug is one where a king is on the fifth rank and an opponent pawn get double pushed. En-passant might look legal to the program but actually be pinned by a rook. Additionally, en-passant doesn't actually move to the square of the captured pawn so en-passanting a checking pawn to evade a check might not even be considered by an engine.
If there is no check, for each pinned piece you can AND its moves with the diagonal/file/rank on which it is pinned. Like I said, a lookup table really helps with this, but none of that matters if you are generating sliding piece moves with loops, in which case you can also consider switching pseudo-legal move generation to lookup tables. And of course, for king moves you can just do the same thing as before.
Lastly, make sure to test your code against stockfish in some common test positions. Good luck!