Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Restart engine when bot exceeds per-move time limit #896

Closed
wants to merge 13 commits into from

Conversation

MarkZH
Copy link
Collaborator

@MarkZH MarkZH commented Jan 26, 2024

Type of pull request:

  • Bug fix
  • Feature
  • Other

Description:

If a user specifies a per-move time limit in their configuration file--e.g., uci_options: go_commands: movetime: 1000 for 1000 milliseconds per move--and the engine exceeds this time limit by more than 10 seconds, the python-chess library will intervene and cancel the search while raising a TimeoutError. Previously, this error would be caught and main play_game() loop would carry on without sending a move to lichess.org.

Now, the TimeoutError is propagated until it hits the backoff decoration of play_game(). This shuts down the engine (correctly, now, due to changes in engine_wrapper.py) and restarts it when backoff restarts play_game(). Even if the engine keeps exhibiting the same buggy behavior, the errors will be displayed on the terminal and log, so the user will know that something is wrong.

Related Issues:

This PR closes #893 and should be a better solution to #623 than the previous PR #628.

Checklist:

  • I have read and followed the contribution guidelines.
  • I have added necessary documentation (if applicable).
  • The changes pass all existing tests.

The python-chess SimpleEngine class is meant to be used as a context
manager, so make use of its __enter__() and __exit__() methods to
ensure complete startup and shutdown. Also, if the engine is shutting
down due to an exception, do not attempt any more communication
(i.e., ping and quit).
Since EngineWrapper automatically closes the engine when exiting from
the engine context in play_game(), catching the TimeoutError is no
longer necessary. If the engine goes way beyond time controls, it is
probably in an error state and can only benefit from restarting.
The new test uses a rudimentary engine (buggy_engine.py) that only plays
moves  to let black win with a scholars mate. However, on the third
move, it pauses long enough for the python-chess library to intervene
and raise an asyncio.TimeoutError exception. In a successful test, the
engine restarts and finishes the game.

Also, add an optional variable for a text log file to collect useful
data during tests. See testing_log_file_name on line 88.
@MarkZH
Copy link
Collaborator Author

MarkZH commented Jan 26, 2024

To see the changes to test_bot/test_bot.py, hide the whitespace changes. Since the buggy_engine made for the new test can only play a set list of moves, I had to change the thread_for_test() to allow changing the opponent to something other than stockfish.


@pytest.mark.timeout(30, method="thread")
def test_buggy_engine() -> None:
"""Test lichess-bot with Stockfish (UCI)."""
Copy link
Member

@AttackingOrDefending AttackingOrDefending Jan 27, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The docstring should be changed since we don't use stockfish.

@AttackingOrDefending
Copy link
Member

lichess-bot plays black so it always receieves an odd number of moves, so the engine that waits for 11 seconds is the outside engine and not the one managed by lichess-bot. You can change buggy_engine.py to

"""An engine that takes too much time to make a move during tests."""

import chess
import time

assert input() == "uci"

print("id name Delay tactic", flush=True)
print("id author MZH", flush=True)
print("uciok", flush=True)

delay_performed = False
just_started = True
scholars_mate = ["a2a3", "e7e5", "a3a4", "f8c5", "a4a5", "d8h4", "a5a6", "h4f2"]

while True:
    command, *remaining = input().split()
    if command == "quit":
        break
    elif command == "isready":
        print("readyok", flush=True)
    elif command == "position":
        spec_type, *remaining = remaining
        assert spec_type == "startpos"
        board = chess.Board()
        if remaining:
            moves_label, *move_list = remaining
            assert moves_label == "moves"
            for move in move_list:
                board.push_uci(move)
        if just_started and len(board.move_stack) > 1:
            delay_performed = True
    elif command == "go":
        move_count = len(board.move_stack)
        if move_count == 3 and not delay_performed:
            print("info string delaying move", flush=True)
            delay_performed = True
            time.sleep(12)
        move = scholars_mate[move_count]
        print(f"bestmove {move}", flush=True)
        just_started = False

and line 346 in test_bot.py to CONFIG["engine"]["uci_options"] = {"go_commands": {"movetime": 100}}.

@MarkZH
Copy link
Collaborator Author

MarkZH commented Jan 28, 2024

I'm going to fix this PR so the git blame for test_bot.py isn't rendered completely useless. I think my editor changed the newlines on every line.

@MarkZH
Copy link
Collaborator Author

MarkZH commented Jan 29, 2024

This PR is replaced by #897 to reduce clutter due to whitespace issues.

@MarkZH MarkZH closed this Jan 29, 2024
@MarkZH MarkZH deleted the timeout-hang branch January 29, 2024 01:26
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Bot doesn't play the move supplied by the engine through UCI
2 participants