Open SolsticeSpectrum opened 1 year ago
The arrows work like this
<svg viewBox="0 0 100 100" class="arrows"><polygon id="arrow-e2d3" data-arrow="e2d3" class="arrow" style="fill: rgba(255, 170, 0, 0.8); opacity: 0.8;" transform="rotate(135 56.25 81.25)" points="54.875 85.75,
54.875 94.42766952966369,
53 94.42766952966369,
56.25 98.92766952966369,
59.5 94.42766952966369,
57.625 94.42766952966369,
57.625 85.75"></polygon></svg>
I made a quick experiment and the Resign buttons is a good way to detect game
# wait for move input box
WebDriverWait(driver, 600).until(
ec.presence_of_element_located((By.XPATH, "/html/body/div/div/div/div/div/button/span[2]")))
Something like this should work
It has the same XPATH no matter if it's bot game or regular game
This should be correct so far
def find_color(board):
while check_exists_by_class("follow-up"):
sleep(1)
# wait for move input box
WebDriverWait(driver, 600).until(
ec.presence_of_element_located((By.XPATH, "/html/body/div/div/div/div/div/button/span[2]")))
# wait for board
WebDriverWait(driver, 600).until(
ec.presence_of_element_located((By.XPATH, "/html/body/div/div/chess-board")))
board_set_for_black = check_exists_by_class("board flipped")
if board_set_for_black:
our_color = 'B'
play_game(board, our_color)
else:
our_color = 'W'
play_game(board, our_color)
except follow-up I didn't figure out yet what that does
except follow-up I didn't figure out yet what that does
Sorry, been a bit busy recently. The "follow-up" is just checking if rematch/analysis board buttons are present. If they are, then the game is over, so just wait. Ideally it should be moved out and have its own function.
I can't figure out this one.
while temp_move_number < 999: # just in-case, lol
if check_exists_by_xpath("/html/body/div[2]/main/div[1]/rm6/l4x/kwdb[" + str(temp_move_number) + "]"):
move = driver.find_element(By.XPATH,
"/html/body/div[2]/main/div[1]/rm6/l4x/kwdb[" + str(temp_move_number) + "]").text
board.push_san(move)
temp_move_number += 1
else:
return temp_move_number
To port it I need to use this format //vertical-move-list/div[x]/div[y]
where x is the number of the move and y is either my move or oponents move depending on if you use div[1] or div[2]
The original code skips the move number by targeting kwdb directly but how do you do that when the code doesn't have custom elements like i5z which would be the number of the move in lichess
Okay so I also figured out the login
def sign_in():
driver.get("https://www.chess.com/login")
username = driver.find_element(By.ID, "username")
password = driver.find_element(By.ID, "password")
username.send_keys(config["chess"]["Username"])
password.send_keys(config["chess"]["Password"])
driver.find_element(By.XPATH, "/html/body/div/div/main/div/form/button").click() # submit
follow-up can be replaced with quick-analysis-component
This might be correct but I am not sure
def clear_arrow():
driver.execute_script("""
var arrows = document.getElementsByClassName("arrow");
while (arrows.length > 0) {
arrows[0].remove();
}
""")
def draw_arrow(result, our_color):
transform = get_piece_transform(result.move, our_color)
move_str = str(result.move)
src = str(move_str[:2])
dst = str(move_str[2:])
board_style = driver.find_element(By.XPATH,
"/html/body/div/div/chess-board").get_attribute(
"style")
board_size = re.search(r'\d+', board_style).group()
driver.execute_script("""
var x1 = arguments[0];
var y1 = arguments[1];
var x2 = arguments[2];
var y2 = arguments[3];
var size = arguments[4];
var src = arguments[5];
var dst = arguments[6];
svg = document.getElementsByClassName("arrows")[0];
if (svg == null)
{
svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
svg.setAttribute("viewBox", "0 0 100 100");
svg.setAttribute("class", "arrows");
svg.setAttribute("width", size);
svg.setAttribute("height", size);
}
polygon = document.createElementNS("http://www.w3.org/2000/svg", "polygon");
polygon.setAttribute("id", `arrow-${src}${dst}`);
polygon.setAttribute("data-arrow", `${src}${dst}`);
polygon.setAttribute("class", "arrow");
polygon.setAttribute("style", "fill: rgba(255, 170, 0, 0.8); opacity: 0.8;");
polygon.setAttribute("points", `${x1} ${y1}, ${x1 + 5} ${y1 + 5}, ${x2 - 5} ${y2 + 5}, ${x2} ${y2}`);
svg.appendChild(polygon);
document.body.appendChild(svg);
""", transform[0], transform[1], transform[2], transform[3], board_size,
src,
dst)
I will have to leave the rest on you because I am barely a python beginner
Okay so the modified draw_arrow function is giving me AttributeError: 'NoneType' object has no attribute 'group'
because chess-board doesn't have any attribute called style.
Also this is probably correct //vertical-move-list/div/div[" + str(temp_move_number) + "]
or at least it doesn't give me errors
looks functional to me so far
//vertical-move-list/div/div[" + str(temp_move_number) + "]
doesn't work, it works for first move only and also I fucked up somewhere because it shows that [us] is the oponent and I removed arrows for now because I have no idea how to do it
Okay I got it somewhat working. I would still prefer arrows over squares but I couldn't figure it out so I just yoinked it from Sangatsu. Bot games don't work. Otherwise I did manage to make tournaments and regular games. It works better than sangatsu as when the game ends I can go and challenge anyone, open any type of game or tournament. In Sangatsu you kinda have to stay on the online game screen or it throws error
import configparser
import chess
import chess.polyglot
import os.path
from selenium import webdriver
from selenium.common.exceptions import NoSuchElementException
from selenium.webdriver.common.by import By
from selenium.webdriver.support.wait import WebDriverWait
from selenium.webdriver.support import expected_conditions as ec
from chess import engine
from time import sleep
from math import ceil
# SCRIPT_DIR is the directory containing this file
SCRIPT_DIR = os.path.dirname(__file__)
# declare globals
profile = webdriver.FirefoxProfile("/home/ruda0/.mozilla/firefox/llaf1xpi.default")
profile.set_preference("general.useragent.override", "Mozilla/5.0 (X11; Linux x86_64; rv:107.0) Gecko/20100101 Firefox/107.0")
driver = webdriver.Firefox(profile, executable_path=os.path.join(SCRIPT_DIR, 'bin', 'geckodriver'))
config = configparser.ConfigParser()
def check_exists_by_xpath(xpath):
try:
driver.find_element(By.XPATH, xpath)
except NoSuchElementException:
return False
return driver.find_element(By.XPATH, xpath)
def check_exists_by_class(classname):
try:
driver.find_element(By.CLASS_NAME, classname)
except NoSuchElementException:
return False
return driver.find_element(By.CLASS_NAME, classname)
def find_color(board):
# sleep if game is over
while check_exists_by_class("arena-standings-tab-component") or check_exists_by_class("quick-analysis-component"):
sleep(1)
# wait for board
WebDriverWait(driver, 600).until(
ec.presence_of_element_located((By.XPATH, "/html/body/div/div/chess-board")))
# determines what color are we
chess_board = driver.find_element(By.XPATH, "/html/body/div/div/chess-board")
classes = chess_board.get_attribute("class")
board_set_for_black = "board flipped" in classes
if board_set_for_black:
our_color = 'B'
play_game(board, our_color)
else:
our_color = 'W'
play_game(board, our_color)
def new_game(board):
board.reset()
WebDriverWait(driver, 600).until(
ec.presence_of_element_located((By.CLASS_NAME, "quick-chat-icon-component")))
if check_exists_by_class("quick-chat-icon-component"):
find_color(board)
def get_previous_moves(board):
temp_move_val = 1
temp_move_number = 1
while temp_move_number < 999: # just to be sure
if check_exists_by_xpath("//vertical-move-list/div[" + str(temp_move_val) + "]/div[" + str(temp_move_number) + "]"):
move = driver.find_element(By.XPATH, "//vertical-move-list/div[" + str(temp_move_val) + "]/div[" + str(temp_move_number) + "]").text
board.push_san(move)
if temp_move_number >= 3:
temp_move_number = 1
temp_move_val += 1
else:
temp_move_number += 2
else:
return temp_move_number
def clear_square():
driver.execute_script("""
var elements = document.getElementsByClassName("highlight");
while (elements.length > 0) {
elements[0].parentNode.removeChild(elements[0]);
}
""")
def draw_square(result):
move_str = str(result.move)
src = str(move_str[:2])
dst = str(move_str[2:])
first = str(ord(src[0])-96) + src[1]
second = str(ord(dst[0])-96) + dst[1]
driver.execute_script("""
element = document.createElement('div');
element.setAttribute("class", "highlight square-""" + str(first) + """");
style1 = "background-color: rgb(255, 255, 0); opacity: 0.5;"
element.setAttribute("style", style1)
document.getElementsByClassName("board")[0].appendChild(element)
element = document.createElement('div');
style2 = "background-color: rgb(0, 255, 100); opacity: 0.5;"
element.setAttribute("style", style2)
element.setAttribute("class", "highlight square-""" + str(second) + """");
document.getElementsByClassName("board")[0].appendChild(element)
""")
def play_game(board, our_color):
_engine = chess.engine.SimpleEngine.popen_uci(os.path.join(SCRIPT_DIR, 'bin', config["engine"]["Binary"]))
# _engine.configure({
# "Skill Level": int(config["engine"]["Skill"]),
# "Hash": int(config["engine"]["Hash"])})
print("[INFO] :: Setting up initial position.")
move_val = 1
move_number = get_previous_moves(board)
print("[INFO] :: Ready.")
# while game is in progress (no rematch/analysis button, etc)
while not check_exists_by_class("board-modal-container-container"):
our_turn = False
if board.turn and our_color == "W":
our_turn = True
elif not board.turn and our_color == "B":
our_turn = True
previous_move_number = move_number
need_draw_square = True
# only get best move once
if our_turn:
with chess.polyglot.open_reader(os.path.join(SCRIPT_DIR, 'bin', 'polyglots', config["engine"]["Polyglot"] + ".bin")) as reader:
move = reader.get(board)
if move is not None:
result = move
else:
# depth=25, nodes=3500000 for stockfish
result = _engine.play(board, chess.engine.Limit(depth=config["engine"]["Depth"].isdigit(), nodes=config["engine"]["Nodes"].isdigit()), game=object, info=chess.engine.INFO_NONE)
while our_turn:
if previous_move_number != move_number:
break
# check for made move
move = check_exists_by_xpath("//vertical-move-list/div[" + str(move_val) + "]/div[" + str(move_number) + "]")
if move:
clear_square()
move = driver.find_element(By.XPATH, "//vertical-move-list/div[" + str(move_val) + "]/div[" + str(move_number) + "]").text
uci = board.push_san(move)
print(str(ceil(move_val)) + '. ' + str(uci.uci()) + ' [us]')
if move_number >= 3:
move_number = 1
move_val += 1
else:
move_number += 2
else:
if need_draw_square:
draw_square(result)
need_draw_square = False
else:
clear_square()
opp_moved = check_exists_by_xpath("//vertical-move-list/div[" + str(move_val) + "]/div[" + str(move_number) + "]")
if opp_moved:
opp_move = driver.find_element(By.XPATH, "//vertical-move-list/div[" + str(move_val) + "]/div[" + str(move_number) + "]").text
uci = board.push_san(opp_move)
print(str(ceil(move_val)) + '. ' + uci.uci())
if move_number >= 3:
move_number = 1
move_val += 1
else:
move_number += 2
# game complete
_engine.quit()
print("[INFO] :: Game complete. Waiting for new game to start.")
new_game(board)
def sign_in():
driver.get("https://www.chess.com/login")
username = driver.find_element(By.ID, "username")
password = driver.find_element(By.ID, "password")
username.send_keys(config["chess"]["Username"])
password.send_keys(config["chess"]["Password"])
driver.find_element(By.XPATH, "/html/body/div/div/main/div/form/button").click() # submit
def main():
os.path.isfile("./config.ini")
config.read("config.ini")
board = chess.Board()
driver.get("https://www.chess.com/home")
sign_in()
new_game(board)
if __name__ == "__main__":
main()
You will also have to put the button logic back, I didn't like it.
So yeah bot games need to be fixed, I don't know how and the squares have to be changed to arrows.
@DarkReaper231 not sure if your still interested in this project but i recently made something similar with chess.com that needed to use arrows. what i did was instead of using chess.com arrows i used lichess' arrows. the svg lichess used is compatible with chess.com board elements. first inject svg in the board (this is the same svg used in lichess)
document.getElementsByClassName("board")[0].insertAdjacentHTML('afterbegin', `
<svg class="cg-shapes" viewBox="-4 -4 8 8" preserveAspectRatio="xMidYMid slice">
<defs>
<marker id="arrowhead-g" orient="auto" markerWidth="4" markerHeight="8" refX="2.05" refY="2.01" cgkey="g"><path d="M0,0 V4 L3,2 Z" fill="#15781B"></path></marker>
</defs>
<g>
</g>
</svg>
`)
you can then draw arrows with this function eg. getCoords('d3h7')
function getCoords(move) {
var firstL = move[0].charCodeAt(0) - 97
var firstN = move[1] - 1
var secondL = move[2].charCodeAt(0) - 97
var secondN = move[3] - 1
var coords = {
x1: String(-3.5 + firstL),
y1: String(3.5 - firstN),
x2: String(-3.5 + secondL),
y2: String(3.5 - secondN),
}
var arrowElement = `
<line stroke="#15781B" stroke-width="0.15625" stroke-linecap="round" marker-end="url(#arrowhead-g)" opacity="1" x1="${coords.x1}" y1="${coords.y1}" x2="${coords.x2}" y2="${coords.y2}" cgHash="688,688,a1,a3,green"></line>
`
document.querySelector("#board-single > svg.cg-shapes > g").innerHTML = arrowElement
}
remember to get the correct selector when you inject arrow elements as board id will change depending on the game
document.querySelector("#board-single > svg.cg-shapes > g")
if you play against other players it will be #board-single
if its against bots its #board-vs-personalities
To simplify it we could have two python scripts: lichess.py, chess.py (that might conflict with the chess library tho)
A lot of reference can be found here: https://github.com/IOKernel/Sangatsu
I would like your python script rather than Sangatsu mostly because your is more universal. It works on bot games, games with friends etc. Sangatsu crashes if you don't load chess.com/play/online or chess.com/play/computer which you have to hardcode since it looks for specific classes. But you could just use xpath /html/body/div/div/chess-board
Overall I think your code base would be more stable for chess.com than Sangatsu and would allow free movement on the website instead of having to restart it everytime I leave to homepage.
In the config.ini we could have new entry [chess] for chess.com credentials