Chess.com Keyboard browser extension
I’m not a very strong chess player, but I enjoy an occasional game. I used to play on chess.com, and one day I decided to implement keyboard controls for it. Such a feature would allow you to learn chess notation and improve your board vision.
The result of this story is ChessHelper extension. It’s an open-source and free to use browser extension.
In this article I want to share the problems and difficulties I had while developing it.
I tracked the time I needed to build the application; by the time of this post writing I’ve spent around 60 hours.
First attempt
In November 2017 I had the idea of an extension, enabling players to make moves on chess.com using keyboard instead of dragging the pieces with mouse.
I really liked the idea, but had no idea how chess.com is implemented. I started examining source files of the project. They were minified, but Google Chrome allowed me to prettify most of it.
I managed to find a few useful abstractions I could work with.
The most useful, probably, was ChessBoard class, instance of which was responsible for state and behaviour handling for a particular chess board.
I used a “Play against computer” page to collect this knowledge, because it allowed me to make moves without worrying that I’ll lose a game or annoy my opponent by leaving the game or doing anything strange (I wasn’t sure how chess.com API will treat my research actions either).
The biggest problem was that I still didn’t understand how to access the instance of the ChessBoard class handling this particular game I’m playing. After some global scope poking I found out that I’m able to get the instance from “myEvent.capturingBoard” global variable. That was extremely lucky for me, because now I just had to figure out the method I need to use in order to trigger game events.
After some playing around with the instance I found out the ways to move pieces with it (by emitting an “onDropPiece” event) or to check whether the move is illegal or not (by calling “board.gameRules.isLegalMove” method).
See my first commit to check the basic implementation.
Features development
After I had a successful demo, I decided to go further and create a browser extension based on the findings I had.
I was writing the code using ES2015, but I decided not to go for any transpilation, because my extension was initially oriented on Google Chrome only and it natively supports ES2015 pretty well.
I had little experience writing browser extensions, so I didn’t know how to insert a JavaScript file of an extension so that it has access to “window” variable, because content script only has access to “document”. I found a correct solution to it later (inject a script with a URL based on extension id), but at first I just decided to inline my scripts and inject them using “document” methods to append a new “script” HTML element. I remember that I had a hard time escaping template literals in my injected scripts because of that.
Anyway, the extension worked and I started to implement a basic interface to collect a move from the player. I added an input to the underneath the board; at first it only supported move in format “e2e4”, but a bit later I also allowed to use a space or a dash as a separator.
I worked on highlighting the squares relevant to the move, using “board.markArea” method at first, but then using built-in ChessBoard interface to draw arrows on board.
I also discovered that on “Live” chess games page I had to access the board instance differently, but, luckily it was also exposed to the global scope.
I fixed the issues in “Live” mode and shared the extension in a relevant chess.com forum topic.
The story continues
I took a long break from the extension development after that. The biggest relocation of my life, from Russia to the Netherlands happened in spring 2018. So the next chunk of work I’ve done on the extension took part in July 2018.
I knew that there were some issues with my extension, because even despite the low amount of people installing it (around 10 people were using it weekly) I had some users uninstalling it. It was also difficult to maintain it for me, because all the code was placed in one file and I had problems inlining the code.
After some research I found out that chess.com developers prepare a new layout version, because when I tried to play while being logged out, the app looked differently. The fix required a lot of work, so I decided to measure the impact at first by setting a Google Analytics event for the layout issues reproduction.
I also decided to invest some time in code structure. So I added CommonJS building using Browserify, injected JavaScript using my extension URL, split the code into modules and added unit tests, because I knew I would need them to implement the feature I planned to implement from the very beginning: algebraic notation.
Algebraic notation
Algebraic notation is a short way of writing the chess moves. Instead of specifying the source and target squares coordinates (“g2-f3”) you specify the type of piece and its target coordinate (“Nf3” for knight moving to f3). This notation can be ambiguous sometimes. If there is more than one knight available for the move, you have to specify which knight you mean exactly (e.g. “Ngf3” for “knight standing on ‘g’ vertical to ‘f3’”).
I didn’t know how easy it would be to implement another notation support. I mean, how should I validate the moves in cases of ambiguity? Should I load my own chess engine and then input the pieces positions in it?
The answer came unexpectedly after some more digging of the global scope and chess.com source code. I understood that I can validate the moves by calling a built-in function “board.gameRules.isLegalMove(board.gameSetup, fromSq, toSq)”. I also could get the position of all the pieces on board, annotated with their types, reading “board.gameSetup.pieces” property.
A type of piece and the target square comes to me from the input (e.g. Kd2); all I need is to find all the pieces matching by types and then count the ones which can make a valid move to the target square. Based on that, we can decide:
- if there are 0 pieces, the move is invalid;
- if there is more than 1 piece, the move is ambiguous;
- is there is exactly one piece, we can execute the move!
As for castling, there are two kinds of it: short castling and long castling. Short one is usually denoted as “0-0” while long one is “0-0-0”. The thing is that castling coordinates are always the same for the both sides; and given I can check whether the move is valid (by calling “isLegalMove” function), I’m able to select the valid castling needed for the player.
E.g. if the input is “0-0”, I know that it’s Kg1 for white and Kg8 for black. Apparently, one of the moves is illegal (two players cannot move at the same time). That’ll give me the needed move.
The move parsing by itself took some time, but it was a really fun thing to do, especially with unit tests to check all of my changes (source code).
After I made my tests pass, and handled all the cases with the amount of matching pieces, I tried to play using the notation and it worked! This moment marks one more big milestone for the project.
More little things
After that, more quiet period of development started. I didn’t start major features, mostly improving what I already had. I enabled the keyboard controls on more pages, added support for promotion syntax (when the pawns reach the latest rank you can convert it to a piece; the notation looks like “e8=Q”, meaning “pawn moves to e8 and turns into a queen”.
Under the hood it moves the pawn to the next square, and then triggers a “click” event on the type of piece you want to promote to.
I also found out that Firefox add-ons are quite the same as the Chrome ones, and I decided to make an effort to make it Firefox compatible as well. I didn’t have to do a lot of work there: the same manifest support worked for both browsers and it pleasantly surprised me.
I also started thinking that the input can serve not only as the moves entry points, but as a command line: similar to Alfred or Spotlight in macOS. So you can resign or offer a draw by running a command.
Of course, I would keep the moves parsing a primary feature, so I decided to add a prefix to such commands.
For most of the commands the implementation would be very simple: mostly I had to trigger a click event on some interface button to execute an action.
I also decided to go for blindfold mode, allowing you to exercise in playing chess without looking on the board. You can play blindfolded with a computer or make an agreement with a friend to play in that mode.
New turn: VueJS
At this point everything was good and I liked overall state of the application. But I started to notice that it stopped working on “Live chess” page, so I decided to check. When I tried to debug the issue I understood, that I can’t find the source files I used to.
It appeared that chess.com developers are in the process of making some huge changes. Apparently, all the board mechanics was rewritten using VueJS as framework.
Kudos to them for doing that, because the old code contained a lot of legacy code and even I could see that; it was probably a good decision for them. But for me it meant than all of my integrations with the board interface aren’t working anymore. I had to do something with it, but I didn’t even know where to start. I haven’t had an experience with VueJS, so I didn’t know if I’d be able to interact with it.
After some research I understood that it nearly impossible to hook into the internal logic of VueJS components, especially if they are properly minified.
I was able to access the internal state of the element via “vue” property though; it was really helpful. Based on the data contained there I was able to check if a move is legal or if it’s the player’s turn.
But I lost the possibility to control player’s moves using built-in API. After some thinking I decided to imitate mouse events on the board in order to “drag” pieces. I had to calculate the square coordinates based on chess board position and size and then trigger the events. You can see the first prototype in this commit.
const toCoords = squareToCoords(targetAreaId);
const { left, top, width } = this.element.getBoundingClientRect();
const squareWidth = width / 8;
const correction = squareWidth / 2;
pieceElement.dispatchEvent(new MouseEvent("mousedown", {
bubbles: true,
cancelable: true,
view: window,
which: 0,
clientX: left + (squareWidth) * Number(fromCoords[0]) - correction,
clientY: top + width - (squareWidth) * Number(fromCoords[1]) + correction,
}));
const mouseupEvent = new MouseEvent("mouseup", {
bubbles: true,
cancelable: true,
view: window,
which: 0,
clientX: left + (squareWidth) * Number(toCoords[0]) - correction,
clientY: top + width - (squareWidth) * Number(toCoords[1]) + correction,
});
pieceElement.dispatchEvent(mouseupEvent);
Surprisingly, the concept worked and I implemented a new interface based on it.
With the update I also lost the ability to highlight board squares and draw arrows using build-in functionality, so I had to draw all this stuff manually on top of the board. It was fun!
Bu ty way, Chess.com didn’t migrate all of their pages to VueJS. I believe that they started with “live” app, but some of them (e.g. “Play with computer”) are still running the old version of script. So, I keep both versions of chess board handlers and initialise the one I need based on the page environment.
Future of the project
What’s the future of the project? Well, there’s a full backlog of features I’d like to implement: speech recognition support, chat integration, moves announcements. My current goals also include raising the community awareness about the extension and collecting feedback.
Hopefully, there will be updates on the project soon 😃
Stay tuned!