Programming a chess bot for Chess.com
Introduction
I recently played a 1 minute chess game against a Grandmaster, it was the strongest chess bot at Chess.com (ELO: 2730). To be honest, it was not
a nice game, a true massacre with an inevitable quick checkmate after 15 seconds. This made me realise one thing: I have exactly 0% chance winning, ever.
Anyway, this post will not be about me playing chess but it is about how I developed a chess bot which did what I can never do.
The challenge
This might come as a huge disappointment, but I’m not going to write a chess engine. A chess engine is responsible to compute the actual moves and for that I’ll be using Stockfish, an open source chess engine. My task will be to write software that can move pieces around on the chessboard and pass moves back and forth between my chess engine and chess.com. But that’s not all, I also have a very old laptop, a MacBook Pro early 2013 (2.4GHz Quad-Core intel i7, 8 GB memory), so performance will be my biggest concern in order to be able to beat the strongest chess.com chess bot in 1 minute matches.
Chess bot Architecture
As it turns out, Chess.com has no API to read from or write to. But that’s fine, the HTML in the browser contains all the information I need.
I started really simple, like manually pasting code into the console of my browser, but eventually that process evolved into the following architecture
Puppeteer is responsible for spinning up the browser and navigate to chess.com, login and finally upload and execute the browser-bundle inside the browser. From there on I have to manually pick a chess.com bot and hit the play button. As soon as the game starts my chess bot automatically activates and starts playing. At the start of each turn all required game information is sent using WebSockets to my NestJS backend, which uses an opening book (super fast) or the Stockfish chess engine. For convenience I run Stockfish inside a Docker container. I also created a docker image with Leela Chess Zero (Lc0), which is an other even stronger open source chess engine, but that one requires a GPU which I don’t have (old laptop). Without a GPU it plays very slow and too weak for the chess.com bot. At the end of the game some game statistics are written to file which can be used for analysis to improve my chess bot. I also have a WebAssembly version of Stockfish which I can inject into the browser as well. It can move extremely fast because no round trip to the backend is needed.
The code that is injected in the browser has two important tasks, move chess pieces and monitor the game to determine when it is time to move.
Turn based game
Chess.com is not going to notify me when it’s my turn, so I have to monitor the HTML for changes that reflect this kind of information. But if you’ve ever played at chess.com you might have noticed that your clock (bottom right corner) is highlighted when you have to move. It is this change in HTML that is ideal for this particular situation. Luckily MutationObserver
will do all the work here
and the callback starts the process of moving
When I run experiments like this, it always means that at some point new insights are gained and parts of the application have to be rewritten, removed or added. In my experience Dependency Injection (DI) and the Publish–subscribe pattern (EventHub) are ideal candidates. DI (not shown in my code examples) gives me control over which classes get injected where and how they are instantiated. EventHub is used to trigger events for different phases during the match, like bot.move.start
or game.over
. This is what I use to connect all the different parts of my application. For example, to add code that measures the time my engine uses to think, I simply do
let start: number;
eventhub.on('bot.move.start', () => start = Date.now());
eventhub.on('bot.move.end', () => console.log(Date.now() - start));
Move chess pieces
The final piece of the puzzle is to move a chess piece. This basically means that you have to fake the mouse and trigger programmatically a mousedown
and mouseup
on the correct HTML elements. The structure of the HTML looks as follows
To understand how mouse interaction is taken care of here, you can use getEventListeners
on these HTML elements. For example, I noticed that the chess pieces didn’t have any listeners, but the chessboard (#game-board
) did. This means, for example, that a move from e2
to e3
requires the coordinates of these squares. Anyway, with all this knowledge it wasn’t very hard to figure out how to make that move
That’s all, let’s see this in action
Analysis
Watching the game above might reveal how well my bot is playing now, but that is not how it started. Not that it was bad in the beginning, but it was losing more than it was winning (and also a lot of draws). Below is a graph which represents a lost game.
The grey line (right axis) represents how well my bot is doing (negative is not good). For example, roughly speaking, if you are a pawn behind, the score goes down by 100 points. A horse or bishop, 300 points. Of course there is more to it, but this will give you some understanding of that curve.
The red and green graphs represent the remaining time during the game. It is clear that my bot (green line) is in serious (time) trouble at the end of the game (move 50). But be that as it may, it is also clear that before this it is not much better (score is around 0). To fix that I added an opening book, enabled pondering and also noticed that chess.com plays weaker when my bot thinks less. So finally, after a lot of tweaking, performance improvements and more tweaking, my bot played as follows
Pondering is simply using the opponent’s move time to consider likely opponent moves
A Ponder hit
means that the opponent did the expected move which my engine was pondering about. In those cases my bot has a slight advantage. In this game my bot starts a bit weaker (negative score) but around move 30 chess.com makes mistakes and the game is already won around move 40. With this strategy my bot never lost a single match!!
The reason behind chess.com playing weaker when my bot moves faster is unknown to me, but my best guess would be that they heavily rely on pondering, which might work against humans but not against my bot, because it can move extremely fast.
Run it yourself
If you like, you can run my chess bot too. First clone the repository and build it
$> git clone https://github.com/scaljeri/chess.com
$> cd chess.com
$> yarn
$> lerna run build
Open the file ./packages/bot/.env
and add your chess.com credentials. That’s it, now you can start playing
$> lerna run start:bot
It will start a browser and it will automatically navigate to the right place for you, wait until you see the ‘play’ button
Now hit play and sit back and relax, my bot will do the rest. By default the WebAssembly chess engine is used, so no docker needed. But, as I already mentioned, I’m using a Macbook Pro so I have no clue what it will do in other environments. If you have issues on Window maybe you can try out Windows Subsystem for Linux (WSL). Feel free to create a Pull Request if you have fixes or improvements!!
Final words
This was a really fun project to work on and this post just shows you the tip of the iceberg (checkout my repository if you would like to know more). I have shown that using just HTML as my inputs and outputs is very effective and super easy. I also realise that there are still many things I can improve to make my chess bot even stronger, but my goal was achieved and my painful loss in 15 seconds completely forgotten.
But, please don’t use this software to cheat against other humans. There is no fun in doing that (you will win), plus Chess.com will find out pretty quickly and block your account forever!
I hope you enjoyed reading this as much as I did doing the experiment, Cheers!