Building a Dart Package to Interface with Chess Engines
Introduction
In the vast world of chess software, powerful chess engines like Stockfish rely on the Universal Chess Interface (UCI) to communicate with graphical interfaces and analysis tools. You might wonder, why build a UCI chess engine wrapper in Dart? Simply put, I needed one. As someone who loves tinkering and coding on weekends, I recently started working on a chess analysis client as a side project.
Initially, I planned to find an existing package to connect my app with chess engines like Stockfish, but after looking around, I couldn’t find one that fully met my requirements or offered a robust solution. That’s why I decided to create my own Dart package, not just for myself, but for anyone else looking for an effective and easy-to-use solution.
This article kicks off a series that I will be doing that details the journey of creating a Dart package for interfacing with chess engines using UCI protocols. By the end of this first phase, the package can start and stop an engine and communicate using basic commands.
What is UCI?
The Universal Chess Interface (UCI) is a communication protocol that allows a chess GUI (Graphical User Interface) to interact seamlessly with a chess engine. It works by exchanging simple text commands. For instance:
uci: Initializes the engine and retrieves information about the engine, such as its name and available options.
quit: Shuts down the engine.
Understanding these fundamentals allows us to develop a robust integration efficiently.
Phase One Goals:
The initial goals for this phase of the project:
Launch a chess engine from Dart.
Communicate asynchronously with the engine.
Implement basic UCI commands (uci and quit).
The package is structured around a single class, UCIChessEngine, managing process interactions and streamlining communication. Initializing the class is as straightforward as creating the object and providing the path to your chosen chess engine executable.
var engine = UCIChessEngine(enginePath);
Spawning the Engine Process
To interact with a chess engine, we first need to spawn a process running the engine binary:
_process = await Process.start(_enginePath, []);
Here, Dart’s Process class allows us to run external programs. _enginePath points to our chess engine executable (like Stockfish), while we pass in an empty array as the second argument because we don’t have to worry about command parameters.
Handling Asynchronous Output
Chess engines constantly send output to stdout and potential error messages to stderr. To handle these asynchronously I am using Dart Streams:
_outputController = StreamController<String>();
_errorController = StreamController<String>();
_process!.stdout
.transform(utf8.decoder)
.transform(const LineSplitter())
.listen((line) => _outputController!.add(line));
_process!.stderr
.transform(utf8.decoder)
.transform(const LineSplitter())
.listen((line) => _errorController!.add(line));
Transforming the raw bytes into UTF-8 strings and splitting them line-by-line provides us with easy-to-process engine outputs.
Initializing UCI Protocol
Once our engine is running, we send the uci command to initialize:
await _sendCommand(’uci’);
The engine responds with critical information like its name, author, and supported options.
The output of the engine after issuing the uci command will look similar to this (my local Stockfish output)
id name Stockfish 17.1
id author the Stockfish developers (see AUTHORS file)
option name Debug Log File type string default <empty>
option name NumaPolicy type string default auto
option name Threads type spin default 1 min 1 max 1024
option name Hash type spin default 16 min 1 max 33554432
option name Clear Hash type button
option name Ponder type check default false
option name MultiPV type spin default 1 min 1 max 256
option name Skill Level type spin default 20 min 0 max 20
option name Move Overhead type spin default 10 min 0 max 5000
option name nodestime type spin default 0 min 0 max 10000
option name UCI_Chess960 type check default false
option name UCI_LimitStrength type check default false
option name UCI_Elo type spin default 1320 min 1320 max 3190
option name UCI_ShowWDL type check default false
option name SyzygyPath type string default <empty>
option name SyzygyProbeDepth type spin default 1 min 1 max 100
option name Syzygy50MoveRule type check default true
option name SyzygyProbeLimit type spin default 7 min 0 max 7
option name EvalFile type string default nn-1c0000000000.nnue
option name EvalFileSmall type string default nn-37f18f62d772.nnue
uciok
We parse these lines until we receive a termination command (uciok).
await for (String line in _outputController!.stream) {
if (line.startsWith(’id name ‘)) {
engineName = line.substring(8);
} else if (line.startsWith(’id author ‘)) {
engineAuthor = line.substring(10);
} else if (line == ‘uciok’) {
break;
}
}
Capturing Engine Metadata
I encapsulate engine metadata into a custom EngineInfo
class:
_engineInfo = EngineInfo(
name: engineName,
author: engineAuthor ?? ‘Unknown’,
options: options,
);
While I don’t currently have a specific use for the detailed engine metadata, I included it for completeness and to benefit anyone who might utilize the package. The metadata is still quite raw, so in future updates, I plan to format it more effectively for practical use.
Stopping the Engine
Properly shutting down our engine is crucial for closing out resources and avoiding memory leaks:
await _sendCommand(’quit’);
_process!.kill();
await _process!.exitCode;
Lessons Learned & What’s Next
This first phase provided a valuable opportunity for me to dive deep into asynchronous programming in Dart, a language I’m actively learning for my ongoing projects. I became familiar with Dart’s approach to asynchronous streams, private variables indicated by underscores, null safety features, classes, interfaces, and control flow structures.
These foundational lessons will help me shape future updates, including more robust command parsing, move handling, analysis capabilities, and comprehensive documentation.
Full code and project can be found on my GitHub.
Stay tuned for more articles in this series as we add more features, refine our interactions, and make chess engines accessible for Dart and Flutter developers everywhere.