Skip to content

Commit 77bb7b2

Browse files
authored
Merge pull request #998 from MarkZH/send-result
Send game result to engines
2 parents 714f672 + 65bf3d0 commit 77bb7b2

2 files changed

Lines changed: 185 additions & 0 deletions

File tree

chess/engine.py

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1266,6 +1266,31 @@ async def analysis(self, board: chess.Board, limit: Optional[Limit] = None, *, m
12661266
and stopping the analysis at any time.
12671267
"""
12681268

1269+
@abc.abstractmethod
1270+
async def send_game_result(self, board: chess.Board, winner: Optional[Color] = None, game_ending: Optional[str] = None, game_complete: bool = True) -> None:
1271+
"""
1272+
Sends the engine the result of the game.
1273+
1274+
XBoard engines receive the final moves and a line of the form
1275+
"result <winner> {<ending>}". The <winner> field is one of "1-0",
1276+
"0-1", "1/2-1/2", or "*" to indicate white won, black won, draw,
1277+
or adjournment, respectively. The <ending> field is a description
1278+
of the specific reason for the end of the game: "White mates",
1279+
"Time forfeiture", "Stalemate", etc.
1280+
1281+
UCI engines do not expect end-of-game information and so are not
1282+
sent anything.
1283+
1284+
:param board: The final state of the board.
1285+
:param winner: Optional. Specify the winner of the game. This is useful
1286+
if the result of the game is not evident from the board--e.g., time
1287+
forfeiture or draw by agreement.
1288+
:param game_ending: Optional. Text describing the reason for the game
1289+
ending. Similarly to the winner paramter, this overrides any game
1290+
result derivable from the board.
1291+
:param game_complete: Optional. Whether the game reached completion.
1292+
"""
1293+
12691294
@abc.abstractmethod
12701295
async def quit(self) -> None:
12711296
"""Asks the engine to shut down."""
@@ -1817,6 +1842,9 @@ def engine_terminated(self, engine: UciProtocol, exc: Exception) -> None:
18171842

18181843
return await self.communicate(UciAnalysisCommand)
18191844

1845+
async def send_game_result(self, board: chess.Board, winner: Optional[Color] = None, game_ending: Optional[str] = None, game_complete: bool = True) -> None:
1846+
pass
1847+
18201848
async def quit(self) -> None:
18211849
self.send_line("quit")
18221850
await asyncio.shield(self.returncode)
@@ -2502,6 +2530,41 @@ def _opponent_configuration(self, *, opponent: Optional[Opponent] = None, engine
25022530
async def send_opponent_information(self, *, opponent: Optional[Opponent] = None, engine_rating: Optional[int] = None) -> None:
25032531
return await self.configure(self._opponent_configuration(opponent=opponent, engine_rating=engine_rating))
25042532

2533+
async def send_game_result(self, board: chess.Board, winner: Optional[Color] = None, game_ending: Optional[str] = None, game_complete: bool = True) -> None:
2534+
class XBoardGameResultCommand(BaseCommand[XBoardProtocol, None]):
2535+
def start(self, engine: XBoardProtocol) -> None:
2536+
if game_ending and any(c in game_ending for c in "{}\n\r"):
2537+
raise EngineError(f"invalid line break or curly braces in game ending message: {game_ending!r}")
2538+
2539+
engine._new(board, engine.game, {}) # Send final moves to engine.
2540+
2541+
outcome = board.outcome(claim_draw=True)
2542+
2543+
if not game_complete:
2544+
result = "*"
2545+
ending = game_ending or ""
2546+
elif winner is not None or game_ending:
2547+
result = "1-0" if winner == chess.WHITE else "0-1" if winner == chess.BLACK else "1/2-1/2"
2548+
ending = game_ending or ""
2549+
elif outcome is not None and outcome.winner is not None:
2550+
result = outcome.result()
2551+
winning_color = "White" if outcome.winner == chess.WHITE else "Black"
2552+
is_checkmate = outcome.termination == chess.Termination.CHECKMATE
2553+
ending = f"{winning_color} {'mates' if is_checkmate else 'variant win'}"
2554+
elif outcome is not None:
2555+
result = outcome.result()
2556+
ending = outcome.termination.name.capitalize().replace("_", " ")
2557+
else:
2558+
result = "*"
2559+
ending = ""
2560+
2561+
ending_text = f"{{{ending}}}" if ending else ""
2562+
engine.send_line(f"result {result} {ending_text}".strip())
2563+
self.result.set_result(None)
2564+
self.set_finished()
2565+
2566+
return await self.communicate(XBoardGameResultCommand)
2567+
25052568
async def quit(self) -> None:
25062569
self.send_line("quit")
25072570
await asyncio.shield(self.returncode)
@@ -2929,6 +2992,12 @@ def analysis(self, board: chess.Board, limit: Optional[Limit] = None, *, multipv
29292992
future = asyncio.run_coroutine_threadsafe(coro, self.protocol.loop)
29302993
return SimpleAnalysisResult(self, future.result())
29312994

2995+
def send_game_result(self, board: chess.Board, winner: Optional[Color] = None, game_ending: Optional[str] = None, game_complete: bool = True) -> None:
2996+
with self._not_shut_down():
2997+
coro = asyncio.wait_for(self.protocol.send_game_result(board, winner, game_ending, game_complete), self.timeout)
2998+
future = asyncio.run_coroutine_threadsafe(coro, self.protocol.loop)
2999+
return future.result()
3000+
29323001
def quit(self) -> None:
29333002
with self._not_shut_down():
29343003
coro = asyncio.wait_for(self.protocol.quit(), self.timeout)

test.py

Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3466,6 +3466,32 @@ def test_uci_info(self):
34663466
info = chess.engine._parse_uci_info("depth 1 seldepth 2 time 16 nodes 1 score cp 72 wdl 249 747 4 hashfull 0 nps 400 tbhits 0 multipv 1", board)
34673467
self.assertEqual(info["wdl"], (249, 747, 4))
34683468

3469+
def test_uci_result(self):
3470+
async def main():
3471+
protocol = chess.engine.UciProtocol()
3472+
mock = chess.engine.MockTransport(protocol)
3473+
3474+
mock.expect("uci", ["uciok"])
3475+
await protocol.initialize()
3476+
mock.assert_done()
3477+
3478+
limit = chess.engine.Limit(time=5)
3479+
checkmate_board = chess.Board("k7/7R/6R1/8/8/8/8/K7 w - - 0 1")
3480+
3481+
mock.expect("ucinewgame")
3482+
mock.expect("isready", ["readyok"])
3483+
mock.expect("position fen k7/7R/6R1/8/8/8/8/K7 w - - 0 1")
3484+
mock.expect("go movetime 5000", ["bestmove g6g8"])
3485+
result = await protocol.play(checkmate_board, limit, game="checkmate")
3486+
self.assertEqual(result.move, checkmate_board.parse_uci("g6g8"))
3487+
checkmate_board.push(result.move)
3488+
self.assertTrue(checkmate_board.is_checkmate())
3489+
await protocol.send_game_result(checkmate_board)
3490+
mock.assert_done()
3491+
3492+
asyncio.set_event_loop_policy(chess.engine.EventLoopPolicy())
3493+
asyncio.run(main())
3494+
34693495
def test_hiarcs_bestmove(self):
34703496
async def main():
34713497
protocol = chess.engine.UciProtocol()
@@ -3665,6 +3691,96 @@ async def main():
36653691
asyncio.set_event_loop_policy(chess.engine.EventLoopPolicy())
36663692
asyncio.run(main())
36673693

3694+
def test_xboard_result(self):
3695+
async def main():
3696+
protocol = chess.engine.XBoardProtocol()
3697+
mock = chess.engine.MockTransport(protocol)
3698+
3699+
mock.expect("xboard")
3700+
mock.expect("protover 2", ["feature ping=1 setboard=1 done=1"])
3701+
await protocol.initialize()
3702+
mock.assert_done()
3703+
3704+
limit = chess.engine.Limit(time=5)
3705+
checkmate_board = chess.Board("k7/7R/6R1/8/8/8/8/K7 w - - 0 1")
3706+
3707+
mock.expect("new")
3708+
mock.expect("force")
3709+
mock.expect("setboard k7/7R/6R1/8/8/8/8/K7 w - - 0 1")
3710+
mock.expect("st 5")
3711+
mock.expect("nopost")
3712+
mock.expect("easy")
3713+
mock.expect("go", ["move g6g8"])
3714+
mock.expect_ping()
3715+
mock.expect("force")
3716+
mock.expect("result 1-0 {White mates}")
3717+
result = await protocol.play(checkmate_board, limit, game="checkmate")
3718+
self.assertEqual(result.move, checkmate_board.parse_uci("g6g8"))
3719+
checkmate_board.push(result.move)
3720+
self.assertTrue(checkmate_board.is_checkmate())
3721+
await protocol.send_game_result(checkmate_board)
3722+
mock.assert_done()
3723+
3724+
unfinished_board = chess.Board()
3725+
mock.expect("new")
3726+
mock.expect("force")
3727+
mock.expect("st 5")
3728+
mock.expect("nopost")
3729+
mock.expect("easy")
3730+
mock.expect("go", ["move e2e4"])
3731+
mock.expect_ping()
3732+
mock.expect("force")
3733+
mock.expect("result *")
3734+
result = await protocol.play(unfinished_board, limit, game="unfinished")
3735+
self.assertEqual(result.move, unfinished_board.parse_uci("e2e4"))
3736+
unfinished_board.push(result.move)
3737+
await protocol.send_game_result(unfinished_board, game_complete=False)
3738+
mock.assert_done()
3739+
3740+
timeout_board = chess.Board()
3741+
mock.expect("new")
3742+
mock.expect("force")
3743+
mock.expect("st 5")
3744+
mock.expect("nopost")
3745+
mock.expect("easy")
3746+
mock.expect("go", ["move e2e4"])
3747+
mock.expect_ping()
3748+
mock.expect("force")
3749+
mock.expect("result 0-1 {Time forfeiture}")
3750+
result = await protocol.play(timeout_board, limit, game="timeout")
3751+
self.assertEqual(result.move, timeout_board.parse_uci("e2e4"))
3752+
timeout_board.push(result.move)
3753+
await protocol.send_game_result(timeout_board, chess.BLACK, "Time forfeiture")
3754+
mock.assert_done()
3755+
3756+
error_board = chess.Board()
3757+
mock.expect("new")
3758+
mock.expect("force")
3759+
mock.expect("st 5")
3760+
mock.expect("nopost")
3761+
mock.expect("easy")
3762+
mock.expect("go", ["move e2e4"])
3763+
mock.expect_ping()
3764+
result = await protocol.play(error_board, limit, game="error")
3765+
self.assertEqual(result.move, error_board.parse_uci("e2e4"))
3766+
error_board.push(result.move)
3767+
for c in "\n\r{}":
3768+
with self.assertRaises(chess.engine.EngineError):
3769+
await protocol.send_game_result(error_board, chess.BLACK, f"Time{c}forfeiture")
3770+
mock.assert_done()
3771+
3772+
material_board = chess.Board("k7/8/8/8/8/8/8/K7 b - - 0 1")
3773+
self.assertTrue(material_board.is_insufficient_material())
3774+
mock.expect("new")
3775+
mock.expect("force")
3776+
mock.expect("setboard k7/8/8/8/8/8/8/K7 b - - 0 1")
3777+
mock.expect("result 1/2-1/2 {Insufficient material}")
3778+
await protocol.send_game_result(material_board)
3779+
mock.assert_done()
3780+
3781+
asyncio.set_event_loop_policy(chess.engine.EventLoopPolicy())
3782+
asyncio.run(main())
3783+
36683784
def test_xboard_analyse(self):
36693785
async def main():
36703786
protocol = chess.engine.XBoardProtocol()

0 commit comments

Comments
 (0)