18
Analyzing Chess Positions in Python - Building a Chess Analysis App (Part 1)
Computers have been significantly better than humans at chess for a long time now. So much so that in the official macOS chess app, the computer has a short delay before it makes a move because "Users tend to get frustrated once they realize how little time their Mac really spends to crush them."
We can, however, use these powerful chess engines to get a better understanding of the game. In this series, we'll build a production-ready chess analysis application. Our users will submit chess positions for deep analysis with an engine.
This first post will focus on how to do the analysis itself.
Luckily for us, there already exists a standard format called FEN (or Forsyth–Edwards Notation). It describes the entire board state in one simple string.
The starting position:
rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1
This includes where all the pieces are, who's turn it is, where the kings are allowed to castle, and more. Our users will submit positions in this format.
You can use the Lichess board editor to set up a chess board and convert it to FEN.
There are a lot of books about advantages in chess. Looking at a simple example:
This example is impossible, but it illustrates an important point. White has an obvious advantage here because Black doesn't have a queen. This is called a material advantage. However, there are many other factors to consider,
such as whose turn it is, how safe each player's king is, etc.
Engines take a ton of information into account and output a score
for a position. These scores are measured in centipawns, which is 1/100th of a pawn.
If an engine says that Black is winning by 100 centipawns, this is similar to saying, "Black's advantage is about the same as if everything is equal, but they are up one pawn."
In some cases, one player can checkmate the other, regardless of what the other player does. These are called "forced mates" or "mate in X moves." They are usually represented with a #
. #1
means that White has a single move that will checkmate Black. #-3
means that Black can checkmate White within the next three moves, no matter what White does.
This is a "mate in 1" because if white moves the queen to G7, it's checkmate.
First, download/install Stockfish from the instructions here. Make note of where it's installed. If you installed it via homebrew on a Mac, you can use which
to find it:
$ which stockfish
/usr/local/bin/stockfish
Next, we'll create a Python virtual environment and install our
dependencies.
$ mkdir chess-api
$ python3 -m venv venv
$ source venv/bin/activate
(venv) $ pip install python-chess
Let's create a small test file to analyze our mate in one position from before:
import chess
import chess.engine
# Change this if stockfish is somewhere else
engine = chess.engine.SimpleEngine.popen_uci("/usr/local/bin/stockfish")
# The position represented in FEN
board = chess.Board("5Q2/5K1k/8/8/8/8/8/8 w - - 0 1")
# Limit our search so it doesn't run forever
info = engine.analyse(board, chess.engine.Limit(depth=20))
info contains a lot of information, but let's just look at two important fields:
{
'score': PovScore(Mate(+1), WHITE),
'pv': [Move.from_uci('f8g7')],
}
The score
tells us what Stockfish thinks of the position, which is a mate in one.
The pv
(short for principal variation) tells us the sequence of moves that the engine expects to be played. In this case, it's saying that it expects White to move their queen on f8 to g7, which is checkmate.
The analyse
function has a few more options we haven't used, and one important one is multipv
. multipv=3
tells the engine to return the top 3 best moves, instead of just the best move.
# ...same as before
board = chess.Board("5Q2/5K1k/8/8/8/8/8/8 w - - 0 1")
# Get the 3 best moves
info = engine.analyse(board, chess.engine.Limit(depth=20), multipv=3)
# Info is now an array with at most 3 elements
# If there aren't 3 valid moves, the array would have less than 3 elements
print(info[0])
print(info[1])
print(info[2])
With everything we learned, we can now write a wrapper function that we'll use in our app.
import chess
import chess.engine
# Change this if stockfish is somewhere else
engine = chess.engine.SimpleEngine.popen_uci("/usr/local/bin/stockfish")
def analyze_position(fen, num_moves_to_return=1, depth_limit=None, time_limit=None):
search_limit = chess.engine.Limit(depth=depth_limit, time=time_limit)
board = chess.Board(fen)
infos = engine.analyse(board, search_limit, multipv=num_moves_to_return)
return [format_info(info) for info in infos]
def format_info(info):
# Normalize by always looking from White's perspective
score = info["score"].white()
# Split up the score into a mate score and a centipawn score
mate_score = score.mate()
centipawn_score = score.score()
return {
"mate_score": mate_score,
"centipawn_score": centipawn_score,
"pv": format_moves(info["pv"]),
}
# Convert the move class to a standard string
def format_moves(pv):
return [move.uci() for move in pv]
Let's use it to analyze this position (Black's turn to move):
print(analyze_position("8/8/6P1/4R3/8/6k1/2r5/6K1 b - - 0 1", num_moves_to_return=3, depth_limit=20))
[
{
'mate_score': -2,
'centipawn_score': None,
'pv': ['c2c1', 'e5e1', 'c1e1']
},
{
'mate_score': None,
'centipawn_score': 0,
'pv': ['c2c8', 'e5e3', 'g3h4', 'e3e7', 'c8c6', ...]
},
{
'mate_score': None,
'centipawn_score': 0,
'pv': ['g3f4', 'e5e7', 'c2c8', 'g6g7', 'c8g8', ...]
}
]
This is a position where Black has a forced checkmate in 2 moves, but there is only one move that forces this checkmate. If Black plays any other move, White can equalize (centipawn_score
of 0). The engine then shows a long sequence of moves that it considers equal.
Under the hood, python-chess communicates with Stockfish using a standard protocol called UCI (Universal Chess Interface). You can replace the path to Stockfish with the path to any engine that implements UCI and it'll work just the same.
Given relatively little code, we are able to analyze chess positions using the most powerful chess engine around.
In the next post, we'll create a backend service using Flask where users can submit positions to be analyzed. We'll create a queue of positions that we analyze so our system doesn't get overloaded. See you then!
18