@@ -438,6 +438,15 @@ class Info(enum.IntFlag):
438438INFO_ALL = Info .ALL
439439
440440
441+ @dataclasses .dataclass
442+ class Opponent :
443+ """Used to store information about an engine's opponent."""
444+ name : Optional [str ]
445+ title : Optional [str ]
446+ rating : Optional [int ]
447+ is_engine : Optional [bool ]
448+
449+
441450class PovScore :
442451 """A relative :class:`~chess.engine.Score` and the point of view."""
443452
@@ -1122,7 +1131,19 @@ async def configure(self, options: ConfigMapping) -> None:
11221131 """
11231132
11241133 @abc .abstractmethod
1125- async def play (self , board : chess .Board , limit : Limit , * , game : object = None , info : Info = INFO_NONE , ponder : bool = False , draw_offered : bool = False , root_moves : Optional [Iterable [chess .Move ]] = None , options : ConfigMapping = {}) -> PlayResult :
1134+ async def send_opponent_information (self , * , opponent : Optional [Opponent ] = None , engine_rating : Optional [int ] = None ) -> None :
1135+ """
1136+ Sends the engine information about its opponent. The information will
1137+ be sent after a new game is announced and before the first move. This
1138+ method should be called before the first move of a game--i.e., the
1139+ first call to :func:`chess.engine.Protocol.play()`.
1140+
1141+ :param opponent: Optional. The opponent's information.
1142+ :param engine_rating: Optional. This engine's own rating. Only used by XBoard engines.
1143+ """
1144+
1145+ @abc .abstractmethod
1146+ async def play (self , board : chess .Board , limit : Limit , * , game : object = None , info : Info = INFO_NONE , ponder : bool = False , draw_offered : bool = False , root_moves : Optional [Iterable [chess .Move ]] = None , options : ConfigMapping = {}, opponent : Optional [Opponent ] = None ) -> PlayResult :
11261147 """
11271148 Plays a position.
11281149
@@ -1148,6 +1169,10 @@ async def play(self, board: chess.Board, limit: Limit, *, game: object = None, i
11481169 analysis. The previous configuration will be restored after the
11491170 analysis is complete. You can permanently apply a configuration
11501171 with :func:`~chess.engine.Protocol.configure()`.
1172+ :param opponent: Optional. Information about a new opponent. Information
1173+ about the original opponent will be restored once the move is
1174+ complete. New opponent information can be made permanent with
1175+ :func:`~chess.engine.Protocol.send_opponent_information()`.
11511176 """
11521177
11531178 @typing .overload
@@ -1433,8 +1458,14 @@ def _id(self, engine: UciProtocol, arg: str) -> None:
14331458 def _isready (self ) -> None :
14341459 self .send_line ("isready" )
14351460
1461+ def _opponent_info (self ) -> None :
1462+ opponent_info = self .config .get ("UCI_Opponent" ) or self .target_config .get ("UCI_Opponent" )
1463+ if opponent_info :
1464+ self .send_line (f"setoption name UCI_Opponent value { opponent_info } " )
1465+
14361466 def _ucinewgame (self ) -> None :
14371467 self .send_line ("ucinewgame" )
1468+ self ._opponent_info ()
14381469 self .first_game = False
14391470 self .ponderhit = False
14401471
@@ -1481,7 +1512,8 @@ def _setoption(self, name: str, value: ConfigValue) -> None:
14811512 builder .append ("value" )
14821513 builder .append (str (value ))
14831514
1484- self .send_line (" " .join (builder ))
1515+ if name != "UCI_Opponent" : # sent after ucinewgame
1516+ self .send_line (" " .join (builder ))
14851517 self .config [name ] = value
14861518
14871519 def _configure (self , options : ConfigMapping ) -> None :
@@ -1500,6 +1532,18 @@ def start(self, engine: UciProtocol) -> None:
15001532
15011533 return await self .communicate (UciConfigureCommand )
15021534
1535+ def _opponent_configuration (self , * , opponent : Optional [Opponent ] = None , engine_rating : Optional [int ] = None ) -> ConfigMapping :
1536+ if opponent and opponent .name and "UCI_Opponent" in self .options :
1537+ rating = opponent .rating or "none"
1538+ title = opponent .title or "none"
1539+ player_type = "computer" if opponent .is_engine else "human"
1540+ return {"UCI_Opponent" : f"{ title } { rating } { player_type } { opponent .name } " }
1541+ else :
1542+ return {}
1543+
1544+ async def send_opponent_information (self , * , opponent : Optional [Opponent ] = None , engine_rating : Optional [int ] = None ) -> None :
1545+ return await self .configure (self ._opponent_configuration (opponent = opponent , engine_rating = engine_rating ))
1546+
15031547 def _position (self , board : chess .Board ) -> None :
15041548 # Select UCI_Variant and UCI_Chess960.
15051549 uci_variant = type (board ).uci_variant
@@ -1576,15 +1620,20 @@ def _go(self, limit: Limit, *, root_moves: Optional[Iterable[chess.Move]] = None
15761620 builder .append ("0000" )
15771621 self .send_line (" " .join (builder ))
15781622
1579- async def play (self , board : chess .Board , limit : Limit , * , game : object = None , info : Info = INFO_NONE , ponder : bool = False , draw_offered : bool = False , root_moves : Optional [Iterable [chess .Move ]] = None , options : ConfigMapping = {}) -> PlayResult :
1623+ async def play (self , board : chess .Board , limit : Limit , * , game : object = None , info : Info = INFO_NONE , ponder : bool = False , draw_offered : bool = False , root_moves : Optional [Iterable [chess .Move ]] = None , options : ConfigMapping = {}, opponent : Optional [Opponent ] = None ) -> PlayResult :
1624+ new_options : Dict [str , ConfigValue ] = {}
1625+ for name , value in options .items ():
1626+ new_options [name ] = value
1627+ new_options .update (self ._opponent_configuration (opponent = opponent ))
1628+
15801629 class UciPlayCommand (BaseCommand [UciProtocol , PlayResult ]):
15811630 def __init__ (self , engine : UciProtocol ):
15821631 super ().__init__ (engine )
15831632
15841633 # May ponderhit only in the same game and with unchanged target
15851634 # options. The managed options UCI_AnalyseMode, Ponder, and
15861635 # MultiPV never change between pondering play commands.
1587- engine .may_ponderhit = board if ponder and not engine .first_game and game == engine .game and not engine ._changed_options (options ) else None
1636+ engine .may_ponderhit = board if ponder and not engine .first_game and game == engine .game and not engine ._changed_options (new_options ) else None
15881637
15891638 def start (self , engine : UciProtocol ) -> None :
15901639 self .info : InfoDict = {}
@@ -1597,16 +1646,18 @@ def start(self, engine: UciProtocol) -> None:
15971646 engine .send_line ("ponderhit" )
15981647 return
15991648
1600- if "UCI_AnalyseMode" in engine .options and "UCI_AnalyseMode" not in engine .target_config and all (name .lower () != "uci_analysemode" for name in options ):
1649+ if "UCI_AnalyseMode" in engine .options and "UCI_AnalyseMode" not in engine .target_config and all (name .lower () != "uci_analysemode" for name in new_options ):
16011650 engine ._setoption ("UCI_AnalyseMode" , False )
16021651 if "Ponder" in engine .options :
16031652 engine ._setoption ("Ponder" , ponder )
16041653 if "MultiPV" in engine .options :
16051654 engine ._setoption ("MultiPV" , engine .options ["MultiPV" ].default )
16061655
1607- engine ._configure (options )
1656+ new_opponent = new_options .get ("UCI_Opponent" ) or engine .target_config .get ("UCI_Opponent" )
1657+ opponent_changed = new_opponent != engine .config .get ("UCI_Opponent" )
1658+ engine ._configure (new_options )
16081659
1609- if engine .first_game or engine .game != game :
1660+ if engine .first_game or engine .game != game or opponent_changed :
16101661 engine .game = game
16111662 engine ._ucinewgame ()
16121663 self .sent_isready = True
@@ -1942,6 +1993,9 @@ def __init__(self) -> None:
19421993 self .options = {
19431994 "random" : Option ("random" , "check" , False , None , None , None ),
19441995 "computer" : Option ("computer" , "check" , False , None , None , None ),
1996+ "name" : Option ("name" , "string" , "" , None , None , None ),
1997+ "engine_rating" : Option ("engine_rating" , "spin" , 0 , None , None , None ),
1998+ "opponent_rating" : Option ("opponent_rating" , "spin" , 0 , None , None , None )
19451999 }
19462000 self .config : Dict [str , ConfigValue ] = {}
19472001 self .target_config : Dict [str , ConfigValue ] = {}
@@ -2047,13 +2101,13 @@ def _variant(self, variant: Optional[str]) -> None:
20472101
20482102 self .send_line (f"variant { variant } " )
20492103
2050- def _new (self , board : chess .Board , game : object , options : ConfigMapping ) -> None :
2104+ def _new (self , board : chess .Board , game : object , options : ConfigMapping , opponent : Optional [ Opponent ] = None ) -> None :
20512105 self ._configure (options )
20522106
20532107 # Set up starting position.
20542108 root = board .root ()
2055- new_options = "random" in options or "computer" in options
2056- new_game = self .first_game or self .game != game or new_options or root != self .board .root ()
2109+ new_options = any ( param in options for param in ( "random" , "computer" ))
2110+ new_game = self .first_game or self .game != game or new_options or opponent or root != self .board .root ()
20572111 self .game = game
20582112 self .first_game = False
20592113 if new_game :
@@ -2068,7 +2122,16 @@ def _new(self, board: chess.Board, game: object, options: ConfigMapping) -> None
20682122
20692123 if self .config .get ("random" ):
20702124 self .send_line ("random" )
2071- if self .config .get ("computer" ):
2125+
2126+ opponent_name = (opponent .name if opponent else None ) or self .target_config .get ("name" )
2127+ if opponent_name and self .features .get ("name" , True ):
2128+ self .send_line (f"name { opponent_name } " )
2129+
2130+ opponent_rating = (opponent .rating if opponent else None ) or self .target_config .get ("opponent_rating" ) or 0
2131+ if self .target_config .get ("engine_rating" ) or opponent_rating :
2132+ self .send_line (f"rating { self .target_config .get ('engine_rating' ) or 0 } { opponent_rating } " )
2133+
2134+ if (opponent and opponent .is_engine ) or (self .target_config .get ("computer" ) if self .config .get ("computer" ) is None else self .config .get ("computer" )):
20722135 self .send_line ("computer" )
20732136
20742137 self .send_line ("force" )
@@ -2122,7 +2185,7 @@ def line_received(self, engine: XBoardProtocol, line: str) -> None:
21222185
21232186 return await self .communicate (XBoardPingCommand )
21242187
2125- async def play (self , board : chess .Board , limit : Limit , * , game : object = None , info : Info = INFO_NONE , ponder : bool = False , draw_offered : bool = False , root_moves : Optional [Iterable [chess .Move ]] = None , options : ConfigMapping = {}) -> PlayResult :
2188+ async def play (self , board : chess .Board , limit : Limit , * , game : object = None , info : Info = INFO_NONE , ponder : bool = False , draw_offered : bool = False , root_moves : Optional [Iterable [chess .Move ]] = None , options : ConfigMapping = {}, opponent : Optional [ Opponent ] = None ) -> PlayResult :
21262189 if root_moves is not None :
21272190 raise EngineError ("play with root_moves, but xboard supports 'include' only in analysis mode" )
21282191
@@ -2134,7 +2197,7 @@ def start(self, engine: XBoardProtocol) -> None:
21342197 self .pong_after_ponder : Optional [str ] = None
21352198
21362199 # Set game, position and configure.
2137- engine ._new (board , game , options )
2200+ engine ._new (board , game , options , opponent )
21382201
21392202 # Limit or time control.
21402203 clock = limit .white_clock if board .turn else limit .black_clock
@@ -2368,7 +2431,7 @@ def _setoption(self, name: str, value: ConfigValue) -> None:
23682431
23692432 self .config [name ] = value = option .parse (value )
23702433
2371- if name in ["random" , "computer" ]:
2434+ if name in ["random" , "computer" , "name" , "engine_rating" , "opponent_rating" ]:
23722435 # Applied in _new.
23732436 pass
23742437 elif name in ["memory" , "cores" ] or name .startswith ("egtpath " ):
@@ -2398,6 +2461,22 @@ def start(self, engine: XBoardProtocol) -> None:
23982461
23992462 return await self .communicate (XBoardConfigureCommand )
24002463
2464+ def _opponent_configuration (self , * , opponent : Optional [Opponent ] = None , engine_rating : Optional [int ] = None ) -> ConfigMapping :
2465+ if opponent is None :
2466+ return {}
2467+
2468+ opponent_info : Dict [str , Union [int , bool , str ]] = {"engine_rating" : engine_rating or 0 ,
2469+ "opponent_rating" : opponent .rating or 0 ,
2470+ "computer" : opponent .is_engine or False }
2471+
2472+ if opponent .name and self .features .get ("name" , True ):
2473+ opponent_info ["name" ] = f"{ opponent .title or '' } { opponent .name } " .strip ()
2474+
2475+ return opponent_info
2476+
2477+ async def send_opponent_information (self , * , opponent : Optional [Opponent ] = None , engine_rating : Optional [int ] = None ) -> None :
2478+ return await self .configure (self ._opponent_configuration (opponent = opponent , engine_rating = engine_rating ))
2479+
24012480 async def quit (self ) -> None :
24022481 self .send_line ("quit" )
24032482 await asyncio .shield (self .returncode )
@@ -2781,16 +2860,24 @@ def configure(self, options: ConfigMapping) -> None:
27812860 future = asyncio .run_coroutine_threadsafe (coro , self .protocol .loop )
27822861 return future .result ()
27832862
2863+ def send_opponent_information (self , * , opponent : Optional [Opponent ] = None , engine_rating : Optional [int ] = None ) -> None :
2864+ with self ._not_shut_down ():
2865+ coro = asyncio .wait_for (
2866+ self .protocol .send_opponent_information (opponent = opponent , engine_rating = engine_rating ),
2867+ self .timeout )
2868+ future = asyncio .run_coroutine_threadsafe (coro , self .protocol .loop )
2869+ return future .result ()
2870+
27842871 def ping (self ) -> None :
27852872 with self ._not_shut_down ():
27862873 coro = asyncio .wait_for (self .protocol .ping (), self .timeout )
27872874 future = asyncio .run_coroutine_threadsafe (coro , self .protocol .loop )
27882875 return future .result ()
27892876
2790- def play (self , board : chess .Board , limit : Limit , * , game : object = None , info : Info = INFO_NONE , ponder : bool = False , draw_offered : bool = False , root_moves : Optional [Iterable [chess .Move ]] = None , options : ConfigMapping = {}) -> PlayResult :
2877+ def play (self , board : chess .Board , limit : Limit , * , game : object = None , info : Info = INFO_NONE , ponder : bool = False , draw_offered : bool = False , root_moves : Optional [Iterable [chess .Move ]] = None , options : ConfigMapping = {}, opponent : Optional [ Opponent ] = None ) -> PlayResult :
27912878 with self ._not_shut_down ():
27922879 coro = asyncio .wait_for (
2793- self .protocol .play (board , limit , game = game , info = info , ponder = ponder , draw_offered = draw_offered , root_moves = root_moves , options = options ),
2880+ self .protocol .play (board , limit , game = game , info = info , ponder = ponder , draw_offered = draw_offered , root_moves = root_moves , options = options , opponent = opponent ),
27942881 self ._timeout_for (limit ))
27952882 future = asyncio .run_coroutine_threadsafe (coro , self .protocol .loop )
27962883 return future .result ()
0 commit comments