wtfseanscool / lichess-autoplay

Autoplay bot/cheat for lichess.org, created with Python
GNU General Public License v3.0
4 stars 0 forks source link

Make chess.com port #3

Open SolsticeSpectrum opened 1 year ago

SolsticeSpectrum commented 1 year ago

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

SolsticeSpectrum commented 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>
SolsticeSpectrum commented 1 year ago

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

SolsticeSpectrum commented 1 year ago

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

wtfseanscool commented 1 year ago

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.

SolsticeSpectrum commented 1 year ago

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

SolsticeSpectrum commented 1 year ago

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)
SolsticeSpectrum commented 1 year ago

I will have to leave the rest on you because I am barely a python beginner

SolsticeSpectrum commented 1 year ago

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

image looks functional to me so far

SolsticeSpectrum commented 1 year ago

//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

SolsticeSpectrum commented 1 year ago

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.

SolsticeSpectrum commented 1 year ago

So yeah bot games need to be fixed, I don't know how and the squares have to be changed to arrows.

LukasOgunfeitimi commented 1 year ago

@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