I'm playing around with a chess engine that I wanted it to know all the rules such as pawn promotion, en-passant, and castling. Currently, I'm using a basic Negamax for the AI but I plan to improve at a later stage.
I have not spent too much effort on the interface as I plan to make it compatible with XBoard / Winboard when I'm bored enough.
Any ideas to get the basics more efficient or dry?
#include <array>
#include <cstring>
#include <iostream>
#include <optional>
#include <string>
#include <string_view>
#include <vector>
// bit-util.h
typedef std::uint8_t uint8;
typedef std::uint64_t uint64;
#ifdef _MSC_VER
#define lzcnt(a) __lzcnt64(a)
#define popcnt(a) __popcnt64(a)
#else
#define lzcnt(a) __builtin_clzl(a)
#define popcnt(a) __builtin_popcountl(a)
#endif
constexpr uint64 one = 1ul;
uint64 bitcalc(int col, int row) {
return one << (col + row * 8);
}
bool excludes(uint64 bits, uint64 flag) {
return !(bits & flag);
}
bool includes(uint64 bits, uint64 flag) {
return bits & flag;
}
uint8 square(uint64 index) {
return 63 - static_cast<uint8>(lzcnt(index));
}
struct Cell
{
Cell(uint8 col, uint8 row)
: col{ col }
, row{ row }
{
}
Cell(uint8 index) {
col = index % 8;
row = index / 8;
}
bool valid()
{
return col >= 0 && col < 8 && row >= 0 && row < 8;
}
bool move(int icol, int irow) {
col += icol;
row += irow;
return valid();
}
uint8 index() {
return col + row * 8;
}
uint64 flag()
{
return one << index();
}
uint8 col;
uint8 row;
};
std::optional<uint64> onboard(int col, int row)
{
if (col >= 0 && col < 8 && row >= 0 && row < 8)
return bitcalc(col, row);
return std::nullopt;
}
int rank(int player, int row)
{
return player ? 7 - row : row;
}
// bitboard.h
enum Piece
{
Pawn,
Rook,
Knight,
Bishop,
Queen,
King,
Max
};
struct BState
{
bool attacks;
uint64 excludes;
};
class BitBoard
{
public:
BitBoard()
: rookMoves{ {-1, 0}, {1, 0}, {0, -1}, {0, 1} }
, bishopMoves{ {-1, -1}, {1, -1}, {-1, 1}, {1, 1} }
, knightMoves{ {-1, 2}, {1, 2} , {-1, -2} , {1, -2}, {-2, 1} , {2, 1} , {-2, -1} , {2, -1} }
, queenMoves{ {-1, -1}, {0, -1} , {1, -1} , {-1, 1} , {0, 1} , {1, 1} , {-1, 0} , {1, 0} }
{
std::memset(state, 0, sizeof(state));
for (int s = 0; s < 2; s++)
{
for (int p = 0; p < Piece::Max; p++)
{
for (uint8 i = 0; i < 64; i++)
{
for (uint8 j = 0; j < 64; j++)
{
generate(s, s ? -1 : 1, p, i, j);
}
}
}
}
}
bool check(int side, int piece, uint8 src, uint8 dst, uint64 all) {
BState& st = state[side][piece][src][dst];
return st.attacks && excludes(all, st.excludes);
}
private:
void generate(int side, int dir, int piece, uint8 src, uint8 dst) {
BState& st = state[side][piece][src][dst];
switch (piece)
{
case Pawn:
genray(st, dir, src, dst, -1, dir, true);
genray(st, dir, src, dst, 1, dir, true);
break;
case Rook:
for (const auto& move : rookMoves)
genray(st, dir, src, dst, move[0], move[1], false);
break;
case Knight:
for (const auto& move : knightMoves)
genray(st, dir, src, dst, move[0], move[1], true);
break;
case Bishop:
for (const auto& move : bishopMoves)
genray(st, dir, src, dst, move[0], move[1], false);
break;
case Queen:
for (const auto& move : queenMoves)
genray(st, dir, src, dst, move[0], move[1], false);
break;
case King:
for (const auto& move : queenMoves)
genray(st, dir, src, dst, move[0], move[1], true);
break;
default:
break;
}
}
void genray(BState& state, int dir, uint8 src, uint8 dst, int inccol, int incrow, bool single)
{
Cell csrc(src);
Cell cdst(dst);
if (csrc.move(inccol, incrow))
{
if (csrc.index() == dst)
{
state.attacks = true;
state.excludes = 0;
}
if (single)
return;
uint64 excludes = csrc.flag();
while (csrc.move(inccol, incrow))
{
if (csrc.index() == dst)
{
state.attacks = true;
state.excludes = excludes;
break;
}
else
{
excludes |= csrc.flag();
}
}
}
}
private:
BState state[2][Piece::Max][64][64];
public:
const int knightMoves[8][2];
const int rookMoves[4][2];
const int bishopMoves[4][2];
const int queenMoves[8][2];
};
// board.h
const static std::string pieces[] = { "PRNBQK", "prnbqk" };
enum Special
{
None,
Promote,
EnPassant,
CastShort,
CastLong,
Draw,
Fail
};
struct Move
{
Move(Piece piece, uint64 src, uint64 dst, uint8 special, int score = 0)
: piece{ piece }
, src{ src }
, dst{ dst }
, special{ special }
, score{ score }
{ }
int score;
uint64 src;
uint64 dst;
uint8 special;
Piece piece;
};
struct Player
{
uint64 prev;
uint64 all;
uint64 pieces[Piece::Max];
};
class Board
{
public:
uint64 moved;
uint64 all;
Player players[2];
void init(std::string_view str)
{
std::memset(this, 0, sizeof(*this));
for (uint64 i = 0; i < str.size(); i++) {
for (int p = 0; p < 2; p++) {
const auto& color = pieces[p];
auto index = color.find(str[i]);
if (index != std::string::npos) {
uint64 bit = one << i;
all |= bit;
players[p].all |= bit;
players[p].pieces[index] |= bit;
}
}
}
}
void print(int depth = 0) {
std::string header = " A B C D E F G H\n";
std::cout << header;
for (int r = 0; r < 8; r++) {
std::cout << 8 - r;
char bk = r % 2;
for (int c = 0; c < 8; c++) {
char ch = ' ';
for (int p = 0; p < 2; p++) {
auto& player = players[p];
for (int i = 0; i < Piece::Max; i++) {
if (player.pieces[i] & bitcalc(c, r))
ch = pieces[p][i];
}
}
std::cout << (bk ? "\033[0;47;30m" : "\033[0;30;107m") << ' ' << ch << ' ';
bk = !bk;
}
std::cout << "\033[m" << 8 - r << '\n';
}
std::cout << header << '\n';
}
void apply(int player, Move move) {
Player& me = players[player];
Player& op = players[!player];
moved |= move.src | move.dst;
me.prev = square(move.src) + square(move.dst) * 64;
switch (move.special) {
case Special::Promote:
removePiece(op, Piece::Max, move.dst);
removePiece(me, Piece::Pawn, move.src);
insertPiece(me, move.piece, move.dst);
break;
case Special::EnPassant:
removePiece(op, Piece::Pawn, bitcalc(Cell(square(move.dst)).col, Cell(square(move.src)).row));
removePiece(me, Piece::Pawn, move.src);
insertPiece(me, Piece::Pawn, move.dst);
break;
case Special::CastShort:
removePiece(me, Piece::Rook, bitcalc(7, rank(player, 0)));
insertPiece(me, Piece::Rook, bitcalc(5, rank(player, 0)));
removePiece(me, Piece::King, move.src);
insertPiece(me, Piece::King, move.dst);
break;
case Special::CastLong:
removePiece(me, Piece::Rook, bitcalc(0, rank(player, 0)));
insertPiece(me, Piece::Rook, bitcalc(3, rank(player, 0)));
removePiece(me, Piece::King, move.src);
insertPiece(me, Piece::King, move.dst);
break;
default:
removePiece(op, Piece::Max, move.dst);
removePiece(me, move.piece, move.src);
insertPiece(me, move.piece, move.dst);
}
}
int eval(Player& player) {
int values[] = { 1, 5, 3, 3, 10, 200 };
int total = 0;
for (int i = 0; i < Piece::Max; i++)
total += static_cast<int>(popcnt(player.pieces[i])) * values[i];
return total;
}
private:
void insertPiece(Player& player, Piece piece, uint64 bit) {
all |= bit;
player.all |= bit;
player.pieces[piece] |= bit;
}
void changePiece(Player& player, uint64 bit, Piece from, Piece to) {
player.pieces[from] &= ~bit;
player.pieces[to] |= bit;
}
void removePiece(Player& player, Piece piece, uint64 bit) {
all &= ~bit;
player.all &= ~bit;
if (piece == Piece::Max) {
for (auto& piece : player.pieces)
piece &= ~bit;
}
else {
player.pieces[piece] &= ~bit;
}
}
};
// AI.h
class AI
{
public:
AI(BitBoard& bitboard)
: bitboard{ bitboard }
{ }
Move move(Board& board, int player, int maxDepth) {
allMoves.clear();
bestMoves.clear();
negaMax(board, player, maxDepth, 0);
if (bestMoves.empty())
return Move(Piece::Max, 0, 0, Special::Draw, 0);
return bestMoves[rand() % bestMoves.size()];
}
int negaMax(Board& board, int player, int maxDepth, int depth) {
int scoreMe = board.eval(board.players[player]);
int scoreOp = board.eval(board.players[!player]);
int max = -9999;
if (depth == maxDepth)
return scoreMe - scoreOp;
for (int i = 0; i < Piece::Max; i++) {
Piece piece = static_cast<Piece>(i);
uint64 bits = board.players[player].pieces[piece];
while (bits) {
uint64 src = bits & (0ul - bits);
int index = square(src);
Cell cell(index);
switch (i) {
case Piece::Pawn:
movePawn(board, player, maxDepth, depth, src, cell, piece, max);
break;
case Piece::Rook:
moveRook(board, player, maxDepth, depth, src, cell, piece, max);
break;
case Piece::Bishop:
moveBishop(board, player, maxDepth, depth, src, cell, piece, max);
break;
case Piece::Knight:
moveKnight(board, player, maxDepth, depth, src, cell, piece, max);
break;
case Piece::Queen:
moveQueen(board, player, maxDepth, depth, src, cell, piece, max);
break;
case Piece::King:
moveKing(board, player, maxDepth, depth, src, cell, piece, max);
break;
}
bits ^= src;
}
}
return max;
}
void recurse(Board board, int player, int maxDepth, int depth, Move move, int& max) {
Board b = board;
b.apply(player, move);
move.score = -negaMax(b, !player, maxDepth, depth + 1);
if (depth == 0) {
allMoves.push_back(move);
if (bestMoves.size() && bestMoves[0].score < move.score)
bestMoves.clear();
if (bestMoves.empty() || bestMoves[0].score == move.score)
bestMoves.push_back(move);
}
else {
if (move.score > max)
max = move.score;
}
}
void pawnPromotion(Board board, int player, int maxDepth, int depth, uint64 src, uint64 dst, int row, Piece piece, int& max) {
if (rank(player, row) == 7) {
for (int p = Piece::Rook; p < Piece::King; p++)
recurse(board, player, maxDepth, depth, Move((Piece)p, src, dst, Special::Promote), max);
}
else {
recurse(board, player, maxDepth, depth, Move(piece, src, dst, 0), max);
}
}
void movePawn(Board& board, int player, int maxDepth, int depth, uint64 src, Cell cell, Piece piece, int& max) {
Player& opponent = board.players[!player];
int direction = player ? -1 : 1;
// forward moves
auto dst = onboard(cell.col, cell.row + direction * 1);
if (dst != std::nullopt && excludes(board.all, *dst)) {
pawnPromotion(board, player, maxDepth, depth, src, *dst, cell.row + direction * 1, piece, max);
if (rank(player, cell.row) == 1)
{
dst = onboard(cell.col, cell.row + direction * 2);
if (dst != std::nullopt && excludes(board.all, *dst)) {
pawnPromotion(board, player, maxDepth, depth, src, *dst, cell.row + direction * 2, piece, max);
}
}
}
// sideways captures
dst = onboard(cell.col - 1, cell.row + direction);
if (dst != std::nullopt && includes(opponent.all, *dst)) {
pawnPromotion(board, player, maxDepth, depth, src, *dst, cell.row + direction, piece, max);
}
dst = onboard(cell.col + 1, cell.row + direction);
if (dst != std::nullopt && includes(opponent.all, *dst)) {
pawnPromotion(board, player, maxDepth, depth, src, *dst, cell.row + direction, piece, max);
}
// en-passant
uint8 psrc = static_cast<uint8>(opponent.prev % 64);
uint8 pdst = static_cast<uint8>(opponent.prev / 64);
if (opponent.pieces[Piece::Pawn] & (one << pdst)) {
Cell ps(psrc);
Cell pd(pdst);
if (ps.row == rank(!player, 1) && pd.row == rank(!player, 3)) {
if (ps.col == cell.col - 1) {
dst = onboard(cell.col - 1, cell.row + direction);
recurse(board, player, maxDepth, depth, Move(piece, src, *dst, Special::EnPassant), max);
}
if (ps.col == cell.col + 1) {
dst = onboard(cell.col + 1, cell.row + direction);
recurse(board, player, maxDepth, depth, Move(piece, src, *dst, Special::EnPassant), max);
}
}
}
}
void moveRook(Board& board, int player, int maxDepth, int depth, uint64 src, Cell cell, Piece piece, int& max) {
for (auto move : bitboard.rookMoves)
moveDir(board, player, maxDepth, depth, src, cell, move[0], move[1], false, piece, max);
}
void moveKnight(Board& board, int player, int maxDepth, int depth, uint64 src, Cell cell, Piece piece, int& max) {
for (auto move : bitboard.knightMoves)
moveDir(board, player, maxDepth, depth, src, cell, move[0], move[1], true, piece, max);
}
void moveBishop(Board& board, int player, int maxDepth, int depth, uint64 src, Cell cell, Piece piece, int& max) {
for (auto move : bitboard.bishopMoves)
moveDir(board, player, maxDepth, depth, src, cell, move[0], move[1], false, piece, max);
}
void moveQueen(Board& board, int player, int maxDepth, int depth, uint64 src, Cell cell, Piece piece, int& max) {
for (auto move : bitboard.queenMoves)
moveDir(board, player, maxDepth, depth, src, cell, move[0], move[1], false, piece, max);
}
void moveKing(Board& board, int player, int maxDepth, int depth, uint64 src, Cell cell, Piece piece, int& max) {
for (auto move : bitboard.queenMoves)
moveDir(board, player, maxDepth, depth, src, cell, move[0], move[1], true, piece, max);
int col = cell.col;
int row = cell.row;
if (rank(player, row) == 0 && col == 4 && excludes(board.moved, src))
{
Player& me = board.players[player];
// castling queenside
if (includes(me.pieces[Piece::Rook], bitcalc(0, row)) && excludes(board.moved, bitcalc(0, row)))
{
if (excludes(board.all, bitcalc(1, row) | bitcalc(2, row) | bitcalc(3, row)))
{
if (canCastle(board, player, row, { 4, 3, 2 }))
{
recurse(board, player, maxDepth, depth, Move(Piece::King, src, bitcalc(col - 2, row), Special::CastLong), max);
}
}
}
// castling otherside
if (includes(me.pieces[Piece::Rook], bitcalc(7, row)) && excludes(board.moved, bitcalc(7, row)))
{
if (excludes(board.all, bitcalc(5, row) | bitcalc(6, row)))
{
if (canCastle(board, player, row, { 4, 5, 6 }))
{
recurse(board, player, maxDepth, depth, Move(Piece::King, src, bitcalc(col + 2, row), Special::CastShort), max);
}
}
}
}
}
bool canCastle(Board& board, int player, int row, std::array<int, 3> cols) {
Player& opponent = board.players[!player];
for (int p = 0; p < Piece::Max; p++) {
uint64 bits = opponent.pieces[p];
while (bits) {
uint64 bit = bits & (0ul - bits);
uint8 src = square(bit);
for (int col : cols) {
if (bitboard.check(!player, p, src, Cell(col, row).index(), board.all))
return false;
}
bits ^= bit;
}
}
return true;
}
void moveDir(Board& board, int player, int maxDepth, int depth, uint64 src, Cell cell, int colInc, int rowInc, bool single, Piece piece, int& max) {
Player& me = board.players[player];
Player& opponent = board.players[!player];
while (cell.move(colInc, rowInc))
{
uint64 dst = cell.flag();
if (includes(me.all, dst))
break;
recurse(board, player, maxDepth, depth, Move(piece, src, dst, 0), max);
if (single || includes(board.all, dst))
break;
}
}
public:
BitBoard& bitboard;
std::vector<Move> bestMoves;
std::vector<Move> allMoves;
};
// chess.cpp
class GamePlayer
{
public:
virtual std::optional<Move> play(AI& ai, Board& board, int side) = 0;
};
class PlayerHuman : public GamePlayer
{
public:
virtual std::optional<Move> play(AI& ai, Board& board, int side) override {
for (;;) {
// validate
ai.move(board, side, 1);
for (;;)
{
auto src = getSquare("Select source: ");
if (src == std::nullopt)
return std::nullopt;
std::string seperator = "";
bool found = false;
for (auto move : ai.allMoves) {
if (move.src == *src) {
if (found == false)
{
std::cout << "Valid destinations: ";
found = true;
}
Cell cell(square(move.dst));
std::cout << seperator << (char)('a' + cell.col) << (char)('8' - cell.row);
seperator = ", ";
}
}
if (found == false)
{
std::printf("No valid moves found from source.\n");
continue;
}
else
{
std::cout << '\n';
}
auto dst = getSquare("Select destination: ");
if (dst == std::nullopt)
return std::nullopt;
for (auto move : ai.allMoves) {
if (move.src == *src && move.dst == *dst) {
if (move.special == Special::Promote) {
auto piece = selectPiece();
if (piece == std::nullopt)
return std::nullopt;
move.piece = *piece;
}
return move;
}
}
std::cout << "Invalid move.\n";
}
}
}
private:
std::optional<uint64> getSquare(std::string_view prompt) {
for (;;) {
std::cout << prompt;
std::string line;
std::getline(std::cin, line);
if (std::cin.bad() || std::cin.eof())
return std::nullopt;
if (line.length() == 2) {
Cell cell(toupper(line[0]) - 'A', '8' - line[1]);
if (cell.valid())
return cell.flag();
}
std::cout << "Invalid square.\n";
}
}
std::optional<Piece> selectPiece() {
std::string prompt = "Select Piece: (R)ook, k(N)ight, (B)ishop or (Q)ueen\n";
for (;;) {
std::cout << prompt;
std::string line;
std::getline(std::cin, line);
if (std::cin.bad() || std::cin.eof())
return std::nullopt;
if (line.length() == 1) {
auto index = pieces[0].find(std::toupper(line[0]));
if (index != std::string::npos)
return static_cast<Piece>(index);
}
std::cout << "Invalid square.\n";
}
}
};
class PlayerAI : public GamePlayer
{
public:
virtual std::optional<Move> play(AI& ai, Board& board, int side) override {
std::cout << "Thinking...";
auto move = ai.move(board, side, 5);
Cell src(square(move.src));
Cell dst(square(move.dst));
std::cout << '\n' << char('a' + src.col) << char('8' - src.row) << " -> " << char('a' + dst.col) << char('8' - dst.row) << "\n";
return move;
}
};
void playGame(GamePlayer& pl1, GamePlayer& pl0) {
Board board;
board.init(
"RNBQKBNR"
"PPPPPPPP"
" "
" "
" "
" "
"pppppppp"
"rnbqkbnr"
);
BitBoard bitboard;
AI ai(bitboard);
int side = 1;
for (;;) {
board.print();
GamePlayer& player = side ? pl1 : pl0;
auto move = player.play(ai, board, side);
if (move == std::nullopt)
return;
board.apply(side, *move);
if ((*move).special == Special::Draw || (*move).score < -100)
{
bool check = ai.move(board, !side, 1).score > 100;
std::cout << (check ? "Checkmate" : "Draw");
return;
}
board.apply(side, *move);
side = !side;
}
}
int main() {
srand(static_cast<unsigned>(time(0)));
PlayerHuman human;
PlayerAI comp;
playGame(human, comp);
}
canCastle
shall also verify that neither King nor Rook ever moved. \$\endgroup\$