Skip to content

Commit 4889834

Browse files
committed
Add optional identifier to Limit instances
This new field in the Limit class is used to signal when the time control for a game has changed. XBoard engines expect to receive a `level` or `st` command to indicate the time control at the start of the game, and then only `time` and `otim` commands before each subsequent move to update the engine's internal clocks. The `level` and `st` commands should only be resent when the time control changes--like in FIDE tournament time controls. During a single time control, the same `clock_id` should be used for every Limit instance. This field has a similar function to the `game` parameter of the `play()` method.
1 parent 622f3a0 commit 4889834

2 files changed

Lines changed: 38 additions & 8 deletions

File tree

chess/engine.py

Lines changed: 19 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -332,6 +332,14 @@ class Limit:
332332
*white_clock* and *black_clock* are, then it is sudden death.
333333
"""
334334

335+
clock_id: object = None
336+
"""
337+
An identifier to use with XBoard engines to signal that the time
338+
control has changed. When this field changes, Xboard engines are
339+
send level or st commands as appropriate. Otherwise, only time
340+
and otim commands are sent to update the engine's clock.
341+
"""
342+
335343
def __repr__(self) -> str:
336344
# Like default __repr__, but without None values.
337345
return "{}({})".format(
@@ -2001,6 +2009,7 @@ def __init__(self) -> None:
20012009
self.target_config: Dict[str, ConfigValue] = {}
20022010
self.board = chess.Board()
20032011
self.game: object = None
2012+
self.clock_id: object = None
20042013
self.first_game = True
20052014

20062015
async def initialize(self) -> None:
@@ -2202,10 +2211,9 @@ def start(self, engine: XBoardProtocol) -> None:
22022211
# Limit or time control.
22032212
clock = limit.white_clock if board.turn else limit.black_clock
22042213
increment = limit.white_inc if board.turn else limit.black_inc
2205-
if limit.remaining_moves or clock is not None or increment is not None:
2206-
base_mins, base_secs = divmod(int(clock or 0), 60)
2207-
engine.send_line(f"level {limit.remaining_moves or 0} {base_mins}:{base_secs:02d} {increment or 0}")
2208-
2214+
if limit.clock_id is None or limit.clock_id != engine.clock_id:
2215+
self._send_time_control(engine, clock, increment)
2216+
engine.clock_id = limit.clock_id
22092217
if limit.nodes is not None:
22102218
if limit.time is not None or limit.white_clock is not None or limit.black_clock is not None or increment is not None:
22112219
raise EngineError("xboard does not support mixing node limits with time limits")
@@ -2217,8 +2225,6 @@ def start(self, engine: XBoardProtocol) -> None:
22172225

22182226
engine.send_line("nps 1")
22192227
engine.send_line(f"st {max(1, int(limit.nodes))}")
2220-
if limit.time is not None:
2221-
engine.send_line(f"st {max(0.01, limit.time)}")
22222228
if limit.depth is not None:
22232229
engine.send_line(f"sd {max(1, int(limit.depth))}")
22242230
if limit.white_clock is not None:
@@ -2270,6 +2276,13 @@ def line_received(self, engine: XBoardProtocol, line: str) -> None:
22702276
else:
22712277
LOGGER.warning("%s: Unexpected engine output: %r", engine, line)
22722278

2279+
def _send_time_control(self, engine: XBoardProtocol, clock: Optional[float], increment: Optional[float]) -> None:
2280+
if limit.remaining_moves or clock is not None or increment is not None:
2281+
base_mins, base_secs = divmod(int(clock or 0), 60)
2282+
engine.send_line(f"level {limit.remaining_moves or 0} {base_mins}:{base_secs:02d} {increment or 0}")
2283+
if limit.time is not None:
2284+
engine.send_line(f"st {max(0.01, limit.time)}")
2285+
22732286
def _post(self, engine: XBoardProtocol, line: str) -> None:
22742287
if not self.result.done():
22752288
self.play_result.info = _parse_xboard_post(line, engine.board, info)

test.py

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3713,7 +3713,9 @@ async def main():
37133713
mock.assert_done()
37143714

37153715
limit = chess.engine.Limit(black_clock=65, white_clock=100,
3716-
black_inc=4, white_inc=8)
3716+
black_inc=4, white_inc=8,
3717+
clock_id="xboard level")
3718+
board = chess.Board()
37173719
mock.expect("new")
37183720
mock.expect("force")
37193721
mock.expect("level 0 1:40 8")
@@ -3723,10 +3725,25 @@ async def main():
37233725
mock.expect("easy")
37243726
mock.expect("go", ["move e2e4"])
37253727
mock.expect_ping()
3726-
result = await protocol.play(chess.Board(), limit)
3728+
result = await protocol.play(board, limit)
37273729
self.assertEqual(result.move, chess.Move.from_uci("e2e4"))
37283730
mock.assert_done()
37293731

3732+
board.push(result.move)
3733+
board.push_uci("e7e5")
3734+
3735+
mock.expect("force")
3736+
mock.expect("e7e5")
3737+
mock.expect("time 10000")
3738+
mock.expect("otim 6500")
3739+
mock.expect("nopost")
3740+
mock.expect("easy")
3741+
mock.expect("go", ["move d2d4"])
3742+
mock.expect_ping()
3743+
result = await protocol.play(board, limit)
3744+
self.assertEqual(result.move, chess.Move.from_uci("d2d4"))
3745+
mock.assert_done()
3746+
37303747
asyncio.set_event_loop_policy(chess.engine.EventLoopPolicy())
37313748
asyncio.run(main())
37323749

0 commit comments

Comments
 (0)