Motivation:
I am trying to make a basic AI agent that can play chess against an opponent. The goal is to see how good it can become through the use of machine learning later on and also learn a the fine details in chess that are hidden from us when we just play it, such as evaluation parameters.
Code:
Here is what I have so far:
import chess, chess.pgn, time, math, io
import numpy as np
from selenium import webdriver
from selenium.webdriver.common.keys import Keys
from selenium.webdriver.common.action_chains import ActionChains
from selenium.webdriver.support.ui import Select
piece_values = {'P': 10, 'N': 30, 'B': 30, 'R': 50, 'Q': 90, 'K': 100, 'p': -10, 'n': -30, 'b': -30, 'r': -50, 'q': -90, 'k': -100}
# These are all flipped
position_values = {
'P' : np.array([ [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0],
[5.0, 5.0, 5.0, 5.0, 5.0, 5.0, 5.0, 5.0],
[1.0, 1.0, 2.0, 3.0, 3.0, 2.0, 1.0, 1.0],
[0.5, 0.5, 1.0, 2.5, 2.5, 1.0, 0.5, 0.5],
[0.0, 0.0, 0.0, 2.0, 2.0, 0.0, 0.0, 0.0],
[0.5, -0.5, -1.0, 0.0, 0.0, -1.0, -0.5, 0.5],
[0.5, 1.0, 1.0, -2.0, -2.0, 1.0, 1.0, 0.5],
[0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0] ]),
'N' : np.array([[-5.0, -4.0, -3.0, -3.0, -3.0, -3.0, -4.0, -5.0],
[-4.0, -2.0, 0.0, 0.0, 0.0, 0.0, -2.0, -4.0],
[-3.0, 0.0, 1.0, 1.5, 1.5, 1.0, 0.0, -3.0],
[-3.0, 0.5, 1.5, 2.0, 2.0, 1.5, 0.5, -3.0],
[-3.0, 0.0, 1.5, 2.0, 2.0, 1.5, 0.0, -3.0],
[-3.0, 0.5, 1.0, 1.5, 1.5, 1.0, 0.5, -3.0],
[-4.0, -2.0, 0.0, 0.5, 0.5, 0.0, -2.0, -4.0],
[-5.0, -4.0, -3.0, -3.0, -3.0, -3.0, -4.0, -5.0] ]),
'B' : np.array([[-2.0, -1.0, -1.0, -1.0, -1.0, -1.0, -1.0, -2.0],
[-1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, -1.0],
[-1.0, 0.0, 0.5, 1.0, 1.0, 0.5, 0.0, -1.0],
[-1.0, 0.5, 0.5, 1.0, 1.0, 0.5, 0.5, -1.0],
[-1.0, 0.0, 1.0, 1.0, 1.0, 1.0, 0.0, -1.0],
[-1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, -1.0],
[-1.0, 0.5, 0.0, 0.0, 0.0, 0.0, 0.5, -1.0],
[-2.0, -1.0, -1.0, -1.0, -1.0, -1.0, -1.0, -2.0] ]),
'R' : np.array([[ 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0],
[ 0.5, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 0.5],
[-0.5, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, -0.5],
[-0.5, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, -0.5],
[-0.5, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, -0.5],
[-0.5, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, -0.5],
[-0.5, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, -0.5],
[ 0.0, 0.0, 0.0, 0.5, 0.5, 0.0, 0.0, 0.0]]),
'Q' : np.array([[-2.0, -1.0, -1.0, -0.5, -0.5, -1.0, -1.0, -2.0],
[-1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, -1.0],
[-1.0, 0.0, 0.5, 0.5, 0.5, 0.5, 0.0, -1.0],
[-0.5, 0.0, 0.5, 0.5, 0.5, 0.5, 0.0, -0.5],
[-0.5, 0.0, 0.5, 0.5, 0.5, 0.5, 0.0, -0.5],
[-1.0, 0.5, 0.5, 0.5, 0.5, 0.5, 0.0, -1.0],
[-1.0, 0.0, 0.5, 0.0, 0.0, 0.0, 0.0, -1.0],
[-2.0, -1.0, -1.0, -0.5, -0.5, -1.0, -1.0, -2.0]]),
'K' : np.array([[ -3.0, -4.0, -4.0, -5.0, -5.0, -4.0, -4.0, -3.0],
[ -3.0, -4.0, -4.0, -5.0, -5.0, -4.0, -4.0, -3.0],
[ -3.0, -4.0, -4.0, -5.0, -5.0, -4.0, -4.0, -3.0],
[ -3.0, -4.0, -4.0, -5.0, -5.0, -4.0, -4.0, -3.0],
[ -2.0, -3.0, -3.0, -4.0, -4.0, -3.0, -3.0, -2.0],
[ -1.0, -2.0, -2.0, -2.0, -2.0, -2.0, -2.0, -1.0],
[ 2.0, 2.0, 0.0, 0.0, 0.0, 0.0, 2.0, 2.0 ],
[ 2.0, 3.0, 1.0, 0.0, 0.0, 1.0, 3.0, 2.0 ]])}
class LichessBot:
def __init__(self, fen):
self.fen = fen
self.bot = webdriver.Firefox(executable_path=r'geckodriver.exe')
def initialize(self):
bot = self.bot
bot.get('https://lichess.org/editor/rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR_w_KQkq_-')
time.sleep(3)
analysis = bot.find_element_by_css_selector(".actions > a:nth-child(2)").click()
time.sleep(1)
def gameSelect(self, fen):
bot = self.bot
fen_area = bot.find_element_by_class_name("analyse__underboard__fen")
bot.execute_script('arguments[0].setAttribute("value", arguments[1]);', fen_area, fen)
# Refresh the page to enter new fen number properly every time
fen_new = bot.find_element_by_class_name("analyse__underboard__fen").get_attribute('value').replace(' ', '_')
bot.get('https://lichess.org/analysis/standard/{}'.format(fen_new))
def gameReturn(self):
bot = self.bot
fen_return = bot.find_element_by_class_name("analyse__underboard__fen").get_attribute('value')
time.sleep(1)
return fen_return
def positionEvaluation(position, piece_values=piece_values, position_values=position_values):
# Position of pieces is not taken into account for their strength
if position_values == 'None':
total_eval = 0
pieces = list(position.piece_map().values())
for piece in pieces:
total_eval += piece_values[str(piece)]
return total_eval
else:
positionTotalEval = 0
pieces = position.piece_map()
for j in pieces:
file = chess.square_file(j)
rank = chess.square_rank(j)
piece_type = str(pieces[j])
positionArray = position_values[piece_type.upper()]
if piece_type.isupper():
flippedPositionArray = np.flip(positionArray, axis=0)
positionTotalEval += piece_values[piece_type] + flippedPositionArray[rank, file]
else:
positionTotalEval += piece_values[piece_type] - positionArray[rank, file]
return positionTotalEval
def minimax(position, depth, alpha, beta, maximizingPlayer, bestMove = 'h1h3'):
if depth == 0 or position.is_game_over():
return positionEvaluation(position, piece_values, position_values), bestMove
if maximizingPlayer:
maxEval = -np.inf
for child in [str(i).replace("Move.from_uci(\'", '').replace('\')', '') for i in list(position.legal_moves)]:
position.push(chess.Move.from_uci(child))
eval_position = minimax(position, depth-1, alpha, beta, False)[0]
position.pop()
maxEval = np.maximum(maxEval, eval_position)
alpha = np.maximum(alpha, eval_position)
if beta <= alpha:
break
return maxEval
else:
minEval = np.inf
minMove = np.inf
for child in [str(i).replace("Move.from_uci(\'", '').replace('\')', '') for i in list(position.legal_moves)]:
position.push(chess.Move.from_uci(child))
eval_position = minimax(position, depth-1, alpha, beta, True)
position.pop()
minEval = np.minimum(minEval, eval_position)
if minEval < minMove:
minMove = minEval
bestMin = child
beta = np.minimum(beta, eval_position)
if beta <= alpha:
break
return minEval, bestMin
# # To check evaluation
# board = chess.Board()
# print(positionEvaluation(board))
# quit()
# Initialize and set up position
lichess = LichessBot('rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq -')
lichess.initialize()
board = chess.Board()
fen = board.fen()
lichess.gameSelect(fen)
while not board.is_game_over():
if board.turn == True:
print('\n[INFO] Your Turn\n=========================')
fen_new = fen
while fen_new == fen:
fen_new = lichess.gameReturn()
board = chess.Board(fen_new)
else:
print('[INFO] AI\'s Turn\n')
minimaxEval, bestMove = minimax(board, 4, -np.inf, np.inf, False)
print("AI Evaluation: {}\nAI Best Move: {}".format(minimaxEval, bestMove))
board.push(chess.Move.from_uci(bestMove))
print("{}\n=========================".format(board))
fen = board.fen()
lichess.gameSelect(fen)
This is what the code does:
Open firefox terminal and go to lichess.org
Enter the analysis mode for a starting chess position
Wait for human player to make a move
Send the FEN to the python program to make that move
Apply minimax algorithm with corresponding depth and position values to evaluate the position and decide the best move
Make this move in the python program
Get the FEN of the current position
Play the best move on the board by pasting FEN into the analysis on lichess
Question:
Right now this only lets me play as the white pieces (computer algorithm works on the black pieces). My question, although it seems basic, is how to make it so that at the start I have the choice of which side to choose? It seems like the minimax algorithm is baised towards computer playing with the black pieces and any attempt I make to adjust this failed to work.
Output:
Here is what a typical output on the console would look like while the game is going on. Nothing special happens when the game ends, I plan to include a summary of the game and outcome later on.
As can be seen, I make sure to double check that the moves are correctly registered by printing the board setup position in the console output after every move.
Final Note:
I am aware the evaluation metric and maybe even the efficiency of the algorithm might not be the best but these will be adjusted once all the fine details, such as the one posted in the question, are answered.