22.05.2014: Updated the bot, should work better now
Hi everyone!
I was in a coding mood during Easter and decided to write a small chess bot with selenium and stockfish engine to cheat a bit on lichess.org.
I think the code is pretty self explanatory and I won't discuss it in depth here. You can tweak the config, the comments should explain what the parameters do.
The config is in the beginning of the code, so modify it there. You should maybe modify it to use your username and password. Make sure that you download stockfish and install it. Then supply the correct path in the 'stockfish_binary' parameter.
As always: Have fun!
Some open issues:
Here is the code:
__author__ = 'nikolai' __date__ = 'Easter 2014' config = { 'username' : 'probably_a_spider', # the login username 'password' : 'somepwd', # the login password 'stockfish_binary' : '/home/nikolai/PycharmProjects/LichessBot/stockfish-dd-src/src/stockfish', # the path to your local stockfish binary #Set to true if the bot should play forever 'pwn_forever' : True, # if the bot should play endlessly 'min_per_side' : 1, # how long each player may play 'increment' : 0, # the increment per move 'thinking_time': .5, # How long stockfish is allowd to search, set to None and stockfish will search 0.75 seconds by default 'thinking_skew': .2 # This is the maximal random derivative in decimal of the thinking time (0-1) (to not make the bot to appear to move in the same time intervals all the time) } from selenium import webdriver from selenium.webdriver import ActionChains from selenium.webdriver.common.by import By from selenium.webdriver.support.ui import WebDriverWait from selenium.webdriver.support import expected_conditions as EC from selenium.common.exceptions import TimeoutException, NoSuchElementException, UnexpectedAlertPresentException import subprocess import os import time import sys import re import random def get(proc, poll=True): if poll: proc.stdin.write('isready\n') buf = '' while True: line = proc.stdout.readline().strip() buf += line if 'readyok' in line: return buf if 'bestmove' in line: return buf def init_stockfish(): if os.path.exists(config['stockfish_binary']): proc = subprocess.Popen([config['stockfish_binary']], universal_newlines=True, stdout=subprocess.PIPE, stdin=subprocess.PIPE) greeting = get(proc) if not 'Stockfish' in greeting: raise ValueError('Couldnt execute stockfish') proc.stdin.write('uci\n') get(proc) # stolen from https://github.com/brandonhsiao/lichess-bot/blob/master/server.py proc.stdin.write('ucinewgame\n') proc.stdin.write('setoption name Hash value 128\n') proc.stdin.write('setoption name Threads value 4\n') proc.stdin.write('setoption name Best Book Move value true\n') proc.stdin.write('setoption name Aggressiveness value 200\n') proc.stdin.write('setoption name Cowardice value 0\n') proc.stdin.write('setoption name Contempt Factor value 50\n') return proc proc = init_stockfish() def make_move(thinking_time=.5, moves=[]): if moves: cmd = 'position startpos moves {}\n'.format(' '.join(moves)) proc.stdin.write(cmd) proc.stdin.write('go infinite\n') try: time.sleep(float(thinking_time)) except ValueError as ve: print(ve) sys.exit(0) proc.stdin.write('stop\n') out = get(proc, False) try: bestmove = re.search(r'bestmove\s(?P[a-h][1-8][a-h][1-8])', out).group('move') ponder = re.search(r'ponder\s(?P[a-h][1-8][a-h][1-8])', out).group('ponder') except AttributeError: return False return bestmove def newgame_stockfish(): proc.stdin.write('ucinewgame\n') get(proc) def quit_stockfish(): proc.stdin.write('quit\n') proc.terminate() def login(): wd = webdriver.Firefox() wd.get('http://en.lichess.org/login') u, p = wd.find_element_by_name("username"), wd.find_element_by_name("password") u.send_keys(config['username']) p.send_keys(config['password']) p.submit() return wd def create_game(wd, min_per_side=config['min_per_side'], increment=config['increment']): wait = WebDriverWait(wd, 10) try: element = wait.until(EC.element_to_be_clickable((By.CSS_SELECTOR,'a[title="Create a game"]'))) element.click() except UnexpectedAlertPresentException: pass except Exception as e: print(e) WebDriverWait(wd, 5).until(EC.presence_of_element_located((By.ID, 'increment'))); # Change the inputs with a bit of jQuery (but wait first until the dom bitch is loaded) wd.execute_script("$('#time').val('{}')".format(str(min_per_side))) wd.execute_script("$('#increment').val('{}')".format(str(increment))) time.sleep(1) wd.find_element_by_name('color').submit() def play(wd): def move(moves=[]): t = config['thinking_time'] or .75 if config['thinking_skew']: t = t * (1 - random.randint(0, config['thinking_skew']*1000) / 1000) move = make_move(thinking_time=t, moves=moves) if move: print('[i]Bot is going to make move {} by calculating {} seconds'.format(move, float(t))) f, t = move[:2], move[2:] for i in range(6): action = ActionChains(wd) action.drag_and_drop(wd.find_element_by_id(f), wd.find_element_by_id(t)) action.perform() try: moved = wd.find_elements_by_css_selector('div.moved.lcs') move1, move2 = moved[0].get_attribute('id'), moved[1].get_attribute('id') if t == move1 or f == move1: break except: break time.sleep(.2) return move else: return False def get_last_move(moves=[]): WebDriverWait(wd, 100).until(EC.presence_of_element_located((By.CSS_SELECTOR, 'div.moved.lcs'))) t = wd.find_element_by_css_selector('div.moved.lcs div.piece').find_element_by_xpath('..').get_attribute('id') f = [e.get_attribute('id') for e in wd.find_elements_by_css_selector('div.moved.lcs') if e.get_attribute('id') != t][0] # If last move was my last move, sleep a bit and try again to get the next move if moves: print(f+t, moves[-1]) while f+t == moves[-1]: # busy polling, that's very inefficient time.sleep(.35) t = wd.find_element_by_css_selector('div.moved.lcs div.piece').find_element_by_xpath('..').get_attribute('id') f = [e.get_attribute('id') for e in wd.find_elements_by_css_selector('div.moved.lcs') if e.get_attribute('id') != t][0] try: wd.find_element_by_css_selector('div.table_with_clock.finished') return False except NoSuchElementException: pass print('[i]Opponent moved {}'.format(f+t)) return f+t try: # wait maximally 100 seconds for an opponent wait = WebDriverWait(wd, 100) # wait until the square a1 is present which means we got a board element = wait.until(EC.presence_of_element_located((By.ID,'a1'))) except TimeoutException as te: print("No board found: {}".format(te)) sys.exit(0) print('We got a board, games beginning...') moves = [] # detect what color we play players = wd.find_elements_by_css_selector('.player > a') is_black = False for e in players: is_black = config['username'] in e.get_attribute('href') and 'black' in e.get_attribute('class') print('[i]Bot is playing {}'.format(['white', 'black'][is_black])) if not is_black: moves.append(move()) while True: print('[i] Moves played: {}'.format(moves), end='\n\n') last = get_last_move(moves) print('last move found was {}'.format(last)) if last: moves.append(last) else: print('no last move appended. Leaving: {}'.format(last)) return m = move(moves) if m: moves.append(m) else: return try: wd.find_element_by_css_selector('div.table_with_clock.finished') return except: pass def go_forever(): wd = login() while True: create_game(wd) newgame_stockfish() play(wd) wd.get('http://en.lichess.org/') quit_stockfish() if __name__ == '__main__': if config['pwn_forever']: go_forever() else: wd = login() create_game(wd) play(wd)