There are many free chess AI engines out there, and most utilize the UCI protocol. It’s fairly tricky to configure a chess program you’ve made to communicate with it–it took me several days to figure out. And since there are no available tutorials, I’ll show you how to do it on Windows with C++. It’s going to be quite in-depth–anyone who wants to do this should be proficient in programming.
I’ll be using Windows named pipes for this implementation. You basically open a hidden communication tunnel to the AI process from your chess program. My chess program opens the AI engine process from within itself and creates the pipes. It’s very important that you understand the UCI protocol documentation, at least the basics.
Here’s the header file for the AI class:
#include
#include
class AI{
public:
AI();
~AI();
bool init(void);
void reset(void);
void moveAgainst(void); // sends user's move
// setter functions
void stop(void);
void setPos(const char* pos);
These function concepts should be pretty self explanatory for now. Now onto the private members.
private:
void cleanup(void);
static unsigned long WINAPI InitThread(void* lpThread);
DWORD WINAPI _AI(LPVOID lpBuffer);
void parseAIMove(const char* str);
void moveAIPiece(void);
char m_pos[4096]; // holds all the moves for engine to interpret
char m_lastAIMove[32];
char m_lastUserMove[32];
// states
bool m_active;
bool m_sendMove;
unsigned long m_exit;
unsigned long m_bread;
unsigned long m_avail;
// engine process variables
STARTUPINFO m_si;
PROCESS_INFORMATION m_pi;
SECURITY_ATTRIBUTES m_sa;
HANDLE m_child_stdin, m_child_stdout, m_hRead, m_hWrite;
HANDLE m_hJob;
JOBOBJECT_EXTENDED_LIMIT_INFORMATION m_jeli;
};
inline void AI::stop(void)
{
m_active = false;
}
inline void AI::setPos(const char* pos)
{
strcpy(m_pos, pos);
}
Now that I’ve laid out the header file, I’ll go through the actual function code and explain it in more detail. First we have the constructor and desctructor.
#include "ai.h"
AI::AI()
{
/* setup the position command */
strcpy(m_pos,"position startpos moves ");
m_searchDepth = 10; // how many moves the engine will search
}
AI::~AI()
{
cleanup();
}
Again, make sure you understand the UCI protocol well enough, otherwise the strcpy line won’t make any sense. This is just one way of communicating with the engine and it’s the way I chose. Everything after “position startpos moves ” will consist of the game moves. And our C string m_pos will be storing those.
And here’s the init function:
/* determine if we are running 64-bit Windows */
static int isOS64Bit(void)
{
typedef BOOL (WINAPI* LPFN_ISWOW64PROCESS)(HANDLE, PBOOL);
LPFN_ISWOW64PROCESS _IsWow64Process = NULL;
BOOL b64bit = FALSE;
_IsWow64Process = (LPFN_ISWOW64PROCESS)GetProcAddress(LoadLibrary("Kernel32.dll"), "IsWow64Process");
if(_IsWow64Process == NULL)
return false;
if(_IsWow64Process(GetCurrentProcess(), &b64bit))
return b64bit;
return false;
}
bool AI::inst(void)
{
char engine_path[MAX_PATH] = {0};
bool engine_started = false;
bool b64bit = isOS64Bit();
bool jobSuccess = false;
// set up the security attributes for AI process
m_sa.bInheritHandle = true;
m_sa.lpSecurityDescriptor = NULL;
m_sa.nLength = sizeof(SECURITY_ATTRIBUTES);
// setup job to kill the child process when calling process terminates
// this prevents the AI process from running when the main program closes
m_hJob = CreateJobObject(NULL, NULL);
if(m_hJob != NULL){
m_jeli.BasicLimitInformation.LimitFlags = JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE;
jobSuccess = SetInformationJobObject(m_hJob, JobObjectExtendedLimitInformation, &m_jeli, sizeof(m_jeli));
}
Okay, so far so good. Now we will setup the pipe to the AI process.
// create the child input pipe
if(!CreatePipe(&m_child_stdin, &m_hWrite, &m_sa, 0)){
return false;
}
// create the child output pipe
if(!CreatePipe(&m_hRead, &m_child_stdout, &m_sa, 0)){
return false;
}
// prepare to launch the process
GetStartupInfo(&m_si);
m_si.dwFlags = STARTF_USESTDHANDLES | STARTF_USESHOWWINDOW;
m_si.wShowWindow = SW_HIDE;
m_si.hStdOutput = m_child_stdout;
m_si.hStdError = m_child_stdout;
m_si.hStdInput = m_child_stdin;
Not so bad, right? Keep in mind that you will need to know some Win32 API to understand how this works. The startup info structure is essential for spawning any new process with CreateProcess().
Now we’ll actually start the AI engine and apply some process settings.
// set the path to the engine process
strcpy(engine_path, "C:/engine.exe");
// start the process
engine_started = CreateProcess(engine_path,
NULL,
NULL,
NULL,
TRUE,
0,
NULL,
&m_si,
&m_pi);
if(!engine_started){
// further error handling...
return false;
}
// assign the auto-terminate job we created earlier to the process
if(jobSuccess){
jobSuccess = AssignProcessToJobObject(m_hJob, m_pi.hProcess);
}
// Optional: set the process priority to below normal to prevent CPU over-usage
SetPriorityClass(m_pi.hProcess, BELOW_NORMAL_PRIORITY_CLASS);
Pretty straightforward. We’ve created the process and assigned the job to it, which will force the engine to terminate once the chess program closes. The more automation within the class, the better.
Now all that’s left to do is start the AI thread and loop for input and output.
if(engine_started){
m_active = true;
(void)CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE)&InitThread, this, 0, 0);
}
return true;
}
Now for the fun part. This actually took a while to perfect. The AI thread will listen to the pipe and check for output, while simultaneously listening for input from the chess program and sending it to the engine when needed. Here’s the code, along with the function to start the thread:
unsigned long WINAPI AI::InitThread(void* lpThread)
{
if(lpThread){
return ((AI*)lpThread)->_AI(NULL);
}
return 0;
}
/* AI looping thread */
DWORD WINAPI AI::_AI(LPVOID lpBuffer)
{
char buf[BUFSIZE];
memset(buf, 0, sizeof(buf));
// wait for process to initialize
Sleep(500);
for(int i=0; m_active; ++i){
// test if the engine is still running
GetExitCodeProcess(m_pi.hProcess, &m_exit);
if(m_exit != STILL_ACTIVE){
m_active = false;
}
// check for output from the engine
PeekNamedPipe(m_hRead, buf, BUFSIZE, &m_bread, &m_avail, NULL);
if(m_bread > 0){
for(;;){
memset(buf, 0, sizeof(buf));
// read the output buffer
ReadFile(m_hRead, buf, BUFSIZE, &m_bread, NULL);
/// if it's not the player's turn
parseAIMove(buf);
Sleep(100);
PeekNamedPipe(m_hRead, buf, BUFSIZE, &m_bread, &m_avail, NULL);
if(m_bread > 0)
continue;
else
break;
}
}
I am not including any of the functions from my chess interface because I want to keep this AI class easily adaptable. Before I call the parseAIMove() function, I use three slashes for the comment–this means you should handle that if statement within your own chess interface.
Moving on to the input:
// loop through the startup sequence for the engine
sprintf(buf, (i == 0) ? "uci " : (i == 1) ? "isready " : (i == 2) ? "setoption name Hash value 512 " : "");
if(m_sendMove == true){
// append the new move from the user
strcat(m_pos, m_lastUserMove);
strcat(m_pos, " "); // a space is essential for the pipes to work here
// send the input
WriteFile(m_hWrite, m_pos, sizeof(m_pos), &m_bread, NULL);
WriteFile(m_hWrite, "\n", 1, &m_bread, NULL); // send newline separately
// now tell the engine to think about the next move
sprintf(buf, "go wtime 60000 btime 60000 depth %d ", m_searchDepth); // insert your own variable for time here, in ms
WriteFile(m_hWrite, buf, sizeof(buf), &m_bread, NULL);
WriteFile(m_hWrite, "\n", 1, &m_bread, NULL);
m_sendMove = false;
/// set the game turn to black
}
else{
WriteFile(m_hWrite, buf, sizeof(buf), &m_bread, NULL);
WriteFile(m_hWrite, "\n", 1, &m_bread, NULL);
if(i > 100000)
i = 5; // reset the incrementer when necessary
}
}
cleanup();
ExitThread(0);
}
That wraps up the main loop. Of course you will have to modify some portions to make it work with your program. The if else statement at the end is specially crafted to start the engine with the conventional method and set some options.
Now that we’ve got that done, we can move on to the other class functions. Most of the work involves translating the user’s moves into the algebraic representation and vice versa.
void AI::parseAIMove(const char* str)
{
const int BESTMOVE_OFFSET = 9;
const int BESTMOVE_END_OFFSET = 4;
char buf[BUFSIZE];
char* token;
char newmove[32];
strcpy(buf, str);
token = strtok(buf, "\n");
while(token != NULL){
if(strstr(token, "bestmove")){
char* p = token + BESTMOVE_OFFSET;
*(p + BESTMOVE_END_OFFSET) = 0;
strcat(m_pos, p);
strcat(m_pos, " ");
strcpy(m_lastAIMove, p);
moveAIPiece();
}
token = strtok(NULL, "\n");
}
}
I figured out how to do this by just watching the output of the engine while I entered a hypothetical game. This function looks line by line at the engine’s output for the string “bestmove” and finds the move afterward. This is how the engine displays its chosen move.
Now for the function that actually translates the engine’s move into an easier form for the chess program to read.
void AI::moveAIPiece(void)
{
int fromX, fromY;
int toX, toY;
/* get the 'from' move first */
// get x component
fromX = m_lastAIMove[1] - '0';
// get y component
switch(m_lastAIMove[0]){
case 'a': fromY = 1; break;
case 'b': fromY = 2; break;
case 'c': fromY = 3; break;
case 'd': fromY = 4; break;
case 'e': fromY = 5; break;
case 'f': fromY = 6; break;
case 'g': fromY = 7; break;
case 'h': fromY = 8; break;
default: fromY = 0; break;
}
/* now get the 'to' move */
// get x component
toX = m_lastAIMove[3] - '0';
// get y component
switch(m_lastAIMove[2]){
case 'a': toY = 1; break;
case 'b': toY = 2; break;
case 'c': toY = 3; break;
case 'd': toY = 4; break;
case 'e': toY = 5; break;
case 'f': toY = 6; break;
case 'g': toY = 7; break;
case 'h': toY = 8; break;
default: toY = 0; break;
}
/// tell chess program to move piece
/// ...
}
Basic stuff, now the function to translate the user’s move into a way the AI will understand.
void AI::moveAgainst(void)
{
/* Note: the global variables referenced here are assumed to hold the user's last move */
/* get the 'from' move */
// x component
switch(g_fromX - 1){
case 0: m_lastUserMove[0] = 'a'; break;
case 1: m_lastUserMove[0] = 'b'; break;
case 2: m_lastUserMove[0] = 'c'; break;
case 3: m_lastUserMove[0] = 'd'; break;
case 4: m_lastUserMove[0] = 'e'; break;
case 5: m_lastUserMove[0] = 'f'; break;
case 6: m_lastUserMove[0] = 'g'; break;
case 7: m_lastUserMove[0] = 'h'; break;
}
// y component
m_lastUserMove[1] = (char)(((int)'0') + g_fromY);
/* get the 'to' move */
// x component
switch(g_toX - 1){
case 0: m_lastUserMove[2] = 'a'; break;
case 1: m_lastUserMove[2] = 'b'; break;
case 2: m_lastUserMove[2] = 'c'; break;
case 3: m_lastUserMove[2] = 'd'; break;
case 4: m_lastUserMove[2] = 'e'; break;
case 5: m_lastUserMove[2] = 'f'; break;
case 6: m_lastUserMove[2] = 'g'; break;
case 7: m_lastUserMove[2] = 'h'; break;
}
// y component
m_lastUserMove[3] = (char)(((int)'0') + g_toY);
// set the sendMove boolean to true, telling the main loop to process the move
m_sendMove = true;
}
So to make a move against the AI engine, it would go something like this:
// retrieve user input and set global x and y values g_ai.moveAgainst();
And with a few of your own customizations that’s all there is to it. Besides the cleanup function:
void AI::cleanup(void)
{
char buf[1024];
// tell the AI engine to terminate
sprintf(buf, "stop ");
WriteFile(m_hWrite, buf, sizeof(buf), &m_bread, NULL);
WriteFile(m_hWrite, "\n", 1, &m_bread, NULL);
sprintf(buf, "quit ");
WriteFile(m_hWrite, buf, sizeof(buf), &m_bread, NULL);
WriteFile(m_hWrite, "\n", 1, &m_bread, NULL);
if(m_pi.hThread) CloseHandle(m_pi.hThread);
if(m_pi.hProcess) CloseHandle(m_pi.hProcess);
if(m_child_stdin) CloseHandle(m_child_stdin);
if(m_child_stdout) CloseHandle(m_child_stdout);
if(m_hRead) CloseHandle(m_hRead);
if(m_hWrite) CloseHandle(m_hWrite);
}
And also, here is the reset function:
void AI::reset(void)
{
char buf[1024];
sprintf(buf, "stop ");
WriteFile(m_hWrite, buf, sizeof(buf), &m_bread, NULL);
WriteFile(m_hWrite, "\n", 1, &m_bread, NULL);
sprintf(buf, "ucinewgame ");
WriteFile(m_hWrite, buf, sizeof(buf), &m_bread, NULL);
WriteFile(m_hWrite, "\n", 1, &m_bread, NULL);
strcpy(m_pos,"position startpos moves ");
}
Just don’t forget to call the init function before using the class. If you have any questions just let me know.

