How to integrate Stockfish into your projects using Python

How to integrate Stockfish into your projects using Python

Avatar of chrislamuk
| 1

Introduction

Stockfish is a chess engine that's free to download and use. It's very popular and used on many websites that allow you to play against a bot. A chess engine is just that... an engine without a graphical interface. However with just a few lines of code you can play against Stockfish using text commands.

In this blog I'll discuss a number of ways to integrate Stockfish into your own DIY chess computer project with a real chess board and pieces... check out my 3Robot project. I'll show how to run Stockfish on your own computer and make use of online Stockfish engines which are ideal for dedicated chess computers with wi-fi enabled microcontrollers. Finally I'll discuss setting up your own Stockfish web server. And once you have Stockfish running it's a short step to adding other engines like Komodo and Leela Chess Zero!

Installing Stockfish

Download the Stockfish executable from https://stockfishchess.org/download/

The version I got to work on my old laptop was the 64-bit version. You may have more luck with the newer versions. Make a note of the path to the Stockfish executable as you'll need it later.

Python programming

Python is a programming language that's very easy to learn and use and is very popular. To program in Python download the Thonny IDE.

Stockfish wrapper

For my first programming example I'll use a Stockfish wrapper. A wrapper is some code that presents an easy-to-use programming interface to something that is otherwise awkward to use.

In Thonny open up the "Manage packages..." dialog and search for "stockfish" and install. This installed ok for me in Windows but in Linux I got a tedious security error and had to force the installation from the terminal:

pip install stockfish --break-system-packages

(Easy alternative: download the wrapper, extract the models.py file and put that alongside your Python scripts. In your code use "from models import Stockfish".) 

Writing a simple chess program in Python

Now we're ready to write some code and play chess. In Thonny type:

from stockfish import Stockfish

# Update the following line with the path to your Stockfish executable
sf = Stockfish(path="/home/linuxuser/Downloads/stockfish-64bit/stockfish-ubuntu-x86-64") # e.g. Linux
# sf = Stockfish(path="C:/stockfish-64bit/stockfish-windows-x86-64.exe")  # e.g. Windows

# Set skill level
sf.set_elo_rating(1500)

# Print board
print(sf.get_board_visual())

while True:
    print('Enter move (e.g. e2e4)')
    myMove = input()

    # Update the board with user's move (assume legal)
    sf.make_moves_from_current_position([myMove])

    # Get Stockfish's move
    computerMove = sf.get_best_move()
    print("Stockfish plays: ", computerMove)

    # Update the board with computer's move
    sf.make_moves_from_current_position([computerMove])

    print(sf.get_board_visual())

(Note: When saving your program don't name it "stockfish.py" or you may get horrible circular reference errors!)

All being well you should see a screen like this.

Not the best chess interface in the world but it's a starting point. Using the wrapper allows us to get a game going with minimal coding.

Visit https://pypi.org/project/stockfish for more on the wrapper. The function that tests move legality doesn't work and the wrapper hasn't been maintained for a few years. Otherwise it's an easy way to start coding with Stockfish... though you may have to tinker with the wrapper itself to get the best out of it.

The UCI interface

Alternatively you can code directly to the Stockfish engine which uses the Universal Chess Interface (UCI) which is "an open communication protocol that enables chess engines to communicate with user interfaces" according to Wikipedia . It's more work but you'll have full control and your code will work with any UCI chess engine.

UCI uses a buffering system, we write commands into a buffer and read the output from another. So the code does some waiting while the output buffer fills up.

Here's another Python script to play chess but this time without the wrapper; it has some extra features like making sure the user's move is legal, and seeing if in check or game over.

import subprocess

exePath = "/home/linuxuser/Downloads/stockfish-64bit/stockfish-ubuntu-x86-64"

sf = subprocess.Popen(exePath, universal_newlines=True, stdin=subprocess.PIPE, stdout=subprocess.PIPE)
fen = ''; isCheck = False; bestMove = ''; depth = 5

def sendUCI(cmd: str, sync: bool, printOutput: bool):
    # Send command
    cmd = f'{cmd}\n' 
    if sync: cmd += 'isready\n'
    sf.stdin.write(cmd); sf.stdin.flush()
    
    # Process output
    while True:
        line = sf.stdout.readline().strip()
                
        if line.startswith('Fen: '): # Get FEN if available
            global fen; fen = line.replace('Fen: ', '')
            
        if line.startswith('Checkers:'): # Is there a check?
            global isCheck; isCheck = line.replace('Checkers:', '') != ''
            
        if line.startswith('bestmove'): # Get computer move if available
            x = line.split(' ')
            global bestMove; bestMove = x[1]
            if printOutput: print(line)
            break
            
        if line == 'readyok':
            break

        if printOutput: print(line)
        
def printBoard():
    sendUCI('d', True, True)
    
def updateBoard(move: str):
    sendUCI(f'position fen {fen} moves {move}', True, False)
    sendUCI('d', True, False) # Get the FEN, don't display board

def calculateMove():
    print('Stockfish is thinking...')
    sendUCI(f'go depth {depth}', False, True)
    
def isGameOver() -> bool:
    sendUCI('go depth 1', False, False) # Any moves available?
    return bestMove == '(none)'

printBoard()

while not isGameOver():
    print('Enter move (e.g. e2e4)')
    userMove = input()
    
    previousFen = fen
    
    updateBoard(userMove)
    
    # Is move legal?
    while fen == previousFen:
        print('Illegal move! Try again')
        userMove = input()
        updateBoard(userMove)
        
    printBoard()
    
    if isGameOver(): break
    
    calculateMove() # Get Stockfish move
    
    updateBoard(bestMove)
    
    printBoard()

    if isCheck: print('Check!')
    
print('Game over!')

Note we only pass the FEN to and fro from Stockfish... there's no need for us to worry about the board itself. Communication between client and server is done using stdin and stdout buffers.

The "d" command is very handy as it displays the board, returns the updated FEN and says if there's a check. This command is non-standard in the UCI specifications and may not seem like much but it saves a great deal of work.

Stockfish doesn't have a specific routine to test move legality but one way is to compare FENs before and after asking Stockfish to update the position; if they are the same then the move is illegal.

To check if a game is over due to checkmate or stalemate you can look at the computer's next best move which will say "none" if game over. If there's a check it's checkmate, else stalemate. 

In addition to the board position the FEN has other useful info: who to move next, castling rights, en passant square, halfmove clock and fullmove number.

Checking for a draw can be done manually. For threefold repetition keep a list of all the FENs minus halfmove clock and fullmove number... the other parameters are needed to determine if it's the same exact position. The halfmove clock is used to detect a draw by fifty-move rule. Checking for a draw by insufficient material can be done by counting pieces in the FEN.

The en passant parameter is helpful so your chess computer can prompt the user to remove the pawn concerned. During promotion the user is prompted for the piece required and the piece added to the move for Stockfish, e.g. a7a8q. In addition when castling the user should be prompted to move the rook.

Other UCI chess engines

Two other UCI engines worth trying are Leela Chess Zero and Komodo; the former is the ground-breaking AI neural network engine which learns its chess through self-play, and the latter is a strong, conventional engine with interesting features like "personality", and also plays Armageddon.

These two engines are missing the Stockfish "d" command which, as mentioned previously, is very useful. However there's no reason why you can't use two engines at the same time... Stockfish for game management and another for the actual computer moves. As an exercise have a go at modifying the above script to play Komodo while keeping Stockfish to manage the game... you shouldn't have to change too much but it helps to pass the engine process into sendUCI() as an extra parameter.

Update: See my new blog "How to play Leela Chess Zero (Lc0) and Maia using Python & Stockfish".

Online Stockfish engines

If your chess computer project has a Raspberry Pi you can just run Stockfish on that. If your microcontroller can't run Stockfish then you can try accessing Stockfish remotely using wi-fi. There are a couple of Stockfish servers on the internet at chess-api.com and stockfish.online. You simply send the FEN and depth and it returns a move.

Here's an example of how to get a move from stockfish.online using a browser:

https://stockfish.online/api/s/v2.php?fen=r1bqkbnr/pppp1p1p/6p1/8/2PQ4/2N5/PP2PPPP/R1B1KB1R%20b%20KQkq%20-%200%206&depth=10

The response is:

{"success":true,"evaluation":2.01,"mate":null,"bestmove":"bestmove d8f6 ponder d4e3","continuation":"d8f6 d4e3 f6e6 c3b5 f8b4 e1d1 e8d8 e3f4 c7c6"}

where the move is "d8f6" along with a hint for the next move "d4e3", plus the evaluation and continuation. Using this method you can build a Stockfish computer using a low-spec microcontroller. The other server at chess-api.com may be more useful as it returns a lot of helpful info about the move, e.g.

{
    "text": "Move d8 → f6 (Qf6): [1.33]. White is winning. Depth 12.",
    "captured": false,
    "promotion": false,
    "isCapture": false,
    "isPromotion": false,
    "isCastling": false,
    "fen": "r1bqkbnr/pppp1p1p/6p1/8/2PQ4/2N5/PP2PPPP/R1B1KB1R b KQkq - 0 6",
    "type": "bestmove",
    "depth": 12,
    "move": "d8f6",
    "eval": 1.33,
    "centipawns": 133,
    "mate": null,
etc.
}

(Quibble: the above returns the FEN I sent to it... it would be much more useful to return the FEN *after* making its move!)

Here's some Python code to access these servers:

import requests
import json

fen = "r1bqkbnr/pppp1p1p/6p1/8/2PQ4/2N5/PP2PPPP/R1B1KB1R b KQkq - 0 6"
depth = 10

# try stockfish.online which uses GET
url = f"https://stockfish.online/api/s/v2.php?fen={fen}&depth={depth}"
response = requests.get(url)
print ("URL: ", url)
print ("Status code returned: ", response.status_code)
print ("Response body: ", response.content)
stockfishResponse = json.loads(response.content)
move = stockfishResponse["bestmove"].split()
print("Move: ", move[1])
print ()

# try chess-api.com which uses POST
url = "https://chess-api.com/v1"
params = {'fen': fen, 'depth': depth}
response = requests.post(url, json = params)
print ("URL: ", url)
print ("Params: ", params)
print ("Status code returned: ", response.status_code)
print ("Response body: ", response.content)
stockfishResponse = json.loads(response.content)
move = stockfishResponse["move"]
print("Move: ", move)

These online solutions are less flexible than actually having Stockfish on your computer but they're an easy way to add Stockfish to your project.

Building your own Stockfish web server

If you like the idea of a web server solution but find the online servers too restrictive then you can always put Stockfish on your computer and add a web server (e.g. Apache for Linux or IIS for Windows) and have your chess project wi-fi to that. This is the route I've gone down for my own 3Robot project. You need just one web page (a Python script) which accepts query string parameters (FEN, depth, etc.) and passes them to the Stockfish executable... the web page then responds with details of the move. Once you've done this for Stockfish you can easily add other engines like Leela Chess Zero or Komodo. You can also get Stockfish to do other tasks like test move legality, if in check or if game over, and save yourself a lot of coding! After a game you could send the moves to the server for analysis later.

Here's a quick overview of how I set up a Stockfish web server. This just sits in my local home network, of course, not the internet! Since I have Linux on my laptop I installed Apache web server and then enabled CGI scripts. I can now create Python scripts and put them in the cgi-bin folder. Make sure file permissions are set to executable and your firewall allows incoming requests. You'll have to do all this as admin, of course.

Here's my web page (Python script named calculate.py) which takes the engine, FEN and depth as query string parameters and returns the move and evaluation.

#!/usr/bin/python3.12

import subprocess
import os

responseError = '{"isok":false,"error":"{error}"}'
responseOK = '{"isok":true,"evalType":"{evalType}","evalValue":{evalValue},"move":"{move}"}'

print('Content-type: text/html') # the mime-type header.
print() # header must be separated from body by 1 empty line.

# Process query string parameters
qs = os.environ['QUERY_STRING']
qs = qs.replace("=", " ")
qs = qs.replace("&", " ")

qsList = qs.split(" ")

# Do some validation
if (len(qsList) != 6):
    print (responseError.replace('{error}', 'Bad query string'))
    quit()

if (qsList[0] != 'engine') or (qsList[2] != 'fen') or (qsList[4] != 'depth'):
    print (responseError.replace('{error}', 'Bad query string'))
    quit()

engineName = qsList[1] # 'sf','km','lc0'
fen = qsList[3].replace("%20", " ") # Decode spaces
depth = int(qsList[5])

if (depth < 1) or (depth > 20):
    print (responseError.replace('{error}', 'Bad depth value. Must be 1..20'))
    quit()

# Set up engine
engineExePath = ''
match engineName:
    case 'sf': engineExePath = "/home/linuxuser/Downloads/stockfish-64bit/stockfish-ubuntu-x86-64"
    case 'km': engineExePath = "/home/linuxuser/Downloads/komodo_dragon/Linux/dragon-linux"
    case 'lc0' : engineExePath = "/home/linuxuser/Downloads/leela chess zero/lc0"

if engineExePath == '':
    print (responseError.replace('{error}', 'Bad engine. Must be: sf km lc0'))
    quit()

engine = subprocess.Popen(engineExePath, universal_newlines=True, stdin=subprocess.PIPE, stdout=subprocess.PIPE)

# Set FEN
engine.stdin.write(f'position fen {fen}\n')
engine.stdin.flush()

# Get engine move
engine.stdin.write(f'go depth {depth}\n')
engine.stdin.flush()

evalSign = 1 if "w" in fen else -1
        
# Process output
while True:
    line = engine.stdout.readline().strip()
    lineSplit = line.split(" ")
    
    if lineSplit[0] == "info":
        for n in range(len(lineSplit)):
            if lineSplit[n] == "score":
                evalType = lineSplit[n + 1]
                evalValue = str(int(lineSplit[n + 2]) * evalSign)
                        
    if line.startswith('bestmove'):
        x = line.split(' ')
        move = x[1]
        break;

# Build response
responseOK = responseOK.replace('{evalType}', evalType)
responseOK = responseOK.replace('{evalValue}', evalValue)
responseOK = responseOK.replace('{move}', move)
print(responseOK)

An example URL to call this script looks like: 

http://192.168.1.158/cgi-bin/calculate.py?engine=lc0&fen=r1bqkbnr/pppp1p1p/6p1/8/2PQ4/2N5/PP2PPPP/R1B1KB1R%20b%20KQkq%20-%200%206&depth=3

where 192.168.1.158 is the local IP address of the computer hosting the web server. A typical response is:

{"isok":true,"evalType":"cp","evalValue":130,"move":"d8f6"}

Similarly another script updateboard.py is needed to update the FEN with a move and return the new FEN and if in check info as per the second Python script above. Then you can amend the second script to use the web server rather than instancing the Stockfish exe.

One other thing to say about these web server solutions is that they're stateless, i.e. the web server does not remember your previous requests or positions... the engine instance only exists long enough to compute its move. Compare this with the scripts above that play a game; there the engine exists for the entire game.

Other server options

Instead of a Stockfish server that uses HTTP you could use Bluetooth instead; it may be simpler technically as you would only need a single Python script running on the chess server, and avoid the need to set up a web server. I chose the web server route as I'm familiar with the technology through work, otherwise I may well have tried Bluetooth first.

There's also the option of writing an app for your mobile phone which runs Stockfish locally and connects to your chess project using Bluetooth. There is a Stockfish exe for ARM devices but other engines may not be so helpful.

No opening book

Stockfish doesn't have an opening book so it tends to play the same first move at any given skill level. Personally I find this quite annoying so I wrote my own code to add an opening book.

For more details, take a look at my blog, "How to Write a Chess Opening Book for Stockfish using Python".

Conclusion

Integrating the Stockfish chess engine into your own projects is easy, especially when using a friendly programming language like Python. You can choose between having the engine in your DIY chess computer (if you have a Raspberry Pi) or using an online Stockfish server. Alternatively you can build your own Stockfish web server and have your DIY chess computer wi-fi to that; the server handles all the chess functionality. And once that's set up it's easy to add other UCI-compliant engines like Leela Chess Zero.

Happy chessing!