Parsing Forsyth Edwards Notation for Chess in RStats

The Queen’s Gambit - Post 4

Inspired by The Queen’s Gambit on Netflix, I’m doing a few posts on Chess in R.

This screenshot from the show explains everything:

Forsyth-Edwards Notation (FEN)

Forsyth-Edwards Notation (FEN) is a short ascii description of the state of play of a game of chess.

See the article on wikipedia for a more complete description.

Examples of FEN

Initial state of a chess game

rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1

This is an actual board state when opening with the queen’s gambit.

rnbqkbnr/ppp1pppp/8/3p4/2PP4/8/PP2PPPP/RNBQKBNR b KQkq c3 0 2

FEN format

A FEN record contains six fields. The separator between fields is a space. The fields are:[5]

  1. Piece placement (from White’s perspective). Each rank is described, starting with rank 8 and ending with rank 1; within each rank, the contents of each square are described from file “a” through file “h”. Following the Standard Algebraic Notation (SAN), each piece is identified by a single letter taken from the standard English names (pawn = “P”, knight = “N”, bishop = “B”, rook = “R”, queen = “Q” and king = “K”). White pieces are designated using upper-case letters (“PNBRQK”) while black pieces use lowercase (“pnbrqk”). Empty squares are noted using digits 1 through 8 (the number of empty squares), and “/” separates ranks.
  2. Active color. “w” means White moves next, “b” means Black moves next.
  3. Castling availability. If neither side can castle, this is “-”. Otherwise, this has one or more letters: “K” (White can castle kingside), “Q” (White can castle queenside), “k” (Black can castle kingside), and/or “q” (Black can castle queenside). A move that temporarily prevents castling does not negate this notation.
  4. En passant target square in algebraic notation. If there’s no en passant target square, this is “-”. If a pawn has just made a two-square move, this is the position “behind” the pawn. This is recorded regardless of whether there is a pawn in position to make an en passant capture.[6]
  5. Halfmove clock: This is the number of halfmoves since the last capture or pawn advance. The reason for this field is that the value is used in the fifty-move rule.[7]
  6. Fullmove number: The number of the full move. It starts at 1, and is incremented after Black’s move.

Parsing Piece Placement from a FEN record

The following code is only determining the placement of pieces on the board. All other information is ignored.

#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
#' Convert a FEN string to a char matrix representing the chess board.
#' 
#' @param fen fen string
#' 
#' @return 8x8 character matrix representing the chess board
#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
fen_to_matrix <- function(fen) {
  
  #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
  # Currently only interested in 1st field in fen i.e. piece placement
  #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
  pieces <- strsplit(fen, ' ')[[1]][1]
  rows   <- strsplit(pieces, "/")[[1]]
  
  #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
  # Replaces digits with the number of spaces they represent
  #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
  for (i in 1:8) {
    rows <- gsub(i, paste(rep(' ', i), collapse=''), rows)
  }
  
  matrix(unlist(strsplit(rows, '')), 8, 8, byrow = TRUE, list(c(8:1), letters[1:8]))
}


fen <- 'rnbqkbnr/ppp1pppp/8/3p4/2PP4/8/PP2PPPP/RNBQKBNR b KQkq c3 0 2'
fen_to_matrix(fen)
##   a   b   c   d   e   f   g   h  
## 8 "r" "n" "b" "q" "k" "b" "n" "r"
## 7 "p" "p" "p" " " "p" "p" "p" "p"
## 6 " " " " " " " " " " " " " " " "
## 5 " " " " " " "p" " " " " " " " "
## 4 " " " " "P" "P" " " " " " " " "
## 3 " " " " " " " " " " " " " " " "
## 2 "P" "P" " " " " "P" "P" "P" "P"
## 1 "R" "N" "B" "Q" "K" "B" "N" "R"

Tighter matrix output

mat <- fen_to_matrix(fen)
mat[mat ==' '] <- '.'
cat(apply(mat, 1, paste, collapse=''), sep="\n")
## rnbqkbnr
## ppp.pppp
## ........
## ...p....
## ..PP....
## ........
## PP..PPPP
## RNBQKBNR

Add a splash of colour with {emphatic}

# remotes::install_github('coolbutuseless/emphatic')
library(emphatic)
library(dplyr)
hl_opt_global(dark_mode = FALSE)

mat <- fen_to_matrix(fen) 
mat[] <- paste0(mat, ' ')

mat %>% 
  hl_matrix('grey90') %>% 
  hl_matrix('grey10', selection = (row(.x) + col(.x)) %% 2 == 1)
     a  b  c  d  e  f  g  h
8   r  n  b  q  k  b  n  r 
7   p  p  p     p  p  p  p 
6                          
5            p             
4         P  P             
3                          
2   P  P        P  P  P  P 
1   R  N  B  Q  K  B  N  R 

ggplot of the board state from a FEN

#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
#' Create a plot of the state of a chess board from a FEN string
#'
#' @param fen FEN string
#'
#' @return ggplot2 object
#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
create_fen_plot <- function(fen) {
  
  #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
  # Convert fen to matrix
  #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
  mat <- fen_to_matrix(fen)
  
  #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
  # Unicode chars for chess pieces
  #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
  unicode <- c(
    ` ` = '',
    P='\u2659', R='\u2656', N='\u2658', B='\u2657', Q='\u2655', K='\u2654', # White
    p='\u265F', r='\u265C', n='\u265E', b='\u265D', q='\u265B', k='\u265A'  # Black
  )
  
  #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
  # Convert the matrix chess board to a data.frame
  #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
  board <- expand.grid(x=1:8, y=1:8) %>%
    mutate(
      tilecol = (x+y)%%2 == 1,
      piece  = as.vector(t(mat[8:1,])),
      colour = piece %in% letters,
      unicode = unicode[piece]
    )
  
  
  #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
  # Create plot
  #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
  ggplot(board, aes(x, y)) + 
    geom_tile(aes(fill = tilecol), colour = 'black', show.legend = FALSE) + 
    geom_text(aes(label = unicode), family="Arial Unicode MS", size = 8) + 
    coord_equal() + 
    theme_void() +
    scale_fill_manual(values = c('grey50', 'grey95'))  +
    labs(
      title = "Forsyth-Edwards Notation of the Queen's Gambit in #RStats",
      subtitle = fen
    )
  
}


create_fen_plot(fen)

Convert a matrix of chess locations into FEN

#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
#' Matrix to Forsyth-Edwards Notation notation
#'
#' This function only considers piece positions. All other information is
#' filled in with dummy values e.g. next move is always set to white (0)
#'
#' @param mat 8x8 character matrix of chess locations. 
#'
#' @return Single character string containing board status in Forsyth-Edwards Notation
#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
matrix_to_fen <- function(mat) {
  
  #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
  # Sanity check board
  # Empty board spaces must be a single blank character
  #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
  stopifnot(is.character(mat))
  stopifnot(nrow(mat) == 8, ncol(mat) == 8)
  mat[is.na(mat) | mat == ''] <- ' '
  
  #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
  # Collapse each row
  #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
  rows <- apply(mat, 1, paste, collapse='')
  
  #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
  # Replace runs of spaces with the integer count of spaces
  #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
  for (i in 8:1) {
    rows <- gsub(paste(rep(' ', i), collapse=''), i, rows)
  }
  
  #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
  # piece position is rows concatentated with "/" as dividing character
  #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
  pieces <- paste(rows, collapse="/")

  #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~  
  # Remaining information filled out with dummy values
  #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~  
  move      <- 'w'
  castling  <- 'KQkq'
  enpassant <- '-'
  halfmove  <- 0
  fullmove  <- 1

  #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
  # Concatenate all the information together - separated by spaces
  #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~  
  paste(pieces, move, castling, enpassant, halfmove, fullmove)
}


fen
## [1] "rnbqkbnr/ppp1pppp/8/3p4/2PP4/8/PP2PPPP/RNBQKBNR b KQkq c3 0 2"
mat <- fen_to_matrix(fen) 
mat
##   a   b   c   d   e   f   g   h  
## 8 "r" "n" "b" "q" "k" "b" "n" "r"
## 7 "p" "p" "p" " " "p" "p" "p" "p"
## 6 " " " " " " " " " " " " " " " "
## 5 " " " " " " "p" " " " " " " " "
## 4 " " " " "P" "P" " " " " " " " "
## 3 " " " " " " " " " " " " " " " "
## 2 "P" "P" " " " " "P" "P" "P" "P"
## 1 "R" "N" "B" "Q" "K" "B" "N" "R"
matrix_to_fen(mat)
## [1] "rnbqkbnr/ppp1pppp/8/3p4/2PP4/8/PP2PPPP/RNBQKBNR w KQkq - 0 1"