0

I've never worked with Stockfish or anything command line based like this before, so it took me a lot of getting used to in send/receiving the messages etc.

I do now have a C++ game project set up and it opens a stockfish process etc and interacts with it (sends commands and listens to commands).

I made two chess games. The first one didn't have stockfish and I done the move logic by myself. It ended up ok to have 1v1 against a friend locally, but really was just for fun. Anyway, when I came to try add Stockfish as CPU opponent, I started to learn about FEN string and all that other good stuff.

So... I decided to make a new version, and I use the FEN string as the basis of the rendering logic (ie. I convert that string into bunch of Piece sprites and lay them out where the string dictates they should go.)

It all works brilliantly. I can start a CPU vs CPU game and watch it moving all the pieces. However, when I come to include a Human player this is where I ran into problems.

The problem I am writing here about at this time is, I want the user to be able to click a piece (let's say he clicks Pawn e2 at the start of match), I want to highlight all the legal squares that can go to. But using Stockfish, I cannot seem to find enough data in the "info" string or any other that I found to piece together a list of valid destination squares for the selected piece.

If there is a command to return something like this it would be grand (eg. "Legal e2") but I am pretty sure there is NOT :( Any way at all would be great , otherwise I have to implement a lot of the logic for the whole game myself just to get this list of legal moves.

That was the question out of the way and I really hope it is possible. Meanwhile, I will post the bulk of the code here (NOTE: The code is work in progress, some stuff returns wrong values etc but I myself having written the code can navigate through that. If you tried to just copy this into your own project, it would probably be a bad example.

#include "Game.h"

#include "MegaEngine.h"
#include <cstdio>

#include "Sprite.h"
#include "Assets.h"
#include "ChessPiece.h"

#include <sstream>

void Game::run()
{
    MegaEngine::initGame(1920,1080);

    init();

    MegaEngine::runGameLoop(this);

    MegaEngine::closeWindow();
}

void Game::init()
{
    Chessboard_Sprite=new Sprite();
    Chessboard_Sprite->init(TEXTURE_CHESSBOARD);

    saAttr.nLength=sizeof(SECURITY_ATTRIBUTES);
    saAttr.bInheritHandle=TRUE;
    saAttr.lpSecurityDescriptor=NULL;

    StartStockfish();
    StartNewGame();

}

void Game::update()
{
    if(isCheckmate)
    {
        printf("\n*******\n*******\n*******\nCHECKMATE!\n*******\n*******\n*******\n");
        return;
    }

    if(isDraw)
    {
        printf("\n*******\n*******\n*******             DRAW!            *******\n*******\n*******\n");
        return;
    }

    if(player_to_act == 'w') // white turn
    {
        if(white_player == CPU_PLAYER)
        {
            //auto-play a turn
            if((autoplayTimer+=MegaEngine::getDeltaTime()) > autoplayInterval)
            {
                autoplayTimer=0.f;
                PlayMove(GetBestMove());
            }
        } 
        else // human player...
        {
            if(MegaEngine::mouseButtonClicked1)
            {
                ChessPiece* clickedPiece=getMouseOverPiece();
                if(clickedPiece)
                {
                    printf("piece clicked Rank,File: %d,%d\n",clickedPiece->GetRank(),clickedPiece->GetFileInt());
                    printf("piece pos str=%s\n",clickedPiece->GetPiecePositionString().c_str());
                    GetLegalMoves(clickedPiece->GetPiecePositionString().c_str());
                }
            }
        }
    }
    else if(player_to_act == 'b') // black turn
    {
        if(white_player == CPU_PLAYER)
        {
            //auto-play a turn
            if((autoplayTimer+=MegaEngine::getDeltaTime()) > autoplayInterval)
            {
                autoplayTimer=0.f;
                PlayMove(GetBestMove());
            }
        }
    }
}

void Game::render()
{
    
    
    Chessboard_Sprite->render();
    
    for(int i=0; i < pieceSprites.size(); i++)
        pieceSprites[i]->render();

    
}

void Game::StartStockfish()
{
    // Start the Stockfish process

    ZeroMemory(&si,sizeof(si));
    si.cb=sizeof(si);
    ZeroMemory(&pi,sizeof(pi));

    // Create pipe for the child process's standard input
    if(!CreatePipe(&hStdinRead,&hStdinWrite,&saAttr,0))
    {
        printf("CreatePipe failed (%d).\n",GetLastError());
        return;
    }

    // Create pipe for the child process's standard output
    if(!CreatePipe(&hStdoutRead,&hStdoutWrite,&saAttr,0))
    {
        printf("CreatePipe failed (%d).\n",GetLastError());
        return;
    }

    // Create pipe for the child process's standard error
    if(!CreatePipe(&hStderrRead,&hStderrWrite,&saAttr,0))
    {
        printf("CreatePipe failed (%d).\n",GetLastError());
        return;
    }

    si.hStdError=GetStdHandle(STD_ERROR_HANDLE);
    si.hStdOutput=hStdoutWrite;
    si.hStdInput=hStdinRead;
    si.dwFlags|=STARTF_USESTDHANDLES;

    if(!CreateProcess(
        (LPCTSTR)L"D:/_Dev/stockfish_15.1_win_x64_avx2/stockfish-windows-2022-x86-64-avx2.exe",   // No module name (use command line)
        (LPTSTR)L"",                // Command line
        NULL,                   // Process handle not inheritable
        NULL,                   // Thread handle not inheritable
        TRUE,                  // Set handle inheritance to TRUE
        0,                      // No creation flags
        NULL,                   // Use parent's environment block
        NULL,                   // Use parent's starting directory
        &si,                    // Pointer to STARTUPINFO structure
        &pi)                    // Pointer to PROCESS_INFORMATION structure
        )
    {
        printf("CreateProcess failed (%d).\n",GetLastError());
    }
}

void Game::CloseStockfish()
{
}

void Game::SendStockfishCommand(std::string str)
{
    DWORD bytesWritten=0;
    str+='\n';
    if(WriteFile(hStdinWrite,str.c_str(),str.length(),&bytesWritten,NULL))
    {
        printf("Sent command:%s  . BytesWritten=%ld\n",str.c_str(),bytesWritten);
    }
}

std::vector<std::string> Game::ReadStockfishOutput(DWORD delayMillis)
{
    // need some form of PeekPipe check before ReadFile to stop it hanging when there was no response but we try to check one anyway.
    Sleep(delayMillis);
    printf("*************************Read called*********************\n");
    std::vector<std::string> lines;
    DWORD bytesRead=0;
    char buffer[1024 * 8];
    while(true)
    {
        // Check if the read operation was successful
        if(!ReadFile(hStdoutRead,buffer,sizeof(buffer),&bytesRead,NULL))
        {
            printf("FAIL bytes READ=%ld\n",bytesRead);
            DWORD errorCode=GetLastError();
            if(errorCode == ERROR_BROKEN_PIPE)
            {
                printf("Pipe was closed\n");
            }
            else
            {
                printf("Error occurred while reading from the pipe: %d\n",errorCode);
                // Additional error handling code
            }
            break;
        }

        std::stringstream bufferStream(std::string(buffer,bytesRead));
        std::string line;
        while(std::getline(bufferStream,line))
        {
            lines.push_back(line);
        }

        fflush(stdout);
        break;

    }
    return lines;
}

void Game::GeneratePiecesFromFenString(std::string fenString)
{
    printf("GeneratePiecesFromFenString() called. 'fenString'=%s\n",fenString.c_str());
    pieceSprites.clear(); //check this clear is sufficient. I probably need to delete pointers and null the references. 
    int rank=0;
    int file=0;
    SDL_Rect sRect={0,0,0,0};
    SDL_FRect dRect={0.f,0.f,0.f,0.f};
    std::vector<char> fenChars(fenString.begin(), fenString.end());
    for(int i=0; i < fenChars.size(); i++)
    {
        char c=fenChars[i];

        if(c == ' ')
        {
            player_to_act=fenChars[i + 1]; // the char after the first space is always there so this won't cause problems.
            break; 
        }
        if(c == '/')
        {
            rank++;
            file=0;
            continue;
        }
        if(c < 'A')
        {
            file+=(int)c - '0'; 
            continue;
        }

        ChessPiece* spr=new ChessPiece();
        spr->init(
            TEXTURE_PIECES,
            7-rank, 
            file++,
            c);
        pieceSprites.push_back(spr);
    }
}

void Game::StartNewGame()
{
    // Send a command to Stockfish
    SendStockfishCommand("uci");
    std::vector<std::string> uci_response=ReadStockfishOutput(20);
    for(int i=0; i < uci_response.size(); i++)
    {
        printf("uci_response %d=%s\n",i,uci_response[i].c_str());
    }

    SendStockfishCommand("isready");
    std::vector<std::string> isready_response=ReadStockfishOutput(20);
    for(int i=0; i < isready_response.size(); i++)
    {
        printf("isready_response %d=%s\n",i,isready_response[i].c_str());
    }

    currentFen=START_FEN;
    GeneratePiecesFromFenString(START_FEN); // TODO: Could turn this into a parameter of this function, so we can start game from any position.
}

std::string Game::GetBestMove()
{
    std::string best;

    std::string cmd="position fen ";
    cmd+=currentFen;
    SendStockfishCommand(cmd);
    SendStockfishCommand("go depth 10");
    std::vector<std::string> position_startpos_response=ReadStockfishOutput((DWORD)cpuThinkTime);
    for(int i=0; i < position_startpos_response.size(); i++)
    {
        printf("go-depth output: %d=%s\n",i,position_startpos_response[i].c_str());
        if(position_startpos_response[i].compare(0,9,"bestmove ") == 0)
        {
            best=position_startpos_response[i].substr(9);
            std::size_t found=best.find(" ");
            best=best.substr(0,found);
        }
    }
    return best;
}

void Game::PlayMove(std::string move)
{
    std::string cmd="position fen ";
    cmd+=currentFen;
    cmd+=" moves ";
    cmd+=move;
    SendStockfishCommand(cmd);

    GetFenFromStockfish();

    GeneratePiecesFromFenString(currentFen); 
}

void Game::GetFenFromStockfish()
{
    SendStockfishCommand("d");
    std::vector<std::string> response=ReadStockfishOutput(100);
    for(int i=0; i < response.size(); i++)
    {
        printf("response %d=%s\n",i,response[i].c_str());
        //cache fen string, for use in rendering pieces
        if(response[i].compare(0,5,"Fen: ") == 0)
        {  
            std::string fenStr=currentFen;
            size_t spaceIndex=fenStr.find(" ");
            if(spaceIndex != std::string::npos)
            {
                fenStr=fenStr.substr(0,spaceIndex);
            }
            MoveLog.push_back(fenStr);
            currentFen=response[i].substr(5);
        }
        //check if king is in check.
        if(response[i].find("Checkers:") == 0 && response[i].length() > 11)
        {
            //printf("KKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKK\nCHECKERS LENGTH=% d\n",(int)response[i].length());
            //TODO: Can get exact square eg "a1" for the piece that put the king into check (for example to highlight it) using substr
        }
    }

    CheckMovesUntilMate();
    CheckDrawConditions();

}

int Game::CheckMovesUntilMate()
{
    SendStockfishCommand("isready");
    std::vector<std::string> isready_response=ReadStockfishOutput(100);
    for(int i=0; i < isready_response.size(); i++)
    {
        printf("isready_response %d=%s\n",i,isready_response[i].c_str());
    }

    SendStockfishCommand("go mate movetime=100");
    StopStockfishAfterTime(105);
    std::vector<std::string> response=ReadStockfishOutput(200);
    for(int i=0; i < response.size(); i++)
    {
        printf("movesuntilmatecheck_response %d=%s\n",i,response[i].c_str());
        if(response[i].find("mate 0") != std::string::npos)
        {
            isCheckmate=true;
        }

    }

    return 6969;
}

void Game::StopStockfishAfterTime(DWORD millis)
{
    Sleep(millis);
    SendStockfishCommand("stop");
}

void Game::CheckDrawConditions()
{
    // check repetition...
    for(int i=0; i < MoveLog.size(); i++)  //TODO: come up with a better way.
    {
        for(int j=i + 1; j < MoveLog.size(); j++)
        {
            for(int k=j + 1; k < MoveLog.size(); k++)
            {
                if(MoveLog[i] == MoveLog[j] && MoveLog[j] == MoveLog[k])
                {
                    printf("DRAW BY REPETITION!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! %d, %d, and %d\n",i,j,k);
                }
            }
        }
    }


    // check 50 rule... (50 rule is checked in the GetFenFromStockfish function
    
    // check stalemate...
}

void Game::GetLegalMoves(std::string squareCode)
{
    std::string cmd="moves ";
    cmd+=squareCode;
    cmd="eval";//TEST
    SendStockfishCommand(cmd);
    StopStockfishAfterTime(2000);
    std::vector<std::string> response=ReadStockfishOutput(20);
    for(int i=0; i < response.size(); i++)
    {
        printf("legal_response %d=%s\n",i,response[i].c_str());

    }
}

ChessPiece* Game::getMouseOverPiece()
{
    for(ChessPiece* piece : pieceSprites)
    {
        SDL_FRect pieceRect=piece->getDstRect();
        if(MegaEngine::mouseX >= pieceRect.x && MegaEngine::mouseX <= pieceRect.x + pieceRect.w &&
            MegaEngine::mouseY >= pieceRect.y && MegaEngine::mouseY <= pieceRect.y + pieceRect.h)
        {
            return piece;
        }
    }
    return nullptr;
}

Well I ended up figuring out one way, but it is very slow. It causes a long delay in the program. Here is the code I used to fix it, but I would love to hear of a better way.

std::vector<std::string> Game::GetLegalMoves(std::string squareCode)
{
    std::string cmd="go searchmoves ";
    cmd+=squareCode;
    std::vector<std::string> legals;
    
    for(int f=0; f < 8; f++)
    {
        for(int r=0; r < 8; r++)
        {
            std::string destinationStr="";
            destinationStr+=(char)(f + 'a');
            destinationStr+=(char)(r + '0' + 1);
            SendStockfishCommand(cmd + destinationStr);
            SendStockfishCommand("stop");

            std::vector<std::string> response=ReadStockfishOutput(20);
            for(int i=0; i < response.size(); i++)
            {
                printf("legal_response %d=%s\n",i,response[i].c_str());
                if((response[i].find("bestmove")) != std::string::npos)
                {
                    if((response[i].find("(n")) == std::string::npos)
                    {
                        std::string str=response[i].substr(11,2); //hardcoded location of destination square
                        legals.push_back(str);
                        printf("LEGAL MOVE ADDED TO LIST\n\n");
                    }
                }          
            }
        }
    }

    //DEBUG INFO...
    printf("legals size=%d\n",(int)legals.size());
    for(int i=0; i < legals.size(); i++)
    {
        printf("LEGALLINE:%s\n",legals[i].c_str());
    }
    // DEBUG END

    return legals;
}

The reason it is so slow is the call to "ReadStockfishOutput(20)" that allows a 20 millisecond halt in the program, but I am running this check brute force on every square in the board. I am trying to find a way that avoids coding all the piece movement logic for each piece and position individually. If I change the "halt" time to zero, it improves quite a lot, but it's still a bit slow.

1
  • Stockfish implements the perft command that is commonly used in chess engines for validating the correctness of their move generation methods. If you run this in Stockfish, it will list out all of the legal moves in a position and the number of possible positions that come after it to the specified depth. E.g. for the starting position, calling "go perft 1" would display all 20 legal moves from the position. You would be able to use that to quickly get the moves at the start of each turn and filter them down for each clicked square.
    – Nelson O
    May 11, 2023 at 15:16

2 Answers 2

1

Stockfish is a chess engine, it's not designed for generating legal moves. There is a legal move generator in the code but the engine is not made to expose legal moves for you.

  • Add a legal move generator outside of Stockfish. It's your responsibility for legal move generation not Stockfish. There are a few on Github.
  • Run multi-PV analysis where the number of PV is something like 100 so you can capture all legal moves from Stockfish. It's a hack.

The first option is the best.

1
  • I actually hacked together a working version. Its fairly snappy now (of course it isn't quite fast enough, there is like 0.2secs delay when you click the piece before the legal moves are captured. I will post the code to github soon. But also will post it here as argument. If you have any good examples of how to do your option 1, I'd love to discuss them. Thanks Jan 14, 2023 at 13:57
1

I solved the problem pretty well. But really I am not totally happy with brute forcing through every square on the board. (But at the same time, I love that my code doesn't need to care which pieces are which etc.)

What was a bit too hefty to post all here. So here is link to Github: https://github.com/RetroCoderTV/MegaChess

If you tried to just copy my entire project to your own Visual Studio, you would need to modify the code a tiny bit. You will find the odd hardcoded string filepaths. You will also need SDL2. I also used my own "Game Template" I had been working on and so the code has Box2d physics integrated even though its obviously not needed. (Might be cool for some kind of "WIN" effect though). You'll need to linker the SDL and Box2d libs and you need to copy some .dll files into the outputted .exe location (eg. "\MegaChess\x64\Debug" ..depends on the configuration you have at your end).

Still this took me a lot of figuring out. Perhaps this will save someone some time one day (IMPORTANT: The way I coded it is NOT best practice. I'm a hobby coder and just done it all with whatever knowledge I had)

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.