chess-lib 0.1.3

A chess movement generator library.
Documentation
import re
import subprocess
import sys
from typing import List, Tuple, Any

STOCKFISH_MOVES = {}
RECHESS_ENGINE_MOVES = {}

def main():
    # python perft.py <path/to/stockfish> <path/to/rechess-engine> <fen> <depth>
    stockfish_exe_path, rechess_exe_path, fen, depth = cli()

    print(stockfish_exe_path)
    print(rechess_exe_path)
    print(fen)
    print(depth)

    print('Executing stockfish, this may take a while...')
    stockfish_process = stockfish_init(stockfish_exe_path)
    stockfish_perft(stockfish_process, fen, [], int(depth))

    print('Executing rechess, this may take a while...')
    rechess_process = rechess_init(rechess_exe_path)
    rechess_perft(rechess_process, fen, [], int(depth))

    print('-------------------------------------')
    print('Missing in rechess engine:\n')
    compare(STOCKFISH_MOVES, RECHESS_ENGINE_MOVES)

    print('-------------------------------------\n')
    print('Missing in stockfish:\n')
    compare(RECHESS_ENGINE_MOVES, STOCKFISH_MOVES)

    print('-------------------------------------')

    # We could run quit to quit it the intended way, but we just terminate now
    stockfish_process.terminate()
    rechess_process.terminate()


def cli() -> (str, str, str, str):
    stockfish_exe_path = sys.argv[1]
    rechess_exe_path = sys.argv[2]
    fen = sys.argv[3]
    depth = sys.argv[4]

    return stockfish_exe_path, rechess_exe_path, fen, depth


def stockfish_perft(stockfish_process: Any, fen: str, moves: List[str], depth: int):
    if depth == 0:
        return add_to_moves_dict(STOCKFISH_MOVES, moves)

    output = stockfish(stockfish_process, fen, to_moves_str(moves), depth)

    for move in parse_output(output):
        stockfish_perft(stockfish_process, fen, moves + [move[0]], depth - 1)


def rechess_perft(rechess_process: Any, fen: str, moves: List[str], depth: int):
    if depth == 0:
        return add_to_moves_dict(RECHESS_ENGINE_MOVES, moves)

    output = rechess(rechess_process, fen, to_moves_str(moves), depth)

    for move in parse_output(output):
        rechess_perft(rechess_process, fen, moves + [move[0]], depth - 1)


def stockfish_init(stockfish_exe_path: str) -> Any:
    return subprocess.Popen(stockfish_exe_path, universal_newlines=True, stdin=subprocess.PIPE, stdout=subprocess.PIPE)


def stockfish(stockfish_process: Any, fen: str, moves: str, depth: int) -> str:
    command = f'position fen {fen} moves {moves}\ngo perft {depth}\n'
    stockfish_process.stdin.write(command)
    stockfish_process.stdin.flush()

    text = ''
    while True:
        output = stockfish_process.stdout.readline().strip()
        text += output

        if 'Nodes searched' in output:
            break

    return text


def rechess_init(rechess_exe_path: str) -> Any:
    return subprocess.Popen([rechess_exe_path, 'chess', 'interactive'], universal_newlines=True, stdin=subprocess.PIPE, stdout=subprocess.PIPE)


def rechess(rechess_process: Any, fen: str, moves: str, depth: int) -> str:
    command = f'perft depth {depth} position fen {fen} moves {moves}\n'
    rechess_process.stdin.write(command)
    rechess_process.stdin.flush()

    text = ''
    while True:
        output = rechess_process.stdout.readline().strip()
        text += output

        if 'Nodes searched' in output:
            break

    return text


def parse_output(text: str) -> List[Tuple[str, int]]:
    move_count_pattern = r"([a-h][1-8][a-h][1-8][qbkr]?): (\d+)"

    move_count_pairs = re.findall(move_count_pattern, text)

    moves = []
    for move, count in move_count_pairs:
        moves += [(move, count)]

    return moves


def compare(a, b, path: str = ''):
    for key in a.keys():
        nested_path = f'{path} {key}'.strip()
        if key in b:
            compare(a[key], b[key], nested_path)
        else:
            print(f'[!] {nested_path}')


def to_moves_str(moves: List[str]) -> str:
    return ' '.join(moves)


def add_to_moves_dict(dic, moves: List[str]):
    d = dic
    for move in moves[:-1]:
        if move in d:
            d = d[move]
        else:
            d = d.setdefault(move, {})
    d[moves[-1]] = {}


if __name__ == '__main__':
    main()