Shoving a chess engine up your butt

29. 12. 2024 | Jakub Kadlčík | EN chess morse gleam elixir meme

In the Lex Fridman Podcast episode #327, Levy Rozman (aka GothamChess) explained a scandal, alleging a chess grandmaster Hans Niemann of cheating at a tournament. Supposedly, it was done through a remotely controlled device inserted into his backside, allowing him to receive the next best move encoded in Morse code vibrations. And because I am evidently twelve years old, I thought it would be really funny to implement this. By the way, Ron Sijm is likely as immature as me, because he beat me to it.

Disclaimer: I am not endorsing cheating in chess or any other sport, I don’t know if the allegations were true or not, and to be honest, I don’t care. I don’t know any of the people involved, and I don’t even play chess. This project is just for shits and giggles, not so that y’all can experience more pleasure during chess tournaments.

Two-minute demo

This post is too long and you will probably not read it. So here is a fun demo of the final product.

Scope of the project

At our disposal, we have several chess engines capable of beating any human. We also have a large offering of intimate hardware built on top of open standards. The most straightforward way to encode chess moves is the Morse code, even though it is not the most effective choice in this situation. Squares on chess boards go from a1 to h8, which leaves us with two-thirds of the alphabet unused, making the code for every character n-times (I am bad at math) longer than necessary. However, creating an optimized encoding is outside the scope of this project.

Playing one side of the chess board will be a two-man job. The agent in the field, receiving intelligence through his anal cavity, and the operator behind the computer monitor, querying a chess engine through the interface we will create.

Joking aside, I could probably crank out a working solution in Python in under an hour just by gluing inquirer, stockfish, morse-talk, and buttplug-py together. But where would be the fun in that? That’s why I am going to learn a new programming language on this project.

Chess engines

There are several chess engines to choose from, such as Stockfish, AlphaZero, Lc0, Shredder, etc. Some of them giving us a choice between running them locally or spawning an instance through lichess.org. I randomly picked Stockfish out of the hat because it is an open-source project, and it is easy to run locally.

Chess grandmasters average around 2600 Elo rating, and according to Gary Linscott, Stockfish performs at 3600 Elo. Don’t worry that it won’t beat our opponents.

To keep things simple, we can say that chess engines are services running in the background (and liken them to Unix daemons). They don’t have any graphical interfaces or CLI tools for interacting with them. At least not tools in the conventional sense. Instead, third-party programs can communicate with chess engines through the Universal Chess Interface (UCI).

$ stockfish
...

uci
id name Stockfish 17
id author the Stockfish developers (see AUTHORS file)
...
uciok

isready
readyok

ucinewgame

position startpos moves a2a3 h7h6 b2b3

go depth 15
...
bestmove d7d5 ponder c1b2

As you can see, Stockfish provides a painfully bad REPL and UCI arms as with a set of usable commands. There is no TAB completion, no help, no command history, no syntax highlighting, no prompt to distinguish between commands and their output, and so on. For better readability, I rendered the inputs red, but in the terminal, everything is the same color.

In this session, we started the game, white moved from a2 to a3, black moved from h7 to h6, then again white moved from b2 to b3. Stockfish then recommends black to move from d7 to d5 and expect white to retaliate with c1 to b2.

Gleam, Elixir, and the BEAM

One of my goals for this project was to learn Gleam, which is a new addition to the BEAM family of languages, preceded by Erlang and Elixir. The main purpose of these languages is to be used for building massively distributed, high-availability systems. So I will be the first one to admit that this project doesn’t exactly fit the use-case. Who knows, maybe I plan to build some distributed system in the future and treat this project as a means to learn the languages.

The BEAM is a virtual machine for running Erlang bytecode in the same sense that JVM is a virtual machine for Java, and .NET is a virtual machine platform for C#. And just as Java, Clojure and Scala compile to the JVM bytecode, C#, Visual Basic, and F# compile to the .NET bytecode, Erlang, Elixir, and Gleam compile to the BEAM bytecode. Allowing us to share code across the related languages.

The main part of this project will be written in Gleam, but we will use Elixir interop for some peripherals. Starting with one of them right now. To control Stockfish we can use Elixir ports, which is the best abstraction for interacting with stdin-based programs that I’ve ever seen.

defmodule Stockfish do
  def new_game do
    port = Port.open({:spawn, "stockfish"}, [:binary])
    Port.command(port, "uci\n")
    Port.command(port, "isready\n")
    Port.command(port, "ucinewgame\n")
    port
  end
end

At the, we are returning the port, so we can pass it to other functions. To implement a move function for example.

defmodule Stockfish do
  ...

  def move(port, position, history) do
    moves =
      history
      |> Enum.concat([position])
      |> Enum.join(" ")
    Port.command(port, "position startpos moves #{moves}\n")
  end
end

You wouldn’t believe how easy it is to use this code from Gleam through externals. Since Gleam is statically typed, we need to tell the compiler the shape of our Elixir functions, but that’s all.

import gleam/erlang/port.{type Port}

@external(erlang, "Elixir.Stockfish", "new_game")
fn new_game() -> Port

@external(erlang, "Elixir.Stockfish", "move")
fn move(game: Port, position: String, history: List(String)) -> Nil

For illustrative purposes, we are going to avoid the domain-specific type masturbation and stick with lists and strings. In the actual implementation, we’ll likely want to avoid exposing the Port directly, and create custom types for Position and History.

Now we can use the functions in Gleam.

new_game() |> move("a2a3", [])

Morse code

Several packages for encoding and decoding Morse code were available, but I decided to write my own called Morsey. I don’t expect any rapid development in the Morse code department, causing this reinvention of the wheel to be the final blow to my personal life, and putting me on a hamster wheel of perpetual maintenance of my Open Source projects.

The source code is available in FrostyX/morsey repository, and look, we already got a star from the one and only Hayleigh Thompson. The package can be used like this:

import gleam/io
import morsey

let text = "Hello world!"
case morsey.encode(text) {
  Ok(symbols) ->
    io.println("Morse code for " <> text <> " is " <> morsey.to_string(symbols))
  Error(morsey.InvalidCharacter(char)) ->
    io.println_error("Invalid character: " <> char)
}

// And the output will be:
// .... . .-.. .-.. --- / .-- --- .-. .-.. -.. -.-.--

We can see several Gleam features in this example. It doesn’t have exceptions and instead returns errors as values. It leans heavily towards pattern matching, there is not even an if-else statement in the language. The pattern matching is exhaustive, otherwise, I would definitely forget to handle the error case. I love all of these features. The one I quite dislike, however, is that for the sake of simplicity, Gleam doesn’t support string formatting.

Intimate hardware

On paper, hardware should’ve been the easy part. There is an exhaustive list of manufacturers and devices supported by the buttplug.io project. I randomly picked some device that was possible to buy in Czech Republic. It paired with my phone within seconds, so I started celebrating an easy victory, just to waste the rest of the day trying to pair the device with my computer.

Here is the ten-thousand-foot view. A toy communicates with your computer via Bluetooth LE. Some manufacturers require pairing before use, some don’t. You don’t use the system Bluetooth manager to connect to the device. Clients like buttplug-py, buttplug-js, etc, don’t control the toy directly, they only communicate with a server like intiface-engine via websockets. The server then does the heavy lifting. You can run the server like this:

$ cargo install intiface-engine
$ ~/.cargo/bin/intiface-engine --websocket-port 12345 --use-bluetooth-le

Websockets

The previous chapter linked officially supported libraries that provide a high-level interface to control the devices. However, there weren’t any for the BEAM family of languages, so I had to raw-dog the websocket communication. It’s almost embarrassing that in fifteen years of programming, I have never used websockets, and this was my first exposure.

An example session can look like this (The snippet only shows messages from the client to the server. The responses were omitted).

$ cargo install websocat
$ ~/.cargo/bin/websocat ws://127.0.0.1:12345/
[{"RequestServerInfo": {"ClientName": "Test Client", "MessageVersion": 1, "Id": 1}}]
[{"RequestDeviceList": {"Id": 2}}]
[{"StartScanning": {"Id": 3}}]
[{"VibrateCmd": {"DeviceIndex": 0, "Speeds": [{"Index": 0, "Speed": 500}], "Id": 4}}]
[{"StopDeviceCmd": {"DeviceIndex": 0, "Id": 5}}]

I created a Gleam package with a high-level interface around this, called Bummer. Its usage is fairly simple.

import bummer

case bummer.connect("ws://127.0.0.1:12345/") {
  Ok(socket) -> {
    io.println("Connected to intiface-engine websocket")
    io.println("Initiating a test sequence")

    bummer.scan(socket, 5000)
    bummer.vibrate(socket, 500)
    bummer.rotate(socket, 500)

    io.println("Test sequence finished")
  }
  Error(_) ->
    "Cannot connect to intiface-engine websocket. Is it running?"
    |> io.println_error

The public API is leaking some internals. The users probably don’t even want to know what communication protocol is being used, and it should be treated as an implementation detail. This calls for more custom types.

Also, websockets are a bi-directional protocol but bummer is currently unidirectional. It was really easy to send messages to the server but due to the Gleam + Elixir interop, I couldn’t figure out how to properly handle messages from the server. I started running out of time, so this remains unfinished. PRs are more then welcome.

Tying it all together

Throughout this blog post, we’ve seen code snippets for communicating with Stockfish, converting text to Morse code, and controlling intimate toys via websockets. The last missing piece is vibrating a Morse code sequence.

fn vibrate(socket: bummer.Connection, sequence: morsey.Sequence) -> Nil {
  // International Morse Code
  // 1. The length of a dot is one unit.
  // 2. A dash is three units.
  // 3. The space between parts of the same letter is one unit.
  // 4. The space between letters is three units.
  // 5. The space between words is seven units.
  let interval = 200
  case sequence {
    [] -> Nil
    [first, ..rest] -> {
      case first {
        morsey.Dot -> bummer.vibrate(socket, interval)
        morsey.Comma -> bummer.vibrate(socket, interval * 3)
        morsey.Space -> sleep(interval * 3)
        morsey.Break -> sleep(interval * 7)
        morsey.Invalid(_) -> Nil
      }
      vibrate(socket, rest)
    }
  }
}

All code is available in my FrostyX/crooked-rook repository.

A room for improvement

This being a fun little project to keep myself busy over the holidays, having no real use-case, and providing zero value to anyone, it had to be cut short. Codes for letters in the Morse alphabet are too long. One could come up with much more effective encoding. Additionally, there is no need to encode the whole move (e.g. a2a3). An experienced chess player can infer the starting position (a2) from the end position (a3). These two combined improvements would result in significantly shorter messages.

The other limitation is having a human relay between the player and the chess engine. I am sure there are AI models capable of observing a chessboard and recognizing all the figures and their positions. The whole system could be autonomous with the exception of the human player.

Sounds like a pt.2 next holiday?