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 Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,7 @@ semrel-dev:
format:
black .
isort .
ruff check --fix
ruff check --fix --unsafe-fixes

check-ruff:
ruff check
Expand Down
14 changes: 10 additions & 4 deletions codenames/classic/runner.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,10 @@ def get_player(self, team: ClassicTeam, role: PlayerRole) -> Player:

class ClassicGameRunner:
def __init__(
self, players: ClassicGamePlayers, state: ClassicGameState | None = None, board: ClassicBoard | None = None
self,
players: ClassicGamePlayers,
state: ClassicGameState | None = None,
board: ClassicBoard | None = None,
):
self.players = players
if not state:
Expand Down Expand Up @@ -153,9 +156,12 @@ def find_team(players: Collection[Player], team: ClassicTeam) -> TeamPlayers:
elif isinstance(player, Operative):
operative = player
else:
raise ValueError(f"Player {player} is not a Spymaster or Operative")
msg = f"Player {player} is not a Spymaster or Operative"
raise ValueError(msg)
if spymaster is None:
raise ValueError(f"No Spymaster found for team {team}")
msg = f"No Spymaster found for team {team}"
raise ValueError(msg)
if operative is None:
raise ValueError(f"No Operative found for team {team}")
msg = f"No Operative found for team {team}"
raise ValueError(msg)
return TeamPlayers(spymaster=spymaster, operative=operative)
4 changes: 1 addition & 3 deletions codenames/classic/score.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,4 @@ def new(blue: int, red: int) -> Score:
def add_point(self, team: ClassicTeam) -> bool:
team_score = self.blue if team == ClassicTeam.BLUE else self.red
team_score.revealed += 1
if team_score.unrevealed == 0:
return True
return False
return team_score.unrevealed == 0
4 changes: 2 additions & 2 deletions codenames/classic/state.py
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,7 @@ def is_game_over(self) -> bool:

def process_clue(self, clue: Clue) -> ClassicGivenClue | None:
if self.is_game_over:
raise GameIsOver()
raise GameIsOver
if self.current_player_role != PlayerRole.SPYMASTER:
raise InvalidTurn("It's not the Spymaster's turn now!")
self.clues.append(clue)
Expand All @@ -141,7 +141,7 @@ def process_clue(self, clue: Clue) -> ClassicGivenClue | None:

def process_guess(self, guess: Guess) -> ClassicGivenGuess | None:
if self.is_game_over:
raise GameIsOver()
raise GameIsOver
if self.current_player_role != PlayerRole.OPERATIVE:
raise InvalidTurn("It's not the Operative's turn now!")
if guess.card_index == PASS_GUESS:
Expand Down
9 changes: 5 additions & 4 deletions codenames/duet/state.py
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,7 @@ def operative_state(self) -> DuetOperativeState:

def process_clue(self, clue: Clue) -> DuetGivenClue | None:
if self.is_game_over:
raise GameIsOver()
raise GameIsOver
if self.current_player_role != PlayerRole.SPYMASTER:
raise InvalidTurn("It's not the Spymaster's turn now!")
self.clues.append(clue)
Expand All @@ -124,7 +124,7 @@ def process_clue(self, clue: Clue) -> DuetGivenClue | None:

def process_guess(self, guess: Guess) -> DuetGivenGuess | None:
if self.is_game_over:
raise GameIsOver()
raise GameIsOver
if self.current_player_role != PlayerRole.OPERATIVE:
raise InvalidTurn("It's not the Operative's turn now!")
if guess.card_index == PASS_GUESS:
Expand All @@ -149,7 +149,7 @@ def process_guess(self, guess: Guess) -> DuetGivenGuess | None:
def dual_card_revealed(self, guess: Guess):
card = self.board[guess.card_index]
if card.revealed:
assert not card.color == DuetColor.GREEN # This should not happen
assert card.color != DuetColor.GREEN # This should not happen
return
if card.color == DuetColor.GREEN:
self._update_score(card_color=DuetColor.GREEN)
Expand Down Expand Up @@ -260,7 +260,8 @@ def game_result(self) -> GameResult | None:
if not result_a or not result_b:
return None
# Otherwise, both sides, finished, no one lost, means the game is won
assert result_a.win and result_b.win
assert result_a.win
assert result_b.win
return TARGET_REACHED

@property
Expand Down
10 changes: 6 additions & 4 deletions codenames/generic/board.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
import abc
import math
from dataclasses import dataclass
from typing import TYPE_CHECKING, Any, Collection, Iterator, Union
from typing import TYPE_CHECKING, Any, Collection, Iterator

from pydantic import BaseModel, field_validator

Expand All @@ -27,13 +27,15 @@ class Board[C: CardColor](BaseModel, abc.ABC):
def convert_cards(cls, v: Any) -> list[Card[C]]:
return list(v)

def __getitem__(self, item: Union[int, str]) -> Card:
def __getitem__(self, item: int | str) -> Card:
if isinstance(item, str):
item = self.find_card_index(item)
if not isinstance(item, int):
raise IndexError(f"Illegal index type for card: {item}")
msg = f"Illegal index type for card: {item}"
raise IndexError(msg)
if item < 0 or item >= self.size:
raise IndexError(f"Card index out of bounds: {item}")
msg = f"Card index out of bounds: {item}"
raise IndexError(msg)
return self.cards[item]

def __iter__(self) -> Iterator[Card[C]]: # type: ignore
Expand Down
3 changes: 2 additions & 1 deletion codenames/generic/move.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,8 @@ def __str__(self) -> str:
def correct(self) -> bool:
card_color = self.guessed_card.color
if not card_color:
raise ValueError(f"Card {self.guessed_card} has no color set")
msg = f"Card {self.guessed_card} has no color set"
raise ValueError(msg)
return self.for_clue.team.as_card_color == card_color

@property
Expand Down
2 changes: 1 addition & 1 deletion codenames/generic/player.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,4 +61,4 @@ def give_clue(self, game_state: S) -> Clue:
class Operative[C: CardColor, T: Team, S: OperativeState](Player[C, T], abc.ABC):
@abc.abstractmethod
def guess(self, game_state: S) -> Guess:
raise NotImplementedError()
raise NotImplementedError
14 changes: 6 additions & 8 deletions codenames/mini/state.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,16 +19,14 @@ def is_sudden_death(self) -> bool:

def process_guess(self, guess: Guess) -> DuetGivenGuess | None:
given_guess = super().process_guess(guess)
# If the guess is wrong or passed the turn, the timer is updated
if not given_guess or not given_guess.correct:
self._update_tokens(mistake=given_guess is not None)
# If the guess is correct, there is nothing to do
if given_guess and given_guess.correct:
return given_guess
# If we reached our target score, and we are not in "sudden death", we consume a timer token
if self.is_game_over and not self.is_sudden_death:
self._update_tokens(mistake=False)
# Otherwise, the guess was incorrect or the operator passed the turn
self._update_tokens(is_mistake=given_guess is not None)
return given_guess

def _update_tokens(self, mistake: bool) -> None:
def _update_tokens(self, is_mistake: bool) -> None:
if self.timer_tokens >= 0:
self.timer_tokens -= 1
if self.timer_tokens == 0:
Expand All @@ -37,7 +35,7 @@ def _update_tokens(self, mistake: bool) -> None:
elif self.timer_tokens < 0:
self.game_result = TIMER_TOKENS_DEPLETED
log.info("Timer tokens depleted (after sudden death)!")
if not mistake:
if not is_mistake:
return
self.allowed_mistakes -= 1
if self.allowed_mistakes == 0:
Expand Down
13 changes: 8 additions & 5 deletions codenames/online/codenames_game/adapter.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,12 @@
from dataclasses import dataclass
from enum import StrEnum
from time import sleep
from typing import Callable, Mapping
from typing import TYPE_CHECKING, Callable, Mapping

from selenium import webdriver
from selenium.common import ElementNotInteractableException
from selenium.webdriver.chrome.service import Service
from selenium.webdriver.common.by import By
from selenium.webdriver.remote.webelement import WebElement

from codenames.classic.board import ClassicBoard
from codenames.generic.move import PASS_GUESS, Clue, Guess
Expand All @@ -28,6 +27,9 @@
)
from codenames.utils.formatting import wrap

if TYPE_CHECKING:
from selenium.webdriver.remote.webelement import WebElement

log = logging.getLogger(__name__)

WEBAPP_URL = "https://codenames.game/"
Expand Down Expand Up @@ -279,7 +281,8 @@ def get_card_containers(self) -> list[WebElement]:
card_elements = self.driver.find_elements(by=By.CLASS_NAME, value="card")
card_elements = [element for element in card_elements if element.text != ""]
if len(card_elements) < 25:
raise ValueError(f"Expected 25 cards, loaded {len(card_elements)}")
msg = f"Expected 25 cards, loaded {len(card_elements)}"
raise ValueError(msg)
return self.driver.find_elements(By.XPATH, value="//div[@role='img']")

def get_clue_input(self) -> WebElement:
Expand Down Expand Up @@ -343,11 +346,11 @@ def poll_elements[
log.debug(f"Polling [{len(element_getters)}] elements...")
try:
return poll_elements(element_getters, timeout_sec=timeout_sec, poll_interval_sec=poll_interval_sec)
except Exception as e:
except Exception:
if screenshot:
log.info(f"{self.log_prefix} Polling failed, saving screenshot...")
self.screenshot("failed polling")
raise e
raise

def screenshot(self, tag: str, raise_on_error: bool = False) -> str | None:
return save_screenshot(adapter=self, tag=tag, raise_on_error=raise_on_error)
Expand Down
7 changes: 3 additions & 4 deletions codenames/online/codenames_game/card_parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,10 +49,9 @@ def _parse_card_color(card_element: WebElement) -> ClassicColor:
for css_class, classic_color in CSS_CLASS_TO_CLASSIC_COLOR.items():
if css_class.lower() in element_classes:
return classic_color
raise ValueError(f"Could not parse card color from element classes: {element_classes}")
msg = f"Could not parse card color from element classes: {element_classes}"
raise ValueError(msg)


def _is_card_revealed(card_container: WebElement) -> bool: # pylint: disable=unused-argument
if "revealed" in card_container.accessible_name:
return True
return False
return "revealed" in card_container.accessible_name
9 changes: 5 additions & 4 deletions codenames/online/codenames_game/runner.py
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@ def all_players_screenshots(self, tag: str):
try:
adapter.screenshot(tag=tag)
except Exception as e: # pylint: disable=broad-except
log.error(f"Error taking screenshot: {e}")
log.exception(f"Error taking screenshot: {e}")

def run_game(self) -> ClassicGameRunner:
self._start_game()
Expand All @@ -103,9 +103,9 @@ def run_game(self) -> ClassicGameRunner:
game_runner.guess_given_subscribers.append(self._handle_guess_given)
try:
game_runner.run_game()
except Exception as e: # pylint: disable=broad-except
except Exception: # pylint: disable=broad-except
self.all_players_screenshots(tag="game error")
raise e
raise
self.host.screenshot("game over")
return game_runner

Expand Down Expand Up @@ -164,7 +164,8 @@ def _get_adapter_for_player(self, player: Player) -> CodenamesGamePlayerAdapter:
for adapter in self.adapters:
if adapter.player == player:
return adapter
raise ValueError(f"Player {player} not found in this game manager.")
msg = f"Player {player} not found in this game manager."
raise ValueError(msg)

def _start_game(self) -> CodenamesGameRunner:
if not self.host_connected:
Expand Down
2 changes: 1 addition & 1 deletion codenames/online/codenames_game/screenshot.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ def save_screenshot(adapter: "CodenamesGamePlayerAdapter", tag: str, raise_on_er
adapter.driver.save_screenshot(path_abs)
except Exception as e: # pylint: disable=broad-except
if raise_on_error:
raise e
raise
log.warning(f"Failed to save screenshot: {e}")
return None
log.info(f"{adapter.log_prefix} Screenshot saved to {path_abs}")
Expand Down
2 changes: 1 addition & 1 deletion codenames/online/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ def multi_click(element: WebElement, times: int = 3, warn: bool = False):

def poll_elements[
T
](element_getters: list[Callable[[], T]], timeout_sec: float = 15, poll_interval_sec: float = 0.5,) -> T:
](element_getters: list[Callable[[], T]], timeout_sec: float = 15, poll_interval_sec: float = 0.5) -> T:
def safe_getter() -> T | None:
for element_getter in element_getters:
try:
Expand Down
2 changes: 1 addition & 1 deletion codenames/utils/vocabulary/english.py
Original file line number Diff line number Diff line change
Expand Up @@ -409,5 +409,5 @@
"young",
"youth",
"zone",
}
},
)
2 changes: 1 addition & 1 deletion codenames/utils/vocabulary/hebrew.py
Original file line number Diff line number Diff line change
Expand Up @@ -424,5 +424,5 @@
"תרבות",
"תרמיל",
"תרסיס",
}
},
)
3 changes: 2 additions & 1 deletion codenames/utils/vocabulary/languages.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,4 +18,5 @@ def get_vocabulary(language: str) -> Vocabulary:
return Vocabulary(language=language, words=ENGLISH_WORDS)
if language == SupportedLanguage.HEBREW:
return Vocabulary(language=language, words=HEBREW_WORDS)
raise NotImplementedError(f"Unknown language: {language}")
msg = f"Unknown language: {language}"
raise NotImplementedError(msg)
46 changes: 44 additions & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -106,8 +106,50 @@ line-length = 120
exclude = ["local", ".deployment"]

[tool.ruff.lint]
select = ["B", "C", "E", "F", "W"]
ignore = []
select = ["ALL"]
ignore = [
"ARG",
"ANN001",
"ANN002",
"ANN003",
"ANN201",
"ANN202",
"ANN204",
"ANN401",
"BLE001",
"D",
"ERA001",
"EM101",
"FA",
"FBT001",
"FBT002",
"G004",
"N818",
"PERF203",
"PGH003",
"PGH004",
"PLR0911",
"PLR0913",
"PLR0915",
"PLR2004",
"PLW0603",
"PT011",
"PTH100",
"PTH103",
"PTH118",
"PYI063",
"RET504",
"RUF001",
"RUF012",
"S101",
"S311",
"SIM105",
"TC001",
"TRY003",
"TRY300",
"TRY401",
"UP035",
]

[tool.black]
line-length = 120
Expand Down
4 changes: 2 additions & 2 deletions tests/classic/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,11 @@
from tests.classic.utils import constants


@pytest.fixture()
@pytest.fixture
def board_10() -> ClassicBoard:
return constants.board_10()


@pytest.fixture()
@pytest.fixture
def board_25() -> ClassicBoard:
return constants.board_25()
Loading
Loading