I have implemented a simple variant of Zobrist keys to store and look up positional data, but whenever I let my engine play itself with either white or black using hashtables and the opponent doesn't, the player using the hashtable consistently performs worse.
I can rule out a significant computational load due to the hash key updates and storing/retrieval of data - the kilonodes per second are more or less within the same range. Secondly, I observe that in the endgame, a 9 ply depth of the player not using hashtables is met with about 11 plies for the player using hashtables. In the early game, there is almost no discernible advantage in search depth (which is a side question - does that seem sensible?). Still, given the increased endgame depth, one would expect a systematic benefit for the player using hashtables - all else being equal (no quiescence search, same allocated time for iterative deepening), that is.
Hence, I believe that the error has to do with storage or retrieval of data; or incorrect hash keys. The latter seems implausible based on tests. I would appreciate if anyone can point out a potential problem with the hashtable implementation.
Here is the code to insert data into the hashtable, which is called at each node when alpha beta returns a value:
void Hashtable::setHashData(int distanceToLeaf, int value, int alpha, int beta, std::tuple<char, char, char, char> preferredMove) {
unsigned long index = Hashtable::hash % Hashtable::HASH_SIZE;
if (((value > alpha) && (value < beta)) || (alpha == beta)) {
Hashtable::table[index].type = NodeType::Exact;
Hashtable::table[index].value = value;
}
else if (value >= beta) {
Hashtable::table[index].type = NodeType::Upper;
Hashtable::table[index].value = value;
}
else if (value <= alpha) {
Hashtable::table[index].type = NodeType::Lower;
Hashtable::table[index].value = value;
}
Hashtable::table[index].distanceToLeaf = distanceToLeaf;
Hashtable::table[index].zobristKey = Hashtable::hash;
Hashtable::table[index].preferredMove = preferredMove;
}
Here is the part of the alpha-beta-search function that invokes the hashtable. This part is executed before the possible moves at this position are determined and alphabeta is called recursively:
int Engine::alphaBeta(ChessBoard* board, Colour colour, char depth, int alpha, int beta)
[...]
if (useHashtable) {
HashData data = Engine::transpos_table.getHashData();
if ((data.zobristKey == Engine::transpos_table.hash) && (data.distanceToLeaf >= (depthLimit - depth))) {
Engine::transpos_table.hashHits++;
if ((data.type == NodeType::Exact))
return data.value;
else if ((data.type == NodeType::Lower) && (data.value < beta))
beta = data.value;
else if ((data.type == NodeType::Upper) && (data.value > alpha))
alpha = data.value;
if (alpha >= beta) return data.value;
if (colour == Colour::White)
return Engine::alphaBeta_preferredMove(board, Colour::White, depth, alpha, beta, data.preferredMove);
else
return Engine::alphaBeta_preferredMove(board, Colour::Black, depth, alpha, beta, data.preferredMove);
}
}
The idea being that I either have searched a position fully (exact node- I can just return the value), or I can update alpha or beta if they provide an improvement of an earlier bound. Moreover, I can store the preferred move (may not be the best one due to an early cutoff) and pass that to an alpha beta implementation that executes this preferred move first (Engine::alphaBeta_preferredMove).
Lastly, here is the code for updating the Zobrist key given a move (this has been tested to be self-inverse, but perhaps there is still a problem somewhere - and apologies for the lack of case statements, and the nasty std::get<> notation). Obviously, this gets called every time a move is made, and every time a move is unmade. "hk_[...]" are (arrays of) random bitstrings per board feature that are initialized at the start.
void Hashtable::updateHash(ChessBoard* board, MoveData move) {
if (move.pieceMoved->colour == Colour::White) {
hash ^= hk_blackToMove;
if (move.pieceMoved->getPieceType() == PieceType::Pawn)
hash = hash ^ hk_whitePawn[std::get<0>(move.orig)][std::get<1>(move.orig)] ^ hk_whitePawn[std::get<0>(move.dest)][std::get<1>(move.dest)];
else if (move.pieceMoved->getPieceType() == PieceType::Knight)
hash = hash ^ hk_whiteKnight[std::get<0>(move.orig)][std::get<1>(move.orig)] ^ hk_whiteKnight[std::get<0>(move.dest)][std::get<1>(move.dest)];
else if (move.pieceMoved->getPieceType() == PieceType::Bishop)
hash = hash ^ hk_whiteBishop[std::get<0>(move.orig)][std::get<1>(move.orig)] ^ hk_whiteBishop[std::get<0>(move.dest)][std::get<1>(move.dest)];
else if (move.pieceMoved->getPieceType() == PieceType::Rook)
hash = hash ^ hk_whiteRook[std::get<0>(move.orig)][std::get<1>(move.orig)] ^ hk_whiteRook[std::get<0>(move.dest)][std::get<1>(move.dest)];
else if (move.pieceMoved->getPieceType() == PieceType::Queen)
hash = hash ^ hk_whiteQueen[std::get<0>(move.orig)][std::get<1>(move.orig)] ^ hk_whiteQueen[std::get<0>(move.dest)][std::get<1>(move.dest)];
else if (move.pieceMoved->getPieceType() == PieceType::King)
hash = hash ^ hk_whiteKing[std::get<0>(move.orig)][std::get<1>(move.orig)] ^ hk_whiteKing[std::get<0>(move.dest)][std::get<1>(move.dest)];
if (move.pieceTaken != nullptr) {
if (move.pieceTaken->getPieceType() == PieceType::Pawn)
hash = hash ^ hk_blackPawn[std::get<0>(move.dest)][std::get<1>(move.dest)];
else if (move.pieceTaken->getPieceType() == PieceType::Knight)
hash = hash ^ hk_blackKnight[std::get<0>(move.dest)][std::get<1>(move.dest)];
else if (move.pieceTaken->getPieceType() == PieceType::Bishop)
hash = hash ^ hk_blackBishop[std::get<0>(move.dest)][std::get<1>(move.dest)];
else if (move.pieceTaken->getPieceType() == PieceType::Rook)
hash = hash ^ hk_blackRook[std::get<0>(move.dest)][std::get<1>(move.dest)];
else if (move.pieceTaken->getPieceType() == PieceType::Queen)
hash = hash ^ hk_blackQueen[std::get<0>(move.dest)][std::get<1>(move.dest)];
else if (move.pieceTaken->getPieceType() == PieceType::King)
hash = hash ^ hk_blackKing[std::get<0>(move.dest)][std::get<1>(move.dest)];
}
if (move.isWhiteLRookCastling)
hash = hash ^ hk_whiteLRookCastling;
else if (move.isWhiteRRookCastling)
hash = hash ^ hk_whiteRRookCastling;
}
else {
hash = hash ^ hk_whiteToMove;
if (move.pieceMoved->getPieceType() == PieceType::Pawn)
hash = hash ^ hk_blackPawn[std::get<0>(move.orig)][std::get<1>(move.orig)] ^ hk_blackPawn[std::get<0>(move.dest)][std::get<1>(move.dest)];
else if (move.pieceMoved->getPieceType() == PieceType::Knight)
hash = hash ^ hk_blackKnight[std::get<0>(move.orig)][std::get<1>(move.orig)] ^ hk_blackKnight[std::get<0>(move.dest)][std::get<1>(move.dest)];
else if (move.pieceMoved->getPieceType() == PieceType::Bishop)
hash = hash ^ hk_blackBishop[std::get<0>(move.orig)][std::get<1>(move.orig)] ^ hk_blackBishop[std::get<0>(move.dest)][std::get<1>(move.dest)];
else if (move.pieceMoved->getPieceType() == PieceType::Rook)
hash = hash ^ hk_blackRook[std::get<0>(move.orig)][std::get<1>(move.orig)] ^ hk_blackRook[std::get<0>(move.dest)][std::get<1>(move.dest)];
else if (move.pieceMoved->getPieceType() == PieceType::Queen)
hash = hash ^ hk_blackQueen[std::get<0>(move.orig)][std::get<1>(move.orig)] ^ hk_blackQueen[std::get<0>(move.dest)][std::get<1>(move.dest)];
else if (move.pieceMoved->getPieceType() == PieceType::King)
hash = hash ^ hk_blackKing[std::get<0>(move.orig)][std::get<1>(move.orig)] ^ hk_blackKing[std::get<0>(move.dest)][std::get<1>(move.dest)];
if (move.pieceTaken != nullptr) {
if (move.pieceTaken->getPieceType() == PieceType::Pawn)
hash = hash ^ hk_whitePawn[std::get<0>(move.dest)][std::get<1>(move.dest)];
else if (move.pieceTaken->getPieceType() == PieceType::Knight)
hash = hash ^ hk_whiteKnight[std::get<0>(move.dest)][std::get<1>(move.dest)];
else if (move.pieceTaken->getPieceType() == PieceType::Bishop)
hash = hash ^ hk_whiteBishop[std::get<0>(move.dest)][std::get<1>(move.dest)];
else if (move.pieceTaken->getPieceType() == PieceType::Rook)
hash = hash ^ hk_whiteRook[std::get<0>(move.dest)][std::get<1>(move.dest)];
else if (move.pieceTaken->getPieceType() == PieceType::Queen)
hash = hash ^ hk_whiteQueen[std::get<0>(move.dest)][std::get<1>(move.dest)];
else if (move.pieceTaken->getPieceType() == PieceType::King)
hash = hash ^ hk_whiteKing[std::get<0>(move.dest)][std::get<1>(move.dest)];
}
if (move.isBlackLRookCastling)
hash = hash ^ hk_blackLRookCastling;
else if (move.isBlackRRookCastling)
hash = hash ^ hk_blackRRookCastling;
}
if (move.prevEnPassantPawn != std::tuple<char, char>(127, 127))
hash = hash ^ hk_enPassantSquare[std::get<0>(move.prevEnPassantPawn)][std::get<1>(move.prevEnPassantPawn)];
}
Hashtable::hash
seems suspicious (it should be a function based on the key, but I'm not sure the definition ofHashtable::hash
)