Skip to content

Commit

Permalink
Add the option to prefer high/low rated players (#865)
Browse files Browse the repository at this point in the history
The bot can now prefer high rated players, low rated players, or have no preference at all.
  • Loading branch information
AttackingOrDefending authored Nov 20, 2023
1 parent 6f8c2c4 commit 7b5e08c
Show file tree
Hide file tree
Showing 5 changed files with 49 additions and 13 deletions.
7 changes: 6 additions & 1 deletion config.py
Original file line number Diff line number Diff line change
Expand Up @@ -230,6 +230,7 @@ def insert_default_values(CONFIG: CONFIG_DICT_TYPE) -> None:
change_value_to_list(CONFIG, "matchmaking", key="challenge_days")
set_config_default(CONFIG, "matchmaking", key="opponent_min_rating", default=600, force_empty_values=True)
set_config_default(CONFIG, "matchmaking", key="opponent_max_rating", default=4000, force_empty_values=True)
set_config_default(CONFIG, "matchmaking", key="rating_preference", default="none")
set_config_default(CONFIG, "matchmaking", key="opponent_allow_tos_violation", default=True)
set_config_default(CONFIG, "matchmaking", key="challenge_variant", default="random")
set_config_default(CONFIG, "matchmaking", key="challenge_mode", default="random")
Expand Down Expand Up @@ -311,11 +312,15 @@ def has_valid_list(name: str) -> bool:
"challenge_increment is required, or a list of challenge_days, or both.")

filter_option = "challenge_filter"
filter_type = (CONFIG.get("matchmaking") or {}).get(filter_option)
filter_type = matchmaking.get(filter_option)
config_assert(filter_type is None or filter_type in FilterType.__members__.values(),
f"{filter_type} is not a valid value for {filter_option} (formerly delay_after_decline) parameter. "
f"Choices are: {', '.join(FilterType)}.")

config_assert(matchmaking.get("rating_preference") in ["none", "high", "low"],
f"{matchmaking.get('rating_preference')} is not a valid `matchmaking:rating_preference` option. "
f"Valid options are 'none', 'high', or 'low'.")

selection_choices = {"polyglot": ["weighted_random", "uniform_random", "best_move"],
"chessdb_book": ["all", "good", "best"],
"lichess_cloud_analysis": ["good", "best"],
Expand Down
1 change: 1 addition & 0 deletions config.yml.default
Original file line number Diff line number Diff line change
Expand Up @@ -196,6 +196,7 @@ matchmaking:
# opponent_min_rating: 600 # Opponents rating should be above this value (600 is the minimum rating in lichess).
# opponent_max_rating: 4000 # Opponents rating should be below this value (4000 is the maximum rating in lichess).
opponent_rating_difference: 300 # The maximum difference in rating between the bot's rating and opponent's rating.
rating_preference: "none" # One of "none", "high", "low".
opponent_allow_tos_violation: false # Set to 'true' to allow challenging bots that violated the Lichess Terms of Service.
challenge_mode: "random" # Set it to the mode in which challenges are sent. Possible options are 'casual', 'rated' and 'random'.
challenge_filter: none # If a bot declines a challenge, do not issue a similar challenge to that bot. Possible options are 'none', 'coarse', and 'fine'.
Expand Down
22 changes: 21 additions & 1 deletion matchmaking.py
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,24 @@ def update_user_profile(self) -> None:
except Exception:
pass

def get_weights(self, online_bots: list[USER_PROFILE_TYPE], rating_preference: str, min_rating: int, max_rating: int,
game_type: str) -> list[int]:
"""Get the weight for each bot. A higher weights means the bot is more likely to get challenged."""
if rating_preference == "high":
# A bot with max_rating rating will be twice as likely to get picked than a bot with min_rating rating.
reduce_ratings_by = min(min_rating - (max_rating - min_rating), min_rating - 1)
# or, reduce_ratings_by = min(2 * min_rating - max_rating, min_rating - 1)
weights = [bot.get("perfs", {}).get(game_type, {}).get("rating", 0) - reduce_ratings_by for bot in online_bots]
elif rating_preference == "low":
# A bot with min_rating rating will be twice as likely to get picked than a bot with max_rating rating.
reduce_ratings_by = max(max_rating - (min_rating - max_rating), max_rating + 1)
# or, reduce_ratings_by = max(2 * max_rating - min_rating, max_rating + 1)
weights = [(reduce_ratings_by - bot.get("perfs", {}).get(game_type, {}).get("rating", 0))
for bot in online_bots]
else:
weights = [1] * len(online_bots)
return weights

def choose_opponent(self) -> tuple[Optional[str], int, int, int, str, str]:
"""Choose an opponent."""
override_choice = random.choice(self.matchmaking_cfg.overrides.keys() + [None])
Expand All @@ -155,6 +173,7 @@ def choose_opponent(self) -> tuple[Optional[str], int, int, int, str, str]:

variant = self.get_random_config_value(match_config, "challenge_variant", self.variants)
mode = self.get_random_config_value(match_config, "challenge_mode", ["casual", "rated"])
rating_preference = match_config.rating_preference

base_time = random.choice(match_config.challenge_initial_time)
increment = random.choice(match_config.challenge_increment)
Expand Down Expand Up @@ -198,9 +217,10 @@ def ready_for_challenge(bot: USER_PROFILE_TYPE) -> bool:
ready_bots = list(filter(ready_for_challenge, online_bots))
online_bots = ready_bots or online_bots
bot_username = None
weights = self.get_weights(online_bots, rating_preference, min_rating, max_rating, game_type)

try:
bot = random.choice(online_bots)
bot = random.choices(online_bots, weights=weights)[0]
bot_profile = self.li.get_public_data(bot["username"])
if bot_profile.get("blocking"):
self.add_to_block_list(bot["username"])
Expand Down
31 changes: 20 additions & 11 deletions test_bot/test_bot.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
import shutil
import importlib
import config
import tarfile
from timer import Timer, to_seconds, seconds
from typing import Any
if __name__ == "__main__":
Expand All @@ -30,17 +31,25 @@ def download_sf() -> None:
"""Download Stockfish 15."""
if os.path.exists(stockfish_path):
return
windows_or_linux = "win" if platform == "win32" else "linux"
base_name = f"stockfish_15_{windows_or_linux}_x64"
exec_name = "stockfish_15_x64"
zip_link = f"https://files.stockfishchess.org/files/{base_name}.zip"
response = requests.get(zip_link, allow_redirects=True)
with open("./TEMP/sf_zip.zip", "wb") as file:
file.write(response.content)
with zipfile.ZipFile("./TEMP/sf_zip.zip", "r") as zip_ref:
zip_ref.extractall("./TEMP/")
shutil.copyfile(f"./TEMP/{base_name}/{exec_name}{file_extension}", stockfish_path)
if windows_or_linux == "linux":
windows_or_linux = "windows" if platform == "win32" else "ubuntu"
if windows_or_linux == "windows":
archive_link = ("https://github.com/official-stockfish/Stockfish/releases/download/sf_16/"
"stockfish-windows-x86-64-modern.zip")
response = requests.get(archive_link, allow_redirects=True)
with open("./TEMP/sf_zip.zip", "wb") as file:
file.write(response.content)
with zipfile.ZipFile("./TEMP/sf_zip.zip", "r") as archive_ref:
archive_ref.extractall("./TEMP/")
shutil.copyfile("./TEMP/stockfish/stockfish-windows-x86-64-modern.exe", stockfish_path)
else:
archive_link = ("https://github.com/official-stockfish/Stockfish/releases/download/sf_16/"
"stockfish-ubuntu-x86-64-modern.tar")
response = requests.get(archive_link, allow_redirects=True)
with open("./TEMP/sf_zip.tar", "wb") as file:
file.write(response.content)
with tarfile.TarFile("./TEMP/sf_zip.tar", "r") as archive_ref:
archive_ref.extractall("./TEMP/")
shutil.copyfile("./TEMP/stockfish/stockfish-ubuntu-x86-64-modern", stockfish_path)
st = os.stat(stockfish_path)
os.chmod(stockfish_path, st.st_mode | stat.S_IEXEC)

Expand Down
1 change: 1 addition & 0 deletions wiki/Configure-lichess-bot.md
Original file line number Diff line number Diff line change
Expand Up @@ -231,6 +231,7 @@ will precede the `go` command to start thinking with `sd 5`. The other `go_comma
- `opponent_min_rating`: The minimum rating of the opponent bot. The minimum rating in lichess is 600.
- `opponent_max_rating`: The maximum rating of the opponent bot. The maximum rating in lichess is 4000.
- `opponent_rating_difference`: The maximum difference between the bot's rating and the opponent bot's rating.
- `rating_preference`: Whether the bot should prefer challenging high or low rated players, or have no preference.
- `opponent_allow_tos_violation`: Whether to challenge bots that violated Lichess Terms of Service. Note that even rated games against them will not affect ratings.
- `challenge_mode`: Possible options are `casual`, `rated` and `random`.
- `challenge_filter`: Whether and how to prevent challenging a bot after that bot declines a challenge. Options are `none`, `coarse`, and `fine`.
Expand Down

0 comments on commit 7b5e08c

Please sign in to comment.