AI finds mate gives wrong behaviour

Discussion of chess software programming and technical issues.

Moderator: Ras

eligolf
Posts: 114
Joined: Sat Nov 14, 2020 12:49 pm
Full name: Elias Nilsson

AI finds mate gives wrong behaviour

Post by eligolf »

Hi,

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;
        }
I would be so happy if any of you could point me out on where to start looking. I have looked at the way I store PV line here and it is exactly the same as in my other 2 engines which works. Perft works in all cases and as said, it plays decent chess otherwise.
eligolf
Posts: 114
Joined: Sat Nov 14, 2020 12:49 pm
Full name: Elias Nilsson

Re: AI finds mate gives wrong behaviour

Post by eligolf »

For anyone experiencing similar issues, I had a bug where I set alpha and beta to short.MinValue and short.MaxValue at the start. This apparently corresponds to -32768 and 32767. I had my mateScore and mateValue set to 98000 and 99000 which apprently caused this strange error. Changing min and max value (elpha and beta) to -100.000 and 100.000 solved the issue :)
Tearth
Posts: 70
Joined: Thu Feb 25, 2021 5:12 pm
Location: Poland
Full name: Pawel Osikowski

Re: AI finds mate gives wrong behaviour

Post by Tearth »

It reminded me a bit similar bug in one of the previous engines where I was also settings something like short.MinValue and short.MaxValue for alpha and beta - it obviously led to a trivial bug where the nega-max was trying to negate short.MinValue which in fact is not giving you any positive value, but exactly the same number (you can't have 32768 in 16 bits + sign). Since this time, I always use constants that aren't at the edge of the type range.