How to play Leela Chess Zero (Lc0) and Maia using Python & Stockfish
Introduction
Leela Chess Zero (Lc0) is the ground-breaking neural network (AI) chess engine. Unlike conventional algorithmic engines which are instructed how to play chess, Lc0 has honed it skill using reinforcement learning and repeated self-play over billions of games. Some conventional engines, like Stockfish, have recently added neural networks to their systems.
Lc0 is free to download and use. With just a few lines of code you can play against Lc0 using text commands! And you also get Maia for free because it uses the Lc0 engine.
This blog is adapted from "How to integrate Stockfish into your projects using Python".
Installing Lc0
Download the Lc0 executable from https://lczero.org
There's a ready-made lc0.exe for Windows users. Linux users need to build the exe manually.
Make a note of the path to the Lc0 executable as you'll need it later.
Lc0 needs a neural network (NN) file to tell it how to play. A NN file contains the weights data from a particular training session; the larger the file the stronger it plays, and the longer it takes to calculate. A graphics card will be useful if you plan on using larger NN files. Keep the NN files in the compressed gzip format on your computer; Lc0 copes with them fine.
Put your chosen NN file in the same folder as the Lc0 program.
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.
Why do we need Stockfish? The Stockfish engine has a special "d" command which is very handy, and saves a great deal of coding. More on this later!
Python programming
Python is a popular programming language that's very easy to learn. To program in Python download the Thonny IDE.
Play Lc0 using Python
Now we're ready to play Leela at chess. Copy and paste this code into Thonny:
import subprocess
exeStockfishPath = "/home/linuxuser/Downloads/stockfish-64bit/stockfish-ubuntu-x86-64"
sf = subprocess.Popen(exeStockfishPath, universal_newlines=True, stdin=subprocess.PIPE, stdout=subprocess.PIPE)
exeLeelaPath = "/home/linuxuser/Downloads/leela chess zero/lc0"
lc0 = subprocess.Popen(exeLeelaPath, universal_newlines=True, stdin=subprocess.PIPE, stdout=subprocess.PIPE)
fen = ''; eval = ''; isCheck = False; bestMove = ''; depth = 5
def sendUCI(process: subprocess.Popen, cmd: str, sync: bool, printOutput: bool):
# Send command
cmd = f'{cmd}\n'
if sync: cmd += 'isready\n'
process.stdin.write(cmd); process.stdin.flush()
# Process output
while True:
line = process.stdout.readline().strip()
if line.startswith('Fen: '): # Get FEN if available
global fen; fen = line.replace('Fen: ', '')
if line.startswith('Final evaluation'): # Get eval if available
line = line.replace('Final evaluation:', '') # Stockfish inconsistent with text...
line = line.replace('Final evaluation', '') # ...so do this twice
x = line.strip().split(' ')
global eval; eval = x[0]
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(sf, 'd', True, True)
sendUCI(sf, 'eval', True, False)
def updateBoard(move: str):
sendUCI(sf, f'position fen {fen} moves {move}', True, False)
sendUCI(sf, 'd', True, False)
def calculateMove(process: subprocess.Popen, depth: int):
print('Engine is thinking...')
sendUCI(process, f'position fen {fen}', True, False)
sendUCI(process, f'go depth {depth}', False, True)
def isGameOver() -> bool:
print ('Evaluation: ',eval)
return eval == 'none'
# set Lc0 engine options
sendUCI(lc0, 'setoption name threads value 1', True, True)
sendUCI(lc0, 'setoption name RamLimitMb value 256', True, True)
#sendUCI(lc0, 'setoption name WeightsFile value /home/linuxuser/Downloads/leela chess zero/NN maia/maia-1500.pb.gz', True, True)
sendUCI(lc0, 'uci', True, True) # show Lc0 engine properties
#fen = 'rnbqkbnr/ppppp2p/5p2/6p1/3PP3/8/PPP2PPP/RNBQKBNR w KQkq - 0 3' # test mate in 1 with d1h5 (Qh5)
#sendUCI(sf, f'position fen {fen}', True, False)
printBoard()
while not isGameOver():
print('Enter move (e.g. e2e4)')
userMove = input()
previousFen = fen
# Update the board with user's move
updateBoard(userMove)
# Check move is legal
while fen == previousFen:
print('Illegal move! Try again')
userMove = input()
updateBoard(userMove)
printBoard()
if isGameOver(): break
# Get engine move
calculateMove(lc0, depth) # use Lc0 engine
#calculateMove(sf, depth) # you can use Stockfish if you like!
# Update the board with engine move
updateBoard(bestMove)
printBoard()
if isCheck: print('Check!')
print('Game over!')
Don't forget to put the file paths to your Lc0 and Stockfish exe into the script.
Both Lc0 and Stockfish engines use the Universal Chess Interface (UCI) which is "an open communication protocol that enables chess engines to communicate with user interfaces" according to Wikipedia. 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.
All being well you should see a screen like this.

The Stockfish "d" command is very handy as it provides a diagram of the board, returns the updated FEN and says if there's a check. The updated FEN can be used to check move legality. You get all this for free with the "d" command. This command is non-standard in the UCI specifications, so sadly Lc0 doesn't have it. It seems strange to have Stockfish and Lc0 running at the same time but it works perfectly fine. Notice Lc0 is only ever called upon to calculate a move, everything else is done with Stockfish.
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.
In addition to the board position the updated FEN has other useful info: who to move next, castling rights, en passant square, halfmove clock and fullmove number.
Stockfish takes care of updating the FEN... there's no need for us to worry about updating the board ourselves. We just push and pull the FEN from the engine. When we want Lc0 to move we set the FEN in Lc0 and get it to calculate. The move is then passed to Stockfish to update the FEN and get the "d" command's output.
To check if a game is over due to checkmate or stalemate you can look at the engine's evaluation which will say "none" if game over. If there's a check it's checkmate, else stalemate. The evaluation may be a bit approximate as we're getting it from Stockfish without any calculation. Alternatively, you can get it from Lc0 after it's moved.
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.
Of course, you can plug in any other UCI-compliant chess engine instead of Lc0, e.g. Komodo.
Lc0 and neural network (NN) files
Lc0 will, by default, look for a NN file in the same folder as the Lc0 program. To tell it to look elsewhere use the "WeightsFile" option. You can add a line to the above Python script to direct Lc0 to load a specific NN file, e.g.
sendUCI(lc0, 'setoption name WeightsFile value <path to NN file>', True, True)
Check it's worked by looking at the engine properties from the "uci" command.
Also take a look at the other Lc0 parameters, such as "threads" and "RamLimitMb" which can be used to tweak Lc0 performance.
How to play against Maia
Maia is the name given to Leela Chess Zero when using neural networks trained on human games. These files were created by the University of Toronto. To play Maia simply download a Maia NN file and either put it in the same folder as the Lc0 program (and remove any other NN files) or use the "WeightsFile" option as mentioned above.
Download Maia NN files from https://lczero.org/play/networks/sparring-nets. You will find a list of NN files based on Elo rating.
No opening book
Leela Chess Zero doesn't have an opening book so it tends to play the same first move at any given skill level. For example, its response to e2e4 is invariably the Sicilian, c7c5. 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".
Bye for now
That's it. Hope you enjoyed this blog.
Don't forget to have a look at my blog "How to integrate Stockfish into your projects using Python" which shows how to use online Stockfish engines and how to set up a chess web server for your DIY chess computers and projects.
Also check out my 3Robot chess computer which now plays against Leela Chess Zero and Maia.
https://www.chess.com/blog/chrislamuk/the-3robot-an-update-with-arduino-giga-and-stockfish
Happy chessing!