The Queen’s Gambit - Post 5
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:
Stockfish
stockfish is a free, open source chess engine which implements the UCI (Universal Chess Interface)
Installation of Stockfish
On MacOS (with homebrew installed): brew install stockfish
See the stockfish website for more details on installation on your system
{processx}
To interface with Stockfish (a command line/terminal program) I’m using
processx
.
processx is a package for executing and controlling
subprocesses (like a command like program) from R. You can write to the program’s stdin
and then read what it outputs (stdout
).
Simple interfacing with Stockfish using {processx}
#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
# Run 'stockfish' on the command line and set up pipes so we can send and
# receive messages from stdin and stdou
#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
sf <- process$new('stockfish', stdin = '|', stdout = '|', stderr = '|')
#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
# UCI standard says that you should always request 'uci' from whatever ches
# ending you're using. This outputs the configuration in stockfish
#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
sf$write_input('uci\n')
cat(sf$read_output_lines())
## Stockfish 12 by the Stockfish developers (see AUTHORS file)
#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
# Initialise the board to the opening state and a couple of basic moves.
# There's no "opening book" in stockfish
#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
sf$write_input('position startpos moves e2e4 e7e5\n')
#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
# Ask stockfish to think about this move for 0.1 second (100 ms)
#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
sf$write_input('go movetime 100\n')
#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
# wait for Stockfish to think
# Need to be certain that it finishes and prints out its move
# Note: probably nicer ways to wait via processx, but haven't dug deep enough yet
#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Sys.sleep(1)
#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
# Get all the output that stockfish produces.
# There's a lot of lines that start with 'info' which are steps along its
# thinking process.
#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
sf_response <- sf$read_output_lines()
#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
# The only line that i'm interested in is the last one which is the
# description of the 'bestmove'
#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
best_move_line <- tail(sf_response, 1)
best_move <- strsplit(best_move_line, " ")[[1]][2]
best_move_line
## [1] "bestmove g1f3 ponder g8f6"
best_move
## [1] "g1f3"
Stockfish-based Chess game in RStats
The following code is a very rough chess engine in R using Stockfish to make the computer’s moves.
Instructions:
- Enter
Your Move:
in Long Algebraic Notation (LAN)- e.g.
e2e4
is a valid opening move - The first 2 symbols are the starting location
e2
i.e. White King’s Pawn - The second 2 symbols are the destination
e4
i.e. the fourth row in columne
- e.g.
- The computer will make a move (after thinking for 0.1 seconds)
- Enter
q
to quit when it’s your turn to move.
Limitiations:
- no move validation, so there’s no prevention of impossible moves.
- What stockfish does with impossible moves is undefined as far as I know.
- No detection of ‘end-of-game’
- no castling
- I’ve given Stockfish very little thinking time by default (0.1s) - feel free to make this longer if you need a challenge.
Click to reveal/hide support functions
#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
#' 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]))
}
#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
#' Create a plot of the state of a chess board from a FEN string
#'
#' @param fen FEN string
#'
#' @return ggplot2 object
#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
plot_board <- function(mat) {
#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
# 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
#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
p <- 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() +
scale_fill_manual(values = c('grey50', 'grey95')) +
scale_y_continuous(breaks = 1:8) +
scale_x_continuous(breaks = 1:8, labels = letters[1:8]) +
theme_bw() +
theme(
axis.title = element_blank(),
plot.background = element_blank(),
panel.grid = element_blank(),
panel.border = element_blank(),
axis.ticks = element_blank()
)
plot(p)
invisible()
}
#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
# Super basic Long Annoation to matrix coordinates
#
# @param lan e.g. e2e5
#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
lan_to_coords <- function(lan) {
stopifnot(nchar(lan) == 4)
lan <- tolower(lan)
bits <- strsplit(lan, '')[[1]]
bits[1] <- match(bits[1], letters[1:8])
bits[3] <- match(bits[3], letters[1:8])
bits <- as.integer(bits)
list(
start = matrix(c(9 - bits[2], bits[1]), nrow = 1),
end = matrix(c(9 - bits[4], bits[3]), nrow = 1)
)
}
library(ggplot2)
library(processx)
#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
# Start the Stockfish proces
#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
sf <- process$new('stockfish', stdin = '|', stdout = '|', stderr = '|')
sf$write_input('uci\n')
jnk <- sf$read_output_lines()
#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
# Initialise board (i.e. matrix) to opening position
#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
fen <- 'rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1'
board <- fen_to_matrix(fen)
plot_board(board)
#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
# Keep track of all the moves played. This list of moves is fed to Stockfish
# every time, and then it decides what the next move should be
#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
all_moves <- c()
#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
#
# Game Loop
#
#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
while(TRUE) {
#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
# Ask user for a move and add it to the list of all moves
#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
move_lan <- readline("Your move? : ")
if (move_lan %in% c('', 'q', 'Q', 'quit')) {
message("Done")
break
}
all_moves <- c(all_moves, move_lan)
#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
# Validate move
#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
# Not done yet
#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
# Move the piece on the local board
#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
move_coords <- lan_to_coords(move_lan)
if (board[move_coords$start] %in% c(NA, ' ', '')) {
stop("Bad move given current board state: ", best_move)
}
board[move_coords$end ] <- board[move_coords$start]
board[move_coords$start] <- ' '
plot_board(board)
#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
# Tell stockfish about the move
#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
moves <- paste(all_moves, collapse = " ")
cmd <- paste('position startpos moves', moves, '\n')
# print(cmd)
sf$write_input(cmd)
#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
# Ask stockfish to consider the next move for 0.1 seconds
#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
sf$write_input('go movetime 100\n')
Sys.sleep(0.2)
#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
# Extract the best move from output from Stockfish
#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
sf_response <- sf$read_output_lines()
best_move_line <- tail(sf_response, 1)
best_move <- strsplit(best_move_line, " ")[[1]][2]
cat("Computer move:", best_move, "\n")
#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
# Add the computers move
#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
all_moves <- c(all_moves, best_move)
#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
# Move the piece on the local board
#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
move_coords <- lan_to_coords(best_move)
if (board[move_coords$start] %in% c(NA, ' ', '')) {
stop("Bad move given current board state: ", best_move)
}
board[move_coords$end ] <- board[move_coords$start]
board[move_coords$start] <- ' '
plot_board(board)
}