Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Match.run() fix, add tests, parses bestmove's info #13

Merged
merged 3 commits into from
Nov 23, 2016
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
123 changes: 95 additions & 28 deletions pystockfish.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,12 @@
:license: GNU General Public License, see LICENSE for more details.
"""

import re
import subprocess
from random import randint
MAX_MOVES = 200
UCI_MOVE_REGEX = "[a-h]\d[a-h]\d[qrnb]?"
PV_REGEX = " pv (?P<move_list>{0}( {0})*)".format(UCI_MOVE_REGEX)


class Match:
Expand Down Expand Up @@ -48,7 +52,12 @@ def __init__(self, engines):
self.winner_name = None

def move(self):
if len(self.moves) > 200:
"""
Advance game by single move, if possible.

@return: logical indicator if move was performed.
"""
if len(self.moves) == MAX_MOVES:
return False
elif len(self.moves) % 2:
active_engine = self.black_engine
Expand All @@ -67,19 +76,18 @@ def move(self):
ponder = movedict.get('ponder')
self.moves.append(bestmove)

if info["score"]["eval"] == "mate":
matenum = info["score"]["value"]
if matenum > 0:
self.winner_engine = active_engine
self.winner = active_engine_name
elif matenum < 0:
self.winner_engine = inactive_engine
self.winner = inactive_engine_name
return False

if ponder != '(none)':
return True
else:
mateloc = info.find('mate')
if mateloc >= 0:
matenum = int(info[mateloc + 5])
if matenum > 0:
self.winner_engine = active_engine
self.winner = active_engine_name
elif matenum < 0:
self.winner_engine = inactive_engine
self.winner = inactive_engine_name
return False

def run(self):
"""
Expand Down Expand Up @@ -180,7 +188,7 @@ def setposition(self, moves=[]):
"""
Move list is a list of moves (i.e. ['e2e4', 'e7e5', ...]) each entry as a string. Moves must be in full algebraic notation.
"""
self.put('position startpos moves %s' % self._movelisttostr(moves))
self.put('position startpos moves %s' % Engine._movelisttostr(moves))
self.isready()

def setfenposition(self, fen):
Expand All @@ -193,31 +201,90 @@ def setfenposition(self, fen):
def go(self):
self.put('go depth %s' % self.depth)

def _movelisttostr(self, moves):
@staticmethod
def _movelisttostr(moves):
"""
Concatenates a list of strings
Concatenates a list of strings.

This is format in which stockfish "setoption setposition" takes move input.
"""
movestr = ''
for h in moves:
movestr += h + ' '
return movestr.strip()
return ' '.join(moves)

def bestmove(self):
last_line = ""
"""
Get proposed best move for current position.

@return: dictionary with 'move', 'ponder', 'info' containing best move's UCI notation,
ponder value and info dictionary.
"""
self.go()
last_info = ""
while True:
text = self.stdout.readline().strip()
split_text = text.split(' ')
if split_text[0] == 'bestmove':
if len(split_text)>=3:
ponder=split_text[3]
else:
ponder=None
print(text)
if split_text[0] == "info":
last_info = Engine._bestmove_get_info(text)
if split_text[0] == "bestmove":
ponder = None if len(split_text[0]) < 3 else split_text[3]
return {'move': split_text[1],
'ponder': ponder,
'info': last_line
}
last_line = text
'info': last_info}

@staticmethod
def _bestmove_get_info(text):
"""
Parse stockfish evaluation output as dictionary.

Examples of input:

"info depth 2 seldepth 3 multipv 1 score cp -656 nodes 43 nps 43000 tbhits 0 \
time 1 pv g7g6 h3g3 g6f7"

"info depth 10 seldepth 12 multipv 1 score mate 5 nodes 2378 nps 1189000 tbhits 0 \
time 2 pv h3g3 g6f7 g3c7 b5d7 d1d7 f7g6 c7g3 g6h5 e6f4"
"""
result_dict = Engine._get_info_pv(text)
result_dict.update(Engine._get_info_score(text))

single_value_fields = ['depth', 'seldepth', 'multipv', 'nodes', 'nps', 'tbhits', 'time']
for field in single_value_fields:
result_dict.update(Engine._get_info_singlevalue_subfield(text, field))

return result_dict

@staticmethod
def _get_info_singlevalue_subfield(info, field):
"""
Helper function for _bestmove_get_info.

Extracts (integer) values for single value fields.
"""
search = re.search(pattern=field + " (?P<value>\d+)", string=info)
return {field: int(search.group("value"))}

@staticmethod
def _get_info_score(info):
"""
Helper function for _bestmove_get_info.

Example inputs:

score cp -100 <- engine is behind 100 centipawns
score mate 3 <- engine has big lead or checkmated opponent
"""
search = re.search(pattern="score (?P<eval>\w+) (?P<value>-?\d+)", string=info)
return {"score": {"eval": search.group("eval"), "value": int(search.group("value"))}}

@staticmethod
def _get_info_pv(info):
"""
Helper function for _bestmove_get_info.

Extracts "pv" field from bestmove's info and returns move sequence in UCI notation.
"""
search = re.search(pattern=PV_REGEX, string=info)
return {"pv": search.group("move_list")}

def isready(self):
"""
Expand Down
1 change: 1 addition & 0 deletions test/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@

17 changes: 17 additions & 0 deletions test/test_functional.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
from pystockfish import *


def quick_checkmate_test():
# 1. e4 e5 2. Bc4 Nc6 3. Qf3 d6
e1 = Engine(depth=6)
e2 = Engine(depth=6)

# match creation must be before calling setposition as Match.__init__ resets position
m = Match(engines={"e1": e1, "e2": e2})

e1.setposition(['e2e4', 'e7e5', 'f1c4', 'b8c6', 'd1f3', 'd7d6'])
e2.setposition(['e2e4', 'e7e5', 'f1c4', 'b8c6', 'd1f3', 'd7d6'])

m.run()
assert m.winner in {"e1", "e2"}