Example #1
0
    def __generate_chess_moves_tree(self, parent_node, chess_board: chess.Board, depth):
        player_color = 'black' if parent_node['player_color'] == 'white' else 'white'
        
        if chess_board.legal_moves.count == 0:
            ## parent'ın yapabilecegi hamle yok demek
            ## parent final node demek, o yüzden bu node icin gidip value hesaplanacak
            board_fen = parent_node['board_fen']
            piece_map = chess_board.piece_map()
            value = self.__evaluate_chess_board(board_fen, chess_board.is_check(), parent_node['player_color'], piece_map)
            parent_node['value'] = value


        if depth == self.__max_depth: ## buna benzer bir islemi eger herhangi bir legal moves yoksa da yapmak lazim ...
            for possible_move in list(chess_board.legal_moves):
                chess_board.push(possible_move)
                board_fen = chess_board.board_fen()
                piece_map = chess_board.piece_map()                
                child = self.__create_node(possible_move, board_fen, player_color, depth)
                parent_node['children'].append(child)
                # her bir child icin value hesapla
                value = self.__evaluate_chess_board(board_fen, chess_board.is_check(), player_color, piece_map)
                child['value'] = value
                chess_board.pop()

            return    
    
        for possible_move in list(chess_board.legal_moves):
            chess_board.push(possible_move)
            board_fen = chess_board.board_fen()            
            child = self.__create_node(possible_move, board_fen, player_color, depth)
            parent_node['children'].append(child)
            self.__generate_chess_moves_tree(child, chess_board, depth + 1)
            chess_board.pop()
Example #2
0
def evaluate_board(board: chess.Board, depth: int, alpha: int, beta: int):
    # return if max depth reached
    if depth == 0:
        evaluation = evaluate_position(board)
        return evaluation

    if board.is_repetition(3):
        return 0

    moves = board.legal_moves

    if moves.count() == 0:
        if board.is_check():
            return get_negative_infinity()

        return 0

    for move in moves:

        board.push(move)
        move_evaluation = -evaluate_board(board, depth - 1, -beta, -alpha)
        board.pop()

        #print(f'Depth: {depth}, Move: {move}, Evaluation: {move_evaluation}, Beta: {beta}, Alpha: {alpha}')

        if move_evaluation >= beta:
            return beta

        if move_evaluation > alpha:
            alpha = move_evaluation

    return alpha
Example #3
0
def make_move(board: chess.Board, time_remaining: float) -> str:
    """
    `board` gives the current board state from which you should make your move.
    See https://python-chess.readthedocs.io/en/v1.4.0/ for full documentation,
    or below for some example usage.

    `time_remaining` gives the number of seconds left on your chess clock.
    You should return a uci-formatted move representing the move that your AI
    wishes to make.

    For example, to move your pawn from e2 to e4, you should return the string 'e2e4'.

    If you make an invalid move, or if your chess clock times out, you forfeit the game.

    Note that you are passed a copy of the board object, so methods such as `board.reset()`
    will not change the master copy, only your local version.
    """

    # Get some interesting information
    playing_white = board.turn == chess.WHITE
    legal_moves = list(board.legal_moves)
    am_i_in_check = board.is_check()

    # TODO: Figure out some clever logic to make a good move
    move = random.choice(legal_moves)

    # Return the code of the move
    return move.uci()
Example #4
0
 def render_board(board: chess.Board) -> BytesIO:
     boardimg = chess.svg.board(
         board=board,
         lastmove=board.peek() if board.move_stack else None,
         check=board.king(turn)
         if board.is_check() or board.is_checkmate() else None,
         flipped=board.turn == chess.BLACK)
     res = BytesIO()
     svg2png(bytestring=boardimg, write_to=res)
     res.seek(0)
     return res
Example #5
0
def evaluate_game_state(board: chess.Board, possible: bool) -> float:
    if board.is_checkmate():
        if possible:
            return Macros.BOARD_POSSIBLE_CHECKMATE_VALUE
        return Macros.BOARD_CHECKMATE_VALUE

    if board.is_stalemate() or board.is_insufficient_material():
        return Macros.BOARD_STALEMATE_VALUE

    if board.is_check():
        if not possible:
            return Macros.BOARD_CHECK_VALUE

    return 0
Example #6
0
def to_png(board: chess.Board, size: int = 400) -> discord.File:
    args = {"size": size}

    try:
        lastmove = board.peek()
        args["lastmove"] = lastmove

        if board.is_check():
            args["check"] = board.king(board.turn)
    except IndexError:
        pass

    svg_image = chess.svg.board(board, **args)
    png = cairosvg.svg2png(bytestring=svg_image.encode("UTF-8"))
    return discord.File(io.BytesIO(png), filename="board.png")
Example #7
0
    def write_json(self, board: chess.Board):
        """
        Writes all of the board info in json
        """

        best_move = self.get_best_move(board)

        output = OrderedDict([
            ('fen', board.fen()),
            ('fullmoveNumber', board.fullmove_number),
            ('result', board.result()),
            ('isGameOver', board.is_game_over()),
            ('isCheckmate', board.is_checkmate()),
            ('isStalemate', board.is_stalemate()),
            ('isInsufficientMaterial', board.is_insufficient_material()),
            ('isSeventyfiveMoves', board.is_seventyfive_moves()),
            ('isFivefoldRepetition', board.is_fivefold_repetition()),
            ('white',
             OrderedDict([
                 ('hasKingsideCastlingRights',
                  board.has_kingside_castling_rights(chess.WHITE)),
                 ('hasQueensideCastlingRights',
                  board.has_queenside_castling_rights(chess.WHITE)),
             ])),
            ('black',
             OrderedDict([
                 ('hasKingsideCastlingRights',
                  board.has_kingside_castling_rights(chess.BLACK)),
                 ('hasQueensideCastlingRights',
                  board.has_queenside_castling_rights(chess.BLACK)),
             ])),
            ('turn',
             OrderedDict([
                 ('color', 'white' if board.turn is chess.WHITE else 'black'),
                 ('isInCheck', board.is_check()),
                 ('bestMove', best_move),
                 ('legalMoves', [move.uci() for move in board.legal_moves]),
                 ('canClaimDraw', board.can_claim_draw()),
                 ('canClaimFiftyMoves', board.can_claim_fifty_moves()),
                 ('canClaimThreefoldRepetition',
                  board.can_claim_threefold_repetition()),
             ])),
        ])

        self.finish(output)
Example #8
0
def is_trapped(board: Board, square: Square) -> bool:
    if board.is_check() or board.is_pinned(board.turn, square):
        return False
    piece = board.piece_at(square)
    if piece.piece_type in [PAWN, KING]:
        return False
    if not is_in_bad_spot(board, square):
        return False
    for escape in board.legal_moves:
        if escape.from_square == square:
            capturing = board.piece_at(escape.to_square)
            if capturing and values[capturing.piece_type] >= values[piece.piece_type]:
                return False
            board.push(escape)
            if not is_in_bad_spot(board, escape.to_square):
                return False
            board.pop()
    return True
Example #9
0
def evaluate(board: chess.Board):

    ENEMY = {chess.WHITE: chess.BLACK, chess.BLACK: chess.WHITE}

    score = 0

    if board.result() == "1-0" or board.result() == "0-1":
        score += 9999

    if board.is_check():
        score += 10

    for sq in chess.SQUARES:
        score += len(board.attackers(board.turn,
                                     sq)) * VALUE[board.piece_type_at(sq)]
        score -= len(board.attackers(ENEMY[board.turn],
                                     sq)) * VALUE[board.piece_type_at(sq)]

    return score
Example #10
0
def evaluate(board: chess.Board, evaluator: Callable[[float, float, float],
                                                     float]):
    # for every piece that is still alive award player 100 points
    result = board.legal_moves.count() * 100

    # being in check usually means we are going the wrong route
    if board.is_check():
        result -= 1000

    if board.has_insufficient_material(chess.WHITE):
        result -= 1000

    # add material advantage that white has
    result += diff_pieces(board)

    # if on white then return as normal since white Maximizes
    if board.turn is chess.WHITE:
        return result
    # if black then negate for minimization
    else:
        return -result
Example #11
0
    def evaluate(self, board: chess.Board, player: bool):
        turn = board.turn
        if board.is_checkmate():
            return -10000 if turn == player else 10000
        if board.is_check():
            return -5000 if turn == player else 5000

        material_sum = 0
        weights = {
            'p': 100,
            'n': 320,
            'b': 330,
            'r': 500,
            'q': 900,
            'k': 20000
        }
        for i in range(64):
            piece = board.piece_at(i)
            if piece:
                color_weight = 1 if piece.color == player else -1
                material_sum += weights[piece.symbol().lower()] * color_weight
        return material_sum
Example #12
0
def evaluateFuzzy(board: chess.Board, evaluator: Callable[[float, float],
                                                          float]):
    player = chess.WHITE if board.turn == True else chess.BLACK
    opponent = chess.WHITE if player == chess.BLACK else chess.BLACK

    total = 0
    # being in check usually means we are going the wrong route
    if board.is_check():
        total -= 1000

    if board.has_insufficient_material(player):
        total -= 1000

    if board.has_insufficient_material(opponent):
        total += 1000

    num_moves_by_piece = {}
    for move in board.legal_moves:
        if move.from_square in num_moves_by_piece:
            num_moves_by_piece[move.from_square] += 1
        else:
            num_moves_by_piece[move.from_square] = 1

    for i in range(1, 7):
        piece_type = board.pieces(i, player)
        num_attacking = 0
        for piece in piece_type:
            movable_spaces = num_moves_by_piece[
                piece] if piece in num_moves_by_piece else 0
            for spot in board.attacks(piece):
                attacked_player = board.color_at(spot)
                if attacked_player == opponent:
                    num_attacking += 1
            total += i + evaluator(movable_spaces, num_attacking)

    return total
    def say_last_move(game: chess.Board):
        """Take a chess.BitBoard instance and speaks the last move from it."""
        move_parts = {
            'K': 'king.ogg',
            'B': 'bishop.ogg',
            'N': 'knight.ogg',
            'R': 'rook.ogg',
            'Q': 'queen.ogg',
            'P': 'pawn.ogg',
            '+': '',
            '#': '',
            'x': 'takes.ogg',
            '=': 'promote.ogg',
            'a': 'a.ogg',
            'b': 'b.ogg',
            'c': 'c.ogg',
            'd': 'd.ogg',
            'e': 'e.ogg',
            'f': 'f.ogg',
            'g': 'g.ogg',
            'h': 'h.ogg',
            '1': '1.ogg',
            '2': '2.ogg',
            '3': '3.ogg',
            '4': '4.ogg',
            '5': '5.ogg',
            '6': '6.ogg',
            '7': '7.ogg',
            '8': '8.ogg'
        }

        bit_board = game.copy()
        move = bit_board.pop()
        san_move = bit_board.san(move)
        voice_parts = []

        if san_move.startswith('O-O-O'):
            voice_parts += ['castlequeenside.ogg']
        elif san_move.startswith('O-O'):
            voice_parts += ['castlekingside.ogg']
        else:
            for part in san_move:
                try:
                    sound_file = move_parts[part]
                except KeyError:
                    logging.warning('unknown char found in san: [%s : %s]', san_move, part)
                    sound_file = ''
                if sound_file:
                    voice_parts += [sound_file]

        if game.is_game_over():
            if game.is_checkmate():
                wins = 'whitewins.ogg' if game.turn == chess.BLACK else 'blackwins.ogg'
                voice_parts += ['checkmate.ogg', wins]
            elif game.is_stalemate():
                voice_parts += ['stalemate.ogg']
            else:
                if game.is_seventyfive_moves():
                    voice_parts += ['75moves.ogg', 'draw.ogg']
                elif game.is_insufficient_material():
                    voice_parts += ['material.ogg', 'draw.ogg']
                elif game.is_fivefold_repetition():
                    voice_parts += ['repetition.ogg', 'draw.ogg']
                else:
                    voice_parts += ['draw.ogg']
        elif game.is_check():
            voice_parts += ['check.ogg']

        if bit_board.is_en_passant(move):
            voice_parts += ['enpassant.ogg']

        return voice_parts
Example #14
0
    def say_last_move(game: chess.Board):
        """Take a chess.BitBoard instance and speaks the last move from it."""

        PicoTalkerDisplay.c_taken = False  #molli
        PicoTalkerDisplay.c_castle = False  #molli
        PicoTalkerDisplay.c_knight = False  #molli
        PicoTalkerDisplay.c_rook = False  #molli
        PicoTalkerDisplay.c_king = False  #molli
        PicoTalkerDisplay.c_bishop = False  #molli
        PicoTalkerDisplay.c_pawn = False  #molli
        PicoTalkerDisplay.c_queen = False  #molli
        PicoTalkerDisplay.c_check = False  #molli
        PicoTalkerDisplay.c_mate = False  #molli
        PicoTalkerDisplay.c_stalemate = False  #molli
        PicoTalkerDisplay.c_draw = False  #molli

        move_parts = {
            'K': 'king.ogg',
            'B': 'bishop.ogg',
            'N': 'knight.ogg',
            'R': 'rook.ogg',
            'Q': 'queen.ogg',
            'P': 'pawn.ogg',
            '+': '',
            '#': '',
            'x': 'takes.ogg',
            '=': 'promote.ogg',
            'a': 'a.ogg',
            'b': 'b.ogg',
            'c': 'c.ogg',
            'd': 'd.ogg',
            'e': 'e.ogg',
            'f': 'f.ogg',
            'g': 'g.ogg',
            'h': 'h.ogg',
            '1': '1.ogg',
            '2': '2.ogg',
            '3': '3.ogg',
            '4': '4.ogg',
            '5': '5.ogg',
            '6': '6.ogg',
            '7': '7.ogg',
            '8': '8.ogg'
        }

        bit_board = game.copy()
        move = bit_board.pop()
        san_move = bit_board.san(move)
        voice_parts = []

        if san_move.startswith('O-O-O'):
            voice_parts += ['castlequeenside.ogg']
            PicoTalkerDisplay.c_castle = True
        elif san_move.startswith('O-O'):
            voice_parts += ['castlekingside.ogg']
            PicoTalkerDisplay.c_castle = True
        else:
            for part in san_move:
                try:
                    sound_file = move_parts[part]
                except KeyError:
                    logging.warning('unknown char found in san: [%s : %s]',
                                    san_move, part)
                    sound_file = ''
                if sound_file:
                    voice_parts += [sound_file]
                    if sound_file == 'takes.ogg':
                        PicoTalkerDisplay.c_taken = True  #molli
                    elif sound_file == 'knight.ogg':
                        PicoTalkerDisplay.c_knight = True  #molli
                    elif sound_file == 'king.ogg':
                        PicoTalkerDisplay.c_king = True  #molli
                    elif sound_file == 'rook.ogg':
                        PicoTalkerDisplay.c_rook = True  #molli
                    elif sound_file == 'pawn.ogg':
                        PicoTalkerDisplay.c_pawn = True  #molli
                    elif sound_file == 'bishop.ogg':
                        PicoTalkerDisplay.c_bishop = True  #molli
                    elif sound_file == 'queen.ogg':
                        PicoTalkerDisplay.c_queen = True  #molli

        if game.is_game_over():
            if game.is_checkmate():
                wins = 'whitewins.ogg' if game.turn == chess.BLACK else 'blackwins.ogg'
                voice_parts += ['checkmate.ogg', wins]
                PicoTalkerDisplay.c_mate = True
            elif game.is_stalemate():
                voice_parts += ['stalemate.ogg']
                PicoTalkerDisplay.c_stalemate = True
            else:
                PicoTalkerDisplay.c_draw = True
                if game.is_seventyfive_moves():
                    voice_parts += ['75moves.ogg', 'draw.ogg']
                elif game.is_insufficient_material():
                    voice_parts += ['material.ogg', 'draw.ogg']
                elif game.is_fivefold_repetition():
                    voice_parts += ['repetition.ogg', 'draw.ogg']
                else:
                    voice_parts += ['draw.ogg']
        elif game.is_check():
            voice_parts += ['check.ogg']
            PicoTalkerDisplay.c_check = True

        if bit_board.is_en_passant(move):
            voice_parts += ['enpassant.ogg']

        return voice_parts
def featureEngineering(id, game, game_collection):
    counter = 0
    update_dict = {}

    first_check = True
    first_queen_move = True

    features_dict = defaultdict(int)
    # {
    #     'promotion': 0,
    #     'total_checks': 0,
    #     'is_checkmate': 0,
    #     'is_stalemate': 0,
    #     'insufficient_material': 0,
    #     'can_claim_draw': 0
    # }

    board = None
    for moveDBObject in game['moves']:
        # print(f"\tUpdating {move['uci']} -> {move['turn']}")
        board = Board(moveDBObject['fen'])
        uci = moveDBObject['uci']
        move = Move.from_uci(uci)

        moved_piece = board.piece_at(move.from_square)
        captured_piece = board.piece_at(move.to_square)

        if moved_piece == QUEEN and first_queen_move:
            features_dict['queen_moved_at'] = board.fullmove_number
            first_queen_move = False

        if captured_piece == QUEEN:
            features_dict['queen_changed_at'] = board.fullmove_number

        if move.promotion:
            features_dict['promotion'] += 1
        if board.is_check():
            features_dict['total_checks'] += 1
            if first_check:
                features_dict['first_check_at'] = board.fullmove_number
                first_check = False

                # castling
                if uci == 'e1g1':
                    features_dict['white_king_castle'] = board.fullmove_number
                elif uci == 'e1c1':
                    features_dict['white_queen_castle'] = board.fullmove_number
                elif uci == 'e8g8':
                    features_dict['black_king_castle'] = board.fullmove_number
                elif uci == 'e8c8':
                    features_dict['black_queen_castle'] = board.fullmove_number

        # update_dict[f"moves.{counter}.turn"] = counter % 2 == 0
        counter += 1

    if counter > 0:
        if board.is_checkmate():
            features_dict['is_checkmate'] += 1
        if board.is_stalemate():
            features_dict['is_stalemate'] += 1
        if board.is_insufficient_material():
            features_dict['insufficient_material'] += 1
        if board.can_claim_draw():
            features_dict['can_claim_draw'] += 1
        features_dict['total_moves'] = board.fullmove_number

        piece_placement = board.fen().split()[0]
        end_pieces = Counter(x for x in piece_placement if x.isalpha())
        # count number of piece at end position
        features_dict.update(
            {'end_' + piece: cnt
             for piece, cnt in end_pieces.items()})

        # print(features_dict)
        game_collection.update_one({"_id": ObjectId(id)},
                                   {"$set": features_dict},
                                   upsert=False)
Example #16
0
class Viridithas():
    def __init__(
        self,
        human: bool = False,
        fen: str = 'rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1',
        pgn: str = '',
        time_limit: float = 15,
        fun: bool = False,
        contempt: int = 3000,
        book: bool = True,
        advancedTC: list = [],
    ):
        if pgn == '':
            self.node = Board(fen)
        else:
            self.node = Board()
            for move in pgn.split():
                try:
                    self.node.push_san(move)
                except Exception:
                    continue
        self.time_limit = time_limit
        if advancedTC:
            self.endpoint = time.time()+advancedTC[0]*60
            self.increment = advancedTC[1]
        else:
            self.endpoint = 0
            self.increment = 0
        self.fun = fun
        self.contempt = contempt
        self.human = human
        self.nodes = 0
        self.advancedTC = advancedTC
        self.hashtable: dict[Hashable, TTEntry] = dict()
        self.inbook = book

    def set_position(self, fen):
        self.node = Board(fen)

    def __repr__(self) -> str:
        return str(self.node) + '\n' + self.__class__.__name__+"-engine at position " + str(self.node.fen())

    def __str__(self) -> str:
        return self.__class__.__name__

    def user_setup(self):
        if input("Do you want to configure the bot? (Y/N) ").upper() != 'Y':
            return

        myname = self.__class__.__name__.upper()

        print(f"BEGINNING USER CONFIGURATION OF {myname}-BOT")

        datadict = get_engine_parameters()

        self.__init__(
            human=datadict["human"],
            fen=datadict["fen"],
            pgn=datadict["pgn"],
            time_limit=datadict["time_limit"],
            fun=datadict["fun"],
            contempt=datadict["contempt"],
            book=datadict["book"],
            advancedTC=datadict["advancedTC"]
        )

    def gameover_check_info(self):
        checkmate = self.node.is_checkmate()
        draw = self.node.is_stalemate() or \
            self.node.is_insufficient_material( ) or \
            self.node.is_repetition(2) or self.node.is_seventyfive_moves() or not any(self.node.generate_legal_moves())
        return checkmate or draw, checkmate, draw

    # @profile
    def evaluate(self, depth: float, checkmate: bool, draw: bool) -> float:
        self.nodes += 1

        if checkmate:
            return MATE_VALUE * int(max(depth+1, 1)) * (1 if self.node.turn else -1)
        if draw:
            return -self.contempt * (1 if self.node.turn else -1)

        rating: float = 0

        rating += pst_eval(self.node)
        # rating += see_eval(self.node)

        # rating += mobility(self.node) * MOBILITY_FACTOR
        
        # rating += piece_attack_counts(self.node) * ATTACK_FACTOR

        # rating += king_safety(self.node) * KING_SAFETY_FACTOR

        # rating += space(self.node) * SPACE_FACTOR

        return rating

    def single_hash_iterator(self, best):
        yield best

    def captures_piece(self, p):  # concentrate on MVV, then LVA
        return itertools.chain(
            self.node.generate_pseudo_legal_moves(self.node.pawns, p),
            self.node.generate_pseudo_legal_moves(self.node.knights, p),
            self.node.generate_pseudo_legal_moves(self.node.bishops, p),
            self.node.generate_pseudo_legal_moves(self.node.rooks, p),
            self.node.generate_pseudo_legal_moves(self.node.queens, p),
            self.node.generate_pseudo_legal_moves(self.node.kings, p),
        )

    #@profile
    def captures(self):  # (MVV/LVA)
        return (m for m in itertools.chain(
            self.captures_piece(
                self.node.occupied_co[not self.node.turn] & self.node.queens),
            self.captures_piece(
                self.node.occupied_co[not self.node.turn] & self.node.rooks),
            self.captures_piece(
                self.node.occupied_co[not self.node.turn] & self.node.bishops),
            self.captures_piece(
                self.node.occupied_co[not self.node.turn] & self.node.knights),
            self.captures_piece(
                self.node.occupied_co[not self.node.turn] & self.node.pawns),
        ) if self.node.is_legal(m))

    def winning_captures(self):  # (MVV/LVA)
        target_all = self.node.occupied_co[not self.node.turn]
        target_3 = target_all & ~self.node.pawns
        target_5 = target_3 & (~self.node.bishops | ~self.node.knights)
        target_9 = target_5 & ~self.node.rooks
        return itertools.chain(
            self.node.generate_pseudo_legal_moves(self.node.pawns, target_all),
            self.node.generate_pseudo_legal_moves(self.node.knights, target_3),
            self.node.generate_pseudo_legal_moves(self.node.bishops, target_3),
            self.node.generate_pseudo_legal_moves(self.node.rooks, target_5),
            self.node.generate_pseudo_legal_moves(self.node.queens, target_9),
            self.node.generate_pseudo_legal_moves(self.node.kings, target_9),
        )

    def losing_captures(self):  # (MVV/LVA)
        target_pawns = self.node.pawns
        target_pnb = target_pawns | self.node.bishops | self.node.knights
        target_pnbr = target_pnb | self.node.rooks
        return itertools.chain(
            self.node.generate_pseudo_legal_moves(self.node.knights, target_pawns),
            self.node.generate_pseudo_legal_moves(self.node.bishops, target_pawns),
            self.node.generate_pseudo_legal_moves(self.node.rooks, target_pnb),
            self.node.generate_pseudo_legal_moves(self.node.queens, target_pnbr),
            self.node.generate_pseudo_legal_moves(self.node.kings, target_pnbr),
        )

    def ordered_moves(self):
        return (m for m in itertools.chain(
            # self.winning_captures(),
            self.captures_piece(
                self.node.occupied_co[not self.node.turn] & self.node.queens),
            self.captures_piece(
                self.node.occupied_co[not self.node.turn] & self.node.rooks),
            self.captures_piece(
                self.node.occupied_co[not self.node.turn] & self.node.bishops),
            self.captures_piece(
                self.node.occupied_co[not self.node.turn] & self.node.knights),
            self.captures_piece(
                self.node.occupied_co[not self.node.turn] & self.node.pawns),
            self.node.generate_pseudo_legal_moves(
                0xffff_ffff_ffff_ffff, ~self.node.occupied_co[not self.node.turn]),
            # self.losing_captures()
        ) if self.node.is_legal(m))

    def pass_turn(self) -> None:
        self.node.push(Move.from_uci("0000"))

    #@profile
    def qsearch(self, alpha: float, beta: float, depth: float, colour: int, gameover: bool, checkmate: bool, draw: bool) -> float:

        val = self.evaluate(1, checkmate, draw) * colour
        if gameover:
            return val
        if val >= beta:
            return beta
        if (val < alpha - QUEEN_VALUE):
            return alpha
        
        alpha = max(val, alpha)

        for capture in self.captures():
            self.node.push(capture)
            gameover, checkmate, draw = self.gameover_check_info()
            score = -self.qsearch(-beta, -alpha, depth - 1, -colour, gameover, checkmate, draw)
            self.node.pop()
            if score >= beta:
                return score
            alpha = max(score, alpha)

        return alpha

    def tt_lookup(self, board: Board) -> "TTEntry":
        key = board._transposition_key()
        return self.hashtable.get(key, TTEntry.default())

    def tt_store(self, board: Board, entry: TTEntry):
        key = board._transposition_key()    
        self.hashtable[key] = entry

    #@profile
    def negamax(self, depth: float, colour: int, alpha: float, beta: float) -> float:
        initial_alpha = alpha

        # (* Transposition Table Lookup; self.node is the lookup key for ttEntry *)
        tt_entry = self.tt_lookup(self.node)
        if not tt_entry.is_null() and tt_entry.depth >= depth:
            if tt_entry.type == EXACT:
                return tt_entry.value
            elif tt_entry.type == LOWERBOUND:
                alpha = max(alpha, tt_entry.value)
            elif tt_entry.type == UPPERBOUND:
                beta = min(beta, tt_entry.value)

            if alpha >= beta:
                return tt_entry.value
            
            if self.node.is_legal(tt_entry.best):
                moves = itertools.chain([tt_entry.best], filter(lambda x: x != tt_entry.best, self.ordered_moves()))
            else:
                moves = self.ordered_moves()
        else:
            moves = self.ordered_moves()

        gameover, checkmate, draw = self.gameover_check_info()

        if gameover:
            return colour * self.evaluate(depth, checkmate, draw)

        if depth < 1:
            return self.qsearch(alpha, beta, depth, colour, gameover, checkmate, draw)

        current_pos_is_check = self.node.is_check()
        if not current_pos_is_check and depth >= 3 and abs(alpha) < MATE_VALUE and abs(beta) < MATE_VALUE:
            # MAKE A NULL MOVE
            self.node.push(Move.null())  
            # PERFORM A LIMITED SEARCH
            value = - self.negamax(depth - 3, -colour, -beta, -beta + 1)
            # UNMAKE NULL MOVE
            self.node.pop()

            if value >= beta:
                return beta

        do_prune = self.pruning(depth, colour, alpha, beta, current_pos_is_check)

        best_move = Move.null()
        search_pv = True
        value = float("-inf")
        for move_idx, move in enumerate(moves):
            if move_idx == 0:
                best_move = move
            gives_check = self.node.gives_check(move)
            is_capture = self.node.is_capture(move)
            is_promo = bool(move.promotion)
            depth_reduction = search_reduction_factor(
                move_idx, current_pos_is_check, gives_check, is_capture, is_promo, depth)
                
            if do_prune:
                if not gives_check and not is_capture: 
                    continue

            self.node.push(move)
            
            if search_pv:
                r = -self.negamax(depth - depth_reduction, -colour, -beta, -alpha)
                value = max(value, r)
            else:
                r = -self.negamax(depth - depth_reduction, -colour, -alpha-1, -alpha)
                if (r > alpha): # // in fail-soft ... & & value < beta) is common
                    r = -self.negamax(depth - depth_reduction, -colour, -beta, -alpha) #// re-search
                value = max(value, r)

            self.node.pop()

            if value > alpha:
                alpha = value
                best_move = move
            alpha = max(alpha, value)
            if alpha >= beta:
                break
            search_pv = False

        # (* Transposition Table Store; self.node is the lookup key for ttEntry *)
        # ttEntry = TTEntry()
        tt_entry.value = value
        if value <= initial_alpha:
            tt_entry.type = UPPERBOUND
        elif value >= beta:
            tt_entry.type = LOWERBOUND
        else:
            tt_entry.type = EXACT
        tt_entry.depth = depth
        tt_entry.best = best_move
        tt_entry.null = False
        self.tt_store(self.node, tt_entry)

        return value

    def pruning(self, depth, colour, alpha, beta, current_pos_is_check):
        if not (not current_pos_is_check and abs(
                alpha) < MATE_VALUE / 2 and abs(beta) < MATE_VALUE / 2) or depth > 2:
            return False

        see = see_eval(self.node) * colour

        DO_D1_PRUNING = depth <= 1 and see + FUTILITY_MARGIN < alpha

        DO_D2_PRUNING = depth <= 2 and see + FUTILITY_MARGIN_2 < alpha

        return DO_D1_PRUNING or DO_D2_PRUNING

    def move_sort(self, moves: list, ratings: list):
        pairs = zip(*sorted(zip(moves, ratings), key=operator.itemgetter(1)))
        moves, ratings = [list(pair) for pair in pairs]
        return moves, ratings

    def pv_string(self):
        count = 0
        moves = []
        while True:
            e = self.tt_lookup(self.node)
            if e.is_null() or not self.node.is_legal(e.best):
                break

            # print(self.node.__str__())
            # print(self.node.__repr__())
            # print(e.best)
            # print(self.node.san(e.best))

            moves.append(self.node.san(e.best))
            self.node.push(e.best)
            count += 1
        
        for _ in moves:
            self.node.pop()

        if count == 0: return ""
        return " ".join(moves)

    def turnmod(self) -> int:
        return -1 if self.node.turn else 1

    def show_iteration_data(self, moves: list, values: list, depth: float, start: float) -> tuple:
        t = round(time.time()-start, 2)
        print(f"{self.node.san(moves[0])} | {-round((self.turnmod()*values[0])/1000, 3)} | {str(t)}s at depth {str(depth + 1)}, {str(self.nodes)} nodes processed, at {str(int(self.nodes / (t+0.00001)))}NPS.\n", f"PV: {self.pv_string()}\n", end="")
        return (self.node.san(moves[0]), self.turnmod()*values[0], self.nodes, depth+1, t)

    def search(self, ponder: bool = False, readout: bool = True):
        val = float("-inf")
        start_time = time.time()
        self.nodes = 0
        moves = [next(self.ordered_moves())]
        saved_position = deepcopy(self.node)

        alpha, beta = float("-inf"), float("inf")
        valWINDOW = PAWN_VALUE / 4

        WINDOW_FAILED = False

        try:
            depth = 1
            while depth < 40:
                best = self.tt_lookup(self.node).best
                time_elapsed = time.time() - start_time
                # check if we aren't going to finish the next search in time
                if time_elapsed > 0.5 * self.time_limit and not ponder and not WINDOW_FAILED:
                    return best, val

                val = self.negamax(
                    depth, self.turnmod(), alpha=alpha, beta=beta)
                # print(val)
                if ((val <= alpha) or (val >= beta)):
                    # We fell outside the window, so try again with a
                    # full-width window (and the same depth).
                    alpha = float("-inf")
                    beta = float("inf")
                    WINDOW_FAILED = True
                    continue
                WINDOW_FAILED = False

                best = self.tt_lookup(self.node).best
                # check if we've run out of time
                if time_elapsed > self.time_limit and not ponder:
                    return best, val

                moves = [self.tt_lookup(self.node).best]
                values = [self.tt_lookup(self.node).value]

                if readout:
                    self.show_iteration_data(moves, values, depth, start_time)

                alpha = val - valWINDOW # Set up the window for the next iteration.
                beta = val + valWINDOW
                depth += 1
        except KeyboardInterrupt:
            self.node = saved_position
            pass
        return moves[0], val

    def ponder(self) -> None:
        self.origin = self.node.copy()
        self.search(ponder=True)

    def get_book_move(self):
        # book = chess.polyglot.open_reader(
        #     r"ProDeo292/ProDeo292/books/elo2500.bin")
        book = chess.polyglot.open_reader(
            r"books/elo2500.bin")
        main_entry = book.find(self.node)
        choice = book.weighted_choice(self.node)
        book.close()
        return main_entry.move, choice.move

    def engine_move(self) -> Move:
        # add flag_func for egtb mode
        if self.advancedTC:
            self.time_limit = (self.endpoint-time.time())/20
        print("Time for move: " + str(round(self.time_limit, 2)) + "s")
        if self.inbook:
            try:
                best, choice = self.get_book_move()
                if self.fun:
                    self.node.push(choice)
                    return choice
                else:
                    self.node.push(best)
                print(chess.pgn.Game.from_board(self.node)[-1])
            except IndexError:
                self.time_limit = self.time_limit*2
                best, _ = self.search()
                self.node.push(best)
                print(chess.pgn.Game.from_board(self.node)[-1])
                self.inbook = False
                self.time_limit = self.time_limit/2
        else:
            best, _ = self.search()
            self.node.push(best)
            print(chess.pgn.Game.from_board(self.node)[-1])
        # self.record_stack()
        self.endpoint += self.increment
        return best

    def user_move(self) -> str:
        move = input("enter move: ")
        while True:
            try:
                self.node.push_san(move)
                break
            except Exception:
                move = input("enter move: ")
        return move

    def display_ending(self) -> None:
        if self.node.is_stalemate():
            print('END BY STALEMATE')
        elif self.node.is_insufficient_material():
            print('END BY INSUFFICIENT MATERIAL')
        elif self.node.is_fivefold_repetition():
            print('END BY FIVEFOLD REPETITION')
        elif self.node.is_checkmate:
            print("BLACK" if self.node.turn == BLACK else "WHITE", 'WINS ON TURN',
                  self.node.fullmove_number)
        else:
            print('END BY UNKNOWN REASON')

    def run_game(self, indefinite=True) -> str:
        while not self.node.is_game_over():
            print(self.__repr__())
            if self.human and self.node.turn:
                try:
                    self.ponder()
                except KeyboardInterrupt:
                    self.node = self.origin
                    self.user_move()

            else:  # SWAP THESE ASAP

                self.engine_move()
            if not indefinite:
                break
        self.display_ending()
        try:
            return str(chess.pgn.Game.from_board(self.node)[-1])
        except Exception:
            return "PGN ERROR"

    def play_viri(self, fen=None):
        player_colour = input(
            "Enter the human player's colour in the form b/w\n--> ")
        while player_colour not in ['b', 'w']:
            player_colour = input(
                "Enter the human player's colour in the form b/w\n--> ")
        player_colour = WHITE if player_colour == 'w' else BLACK
        timeControl = int(
            input("how many seconds should viri get per move?\n--> "))
        self.__init__(human=True, time_limit=timeControl, fen=fen, book=True, fun=False)
        self.fun = False
        while not self.node.is_game_over():
            print(self.__repr__())
            if player_colour == self.node.turn:
                # try:
                #     self.ponder()
                # except KeyboardInterrupt:
                #     self.node = self.origin
                #     self.user_move()
                self.user_move()
            else:
                self.engine_move()
        self.display_ending()

    def perftx(self, n):
        if n == 0:
            self.nodes += 1
        else:
            for move in self.ordered_moves():
                self.node.push(move)
                self.perftx(n - 1)
                self.node.pop()

    def perft(self, n):
        self.nodes = 0
        self.perftx(n)
        print(self.nodes)

    def uci(self):
        start = input()
        while start != "uci":
            start = input()
        print("id", end="")
        while True:
            command = input()
            if command == "ucinewgame":
                board = Board()
            elif command.split()[0] == "position":
                fen = command[command.index(
                    "fen") + 3: command.index("moves") - 1]
                moves = command[command.index("moves"):].split()
                if fen == "startpos":
                    fen = 'rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1'
                board = Board(fen)
                for move in moves:
                    board.push(Move.from_uci(move))