Chess in Godot

With Chess games there are 2 aspects: the Chess Engine and the Chess Board implementation.

It’s a tough undertaking to code a Chess algorithm, basically AI. These need to examine many move scenarios quickly which normally means that the code should be compiled and not developed in a relatively slow scripting language such as GDScript. In fact, there are examples of Super Computers such as Deep Blue being used for this purpose to challenge Chess grand masters.

Luckily for us, we can leverage open source Chess Engines that we can connect to from a Godot Chess Board program. This is my approach here.

These engines are run as CLI (Command Line Interface) programs that we may communicate with by piping instructions to them and receiving responses back, using for example UCI (Universal Chess Interface) protocol.

So our first task is to start up the Chess Engine and to establish communications with it.

Download a Chess Engine

One of the best seems to be Stock Fish, so let’s download that in the assumption that it will out-perform any attempts that we make at coding our own.

Chess Engine Interface

Stock Fish uses the UCI (Universal Chess Interface) to handle commands issued from the command line, and its responses are sent to the standard output. But the Chess Engine process continues to run awaiting further commands and printing responses.

Godot has the OS.execute method that may run a program and get the response after the program terminates, but that is not what we want here.

What we need is an API to connect to. This could entail writing a plugin that includes the code for the Chess Engine.

Another approach is to use UDP (User Datagram Protocol) which is supported by Godot’s PacketPeerUDP class. This is used to connect to a UDP server. Servers output essentially text data if the data is unencrypted, so we could pass IO data to and from the Chess Engine with a simple server implementation.

Many programming languages (Python, Go, C#) include server functionality. I wrote the server in Go.


package main

// This program is spawned as a sub process from the Godot UDP interface script
// It serves as a pipe between Godot and a running CLI process
// The CLI process is spawned from the path that is passed as a command line arg
// But we should spawn it after receiving the first UDP packet so that we know the address
// of the client to send any initial stdout text to.

import (
	"bufio"
	"fmt"
	"io"
	"net"
	"os"
	"os/exec"
)

func main() {
	var clientAddr net.Addr

	// Expect an executable path as 2nd arg
	args := os.Args
	if len(args) < 2 {
		os.Exit(1)
	}

	// Set up UDP listner
	pc, err := net.ListenPacket("udp", ":7070")
	if err != nil {
		os.Exit(2)
	}
	defer pc.Close()

	// Set up external process
	proc := exec.Command(args[1])

	// The process input is obtained in the form of an io.WriteCloser. The underlying implementation uses the os.Pipe
	stdin, _ := proc.StdinPipe()
	defer stdin.Close()

	// Watch the output of the executed process
	stdout, _ := proc.StdoutPipe()
	defer stdout.Close()

	// Run the stdout scanner in a thread
	// It will write the stdout text via a pipe to our UDP client
	go func() {
		s := bufio.NewScanner(stdout)
		for s.Scan() {
			txt := s.Text()
			if _, err := pc.WriteTo([]byte(txt), clientAddr); err != nil {
				os.Exit(3)
			}
		}
	}()

	// Pipe text packets received from our UDP client to stdin
	buffer := make([]byte, 1024)
	for {
		n, addr, err := pc.ReadFrom(buffer)
		if err == nil {
			if clientAddr == nil {
				// Start the subprocess
				proc.Start()
			}
			clientAddr = addr
			// Only write the first line of the buffer (not the whole buffer)
			io.WriteString(stdin, fmt.Sprintf("%s\n", buffer[:n]))

		} else {
			os.Exit(4)
		}
	}
}

This server called iopiper is started as a command line program from Godot with the OS.execute method. Then it listens on port 7070 for UDP packets and responds back to the UDP client address that it received.

The code to start the server and communicate with it was put into a class called Engine coded in GDScript. Here is the code: Engine.gd

extends Node

class_name Engine

# Provide functionality for interactions with a Chess Engine
# Also, find the installed exe files rather than needing UI entry by user

var iopiper # Path of UDP to CLI app bridge in the bin directory
var engine # Path of installed Chess Engine in the engine directory
var server_pid = 0

signal done

func _ready():
	# Get the base path of the application files
	var cmd = "pwd"
	var ext = ""
	if OS.get_name() == "Windows":
		cmd = "cd"
		ext = ".exe"
	var output = []
	var _exit_code = OS.execute(cmd, [], true, output)
	var path = output[0].strip_edges() # Remove cstring null termination char
	# Allow for running in dev mode, so back peddle from src folder
	var src_pos = path.find("src")
	if src_pos > -1:
		path = path.substr(0, src_pos - 1)
	
	# Form paths to the executables
	# Use forward slash for all platforms
	iopiper = path + "/bin/iopiper" + ext
	engine = path + "/engine/"

	# Get the first file found in the engine folder for the Chess Engine to use
	var dir = Directory.new()
	if dir.open(engine) == OK:
		dir.list_dir_begin(true)
		engine += dir.get_next()


func start_udp_server():
	var err = ""
	# Check for existence of the exe files
	var file = File.new()
	if !file.file_exists(iopiper):
		err = "Missing iopiper at: " + iopiper
	elif !file.file_exists(engine):
		err = "Missing engine at: " + engine
	else:
		server_pid = OS.execute(iopiper, [engine], false)
		if server_pid < 400: # PIDs are likely above this value and error codes below it
			err = "Unable to start UDP server with error code: " + server_pid
			server_pid = 0
		else:
			$UDPClient.set_server()
	return { "started": err == "", "error": err }


func stop_udp_server():
	# Return 0 or an error code
	var ret_code = 0
	if server_pid > 0:
		ret_code = OS.kill(server_pid)
		server_pid = 0
	return ret_code 


func send_packet(pkt: String):
	print("Sent packet: ", pkt)
	$UDPClient.send_packet(pkt)
	$Timer.start()


func _on_Timer_timeout():
	stop_udp_server()
	emit_signal("done", false, "")


func _on_UDPClient_got_packet(pkt):
	$Timer.stop()
	emit_signal("done", true, pkt)


func _on_Engine_tree_exited():
	stop_udp_server()

Using this script we may:

  • Start the UDP server
  • Stop the UDP server (kill the process)
  • Connect to a done signal to receive response packets of data

See the UCI command specification to study the available commands and types of responses.

With this in place we may create a Main scene and add the Engine scene node to it.

Main Scene

In the Main scene we may start by creating a state machine to control the running of the game.

# states
enum { IDLE, CONNECTING, STARTING, PLAYER_TURN, ENGINE_TURN, PLAYER_WIN, ENGINE_WIN }
# events
enum { CONNECT, NEW_GAME, DONE, ERROR, MOVE }

The state machine function with have inputs for the event and messages from the chess engine.

func handle_state(event, msg = ""):
	match state:
		IDLE:
			match event:
                NEW_GAME:

Main.gd

Also, we need a UI with buttons, labels, and a chess board.

We will create a Board scene for the Chess Board and its functions.

Board.gd

More to follow …

I have previously created a Chess Game in Godot and published the code here: Godot Chess

Comments Forum

More Godot projects