diff --git a/pyproject.toml b/pyproject.toml index c9b8753..34dba65 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -2,7 +2,7 @@ [tool.poetry] name = "codenames-solvers" -version = "1.8.0" +version = "1.8.1" description = "Solvers implementation for Codenames board game in python." authors = ["Michael Kali ", "Asaf Kali "] readme = "README.md" diff --git a/solvers/naive/naive_duet.py b/solvers/naive/naive_duet.py new file mode 100644 index 0000000..8f5e87e --- /dev/null +++ b/solvers/naive/naive_duet.py @@ -0,0 +1,30 @@ +from codenames.duet.player import DuetPlayer, DuetTeam +from codenames.generic.board import Board +from codenames.generic.move import Clue, GivenClue, GivenGuess, Guess +from codenames.generic.player import Operative, Spymaster +from codenames.generic.state import OperativeState, SpymasterState + + +class UnifiedDuetPlayer(DuetPlayer): + def __init__(self, name: str, spymaster: Spymaster, operative: Operative): + super().__init__(name, team=DuetTeam.MAIN) + self.spymaster = spymaster + self.operative = operative + + def give_clue(self, game_state: SpymasterState) -> Clue: + return self.spymaster.give_clue(game_state) + + def guess(self, game_state: OperativeState) -> Guess: + return self.operative.guess(game_state) + + def on_game_start(self, board: Board): + self.spymaster.on_game_start(board) + self.operative.on_game_start(board) + + def on_clue_given(self, given_clue: GivenClue): + self.spymaster.on_clue_given(given_clue) + self.operative.on_clue_given(given_clue) + + def on_guess_given(self, given_guess: GivenGuess): + self.spymaster.on_guess_given(given_guess) + self.operative.on_guess_given(given_guess) diff --git a/solvers/naive/proposal_generator.py b/solvers/naive/proposal_generator.py index b8036fa..069c037 100644 --- a/solvers/naive/proposal_generator.py +++ b/solvers/naive/proposal_generator.py @@ -123,9 +123,9 @@ def neutral_indices(self) -> np.ndarray: return self.board_data.color == self.proposal_colors.neutral # type: ignore @property - def opponent_indices(self) -> np.ndarray: + def opponent_indices(self) -> np.ndarray | None: if not self.proposal_colors.opponent: - return np.array([]) + return None return self.board_data.color == self.proposal_colors.opponent # type: ignore @property @@ -214,7 +214,7 @@ def proposal_from_similarity( board_distances: np.ndarray = cosine_distance(clue_vector, self.board_vectors) # type: ignore clue_to_group = board_distances[group_indices] clue_to_neutral = board_distances[self.neutral_indices] - clue_to_opponent = board_distances[self.opponent_indices] + clue_to_opponent = board_distances[self.opponent_indices] if self.opponent_indices is not None else None clue_to_assassin = board_distances[self.assassin_indices] proposal = Proposal( word_group=word_group, @@ -222,7 +222,7 @@ def proposal_from_similarity( clue_word_frequency=self.get_word_frequency(clue), distance_group=np.max(clue_to_group), distance_neutral=np.min(clue_to_neutral) if clue_to_neutral.size > 0 else 0, - distance_opponent=np.min(clue_to_opponent) if clue_to_opponent.size > 0 else 0, + distance_opponent=np.min(clue_to_opponent) if clue_to_opponent is not None else 1, distance_assassin=np.min(clue_to_assassin), board_distances=self._get_board_distances_dict(board_distances), ) diff --git a/tests/conftest.py b/tests/conftest.py index e6e480b..c6f486d 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,7 +1,8 @@ import pytest from codenames.classic.board import ClassicBoard +from codenames.duet.board import DuetBoard -BOARD_DATA = { +BOARD_DATA_CLASSIC = { "language": "english", "cards": [ {"word": "park", "color": "RED", "revealed": False}, @@ -32,7 +33,68 @@ ], } +BOARD_DATA_DUET = { + "language": "english", + "cards": [ + {"word": "queen", "color": "GREEN", "revealed": False}, + {"word": "violet", "color": "GREEN", "revealed": False}, + {"word": "king", "color": "NEUTRAL", "revealed": False}, + {"word": "moon", "color": "GREEN", "revealed": False}, + {"word": "gymnast", "color": "GREEN", "revealed": False}, + {"word": "paper", "color": "GREEN", "revealed": False}, + {"word": "jupiter", "color": "NEUTRAL", "revealed": False}, + {"word": "lock", "color": "NEUTRAL", "revealed": False}, + {"word": "mail", "color": "NEUTRAL", "revealed": False}, + {"word": "london", "color": "NEUTRAL", "revealed": False}, + {"word": "earth", "color": "NEUTRAL", "revealed": False}, + {"word": "avalanche", "color": "NEUTRAL", "revealed": False}, + {"word": "park", "color": "GREEN", "revealed": False}, + {"word": "flood", "color": "GREEN", "revealed": False}, + {"word": "teenage", "color": "GREEN", "revealed": False}, + {"word": "high-school", "color": "GREEN", "revealed": False}, + {"word": "newton", "color": "NEUTRAL", "revealed": False}, + {"word": "ninja", "color": "NEUTRAL", "revealed": False}, + {"word": "drill", "color": "ASSASSIN", "revealed": False}, + {"word": "spiderman", "color": "ASSASSIN", "revealed": False}, + {"word": "parrot", "color": "NEUTRAL", "revealed": False}, + {"word": "tomato", "color": "NEUTRAL", "revealed": False}, + {"word": "kiss", "color": "ASSASSIN", "revealed": False}, + {"word": "egypt", "color": "NEUTRAL", "revealed": False}, + {"word": "ski", "color": "NEUTRAL", "revealed": False}, + ], +} + +BOARD_DATA_DUET_SMALL = { + "language": "english", + "cards": [ + {"word": "queen", "color": "GREEN", "revealed": False}, + {"word": "violet", "color": "GREEN", "revealed": False}, + {"word": "king", "color": "NEUTRAL", "revealed": False}, + {"word": "moon", "color": "GREEN", "revealed": False}, + {"word": "gymnast", "color": "NEUTRAL", "revealed": False}, + {"word": "spiderman", "color": "ASSASSIN", "revealed": False}, + {"word": "parrot", "color": "NEUTRAL", "revealed": False}, + {"word": "tomato", "color": "NEUTRAL", "revealed": False}, + {"word": "kiss", "color": "ASSASSIN", "revealed": False}, + {"word": "egypt", "color": "GREEN", "revealed": False}, + {"word": "ski", "color": "GREEN", "revealed": False}, + {"word": "avalanche", "color": "GREEN", "revealed": False}, + {"word": "park", "color": "NEUTRAL", "revealed": False}, + {"word": "flood", "color": "NEUTRAL", "revealed": False}, + ], +} + + +@pytest.fixture +def classic_board() -> ClassicBoard: + return ClassicBoard.model_validate(BOARD_DATA_CLASSIC) + + +@pytest.fixture +def duet_board() -> DuetBoard: + return DuetBoard.model_validate(BOARD_DATA_DUET) + @pytest.fixture -def english_board() -> ClassicBoard: - return ClassicBoard.model_validate(BOARD_DATA) +def duet_board_small() -> DuetBoard: + return DuetBoard.model_validate(BOARD_DATA_DUET_SMALL) diff --git a/tests/test_cli_players.py b/tests/test_cli_players.py index 8fe3529..05d8d24 100644 --- a/tests/test_cli_players.py +++ b/tests/test_cli_players.py @@ -10,7 +10,7 @@ @patch("builtins.input") -def test_cli_players_game(mock_input, english_board: ClassicBoard): +def test_cli_players_game(mock_input, classic_board: ClassicBoard): blue_spymaster = CLISpymaster("Leonardo", team=ClassicTeam.BLUE) blue_operative = CLIOperative("Bard", team=ClassicTeam.BLUE) red_spymaster = CLISpymaster("Adam", team=ClassicTeam.RED) @@ -30,7 +30,7 @@ def test_cli_players_game(mock_input, english_board: ClassicBoard): "Fail, 2", # Clue "teenage", # Assassin ] - runner = ClassicGameRunner(players=players, board=english_board) + runner = ClassicGameRunner(players=players, board=classic_board) winner = runner.run_game() assert winner.team == ClassicTeam.RED assert winner.reason == WinningReason.OPPONENT_HIT_ASSASSIN diff --git a/tests/test_naive_flow_classical.py b/tests/test_naive_flow.py similarity index 51% rename from tests/test_naive_flow_classical.py rename to tests/test_naive_flow.py index 785129c..a0a4490 100644 --- a/tests/test_naive_flow_classical.py +++ b/tests/test_naive_flow.py @@ -5,8 +5,13 @@ from codenames.classic.board import ClassicBoard from codenames.classic.color import ClassicTeam from codenames.classic.runner import ClassicGamePlayers, ClassicGameRunner +from codenames.duet.board import DuetBoard +from codenames.duet.runner import DuetGamePlayers, DuetGameRunner +from codenames.duet.score import TARGET_REACHED +from codenames.duet.state import DuetGameState from gensim.models import KeyedVectors +from solvers.naive.naive_duet import UnifiedDuetPlayer from solvers.naive.naive_operative import NaiveOperative from solvers.naive.naive_spymaster import NaiveSpymaster from tests.resources.resource_manager import get_resource_path @@ -24,14 +29,31 @@ def mock_load_word2vec_format(*args, **kwargs): @pytest.mark.slow @mock.patch("gensim.models.KeyedVectors.load", new=mock_load_word2vec_format) -def test_complete_naive_flow(english_board: ClassicBoard): +def test_complete_naive_flow_classic(classic_board: ClassicBoard): blue_spymaster = NaiveSpymaster("Leonardo", team=ClassicTeam.BLUE) blue_operative = NaiveOperative("Bard", team=ClassicTeam.BLUE) red_spymaster = NaiveSpymaster("Adam", team=ClassicTeam.RED) red_operative = NaiveOperative("Eve", team=ClassicTeam.RED) players = ClassicGamePlayers.from_collection(blue_spymaster, blue_operative, red_spymaster, red_operative) - runner = ClassicGameRunner(players, board=english_board) + runner = ClassicGameRunner(players, board=classic_board) runner.run_game() assert runner.state.winner is not None + + +@pytest.mark.slow +@mock.patch("gensim.models.KeyedVectors.load", new=mock_load_word2vec_format) +def test_complete_naive_flow_duet(duet_board_small: DuetBoard): + dual_board = DuetBoard.dual_board(duet_board_small, seed=0) + spymaster = NaiveSpymaster(name="", team=ClassicTeam.BLUE) + operative = NaiveOperative(name="", team=ClassicTeam.BLUE) + player_a = UnifiedDuetPlayer(name="Alice", spymaster=spymaster, operative=operative) + player_b = UnifiedDuetPlayer(name="Bob", spymaster=spymaster, operative=operative) + + game_state = DuetGameState.from_boards(board_a=duet_board_small, board_b=dual_board) + players = DuetGamePlayers(player_a=player_a, player_b=player_b) + runner = DuetGameRunner(players=players, state=game_state) + runner.run_game() + + assert runner.state.game_result == TARGET_REACHED