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:
- Sometimes the last move fails because the bot won't to start a new game before it can checkmate
- Promoting doesn't work yet :/
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)