@@ -1281,27 +1281,13 @@ def _bestmove(self, engine: UciProtocol, arg: str) -> None:
12811281 if self .pondering :
12821282 self .pondering = False
12831283 elif not self .result .cancelled ():
1284- tokens = arg .split (None , 2 )
1284+ best = _parse_uci_bestmove (engine .board , arg )
1285+ self .result .set_result (PlayResult (best .move , best .ponder , self .info ))
12851286
1286- bestmove = None
1287- if tokens [0 ] != "(none)" :
1288- try :
1289- bestmove = engine .board .parse_uci (tokens [0 ])
1290- except ValueError as err :
1291- raise EngineError (err )
1292-
1293- pondermove = None
1294- if bestmove is not None and len (tokens ) >= 3 and tokens [1 ] == "ponder" and tokens [2 ] != "(none)" :
1295- engine .board .push (bestmove )
1296- try :
1297- pondermove = engine .board .push_uci (tokens [2 ])
1298- except ValueError :
1299- LOGGER .exception ("engine sent invalid ponder move" )
1300-
1301- self .result .set_result (PlayResult (bestmove , pondermove , self .info ))
1302-
1303- if ponder and pondermove :
1287+ if ponder and best .move and best .ponder :
13041288 self .pondering = True
1289+ engine .board .push (best .move )
1290+ engine .board .push (best .ponder )
13051291 engine ._position (engine .board )
13061292 engine ._go (limit , ponder = True )
13071293
@@ -1369,8 +1355,9 @@ def _info(self, engine: UciProtocol, arg: str) -> None:
13691355 def _bestmove (self , engine : UciProtocol , arg : str ) -> None :
13701356 if not self .result .done ():
13711357 raise EngineError ("was not searching, but engine sent bestmove" )
1358+ best = _parse_uci_bestmove (engine .board , arg )
13721359 self .set_finished ()
1373- self .analysis .set_finished ()
1360+ self .analysis .set_finished (best )
13741361
13751362 def cancel (self , engine : UciProtocol ) -> None :
13761363 engine .send_line ("stop" )
@@ -1479,6 +1466,27 @@ def _parse_uci_info(arg: str, root_board: chess.Board, selector: Info = INFO_ALL
14791466
14801467 return info
14811468
1469+ def _parse_uci_bestmove (board : chess .Board , args : str ) -> "BestMove" :
1470+ tokens = args .split (None , 2 )
1471+
1472+ move = None
1473+ ponder = None
1474+ if tokens [0 ] != "(none)" :
1475+ try :
1476+ move = board .push_uci (tokens [0 ])
1477+ except ValueError as err :
1478+ raise EngineError (err )
1479+
1480+ try :
1481+ if len (tokens ) >= 3 and tokens [1 ] == "ponder" and tokens [2 ] != "(none)" :
1482+ ponder = board .parse_uci (tokens [2 ])
1483+ except ValueError :
1484+ LOGGER .exception ("engine sent invalid ponder move" )
1485+ finally :
1486+ board .pop ()
1487+
1488+ return BestMove (move , ponder )
1489+
14821490
14831491class UciOptionMap (MutableMapping [str , T ]):
14841492 """Dictionary with case-insensitive keys."""
@@ -1868,6 +1876,7 @@ async def analysis(self, board: chess.Board, limit: Optional[Limit] = None, *, m
18681876 class XBoardAnalysisCommand (BaseCommand [XBoardProtocol , AnalysisResult ]):
18691877 def start (self , engine : XBoardProtocol ) -> None :
18701878 self .stopped = False
1879+ self .best_move : Optional [chess .Move ] = None
18711880 self .analysis = AnalysisResult (stop = lambda : self .cancel (engine ))
18721881 self .final_pong : Optional [str ] = None
18731882
@@ -1908,6 +1917,10 @@ def _post(self, engine: XBoardProtocol, line: str) -> None:
19081917 post_info = _parse_xboard_post (line , engine .board , info | INFO_BASIC )
19091918 self .analysis .post (post_info )
19101919
1920+ pv = post_info .get ("pv" )
1921+ if pv :
1922+ self .best_move = pv [0 ]
1923+
19111924 if limit is not None :
19121925 if limit .time is not None and typing .cast (float , post_info .get ("time" , 0 )) >= limit .time :
19131926 self .cancel (engine )
@@ -1924,7 +1937,7 @@ def end(self, engine: XBoardProtocol) -> None:
19241937 self .time_limit_handle .cancel ()
19251938
19261939 self .set_finished ()
1927- self .analysis .set_finished ()
1940+ self .analysis .set_finished (BestMove ( self . best_move , None ) )
19281941
19291942 def cancel (self , engine : XBoardProtocol ) -> None :
19301943 if self .stopped :
@@ -2045,7 +2058,7 @@ def _parse_xboard_post(line: str, root_board: chess.Board, selector: Info = INFO
20452058 pv_tokens .insert (0 , token )
20462059 break
20472060
2048- if len (integer_tokens ) < 4 or not selector :
2061+ if len (integer_tokens ) < 4 :
20492062 return info
20502063
20512064 # Required integer tokens.
@@ -2079,9 +2092,6 @@ def _parse_xboard_post(line: str, root_board: chess.Board, selector: Info = INFO
20792092 info ["tbhits" ] = integer_tokens .pop (0 )
20802093
20812094 # Principal variation.
2082- if not (selector & INFO_PV ):
2083- return info
2084-
20852095 pv = []
20862096 board = root_board .copy (stack = False )
20872097 for token in pv_tokens :
@@ -2092,11 +2102,26 @@ def _parse_xboard_post(line: str, root_board: chess.Board, selector: Info = INFO
20922102 pv .append (board .push_xboard (token ))
20932103 except ValueError :
20942104 break
2105+
2106+ if not (selector & INFO_PV ):
2107+ break
20952108 info ["pv" ] = pv
20962109
20972110 return info
20982111
20992112
2113+ class BestMove :
2114+ """Returned by :func:`chess.engine.AnalysisResult.wait()`."""
2115+
2116+ def __init__ (self , move : Optional [chess .Move ], ponder : Optional [chess .Move ]):
2117+ self .move = move
2118+ self .ponder = ponder
2119+
2120+ def __repr__ (self ) -> str :
2121+ return "<{} at {:#x} (move={}, ponder={}>" .format (
2122+ type (self ).__name__ , id (self ), self .move , self .ponder )
2123+
2124+
21002125class AnalysisResult :
21012126 """
21022127 Handle to ongoing engine analysis.
@@ -2112,7 +2137,7 @@ def __init__(self, stop: Optional[Callable[[], None]] = None):
21122137 self ._queue : asyncio .Queue [InfoDict ] = asyncio .Queue ()
21132138 self ._posted_kork = False
21142139 self ._seen_kork = False
2115- self ._finished : asyncio .Future [None ] = asyncio .Future ()
2140+ self ._finished : asyncio .Future [BestMove ] = asyncio .Future ()
21162141 self .multipv : List [InfoDict ] = [{}]
21172142
21182143 def post (self , info : InfoDict ) -> None :
@@ -2132,9 +2157,9 @@ def _kork(self):
21322157 self ._posted_kork = True
21332158 self ._queue .put_nowait ({})
21342159
2135- def set_finished (self ) -> None :
2160+ def set_finished (self , best : BestMove ) -> None :
21362161 if not self ._finished .done ():
2137- self ._finished .set_result (None )
2162+ self ._finished .set_result (best )
21382163 self ._kork ()
21392164
21402165 def set_exception (self , exc : Exception ) -> None :
@@ -2151,9 +2176,9 @@ def stop(self) -> None:
21512176 self ._stop ()
21522177 self ._stop = None
21532178
2154- async def wait (self ) -> None :
2179+ async def wait (self ) -> BestMove :
21552180 """Waits until the analysis is complete (or stopped)."""
2156- await self ._finished
2181+ return await self ._finished
21572182
21582183 async def get (self ) -> InfoDict :
21592184 """
@@ -2464,7 +2489,7 @@ def stop(self) -> None:
24642489 with self .simple_engine ._not_shut_down ():
24652490 self .simple_engine .protocol .loop .call_soon_threadsafe (self .inner .stop )
24662491
2467- def wait (self ) -> None :
2492+ def wait (self ) -> BestMove :
24682493 with self .simple_engine ._not_shut_down ():
24692494 future = asyncio .run_coroutine_threadsafe (self .inner .wait (), self .simple_engine .protocol .loop )
24702495 return future .result ()
0 commit comments