Skip to content

Commit

Permalink
Functions for static evaluation and resuming full strength. Also upda…
Browse files Browse the repository at this point in the history
…ted docs and added warnings for weaker settings. (zhelyabuzhsky#38)

* Add functions to get the static evaluation, and resume full strength. Also updated some documentation.

* Update static eval test.

* Account for earlier versions of stockfish prefixing the static evaluation line with 'Total Evaluation'.

* Update README.md

* Issue warnings in get_top_moves, get_evaluation, and get_wdl_stats, when Stockfish is set to play on a weaker setting.

* Remove unnecessary calls to set the fen position.
  • Loading branch information
johndoknjas authored May 23, 2023
1 parent 5176411 commit 417e57d
Show file tree
Hide file tree
Showing 3 changed files with 168 additions and 8 deletions.
33 changes: 31 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -164,7 +164,8 @@ 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.
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.

```python
stockfish.get_top_moves(3)
Expand Down Expand Up @@ -245,6 +246,11 @@ stockfish.set_skill_level(15)
stockfish.set_elo_rating(1350)
```

### Put the engine back to full strength (if you've previously lowered the ELO or skill level)
```python
stockfish.resume_full_strength()
```

### Set the engine's depth

```python
Expand Down Expand Up @@ -346,12 +352,13 @@ stockfish.get_board_visual(False)
h g f e d c b a
```

### Get the current board evaluation in centipawns or mate in x
### Get the current position's evaluation in centipawns or mate in x

```python
stockfish.get_evaluation()
```

Stockfish searches to the specified depth and evaluates the current position.
A dictionary is returned representing the evaluation. Two example return values:

```text
Expand All @@ -363,6 +370,28 @@ A dictionary is returned representing the evaluation. Two example return values:
If stockfish.get_turn_perspective() is True, then the eval value is relative to the side to move.
Otherwise, positive is advantage white, negative is advantage black.

### Get the current position's 'static evaluation'

```python
stockfish.get_static_eval()
```

Sends the 'eval' command to Stockfish. This will get it to 'directly' evaluate the current position
(i.e., no search is involved), and output a float value (not a whole number centipawn).

If one side is in check or mated, recent versions of Stockfish will output 'none' for the static eval.
In this case, the function will return None.

Some example return values:

```text
-5.27
0.28
None
```

### Run benchmark

#### BenchmarkParameters
Expand Down
81 changes: 75 additions & 6 deletions stockfish/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
from dataclasses import dataclass
from enum import Enum
import re
import warnings


class Stockfish:
Expand Down Expand Up @@ -237,6 +238,16 @@ def _go_remaining_time(self, wtime: Optional[int], btime: Optional[int]) -> None
cmd += f" btime {btime}"
self._put(cmd)

def _on_weaker_setting(self) -> bool:
return (
self._parameters["UCI_LimitStrength"]
or self._parameters["Skill Level"] < 20
)

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 = True
) -> None:
Expand Down Expand Up @@ -417,6 +428,17 @@ def set_elo_rating(self, elo_rating: int = 1350) -> None:
{"UCI_LimitStrength": True, "UCI_Elo": elo_rating}
)

def resume_full_strength(self) -> None:
"""Puts Stockfish back to full strength, if you've previously lowered the elo or skill level.
Returns:
`None`
Example:
>>> stockfish.reset_to_full_strength()
"""
self.update_engine_parameters({"UCI_LimitStrength": False, "Skill Level": 20})

def set_depth(self, depth: int = 15) -> None:
"""Sets current depth of Stockfish engine.
Expand Down Expand Up @@ -638,6 +660,12 @@ def get_wdl_stats(self) -> Optional[List]:
raise RuntimeError(
"Your version of Stockfish isn't recent enough to have the UCI_ShowWDL option."
)
if self._on_weaker_setting():
self._weaker_setting_warning(
"""Note that even though you've set Stockfish to play on a weaker elo or skill level,"""
+ """ get_wdl_stats will still return full strength Stockfish's wdl stats of the position."""
)

self._go()
lines = []
while True:
Expand Down Expand Up @@ -680,19 +708,26 @@ def does_current_engine_version_have_wdl_option(self) -> bool:
# the last line SF outputs for the "uci" command.

def get_evaluation(self) -> dict:
"""Evaluates current position
"""Searches to the specified depth and evaluates the current position
Returns:
A dictionary of the current advantage with "type" as "cp" (centipawns) or "mate" (mate in n moves)
"""
evaluation = dict()
fen_position = self.get_fen_position()
compare = 1 if self.get_turn_perspective() or ("w" in fen_position) else -1

if self._on_weaker_setting():
self._weaker_setting_warning(
"""Note that even though you've set Stockfish to play on a weaker elo or skill level,"""
+ """ get_evaluation will still return full strength Stockfish's evaluation of the position."""
)

compare = (
1 if self.get_turn_perspective() or ("w" in self.get_fen_position()) else -1
)
# If the user wants the evaluation specified relative to who is to move, this will be done.
# Otherwise, the evaluation will be in terms of white's side (positive meaning advantage white,
# negative meaning advantage black).
self._put(f"position {fen_position}")
self._go()
evaluation = dict()
while True:
text = self._read_line()
splitted_text = text.split(" ")
Expand All @@ -706,6 +741,35 @@ def get_evaluation(self) -> dict:
elif splitted_text[0] == "bestmove":
return evaluation

def get_static_eval(self) -> Optional[float]:
"""Sends the 'eval' command to stockfish to get the static evaluation. The current position is
'directly' evaluated -- i.e., no search is involved.
Returns:
A float representing the static eval, unless one side is in check or checkmated,
in which case None is returned.
"""

# Stockfish gives the static eval from white's perspective:
compare = (
1
if not self.get_turn_perspective() or ("w" in self.get_fen_position())
else -1
)
self._put("eval")
while True:
text = self._read_line()
if text.startswith("Final evaluation") or text.startswith(
"Total Evaluation"
):
splitted_text = text.split()
eval = splitted_text[2]
if eval == "none":
assert "(in check)" in text
return None
else:
return float(eval) * compare

def get_top_moves(
self,
num_top_moves: int = 5,
Expand Down Expand Up @@ -742,12 +806,17 @@ def get_top_moves(
"""
if num_top_moves <= 0:
raise ValueError("num_top_moves is not a positive number.")
# to get number of top moves, we use Stockfish's MultiPV option (ie. multiple principal variations)
if self._on_weaker_setting():
self._weaker_setting_warning(
"""Note that even though you've set Stockfish to play on a weaker elo or skill level,"""
+ """ get_top_moves will still return the top moves of full strength Stockfish."""
)

# remember global values
old_multipv = self._parameters["MultiPV"]
old_num_nodes = self._num_nodes

# to get number of top moves, we use Stockfish's MultiPV option (i.e., multiple principal variations).
# set MultiPV to num_top_moves requested
if num_top_moves != self._parameters["MultiPV"]:
self._set_option("MultiPV", num_top_moves)
Expand Down
62 changes: 62 additions & 0 deletions tests/stockfish/test_models.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import pytest
from timeit import default_timer
import time
import warnings

from stockfish import Stockfish, StockfishException

Expand Down Expand Up @@ -220,18 +221,21 @@ def test_set_skill_level(self, stockfish):
)
assert stockfish.get_engine_parameters()["Skill Level"] == 1
assert stockfish.get_engine_parameters()["UCI_LimitStrength"] == 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 not stockfish._on_weaker_setting()

def test_set_elo_rating(self, stockfish):
stockfish.set_fen_position(
"rnbqkbnr/ppp2ppp/3pp3/8/4P3/5N2/PPPP1PPP/RNBQKB1R w KQkq - 0 1"
)

assert stockfish.get_engine_parameters()["UCI_Elo"] == 1350
assert not stockfish._on_weaker_setting()

stockfish.set_elo_rating(2000)
assert stockfish.get_best_move() in (
Expand All @@ -247,6 +251,7 @@ def test_set_elo_rating(self, stockfish):
)
assert stockfish.get_engine_parameters()["UCI_Elo"] == 2000
assert stockfish.get_engine_parameters()["UCI_LimitStrength"] == True
assert stockfish._on_weaker_setting()

stockfish.set_elo_rating(1350)
assert stockfish.get_best_move() in (
Expand All @@ -263,6 +268,7 @@ def test_set_elo_rating(self, stockfish):
)
assert stockfish.get_engine_parameters()["UCI_Elo"] == 1350
assert stockfish.get_engine_parameters()["UCI_LimitStrength"] == True
assert stockfish._on_weaker_setting()

stockfish.set_elo_rating(2850)
major_version = stockfish.get_stockfish_major_version()
Expand All @@ -274,6 +280,27 @@ def test_set_elo_rating(self, stockfish):
assert stockfish.get_best_move() in expected_best_moves

assert stockfish.get_engine_parameters()["UCI_Elo"] == 2850
assert stockfish._on_weaker_setting()

@pytest.mark.slow
def test_resume_full_strength(self, stockfish):
stockfish.set_fen_position(
"1r1qrbk1/2pb1pp1/p4n1p/P3P3/3P4/NB4BP/6P1/R2QR1K1 b - - 0 1"
)
stockfish.set_depth(13)
stockfish.set_elo_rating(1350)
assert stockfish._on_weaker_setting()
best_moves = ["d7c6", "d7f5"]
low_elo_moves = [stockfish.get_best_move() for _ in range(15)]
assert not all(x in best_moves for x in low_elo_moves)
stockfish.set_skill_level(1)
assert stockfish._on_weaker_setting()
low_skill_level_moves = [stockfish.get_best_move() for _ in range(15)]
assert not all(x in best_moves for x in low_skill_level_moves)
stockfish.resume_full_strength()
assert not stockfish._on_weaker_setting()
full_strength_moves = [stockfish.get_best_move() for _ in range(15)]
assert all(x in best_moves for x in full_strength_moves)

def test_specific_params(self, stockfish):
old_parameters = {
Expand Down Expand Up @@ -483,6 +510,7 @@ def test_get_stockfish_major_version(self, stockfish):
stockfish.get_stockfish_major_version() in (8, 9, 10, 11, 12, 13, 14, 15)
) != stockfish.is_development_build_of_engine()

@pytest.mark.slow
def test_get_evaluation_cp(self, stockfish):
stockfish.set_depth(20)
stockfish.set_fen_position(
Expand All @@ -494,6 +522,14 @@ def test_get_evaluation_cp(self, stockfish):
and evaluation["value"] >= 60
and evaluation["value"] <= 150
)
stockfish.set_skill_level(1)
with pytest.warns(UserWarning):
evaluation = stockfish.get_evaluation()
assert (
evaluation["type"] == "cp"
and evaluation["value"] >= 60
and evaluation["value"] <= 150
)

def test_get_evaluation_checkmate(self, stockfish):
stockfish.set_fen_position("1nb1k1n1/pppppppp/8/6r1/5bqK/6r1/8/8 w - - 2 2")
Expand All @@ -505,6 +541,21 @@ def test_get_evaluation_stalemate(self, stockfish):
stockfish.set_turn_perspective(not stockfish.get_turn_perspective())
assert stockfish.get_evaluation() == {"type": "cp", "value": 0}

def test_get_static_eval(self, stockfish):
stockfish.set_turn_perspective(False)
stockfish.set_fen_position("r7/8/8/8/8/5k2/4p3/4K3 w - - 0 1")
assert stockfish.get_static_eval() < -3
assert isinstance(stockfish.get_static_eval(), float)
stockfish.set_fen_position("r7/8/8/8/8/5k2/4p3/4K3 b - - 0 1")
assert stockfish.get_static_eval() < -3
stockfish.set_turn_perspective()
assert stockfish.get_static_eval() > 3
stockfish.set_fen_position("r7/8/8/8/8/5k2/4p3/4K3 w - - 0 1")
assert stockfish.get_static_eval() < -3
if stockfish.get_stockfish_major_version() >= 12:
stockfish.set_fen_position("8/8/8/8/8/4k3/4p3/r3K3 w - - 0 1")
assert stockfish.get_static_eval() is None

def test_set_depth(self, stockfish):
stockfish.set_depth(12)
assert stockfish._depth == 12
Expand Down Expand Up @@ -656,6 +707,13 @@ def test_get_top_moves(self, stockfish):
{"Move": "g1f1", "Centipawn": None, "Mate": -2},
{"Move": "g1h1", "Centipawn": None, "Mate": -1},
]
stockfish.set_elo_rating()
with pytest.warns(UserWarning):
top_moves = stockfish.get_top_moves(2)
assert top_moves == [
{"Move": "g1f1", "Centipawn": None, "Mate": -2},
{"Move": "g1h1", "Centipawn": None, "Mate": -1},
]

def test_get_top_moves_mate(self, stockfish):
stockfish.set_depth(10)
Expand Down Expand Up @@ -830,6 +888,10 @@ def test_get_wdl_stats(self, stockfish):

stockfish.set_fen_position("8/8/8/8/8/3k4/3p4/3K4 w - - 0 1")
assert stockfish.get_wdl_stats() is None

stockfish.set_skill_level(1)
with pytest.warns(UserWarning):
stockfish.get_wdl_stats()
else:
with pytest.raises(RuntimeError):
stockfish.get_wdl_stats()
Expand Down

0 comments on commit 417e57d

Please sign in to comment.