zhelyabuzhsky / stockfish

Integrates the Stockfish chess engine with Python
https://pypi.python.org/pypi/stockfish
MIT License
278 stars 56 forks source link

FR - Include chess notation entry #110

Open FalcoGer opened 2 years ago

FalcoGer commented 2 years ago

I have written a little script that converts chess notation into valid moves for stockfish.py. Maybe someone can refine it a bit and put it in?

The goal of my implementation wasn't to make it correct but to make it convenient for user entry. But I'm sure it can be adopted to make it more rigid. For example I tried to remove capitalization requirements, but that resulted in only bishops needing to be capitalized to make it work. If you want to be more stringent you could enforce all piece descriptions. The code should be pretty self explanatory, but if you have questions, feel free to ask.

Here's a brief overview The function will take the stockfish board, a move string in chess notation and a color argument. The color argument, when set to not False will output messages in color. It is not important for this function for the purpose of translating chess notation to stockfish notation. It is there because in my program whenever something is referenced to one side or another, I use the colored library to provide colored text on the terminal, in this case for the returned error messages. It will either return a valid move, or an error message.

See a working example here: https://github.com/FalcoGer/pychess but you might have to adjust the stockfish path. Feel free to take any code you want from that.

class Player:
    BLACK = "Black"
    WHITE = "White"

def getNTM(stockfish: Stockfish) -> str:
    return Player.WHITE if (stockfish.get_fen_position().find(" w ") > 0) else Player.BLACK

def resolveMove(stockfish: Stockfish, move: str, color: bool = True) -> str:
    # turn shorthand notation into valid moves for stockfish
    # "e4" = pawn to e4
    # "xe4" = pawn captures e4
    # "bxc5" = pawn in b file captures c5
    # "Bxc5" = bishop captures c5
    # "Bbxc5" = bishop in b file captures c5 (because another bishop could also capture c5)
    # "B3xd4" = bishop from rank 3 captures d4 (because another bishop from the same file could also capture)
    # "ra6" = rook to a6
    # "rfa6" = rook in f file (f6) to a6
    # "qxf4" = queen captures f4
    # "d2d1q" = pawn from d2 to d1 turns into a queen
    # "d2xe1q" = pawn from d2 captures e1 and turns into a queen
    move = move.replace(" ", "").replace("-", "")
    ntm = getNTM(stockfish)
    if len(move) == 0:
        return "Empty move"
    if regex.match("^(?:[a-h][1-8]){2}[qrnb]$", move.lower()):
        move = move.lower()
        if stockfish.is_move_correct(move):
            return move
        else:
            return "Invalid move."
    else:
        # castling
        if (move.lower() == 'oo'):
            # castle king side
            # need to check if it's actually a king there as another piece could also have a valid move.
            if ntm == Player.WHITE and stockfish.get_what_is_on_square("e1") == Stockfish.Piece.WHITE_KING:
                move = "e1g1"
            elif ntm == Player.BLACK and stockfish.get_what_is_on_square("e8") == Stockfish.Piece.BLACK_KING:
                move = "e8g8"
            else:
                move = "Invalid"
            if stockfish.is_move_correct(move):
                return move
            else:
                return "Can not castle king side."
        elif (move.lower() == 'ooo'):
            # castle queen side
            if ntm == Player.WHITE and stockfish.get_what_is_on_square("e1") == Stockfish.Piece.WHITE_KING:
                move = "e1c1"
            elif ntm == Player.BLACK and stockfish.get_what_is_on_square("e8") == Stockfish.Piece.BLACK_KING:
                move = "e8c8"
            else:
                move = "Invalid"
            if stockfish.is_move_correct(move):
                return move
            else:
                return "Can not castle queen side."

        # resolve the rest with regex
        # do not allow lower case 'b' in first group because it conflicts with second group
        # allow other lower case letters for convenience
        match = regex.match("^([RNBKQrnkq]?)([a-h]?)([1-8]?)(x?)([a-h][1-8])(=?[RNBKQrnbkq]?)$", move)
        if match == None:
            return "Not a valid move string."
        groups = match.groups()
        piece = None

        # resolve piece class
        if len(groups[0]) == 0:
            piece = Stockfish.Piece.WHITE_PAWN if ntm == Player.WHITE else Stockfish.Piece.BLACK_PAWN
        else:
            if groups[0].lower() == 'r':
                piece = Stockfish.Piece.WHITE_ROOK if ntm == Player.WHITE else Stockfish.Piece.BLACK_ROOK
            elif groups[0] == 'B': # bxc6 is a pawn from b, not a bishop.
                piece = Stockfish.Piece.WHITE_BISHOP if ntm == Player.WHITE else Stockfish.Piece.BLACK_BISHOP
            elif groups[0].lower() == 'n':
                piece = Stockfish.Piece.WHITE_KNIGHT if ntm == Player.WHITE else Stockfish.Piece.BLACK_KNIGHT
            elif groups[0].lower() == 'k':
                piece = Stockfish.Piece.WHITE_KING if ntm == Player.WHITE else Stockfish.Piece.BLACK_KING
            elif groups[0].lower() == 'q':
                piece = Stockfish.Piece.WHITE_QUEEN if ntm == Player.WHITE else Stockfish.Piece.BLACK_QUEEN
            else:
                return f"Can not determine piece to move ('{groups[0]}')."

        # resolve source file
        src_file = None
        if len(groups[1]) == 1:
            src_file = groups[1]

        # resolve source rank
        src_rank = None
        if len(groups[2]) == 1:
            src_rank = groups[2]

        # resolve capture
        isCapture = groups[3] == 'x'

        # pawn conversion
        turnsInto = groups[5].lstrip('=')

        # resolve dst
        dst = groups[4]

        # resolve src
        src = None
        # find src
        if src_file != None and src_rank != None:
            src = f"{src_file}{src_rank}"
        else:
            possibleSrc = []
            # run through all the squares and check all the pieces if they can move to the square
            for file in range(ord('a'), ord('h') + 1):
                file = chr(file)
                if src_file != None and src_file != file:
                    continue
                for rank in range(1,8+1):
                    rank = str(rank)
                    if src_rank != None and src_rank != rank:
                        continue
                    src = f"{file}{rank}"
                    if piece == stockfish.get_what_is_on_square(src) and stockfish.is_move_correct(f"{src}{dst}{turnsInto}"):
                        possibleSrc.append(src)
            if len(possibleSrc) == 1:
                src = possibleSrc[0]
            elif len(possibleSrc) == 0:
                pieceDesc = str(piece).replace("Piece.", "")
                if color:
                    pieceDesc = colored(pieceDesc, color=ColorConst.WHITE_PIECE if ntm == Player.WHITE else ColorConst.BLACK_PIECE, on_color=ColorConst.FEN_BG, attrs=['bold'])
                if src_rank != None and src_file == None:
                    pieceDesc = pieceDesc + f" from rank {src_rank}"
                elif src_rank == None and src_file != None:
                    pieceDesc = pieceDesc + f" from file {src_file}"
                # no need to check for both since that is already covered above
                # no need to check for neither since no additional description is needed
                return f"No {pieceDesc} can go to {dst}"
            else:
                pieceDesc = str(piece).replace("Piece.", "")
                if color:
                    pieceDesc = colored(pieceDesc, color=ColorConst.WHITE_PIECE if ntm == Player.WHITE else ColorConst.BLACK_PIECE, on_color=ColorConst.FEN_BG, attrs=['bold'])

                return f"Could not determine which {pieceDesc} you want to move to {dst}"
        # build stockfish move
        move = f"{src}{dst}{turnsInto}"
        # check if resolved move is indeed a capture
        if stockfish.is_move_correct(move):
            if ((isCapture and turnsInto != '' and stockfish.get_what_is_on_square(dst) == None) or (isCapture and turnsInto == '' and stockfish.will_move_be_a_capture(move) ==Stockfish.Capture.NO_CAPTURE)):
                return "Move is no Capture"
            elif (not isCapture and turnsInto != '' and stockfish.get_what_is_on_square(dst) != None) or (not isCapture and turnsInto == '' and stockfish.will_move_be_a_capture(move) != Stockfish.Capture.NO_CAPTURE):
                print("Warning: Move results in a capture, but capture was not indicated by the move string.")
            return move
    return "Invalid Move"
johndoknjas commented 2 years ago

Hi, thanks for this - I think going from human notation --> stockfish notation (and the other way around) would be a nice feature to add. I can definitely get started on including this for the library, but are you sure you don't want to make the PR? It's quite a bit of work you've done here, and I doubt I would be modifying much.

kieferro commented 1 year ago

Hello, this project is no longer maintained in this repo but on this fork. (For more information about this please look here).

Because there is an associated PR, there is probably no need to transfer this issue or create a proxy issue. But please look at the other repo for updates on this