Before starting with the various implementation details I would like to point you to the superb website at chessprogramming.org. A lot of the algorithms and concepts I'll try to demonstrate in the following chapters are discussed there in great detail.
When developing a chess application, one of the key components you'll need to handle is the representation of the chessboard and pieces. One efficient way to represent the board is through the use of bitboards.
A bitboard is a compact representation of a chessboard using a 64-bit integer. Each bit in the integer corresponds to a square on the chessboard. For example, in a standard 8x8 chessboard layout:
The least significant bit (LSB) represents a1 (the bottom-left square).
The most significant bit (MSB) represents h8 (the top-right square).
In a bitboard, each piece type (e.g., pawns, knights, bishops) can be represented as a separate 64-bit integer. With this representation, you can perform operations on the entire board or on individual pieces using bitwise operations.
Advantages of Using Bitboards
Memory Efficiency: A bitboard uses only 8 bytes (64 bits) to represent an entire chessboard, which is significantly more efficient than using a 2D array of objects.
Speed: Bitwise operations (AND, OR, NOT, XOR) are extremely fast and can be leveraged to quickly calculate moves, attacks, and other game states.
Simplicity in Move Generation: Bitboards simplify the implementation of move generation algorithms. For example, you can easily calculate possible moves for a knight by using precomputed masks and bitwise operations.
Parallelism: Bitboards can take advantage of modern CPU architectures by allowing operations on multiple bits simultaneously, which can lead to performance improvements.
To make this point more clear let's look at the following example:
Assume we have a attack set of a queen, and like to know whether the queen attacks opponent pieces it may capture, we need to 'and' the queen-attacks with the set of opponent pieces.
Representing the demonstrated idea in Swift code is actually easier than one might think when confronted with the idea for the first time. Here is my solution for that problem:
With bitboards implemented we have an extremely performant way to implement our chess logic like finding valid moves, or determining whether a check or checkmate is delivered on the board.
What we need to have a look at next is parsing a FEN encoded chess position as this is, as we learned, an integral part of encoding a given chess position.
Lets do a quick recap at what we are looking at. A FEN consists of 6 parts:
rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1
My FEN parser implementation is relaxed. Missing FEN fields will be accepted (except the board part) and will be filled with default values. Also FEN fields can either be separated by a space or by an underscore.
Various FEN extensions like X-FEN and Shredder-FEN exist, as well as some differences in piece placement data notation for chess variants like Crazyhouse (Promotion Status in FEN data). While my parser supports these I won't talk about that in detail as I think this is not really relevant for the problem at hand.
First the FEN string is split into the various parts:
After each part is successfully parsed I remove it from the array. Before dealing with the most interesting part, the piece placement data, lets quickly have a look at the other parts of a FEN. Extracting the information which player's turn it is seems pretty straight forward:
The other parts are similarly easy and I won't go into detail for everyone of them.
Let's look at the more interesting part, the piece placement data.
We start with an empty board and with the last rank and first file as we learned in the first part of this series. Next we loop over every character in the string. If we encounter a / and are positioned at the last file of a rank we move on to the next rank. Otherwise we move on with our parsing logic. At this point we examine whether we are currently looking at a number or a character. If a number is found this means we are dealing with one or more empty squares so we can just skip those and move on. If a character is found we check whether the character is lowercased or uppercased to determine whether a white or a black piece is encoded. Ultimately we determine the actual piece information based on the predefined map and update our board representation.
And with that we are able to read any given FEN string and convert this into our custom board data structure.
As this post is already quite lengthy I will only briefly talk about some of the challenges parsing SANs:
Castling notation can either be represented with the letter O or the number 0 so your implementation should be able to handle both.
If two or more identical pieces can move to the same square the notation accounts for this with standardized disambiguation rules. The parser needs to be able to handle those cases.
Other than that it is pretty straight-forward to extract the source and destination square, as well as the promotion information from a given SAN into a move value type.
In my implementation I also check for move validity with the help of my bitboard implementation. To round things up lets look at an example on how to parse a castling notation like O-O.
What is actually happening here? Lets say it is whites turn to play. So a SAN like O-O would mean to castle the white king to safety on the kingside. But how do we find the square the white king is currently positioned at? There are several options to choose from but since we are using bitboards to represent the chessboard we have a very performant way to do so. To find the white kings position all we have to do is to take the bitboard of all white pieces and build the intersection of the bitboard encoding all king pieces. The result should be a bitboard containing only one 1 representing the white king piece. As we learned the bitboard ist a UInt64 with each bit representing a square so to convert this binary value to the actual square all we need to do is to solve this rather trivial equation: square = 64 - 1 - leadingZeroBitCount.
Conclusion
This was quite a lot to cover and I tried my best to keep it concise where I could. Building a package to handle all the related chess logic is quite a big task as you can see. Fortunately some open source packages exist where you can draw inspiration from or which can help you if you are stuck with the problem at hand. I hope this little overview helped you understand what it takes to build a full-blown chess logic package. Next up I'd like to talk about representing the game of chess not only in data but also on screen with a handful of UI components.