Rendering the chess board – declaratively. Part 1.

Having decided that I want to include the ability of stepping through chess games in the MVP, I set to implementing that feature.

My first thought was  to not reinvent the wheel and use chessboard.js to render the chess board. However, it turned out it uses jQuery and I didn't want jQuery in my app. I thus started replacing jQuery methods with native browser ones. However, this led me down a rabbit hole and after a good amount of work I still didn't see how much work it would be.

Declaratively rendering the chess board. Will it work?

I thought about that for a little bit and decided to give an Ember-based, declarative rendering solution a try. I had some concerns about the performance of rendering a component for each square of the board. However, Glimmer components are supposedly really fast and there is never going to be more than 64 of these components ever for a chess board, so scaling issues are kept at bay (unless I'll want to have dozens of chess board on screen at the same time, I guess).

I also reminded myself that this project is for trying out new things, so I rolled up my sleeves and started the work.

One chess board and sixty-four squares

The details of a game are shown on the Game details page, so I created a ChessBoard component that I rendered from that template:

// app/templates/games/game.hbs
<ChessBoard
  @pieces={{this.model.pieces}}
/>

Declarative rendering, Exhibit A: I just pass the pieces on the board to ChessBoard and have the component render them. Should the pieces change when the visitor steps through the game, the component will re-render.

Let's see how the pieces accessor works in the Game model:

// app/models/game.js
export default class GameModel extends Model {
  // (...)
  @attr() moves

  @attr() fen

  // The first row is the 8th rank!
  fenForStartingPosition = 'rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1';

  get pieces() {
    /*
      Return all the pieces on the board, keyed by square "ids"
      {
        d6: 'wQ',
        d4: 'wP',
        e4: 'wK'
      }
    */
    let pieces = {};
    let files = 'abcdefgh';
    let fen = this.fen || this.fenForStartingPosition;
    let ranks = fen.split('/');
    let lastRank = ranks[ranks.length - 1].split(/\s+/)[0];
    ranks = [...ranks.slice(0, -1), lastRank];
    for (let rankIndex = 0; rankIndex < 8; rankIndex++) {
      let rank = ranks[rankIndex];
      let fileIndex = 0;
      while (fileIndex < 8) {
        let symbol = rank[fileIndex];
        let charCode = symbol.charCodeAt(0);
        // The first FEN row is the 8th rank
        let squareKey = files[fileIndex] + (8 - rankIndex);
        if (charCode >= 49 && charCode <= 56) {
          // a number that represents empty squares
          fileIndex += parseInt(symbol, 10);
        }
        // it's a piece
        if (charCode >= 66 && charCode < 98) {
          // it's a capital letter denoting a piece => a white piece
          pieces[squareKey] = 'w' + symbol.toUpperCase();
        }
        if (charCode >= 98 && charCode <= 114) {
          // it's a black piece
          pieces[squareKey] = 'b' + symbol.toUpperCase();
        }
        fileIndex += 1;
      }
    }
    return pieces;
  }
}

Some explanation is in order.

Describing the state of the chess board

First, the Forsyth–Edwards Notation (or FEN, for short) is used to describe "the state" of the chess board, including where the pieces are, which side can castle and to which side, who's to move and the possible "en passant" position.

The pieces getter takes a FEN and transforms it to a form that's easy to work with. It returns a JS object whose keys are the square ids ("e4", "h6", etc.) and the related value for each key is the piece that occupies that square. If there's no pieces on that square, there's no key in the object.

FEN is tricky in that the 8th rank comes first in the notation so above the "indexes start at 0 in the world of computers" problem, we also have to grapple with that. Life is so hard :p

Back to the drawing chess board

Please note two things: there's at least one bug in the method and I don't think the best place for it is in the Game model but it's good enough for a start. I'll fix and refactor it later (in fact, I'd already did but I'm usually ahead in development compared to blogging :) ).

The chess-board.js file is pretty slim:

// app/components/chess-board.js
import Component from '@glimmer/component';

export default class ChessBoardComponent extends Component {
  get ranks() {
    return Array.from('12345678').map(Number);
  }

  get reversedRanks() {
    return this.ranks.reverse();
  }

  get files() {
    return Array.from('abcdefgh');
  }
}

The template is as follows:

// app/templates/components/chess-board.hbs
<div class="board w-400px flex flex-col">
  {{#each this.reversedRanks as |rank|}}
    <div class="flex flex-row">
      {{#each this.files as |file index|}}
        <BoardSquare
          @rank={{rank}}
          @file={{file}}
          @numericFile={{index}}
          @piece={{get @pieces (concat file rank)}}
        />
      {{/each}}
    </div>
  {{/each}}
</div>

As usually chess boards are shown with the first rank (the white pieces) shown at the bottom and the eight rank (the black pieces) at the top, we reverse the ranks and start drawing at the 8th rank (see this.reversedRanks).

The final piece is the BoardSquare component. As established above, there's going to be 64 of these, so they better be cheap to render!

In the first iteration, I decided to keep it simple and use a backing class (the .js file), although I heard it can slow things down. "We'll cross that bridge when we come to it" – I murmured to myself as I generated the component and started writing the only thing I needed a backing class for, to decide whether the square is a light or dark one:

// app/components/board-square.js
import Component from '@glimmer/component';

export default class BoardSquareComponent extends Component {
  rank = null;
  file = null;
  numericFile = null;

  get lightSquare() {
    let evenRank = this.args.rank % 2 === 0;
    let evenFile = (this.args.numericFile + 1) % 2 === 0;
    return evenRank ? !evenFile : evenFile;
  }
}

The rule for deciding whether it's a light or dark square changes depending on which rank we're on. The bottom-left corner on a chess board is rank 0, file 0 and is a light one, but rank 1, file 0 is dark, so some trial & error was needed but the above works.

The logic for lightSquare being in the backing class, the template can be kept simple:

{{!-- app/templates/components/board-square.hbs --}}
<div
  class="w-50px h-50px {{if this.lightSquare "light-square" "dark-square"}}"
  ...attributes
>
  {{#if @piece}}
    <img
      src="/images/pieces/wikipedia/{{@piece}}.png"
      alt={{@piece}}
    >
  {{/if}}
</div>

(I snatched the images for the pieces from the chessboard.js library.)

The final piece is the definition of light-square and dark-square CSS classes. In Tailwind you can define components to bundle together a set of related Tailwind classes. ember-cli-tailwind makes it even simpler: you can just define these components in any file under app/tailwind/components:

.light-square {
  @apply bg-beige-light text-beige-darker;
}

.dark-square {
  @apply bg-beige-darker text-beige-light;
}

And it has worked, we have a handsome-looking chess board in the starting position:

Ready for battle.

There is so much more...

The above is a "first pass" implementation and I've already refactored a few of the pieces as the additional, chess board-related features made it necessary. And of course we currently just render a chess board with the pieces in the starting position.

On top of that, we'll need to:

  • Display letters and numbers for the files and ranks
  • Display the moves of the game, with the result displayed after the final move
  • Enable the user to step through the game, forward, backward, and to jump to any move.
  • As a stretch goal, it would be extremely nice if the pieces could move to their next position.

My hope is that declarative rendering will make some of the tasks ahead a lot simpler. And that the performance of rendering pieces and moves will be bearable.

Stay tuned for more – or just sign up below!