Sign Up Docs Tech Blog Product Blog

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.”


Comment from the mac chess source code

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.

How are chess positions represented?#

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.

How do chess engines represent who’s winning and by how much?#

There are a lot of books about advantages in chess. Looking at a simple example:


Chess board without queen

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 players' kings are, 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.


Mate in one

This is a “mate in 1” because if white moves the queen to G7, it’s checkmate.

Analyzing a chess position with Stockfish and python-chess#

Stockfish is an open-source chess engine. It is often regarded as the strongest chess engine that exists today. We’ll use the python library python-chess to interact with it.

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:


Mate in one
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.

Searching for more than one move#

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])

Putting it all together#

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):


A different position
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 very long sequence of moves that it considers equal.

Analyzing with other engines#

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.

What next?#

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!

PropelAuth © 2022