Tutorials:

Rendering a Chess Board

- Avery N. Nortonsmith

The last tutorial I wrote ended up being pretty long. This time I'll try for something a little more "byte-sized".

In this tutorial I'll show you how to render an HTML chess board from Forsyth-Edwards Notation, a common format for representing chess boards with text. Our end goal is to go from this:

1Q6/5pk1/2p3p1/1p2N2p/1b5P/1bn5/2r3P1/2K5

To this:

    
  ♟︎ 
 ♟︎ ♟︎
♟︎  ♟︎
    
♝   
 ♜ ♙
    

And hopefully learn a bit of Pointless along the way.


Forsyth-Edwards Notation (FEN) uses letters to represent chess pieces: R for rooks, N for knights, B for bishops, Q for queens, K for kings, and P for pawns. White pieces use uppercase characters, and black pieces use lowercase. Rows of the board are separated with slashes /. Empty squares within a row are represented with the digits 1-8, corresponding to the number of consecutive empty squares.

In a FEN string, the squares of a board are recorded from left-to-right, top-to-bottom. Here's the FEN encoding of the starting state for chess games rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR.

(A FEN string contains other game-state data -- which side moves next, move number, etc -- however, for this tutorial we'll only consider the board representation component)

Before we tackle rendering to HTML, we'll write a function showBoard which converts a FEN string to a more human-readable representation. The most basic version of this function splits the FEN string into rows (at the / character), and joins the rows with newlines:

fen = "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR"

output =
  fen
  |> showBoard
  |> println

------------------------------------------------------------------------------

showBoard(fen) =
  fen
  |> split("/")
  |> join("\n")
$ ./bin/pointless render.ptls

rnbqkbnr
pppppppp
8
8
8
8
PPPPPPPP
RNBQKBNR

This simple implementation gives us a halfway-decent board representation for the starting board position. However, things get messier when we apply it to more complex positions:

fen = "1Q6/5pk1/2p3p1/1p2N2p/1b5P/1bn5/2r3P1/2K5"
$ ./bin/pointless render.ptls

1Q6
5pk1
2p3p1
1p2N2p
1b5P
1bn5
2r3P1
2K5

To get something more readable we need to convert the digit-representation for blank squares into actual spaces. We'll write a function expandRow that will substitute the correct number of spaces for each spacing digit.

To start, expandRow splits a row string into a list of characters using toList. We use the map function to call expandRow on each row string in the FEN. The results of this are shown below:

showBoard(fen) =
  fen
  |> split("/")
  |> map(expandRow)
  |> join("\n")

------------------------------------------------------------------------------

expandRow(row) =
  row
  |> toList
$ ./bin/pointless render.ptls

["1", "Q", "6"]
["5", "p", "k", "1"]
["2", "p", "3", "p", "1"]
["1", "p", "2", "N", "2", "p"]
["1", "b", "5", "P"]
["1", "b", "n", "5"]
["2", "r", "3", "P", "1"]
["2", "K", "5"]

Next, we modify expandRow to call a new function, expandChar on each character in the row string. expandChar uses the dictionary spaceDict along with the function getDefault to replace spacing digits with spaces while leaving piece characters unaltered. The statement getDefault(spaceDict, char, char) gets the value for the key char in spaceDict, or returns the original char if char is not a key in spaceDict. expandRow now produces the following results:

expandRow(row) =
  row
  |> toList
  |> map(expandChar)

expandChar(char) =
  getDefault(spaceDict, char, char)

spaceDict = {
  "8": "        ",
  "7": "       ",
  "6": "      ",
  "5": "     ",
  "4": "    ",
  "3": "   ",
  "2": "  ",
  "1": " ",
}
$ ./bin/pointless render.ptls

[" ", "Q", "      "]
["     ", "p", "k", " "]
["  ", "p", "   ", "p", " "]
[" ", "p", "  ", "N", "  ", "p"]
[" ", "b", "     ", "P"]
[" ", "b", "n", "     "]
["  ", "r", "   ", "P", " "]
["  ", "K", "     "]

The last step for expandRow after converting the spacing digits in each row is to join the resulting characters back together. This is done with the join function:

expandRow(row) =
  row
  |> toList
  |> map(expandChar)
  |> join("")

Here's what our entire program and its output look like so far:

fen = "1Q6/5pk1/2p3p1/1p2N2p/1b5P/1bn5/2r3P1/2K5"

output =
  fen
  |> showBoard
  |> println

------------------------------------------------------------------------------

showBoard(fen) =
  fen
  |> split("/")
  |> map(expandRow)
  |> join("\n")

------------------------------------------------------------------------------

expandRow(row) =
  row
  |> toList
  |> map(expandChar)
  |> join("")

expandChar(char) =
  getDefault(spaceDict, char, char)

spaceDict = {
  "8": "        ",
  "7": "       ",
  "6": "      ",
  "5": "     ",
  "4": "    ",
  "3": "   ",
  "2": "  ",
  "1": " ",
}
$ ./bin/pointless render.ptls

 Q      
     pk 
  p   p 
 p  N  p
 b     P
 bn     
  r   P 
  K 

Running the program now gives us a board representation with proper spacing.

This board representation might be more readable than a FEN string, but it's still not ideal. A nicer representation would use symbols for pieces rather than letters, and would show the colors of board squares. While it's possible to use Unicode symbols and ANSI color codes to achieve these features in the terminal, using HTML and CSS to create board renderings will give us more stylistic control and make our renderings compatible with more devices.

To get started we'll add a new function renderHTML which will convert the board string output from showBoard into HTML. The HTML output is then passed to println:

output =
  fen
  |> showBoard
  |> renderHTML
  |> println

We want renderHTML to display each square with the correct square color. We'll do this by making a list of pairs of the form (squareColor, pieceChar) for each square in the input board string. This list is made by calling zip with two arguments: an infinite list of alternating Light and Dark labels, and the list of characters in boardStr.

Since we iterate through board squares from left-to-right, top-to-bottom, the first square a8 is Light.

Note that since each row is represented with an odd number of characters (8 squares and 1 newline, 9 total), the starting color of each row alternates as desired.

Our initial implementation of renderHTML and its output are shown below:

colors =
  [Light, Dark] |> repeat |> concat

renderHTML(boardStr) =
  boardStr
  |> toList
  |> zip(colors)
$ ./bin/pointless render.ptls

[(Light, " "), (Dark, "Q"), (Light, " "), (Dark, " "), ...]

To display our board, we'll use the Chess Merida Unicode font, which has symbols for each chess piece on both light and dark squares. The characters for each symbol are stored in the renderSyms dictionary in the code below. Some of the characters won't render correctly until we display them in the merida font; until then they look like this: .

To render our (squareColor, pieceChar) pairs, we simply take the corresponding character from renderSyms. Note that newlines are mapped to newlines, regardless of their "color":

renderHTML(boardStr) =
  boardStr
  |> toList
  |> zip(colors)
  |> map(getIndex(renderSyms))
  |> join("")

------------------------------------------------------------------------------

renderSyms = {
  (Dark,  "R" ): "",
  (Dark,  "r" ): "",
  (Dark,  "N" ): "",
  (Dark,  "n" ): "",
  (Dark,  "B" ): "",
  (Dark,  "b" ): "",
  (Dark,  "Q" ): "",
  (Dark,  "q" ): "",
  (Dark,  "K" ): "",
  (Dark,  "k" ): "",
  (Dark,  "P" ): "",
  (Dark,  "p" ): "",
  (Dark,  " " ): "",
  (Dark,  "\n"): "\n",
  (Light, "R" ): "♖",
  (Light, "r" ): "♜",
  (Light, "N" ): "♘",
  (Light, "n" ): "♞",
  (Light, "B" ): "♗",
  (Light, "b" ): "♝",
  (Light, "Q" ): "♕",
  (Light, "q" ): "♛",
  (Light, "K" ): "♔",
  (Light, "k" ): "♚",
  (Light, "P" ): "♙",
  (Light, "p" ): "♟︎",
  (Light, " " ): " ",
  (Light, "\n"): "\n",
}
$ ./bin/pointless render.ptls

    
  ♟︎ 
 ♟︎ ♟︎
♟︎  ♟︎
    
♝   
 ♜ ♙
    

The results don't look like much in the terminal, where most of the characters won't render properly.

Time to write some CSS.

The styles variable below contains CSS to load our chess font and apply some styling to our board element. The function formatTemplate creates a string containing these styles, along with the symbol characters taken from renderSyms wrapped in an HTML <pre> tag:

styles = """<style>
  @font-face {
    font-family: "merida";
    src: url("merida.woff2") format("woff2");
  }

  .board {
    font-family: "merida";
    font-size: 40px;
    border: 1px solid #ccc;
    display: inline-block;
    padding: 3px;
  }
</style>
"""

formatTemplate(boardChars) =
  format("{}\n<pre class='board'>{}</pre>", [styles, boardChars])

We update renderHTML to call formatTemplate to complete our program, which now produces the following HTML output:

renderHTML(boardStr) =
  boardStr
  |> toList
  |> zip(colors)
  |> map(getIndex(renderSyms))
  |> join("")
  |> formatTemplate
$ ./bin/pointless render.ptls

<style>
  @font-face {
    font-family: "merida";
    src: url("merida.woff2") format("woff2");
  }

  .board {
    font-family: "merida";
    font-size: 40px;
    border: 1px solid #ccc;
    display: inline-block;
    padding: 3px;
  }
</style>

<pre class='board'>    
  ♟︎ 
 ♟︎ ♟︎
♟︎  ♟︎
    
♝   
 ♜ ♙
    </pre>

Now we just have to load our HTML output into the browser. We can run the script again and save the output to the file render.html:

$ ./bin/pointless render.ptls > render.html

And load render.html in a browser:

$ firefox render.html

Which should give us something like this:

    
  ♟︎ 
 ♟︎ ♟︎
♟︎  ♟︎
    
♝   
 ♜ ♙
    

Nice! Much better than just displaying characters in the terminal.

At this point you might be thinking about ways to extend this sort of program to render an entire game. I had the same though when I wrote this tutorial, and I ended up building a site that lets you design and order posters of games using Portable Game Notation (a text format for encoding entire games). You can check it out here!

Here's the full code of our final program:

fen = "1Q6/5pk1/2p3p1/1p2N2p/1b5P/1bn5/2r3P1/2K5"

output =
  fen
  |> showBoard
  |> renderHTML
  |> println

------------------------------------------------------------------------------

showBoard(fen) =
  fen
  |> split("/")
  |> map(expandRow)
  |> join("\n")

------------------------------------------------------------------------------

expandRow(row) =
  row
  |> toList
  |> map(expandChar)
  |> join("")

expandChar(char) =
  getDefault(spaceDict, char, char)

spaceDict = {
  "8": "        ",
  "7": "       ",
  "6": "      ",
  "5": "     ",
  "4": "    ",
  "3": "   ",
  "2": "  ",
  "1": " ",
}

------------------------------------------------------------------------------

colors =
  [Light, Dark] |> repeat |> concat

renderHTML(boardStr) =
  boardStr
  |> toList
  |> zip(colors)
  |> map(getIndex(renderSyms))
  |> join("")
  |> formatTemplate

------------------------------------------------------------------------------

renderSyms = {
  (Dark,  "R" ): "",
  (Dark,  "r" ): "",
  (Dark,  "N" ): "",
  (Dark,  "n" ): "",
  (Dark,  "B" ): "",
  (Dark,  "b" ): "",
  (Dark,  "Q" ): "",
  (Dark,  "q" ): "",
  (Dark,  "K" ): "",
  (Dark,  "k" ): "",
  (Dark,  "P" ): "",
  (Dark,  "p" ): "",
  (Dark,  " " ): "",
  (Dark,  "\n"): "\n",
  (Light, "R" ): "♖",
  (Light, "r" ): "♜",
  (Light, "N" ): "♘",
  (Light, "n" ): "♞",
  (Light, "B" ): "♗",
  (Light, "b" ): "♝",
  (Light, "Q" ): "♕",
  (Light, "q" ): "♛",
  (Light, "K" ): "♔",
  (Light, "k" ): "♚",
  (Light, "P" ): "♙",
  (Light, "p" ): "♟︎",
  (Light, " " ): " ",
  (Light, "\n"): "\n",
}

------------------------------------------------------------------------------

styles = """<style>
  @font-face {
    font-family: "merida";
    src: url("merida.woff2") format("woff2");
  }

  .board {
    font-family: "merida";
    font-size: 40px;
    border: 1px solid #ccc;
    display: inline-block;
    padding: 3px;
  }
</style>
"""

formatTemplate(boardChars) =
  format("{}\n<pre class='board'>{}</pre>", [styles, boardChars])