This document is a description of the design of mchess and
mchess2
Piece representation
The chess board consists of 64 squares. Each square may be empty or contain a
white or black piece. Each piece is given an integer value (this is not
related to the worth of the piece in number of pawns).
This enumeration is called e_piece and is defined in the file
CMove.h:
typedef enum {
EM = 0, // Empty
WP = 1, // White Pawn
WN = 2, // White Knight
WB = 3, // White Bishop
WR = 4, // White Rook
WQ = 5, // White Queen
WK = 6, // White King
BP = -1, // Black Pawn
BN = -2, // Black Knight
BB = -3, // Black Bishop
BR = -4, // Black Rook
BQ = -5, // Black Queen
BK = -6, // Black King
IV = 99 // INVALID
} e_piece;
The above is merely an enumeration, and the actual numerical values are
arbitrary. However, I do in the following rely on the fact that:
- The empty space has value zero
- All white pieces have positive values
- All black pieces have negative values
Additionally, I have a special value for an invalid piece. This has
several uses, the foremost will be explained in the following.
Square representation
An important function in a chess engine is to be able to generate a list of all
legal moves in a given position. This function is used all the time, and
therefore must be as fast as possible.
When writing such a function, one must first consider how to represent the
board configuration, i.e. where each piece is placed, or equivalently, which
piece is on a given square.
I've chosen to represent the board as a simple
one-dimensional array of integers. Each index in the array corresponds to a
particular square, and each value in the array is the corresponding piece,
according to the enumeration above.
When generating a list of legal moves, it is necessary to check if a piece is
about to move outside the edge of the board. For instance, a rook on H1 can not
move to the right.
This is dealt with in a very smart way: The board is enlarged, so that
it is surrounded by two layers of invalid squares. This gives a total of 12
rows of ten squares each, i.e. 120 elements in the array.
| 110 |
111 |
112 |
113 |
114 |
115 |
116 |
117 |
118 |
119 |
| 100 |
101 |
102 |
103 |
104 |
105 |
106 |
107 |
108 |
109 |
| 90 |
A8 |
B8 |
C8 |
D8 |
E8 |
F8 |
G8 |
H8 |
99 |
| 80 |
A7 |
B7 |
C7 |
D7 |
E7 |
F7 |
G7 |
H7 |
89 |
| 70 |
A6 |
B6 |
C6 |
D6 |
E6 |
F6 |
G6 |
H6 |
79 |
| 60 |
A5 |
B5 |
C5 |
D5 |
E5 |
F5 |
G5 |
H5 |
69 |
| 50 |
A4 |
B4 |
C4 |
D4 |
E4 |
F4 |
G4 |
H4 |
59 |
| 40 |
A3 |
B3 |
C3 |
D3 |
E3 |
F3 |
G3 |
H3 |
49 |
| 30 |
A2 |
B2 |
C2 |
D2 |
E2 |
F2 |
G2 |
H2 |
39 |
| 20 |
A1 |
B1 |
C1 |
D1 |
E1 |
F1 |
G1 |
H1 |
29 |
| 10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
| 0 |
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
When testing whether a move is legal, it is enough to test if the target square
is empty or contains an enemy piece. It is not even necessary to exlicitly test
for invalid squares.
An enumeration is used to define the index of each square. This enumeration is
defined in the file CSquare.h. This is purely for code readability,
i.e. if I refer to a square as E2 it is more readable, than using the integer
35.
enum // Squares
{
A8 = 91, B8, C8, D8, E8, F8, G8, H8,
A7 = 81, B7, C7, D7, E7, F7, G7, H7,
A6 = 71, B6, C6, D6, E6, F6, G6, H6,
A5 = 61, B5, C5, D5, E5, F5, G5, H5,
A4 = 51, B4, C4, D4, E4, F4, G4, H4,
A3 = 41, B3, C3, D3, E3, F3, G3, H3,
A2 = 31, B2, C2, D2, E2, F2, G2, H2,
A1 = 21, B1, C1, D1, E1, F1, G1, H1,
};
The CSquare class
It does become necessary to determine the row and/or column number of a given
square, for instance when checking for pawn promotion. Therefore I've chosen
to write a class CSquare to handle this. The following is the
essential ingredients:
class CSquare
{
public:
int row() const {return (m_sq/10) - 1;} // returns 1 - 8
int col() const {return (m_sq%10);} // returns 1 - 8
operator int() const { return m_sq; } // Implicit conversion to integer
private:
uint8_t m_sq; // Internal representation, 0 - 119.
}; // end of class CSquare
The conversion operator int() allows the CSquare class to be used
directly as an array index.
Move representation
The next to consider is how to represent a move. Obviously, we need an
originating square, and a target square. However, this is not quite enough.
When a pawn reaches the back rank, it may promote to any other piece, and this
must be given as well.
The CMove class
The class CMove is used to represent a single chess move. The essential
ingredients in this class are:
class CMove
{
public:
CSquare From(void) const {return m_from;}
CSquare To(void) const {return m_to;}
private:
CSquare m_from;
CSquare m_to;
int8_t m_promoted; // This handles pawn promotion.
}; // end of CMove
Note the member variable m_promoted. This is only used during pawn
promotion, where it contains the piece that the pawn promotes to. In case of an
ordinary move, the variable is not used, and should be set to zero (empty).
Enhancements
This class contains two additional private member variables:
int8_t m_piece;
int8_t m_captured;
The first, m_piece, contains the current piece being moved, the second
variable, m_captured, contains the piece being captured, if any. They
are both used only for printing the current move to the user.
The CMoveList class
When generating a list of all the legal moves in a chess position, it becomes necessary to
manipulate lists of moves. I've found it convenient to write a separate class just for this:
class CMoveList
{
public:
void clear()
{
m_moveList.clear();
}
void push_back(const CMove& move)
{
m_moveList.push_back(move);
}
unsigned int size() const
{
return m_moveList.size();
}
const CMove & operator [] (unsigned int ix) const { return m_moveList[ix]; }
private:
std::vector m_moveList;
}; // end of CMoveList
The purpose of this class is just to encapsulate the semantics of an ordinary
array. Here I've chosen to implement the array using the std::vector
container class.
When populating the list, the functions clear() and push_back(move) are used.
When examining the list, the functions size() and operator [] are used.
Board representation
The CBoard class
This class is defined in the file CBoard.h and contains all the
information defining the current position on the board. Of course, this
includes the above mentioned array. Additionally, it includes an integer value
that specifies whether it is the white or the black pieces to move.
class CBoard
{
public:
void newGame();
void find_legal_moves(CMoveList &moves) const;
void make_move(const CMove &move);
int get_value();
private:
std::vector m_board;
int m_side_to_move;
}; // end of class CBoard
The function newGame() resets the board to the starting position, the
function make_move() updates the board position with the supplied
move, and finally find_legal_moves() generates a list of all legal moves
in the current board position.
Enhancements
To improve performance, I've added another member variable that keeps track of
the current material balance.
int m_material;
This is kind of like a cache. Instead of
calculating the material balance after each move, it is much faster to check
for captures and promotion, and adjust the current cache incrementally.
Searching for the best move
The AI class
This class contains an implementation of the alpha-beta pruning algorithm.
This is a complicated function, and is best described in wikipedia
and the chess
programming site.
The class interface is very simple, however:
class AI
{
public:
AI(CBoard& board) : m_board(board) { }
CMove find_best_move();
private:
int search(int alpha, int beta, int level);
CBoard& m_board;
}; // end of class AI
Note that the m_board variable is a reference. This is to avoid making copies of the entire CBoard class.
Final remarks
These are the basic ingredients necessary to make a working chess engine.
There only remains to write a main() function that instantiates a
CBoard and a AI object. The you must read the user input and
call ai.find_best_move().