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]
- 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.
- Active color. “w” means White moves next, “b” means Black moves next.
- 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.
- 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]
- 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]
- 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"