Code source de crocrodile.engine

# -*- coding: utf-8 -*-
"""
MyEngine Engine base.

Base engine
"""
from __future__ import annotations

from __future__ import print_function

import copy
import math
import pickle
import random
import sys
import time
from typing import Optional

import chess
import chess.polyglot
import crocrodile.engine.evaluate as evaluation
import crocrodile.nn as nn
import requests

PAWN_VALUE = 130
KNIGHT_VALUE = 290
BISHOP_VALUE = 310
ROOK_VALUE = 500
QUEEN_VALUE = 901
KING_VALUE = 0  # Infinity is too complex
BISHOPS_PAIR = 50
PROTECTED_KING = 5
PIECES_VALUES = {
    "p": PAWN_VALUE,
    "n": KNIGHT_VALUE,
    "b": BISHOP_VALUE,
    "r": ROOK_VALUE,
    "q": QUEEN_VALUE,
    "k": KING_VALUE,
}
CENTRAL_SQUARES = [36, 35, 28, 27]
ELARGED_SQUARES = [45, 44, 43, 42, 37, 34, 29, 26, 21, 20, 19, 18]
SEVENTH_ROW = [55, 54, 53, 52, 51, 50, 49, 48]
EIGHT_ROW = [56, 57, 58, 59, 60, 61, 62, 63]
SECOND_ROW = [15, 14, 13, 12, 11, 10, 9, 8]
FIRST_ROW = [0, 1, 2, 3, 4, 5, 6, 7]
VARIANTS = ["standard", "chess960"]


[docs]def printi(*args): """Debug mode printer.""" print("info string", args)
[docs]class EngineBase: """Engine base.""" def __init__(self, name, author, board=chess.Board()): """Initialise engine.""" self.name = name self.author = author self.board = board self.tb: dict = {} self.tb_limit = 10000000 self.nn_tb = dict() self.nn_tb_limit = 4096 self.nn = nn.NeuralNetwork() self.nn.load_layers(0) self.evaluator = evaluation.Evaluator() self.nn.load_layers(0) self.nodes = 0 self.depth = None self.opening_book = chess.polyglot.open_reader("./book.bin") self.obhits: int = 0 self.tbhits: int = 0 self.hashlimit: int = 16 # Megabytes (Hash) self.use_nn: bool = True # Used to disable NN (NeuralNetwork) self.hashfull: int = 0 # info hashfull self.own_book: bool = False # Use own book (OwnBook) self.syzygy_online: bool = False # Use online Syzygy Lichess tables (SyzygyOnline) self.syzygy_tb: Optional[chess.syzygy.Tablebase] = None # Path to Syzygy Tables, if implemented (SyzygyPath) self.hashpath: str = "" # Path to a file used to store hash table
[docs] def tb_update(self): """ Update hash tables from file. :return: Nothing. :rtype: None """ self.tb.update(pickle.load(open(self.hashpath, "br")))
[docs] def evaluate(self, board): """Evaluate position.""" return self.evaluator.evaluate(board)
[docs] def search(self, board, depth, maximize_white, limit_time): """Search best move (Minimax from wikipedia).""" self.nodes = 0 self.obhits: int = 0 self.tbhits: int = 0 self.hashfull: int = 0 start_time: float = time.time() eval, move = self.minimax_nn(board, depth, maximize_white, limit_time) calc_time: float = time.time() - start_time if board.turn is chess.BLACK: eval = - eval if eval != float("inf"): print( f"info depth {depth} nodes {self.nodes} score cp {eval} pv {move} tbhits {self.tbhits} time {int(calc_time * 1000)} nps {int(self.nodes / calc_time)} hashfull {self.hashfull} string obhits:{self.obhits}" ) # Update hash file if self.hashpath: pickle.dump(self.tb, open(self.hashpath, "bw")) return eval, move
[docs] def minimax_std(self, board, depth, maximimize_white, limit_time): """Minimax algorithm from Wikipedia with NN.""" if depth == 0 or board.is_game_over(): zobrist_hash = chess.polyglot.zobrist_hash(board) if zobrist_hash not in self.tb: # if j'ai du temps self.tb[zobrist_hash] = self.evaluate(board) if len(self.tb) > self.tb_limit: del self.tb[list(self.tb.keys())[0]] # else # evaluation = self.tb[zobrist_hash] # evaluation = self.evaluate(board) attackers = board.attackers(board.turn, board.peek().to_square) if len(attackers) > 0: # Quiescent if board.turn == chess.WHITE: evaluation += PIECES_VALUES[ board.piece_map()[board.peek().to_square].symbol().lower() ] else: evaluation -= PIECES_VALUES[ board.piece_map()[board.peek().to_square].symbol().lower() ] return evaluation, chess.Move.from_uci("0000") if maximimize_white: value = -float("inf") legal_moves = list(board.legal_moves) list_best_moves = [legal_moves[0]] for move in legal_moves: if time.time() > limit_time: return float("inf"), chess.Move.from_uci("0000") test_board = chess.Board(fen=board.fen()) test_board.push(move) evaluation = self.minimax_std(test_board, depth - 1, False, limit_time)[ 0 ] if value == evaluation: list_best_moves.append(move) elif value < evaluation: value = evaluation list_best_moves = [move] return value, random.choice(list_best_moves) else: # minimizing white value = float("inf") legal_moves = list(board.legal_moves) list_best_moves = [legal_moves[0]] for move in legal_moves: if time.time() > limit_time: return float("inf"), chess.Move.from_uci("0000") test_board = chess.Board(fen=board.fen()) test_board.push(move) evaluation = self.minimax_std(test_board, depth - 1, True, limit_time)[ 0 ] if value == evaluation: list_best_moves.append(move) elif value > evaluation: value = evaluation list_best_moves = [move] return value, random.choice(list_best_moves)
[docs] def nn_select_best_moves(self, board: chess.Board): """Select best moves in board.""" good_moves = True if self.use_nn: hash = chess.polyglot.zobrist_hash(board) if hash not in self.nn_tb: good_moves = list() for move in board.legal_moves: if self.nn.check_move(board.fen(), move.uci()): good_moves.append(move) self.nn_tb[hash] = good_moves good_moves = self.nn_tb[hash] if not good_moves: good_moves = list(board.legal_moves) self.nn_tb[hash] = good_moves if int(sys.getsizeof(self.nn_tb) / 1024 / 1024) >= self.hashlimit / 2: del self.nn_tb[list(self.nn_tb.keys())[0]] self.hashfull += 1 return good_moves else: return list(board.legal_moves)
[docs] def get_book_move(self, board: chess.Board) -> Optional[chess.Move]: """Get move from opening book. :param board: Board to get move. :type board: chess.Board :return: A random move from the opening book? :rtype: Optional[chess.Move] """ try: return self.opening_book.weighted_choice(board).move except IndexError: return False
[docs] def get_syzygy(self, board: chess.Board) -> tuple[int, chess.Move]: """ Get a move from Syzygy tablebases. :param chess.Board board: Board to get best move and evaluation. :return: The evaluation from Syzygy tablebases and the best move. :rtype: tuple[int, chess.Move] """ # Generate DTZ list wdl: dict[int, dict[int, chess.Move]] = {2: {}, 1: {}, 0: {}, -1: {}, -2: {}} for move in board.legal_moves: test_board: chess.Board = board.copy() test_board.push(move) wdl[self.syzygy_tb.probe_wdl(test_board)][self.syzygy_tb.probe_dtz(test_board)] = move # Get best WDL best_wdl: int = -2 while wdl[best_wdl] == {}: best_wdl += 1 # Get best move best_dtz: int = 0 best_dtz = max(wdl[best_wdl].items(), key=lambda key: key[0])[0] # Evaluation print(wdl) syzygy_evaluation: int = 0 if best_wdl < 0: if board.turn: syzygy_evaluation = 10000 else: syzygy_evaluation = -10000 elif best_wdl == 0: syzygy_evaluation = 0 else: if board.turn: syzygy_evaluation = -10000 else: syzygy_evaluation = 10000 # Return return syzygy_evaluation, wdl[best_wdl][best_dtz]
# + param time + param best move depth-1 + param evaluation
[docs] def minimax_nn(self, board: chess.Board, depth, maximimize_white, limit_time): """Minimax algorithm from Wikipedia with NN.""" self.nodes += 1 if depth == 0 or board.is_game_over(): evaluation = self.evaluate(board) # evaluation = self.evaluate(board) """attackers = board.attackers(board.turn, board.peek().to_square) if len(attackers) > 0: # Quiescent if board.turn == chess.WHITE: evaluation += PIECES_VALUES[board.piece_map() [board.peek().to_square]. symbol().lower()] else: evaluation -= PIECES_VALUES[board.piece_map() [board.peek().to_square]. symbol().lower()]""" return evaluation, chess.Move.from_uci("0000") if maximimize_white: book_move = None if self.own_book and board.fullmove_number < 15 and (book_move := self.get_book_move(board)): self.obhits += 1 return 10000, book_move if self.syzygy_online and len(board.piece_map()) <= 7: formatted_fen = board.fen().replace(" ", "_") data = requests.get( f"http://tablebase.lichess.ovh/standard?fen={formatted_fen}" ).json() good_move = chess.Move.from_uci(data["moves"][0]["uci"]) if data["category"] in ("win", "maybe_win", "cursed-win"): self.tbhits += 1 return 10000, good_move elif data["category"] in ("loss", "maybe-loss", "blessed-loss"): self.tbhits += 1 return -10000, good_move elif data["category"] == "draw": self.tbhits += 1 return 0, good_move else: pass if self.syzygy_tb and len(board.piece_map()) <= 6: try: return self.get_syzygy(board) except chess.syzygy.MissingTableError: pass value = -float("inf") legal_moves = list(board.legal_moves) list_best_moves = [legal_moves[0]] for move in self.nn_select_best_moves(board): if time.time() > limit_time: return float("inf"), chess.Move.from_uci("0000") test_board = chess.Board(fen=board.fen()) test_board.push(move) hash = chess.polyglot.zobrist_hash(test_board) if hash in self.tb and self.tb[hash][0] >= depth: evaluation: int = self.tb[hash][1] else: evaluation = self.minimax_nn( test_board, depth - 1, False, limit_time )[0] self.tb[hash] = (copy.copy(depth), copy.copy(evaluation)) if int(sys.getsizeof(self.tb) / 1024 / 1024) >= self.hashlimit / 2: del self.tb[list(self.tb.keys())[0]] self.hashfull += 1 if value == evaluation: list_best_moves.append(move) elif value < evaluation: value = evaluation list_best_moves = [move] return value, random.choice(list_best_moves) else: book_move = None if self.own_book and board.fullmove_number < 15 and (book_move := self.get_book_move(board)): self.obhits += 1 return -10000, book_move if self.syzygy_online and len(board.piece_map()) <= 7: formatted_fen = board.fen().replace(" ", "_") data = requests.get( f"http://tablebase.lichess.ovh/standard?fen={formatted_fen}" ).json() good_move = chess.Move.from_uci(data["moves"][0]["uci"]) if data["category"] in ("win", "maybe_win", "cursed-win"): self.tbhits += 1 return -10000, good_move elif data["category"] in ("loss", "maybe-loss", "blessed-loss"): self.tbhits += 1 return 10000, good_move elif data["category"] == "draw": self.tbhits += 1 return 0, good_move else: pass if self.syzygy_tb and len(board.piece_map()) <= 6: try: return self.get_syzygy(board) except chess.syzygy.MissingTableError: pass # minimizing white value = float("inf") legal_moves = list(board.legal_moves) list_best_moves = [legal_moves[0]] for move in self.nn_select_best_moves(board): if time.time() > limit_time: return float("inf"), chess.Move.from_uci("0000") test_board = chess.Board(fen=board.fen()) test_board.push(move) hash = chess.polyglot.zobrist_hash(test_board) if hash in self.tb and self.tb[hash][0] >= depth: evaluation: int = self.tb[hash][1] else: evaluation = self.minimax_nn( test_board, depth - 1, True, limit_time )[0] self.tb[hash] = (copy.copy(depth), copy.copy(evaluation)) if int(sys.getsizeof(self.tb) / 1024 / 1024) >= self.hashlimit / 2: del self.tb[list(self.tb.keys())[0]] self.hashfull += 1 if value == evaluation: list_best_moves.append(move) elif value > evaluation: value = evaluation list_best_moves = [move] return value, random.choice(list_best_moves)