This article is part of a series – the contents page is here.
In earlier articles we’ve seen the code for generating moves. Before we use it to build a playing engine, let’s take a detour and consider how we’re going to run that engine.
We’ve already identified that, thanks to the vastness of chess’s move tree, our engine could be slow to run. Sophisticated modern engines can generate strong moves in just a few milliseconds, but unfortunately such speed and strength is beyond the scope of this project. We’re going to have to wait a few seconds for Shallow Thought to calculate its moves. [Update – March 2023: back in 2017 I noted that Shallow Thought ran much faster on Chrome than on Edge. Nowadays I would say that, if anything, Edge has the ‘edge’ in speed].
But lets make a virtue of necessity and use this as an opportunity to study ways to perform slow CPU-bound tasks in a browser. As I’m sure you know, browsers are mostly single-threaded, and in any event only that main thread can touch the DOM tree. But nowadays we have an option for running a task in a background browser thread: Web Workers. The discussion below might interest you if you haven’t worked with these before.
The Player classes
Players are represented by the HumanPlayer
and ArtificialPlayer
classes, which derive from the PlayerBase
class (see /src/app/ui/playerbase.ts
). The players implement an activate()
method, which is called each time the player is called upon to make a move. In the case of the HumanPlayer, activate()
simply kicks the player into listening mode so that it will respond to the mouse clicks on the squares where the move is to be made. But for ArtificialPlayer, activate()
invokes the engine.
ArtificialPlayer.activate()
Here’s the salient part of the ArtificialPlayer code (with some irrelevant bits removed):
export class ArtificialPlayer extends PlayerBase {
private currentBoard: Chess.Board;
private engineWorker: Worker;
activate(board: Chess.Board): void {
if (!this.engineWorker) {
this.engineWorker = new Worker(new URL('./artificialPlayerDispatch.ts', import.meta.url));
}
this.currentBoard = board;
this.engineWorker.onmessage = this.onMoveDecision;
this.engineWorker.postMessage(board);
}
public onMoveDecision = (e: MessageEvent) => {
...
this.playedMove = Chess.GameMove.deserialize(e.data);
this.move.emit(this.playedMove);
}
}
Note the following:
- We declare a
Worker
object calledengineWorker
and on first use we initialize it fromartificialPlayerDispatch.ts
. This is packaged in an extra Angular ‘webworker’ bundle in our Angular configuration that contains only the move-calculating code. We won’t have access to any DOM-related stuff inside the Worker and we will run into errors if we even try to bring in modules that expect to use thewindow
object, for example. - We hook up a handler for the Worker’s
onmessage
event. We have to use messages to communicate across the threads – there’s no shared memory. The essence of our handler is to turn the message into aGameMove
object and emit it through ourmove
observable. - Having prepared the engineWorker, we invoke it by posting a message. Notice that the message consists only of a
Board
object containing the position to be played.
So does that message parameter arrive on the Worker thread as a full functioning Board
object? Unfortunately no! It’s a bit like serialization – the materialized object contains the right data but loses the prototype chain and becomes a plain old Object
. One of the worker’s tasks will be to fix it up. Let’s now switch over to the worker thread.
The engine bundle
Once the Worker picks up our posted message it starts to run the code in engine.bundle.js. The entry point for this bundle is artificialPlayerDispatch.ts
artificialPlayerDispatch.ts
Here’s the entry code (again with some irrelevant detail removed so that we can focus on the message passing).
onmessage = function (event) { // Marshall the Board object that we have been sent. var board: Chess.Board = Object.assign(new Chess.Board, event.data); // Prepare a player object that will calculate the next move and tell us about its progress. var computerPlayer = new ComputerPlayer(); var selectedMove = computerPlayer.getBestMove(board); // Send our selected move back to the main thread. (<any>self).postMessage(selectedMove); }
We start by using Object.assign()
to turn event.data (the raw message Object) into the fully functioning Board object that we know and love. Then it’s a simple matter of creating a ComputerPlayer object (basically, the engine) and asking it to tell us what move to play for that board. Finally, the Worker’s self
object gives us the postMessage
method that sends the GameMove object back to the main thread. Doing so will trigger the onMoveDecision
method in ArtificialPlayer. Look back at that code and you will see a call to GameMove.deserialize()
. This is the same chore of turning the posted data back into a strongly typed object on the main thread.
Conclusion
So what do we get for all this extra work? Simple – we get a user interface that doesn’t freeze up while the computer thinks about its move. In the full code base you will see that we add extra messages to drive a progress bar that lets the user know how much thinking is still to be done. That wouldn’t be possible if the engine was running on the main thread.