DEV Community

Cover image for How to make a Lichess bot in Python
Kai Pereira
Kai Pereira

Posted on • Originally published at Medium

How to make a Lichess bot in Python

Chess is one of the most popular strategic games played all around the world. Chess bots now dominate even the highest level grand-masters so how do we make one?

To get started all you’ll need is Python3.9 and Github installed!

Outline

Lichess Communication:

Setting up a communication to lichess using lichess-bot.

Chess Bot:

Building a minimax chess engine using position and material eval.

Hosting:

Using https://railway.app/ to host your chess bot.

Lichess Communication

Cloning

To start off we’re going to set up a brand new VSCode project!

Image description

We’re going to use lichess-bot to set up a live data stream to lichess so that we can play games withing their application.

Clone https://github.com/lichess-bot-devs/lichess-bot.git into your new project! Make sure you have github installed!

git clone https://github.com/lichess-bot-devs/lichess-bot.git
Enter fullscreen mode Exit fullscreen mode

Image description

Virtual Env

Now we’re going to set up a consistent virtual env for python.

This will help with maintaining consistency and managing dependencies in our application.

cd lichess-bot
pip install virtualenv
python -m venv venv
venv\Scripts\activate
pip install -r requirements.txt
Enter fullscreen mode Exit fullscreen mode

Image description

Once this is done, copy config.yml.default to *config.yml*

Image description

Lichess Bot Account

Once you’re finished, we’re going to set up a lichess bot account. Head over to https://lichess.org/ and create a new account

It’s important that this account has not games played in order to become a bot account.

Once you’ve create an account, go to preferences -> API access tokens and create a key that looks like this.

Image description

Now put the following curl command into terminal and replace YOUR_TOKEN with the token you get after creating.

Make sure to save this token into a save place too, because we’re going to be using it again!

curl -d '' https://lichess.org/api/bot/account/upgrade -H "Authorization: Bearer YOUR_TOKEN"
Enter fullscreen mode Exit fullscreen mode

Image description

Head back to lichess and you should now have a bot account!!

Image description

Now that you have a bot account, add your key we created earlier (you can create a new one with bot scope too) into the lichess config.yml file. Replace xxxxxxxxxx with your key:

Image description

Playing against it

Now that you have your bot all set up, it’s time to try it out.

In config.yml, switch name to “RandomMove” and protocol to “homemade”.

Image description

We want to be able to play most games against our bot so uncomment correspondance and switch 14 days to .inf.

Image description

Then run inside of your terminal:

python3 lichess-bot.py
Enter fullscreen mode Exit fullscreen mode

Image description

You’re now connected to lichess and ready to start playing some games against your bot!!

Note: You’ll need to switch your lichess account to play against your bot.

Image description

But right now you’re bot plays random moves, let’s make it STRONG now.

Image description

Building the AI

How it’s going to work

To build the AI, we’re going to be using a minimax search with position and material eval, pretty simple but strong. 

Minimax will basically search through millions of possible positions from the current board, evaluate them and then give us which one is best. Think of it as a massive tree.

Image description

Evaluation is how we’re going to determine whether this position of millions of others is better than the others.

Position eval takes every piece in that position, finds what square it’s on and then gives it a point rating.

Image description

Each of these numbers are a spot on the board and how much the piece is worth on that square.

Then we add this value to their material eval:

Image description

You’ll notice I usually give the bishop 330, just to differentiate it from the knight and because they’re usually stronger later into the endgame.

And this is how we’re going to evaluate our positions.

Let’s get programming

Create a new folder inside of /engines called bot. Inside of this folder, create a file called main.py.

Image description

Position Evaluation

Create a new file inside of /bot called positions.py. Inside of the file, put all the positions eval for each piece:

# Pawn
pawn = [
    0, 0, 0, 0, 0, 0, 0, 0,
    5, 10, 10, -20, -20, 10, 10, 5,
    5, -5, -10, 0, 0, -10, -5, 5,
    0, 0, 0, 20, 20, 0, 0, 0,
    5, 5, 10, 25, 25, 10, 5, 5,
    10, 10, 20, 30, 30, 20, 10, 10,
    50, 50, 50, 50, 50, 50, 50, 50,
    0, 0, 0, 0, 0, 0, 0, 0]


# Knight
knight = [
    -50, -40, -30, -30, -30, -30, -40, -50,
    -40, -20, 0, 5, 5, 0, -20, -40,
    -30, 5, 10, 15, 15, 10, 5, -30,
    -30, 0, 15, 20, 20, 15, 0, -30,
    -30, 5, 15, 20, 20, 15, 5, -30,
    -30, 0, 10, 15, 15, 10, 0, -30,
    -40, -20, 0, 0, 0, 0, -20, -40,
    -50, -40, -30, -30, -30, -30, -40, -50]


# Bishop
bishop = [
    -20, -10, -10, -10, -10, -10, -10, -20,
    -10, 5, 0, 0, 0, 0, 5, -10,
    -10, 10, 10, 10, 10, 10, 10, -10,
    -10, 0, 10, 10, 10, 10, 0, -10,
    -10, 5, 5, 10, 10, 5, 5, -10,
    -10, 0, 5, 10, 10, 5, 0, -10,
    -10, 0, 0, 0, 0, 0, 0, -10,
    -20, -10, -10, -10, -10, -10, -10, -20]


# Rook
rook = [
    0, 0, 0, 5, 5, 0, 0, 0,
    -5, 0, 0, 0, 0, 0, 0, -5,
    -5, 0, 0, 0, 0, 0, 0, -5,
    -5, 0, 0, 0, 0, 0, 0, -5,
    -5, 0, 0, 0, 0, 0, 0, -5,
    -5, 0, 0, 0, 0, 0, 0, -5,
    5, 10, 10, 10, 10, 10, 10, 5,
    0, 0, 0, 0, 0, 0, 0, 0]


# Queen
queen = [
    -20, -10, -10, -5, -5, -10, -10, -20,
    -10, 0, 0, 0, 0, 0, 0, -10,
    -10, 5, 5, 5, 5, 5, 0, -10,
    0, 0, 5, 5, 5, 5, 0, -5,
    -5, 0, 5, 5, 5, 5, 0, -5,
    -10, 0, 5, 5, 5, 5, 0, -10,
    -10, 0, 0, 0, 0, 0, 0, -10,
    -20, -10, -10, -5, -5, -10, -10, -20]


# King
king = [
    20, 30, -5, 0, -5, -5, 30, 20,
    20, 20, -5, -5, -5, -5, 20, 20,
    -10, -20, -20, -20, -20, -20, -20, -10,
    -20, -30, -30, -40, -40, -30, -30, -20,
    -30, -40, -40, -50, -50, -40, -40, -30,
    -30, -40, -40, -50, -50, -40, -40, -30,
    -30, -40, -40, -50, -50, -40, -40, -30,
    -30, -40, -40, -50, -50, -40, -40, -30]
Enter fullscreen mode Exit fullscreen mode

If you want your bot to play a specific way, you can alter these values but these will give you a generally good bot.

Material Evaluation

Now we’re going to give each of our pieces a value.

Create a new file called material.py inside of bot. Pieces are going to get the following values:

  • Pawn: 100

  • Knights: 310

  • Bishops: 330

  • Rook: 500

  • Queen: 900

Inside of material.py, add the following code:

import chess

def get_material(board):
    # Weights
    pw = 100
    kw = 310
    bw = 330
    rw = 500
    qw = 900
    kingw = 20000

    wp = len(board.pieces(chess.PAWN, chess.WHITE))
    wr = len(board.pieces(chess.ROOK, chess.WHITE))
    wk = len(board.pieces(chess.KNIGHT, chess.WHITE))
    wb = len(board.pieces(chess.BISHOP, chess.WHITE))
    wq = len(board.pieces(chess.QUEEN, chess.WHITE))
    wking = len(board.pieces(chess.KING, chess.WHITE))

    bp = len(board.pieces(chess.PAWN, chess.BLACK))
    br = len(board.pieces(chess.ROOK, chess.BLACK))
    bk = len(board.pieces(chess.KNIGHT, chess.BLACK))
    bb = len(board.pieces(chess.BISHOP, chess.BLACK))
    bq = len(board.pieces(chess.QUEEN, chess.BLACK))
    bking = len(board.pieces(chess.KING, chess.BLACK))

    # White
    wpw = wp * pw
    # Rook weight increases for less pawns
    wrw = wr * rw
    # Knight weight goes down for each enemy pawn gone (8 pawns)
    wkw = wk * kw
    wbw = wb * bw
    wqw = wq * qw
    wkingw = wking * kingw

    # Black
    bpw = bp * pw
    # Rook weight increases for less pawns
    brw = br * rw
    # Knight weight goes down for each enemy pawn gone  (8 pawns)
    bkw = bk * kw
    bbw = bb * bw
    bqw = bq * qw
    bkingw = bking * kingw

    white_material = wpw + wrw + wkw + wbw + wqw + wkingw
    black_material = bpw + brw + bkw + bbw + bqw + bkingw

    total_material = white_material - black_material

    return total_material
Enter fullscreen mode Exit fullscreen mode

First, we assign our weights to our pieces. Next we grab all of those pieces on the board. Finally we do we calculations for the total piece eval of each side and make the material eval.

The final eval function

Now using the positions and the material eval, we’re going to calulcate the board evaluation.

Create a new file called evaluation.py. Put the following code inside.

from .material import get_material
import chess
from . import positions

def get_evaluation(board):

    # Check for checkmate of the opponent
    if board.is_checkmate():
        if board.turn:
            return -9999
        else:
            return 9999
    if board.is_stalemate():
            return 0
    if board.is_insufficient_material():
            return 0

    total_material = get_material(board)

    pawnsq = sum([positions.pawn[i] for i in board.pieces(chess.PAWN, chess.WHITE)])
    pawnsq = pawnsq + sum([-positions.pawn[chess.square_mirror(i)]
                        for i in board.pieces(chess.PAWN, chess.BLACK)])
    knightsq = sum([positions.knight[i] for i in board.pieces(chess.KNIGHT, chess.WHITE)])
    knightsq = knightsq + sum([-positions.knight[chess.square_mirror(i)]
                            for i in board.pieces(chess.KNIGHT, chess.BLACK)])
    bishopsq = sum([positions.bishop[i] for i in board.pieces(chess.BISHOP, chess.WHITE)])
    bishopsq = bishopsq + sum([-positions.bishop[chess.square_mirror(i)]
                            for i in board.pieces(chess.BISHOP, chess.BLACK)])
    rooksq = sum([positions.rook[i] for i in board.pieces(chess.ROOK, chess.WHITE)])
    rooksq = rooksq + sum([-positions.rook[chess.square_mirror(i)]
                        for i in board.pieces(chess.ROOK, chess.BLACK)])
    queensq = sum([positions.queen[i] for i in board.pieces(chess.QUEEN, chess.WHITE)])
    queensq = queensq + sum([-positions.queen[chess.square_mirror(i)]
                            for i in board.pieces(chess.QUEEN, chess.BLACK)])
    kingsq = sum([positions.king[i] for i in board.pieces(chess.KING, chess.WHITE)])
    kingsq = kingsq + sum([-positions.king[chess.square_mirror(i)]
                        for i in board.pieces(chess.KING, chess.BLACK)])

    eval = total_material + pawnsq + knightsq + rooksq + queensq + kingsq 

    return eval
Enter fullscreen mode Exit fullscreen mode

Let’s break this function down:

  1. If the board is a stalemate return 0, if it’s checkmate, return an impossibly high eval.

  2. If it’s neither of these scenarios calculate the position evals and add the material eval to it.

  3. Return the eval to be used

Openings

Bots sometimes struggle with variety and openings so I like to give it a bunch of openings to play. This isn’t required but I prefer to add it in.

First download this dataset of 2700 openings: https://drive.google.com/file/d/1zDjc9O3QmqvVKfBZ1fQsjXJG6ThwUAYa/view?usp=sharing

Next, add it into your workspace in the /bot folder. Your workspace should look like this at this point: 

Image description

Create a new file inside /bot now called opening.py. Add the following code to it:

import pandas as pd
import chess
import chess.pgn
import random
import os


def play_opening(board):
    next_opening_moves = [];

    # If we go first, we just play e4
    if board.turn == chess.WHITE and board.fullmove_number == 1:
        next_opening_moves.append("e2e4")

    new_board = chess.Board()

    # Get the current directory of game.py
    current_directory = os.path.dirname(os.path.abspath(__file__))

    # Define the file path relative to the current directory
    file_path = os.path.join(current_directory, 'openings.csv')

    # Get all of the SAN notations
    chess_openings = pd.read_csv(file_path)

    chess_openings = chess_openings["moves"].tolist()

    # Loop over each opening
    # If it "contains" the same board position as our current board
    # Return it's next move
    for opening in chess_openings:
        moves_in_openings = opening.split();

        for index, move in enumerate(moves_in_openings):
            try:
                new_board.push_san(move)

                if board == new_board:
                    next_move = board.parse_san(moves_in_openings[index + 1]).uci()
                    next_opening_moves.append(next_move)
            except:
                break;


        new_board.reset()


    # If there are no more opening moves, return None
    if not next_opening_moves:
        return None

    # If there is valid openings, randomly choose the next move of them
    random_opening_from_array = random.choice(next_opening_moves)

    return random_opening_from_array
Enter fullscreen mode Exit fullscreen mode

First, if we’re white, it will play e4 (My fav move yk). Next it will read the CSV file and if the opening is inside of here, it will play the next move in the sequence. If there’s no more opening moves, then it will allow our evaluation function to continue working.

Minimax function

Our minimax function is going to let us find the next best move in the future based off millions of possible moves.

Image description

Create a new file called minimax.py and put the following code inside:

from .eval import get_evaluation
import numpy as np

def minimax(board, depth, alpha, beta, maximizing_player):
  if depth == 0 or board.is_game_over():
    return get_evaluation(board)

  if maximizing_player:
    max_eval = -np.inf
    for move in board.legal_moves:
      board.push(move)
      eval = minimax(board, depth - 1, alpha, beta, False)
      board.pop()
      max_eval = max(max_eval, eval)
      alpha = max(alpha, eval)
      if beta <= alpha:
        break
    return max_eval
  else:
    min_eval = np.inf
    for move in board.legal_moves:
      board.push(move)
      eval = minimax(board, depth - 1, alpha, beta, True)
      board.pop()
      min_eval = min(min_eval, eval)
      beta = min(beta, eval)
      if beta <= alpha:
        break
    return min_eval
Enter fullscreen mode Exit fullscreen mode

Basically this function will take in a board depth (how many moves to look in the future) and then will find every single possible move based off of this.

It will also use alpha beta pruning to speed it up and optimise it for only the top tier moves.

If you want to understand more about this, check out the chess programming wiki: https://www.chessprogramming.org/Minimax

Let’s get it playing!

Navigate to /main.py and add the following code:

import chess
import numpy as np

from .opening import play_opening
from .minimax import minimax


def get_move(board, depth):
    opening_move = play_opening(board)

    if opening_move:
        print("PLAYING OPENING MOVE: ", opening_move)
        return opening_move

    top_move = None;

    # Opposite of our minimax
    if board.turn == chess.WHITE:
      top_eval = -np.inf
    else:
      top_eval = np.inf

    for move in board.legal_moves:
        board.push(move)

        # WHEN WE ARE BLACK, WE WANT TRUE AND TO GRAB THE SMALLEST VALUE
        eval = minimax(board, depth - 1, -np.inf, np.inf, board.turn)

        board.pop()

        if board.turn == chess.WHITE:
            if eval > top_eval:
                top_move = move
                top_eval = eval
        else:
            if eval < top_eval:
                top_move = move
                top_eval = eval

    print("CHOSEN MOVE: ", top_move, "WITH EVAL: ", top_eval)
    return top_move
Enter fullscreen mode Exit fullscreen mode

First, if there’s an opening, we’re going to play it. If not, we’re going to use our minimax function to find the best move.

Next, navigate to /lichess-bot/homemade.py. We’re going to add our engine into this so we can use it with the lichess api.

At the top of this file, import your engine in:

from engines.bot.main import get_move
Enter fullscreen mode Exit fullscreen mode

Image description

Now add your engine strategy into the file, make sure you put this BELOW the ExampleEngine class:

class PyBot(ExampleEngine):
    def search(self, board: chess.Board, time_limit: Limit, ponder: bool, draw_offered: bool, root_moves: MOVE) -> PlayResult:
        print("GETTING MOVE!")

        move = get_move(board, 4)

        return PlayResult(move, None)
Enter fullscreen mode Exit fullscreen mode

Finally, switch the engine name inside of config.yml to PyBot.

Image description

You can now play your bot!! Run the server with (make sure you’re in the directory):

python3 lichess-bot.py
Enter fullscreen mode Exit fullscreen mode

Image description

Now go to https://lichess.org/*your-bot* and challenge it to a game!

Image description

Hosting

Now if you want to take this a step farther, you can host it so that you never have to have the server run yourself.

Docker

We’re going to be using a custom dockerfile in order to host everything. This will ensure consistency from our dev env to the providers env.

Inside of your root directory, create a file called Dockerfile (with uppercase D) and add the following instructions:

# Use an official Python runtime as a parent image
FROM python:3.9-slim

# Set environment variables
ENV PYTHONDONTWRITEBYTECODE 1
ENV PYTHONUNBUFFERED 1
ENV NIXPACKS_PATH /opt/venv/bin:$NIXPACKS_PATH

# Set the working directory in the container
WORKDIR /app

# Install system dependencies
RUN apt-get update \
    && apt-get install -y --no-install-recommends \
    build-essential \
    && apt-get clean \
    && rm -rf /var/lib/apt/lists/*

# Copy the current directory contents into the container at /app
COPY . /app/

# Create a virtual environment and set it as the Python environment
RUN python -m venv /opt/venv
ENV PATH="/opt/venv/bin:$PATH"

# Install Python dependencies
RUN pip install --upgrade pip \
    && pip install -r requirements.txt

# Command to run the application
CMD ["python", "lichess-bot.py"]
Enter fullscreen mode Exit fullscreen mode

You’re project should now be looking like this:

Image description

Github

We need to push our entire project to github so our hosting can connect to it and run it.

Navigate to https://github.com/ and create a new repository:

Image description

Next go back to your code and tap the .gitignore. Remove the following lines:

*.yml
/engines/*
Enter fullscreen mode Exit fullscreen mode

Now let’s update the requirements.txt to make sure they get added when we build the Dockerfile. Add these to it:

numpy==1.26.0
pandas==2.2.2
Enter fullscreen mode Exit fullscreen mode

It should look something like this now:

Image description

Then run the following commands:

git reset -- lichess-bot
git add .
git commit -m "hosting"
git remote add origin https://github.com/username/repository.git - CHANGE THIS
git push --set-upstream origin master
Enter fullscreen mode Exit fullscreen mode

This will initialize the repo, remove the submodule, add the files and then push them to Github.

Image description

Let’s get hosting

Next let’s get hosting! Go to https://railway.app/ and create an account. Now create a new project and select your repo:

Image description

Now check the build logs and make sure everything is good and you’re chess bot should be done!

Image description

Congratulation on making you’re first chess bot!

Image description

Next steps

Now if you want to improve your chess bot, you can head on over to the chess programming wiki https://www.chessprogramming.org/Main_Page and check out all the ways to improve the bot.

You can also go back through the code and make it more efficient so it’s faster and can have a farther depth.

Conclusion

If you enjoyed this detailed tutorial and interested in more of my stuff, you can check out my website: https://kaipereira.com/ or my Github https://github.com/KaiPereira

Top comments (0)