SebLague / Chess-Challenge

Create your own tiny chess bot!
https://www.youtube.com/watch?v=Ne40a5LkK6A
MIT License
1.77k stars 1.06k forks source link

Board.IsDraw() not working in Version 1.17 #393

Closed LooveToLoose closed 1 year ago

LooveToLoose commented 1 year ago

How to reproduce in version 1.17:

public Move Think(Board board, Timer timer)
{
    // some calculation to find the bestMove (board is fully reset to its original state after this)

    board.MakeMove(bestMove);
    if (board.IsDraw())
        Console.WriteLine("ABOUT TO MAKE A MOVE THAT RESULTS IN A DRAW.");
    board.UndoMove(bestMove);

    return bestMove; 
}

Expected behaviour: Whenever the AI makes a move that results in a draw, we should see "ABOUT TO MAKE A MOVE THAT RESULTS IN A DRAW." in the console

Current behaviour: The debug message never shows up in the console even if the next turn results in a draw. The play-out altorithm catches the draw, but you can't check for a draw in the Think function. As it is not currently possible to detect a draw, my AIs love spinning in circles and producing one tie after another. :D

Human error? I'm wondering if there is anything on my end that I might be doing wrong? If you have any ideas, please let me know, but I think the code example shown above is fairly waterproof, right? We should see the debug message if the game is about to result in a draw, but we don't.

Thanks for having a look! :)

LooveToLoose commented 1 year ago
public Move Think(Board board, Timer timer)
    {
        Console.WriteLine(board.ZobristKey);

        // My Calculation

        board.MakeMove(bestMove);
        if (board.IsDraw())
            Console.WriteLine("ABOUT TO MAKE A MOVE THAT RESULTS IN A DRAW. BUT WHY???");
        board.UndoMove(bestMove);

        Console.WriteLine(board.ZobristKey);
        return bestMove; 
    }

ZobristKeys match up as well, so the board is definitely reset correctly.

SebLague commented 1 year ago

Hi, I'm struggling to reproduce this issue. Would you be able to share a position and some more code for reproducing? Here's a simple test for example that shows IsDraw working for stalemate/repetition/insufficient material.

using ChessChallenge.API;
using System;

public class MyBot : IChessBot
{
    public Move Think(Board board, Timer timer)
    {
        Console.WriteLine("Testing draw...");

        // Stalemate test
        Board testBoard = Board.CreateBoardFromFEN("3k4/8/3K4/8/8/8/8/4Q3 w - - 0 1");
        Move stalemateMove = new Move("e1e6", testBoard);
        testBoard.MakeMove(stalemateMove);
        if (testBoard.IsDraw())
        {
            Console.WriteLine("Move enters stalemate");
        }

        // Repetition test
        testBoard = Board.CreateBoardFromFEN("3k4/8/3K4/8/8/8/8/4Q3 w - - 0 1");
        testBoard.MakeMove(new Move("e1e2", testBoard));
        testBoard.MakeMove(new Move("d8c8", testBoard));
        testBoard.MakeMove(new Move("d6c6", testBoard));
        testBoard.MakeMove(new Move("c8d8", testBoard));
        Move repeatedPositionMove = new Move("c6d6", testBoard);
        testBoard.MakeMove(repeatedPositionMove);
        // Note: this function will return true if the same position has occurred twice on the board
        // (rather than 3 times, which is when the game is actually drawn).
        // This quirk is to help bots avoid repeating positions unnecessarily.
        if (testBoard.IsDraw())
        {
            Console.WriteLine("Move enters repeated position");
        }

        // Insufficient material test
        testBoard = Board.CreateBoardFromFEN("3k4/8/3K4/8/8/8/8/4Q3 w - - 0 1");
        testBoard.MakeMove(new Move("e1e8", testBoard));
        testBoard.MakeMove(new Move("d8e8", testBoard));
        if (testBoard.IsDraw())
        {
            Console.WriteLine("Move enters insufficient material draw");
        }

        // Return anything
        return board.GetLegalMoves()[0];
    }
}
LooveToLoose commented 1 year ago

Hey, thanks for having a look. Here is how to reproduce it.

1) Copy this into MyBot:

using ChessChallenge.API;
using System;

public class MyBot : IChessBot
{
    const int maxSearchDepth = 10;
    int defaultSearchDepth = 4;

    Move[][] allPossibleMoves = new Move[maxSearchDepth][];
    Move bestMove;

    public MyBot()
    {
        for (int i = 0; i < maxSearchDepth; i++) {
            allPossibleMoves[i] = new Move[218];
        }
    }

    public Move Think(Board board, Timer timer)
    {
        Console.WriteLine(board.ZobristKey);

        Span<Move> moves = allPossibleMoves[defaultSearchDepth].AsSpan();
        moves.Clear();
        board.GetLegalMovesNonAlloc(ref moves);
        bestMove = moves[0];
        MoveCalculation(board, defaultSearchDepth, true);

        board.MakeMove(bestMove);
        if (board.IsDraw())
            Console.WriteLine("ABOUT TO MAKE A MOVE THAT RESULTS IN A DRAW.");
        board.UndoMove(bestMove);

        Console.WriteLine(board.ZobristKey);
        return bestMove; 
    }

    public float MoveCalculation(Board board, int depthleft, bool setBestMove)
    {
        if (depthleft == 0)
            return EvaluatePosition(board);

        Span<Move> moves = allPossibleMoves[depthleft].AsSpan();
        board.GetLegalMovesNonAlloc(ref moves);
        float bestScore = float.MinValue;
        foreach (Move move in moves) {
            board.MakeMove(move);
            float score = -MoveCalculation(board, depthleft - 1, false);
            board.UndoMove(move);
            if (score > bestScore)
            {
                bestScore = score;
                if (setBestMove)
                {
                    bestMove = move;
                }
            }
        }
        return bestScore;
    }

    public float EvaluatePosition(Board board)
    {
        float evaluation = 0;

        // If draw:
        if (board.IsDraw())
            return float.MaxValue; // Essentially treat being in a draw as a win if it is your turn, making sure the opponent should never allow a position where a draw is possible.

        // If checkmate:
        if (board.IsInCheckmate())
            return float.MinValue; // Very bad outcome for the player who is checkmated so assigning a very negative score.

        return evaluation;
    }
}

2) Play a MyBot vs MyBot match

3) Outcome: The match results in a draw within only a few moves but we never get the "ABOUT TO MAKE A MOVE THAT RESULTS IN A DRAW" debug message. (So as far as I am aware it is only the draw by repetition that is not being detected, but that is usually the only way I draw to be honest.)

loki12241224 commented 1 year ago

I am currently having the same issue where it is impossible for my ai to detect a draw. I directly test for the board position after a move being a draw before making that move and it will never return true regardless of the outcome of a match.

it is literally just

makemove- detect if draw- this never goes off return move-

SebLague commented 1 year ago

Thanks for bringing this to my attention @LooveToLoose, I have just uploaded a patch for this bug. Please note -- in case you haven't seen it in the docs -- that you will get the 'results in draw' message one move early, but this is intended behaviour in this API:

Note: this function will return true if the same position has occurred twice on the board (rather than 3 times,
which is when the game is actually drawn). This quirk is to help bots avoid repeating positions unnecessarily.
LooveToLoose commented 1 year ago

One move early makes perfect sense cause the AIs will just keep spinning in circles otherwise anyways. Excellent.

I just tested it out and it now works as expected. So I can confirm that the issues has been resolved! Thanks a lot.

I hope you know how much joy you bring people with this little competition. This is so much fun. I am totally addicted. Haha. Thanks agian. Keep up the great work! <3