I recently got into chess and thought it would be fun to look at chess programming while i am it. This post is about python-chess library which provides data structures and APIs for chess board.

Board Link to heading

The standard board positions is represented with Forsyth–Edwards Notation or FEN. The main class is Board which have the initial FEN as show below.

Piece placement data: Each rank is described, starting with rank 8 and ending with rank 1, with a “/” between each one; within each rank, the contents of the squares are described in order from the a-file to the h-file. Each piece is identified by a single letter taken from the standard English names in algebraic notation (pawn = “P”, knight = “N”, bishop = “B”, rook = “R”, queen = “Q” and king = “K”). White pieces are designated using uppercase letters (“PNBRQK”), while black pieces use lowercase letters (“pnbrqk”). A set of one or more consecutive empty squares within a rank is denoted by a digit from “1” to “8”, corresponding to the number of squares.

Active color: “w” means that White is to move; “b” means that Black is to move.

Castling availability: If neither side has the ability to castle, this field uses the character “-”. Otherwise, this field contains one or more letters: “K” if White can castle kingside, “Q” if White can castle queenside, “k” if Black can castle kingside, and “q” if Black can castle queenside. A situation that temporarily prevents castling does not prevent the use of this notation.

En passant target square: This is a square over which a pawn has just passed while moving two squares; it is given in algebraic notation. If there is no en passant target square, this field uses the character “-”. This is recorded regardless of whether there is a pawn in position to capture en passant.[6] An updated version of the spec has since made it so the target square is recorded only if a legal en passant capture is possible, but the old version of the standard is the one most commonly used.[7][8]

Halfmove clock: The number of halfmoves since the last capture or pawn advance, used for the fifty-move rule.[9]

Fullmove number: The number of the full moves. It starts at 1 and is incremented after Black’s move.

From this FEN, it’s white turn with castling is available for both white and black.

>>> import chess
>>> board = chess.Board()
>>> board
Board('rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1')

Without any moves, The positions of board is the initial board state. This is output of print.

>>> print(board)
r n b q k b n r
p p p p p p p p
. . . . . . . .
. . . . . . . .
. . . . . . . .
. . . . . . . .
P P P P P P P P
R N B Q K B N R

There are some useful APIs provided by Board. We can list the legal moves, print status/results of the game.

>>> board.legal_moves
<LegalMoveGenerator at 0x10143f620 (Nh3, Nf3, Nc3, Na3, h3, g3, f3, e3, d3, c3, b3, a3, h4, g4, f4, e4, d4, c4, b4, a4)>

>>> board.result()
'*'

>>> board.is_game_over()
False

Engine Link to heading

Another neat trick is integrating stockfish which is a leading chess engines used by many websites such as chess.com.

import chess.engine
engine_path = "stockfish"
engine = chess.engine.SimpleEngine.popen_uci(engine_path)
info = engine.analyse(board, chess.engine.Limit(time=5.0))
{'depth': 30,
 'hashfull': 989,
 'lowerbound': True,
 'multipv': 1,
 'nodes': 6320080,
 'nps': 1264016,
 'pv': [Move.from_uci('e2e4')],
 'score': PovScore(Cp(+27), WHITE),
 'seldepth': 55,
 'string': 'NNUE evaluation using nn-37f18f62d772.nnue (6MiB, (22528, 128, 15, '
           '32, 1))',
 'tbhits': 0,
 'time': 5.0}

Moves Link to heading

Apparently, There are several formats or protocols for chess representation. UCI is probably the most popular one.

UCI stands for Universal Chess Interface, which is a communication protocol for chess engines to interact with a graphical chess interface. It specifies the commands and responses that the chess engine must support to play chess games.

parse_uci can be used to create uci move object. We can use the move to update the board.

>>> human_move = "e2e4"
... move = board.parse_uci(human_move)
... 
>>> move
Move.from_uci('e2e4')
>>> board.push(move)
>>> print(board)
r n b q k b n r
p p p p p p p p
. . . . . . . .
. . . . . . . .
. . . . P . . .
. . . . . . . .
P P P P . P P P
R N B Q K B N R

Now we have stockfish engine and API to play the game. We can use to play the black by finding the next move and push it into the board.

>>> result = engine.play(board, chess.engine.Limit(time=1.0))
>>> board.push(result.move)
>>> print(board)
r n b q k b n r
p p p p . p p p
. . . . . . . .
. . . . p . . .
. . . . P . . .
. . . . . . . .
P P P P . P P P
R N B Q K B N R

Full Game Link to heading

This is a quick-and-dirty working chess game. It uses stockfish for the black and UCI on the command line for white.

import chess
import chess.engine

def main():
    stockfish_path = "stockfish"
    board = chess.Board()

    with chess.engine.SimpleEngine.popen_uci(stockfish_path) as engine:
        while not board.is_game_over():
            print(board)

            while True:
                human_move = input("Your move (in UCI, e.g. e2e4): ")
                try:
                    move = board.parse_uci(human_move)

                    if move in board.legal_moves:
                        board.push(move)
                        break
                    else:
                        print("Illegal move, try again.")
                except ValueError:
                    print("Invalid move format, try again.")

            if board.is_game_over():
                break

            print("Stockfish is thinking...")
            result = engine.play(board, chess.engine.Limit(time=1.0))
            board.push(result.move)

        print(board)
        print("Game over:", board.result())

if __name__ == "__main__":
    main()