Chess engine code – running in the background

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:

  1. We declare a Worker object called engineWorker and on first use we initialize it from artificialPlayerDispatch.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 the window object, for example.
  2. 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 a GameMove object and emit it through our move observable.
  3. 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.

Advertisement

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s