Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion lib/lichess.py
Original file line number Diff line number Diff line change
Expand Up @@ -405,7 +405,7 @@ def get_ongoing_games(self) -> list[GameType] | None:
"""
Get the bot's ongoing games.

If an error occurs when retreiving the games, None is returned.
If an error occurs when retrieving the games, None is returned.
"""
with contextlib.suppress(Exception):
response = cast(dict[str, list[GameType]], self.api_get_json("playing"))
Expand Down
16 changes: 8 additions & 8 deletions lib/matchmaking.py
Original file line number Diff line number Diff line change
Expand Up @@ -157,16 +157,16 @@ def choose_opponent(self) -> tuple[str | None, int, int, int, str, str]:

base_time = random.choice(match_config.challenge_initial_time)
increment = random.choice(match_config.challenge_increment)
days = random.choice(match_config.challenge_days)
num_days = random.choice(match_config.challenge_days)

play_correspondence = [bool(days), not bool(base_time or increment)]
play_correspondence = [bool(num_days), not bool(base_time or increment)]
if random.choice(play_correspondence):
base_time = 0
increment = 0
else:
days = 0
num_days = 0

game_type = game_category(variant, base_time, increment, days)
game_type = game_category(variant, base_time, increment, num_days)

min_rating = match_config.opponent_min_rating
max_rating = match_config.opponent_max_rating
Expand Down Expand Up @@ -210,7 +210,7 @@ def ready_for_challenge(bot: UserProfileType) -> bool:
else:
logger.error("No suitable bots found to challenge.")

return bot_username, base_time, increment, days, variant, mode
return bot_username, base_time, increment, num_days, variant, mode

def get_random_config_value(self, config: Configuration, parameter: str, choices: list[str]) -> str:
"""Choose a random value from `choices` if the parameter value in the config is `random`."""
Expand Down Expand Up @@ -337,20 +337,20 @@ def declined_challenge(self, event: EventType) -> None:
self.show_earliest_challenge_time()


def game_category(variant: str, base_time: int, increment: int, days: int) -> str:
def game_category(variant: str, base_time: int, increment: int, num_days: int) -> str:
"""
Get the game type (e.g. bullet, atomic, classical). Lichess has one rating for every variant regardless of time control.

:param variant: The game's variant.
:param base_time: The base time in seconds.
:param increment: The increment in seconds.
:param days: If the game is correspondence, we have some days to play the move.
:param num_days: If the game is correspondence, we have some days to play the move.
:return: The game category.
"""
game_duration = base_time + increment * 40
if variant != "standard":
return variant
if days:
if num_days:
return "correspondence"
if game_duration < 179:
return "bullet"
Expand Down
2 changes: 1 addition & 1 deletion lib/timer.py
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ class Timer:
A timer for use in lichess-bot. An instance of timer can be used both as a countdown timer and a stopwatch.

If the duration argument in the __init__() method is greater than zero, then
the method is_expired() indicates when the intial duration has passed. The
the method is_expired() indicates when the initial duration has passed. The
method time_until_expiration() gives the amount of time left until the timer
expires.

Expand Down
167 changes: 167 additions & 0 deletions test_bot/test_matchmaking.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
"""Test functions for matchmaking module."""
from lib.matchmaking import game_category


def test_game_category_standard_bullet() -> None:
"""Test bullet time control with config values."""
# challenge_initial_time: 60 (1 min), challenge_increment: 1
# 60 + 1*40 = 100 seconds < 179 = bullet
assert game_category("standard", 60, 1, 0) == "bullet"

# challenge_initial_time: 60, challenge_increment: 2
# 60 + 2*40 = 140 seconds < 179 = bullet
assert game_category("standard", 60, 2, 0) == "bullet"


def test_game_category_standard_blitz() -> None:
"""Test blitz time control with config values."""
# challenge_initial_time: 180 (3 min), challenge_increment: 1
# 180 + 1*40 = 220 seconds, 179 <= 220 < 479 = blitz
assert game_category("standard", 180, 1, 0) == "blitz"

# challenge_initial_time: 180, challenge_increment: 2
# 180 + 2*40 = 260 seconds, 179 <= 260 < 479 = blitz
assert game_category("standard", 180, 2, 0) == "blitz"


def test_game_category_standard_rapid() -> None:
"""Test rapid time control."""
# 10 minutes + 5 seconds increment
# 600 + 5*40 = 800 seconds, 479 <= 800 < 1499 = rapid
assert game_category("standard", 600, 5, 0) == "rapid"

# 15 minutes no increment
# 900 + 0*40 = 900 seconds, 479 <= 900 < 1499 = rapid
assert game_category("standard", 900, 0, 0) == "rapid"


def test_game_category_standard_classical() -> None:
"""Test classical time control with max config values."""
# max_base: 1800 (30 min), max_increment: 20
# 1800 + 20*40 = 2600 seconds >= 1499 = classical
assert game_category("standard", 1800, 20, 0) == "classical"

# 25 minutes no increment
# 1500 + 0*40 = 1500 seconds >= 1499 = classical
assert game_category("standard", 1500, 0, 0) == "classical"


def test_game_category_correspondence() -> None:
"""Test correspondence games with config values."""
# min_days: 1
assert game_category("standard", 0, 0, 1) == "correspondence"

# challenge_days: 2
assert game_category("standard", 0, 0, 2) == "correspondence"

# max_days: 14
assert game_category("standard", 0, 0, 14) == "correspondence"


def test_game_category_variants() -> None:
"""Test chess variants from config."""
assert game_category("atomic", 60, 1, 0) == "atomic"
assert game_category("chess960", 180, 2, 0) == "chess960"
assert game_category("crazyhouse", 600, 5, 0) == "crazyhouse"
assert game_category("horde", 60, 0, 0) == "horde"
assert game_category("kingOfTheHill", 180, 1, 0) == "kingOfTheHill"
assert game_category("racingKings", 600, 0, 0) == "racingKings"
assert game_category("threeCheck", 60, 1, 0) == "threeCheck"
assert game_category("antichess", 180, 2, 0) == "antichess"


def test_game_category_time_boundaries() -> None:
"""Test edge cases at time control boundaries."""
# Exactly at bullet/blitz boundary
# 179 seconds should be blitz (179 < 179 is False)
assert game_category("standard", 179, 0, 0) == "blitz"

# Just below boundary
assert game_category("standard", 178, 0, 0) == "bullet"

# Exactly at blitz/rapid boundary
assert game_category("standard", 479, 0, 0) == "rapid"

# Just below
assert game_category("standard", 478, 0, 0) == "blitz"

# Exactly at rapid/classical boundary
assert game_category("standard", 1499, 0, 0) == "classical"

# Just below
assert game_category("standard", 1498, 0, 0) == "rapid"


def test_game_category_min_config_values() -> None:
"""Test minimum config values."""
# min_base: 0, min_increment: 0
# This is an edge case: 0 + 0*40 = 0 < 179 = bullet
assert game_category("standard", 0, 0, 0) == "bullet"

# min_base: 0, min_increment: 0, min_days: 1
assert game_category("standard", 0, 0, 1) == "correspondence"


def test_game_category_correspondence_overrides_time() -> None:
"""Test that correspondence takes precedence over time controls."""
# If both days and time controls are set, days takes precedence
assert game_category("standard", 1800, 20, 1) == "correspondence"
assert game_category("standard", 60, 1, 2) == "correspondence"


def test_game_category_variant_overrides_time() -> None:
"""Test that variants override time control categorization."""
# Variants are returned regardless of time control
# Even if time would be "classical", variant name is returned
assert game_category("atomic", 1800, 20, 0) == "atomic"
assert game_category("horde", 60, 1, 0) == "horde"

# Variants override correspondence too
assert game_category("chess960", 0, 0, 14) == "chess960"


def test_game_category_negative_values() -> None:
"""Test edge case with negative values (should not happen in practice)."""
# Negative base time
assert game_category("standard", -100, 5, 0) == "bullet"

# Negative increment results in negative duration
result = game_category("standard", 100, -10, 0)
# 100 + (-10)*40 = -300, which is < 179, so bullet
assert result == "bullet"


def test_game_category_realistic_scenarios() -> None:
"""Test realistic game scenarios from actual lichess games."""
# 1+0 bullet
assert game_category("standard", 60, 0, 0) == "bullet"

# 2+1 bullet
assert game_category("standard", 120, 1, 0) == "bullet"

# 3+0 blitz
assert game_category("standard", 180, 0, 0) == "blitz"

# 3+2 blitz
assert game_category("standard", 180, 2, 0) == "blitz"

# 5+0 blitz
assert game_category("standard", 300, 0, 0) == "blitz"

# 5+3 blitz
assert game_category("standard", 300, 3, 0) == "blitz"

# 10+0 rapid
assert game_category("standard", 600, 0, 0) == "rapid"

# 15+5 rapid
assert game_category("standard", 900, 5, 0) == "rapid"

# 15+10 rapid
assert game_category("standard", 900, 10, 0) == "rapid"

# 30+0 classical
assert game_category("standard", 1800, 0, 0) == "classical"

# 30+20 classical
assert game_category("standard", 1800, 20, 0) == "classical"
Loading