I have written my engine and it performs all perft cases perfectly and it plays decent chess given the simple evaluation function I have right now. The only issue is that when it find mate (or that it will be mated) it somehow gets the wrong PV line and just fills it up with 0:s. Given this position for example where AI plays white it gets the wrong result, or if I play Qe7 it will also go wrong and give a 0 move:
[fen]2k5/8/1K6/4Q3/8/8/8/8 w - - 0 1[/fen]
I have a working C# engine without bitboards which looks almost identical (except for the part where I have to check if move is legal) and also I have a bitboard engine written in Python with EXACTLY the same rows of code, only translated to C#. I have no idea where I go wrong and have been stuck on this for some time now. Things I tried:
- Changed the array of moves to a list instead so that I wouldn't have empty spots at the end of the array, no change.
- Removed both the check for draw and sort move functions with same results.
- Printed the isInCheck result and it correctly thinks that black is in check when it has no legal moves left.
Here is the code, I can explain if there are any uncertainties or if I left something out:
Code: Select all
// -----------------------------------------------------------------------------------
// ###################################################################################
// ITERATIVE DEEPENING
// ###################################################################################
// -----------------------------------------------------------------------------------
public static ulong FindBestMove(BoardState _boardState, int _maxDepth)
{
// Init variables
boardState = _boardState;
maxPly = 60;
mateScore = SearchConstants.mateScore;
mateValue = SearchConstants.mateValue;
bestMove = 0ul;
bestScore = 0;
nodes = 0;
stopSearch = false;
stopWatch = Stopwatch.StartNew();
// Start iterative deepening to find the best move
for (int depth = 1; depth <= _maxDepth; depth++)
{
// Init variables
int alpha = SearchConstants.minValue;
int beta = SearchConstants.maxValue;
int ply = 0;
// Reset tables
ResetSortingTables();
ResetPVTables();
// Set follow pv to true first
followPV = true;
// Search position
float score = Negamax(depth, ply, alpha, beta);
// Timing for current depth
searchTimer = stopWatch.ElapsedMilliseconds;
// If we don't stop the search, save some variables for next iteration
if (true) // Later: !stopSearch
{
// Update best move and best score so far
bestMove = pvTable[0][0];
bestScore = score;
// Draw pv line for last iteration
if (depth == _maxDepth)
{
string pvLine = "";
for (int index = 0; index < _maxDepth; index++)
{
ulong move = pvTable[0][index];
int moveFrom = (int)Move.GetFromSquare(move);
int moveTo = (int)Move.GetToSquare(move);
pvLine += GameConstants.squareIndexToString(moveFrom) + "-" + GameConstants.squareIndexToString(moveTo) + ", ";
}
UnityEngine.Debug.Log("PV line: " + pvLine);
}
}
}
UnityEngine.Debug.Log("Time: " + searchTimer / 1000 + " s." + "\n" +
"Nodes: " + nodes + "\n" +
"Nodes/s: " + Math.Round((nodes / (searchTimer / 1000))));
return bestMove;
}
// -----------------------------------------------------------------------------------
// ###################################################################################
// NEGAMAX LOOP
// ###################################################################################
// -----------------------------------------------------------------------------------
private static float Negamax(int depth, int ply, float alpha, float beta)
{
float score;
bool foundPV = false;
// Init PV
pvTable[ply][ply] = 0;
pvLength[ply] = 0;
// Find if it is a 3 fold repetition
if (boardState.CheckForDraw()) return 0;
// Check if we reached depth 0
if (depth == 0)
{
score = Evaluate.EvaluateOnlyMaterial(boardState);
return score;
}
// Check if we are in check
int ownKingIndex = boardState.ColorToMove == Color.White ?
BitOperations.GetLSBIndex(boardState.Pieces[Color.White][Piece.King]) :
BitOperations.GetLSBIndex(boardState.Pieces[Color.Black][Piece.King]);
bool isInCheck = boardState.IsSquareAttacked(boardState.ColorToMove, ownKingIndex);
// Get legal moves
ulong[] children = boardState.GetAllMoves();
// Sort moves
SortMoves(ply, children);
// If we are following PV line we want to enable PV scoring
if (followPV) EnablePVScoring(ply, children);
// Init number of legal moves and moves searched
int legalMoves = 0;
// Loop over all moves
foreach (ulong move in children)
{
// Break if finding empty move (list has ended)
if (move == 0) break;
// Try if move is legal
if (boardState.MakeMove(move))
{
// Increase legal moves and node count
legalMoves++;
nodes++;
// Do a normal search
score = -Negamax(depth - 1, ply + 1, -beta, -alpha);
// Unmake the move
boardState.UnmakeMove(move);
// Found a better move
if (score > alpha)
{
foundPV = true;
alpha = score;
// History moves (non capturing and not enpassant)
if (!Move.IsCapture((int)Move.GetMoveFlag(move)))
{
historyMoves[(int)Move.GetPieceMoved(move)][(int)Move.GetToSquare(move)] += depth;
}
// Write PV move to PV table for the given ply
pvTable[ply][ply] = move;
// Loop over the next ply
for (int j = 0; j < pvLength[ply + 1]; j++)
{
// Copy move from deeper ply into current ply's line
pvTable[ply][ply + 1 + j] = pvTable[ply + 1][ply + 1 + j];
}
// Adjust PV lenght
pvLength[ply] = 1 + pvLength[ply + 1];
// Fail hard beta cutoff (node fails high)
if (score >= beta)
{
// Store killer moves (non capturing and not enpassant)
if (!Move.IsCapture((int)Move.GetMoveFlag(move)))
{
killerMoves[1][ply] = killerMoves[0][ply];
killerMoves[0][ply] = move;
}
return beta;
}
}
}
}
// If we don't have any legal moves in the position, check if it is mate or stalemate
if (legalMoves == 0)
{
// If checkmate, return the mate value
if (isInCheck)
{
return -mateValue + ply;
}
// Else stalemate, return 0
else
{
return 0;
}
}
// Node fails low
return alpha;
}