Source code for Clients.ChessDotComClient
from typing import Optional, Tuple
import chess
import time
from datetime import timedelta
from chess import engine
from selenium import webdriver
from selenium.webdriver import ActionChains
from selenium.common.exceptions import StaleElementReferenceException
from Clients import ClientInterface
[docs]class ChessDotComClient(ClientInterface.ClientInterface):
""" Defines the client to play on `chess.com <chess.com>`_ """
_driver: webdriver
color: chess.Color
def __init__(self, headless: bool = False, path_userdata: str = None):
""" Starts the chess.com client.
Using the chess.com client requires that chromedriver to be installed. Starting the client will open a new
chromedriver window. No version of chrome should be running if the path_userdata is set. The game starts once
the start method is called
:param headless: Needs to be set to false if the device running the code is headless
:param path_userdata: Optional path to the chrome user date, allows chess.com to remember login
"""
options = webdriver.ChromeOptions()
if path_userdata is not None:
options.add_argument("user-data-dir=" + path_userdata)
if headless:
options.headless = True
self._driver = webdriver.Chrome(options=options)
self._driver.get("https://www.chess.com")
# Check if game is ready and set color
super().__init__(self.start_new_game())
print("game started as " + ("black" if self.color else "white"))
def __del__(self):
"""" Terminates engine """
self._driver.quit()
[docs] def get_move(self) -> chess.engine.PlayResult:
""" Detects client move from chess.com
:returns: next move played by the client in the engine output format
"""
while True:
try:
# Find highlighted squares
res = self._driver.find_elements_by_xpath("//div[starts-with(@data-test-element,'highlight')]")
if len(res) >= 2:
# Two highlighted square indicate a move, first hit usually from square second to square
from_square_id = res[0].get_attribute("class")[-2:] # Last two char of class name is the square
to_square_id = res[1].get_attribute("class")[-2:]
from_square = chess.square(int(from_square_id[0]) - 1, int(from_square_id[1]) - 1)
to_square = chess.square(int(to_square_id[0]) - 1, int(to_square_id[1]) - 1)
# Set from and to square
if self._board.piece_at(from_square) is None \
or self._board.piece_at(from_square).color is not self.color:
temp = from_square
from_square = to_square
to_square = temp
# If new move is promotion
if self._board.piece_at(from_square) is not None \
and self._board.piece_at(from_square).piece_type is chess.PAWN \
and chess.square_rank(to_square) == (7 if self.color is chess.WHITE else 0):
# Check promotion square (it has a unique name)
promotion_square_id = "piece.square-{piece_file}{piece_rank}.{piece_color}".format(
piece_file=chess.square_file(to_square) + 1,
piece_rank=chess.square_rank(to_square) + 1,
piece_color='w' if self.color is chess.WHITE else 'b'
)
piece: chess.Piece
if self._driver.find_elements_by_id(promotion_square_id + 'q') is not []:
piece = chess.QUEEN # if promotion piece square contains queen
elif self._driver.find_elements_by_id(promotion_square_id + 'r') is not []:
piece = chess.ROOK
elif self._driver.find_elements_by_id(promotion_square_id + 'n') is not []:
piece = chess.KNIGHT
elif self._driver.find_elements_by_id(promotion_square_id + 'b') is not []:
piece = chess.BISHOP
else:
raise Exception("Promotion piece detection failed")
# set move with promotion field
move = chess.Move(from_square, to_square, promotion=piece)
else:
# If no promotion just set new move
move = chess.Move(from_square, to_square)
if move in self._board.legal_moves:
# if the move is legal
self._board.push(move)
res = self._driver.find_elements_by_class_name(
"form-button-component.form-button-small.form-button-primary.draw-offer-accept")
draw_offer = res != []
return chess.engine.PlayResult(move, None, draw_offered=draw_offer)
else: # Old move found
time.sleep(0.5)
except StaleElementReferenceException: # Occurs if new move is played during processing
pass
[docs] def set_move(self, move: engine.PlayResult):
""" Plays new move on chess.com
:param move: reports move of opponent to client using normal engine output format
"""
if move.move is None:
if move.resigned:
resign_button = self._driver.find_elements_by_class_name("secondary-controls-icon")
# If a bot is being played
if len(resign_button) != 0:
resign_button[-1].click()
else:
# Resign confirmation should be turned off
self._driver.find_element_by_class_name("resign-button-component.live-game-buttons-button").click()
return
else:
raise Exception("Internal error empty move given as input")
if move.draw_offered:
draw = self._driver.find_element_by_class_name("draw-button-component.live-game-buttons-button")
draw.click()
piece = self._board.piece_at(move.move.from_square)
self._board.push(move.move)
piece_char: str = chess.piece_symbol(piece.piece_type).lower()
piece_color = 'w' if piece.color == chess.WHITE else 'b'
piece_elem_name = "piece.{piece_color}{piece_char}.square-{piece_file}{piece_rank}".format(
piece_color=piece_color,
piece_char=piece_char,
piece_file=chess.square_file(move.move.from_square) + 1,
piece_rank=chess.square_rank(move.move.from_square) + 1)
# Find square with the piece to move
piece_elem = self._driver.find_element_by_class_name(piece_elem_name)
square_height = piece_elem.size['height']
# Calculate relative position of to square from whites perspective
transpose_file = chess.square_file(move.move.to_square) - chess.square_file(move.move.from_square)
transpose_rank = chess.square_rank(move.move.to_square) - chess.square_rank(move.move.from_square)
# Flip transpose if player if black
transpose_file = transpose_file if self.color is chess.WHITE else -transpose_file
transpose_rank = transpose_rank if self.color is chess.WHITE else - transpose_rank
# Drag piece with transposition
act = ActionChains(self._driver)
act.drag_and_drop_by_offset(piece_elem,
- transpose_file * square_height,
transpose_rank * square_height).perform()
if move.move.promotion is not None:
time.sleep(0.1)
piece_char: str = chess.piece_symbol(move.move.promotion).lower()
piece_elem_name = "promotion-piece.{piece_color}{piece_char}".format(
piece_color=piece_color,
piece_char=piece_char)
piece_elem = self._driver.find_element_by_class_name(piece_elem_name)
piece_elem.click()
[docs] def game_is_over(self) -> bool:
""" Checks if game is over by checking if the position is mate or the game over window is open
:return: Returns True iff game is over
"""
game_end_name = "section-container-component-light-mode-modal-content-component.modal-game-over" \
"-component.modal-game-over-rounded-grey"
return self._driver.find_elements_by_class_name(game_end_name) != [] or self._board.is_game_over()
[docs] def synchronize_clocks(self, clock: Optional[Tuple[timedelta, timedelta]] = None) \
-> Optional[Tuple[timedelta, timedelta]]:
""" Obtains time from chess.com if the clock is running else returns input
:param clock: Input clock from the other client
:return: return clock from current client
"""
client = self._driver.find_elements_by_xpath("//*[@id='board-layout-player-top']//div//div[3]/span")
opponent = self._driver.find_elements_by_xpath("//*[@id='board-layout-player-bottom']//div//div[3]/span")
if client is not [] and opponent is not []:
client_time_str = str(client[0].text).split(":")
client_time = timedelta(minutes=int(client_time_str[0]),
seconds=int(client_time_str[1].replace(":", "")))
opponent_time_str = str(client[0].text).split(":")
opponent_time = timedelta(minutes=int(opponent_time_str[0]),
seconds=int(opponent_time_str[1].replace(":", "")))
return (client_time, opponent_time) if self.color else (opponent_time, client_time)
else:
return clock
[docs] def start_new_game(self) -> chess.Color:
""" Resets the client to start a new game, unique method for chessdotcom client
This method can be used to start a new game using the same client. This prevents us from having to reload the
chromedriver
:return: Returns new color of client
"""
while True:
if self._driver.find_elements_by_class_name("resign-button-component.live-game-buttons-button") != [] or \
len(self._driver.find_elements_by_class_name("secondary-controls-icon")) == 4:
break
else:
time.sleep(0.5)
# Find color of client
y_coord_w_rook = self._driver.find_element_by_class_name("piece.wr.square-11").location['y']
y_coord_b_rook = self._driver.find_element_by_class_name("piece.br.square-18").location['y']
color = chess.WHITE if y_coord_b_rook > y_coord_w_rook else chess.BLACK
self._board = chess.Board()
# configure metadata
names = self._driver.find_elements_by_class_name(
"user-username-component.user-username-dark.user-username-link.user-tagline-username")
rating = self._driver.find_elements_by_class_name("user-tagline-rating")
if not names:
names = self._driver.find_elements_by_class_name(
"user-username-component.user-username-lightgray.user-tagline-username")
if len(names) == 2:
if 0 == len(rating):
self.metadata[("white" if color else "black") + "_name"] = names[0].text
else:
self.metadata[("white" if color else "black") + "_name"] = names[0].text + " " + rating[0].text
if 1 == len(rating):
self.metadata[("white" if not color else "black") + "_name"] = names[1].text
else:
self.metadata[("white" if not color else "black") + "_name"] = names[1].text + " " + rating[1].text
return color