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.