Chess Engine to Unreal Engine Communication with Pipes | Devlog

by Arthur Ontuzhan


Posted 2 months, 3 weeks ago
Last edit 2 months, 3 weeks ago

Share on

So I’m trying to make a chess gacha game. And so far I have created a basic chess game in Unreal Engine, but you can play only against yourself. And as we know, it gets boring really fast to play a two-player game without a second player, so it would be nice to have an opponent that could play against you at any time of the day. But luckily for me, there are these computer programs called chess engines. And theoretically, I should be able to take a chess engine, connect it to my game, and then I should have an always-available opponent to play against.

So to do that, I will use interprocess communication, which means that I will simply make the game and chess engine talk with each other. But before doing anything, it’s probably a good idea to take a quick look at how chess engines work.

So let's have a look at one of the most popular chess engines, called Stockfish.

So Stockfish is a command-line program, and that means that you interact with it by using text commands.

As you can see, it doesn't understand regular human language commands. So we need to use commands from the UCI protocol to communicate with it. If we put the right commands into Stockfish, we will eventually get some output from the engine that will say that it's the best move.

Will it actually be the best move? It will depend on the commands we put in, but right now the goal is to make a chess engine to communicate with the game I'm making. So to do that, I need to figure out how to run a chess engine from Unreal Engine, how to write input into it, and how to read output from it. So to run a chess engine, we can use this Unreal Engine C++ function.

FGenericPlatformProcess::CreateProc(
    const TCHAR * URL,
    const TCHAR * Parms,
    bool bLaunchDetached,
    bool bLaunchHidden,
    bool bLaunchReallyHidden,
    uint32 * OutProcessID,
    int32 PriorityModifier,
    const TCHAR * OptionalWorkingDirectory,
    void * PipeWriteChild,
    void * PipeReadChild
)

This function has lots of parameters, but if we want to just run a chess engine with it, we just need to set the first parameter for it and leave the rest as default or empty values.

FString EnginePath = TEXT("C:\\stockfish\\fairy-stockfish_x86-64.exe"); //Please don't use hardcoded values like this
FPlatformProcess::CreateProc(*EnginePath, TEXT(""), false, false, false, nullptr, 0, nullptr, nullptr, nullptr);

Like in this example, we have set the first parameter value to the path of the chess engine's executable, and all the rest parameters are set to default or empty values.

So now, if we ran this part of the code, it would simply run the chess engine's executable. If we go back to the documentation and look again at the function's parameter list, we will see that the last two parameters are handles for pipes for output and input redirection.

So let me show how I struggled to understand how to use those parameters. At first, I declared two pipe handle variables and put them as the parameters for the function.

In header file:

void* PipeWriteChild;
void* PipeReadChild;

In CPP file:

FPlatformProcess::CreateProc(*EnginePath, TEXT(""), false, false, false, nullptr, 0, nullptr, PipeWriteChild, PipeReadChild);

And I also found out about these functions that are supposed to read and write data from pipes, so I implemented them in my test setup.

In header file:

UFUNCTION(BlueprintCallable)
FString ReadFromChessEngine();

UFUNCTION(BlueprintCallable)
void WriteToChessEngine(const FString& Message);

In CPP file:

FString UChessEngineExample::ReadFromChessEngine()
{
	return FPlatformProcess::ReadPipe(PipeReadChild);
}

void UChessEngineExample::WriteToChessEngine(const FString& Message)
{
	FPlatformProcess::WritePipe(PipeWriteChild, Message);
}

And when I tested it, it didn't work, the read and write functions did nothing. So then I found this create-pipe function and implemented it.

FPlatformProcess::CreatePipe(PipeReadChild, PipeWriteChild);
FPlatformProcess::CreateProc(*EnginePath, TEXT(""), false, false, false, nullptr, 0, nullptr, PipeWriteChild, PipeReadChild);

And now, whenever I would run the chess engine, it would instantly close. But the read function would read the first output of the chess engine or any input I tried to write to it.

So after multiple days of unsuccessful attempts, I started to believe that maybe it's not my stupidity that's holding me back, but maybe there's a bug in Unreal Engine. So I decided that I should try to make the same functionality using standard C++ outside the Unreal Engine. So I would know that I'm not the issue before I post a hate thread about how buggy Unreal Engine is in an attempt to boost my ego. But while trying to make the same thing in standard C++, I found this code example that made me realize what I was doing wrong and that I'm dumber than I thought.

So my main issue was that I was using only two pipe handle variables. So let's imagine a pipe, and now let's imagine that each of those variables represents an end of the pipe.

When I set those variables in the function, I thought I would set which side of the pipe would be connected to my game and which side would be connected to the chess engine. And then I could use this pipe to read output from the chess engine and also write input to it. But it turns out that pipes flow only in one direction. So I need two pipes to be able to fully communicate with the chess engine. And to correctly set up two pipes, I need 4 pipe handle variables: 2 for the chess engine side and 2 for the game side.

In header file:

void* PipeWriteChild;
void* PipeReadChild;
void* PipeWriteParent;
void* PipeReadParent;

And also, I need to set them up correctly by using the create pipe function.

In CPP file:

FPlatformProcess::CreatePipe(PipeReadParent, PipeWriteChild, false);
FPlatformProcess::CreatePipe(PipeReadChild, PipeWriteParent, true);
ProcHandle = FPlatformProcess::CreateProc(*EnginePath, TEXT(""), false, false, false, nullptr, 0, nullptr, PipeWriteChild, PipeReadChild);

If we look at the functions and imagine the pipes again, then we can see that the first parameter of the function defines the exit of the pipe, and the second parameter defines the entrance of the pipe.

But we also see that now there's a third parameter in the function, and it's set to false for one pipe and true for the other pipe. So it needs to be set to true for the pipe that will be sending input from the game to the chess engine. If it's not set to true, then the chess engine will close itself instantly after it's launched.

We also need to set the right pipe handles for the read and write functions.

In CPP file:

FString UChessEngineExample::ReadFromChessEngine()
{
	return FPlatformProcess::ReadPipe(PipeReadParent);
}

void UChessEngineExample::WriteToChessEngine(const FString& Message)
{
	FPlatformProcess::WritePipe(PipeWriteParent, Message);
}

For the read function, we use a handle from the read pipe that's on the game side. And for the write function, we use a handle from the write pipe that's also on the game side.

And now we will see that our read and write functions work as expected, but we will notice that the chess game process is still up after we close the game. To fix that, we need to declare a process handle variable, and then we can set it from the create process function.

In header file:

FProcHandle ProcHandle;

In CPP file:

ProcHandle = FPlatformProcess::CreateProc(*EnginePath, TEXT(""), false, false, false, nullptr, 0, nullptr, PipeWriteChild, PipeReadChild);

And then we can use the process handle variable to close the chess engine process. Like in this example, I have overridden the begin destroy function, so every time this actor component is destroyed, it will also close the chess engine process.

In CPP file:

void UChessEngineExample::BeginDestroy()
{
	Super::BeginDestroy();

	FPlatformProcess::ClosePipe(PipeReadParent, PipeWriteChild);
	FPlatformProcess::ClosePipe(PipeReadChild, PipeWriteParent);
	FPlatformProcess::CloseProc(ProcHandle);
}

If you were carefully looking at my code, you probably noticed that I'm not using a regular stockfish as my chess engine. At first, I tried using a regular stockfish version, but I quickly discovered that even the easiest possible difficulty setting is too hard for me. So I found another chess engine called Fairy Stockfish, which is derived from stockfish and can be set to an even easier difficulty setting. So I guess after figuring all these things out, I can finally start to work on the gacha part of the game.

Full code:

Header file:

#pragma once

#include "CoreMinimal.h"
#include "Components/ActorComponent.h"
#include "ChessEngineExample.generated.h"


UCLASS( ClassGroup=(Custom), meta=(BlueprintSpawnableComponent) )
class CHESSGAME_API UChessEngineExample : public UActorComponent
{
	GENERATED_BODY()

public:	
	// Sets default values for this component's properties
	UChessEngineExample();

	FString EnginePath = TEXT("C:\\stockfish\\fairy-stockfish_x86-64.exe");

	UFUNCTION(BlueprintCallable)
	void RunChessEngine();

	void* PipeWriteChild;
	void* PipeReadChild;
	void* PipeWriteParent;
	void* PipeReadParent;

	FProcHandle ProcHandle;

	UFUNCTION(BlueprintCallable)
	FString ReadFromChessEngine();

	UFUNCTION(BlueprintCallable)
	void WriteToChessEngine(const FString& Message);

	virtual void BeginDestroy();

protected:
	// Called when the game starts
	virtual void BeginPlay() override;

public:	
	// Called every frame
	virtual void TickComponent(float DeltaTime, ELevelTick TickType, FActorComponentTickFunction* ThisTickFunction) override;

		
};

CPP file:

#include "ChessEngineExample.h"

// Sets default values for this component's properties
UChessEngineExample::UChessEngineExample()
{
	// Set this component to be initialized when the game starts, and to be ticked every frame.  You can turn these features
	// off to improve performance if you don't need them.
	PrimaryComponentTick.bCanEverTick = true;

	// ...
}

void UChessEngineExample::RunChessEngine()
{
	FPlatformProcess::CreatePipe(PipeReadParent, PipeWriteChild, false);
	FPlatformProcess::CreatePipe(PipeReadChild, PipeWriteParent, true);
	ProcHandle = FPlatformProcess::CreateProc(*EnginePath, TEXT(""), false, false, false, nullptr, 0, nullptr, PipeWriteChild, PipeReadChild);
}

FString UChessEngineExample::ReadFromChessEngine()
{
	return FPlatformProcess::ReadPipe(PipeReadParent);
}

void UChessEngineExample::WriteToChessEngine(const FString& Message)
{
	FPlatformProcess::WritePipe(PipeWriteParent, Message);
}

void UChessEngineExample::BeginDestroy()
{
	Super::BeginDestroy();

	FPlatformProcess::ClosePipe(PipeReadParent, PipeWriteChild);
	FPlatformProcess::ClosePipe(PipeReadChild, PipeWriteParent);
	FPlatformProcess::CloseProc(ProcHandle);
}


// Called when the game starts
void UChessEngineExample::BeginPlay()
{
	Super::BeginPlay();

	// ...
	
}


// Called every frame
void UChessEngineExample::TickComponent(float DeltaTime, ELevelTick TickType, FActorComponentTickFunction* ThisTickFunction)
{
	Super::TickComponent(DeltaTime, TickType, ThisTickFunction);

	// ...
}

Share on