Skip to content

Commit

Permalink
Merge pull request #234 from Mk-Chan/master
Browse files Browse the repository at this point in the history
Added offer_draw() API and DrawHandler
  • Loading branch information
niklasf committed Nov 20, 2017
2 parents ad36dc8 + 82574af commit 256e27b
Show file tree
Hide file tree
Showing 2 changed files with 204 additions and 2 deletions.
125 changes: 123 additions & 2 deletions chess/xboard.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,89 @@
import chess


RESULTS = [DRAW, WHITE_WIN, BLACK_WIN] = ["1/2-1/2", "1-0", "0-1"]


class DrawHandler(object):
"""
Chess engines may send a draw offer after playing it's move and may recieve
one during an offer during it's calculations. A draw handler can be used to
send, or react to, this information.
>>> # Register a standard draw handler.
>>> draw_handler = chess.xboard.DrawHandler()
>>> engine.draw_handler = draw_handler
>>> # Start a search.
>>> engine.setboard(board)
>>> engine.st(1)
>>> engine.go()
e2e4
offer draw
>>>
>>> # Do some relevant work.
>>> # Check if a draw offer is pending at any given time.
>>> draw_handler.pending_offer
True
See :attr:`~chess.xboard.DrawHandler.pending_offer` for a way to access
this flag in a thread-safe way during search.
If you want to be notified whenever new information is available
you would usually subclass the *DrawHandler* class:
>>> class MyHandler(chess.xboard.DrawHandler):
... def offer_draw(self):
... # Called whenever `offer draw` has been processed.
... super(MyHandler, self).offer_draw()
... print(self.pending_offer)
"""
def __init__(self):
self.lock = threading.Lock()
self.pending_offer = False

def pre_offer(self):
"""
Received a new draw offer about to be processed.
When subclassing remember to call this method of the parent class in
order to keep the locking in tact.
"""
self.lock.acquire()

def post_offer(self):
"""
Processing of a draw offer has been finished.
When subclassing remember to call this method of the parent class in
order to keep the locking in tact.
"""
self.lock.release()

def offer_draw(self):
"""A draw has been offered."""
with self.lock:
self.pending_offer = True

def clear_offer(self):
"""The draw offer has expired."""
with self.lock:
self.pending_offer = False

def acquire(self, blocking=True):
return self.lock.acquire(blocking)

def release(self):
return self.lock.release()

def __enter__(self):
self.acquire()
return self.pending_offer

def __exit__(self, exc_type, exc_value, traceback):
self.release()


class PostHandler(object):
"""
Chess engines may send information about their calculations if enabled
Expand Down Expand Up @@ -213,10 +296,11 @@ def __init__(self, Executor=concurrent.futures.ThreadPoolExecutor):
self.author = None
self.features = FeatureMap()
self.pong = threading.Event()
self.ping_num = None
self.ping_num = 123
self.pong_received = threading.Condition()
self.auto_force = False
self.in_force = False
self.end_result = None

self.move = None
self.move_received = threading.Event()
Expand All @@ -225,6 +309,8 @@ def __init__(self, Executor=concurrent.futures.ThreadPoolExecutor):
self.terminated = threading.Event()

self.post_handlers = []
self.draw_handler = None
self.engine_offered_draw = False

self.pool = Executor(max_workers=3)
self.process = None
Expand Down Expand Up @@ -255,6 +341,8 @@ def on_line_received(self, buf):
return self._pong(command_and_args[1])
elif command_and_args[0] == "move":
return self._move(command_and_args[1])
elif command_and_args[0] == "offer" and command_and_args[1] == "draw":
return self._offer_draw()
elif len(command_and_args) >= 5:
return self._post(buf)

Expand All @@ -270,6 +358,14 @@ def on_terminated(self):
with self.state_changed:
self.state_changed.notify_all()

def _offer_draw(self):
if self.draw_handler:
if self.draw_handler.pending_offer and not self.engine_offered_draw:
self._end_game(DRAW)
else:
self.engine_offered_draw = True
self.draw_handler.offer_draw()

def _feature(self, features):
"""
Does not conform to CECP spec regarding `done` and instead reads all
Expand Down Expand Up @@ -334,6 +430,9 @@ def _move(self, arg):
LOGGER.exception("exception parsing move")

self.move_received.set()
if self.draw_handler:
self.draw_handler.clear_offer()
self.engine_offered_draw = False
for post_handler in self.post_handlers:
post_handler.on_move(self.move)

Expand Down Expand Up @@ -412,6 +511,10 @@ def _assert_not_busy(self, cmd):
if not self.idle:
raise EngineStateException("{} command while engine is busy", cmd)

def _end_game(self, result):
self.end_result = result
self.stop()

def command(self, msg):
def cmd():
with self.semaphore:
Expand All @@ -433,7 +536,6 @@ def ping(self, async_callback=None):
def command():
with self.semaphore:
with self.pong_received:
self.ping_num = random.randint(1, 100)
self.send_line("ping " + str(self.ping_num))
self.pong_received.wait()

Expand All @@ -442,6 +544,21 @@ def command():

return self._queue_command(command, async_callback)

def offer_draw(self, async_callback=None):
"""
Command used to offer the engine a draw.
The engine may respond with `offer draw` to agree and may ignore the
offer to disagree.
:return: Nothing
"""
self._assert_supports_feature("draw")
if self.draw_handler:
self.draw_handler.offer_draw()
command = self.command("offer draw")
return self._queue_command(command, async_callback)

def pondering(self, ponder, async_callback=None):
"""
Tell the engine whether to ponder or not.
Expand Down Expand Up @@ -953,6 +1070,10 @@ def usermove(self, move, async_callback=None):
if self.features.supports("usermove"):
builder.append("usermove")

if self.draw_handler:
self.draw_handler.clear_offer()
self.engine_offered_draw = False

if self.auto_force:
self.force()
elif not self.in_force:
Expand Down
81 changes: 81 additions & 0 deletions test.py
Original file line number Diff line number Diff line change
Expand Up @@ -2411,6 +2411,87 @@ def test_async_terminate(self):
self.assertTrue(command.done())


class XboardEngineTestCase(unittest.TestCase):

def setUp(self):
self.engine = chess.xboard.Engine()
self.mock = chess.engine.MockProcess(self.engine)
self.mock.expect("xboard")
self.mock.expect("protover 2")
self.mock.expect("post")
self.mock.expect("easy")
self.mock.expect("ping 123", ("pong 123", ))
self.engine.xboard()
self.mock.assert_done()

def tearDown(self):
self.engine.terminate()
self.mock.assert_terminated()

def test_engine_offer_draw_during_engine_search(self):
draw_handler = chess.xboard.DrawHandler()
self.engine.draw_handler = draw_handler

self.mock.expect("nopost")
self.engine.nopost()
self.mock.expect("st 10")
self.engine.st(10)
self.mock.expect("go", ("offer draw", ))
self.engine.go(async_callback=True)

time.sleep(0.01)

self.assertEqual(draw_handler.pending_offer, True)
self.assertEqual(self.engine.engine_offered_draw, True)

self.mock.expect("?", ("move e2e4", ))
self.engine._end_game(chess.xboard.DRAW)

self.assertEqual(self.engine.end_result, chess.xboard.DRAW)
self.mock.assert_done()

def test_engine_offer_draw_during_human_turn(self):
draw_handler = chess.xboard.DrawHandler()
self.engine.draw_handler = draw_handler

self.mock.expect("nopost")
self.engine.nopost()
self.mock.expect("st 1")
self.engine.st(1)
self.mock.expect("go", ("move e2e4", "offer draw", ))
self.engine.go()

time.sleep(0.01)

self.assertEqual(draw_handler.pending_offer, True)
self.assertEqual(self.engine.engine_offered_draw, True)

self.engine._end_game(chess.xboard.DRAW)

self.assertEqual(self.engine.end_result, chess.xboard.DRAW)
self.mock.assert_done()

def test_human_offer_draw_during_engine_search(self):
draw_handler = chess.xboard.DrawHandler()
self.engine.draw_handler = draw_handler

self.mock.expect("nopost")
self.engine.nopost()
self.mock.expect("st 10")
self.engine.st(10)
self.mock.expect("go")
self.engine.go(async_callback=True)

self.mock.expect("offer draw", ("offer draw", ))
self.mock.expect("?")
self.engine.offer_draw()

time.sleep(0.01)

self.assertEqual(self.engine.end_result, chess.xboard.DRAW)
self.mock.assert_done()


class UciEngineTestCase(unittest.TestCase):

def setUp(self):
Expand Down

0 comments on commit 256e27b

Please sign in to comment.