commit db8dcee339bbb2305c2fe711a0292c19f64ce1bb Author: luk3k Date: Tue Jan 23 19:03:16 2024 +0100 initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..64e4208 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +/stockfish/ diff --git a/engine.py b/engine.py new file mode 100644 index 0000000..01413ed --- /dev/null +++ b/engine.py @@ -0,0 +1,67 @@ +import chess +import chess.engine +import random +import eval + + +def main(): + fools_mate = "rnbqkbnr/pppp1ppp/4p3/8/5PP1/8/PPPPP2P/RNBQKBNR b KQkq f3 0 2" + board = chess.Board(fools_mate) + print(board, '\n') + moves = {} + for i in range(10): + move = pick_move(board) + if move is None: + break + + simulate_game(board, move, 100) + moves[move] = board + board = chess.Board(fools_mate) + + analyze_results(moves) + + +def analyze_results(moves: dict): + for m, b in moves.items(): + manual_score = eval.score_game(b) + engine_score = eval.analyze_with_stockfish(b) + print(f"score for move {m}: manual_score={manual_score}, engine_score={engine_score}") + + +def pick_move(board: chess.Board) -> chess.Move | None: + """ + Pick a random move + :param board: chess board + :return: a valid move or None if no valid move available + """ + if len(list(board.legal_moves)) == 0: + return None + return random.choice(list(board.legal_moves)) + + +def simulate_game(board: chess.Board, move: chess.Move, depth: int): + """ + Simulate a game starting with the given move + :param board: chess board + :param move: chosen move + :param depth: number of moves that should be simulated after playing the chosen move + :return: the score for the simulated game + """ + engine = chess.engine.SimpleEngine.popen_uci("./stockfish/stockfish-ubuntu-x86-64-avx2") + board.push(move) + print(move) + print(board, '\n') + for i in range(depth): + if board.is_game_over(): + engine.quit() + return + r = engine.play(board, chess.engine.Limit(depth=2)) + print(r) + board.push(r.move) + print(board, '\n') + + engine.quit() + + +if __name__ == '__main__': + main() diff --git a/eval.py b/eval.py new file mode 100644 index 0000000..4dad816 --- /dev/null +++ b/eval.py @@ -0,0 +1,133 @@ +import chess +import chess.engine + +# Eval constants for scoring chess boards +# Evaluation metric inspired by Tomasz Michniewski: https://www.chessprogramming.org/Simplified_Evaluation_Function + +PIECE_VALUES = { + chess.PAWN: 100, + chess.KNIGHT: 320, + chess.BISHOP: 330, + chess.ROOK: 500, + chess.QUEEN: 900, + chess.KING: 20000 +} + +pawn_eval = [ + 0, 0, 0, 0, 0, 0, 0, 0, + 5, 10, 10, -20, -20, 10, 10, 5, + 5, -5, -10, 0, 0, -10, -5, 5, + 0, 0, 0, 20, 20, 0, 0, 0, + 5, 5, 10, 25, 25, 10, 5, 5, + 10, 10, 20, 30, 30, 20, 10, 10, + 50, 50, 50, 50, 50, 50, 50, 50, + 0, 0, 0, 0, 0, 0, 0, 0 +] +knight_eval = [ + -50, -40, -30, -30, -30, -30, -40, -50, + -40, -20, 0, 0, 0, 0, -20, -40, + -30, 0, 10, 15, 15, 10, 0, -30, + -30, 5, 15, 20, 20, 15, 5, -30, + -30, 0, 15, 20, 20, 15, 0, -30, + -30, 5, 10, 15, 15, 10, 5, -30, + -40, -20, 0, 5, 5, 0, -20, -40, + -50, -40, -30, -30, -30, -30, -40, -50 +] +bishop_eval = [ + -20, -10, -10, -10, -10, -10, -10, -20, + -10, 5, 0, 0, 0, 0, 5, -10, + -10, 10, 10, 10, 10, 10, 10, -10, + -10, 0, 10, 10, 10, 10, 0, -10, + -10, 5, 5, 10, 10, 5, 5, -10, + -10, 0, 5, 10, 10, 5, 0, -10, + -10, 0, 0, 0, 0, 0, 0, -10, + -20, -10, -10, -10, -10, -10, -10, -20 +] +rook_eval = [ + 0, 0, 0, 5, 5, 0, 0, 0, + -5, 0, 0, 0, 0, 0, 0, -5, + -5, 0, 0, 0, 0, 0, 0, -5, + -5, 0, 0, 0, 0, 0, 0, -5, + -5, 0, 0, 0, 0, 0, 0, -5, + -5, 0, 0, 0, 0, 0, 0, -5, + 5, 10, 10, 10, 10, 10, 10, 5, + 0, 0, 0, 0, 0, 0, 0, 0 +] +queen_eval = [ + -20, -10, -10, -5, -5, -10, -10, -20, + -10, 0, 0, 0, 0, 0, 0, -10, + -10, 0, 5, 5, 5, 5, 0, -10, + -5, 0, 5, 5, 5, 5, 0, -5, + 0, 0, 5, 5, 5, 5, 0, -5, + -10, 5, 5, 5, 5, 5, 0, -10, + -10, 0, 5, 0, 0, 0, 0, -10, + -20, -10, -10, -5, -5, -10, -10, -20 +] +king_eval = [ + 20, 30, 10, 0, 0, 10, 30, 20, + 20, 20, 0, 0, 0, 0, 20, 20, + -10, -20, -20, -20, -20, -20, -20, -10, + 20, -30, -30, -40, -40, -30, -30, -20, + -30, -40, -40, -50, -50, -40, -40, -30, + -30, -40, -40, -50, -50, -40, -40, -30, + -30, -40, -40, -50, -50, -40, -40, -30, + -30, -40, -40, -50, -50, -40, -40, -30 +] + +PIECE_TABLES = { + chess.WHITE: { + chess.PAWN: pawn_eval, + chess.KNIGHT: knight_eval, + chess.BISHOP: bishop_eval, + chess.ROOK: rook_eval, + chess.QUEEN: queen_eval, + chess.KING: king_eval + }, + chess.BLACK: { + chess.PAWN: list(reversed(pawn_eval)), + chess.KNIGHT: list(reversed(knight_eval)), + chess.BISHOP: list(reversed(bishop_eval)), + chess.ROOK: list(reversed(rook_eval)), + chess.QUEEN: list(reversed(queen_eval)), + chess.KING: list(reversed(king_eval)) + } +} + + +def score_game(board: chess.Board) -> float: + """ + Calculate the score of the given board regarding the given color + :param board: the chess board + :return: score metric + """ + outcome = board.outcome() + if outcome is not None: + if outcome.termination == chess.Termination.CHECKMATE: + return float('inf') if outcome.winner == chess.WHITE else float('-inf') + else: # draw + return 0 + + score = 0 + for s in chess.SQUARES: + piece = board.piece_at(s) + if piece is None: + continue + + if piece.color == chess.WHITE: + score += PIECE_VALUES[piece.piece_type] * PIECE_TABLES[chess.WHITE][piece.piece_type][s] + else: + score -= PIECE_VALUES[piece.piece_type] * PIECE_TABLES[chess.BLACK][piece.piece_type][s] + + return score + + +def analyze_with_stockfish(board: chess.Board) -> chess.engine.PovScore: + """ + Calculate the score of the given board using stockfish + :param board: + :return: + """ + engine = chess.engine.SimpleEngine.popen_uci("./stockfish/stockfish-ubuntu-x86-64-avx2") + info = engine.analyse(board, chess.engine.Limit(depth=20)) + engine.quit() + return info["score"] diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..a4adb10 --- /dev/null +++ b/requirements.txt @@ -0,0 +1 @@ +chess==1.10.0 \ No newline at end of file