From bcde8f364e2e7bac0315aa2517a79283bcc5dd0a Mon Sep 17 00:00:00 2001 From: johndoknjas Date: Fri, 23 Aug 2024 15:13:28 -0700 Subject: [PATCH 01/36] Make the `send_ucinewgame_token` param False by default in `set_fen_position`. --- stockfish/models.py | 6 +++--- tests/stockfish/test_models.py | 31 ++++++++++++++++++------------- 2 files changed, 21 insertions(+), 16 deletions(-) diff --git a/stockfish/models.py b/stockfish/models.py index 05c8157..e0d5310 100644 --- a/stockfish/models.py +++ b/stockfish/models.py @@ -299,7 +299,7 @@ def _weaker_setting_warning(self, message: str) -> None: warnings.warn(message, stacklevel=3) def set_fen_position( - self, fen_position: str, send_ucinewgame_token: bool = True + self, fen_position: str, send_ucinewgame_token: bool = False ) -> None: """Sets current board position in Forsyth-Edwards notation (FEN). @@ -309,8 +309,8 @@ def set_fen_position( send_ucinewgame_token: Whether to send the `ucinewgame` token to the Stockfish engine. - The most prominent effect this will have is clearing Stockfish's transposition table, - which should be done if the new position is unrelated to the current position. + This will clear Stockfish's hash table, which should generally only be done if the new + position is unrelated to the current one (such as a new game). Returns: `None` diff --git a/tests/stockfish/test_models.py b/tests/stockfish/test_models.py index bc505eb..c5fd3fc 100644 --- a/tests/stockfish/test_models.py +++ b/tests/stockfish/test_models.py @@ -127,23 +127,25 @@ def test_set_fen_position_mate(self, stockfish: Stockfish): assert stockfish.info == "info depth 0 score mate 0" def test_clear_info_after_set_new_fen_position(self, stockfish: Stockfish): - stockfish.set_fen_position("8/8/8/6pp/8/4k1PP/r7/4K3 b - - 11 52") + stockfish.set_fen_position("8/8/8/6pp/8/4k1PP/r7/4K3 b - - 11 52", True) stockfish.get_best_move() - stockfish.set_fen_position("8/8/8/6pp/8/4k1PP/8/r3K3 w - - 12 53") + stockfish.set_fen_position("8/8/8/6pp/8/4k1PP/8/r3K3 w - - 12 53", True) assert stockfish.info == "" - stockfish.set_fen_position("8/8/8/6pp/8/4k1PP/r7/4K3 b - - 11 52") + stockfish.set_fen_position("8/8/8/6pp/8/4k1PP/r7/4K3 b - - 11 52", True) stockfish.get_best_move() stockfish.set_fen_position("8/8/8/6pp/8/4k1PP/8/r3K3 w - - 12 53", False) assert stockfish.info == "" def test_set_fen_position_starts_new_game(self, stockfish: Stockfish): stockfish.set_fen_position( - "7r/1pr1kppb/2n1p2p/2NpP2P/5PP1/1P6/P6K/R1R2B2 w - - 1 27" + "7r/1pr1kppb/2n1p2p/2NpP2P/5PP1/1P6/P6K/R1R2B2 w - - 1 27", True ) stockfish.get_best_move() assert stockfish.info != "" - stockfish.set_fen_position("3kn3/p5rp/1p3p2/3B4/3P1P2/2P5/1P3K2/8 w - - 0 53") + stockfish.set_fen_position( + "3kn3/p5rp/1p3p2/3B4/3P1P2/2P5/1P3K2/8 w - - 0 53", True + ) assert stockfish.info == "" def test_set_fen_position_second_argument(self, stockfish: Stockfish): @@ -182,7 +184,9 @@ def test_is_move_correct_not_first_move(self, stockfish: Stockfish): ) # fmt: on def test_last_info(self, stockfish: Stockfish, value): - stockfish.set_fen_position("r6k/6b1/2b1Q3/p6p/1p5q/3P2PP/5r1K/8 w - - 1 31") + stockfish.set_fen_position( + "r6k/6b1/2b1Q3/p6p/1p5q/3P2PP/5r1K/8 w - - 1 31", True + ) stockfish.get_best_move() assert value in stockfish.info @@ -516,20 +520,20 @@ def test_get_evaluation_stalemate(self, stockfish: Stockfish): def test_get_static_eval(self, stockfish: Stockfish): stockfish.set_turn_perspective(False) - stockfish.set_fen_position("r7/8/8/8/8/5k2/4p3/4K3 w - - 0 1") + stockfish.set_fen_position("r7/8/8/8/8/5k2/4p3/4K3 w - - 0 1", True) static_eval_1 = stockfish.get_static_eval() assert isinstance(static_eval_1, float) and static_eval_1 < -3 - stockfish.set_fen_position("r7/8/8/8/8/5k2/4p3/4K3 b - - 0 1") + stockfish.set_fen_position("r7/8/8/8/8/5k2/4p3/4K3 b - - 0 1", True) static_eval_2 = stockfish.get_static_eval() assert isinstance(static_eval_2, float) and static_eval_2 < -3 stockfish.set_turn_perspective() static_eval_3 = stockfish.get_static_eval() assert isinstance(static_eval_3, float) and static_eval_3 > 3 - stockfish.set_fen_position("r7/8/8/8/8/5k2/4p3/4K3 w - - 0 1") + stockfish.set_fen_position("r7/8/8/8/8/5k2/4p3/4K3 w - - 0 1", True) static_eval_4 = stockfish.get_static_eval() assert isinstance(static_eval_4, float) and static_eval_4 < -3 if stockfish.get_stockfish_major_version() >= 12: - stockfish.set_fen_position("8/8/8/8/8/4k3/4p3/r3K3 w - - 0 1") + stockfish.set_fen_position("8/8/8/8/8/4k3/4p3/r3K3 w - - 0 1", True) assert stockfish.get_static_eval() is None stockfish.set_position(None) stockfish.get_static_eval() @@ -879,7 +883,7 @@ def test_make_moves_transposition_table_speed(self, stockfish: Stockfish): total_time_calculating_second = 0.0 for i in range(len(positions_considered)): - stockfish.set_fen_position(positions_considered[i]) + stockfish.set_fen_position(positions_considered[i], True) start = default_timer() stockfish.get_best_move() total_time_calculating_second += default_timer() - start @@ -898,7 +902,7 @@ def test_get_wdl_stats(self, stockfish: Stockfish): assert abs(wdl_stats[0] - wdl_stats[2]) / wdl_stats[0] < 0.15 stockfish.set_fen_position( - "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1" + "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1", True ) wdl_stats_2 = stockfish.get_wdl_stats() assert isinstance(wdl_stats_2, list) @@ -909,7 +913,8 @@ def test_get_wdl_stats(self, stockfish: Stockfish): assert stockfish.get_wdl_stats() is None stockfish.set_fen_position( - "rnbqkb1r/pp3ppp/3p1n2/1B2p3/3NP3/2N5/PPP2PPP/R1BQK2R b KQkq - 0 6" + "rnbqkb1r/pp3ppp/3p1n2/1B2p3/3NP3/2N5/PPP2PPP/R1BQK2R b KQkq - 0 6", + True, ) wdl_stats_3 = stockfish.get_wdl_stats() assert isinstance(wdl_stats_3, list) and len(wdl_stats_3) == 3 From 80984c237f7788fd18aa5b359a1eba120480362a Mon Sep 17 00:00:00 2001 From: johndoknjas Date: Fri, 23 Aug 2024 15:16:12 -0700 Subject: [PATCH 02/36] Make the `send_ucinewgame_token` param of `_prepare_for_new_position` not optional. --- stockfish/models.py | 2 +- tests/stockfish/test_models.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/stockfish/models.py b/stockfish/models.py index e0d5310..8435232 100644 --- a/stockfish/models.py +++ b/stockfish/models.py @@ -208,7 +208,7 @@ def reset_engine_parameters(self) -> None: """ self.update_engine_parameters(self._DEFAULT_STOCKFISH_PARAMS) - def _prepare_for_new_position(self, send_ucinewgame_token: bool = True) -> None: + def _prepare_for_new_position(self, send_ucinewgame_token: bool) -> None: if send_ucinewgame_token: self._put("ucinewgame") self._is_ready() diff --git a/tests/stockfish/test_models.py b/tests/stockfish/test_models.py index c5fd3fc..e547c74 100644 --- a/tests/stockfish/test_models.py +++ b/tests/stockfish/test_models.py @@ -919,7 +919,7 @@ def test_get_wdl_stats(self, stockfish: Stockfish): wdl_stats_3 = stockfish.get_wdl_stats() assert isinstance(wdl_stats_3, list) and len(wdl_stats_3) == 3 - stockfish._prepare_for_new_position() + stockfish._prepare_for_new_position(True) wdl_stats_4 = stockfish.get_wdl_stats(get_as_tuple=True) assert isinstance(wdl_stats_4, tuple) and len(wdl_stats_4) == 3 assert wdl_stats_3 == list(wdl_stats_4) From 016f443962287948067d6adff9e43cf5e5e57e2b Mon Sep 17 00:00:00 2001 From: johndoknjas Date: Fri, 23 Aug 2024 15:31:05 -0700 Subject: [PATCH 03/36] Use `is` rather than `==` in tests with booleans to check that the value is not just truthy. --- tests/stockfish/test_models.py | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/tests/stockfish/test_models.py b/tests/stockfish/test_models.py index e547c74..aa76be8 100644 --- a/tests/stockfish/test_models.py +++ b/tests/stockfish/test_models.py @@ -204,13 +204,13 @@ def test_set_skill_level(self, stockfish: Stockfish): ) # fmt: on assert stockfish.get_engine_parameters()["Skill Level"] == 1 - assert stockfish.get_engine_parameters()["UCI_LimitStrength"] == False + assert stockfish.get_engine_parameters()["UCI_LimitStrength"] is False assert stockfish._on_weaker_setting() stockfish.set_skill_level(20) assert stockfish.get_best_move() in ("d2d4", "c2c4") assert stockfish.get_engine_parameters()["Skill Level"] == 20 - assert stockfish.get_engine_parameters()["UCI_LimitStrength"] == False + assert stockfish.get_engine_parameters()["UCI_LimitStrength"] is False assert not stockfish._on_weaker_setting() def test_set_elo_rating(self, stockfish: Stockfish): @@ -228,7 +228,7 @@ def test_set_elo_rating(self, stockfish: Stockfish): ) # fmt: on assert stockfish.get_engine_parameters()["UCI_Elo"] == 2000 - assert stockfish.get_engine_parameters()["UCI_LimitStrength"] == True + assert stockfish.get_engine_parameters()["UCI_LimitStrength"] is True assert stockfish._on_weaker_setting() stockfish.set_elo_rating(1350) @@ -238,7 +238,7 @@ def test_set_elo_rating(self, stockfish: Stockfish): ) # fmt: on assert stockfish.get_engine_parameters()["UCI_Elo"] == 1350 - assert stockfish.get_engine_parameters()["UCI_LimitStrength"] == True + assert stockfish.get_engine_parameters()["UCI_LimitStrength"] is True assert stockfish._on_weaker_setting() stockfish.set_elo_rating(2850) @@ -618,11 +618,11 @@ def test_constructor(self, stockfish: Stockfish): assert stockfish_2_params[key] == 2850 assert stockfish_1_params[key] == 1350 elif key == "UCI_LimitStrength": - assert stockfish_2_params[key] == True - assert stockfish_1_params[key] == False + assert stockfish_2_params[key] is True + assert stockfish_1_params[key] is False elif key == "UCI_Chess960": - assert stockfish_2_params[key] == True - assert stockfish_1_params[key] == False + assert stockfish_2_params[key] is True + assert stockfish_1_params[key] is False else: assert stockfish_2_params[key] == stockfish_1_params[key] @@ -652,18 +652,18 @@ def test_parameters_functions(self, stockfish: Stockfish): elif key == "MultiPV": assert value == 2 elif key == "UCI_Chess960": - assert value == True + assert value is True else: assert updated_parameters[key] == old_parameters[key] - assert stockfish.get_engine_parameters()["UCI_LimitStrength"] == False + assert stockfish.get_engine_parameters()["UCI_LimitStrength"] is False stockfish.update_engine_parameters({"UCI_Elo": 2000, "Skill Level": 19}) assert stockfish.get_engine_parameters()["UCI_Elo"] == 2000 assert stockfish.get_engine_parameters()["Skill Level"] == 19 - assert stockfish.get_engine_parameters()["UCI_LimitStrength"] == False + assert stockfish.get_engine_parameters()["UCI_LimitStrength"] is False stockfish.update_engine_parameters({"UCI_Elo": 2000}) - assert stockfish.get_engine_parameters()["UCI_LimitStrength"] == True + assert stockfish.get_engine_parameters()["UCI_LimitStrength"] is True stockfish.update_engine_parameters({"Skill Level": 20}) - assert stockfish.get_engine_parameters()["UCI_LimitStrength"] == False + assert stockfish.get_engine_parameters()["UCI_LimitStrength"] is False assert stockfish.get_fen_position() == "4rkr1/4p1p1/8/8/8/8/8/5K1R w H - 0 100" stockfish.reset_engine_parameters() assert stockfish.get_engine_parameters() == old_parameters From 20378562874ffe1fb62dcd64497cf1317084cd9c Mon Sep 17 00:00:00 2001 From: johndoknjas Date: Fri, 23 Aug 2024 15:49:33 -0700 Subject: [PATCH 04/36] Change the name of `set_position` to `make_moves_from_start`. --- README.md | 2 +- stockfish/models.py | 6 +++--- tests/stockfish/test_models.py | 28 ++++++++++++++-------------- 3 files changed, 18 insertions(+), 18 deletions(-) diff --git a/README.md b/README.md index 77efddd..213d5bb 100644 --- a/README.md +++ b/README.md @@ -90,7 +90,7 @@ The `__del__()` method of the Stockfish class will call send_quit_command(), but ### Set position by a sequence of moves from the starting position ```python -stockfish.set_position(["e2e4", "e7e6"]) +stockfish.make_moves_from_start(["e2e4", "e7e6"]) ``` ### Update position by making a sequence of moves from the current position diff --git a/stockfish/models.py b/stockfish/models.py index 8435232..ca818f2 100644 --- a/stockfish/models.py +++ b/stockfish/models.py @@ -321,8 +321,8 @@ def set_fen_position( self._prepare_for_new_position(send_ucinewgame_token) self._put(f"position fen {fen_position}") - def set_position(self, moves: Optional[List[str]] = None) -> None: - """Sets current board position. + def make_moves_from_start(self, moves: Optional[List[str]] = None) -> None: + """Sets the position by making a sequence of moves from the starting position of chess. Args: moves: @@ -333,7 +333,7 @@ def set_position(self, moves: Optional[List[str]] = None) -> None: `None` Example: - >>> stockfish.set_position(['e2e4', 'e7e5']) + >>> stockfish.make_moves_from_start(['e2e4', 'e7e5']) """ self.set_fen_position( "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1", True diff --git a/tests/stockfish/test_models.py b/tests/stockfish/test_models.py index aa76be8..f3b2534 100644 --- a/tests/stockfish/test_models.py +++ b/tests/stockfish/test_models.py @@ -63,26 +63,26 @@ def test_get_best_move_remaining_time_first_move(self, stockfish: Stockfish): best_move = stockfish.get_best_move(wtime=5 * 60 * 1000, btime=1000) assert best_move in ("e2e3", "e2e4", "g1f3", "b1c3", "d2d4") - def test_set_position_resets_info(self, stockfish: Stockfish): - stockfish.set_position(["e2e4", "e7e6"]) + def test_make_moves_from_start_resets_info(self, stockfish: Stockfish): + stockfish.make_moves_from_start(["e2e4", "e7e6"]) stockfish.get_best_move() assert stockfish.info != "" - stockfish.set_position(["e2e4", "e7e6"]) + stockfish.make_moves_from_start(["e2e4", "e7e6"]) assert stockfish.info == "" def test_get_best_move_not_first_move(self, stockfish: Stockfish): - stockfish.set_position(["e2e4", "e7e6"]) + stockfish.make_moves_from_start(["e2e4", "e7e6"]) best_move = stockfish.get_best_move() assert best_move in ("d2d4", "g1f3") def test_get_best_move_time_not_first_move(self, stockfish: Stockfish): - stockfish.set_position(["e2e4", "e7e6"]) + stockfish.make_moves_from_start(["e2e4", "e7e6"]) best_move = stockfish.get_best_move_time(1000) assert best_move in ("d2d4", "g1f3") @pytest.mark.slow def test_get_best_move_remaining_time_not_first_move(self, stockfish: Stockfish): - stockfish.set_position(["e2e4", "e7e6"]) + stockfish.make_moves_from_start(["e2e4", "e7e6"]) best_move = stockfish.get_best_move(wtime=1000) assert best_move in ("d2d4", "a2a3", "d1e2", "b1c3") best_move = stockfish.get_best_move(btime=1000) @@ -93,15 +93,15 @@ def test_get_best_move_remaining_time_not_first_move(self, stockfish: Stockfish) assert best_move in ("e2e3", "e2e4", "g1f3", "b1c3", "d2d4") def test_get_best_move_checkmate(self, stockfish: Stockfish): - stockfish.set_position(["f2f3", "e7e5", "g2g4", "d8h4"]) + stockfish.make_moves_from_start(["f2f3", "e7e5", "g2g4", "d8h4"]) assert stockfish.get_best_move() is None def test_get_best_move_time_checkmate(self, stockfish: Stockfish): - stockfish.set_position(["f2f3", "e7e5", "g2g4", "d8h4"]) + stockfish.make_moves_from_start(["f2f3", "e7e5", "g2g4", "d8h4"]) assert stockfish.get_best_move_time(1000) is None def test_get_best_move_remaining_time_checkmate(self, stockfish: Stockfish): - stockfish.set_position(["f2f3", "e7e5", "g2g4", "d8h4"]) + stockfish.make_moves_from_start(["f2f3", "e7e5", "g2g4", "d8h4"]) assert stockfish.get_best_move(wtime=1000) is None assert stockfish.get_best_move(btime=1000) is None assert stockfish.get_best_move(wtime=1000, btime=1000) is None @@ -170,7 +170,7 @@ def test_is_move_correct_first_move(self, stockfish: Stockfish): assert stockfish.is_move_correct("a2a3") is True def test_is_move_correct_not_first_move(self, stockfish: Stockfish): - stockfish.set_position(["e2e4", "e7e6"]) + stockfish.make_moves_from_start(["e2e4", "e7e6"]) assert stockfish.is_move_correct("e2e1") is False assert stockfish.is_move_correct("a2a3") is True @@ -354,7 +354,7 @@ def test_chess960_position(self, stockfish: Stockfish): assert stockfish.will_move_be_a_capture("f1g1") is Stockfish.Capture.NO_CAPTURE def test_get_board_visual_white(self, stockfish: Stockfish): - stockfish.set_position(["e2e4", "e7e6", "d2d4", "d7d5"]) + stockfish.make_moves_from_start(["e2e4", "e7e6", "d2d4", "d7d5"]) if stockfish.get_stockfish_major_version() >= 12: expected_result = ( "+---+---+---+---+---+---+---+---+\n" @@ -406,7 +406,7 @@ def test_get_board_visual_white(self, stockfish: Stockfish): # the second line read after stockfish._put("d") now will be the +---+---+---+ of the new outputted board. def test_get_board_visual_black(self, stockfish: Stockfish): - stockfish.set_position(["e2e4", "e7e6", "d2d4", "d7d5"]) + stockfish.make_moves_from_start(["e2e4", "e7e6", "d2d4", "d7d5"]) if stockfish.get_stockfish_major_version() >= 12: expected_result = ( "+---+---+---+---+---+---+---+---+\n" @@ -467,7 +467,7 @@ def test_get_fen_position(self, stockfish: Stockfish): assert "+---+---+---+" in stockfish._read_line() def test_get_fen_position_after_some_moves(self, stockfish: Stockfish): - stockfish.set_position(["e2e4", "e7e6"]) + stockfish.make_moves_from_start(["e2e4", "e7e6"]) assert ( stockfish.get_fen_position() == "rnbqkbnr/pppp1ppp/4p3/8/4P3/8/PPPP1PPP/RNBQKBNR w KQkq - 0 2" @@ -535,7 +535,7 @@ def test_get_static_eval(self, stockfish: Stockfish): if stockfish.get_stockfish_major_version() >= 12: stockfish.set_fen_position("8/8/8/8/8/4k3/4p3/r3K3 w - - 0 1", True) assert stockfish.get_static_eval() is None - stockfish.set_position(None) + stockfish.make_moves_from_start(None) stockfish.get_static_eval() stockfish._put("go depth 2") assert stockfish._read_line() != "" From 68b10799bbad191d5c48400a0c96c39b60bb03e1 Mon Sep 17 00:00:00 2001 From: johndoknjas Date: Fri, 23 Aug 2024 15:51:18 -0700 Subject: [PATCH 05/36] The `set_elo_rating` test occasionally fails on my computer (on past commits too). So add `a2a4`. --- tests/stockfish/test_models.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/stockfish/test_models.py b/tests/stockfish/test_models.py index f3b2534..c3fa5de 100644 --- a/tests/stockfish/test_models.py +++ b/tests/stockfish/test_models.py @@ -224,7 +224,7 @@ def test_set_elo_rating(self, stockfish: Stockfish): stockfish.set_elo_rating(2000) # fmt: off assert stockfish.get_best_move() in ( - "d2d4", "b1c3", "d1e2", "c2c4", "f1e2", "h2h3", "c2c3", "f1d3", "a2a3" + "d2d4", "b1c3", "d1e2", "c2c4", "f1e2", "h2h3", "c2c3", "f1d3", "a2a3", "a2a4" ) # fmt: on assert stockfish.get_engine_parameters()["UCI_Elo"] == 2000 From ba7faa2144aeb882548f44b99d60f4eae84676af Mon Sep 17 00:00:00 2001 From: johndoknjas Date: Fri, 23 Aug 2024 21:03:22 -0700 Subject: [PATCH 06/36] Do not send the `ucinewgame` token by default in `make_moves_from_starting_position`. --- stockfish/models.py | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/stockfish/models.py b/stockfish/models.py index ca818f2..c8567bf 100644 --- a/stockfish/models.py +++ b/stockfish/models.py @@ -197,7 +197,7 @@ def update_engine_parameters(self, parameters: Optional[dict]) -> None: for name, value in new_param_values.items(): self._set_option(name, value) - self.set_fen_position(self.get_fen_position(), False) + self.set_fen_position(self.get_fen_position()) # Getting SF to set the position again, since UCI option(s) have been updated. def reset_engine_parameters(self) -> None: @@ -301,7 +301,7 @@ def _weaker_setting_warning(self, message: str) -> None: def set_fen_position( self, fen_position: str, send_ucinewgame_token: bool = False ) -> None: - """Sets current board position in Forsyth-Edwards notation (FEN). + """Sets the current board position from Forsyth-Edwards notation (FEN). Args: fen_position: @@ -310,7 +310,7 @@ def set_fen_position( send_ucinewgame_token: Whether to send the `ucinewgame` token to the Stockfish engine. This will clear Stockfish's hash table, which should generally only be done if the new - position is unrelated to the current one (such as a new game). + position will be unrelated to the current one (such as a new game). Returns: `None` @@ -321,7 +321,9 @@ def set_fen_position( self._prepare_for_new_position(send_ucinewgame_token) self._put(f"position fen {fen_position}") - def make_moves_from_start(self, moves: Optional[List[str]] = None) -> None: + def make_moves_from_start( + self, moves: Optional[List[str]] = None, send_ucinewgame_token: bool = False + ) -> None: """Sets the position by making a sequence of moves from the starting position of chess. Args: @@ -329,6 +331,11 @@ def make_moves_from_start(self, moves: Optional[List[str]] = None) -> None: A list of moves to set this position on the board. Must be in full algebraic notation. + send_ucinewgame_token: + Whether to send the `ucinewgame` token to the Stockfish engine. + This will clear Stockfish's hash table, which should generally only be done if the new + position will be unrelated to the current one (such as a new game). + Returns: `None` @@ -336,7 +343,7 @@ def make_moves_from_start(self, moves: Optional[List[str]] = None) -> None: >>> stockfish.make_moves_from_start(['e2e4', 'e7e5']) """ self.set_fen_position( - "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1", True + "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1", send_ucinewgame_token ) self.make_moves_from_current_position(moves) @@ -671,7 +678,7 @@ def is_fen_valid(self, fen: str) -> bool: # Using a new temporary SF instance, in case the fen is an illegal position that causes # the SF process to crash. best_move: Optional[str] = None - temp_sf.set_fen_position(fen, False) + temp_sf.set_fen_position(fen) try: temp_sf._put("go depth 10") best_move = temp_sf._get_best_move_from_sf_popen_process() From 8a56c11b1bace553911193f346a19e6150b009d6 Mon Sep 17 00:00:00 2001 From: John Doknjas <32089502+johndoknjas@users.noreply.github.com> Date: Fri, 23 Aug 2024 21:23:25 -0700 Subject: [PATCH 07/36] Update README.md --- README.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/README.md b/README.md index 213d5bb..3fbfd12 100644 --- a/README.md +++ b/README.md @@ -108,6 +108,9 @@ Also, if you want to play Chess960, it's recommended you first update the "UCI_C stockfish.set_fen_position("rnbqkbnr/pppp1ppp/4p3/8/4P3/8/PPPP1PPP/RNBQKBNR w KQkq - 0 2") ``` +Note that if you'd like to send the `ucinewgame` command to Stockfish (which will reset its hash table), +send `True` as the second argument to `set_fen_position` or `make_moves_from_starting_position`. + ### Check whether the given FEN is valid This function returns a bool saying whether the passed in FEN is valid (both syntax wise and whether the position represented is legal). From c682d6bfbd6ff5824097c160924e92d35e40fb8a Mon Sep 17 00:00:00 2001 From: johndoknjas Date: Fri, 23 Aug 2024 21:26:52 -0700 Subject: [PATCH 08/36] Format with black. --- stockfish/models.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/stockfish/models.py b/stockfish/models.py index c8567bf..6e0f3bf 100644 --- a/stockfish/models.py +++ b/stockfish/models.py @@ -343,7 +343,8 @@ def make_moves_from_start( >>> stockfish.make_moves_from_start(['e2e4', 'e7e5']) """ self.set_fen_position( - "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1", send_ucinewgame_token + "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1", + send_ucinewgame_token, ) self.make_moves_from_current_position(moves) From 96163d2c2051a077fe400fc57d8a4d6af418ad94 Mon Sep 17 00:00:00 2001 From: johndoknjas Date: Fri, 23 Aug 2024 21:30:17 -0700 Subject: [PATCH 09/36] Update test. --- tests/stockfish/test_models.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/stockfish/test_models.py b/tests/stockfish/test_models.py index c3fa5de..32251e0 100644 --- a/tests/stockfish/test_models.py +++ b/tests/stockfish/test_models.py @@ -535,7 +535,7 @@ def test_get_static_eval(self, stockfish: Stockfish): if stockfish.get_stockfish_major_version() >= 12: stockfish.set_fen_position("8/8/8/8/8/4k3/4p3/r3K3 w - - 0 1", True) assert stockfish.get_static_eval() is None - stockfish.make_moves_from_start(None) + stockfish.make_moves_from_start(None, True) stockfish.get_static_eval() stockfish._put("go depth 2") assert stockfish._read_line() != "" From 5f83e139c62f5bae09d7129521800046e1e879cf Mon Sep 17 00:00:00 2001 From: johndoknjas Date: Fri, 23 Aug 2024 23:26:03 -0700 Subject: [PATCH 10/36] Some minor pylint recommendations. --- stockfish/models.py | 30 +++++++++++++----------------- 1 file changed, 13 insertions(+), 17 deletions(-) diff --git a/stockfish/models.py b/stockfish/models.py index 6e0f3bf..325d5ce 100644 --- a/stockfish/models.py +++ b/stockfish/models.py @@ -764,7 +764,7 @@ def does_current_engine_version_have_wdl_option(self) -> bool: splitted_text = self._read_line().split(" ") if splitted_text[0] == "uciok": return False - elif "UCI_ShowWDL" in splitted_text: + if "UCI_ShowWDL" in splitted_text: self._discard_remaining_stdout_lines("uciok") return True @@ -834,8 +834,7 @@ def get_static_eval(self) -> Optional[float]: if static_eval == "none": assert "(in check)" in text return None - else: - return float(static_eval) * compare + return float(static_eval) * compare def get_top_moves( self, @@ -1081,25 +1080,22 @@ def will_move_be_a_capture(self, move_value: str) -> Capture: if ending_square_piece is not None: if not self._parameters["UCI_Chess960"]: return Stockfish.Capture.DIRECT_CAPTURE - else: - # Check for Chess960 castling: - castling_pieces = [ - [Stockfish.Piece.WHITE_KING, Stockfish.Piece.WHITE_ROOK], - [Stockfish.Piece.BLACK_KING, Stockfish.Piece.BLACK_ROOK], - ] - if [starting_square_piece, ending_square_piece] in castling_pieces: - return Stockfish.Capture.NO_CAPTURE - else: - return Stockfish.Capture.DIRECT_CAPTURE - elif move_value[2:4] == self.get_fen_position().split()[ + # Check for Chess960 castling: + castling_pieces = [ + [Stockfish.Piece.WHITE_KING, Stockfish.Piece.WHITE_ROOK], + [Stockfish.Piece.BLACK_KING, Stockfish.Piece.BLACK_ROOK], + ] + if [starting_square_piece, ending_square_piece] in castling_pieces: + return Stockfish.Capture.NO_CAPTURE + return Stockfish.Capture.DIRECT_CAPTURE + if move_value[2:4] == self.get_fen_position().split()[ 3 ] and starting_square_piece in [ Stockfish.Piece.WHITE_PAWN, Stockfish.Piece.BLACK_PAWN, ]: return Stockfish.Capture.EN_PASSANT - else: - return Stockfish.Capture.NO_CAPTURE + return Stockfish.Capture.NO_CAPTURE def get_stockfish_full_version(self) -> float: """Returns Stockfish engine full version.""" @@ -1192,7 +1188,7 @@ def _parse_stockfish_version(self, version_text: str = "") -> None: except Exception as e: raise Exception( "Unable to parse Stockfish version. You may be using an unsupported version of Stockfish." - ) + ) from e def _get_stockfish_version_from_build_date( self, date_string: str = "" From b5565065a45bc3f2a3bbd749b5f6640516780273 Mon Sep 17 00:00:00 2001 From: John Doknjas <32089502+johndoknjas@users.noreply.github.com> Date: Sat, 24 Aug 2024 01:53:49 -0700 Subject: [PATCH 11/36] Update README.md --- README.md | 49 ++++++++++++++++++++++++++++++++++++++++--------- 1 file changed, 40 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index 3fbfd12..be9c213 100644 --- a/README.md +++ b/README.md @@ -79,6 +79,12 @@ These parameters can also be updated at any time by calling the "update_engine_p stockfish.update_engine_parameters({"Hash": 2048, "UCI_Chess960": True}) # Gets stockfish to use a 2GB hash table, and also to play Chess960. ``` +As for the depth, it can also be updated, by using the following function. Note that if you don't set depth to a value yourself, the python module will initialize it to 15 by default. + +```python +stockfish.set_depth(12) +``` + When you're done using the Stockfish engine process, you can send the "quit" uci command to it with: ```python @@ -92,17 +98,23 @@ The `__del__()` method of the Stockfish class will call send_quit_command(), but ```python stockfish.make_moves_from_start(["e2e4", "e7e6"]) ``` +If you'd just like to set up the starting position without making any moves from it, just call this function without sending an argument: +```python +stockfish.make_moves_from_start() +``` ### Update position by making a sequence of moves from the current position +Function takes a list of strings as its argument. Each string represents a move, and must have the format of the starting coordinate followed by the ending coordinate. If a move leads to a pawn promoting, then an additional character must be appended at the end (to indicate what piece the pawn promotes into). +Other types of special moves (e.g., checks, captures, checkmates, en passants) do not need any special notation; the starting coordinate followed by the ending coordinate is all the information that's needed. Note that castling is represented by the starting coordinate of the king followed by the ending coordinate of the king. So "e1g1" would be used for white castling kingside, assuming the white king is still on e1 and castling is legal. +Example call (assume in the current position, it is White's turn): ```python -stockfish.make_moves_from_current_position(["g4d7", "a8b8", "f1d1"]) +stockfish.make_moves_from_current_position(["g4d7", "a8b8", "f1d1", "b2b1q"]) # Moves the white piece on g4 to d7, then the black piece on a8 to b8, then the white piece on f1 to d1, and finally pushes the black b2-pawn to b1, promoting it into a queen. ``` ### Set position by Forsyth–Edwards Notation (FEN) -If you'd like to first check if your fen is valid, call the is_fen_valid() function below. -Also, if you want to play Chess960, it's recommended you first update the "UCI_Chess960" engine parameter to be True, before calling set_fen_position. +Note that if you want to play Chess960, it's recommended you first update the "UCI_Chess960" engine parameter to be True, before calling set_fen_position. ```python stockfish.set_fen_position("rnbqkbnr/pppp1ppp/4p3/8/4P3/8/PPPP1PPP/RNBQKBNR w KQkq - 0 2") @@ -118,6 +130,10 @@ The function isn't perfect and won't catch all cases, but generally it should re For example, one exception is positions which are legal, but have no legal moves. I.e., for checkmates and stalemates, this function will incorrectly say the fen is invalid. +Note that the function checks whether a position is legal by temporarily creating a new Stockfish process, and +then seeing if it can return a best move (and also not crash). Whatever the outcome may be though, this +temporary SF process should terminate after the function call. + ```python stockfish.is_fen_valid("rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1") ``` @@ -164,6 +180,8 @@ e2e4 ### Check if a move is legal in the current position +Returns True if the passed in move is legal in the current position. + ```python stockfish.is_move_correct('a2a3') ``` @@ -174,15 +192,21 @@ True ### Get info on the top n moves -Get moves, centipawns, and mates for the top n moves. If the move is a mate, the Centipawn value will be None, and vice versa. Note that if you have stockfish on a weaker elo or skill level setting, the top moves returned by this -function will still be for full strength. +Returns a list of dictionaries, where each dictionary represents a move's info. Each dictionary will contain a value for the 'Move' key, and either the 'Centipawn' or 'Mate' value will be a number (the other will be None). +Positive values mean advantage white, negative advantage black (unless you're using the turn perspective setting). + +Positive values mean advantage White, negative values mean advantage Black (unless you're using the turn perspective option, in which case positive is for the side to move). + +Note that if you have stockfish on a weaker elo or skill level setting, the top moves returned by this function will still be for full strength. + +Let's consider an example where Black is to move, and the top 3 moves are a mate, winning material, or being slightly worse. We'll assume the turn perspective setting is off. ```python stockfish.get_top_moves(3) # [ -# {'Move': 'f5h7', 'Centipawn': None, 'Mate': 1}, -# {'Move': 'f5d7', 'Centipawn': 713, 'Mate': None}, -# {'Move': 'f5h5', 'Centipawn': -31, 'Mate': None} +# {'Move': 'f5h3', 'Centipawn': None, 'Mate': -1}, # The move f5h3 leads to a mate in 1 for Black. +# {'Move': 'f5d7', 'Centipawn': -713, 'Mate': None}, # f5d7 leads to an evaluation of 7.13 in Black's favour. +# {'Move': 'f5h5', 'Centipawn': 31, 'Mate': None} # f5h5 leads to an evaluation of 0.31 in White's favour. # ] ``` @@ -288,7 +312,7 @@ stockfish.set_elo_rating(1350) stockfish.resume_full_strength() ``` -### Set the engine's depth +### Set the engine's search depth ```python stockfish.set_depth(15) @@ -493,12 +517,19 @@ False If the square is empty, the None object is returned. Otherwise, one of 12 enum members of a custom Stockfish.Piece enum will be returned. Each of the 12 members of this enum is named in the following pattern: _colour_ followed by _underscore_ followed by _piece name_, where the colour and piece name are in all caps. +The value of each enum member is a char representing the piece (uppercase is white, lowercase is black). +For white, it will be one of "P", "N", "B", "R", "Q", or "K". For black the same chars, except lowercase. For example, say the current position is the starting position: ```python stockfish.get_what_is_on_square("e1") # returns Stockfish.Piece.WHITE_KING +stockfish.get_what_is_on_square("e1").value # result is "K" stockfish.get_what_is_on_square("d8") # returns Stockfish.Piece.BLACK_QUEEN +stockfish.get_what_is_on_square("d8").value # result is "q" stockfish.get_what_is_on_square("h2") # returns Stockfish.Piece.WHITE_PAWN +stockfish.get_what_is_on_square("h2").value # result is "P" +stockfish.get_what_is_on_square("g8") # returns Stockfish.Piece.BLACK_KNIGHT +stockfish.get_what_is_on_square("g8").value # result is "n" stockfish.get_what_is_on_square("b5") # returns None ``` From 7aee8d05b236a8f821e3276ea8fbed0d009adbaa Mon Sep 17 00:00:00 2001 From: johndoknjas Date: Sat, 24 Aug 2024 01:58:04 -0700 Subject: [PATCH 12/36] Trim some trailing whitespace. --- README.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index be9c213..c13d007 100644 --- a/README.md +++ b/README.md @@ -105,8 +105,8 @@ stockfish.make_moves_from_start() ### Update position by making a sequence of moves from the current position -Function takes a list of strings as its argument. Each string represents a move, and must have the format of the starting coordinate followed by the ending coordinate. If a move leads to a pawn promoting, then an additional character must be appended at the end (to indicate what piece the pawn promotes into). -Other types of special moves (e.g., checks, captures, checkmates, en passants) do not need any special notation; the starting coordinate followed by the ending coordinate is all the information that's needed. Note that castling is represented by the starting coordinate of the king followed by the ending coordinate of the king. So "e1g1" would be used for white castling kingside, assuming the white king is still on e1 and castling is legal. +Function takes a list of strings as its argument. Each string represents a move, and must have the format of the starting coordinate followed by the ending coordinate. If a move leads to a pawn promoting, then an additional character must be appended at the end (to indicate what piece the pawn promotes into). +Other types of special moves (e.g., checks, captures, checkmates, en passants) do not need any special notation; the starting coordinate followed by the ending coordinate is all the information that's needed. Note that castling is represented by the starting coordinate of the king followed by the ending coordinate of the king. So "e1g1" would be used for white castling kingside, assuming the white king is still on e1 and castling is legal. Example call (assume in the current position, it is White's turn): ```python stockfish.make_moves_from_current_position(["g4d7", "a8b8", "f1d1", "b2b1q"]) # Moves the white piece on g4 to d7, then the black piece on a8 to b8, then the white piece on f1 to d1, and finally pushes the black b2-pawn to b1, promoting it into a queen. @@ -195,7 +195,7 @@ True Returns a list of dictionaries, where each dictionary represents a move's info. Each dictionary will contain a value for the 'Move' key, and either the 'Centipawn' or 'Mate' value will be a number (the other will be None). Positive values mean advantage white, negative advantage black (unless you're using the turn perspective setting). -Positive values mean advantage White, negative values mean advantage Black (unless you're using the turn perspective option, in which case positive is for the side to move). +Positive values mean advantage White, negative values mean advantage Black (unless you're using the turn perspective option, in which case positive is for the side to move). Note that if you have stockfish on a weaker elo or skill level setting, the top moves returned by this function will still be for full strength. @@ -517,8 +517,8 @@ False If the square is empty, the None object is returned. Otherwise, one of 12 enum members of a custom Stockfish.Piece enum will be returned. Each of the 12 members of this enum is named in the following pattern: _colour_ followed by _underscore_ followed by _piece name_, where the colour and piece name are in all caps. -The value of each enum member is a char representing the piece (uppercase is white, lowercase is black). -For white, it will be one of "P", "N", "B", "R", "Q", or "K". For black the same chars, except lowercase. +The value of each enum member is a char representing the piece (uppercase is white, lowercase is black). +For white, it will be one of "P", "N", "B", "R", "Q", or "K". For black the same chars, except lowercase. For example, say the current position is the starting position: ```python From 559dcaacdafdaf7cc026f13085e87677e4bd49dd Mon Sep 17 00:00:00 2001 From: johndoknjas Date: Sat, 24 Aug 2024 19:02:53 -0700 Subject: [PATCH 13/36] Correctly do short-circuiting. --- stockfish/models.py | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/stockfish/models.py b/stockfish/models.py index 2ced4ce..cad589d 100644 --- a/stockfish/models.py +++ b/stockfish/models.py @@ -647,14 +647,12 @@ def _is_fen_syntax_valid(fen: str) -> bool: fen_fields = fen.split() - if any( - ( - len(fen_fields) != 6, - len(fen_fields[0].split("/")) != 8, - any(x not in fen_fields[0] for x in "Kk"), - any(not fen_fields[x].isdigit() for x in (4, 5)), - int(fen_fields[4]) >= int(fen_fields[5]) * 2, - ) + if ( + len(fen_fields) != 6 + or len(fen_fields[0].split("/")) != 8 + or any(x not in fen_fields[0] for x in "Kk") + or any(not fen_fields[x].isdigit() for x in (4, 5)) + or int(fen_fields[4]) >= int(fen_fields[5]) * 2 ): return False From d2701b7105f01f2c3a36d30d95e731ed17a0e5de Mon Sep 17 00:00:00 2001 From: johndoknjas Date: Sun, 25 Aug 2024 19:51:58 -0700 Subject: [PATCH 14/36] Do not have the option of sending the `ucinewgame` command from one of the set position functions. Rather, make a new function that can do this if the user wants. --- README.md | 9 +++-- stockfish/models.py | 33 +++++++--------- tests/stockfish/test_models.py | 69 +++++++++++++++++----------------- 3 files changed, 54 insertions(+), 57 deletions(-) diff --git a/README.md b/README.md index c13d007..dac18db 100644 --- a/README.md +++ b/README.md @@ -120,9 +120,6 @@ Note that if you want to play Chess960, it's recommended you first update the "U stockfish.set_fen_position("rnbqkbnr/pppp1ppp/4p3/8/4P3/8/PPPP1PPP/RNBQKBNR w KQkq - 0 2") ``` -Note that if you'd like to send the `ucinewgame` command to Stockfish (which will reset its hash table), -send `True` as the second argument to `set_fen_position` or `make_moves_from_starting_position`. - ### Check whether the given FEN is valid This function returns a bool saying whether the passed in FEN is valid (both syntax wise and whether the position represented is legal). @@ -512,6 +509,12 @@ stockfish.is_development_build_of_engine() False ``` +### Send the "ucinewgame" command to the Stockfish engine process. +This command will clear Stockfish's hash table, which is relatively expensive and should generally only be done if the new position will be completely unrelated to the current one (such as a new game). +```python +stockfish.send_ucinewgame_command() +``` + ### Find what is on a certain square If the square is empty, the None object is returned. Otherwise, one of 12 enum members of a custom diff --git a/stockfish/models.py b/stockfish/models.py index cad589d..6893112 100644 --- a/stockfish/models.py +++ b/stockfish/models.py @@ -210,9 +210,17 @@ def reset_engine_parameters(self) -> None: """ self.update_engine_parameters(self._DEFAULT_STOCKFISH_PARAMS) + def send_ucinewgame_command(self) -> None: + """Sends the `ucinewgame` command to the Stockfish engine. This will clear Stockfish's + hash table, which is relatively expensive and should generally only be done if the + new position will be completely unrelated to the current one (such as a new game). + """ + if self._stockfish.poll() is None: + self._put("ucinewgame") + def _prepare_for_new_position(self, send_ucinewgame_token: bool) -> None: if send_ucinewgame_token: - self._put("ucinewgame") + self.send_ucinewgame_command() self._is_ready() self.info = "" @@ -300,32 +308,23 @@ def _weaker_setting_warning(self, message: str) -> None: """Will issue a warning, referring to the function that calls this one.""" warnings.warn(message, stacklevel=3) - def set_fen_position( - self, fen_position: str, send_ucinewgame_token: bool = False - ) -> None: + def set_fen_position(self, fen_position: str) -> None: """Sets the current board position from Forsyth-Edwards notation (FEN). Args: fen_position: FEN string of board position. - send_ucinewgame_token: - Whether to send the `ucinewgame` token to the Stockfish engine. - This will clear Stockfish's hash table, which should generally only be done if the new - position will be unrelated to the current one (such as a new game). - Returns: `None` Example: >>> stockfish.set_fen_position("1nb1k1n1/pppppppp/8/6r1/5bqK/6r1/8/8 w - - 2 2") """ - self._prepare_for_new_position(send_ucinewgame_token) + self._prepare_for_new_position(False) self._put(f"position fen {fen_position}") - def make_moves_from_start( - self, moves: Optional[List[str]] = None, send_ucinewgame_token: bool = False - ) -> None: + def make_moves_from_start(self, moves: Optional[List[str]] = None) -> None: """Sets the position by making a sequence of moves from the starting position of chess. Args: @@ -333,11 +332,6 @@ def make_moves_from_start( A list of moves to set this position on the board. Must be in full algebraic notation. - send_ucinewgame_token: - Whether to send the `ucinewgame` token to the Stockfish engine. - This will clear Stockfish's hash table, which should generally only be done if the new - position will be unrelated to the current one (such as a new game). - Returns: `None` @@ -345,8 +339,7 @@ def make_moves_from_start( >>> stockfish.make_moves_from_start(['e2e4', 'e7e5']) """ self.set_fen_position( - "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1", - send_ucinewgame_token, + "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1" ) self.make_moves_from_current_position(moves) diff --git a/tests/stockfish/test_models.py b/tests/stockfish/test_models.py index 2a15c28..7f4fd82 100644 --- a/tests/stockfish/test_models.py +++ b/tests/stockfish/test_models.py @@ -127,41 +127,45 @@ def test_set_fen_position_mate(self, stockfish: Stockfish): assert stockfish.info == "info depth 0 score mate 0" def test_clear_info_after_set_new_fen_position(self, stockfish: Stockfish): - stockfish.set_fen_position("8/8/8/6pp/8/4k1PP/r7/4K3 b - - 11 52", True) + stockfish.send_ucinewgame_command() + stockfish.set_fen_position("8/8/8/6pp/8/4k1PP/r7/4K3 b - - 11 52") stockfish.get_best_move() - stockfish.set_fen_position("8/8/8/6pp/8/4k1PP/8/r3K3 w - - 12 53", True) + stockfish.send_ucinewgame_command() + stockfish.set_fen_position("8/8/8/6pp/8/4k1PP/8/r3K3 w - - 12 53") assert stockfish.info == "" - stockfish.set_fen_position("8/8/8/6pp/8/4k1PP/r7/4K3 b - - 11 52", True) + stockfish.send_ucinewgame_command() + stockfish.set_fen_position("8/8/8/6pp/8/4k1PP/r7/4K3 b - - 11 52") stockfish.get_best_move() - stockfish.set_fen_position("8/8/8/6pp/8/4k1PP/8/r3K3 w - - 12 53", False) + stockfish.set_fen_position("8/8/8/6pp/8/4k1PP/8/r3K3 w - - 12 53") assert stockfish.info == "" def test_set_fen_position_starts_new_game(self, stockfish: Stockfish): + stockfish.send_ucinewgame_command() stockfish.set_fen_position( - "7r/1pr1kppb/2n1p2p/2NpP2P/5PP1/1P6/P6K/R1R2B2 w - - 1 27", True + "7r/1pr1kppb/2n1p2p/2NpP2P/5PP1/1P6/P6K/R1R2B2 w - - 1 27" ) stockfish.get_best_move() assert stockfish.info != "" - stockfish.set_fen_position( - "3kn3/p5rp/1p3p2/3B4/3P1P2/2P5/1P3K2/8 w - - 0 53", True - ) + stockfish.send_ucinewgame_command() + stockfish.set_fen_position("3kn3/p5rp/1p3p2/3B4/3P1P2/2P5/1P3K2/8 w - - 0 53") assert stockfish.info == "" def test_set_fen_position_second_argument(self, stockfish: Stockfish): stockfish.set_depth(16) + stockfish.send_ucinewgame_command() stockfish.set_fen_position( - "rnbqk2r/pppp1ppp/3bpn2/8/3PP3/2N5/PPP2PPP/R1BQKBNR w KQkq - 0 1", True + "rnbqk2r/pppp1ppp/3bpn2/8/3PP3/2N5/PPP2PPP/R1BQKBNR w KQkq - 0 1" ) assert stockfish.get_best_move() == "e4e5" stockfish.set_fen_position( - "rnbqk2r/pppp1ppp/3bpn2/4P3/3P4/2N5/PPP2PPP/R1BQKBNR b KQkq - 0 1", False + "rnbqk2r/pppp1ppp/3bpn2/4P3/3P4/2N5/PPP2PPP/R1BQKBNR b KQkq - 0 1" ) assert stockfish.get_best_move() in ("d6e7", "d6b4") stockfish.set_fen_position( - "rnbqk2r/pppp1ppp/3bpn2/8/3PP3/2N5/PPP2PPP/R1BQKBNR w KQkq - 0 1", False + "rnbqk2r/pppp1ppp/3bpn2/8/3PP3/2N5/PPP2PPP/R1BQKBNR w KQkq - 0 1" ) assert stockfish.get_best_move() == "e4e5" @@ -184,9 +188,8 @@ def test_is_move_correct_not_first_move(self, stockfish: Stockfish): ) # fmt: on def test_last_info(self, stockfish: Stockfish, value): - stockfish.set_fen_position( - "r6k/6b1/2b1Q3/p6p/1p5q/3P2PP/5r1K/8 w - - 1 31", True - ) + stockfish.send_ucinewgame_command() + stockfish.set_fen_position("r6k/6b1/2b1Q3/p6p/1p5q/3P2PP/5r1K/8 w - - 1 31") stockfish.get_best_move() assert value in stockfish.info @@ -520,22 +523,27 @@ def test_get_evaluation_stalemate(self, stockfish: Stockfish): def test_get_static_eval(self, stockfish: Stockfish): stockfish.set_turn_perspective(False) - stockfish.set_fen_position("r7/8/8/8/8/5k2/4p3/4K3 w - - 0 1", True) + stockfish.send_ucinewgame_command() + stockfish.set_fen_position("r7/8/8/8/8/5k2/4p3/4K3 w - - 0 1") static_eval_1 = stockfish.get_static_eval() assert isinstance(static_eval_1, float) and static_eval_1 < -3 - stockfish.set_fen_position("r7/8/8/8/8/5k2/4p3/4K3 b - - 0 1", True) + stockfish.send_ucinewgame_command() + stockfish.set_fen_position("r7/8/8/8/8/5k2/4p3/4K3 b - - 0 1") static_eval_2 = stockfish.get_static_eval() assert isinstance(static_eval_2, float) and static_eval_2 < -3 stockfish.set_turn_perspective() static_eval_3 = stockfish.get_static_eval() assert isinstance(static_eval_3, float) and static_eval_3 > 3 - stockfish.set_fen_position("r7/8/8/8/8/5k2/4p3/4K3 w - - 0 1", True) + stockfish.send_ucinewgame_command() + stockfish.set_fen_position("r7/8/8/8/8/5k2/4p3/4K3 w - - 0 1") static_eval_4 = stockfish.get_static_eval() assert isinstance(static_eval_4, float) and static_eval_4 < -3 if stockfish.get_stockfish_major_version() >= 12: - stockfish.set_fen_position("8/8/8/8/8/4k3/4p3/r3K3 w - - 0 1", True) + stockfish.send_ucinewgame_command() + stockfish.set_fen_position("8/8/8/8/8/4k3/4p3/r3K3 w - - 0 1") assert stockfish.get_static_eval() is None - stockfish.make_moves_from_start(None, True) + stockfish.send_ucinewgame_command() + stockfish.make_moves_from_start(None) stockfish.get_static_eval() stockfish._put("go depth 2") assert stockfish._read_line() != "" @@ -854,17 +862,8 @@ def test_make_moves_from_current_position(self, stockfish: Stockfish): stockfish.make_moves_from_current_position([invalid_move]) @pytest.mark.slow - def test_make_moves_transposition_table_speed(self, stockfish: Stockfish): - """ - make_moves_from_current_position won't send the "ucinewgame" token to Stockfish, since it - will reach a new position similar to the current one. Meanwhile, set_fen_position will send this - token (unless the user specifies otherwise), since it could be going to a completely new position. - - A big effect of sending this token is that it resets SF's transposition table. If the - new position is similar to the current one, this will affect SF's speed. This function tests - that make_moves_from_current_position doesn't reset the transposition table, by verifying SF is faster in - evaluating a consecutive set of positions when the make_moves_from_current_position function is used. - """ + def test_not_resetting_hash_table_speed(self, stockfish: Stockfish): + """Tests that not resetting the hash table between related positions makes SF faster.""" stockfish.set_depth(16) positions_considered = [] @@ -883,7 +882,8 @@ def test_make_moves_transposition_table_speed(self, stockfish: Stockfish): total_time_calculating_second = 0.0 for i in range(len(positions_considered)): - stockfish.set_fen_position(positions_considered[i], True) + stockfish.send_ucinewgame_command() + stockfish.set_fen_position(positions_considered[i]) start = default_timer() stockfish.get_best_move() total_time_calculating_second += default_timer() - start @@ -901,8 +901,9 @@ def test_get_wdl_stats(self, stockfish: Stockfish): assert wdl_stats[1] > wdl_stats[0] * 7 assert abs(wdl_stats[0] - wdl_stats[2]) / wdl_stats[0] < 0.15 + stockfish.send_ucinewgame_command() stockfish.set_fen_position( - "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1", True + "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1" ) wdl_stats_2 = stockfish.get_wdl_stats() assert isinstance(wdl_stats_2, list) @@ -912,9 +913,9 @@ def test_get_wdl_stats(self, stockfish: Stockfish): stockfish.set_fen_position("8/8/8/8/8/6k1/6p1/6K1 w - - 0 1") assert stockfish.get_wdl_stats() is None + stockfish.send_ucinewgame_command() stockfish.set_fen_position( - "rnbqkb1r/pp3ppp/3p1n2/1B2p3/3NP3/2N5/PPP2PPP/R1BQK2R b KQkq - 0 6", - True, + "rnbqkb1r/pp3ppp/3p1n2/1B2p3/3NP3/2N5/PPP2PPP/R1BQK2R b KQkq - 0 6" ) wdl_stats_3 = stockfish.get_wdl_stats() assert isinstance(wdl_stats_3, list) and len(wdl_stats_3) == 3 From ab682b94674d585cf5705149cedd5750b99f8bea Mon Sep 17 00:00:00 2001 From: johndoknjas Date: Sun, 25 Aug 2024 21:43:13 -0700 Subject: [PATCH 15/36] Don't send the `ucinewgame` command in the constructor; if the hash table is only a couple GBs, this can take more than half a second. --- stockfish/models.py | 10 ++++------ tests/stockfish/test_models.py | 3 ++- 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/stockfish/models.py b/stockfish/models.py index 6893112..d45ab7f 100644 --- a/stockfish/models.py +++ b/stockfish/models.py @@ -118,7 +118,7 @@ def __init__( if self.does_current_engine_version_have_wdl_option(): self._set_option("UCI_ShowWDL", True, False) - self._prepare_for_new_position(True) + self._prepare_for_new_position() def set_debug_view(self, activate: bool) -> None: self._debug_view = activate @@ -218,9 +218,7 @@ def send_ucinewgame_command(self) -> None: if self._stockfish.poll() is None: self._put("ucinewgame") - def _prepare_for_new_position(self, send_ucinewgame_token: bool) -> None: - if send_ucinewgame_token: - self.send_ucinewgame_command() + def _prepare_for_new_position(self) -> None: self._is_ready() self.info = "" @@ -321,7 +319,7 @@ def set_fen_position(self, fen_position: str) -> None: Example: >>> stockfish.set_fen_position("1nb1k1n1/pppppppp/8/6r1/5bqK/6r1/8/8 w - - 2 2") """ - self._prepare_for_new_position(False) + self._prepare_for_new_position() self._put(f"position fen {fen_position}") def make_moves_from_start(self, moves: Optional[List[str]] = None) -> None: @@ -359,7 +357,7 @@ def make_moves_from_current_position(self, moves: Optional[List[str]]) -> None: """ if not moves: return - self._prepare_for_new_position(False) + self._prepare_for_new_position() for move in moves: if not self.is_move_correct(move): raise ValueError(f"Cannot make move: {move}") diff --git a/tests/stockfish/test_models.py b/tests/stockfish/test_models.py index 7f4fd82..aa681bf 100644 --- a/tests/stockfish/test_models.py +++ b/tests/stockfish/test_models.py @@ -920,7 +920,8 @@ def test_get_wdl_stats(self, stockfish: Stockfish): wdl_stats_3 = stockfish.get_wdl_stats() assert isinstance(wdl_stats_3, list) and len(wdl_stats_3) == 3 - stockfish._prepare_for_new_position(True) + stockfish.send_ucinewgame_command() + stockfish._prepare_for_new_position() wdl_stats_4 = stockfish.get_wdl_stats(get_as_tuple=True) assert isinstance(wdl_stats_4, tuple) and len(wdl_stats_4) == 3 assert wdl_stats_3 == list(wdl_stats_4) From 39822e155a73704b4f1a24369400792f370d9f80 Mon Sep 17 00:00:00 2001 From: johndoknjas Date: Sun, 25 Aug 2024 22:06:14 -0700 Subject: [PATCH 16/36] Call `self._is_ready()` before sending a command. Also remove some unnecessary calls to `self._is_ready()`. --- stockfish/models.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/stockfish/models.py b/stockfish/models.py index d45ab7f..804c758 100644 --- a/stockfish/models.py +++ b/stockfish/models.py @@ -219,13 +219,14 @@ def send_ucinewgame_command(self) -> None: self._put("ucinewgame") def _prepare_for_new_position(self) -> None: - self._is_ready() self.info = "" def _put(self, command: str) -> None: if not self._stockfish.stdin: raise BrokenPipeError() if self._stockfish.poll() is None and not self._has_quit_command_been_sent: + if command != "isready": + self._is_ready() if self._debug_view: print(f">>> {command}\n") self._stockfish.stdin.write(f"{command}\n") @@ -258,7 +259,6 @@ def _set_option( self._put(f"setoption name {name} value {str_rep_value}") if update_parameters_attribute: self._parameters.update({name: value}) - self._is_ready() def _validate_param_val(self, name: str, value: Any) -> None: if name not in Stockfish._PARAM_RESTRICTIONS: From 19ba2c4c7770d6c46012733e6a9f97823d367cdc Mon Sep 17 00:00:00 2001 From: johndoknjas Date: Mon, 26 Aug 2024 07:13:59 -0700 Subject: [PATCH 17/36] Add a docstring to `_put`. --- stockfish/models.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/stockfish/models.py b/stockfish/models.py index 804c758..8458db0 100644 --- a/stockfish/models.py +++ b/stockfish/models.py @@ -222,6 +222,8 @@ def _prepare_for_new_position(self) -> None: self.info = "" def _put(self, command: str) -> None: + """Sends a command to the Stockfish engine. Note that if there's any existing output still + in stdout, it will be cleared.""" if not self._stockfish.stdin: raise BrokenPipeError() if self._stockfish.poll() is None and not self._has_quit_command_been_sent: From 4123b4310aa080df21d3b8a513f06646748b0c95 Mon Sep 17 00:00:00 2001 From: johndoknjas Date: Mon, 26 Aug 2024 07:32:13 -0700 Subject: [PATCH 18/36] Remove the `_is_ready` function and put its code directly in the `_put` function (since it is only used there). --- stockfish/models.py | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/stockfish/models.py b/stockfish/models.py index 8458db0..d6ba42f 100644 --- a/stockfish/models.py +++ b/stockfish/models.py @@ -222,13 +222,15 @@ def _prepare_for_new_position(self) -> None: self.info = "" def _put(self, command: str) -> None: - """Sends a command to the Stockfish engine. Note that if there's any existing output still - in stdout, it will be cleared.""" + """Sends a command to the Stockfish engine. Note that this function shouldn't be called if + there's any existing output in stdout that's still needed.""" if not self._stockfish.stdin: raise BrokenPipeError() if self._stockfish.poll() is None and not self._has_quit_command_been_sent: if command != "isready": - self._is_ready() + self._put("isready") + while self._read_line() != "readyok": + pass if self._debug_view: print(f">>> {command}\n") self._stockfish.stdin.write(f"{command}\n") @@ -273,11 +275,6 @@ def _validate_param_val(self, name: str, value: Any) -> None: if maximum is not None and type(value) is int and value > maximum: raise ValueError(f"{value} is over {name}'s maximum value of {maximum}") - def _is_ready(self) -> None: - self._put("isready") - while self._read_line() != "readyok": - pass - def _go(self) -> None: self._put(f"go depth {self._depth}") From 4f53176ecdf409111ef662a8464896178c5b62ee Mon Sep 17 00:00:00 2001 From: johndoknjas Date: Mon, 26 Aug 2024 17:02:22 -0700 Subject: [PATCH 19/36] Make `_is_ready` back into its own function. --- stockfish/models.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/stockfish/models.py b/stockfish/models.py index d6ba42f..aa6223d 100644 --- a/stockfish/models.py +++ b/stockfish/models.py @@ -228,9 +228,7 @@ def _put(self, command: str) -> None: raise BrokenPipeError() if self._stockfish.poll() is None and not self._has_quit_command_been_sent: if command != "isready": - self._put("isready") - while self._read_line() != "readyok": - pass + self._is_ready() if self._debug_view: print(f">>> {command}\n") self._stockfish.stdin.write(f"{command}\n") @@ -275,6 +273,11 @@ def _validate_param_val(self, name: str, value: Any) -> None: if maximum is not None and type(value) is int and value > maximum: raise ValueError(f"{value} is over {name}'s maximum value of {maximum}") + def _is_ready(self) -> None: + self._put("isready") + while self._read_line() != "readyok": + pass + def _go(self) -> None: self._put(f"go depth {self._depth}") From 8cc04be8eee16e272b1ad2bf0e0fa314b16d7ba0 Mon Sep 17 00:00:00 2001 From: johndoknjas Date: Mon, 26 Aug 2024 17:19:02 -0700 Subject: [PATCH 20/36] After sending the `ucinewgame` command, wait until Stockfish finishes doing it. --- stockfish/models.py | 3 +++ tests/stockfish/test_models.py | 7 +++++++ 2 files changed, 10 insertions(+) diff --git a/stockfish/models.py b/stockfish/models.py index aa6223d..e9a2291 100644 --- a/stockfish/models.py +++ b/stockfish/models.py @@ -217,6 +217,7 @@ def send_ucinewgame_command(self) -> None: """ if self._stockfish.poll() is None: self._put("ucinewgame") + self._is_ready() def _prepare_for_new_position(self) -> None: self.info = "" @@ -274,6 +275,8 @@ def _validate_param_val(self, name: str, value: Any) -> None: raise ValueError(f"{value} is over {name}'s maximum value of {maximum}") def _is_ready(self) -> None: + """Waits if the engine is busy. Note that this function shouldn't be called if + there's any existing output in stdout that's still needed.""" self._put("isready") while self._read_line() != "readyok": pass diff --git a/tests/stockfish/test_models.py b/tests/stockfish/test_models.py index aa681bf..e414763 100644 --- a/tests/stockfish/test_models.py +++ b/tests/stockfish/test_models.py @@ -1272,3 +1272,10 @@ def test_get_engine_parameters(self, stockfish: Stockfish): params.update({"Skill Level": 10}) assert params["Skill Level"] == 10 assert stockfish._parameters["Skill Level"] == 20 + + @pytest.mark.slow + def test_uci_new_game_wait(self, stockfish: Stockfish): + stockfish.update_engine_parameters({"Hash": 2048}) + start = time.time_ns() + stockfish.send_ucinewgame_command() + assert time.time_ns() - start > 1000000 From 07afd5c83f055f6af815ed06fc593492a82d94be Mon Sep 17 00:00:00 2001 From: johndoknjas Date: Mon, 26 Aug 2024 17:36:22 -0700 Subject: [PATCH 21/36] Add a couple other `_is_ready()` checks. --- stockfish/models.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/stockfish/models.py b/stockfish/models.py index e9a2291..51c0159 100644 --- a/stockfish/models.py +++ b/stockfish/models.py @@ -119,6 +119,7 @@ def __init__( self._set_option("UCI_ShowWDL", True, False) self._prepare_for_new_position() + self._is_ready() def set_debug_view(self, activate: bool) -> None: self._debug_view = activate @@ -262,6 +263,7 @@ def _set_option( self._put(f"setoption name {name} value {str_rep_value}") if update_parameters_attribute: self._parameters.update({name: value}) + self._is_ready() def _validate_param_val(self, name: str, value: Any) -> None: if name not in Stockfish._PARAM_RESTRICTIONS: From 351be02cee819ccbbd1022a9e3b5c5e4d4ed07b4 Mon Sep 17 00:00:00 2001 From: johndoknjas Date: Mon, 26 Aug 2024 18:20:32 -0700 Subject: [PATCH 22/36] Make the max allowed hash size 2^25 MB if on a 64-bit system. --- stockfish/models.py | 3 ++- tests/stockfish/test_models.py | 8 +++++++- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/stockfish/models.py b/stockfish/models.py index 51c0159..db08e74 100644 --- a/stockfish/models.py +++ b/stockfish/models.py @@ -15,6 +15,7 @@ import re import datetime import warnings +import platform class Stockfish: @@ -42,7 +43,7 @@ class Stockfish: _PARAM_RESTRICTIONS: Dict[str, Tuple[type, Optional[int], Optional[int]]] = { "Debug Log File": (str, None, None), "Threads": (int, 1, 1024), - "Hash": (int, 1, 2048), + "Hash": (int, 1, 2 ** (25 if "64" in platform.machine() else 11)), "Ponder": (bool, None, None), "MultiPV": (int, 1, 500), "Skill Level": (int, 0, 20), diff --git a/tests/stockfish/test_models.py b/tests/stockfish/test_models.py index e414763..82129c5 100644 --- a/tests/stockfish/test_models.py +++ b/tests/stockfish/test_models.py @@ -2,6 +2,7 @@ from timeit import default_timer import time from typing import List, Optional, Dict +import platform from stockfish import Stockfish, StockfishException @@ -309,13 +310,14 @@ def test_update_engine_parameters_wrong_vals(self, stockfish: Stockfish): assert set(stockfish.get_engine_parameters().keys()) <= set( Stockfish._PARAM_RESTRICTIONS.keys() ) + max_hash = 2 ** (25 if "64" in platform.machine() else 11) bad_values: Dict[str, List] = { "Threads": ["1", False, 0, -1, 1025, 1.0], "UCI_Chess960": ["true", "false", "True", 1], "Contempt": [-101, 101, "0", False], "UCI_LimitStrength": ["true", "false", "False", 1, 0], "Ponder": ["true", "false", "True", "False", 0], - "Hash": [-1, 4096, -2048, True, 0], + "Hash": [-1, max_hash * 2, max_hash + 1, -2048, True, 0], "Not key": [0], } for name in bad_values: @@ -1279,3 +1281,7 @@ def test_uci_new_game_wait(self, stockfish: Stockfish): start = time.time_ns() stockfish.send_ucinewgame_command() assert time.time_ns() - start > 1000000 + + def test_hash_size_platform(self, stockfish: Stockfish): + max_hash = stockfish._PARAM_RESTRICTIONS["Hash"][2] + assert max_hash == 2 ** (25 if "64" in platform.machine() else 11) From 05aacd45696ce58ade3762545d06a723554b3a48 Mon Sep 17 00:00:00 2001 From: johndoknjas Date: Mon, 26 Aug 2024 18:50:30 -0700 Subject: [PATCH 23/36] Update comment in test. --- tests/stockfish/test_models.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/stockfish/test_models.py b/tests/stockfish/test_models.py index 82129c5..2ebc93e 100644 --- a/tests/stockfish/test_models.py +++ b/tests/stockfish/test_models.py @@ -1137,7 +1137,8 @@ def test_invalid_fen_king_attacked(self, stockfish: Stockfish, fen): fen == "8/8/8/3k4/3K4/8/8/8 b - - 0 1" and stockfish.get_stockfish_major_version() >= 14 ): - # Since for that FEN, SF 15 actually outputs a best move without crashing (unlike SF 14 and earlier). + # Since for this FEN, more recent versions of SF (some dev versions of 14 and later) + # output a best move without crashing. return if ( fen == "2k2q2/8/8/8/8/8/8/2Q2K2 w - - 0 1" From ca1e7462f82f63b67f620684adda1c929443855c Mon Sep 17 00:00:00 2001 From: johndoknjas Date: Mon, 26 Aug 2024 19:04:11 -0700 Subject: [PATCH 24/36] Replace `assert` usages in `models.py`, as assertions can be ignored by not running in debug mode. --- stockfish/models.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/stockfish/models.py b/stockfish/models.py index db08e74..d8604c5 100644 --- a/stockfish/models.py +++ b/stockfish/models.py @@ -841,7 +841,8 @@ def get_static_eval(self) -> Optional[float]: self._read_line() # Consume the remaining line (for some reason `eval` outputs an extra newline) if static_eval == "none": - assert "(in check)" in text + if "(in check)" not in text: + raise RuntimeError() return None return float(static_eval) * compare @@ -1016,7 +1017,8 @@ def get_perft(self, depth: int) -> Tuple[int, dict[str, int]]: num_nodes = int(line.split(":")[1]) break move, num = line.split(":") - assert move not in move_possibilities + if move in move_possibilities: + raise RuntimeError() move_possibilities[move] = int(num) self._read_line() # Consumes the remaining newline stockfish outputs. From 06182031fbc4bf242adf9efd0e2ee5153e76f3aa Mon Sep 17 00:00:00 2001 From: johndoknjas Date: Tue, 27 Aug 2024 07:07:14 -0700 Subject: [PATCH 25/36] Modify the description in the readme to match that in `setup.py`. --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index dac18db..8ce7536 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@ > [!NOTE] > This section refers to the technical application. If you are looking for information regarding the status of this project and the original repo, please look [here](https://github.com/py-stockfish/stockfish/tree/master#status-of-the-project). -Implements an easy-to-use Stockfish class to integrates the Stockfish chess engine with Python. +Wraps the open-source Stockfish chess engine for easy integration into python. ## Install From 25473eb289b673a3bae6c9084cfc1a2bd4cc867a Mon Sep 17 00:00:00 2001 From: johndoknjas Date: Tue, 27 Aug 2024 07:49:07 -0700 Subject: [PATCH 26/36] Undo a change from a few years ago in commit 1c7be8b8, where the make_moves function was modified to set the fen position after each move (in order to check for the correctness of each move). The issue with this is it will not allow the engine to detect game-state stuff like threefolds, even if the user sends all the moves in the game at once. --- stockfish/models.py | 5 +---- tests/stockfish/test_models.py | 7 +++++-- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/stockfish/models.py b/stockfish/models.py index d8604c5..e57866f 100644 --- a/stockfish/models.py +++ b/stockfish/models.py @@ -366,10 +366,7 @@ def make_moves_from_current_position(self, moves: Optional[List[str]]) -> None: if not moves: return self._prepare_for_new_position() - for move in moves: - if not self.is_move_correct(move): - raise ValueError(f"Cannot make move: {move}") - self._put(f"position fen {self.get_fen_position()} moves {move}") + self._put(f"position fen {self.get_fen_position()} moves {' '.join(moves)}") def get_board_visual(self, perspective_white: bool = True) -> str: """Returns a visual representation of the current board position. diff --git a/tests/stockfish/test_models.py b/tests/stockfish/test_models.py index 2ebc93e..5ccd813 100644 --- a/tests/stockfish/test_models.py +++ b/tests/stockfish/test_models.py @@ -859,9 +859,12 @@ def test_make_moves_from_current_position(self, stockfish: Stockfish): invalid_moves = ["d1e3", "e1g1", "c3d5", "c1d4", "a7a6", "e1d2", "word"] + current_fen = stockfish.get_fen_position() + stockfish.make_moves_from_current_position(invalid_moves) + assert current_fen == stockfish.get_fen_position() for invalid_move in invalid_moves: - with pytest.raises(ValueError): - stockfish.make_moves_from_current_position([invalid_move]) + stockfish.make_moves_from_current_position([invalid_move]) + assert current_fen == stockfish.get_fen_position() @pytest.mark.slow def test_not_resetting_hash_table_speed(self, stockfish: Stockfish): From eaf6a0a351f379b89c661ecc881cd098ecefacd4 Mon Sep 17 00:00:00 2001 From: johndoknjas Date: Tue, 27 Aug 2024 08:31:34 -0700 Subject: [PATCH 27/36] Add a threefold detection test for the make_moves function. --- tests/stockfish/test_models.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/tests/stockfish/test_models.py b/tests/stockfish/test_models.py index 5ccd813..28d63d1 100644 --- a/tests/stockfish/test_models.py +++ b/tests/stockfish/test_models.py @@ -1289,3 +1289,8 @@ def test_uci_new_game_wait(self, stockfish: Stockfish): def test_hash_size_platform(self, stockfish: Stockfish): max_hash = stockfish._PARAM_RESTRICTIONS["Hash"][2] assert max_hash == 2 ** (25 if "64" in platform.machine() else 11) + + def test_threefold_detection(self, stockfish: Stockfish): + stockfish.set_depth(5) + stockfish.make_moves_from_current_position(["g1f3", "g8f6", "f3g1", "f6g8", "g1f3", "g8f6", "f3g1", "f6g8", "g1f3"]) + assert stockfish.get_evaluation()["value"] == 0 \ No newline at end of file From bbcfa8beaf068da70c8287ca49701532128c3698 Mon Sep 17 00:00:00 2001 From: johndoknjas Date: Tue, 27 Aug 2024 09:48:06 -0700 Subject: [PATCH 28/36] Update gitignore. --- .gitignore | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index f274de8..9a3f603 100644 --- a/.gitignore +++ b/.gitignore @@ -18,6 +18,6 @@ */__pycache__ */*/__pycache__ coverage.xml -.coverage +.coverage* docs/ .vscode/ From 55c025a53eb56ed6c02606421aff3228a474a0a0 Mon Sep 17 00:00:00 2001 From: johndoknjas Date: Tue, 27 Aug 2024 09:56:37 -0700 Subject: [PATCH 29/36] Format with black. --- tests/stockfish/test_models.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/tests/stockfish/test_models.py b/tests/stockfish/test_models.py index 28d63d1..a838f35 100644 --- a/tests/stockfish/test_models.py +++ b/tests/stockfish/test_models.py @@ -1292,5 +1292,7 @@ def test_hash_size_platform(self, stockfish: Stockfish): def test_threefold_detection(self, stockfish: Stockfish): stockfish.set_depth(5) - stockfish.make_moves_from_current_position(["g1f3", "g8f6", "f3g1", "f6g8", "g1f3", "g8f6", "f3g1", "f6g8", "g1f3"]) - assert stockfish.get_evaluation()["value"] == 0 \ No newline at end of file + stockfish.make_moves_from_current_position( + ["g1f3", "g8f6", "f3g1", "f6g8", "g1f3", "g8f6", "f3g1", "f6g8", "g1f3"] + ) + assert stockfish.get_evaluation()["value"] == 0 From 693cbd032bcd4a076fbd6824d60864403b245ff7 Mon Sep 17 00:00:00 2001 From: johndoknjas Date: Tue, 27 Aug 2024 10:50:17 -0700 Subject: [PATCH 30/36] Expand the threefold test, and add an allowed value in another test. --- tests/stockfish/test_models.py | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/tests/stockfish/test_models.py b/tests/stockfish/test_models.py index a838f35..6ba0037 100644 --- a/tests/stockfish/test_models.py +++ b/tests/stockfish/test_models.py @@ -87,7 +87,7 @@ def test_get_best_move_remaining_time_not_first_move(self, stockfish: Stockfish) best_move = stockfish.get_best_move(wtime=1000) assert best_move in ("d2d4", "a2a3", "d1e2", "b1c3") best_move = stockfish.get_best_move(btime=1000) - assert best_move in ("d2d4", "b1c3") + assert best_move in ("d2d4", "b1c3", "g1f3") best_move = stockfish.get_best_move(wtime=1000, btime=1000) assert best_move in ("d2d4", "b1c3", "g1f3") best_move = stockfish.get_best_move(wtime=5 * 60 * 1000, btime=1000) @@ -1296,3 +1296,15 @@ def test_threefold_detection(self, stockfish: Stockfish): ["g1f3", "g8f6", "f3g1", "f6g8", "g1f3", "g8f6", "f3g1", "f6g8", "g1f3"] ) assert stockfish.get_evaluation()["value"] == 0 + stockfish.make_moves_from_start( + ["g1f3", "g8f6", "f3g1", "f6g8", "g1f3", "g8f6", "f3g1", "f6g8", "g1f3"] + ) + assert stockfish.get_evaluation()["value"] == 0 + stockfish.make_moves_from_start( + ["g1f3", "g8f6", "f3g1", "f6g8", "g1f3"] + ) + assert stockfish.get_evaluation()["value"] < 0 + stockfish.make_moves_from_start( + ["g8f6", "f3g1", "f6g8", "g1f3"] + ) + assert stockfish.get_evaluation()["value"] < 0 From 8f98dcdc0031b8e27e4a82c67c2ff627b44a17ba Mon Sep 17 00:00:00 2001 From: johndoknjas Date: Tue, 27 Aug 2024 12:35:57 -0700 Subject: [PATCH 31/36] Format with black. --- tests/stockfish/test_models.py | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/tests/stockfish/test_models.py b/tests/stockfish/test_models.py index 6ba0037..7a06c22 100644 --- a/tests/stockfish/test_models.py +++ b/tests/stockfish/test_models.py @@ -1300,11 +1300,7 @@ def test_threefold_detection(self, stockfish: Stockfish): ["g1f3", "g8f6", "f3g1", "f6g8", "g1f3", "g8f6", "f3g1", "f6g8", "g1f3"] ) assert stockfish.get_evaluation()["value"] == 0 - stockfish.make_moves_from_start( - ["g1f3", "g8f6", "f3g1", "f6g8", "g1f3"] - ) + stockfish.make_moves_from_start(["g1f3", "g8f6", "f3g1", "f6g8", "g1f3"]) assert stockfish.get_evaluation()["value"] < 0 - stockfish.make_moves_from_start( - ["g8f6", "f3g1", "f6g8", "g1f3"] - ) + stockfish.make_moves_from_start(["g8f6", "f3g1", "f6g8", "g1f3"]) assert stockfish.get_evaluation()["value"] < 0 From 9458184c6decf4a618483fd80e920f3fdc46e4bc Mon Sep 17 00:00:00 2001 From: johndoknjas Date: Tue, 27 Aug 2024 12:41:20 -0700 Subject: [PATCH 32/36] Fix test. --- tests/stockfish/test_models.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/stockfish/test_models.py b/tests/stockfish/test_models.py index 7a06c22..c3facaa 100644 --- a/tests/stockfish/test_models.py +++ b/tests/stockfish/test_models.py @@ -1302,5 +1302,5 @@ def test_threefold_detection(self, stockfish: Stockfish): assert stockfish.get_evaluation()["value"] == 0 stockfish.make_moves_from_start(["g1f3", "g8f6", "f3g1", "f6g8", "g1f3"]) assert stockfish.get_evaluation()["value"] < 0 - stockfish.make_moves_from_start(["g8f6", "f3g1", "f6g8", "g1f3"]) + stockfish.make_moves_from_current_position(["g8f6", "f3g1", "f6g8", "g1f3"]) assert stockfish.get_evaluation()["value"] < 0 From 903e52ecf638877080971b534b08fc31beb1a637 Mon Sep 17 00:00:00 2001 From: johndoknjas Date: Tue, 27 Aug 2024 12:48:19 -0700 Subject: [PATCH 33/36] Fix mypy error. --- tests/stockfish/test_models.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/tests/stockfish/test_models.py b/tests/stockfish/test_models.py index c3facaa..84ed1ba 100644 --- a/tests/stockfish/test_models.py +++ b/tests/stockfish/test_models.py @@ -1301,6 +1301,10 @@ def test_threefold_detection(self, stockfish: Stockfish): ) assert stockfish.get_evaluation()["value"] == 0 stockfish.make_moves_from_start(["g1f3", "g8f6", "f3g1", "f6g8", "g1f3"]) - assert stockfish.get_evaluation()["value"] < 0 + assert ( + isinstance((eval := stockfish.get_evaluation()["value"]), int) and eval < 0 + ) stockfish.make_moves_from_current_position(["g8f6", "f3g1", "f6g8", "g1f3"]) - assert stockfish.get_evaluation()["value"] < 0 + assert ( + isinstance((eval := stockfish.get_evaluation()["value"]), int) and eval < 0 + ) From 3854c9016685a5ddeb76d6caf9bc5c2fc3242ba3 Mon Sep 17 00:00:00 2001 From: johndoknjas Date: Tue, 27 Aug 2024 13:04:26 -0700 Subject: [PATCH 34/36] Write helper function to test for types and then compare. --- tests/stockfish/test_models.py | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/tests/stockfish/test_models.py b/tests/stockfish/test_models.py index 84ed1ba..e724393 100644 --- a/tests/stockfish/test_models.py +++ b/tests/stockfish/test_models.py @@ -1,12 +1,19 @@ import pytest from timeit import default_timer import time -from typing import List, Optional, Dict +from typing import List, Optional, Dict, Callable, Type, Any import platform +import operator from stockfish import Stockfish, StockfishException +def compare(first, second, op: Callable[[Any, Any], bool], expected_type: Type) -> bool: + return all(isinstance(x, expected_type) for x in (first, second)) and op( + first, second + ) + + class TestStockfish: @pytest.fixture def stockfish(self) -> Stockfish: @@ -1301,10 +1308,6 @@ def test_threefold_detection(self, stockfish: Stockfish): ) assert stockfish.get_evaluation()["value"] == 0 stockfish.make_moves_from_start(["g1f3", "g8f6", "f3g1", "f6g8", "g1f3"]) - assert ( - isinstance((eval := stockfish.get_evaluation()["value"]), int) and eval < 0 - ) + assert compare(stockfish.get_evaluation()["value"], 0, operator.lt, int) stockfish.make_moves_from_current_position(["g8f6", "f3g1", "f6g8", "g1f3"]) - assert ( - isinstance((eval := stockfish.get_evaluation()["value"]), int) and eval < 0 - ) + assert compare(stockfish.get_evaluation()["value"], 0, operator.lt, int) From 4e28a420b80ae13923b5bd303581905c25b94daa Mon Sep 17 00:00:00 2001 From: johndoknjas Date: Tue, 27 Aug 2024 13:16:30 -0700 Subject: [PATCH 35/36] Use the new `compare` helper function in some other tests too. --- tests/stockfish/test_models.py | 32 ++++++++++++++------------------ 1 file changed, 14 insertions(+), 18 deletions(-) diff --git a/tests/stockfish/test_models.py b/tests/stockfish/test_models.py index e724393..1d87b12 100644 --- a/tests/stockfish/test_models.py +++ b/tests/stockfish/test_models.py @@ -493,19 +493,17 @@ def test_get_evaluation_cp(self, stockfish: Stockfish): ) evaluation = stockfish.get_evaluation() assert ( - evaluation["type"] == "cp" - and isinstance(evaluation["value"], int) - and evaluation["value"] >= 60 - and evaluation["value"] <= 150 + compare(evaluation["type"], "cp", operator.eq, str) + and compare(evaluation["value"], 60, operator.ge, int) + and compare(evaluation["value"], 150, operator.le, int) ) stockfish.set_skill_level(1) with pytest.warns(UserWarning): evaluation = stockfish.get_evaluation() assert ( - evaluation["type"] == "cp" - and isinstance(evaluation["value"], int) - and evaluation["value"] >= 60 - and evaluation["value"] <= 150 + compare(evaluation["type"], "cp", operator.eq, str) + and compare(evaluation["value"], 60, operator.ge, int) + and compare(evaluation["value"], 150, operator.le, int) ) @pytest.mark.slow @@ -535,18 +533,18 @@ def test_get_static_eval(self, stockfish: Stockfish): stockfish.send_ucinewgame_command() stockfish.set_fen_position("r7/8/8/8/8/5k2/4p3/4K3 w - - 0 1") static_eval_1 = stockfish.get_static_eval() - assert isinstance(static_eval_1, float) and static_eval_1 < -3 + assert compare(static_eval_1, -3, operator.lt, float) stockfish.send_ucinewgame_command() stockfish.set_fen_position("r7/8/8/8/8/5k2/4p3/4K3 b - - 0 1") static_eval_2 = stockfish.get_static_eval() - assert isinstance(static_eval_2, float) and static_eval_2 < -3 + assert compare(static_eval_2, -3, operator.lt, float) stockfish.set_turn_perspective() static_eval_3 = stockfish.get_static_eval() - assert isinstance(static_eval_3, float) and static_eval_3 > 3 + assert compare(static_eval_3, 3, operator.gt, float) stockfish.send_ucinewgame_command() stockfish.set_fen_position("r7/8/8/8/8/5k2/4p3/4K3 w - - 0 1") static_eval_4 = stockfish.get_static_eval() - assert isinstance(static_eval_4, float) and static_eval_4 < -3 + assert compare(static_eval_4, -3, operator.lt, float) if stockfish.get_stockfish_major_version() >= 12: stockfish.send_ucinewgame_command() stockfish.set_fen_position("8/8/8/8/8/4k3/4p3/r3K3 w - - 0 1") @@ -817,14 +815,12 @@ def test_turn_perspective(self, stockfish: Stockfish): assert stockfish.get_turn_perspective() moves = stockfish.get_top_moves(1) assert moves[0]["Centipawn"] > 0 - eval = stockfish.get_evaluation()["value"] - assert isinstance(eval, int) and eval > 0 + assert compare(stockfish.get_evaluation()["value"], 0, operator.gt, int) stockfish.set_turn_perspective(False) assert stockfish.get_turn_perspective() is False moves = stockfish.get_top_moves(1) assert moves[0]["Centipawn"] < 0 - eval = stockfish.get_evaluation()["value"] - assert isinstance(eval, int) and eval < 0 + assert compare(stockfish.get_evaluation()["value"], 0, operator.lt, int) def test_turn_perspective_raises_type_error(self, stockfish: Stockfish): with pytest.raises(TypeError): @@ -1302,11 +1298,11 @@ def test_threefold_detection(self, stockfish: Stockfish): stockfish.make_moves_from_current_position( ["g1f3", "g8f6", "f3g1", "f6g8", "g1f3", "g8f6", "f3g1", "f6g8", "g1f3"] ) - assert stockfish.get_evaluation()["value"] == 0 + assert compare(stockfish.get_evaluation()["value"], 0, operator.eq, int) stockfish.make_moves_from_start( ["g1f3", "g8f6", "f3g1", "f6g8", "g1f3", "g8f6", "f3g1", "f6g8", "g1f3"] ) - assert stockfish.get_evaluation()["value"] == 0 + assert compare(stockfish.get_evaluation()["value"], 0, operator.eq, int) stockfish.make_moves_from_start(["g1f3", "g8f6", "f3g1", "f6g8", "g1f3"]) assert compare(stockfish.get_evaluation()["value"], 0, operator.lt, int) stockfish.make_moves_from_current_position(["g8f6", "f3g1", "f6g8", "g1f3"]) From 4007b6c302ac1e089db44699a1ca08c5a64c2992 Mon Sep 17 00:00:00 2001 From: johndoknjas Date: Tue, 27 Aug 2024 13:28:39 -0700 Subject: [PATCH 36/36] Fix problem in test. --- tests/stockfish/test_models.py | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/tests/stockfish/test_models.py b/tests/stockfish/test_models.py index 1d87b12..50e5faf 100644 --- a/tests/stockfish/test_models.py +++ b/tests/stockfish/test_models.py @@ -532,19 +532,15 @@ def test_get_static_eval(self, stockfish: Stockfish): stockfish.set_turn_perspective(False) stockfish.send_ucinewgame_command() stockfish.set_fen_position("r7/8/8/8/8/5k2/4p3/4K3 w - - 0 1") - static_eval_1 = stockfish.get_static_eval() - assert compare(static_eval_1, -3, operator.lt, float) + assert compare(stockfish.get_static_eval(), -3.0, operator.lt, float) stockfish.send_ucinewgame_command() stockfish.set_fen_position("r7/8/8/8/8/5k2/4p3/4K3 b - - 0 1") - static_eval_2 = stockfish.get_static_eval() - assert compare(static_eval_2, -3, operator.lt, float) + assert compare(stockfish.get_static_eval(), -3.0, operator.lt, float) stockfish.set_turn_perspective() - static_eval_3 = stockfish.get_static_eval() - assert compare(static_eval_3, 3, operator.gt, float) + assert compare(stockfish.get_static_eval(), 3.0, operator.gt, float) stockfish.send_ucinewgame_command() stockfish.set_fen_position("r7/8/8/8/8/5k2/4p3/4K3 w - - 0 1") - static_eval_4 = stockfish.get_static_eval() - assert compare(static_eval_4, -3, operator.lt, float) + assert compare(stockfish.get_static_eval(), -3.0, operator.lt, float) if stockfish.get_stockfish_major_version() >= 12: stockfish.send_ucinewgame_command() stockfish.set_fen_position("8/8/8/8/8/4k3/4p3/r3K3 w - - 0 1")