Skip to content

Commit

Permalink
🌴 Naive solver: Duet fixes, add tests
Browse files Browse the repository at this point in the history
  • Loading branch information
asaf-kali committed Nov 19, 2024
1 parent b41d0d2 commit 03acda7
Show file tree
Hide file tree
Showing 6 changed files with 126 additions and 12 deletions.
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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 <[email protected]>", "Asaf Kali <[email protected]>"]
readme = "README.md"
Expand Down
30 changes: 30 additions & 0 deletions solvers/naive/naive_duet.py
Original file line number Diff line number Diff line change
@@ -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)
8 changes: 4 additions & 4 deletions solvers/naive/proposal_generator.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -214,15 +214,15 @@ 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,
clue_word=self.board_format(clue),
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),
)
Expand Down
68 changes: 65 additions & 3 deletions tests/conftest.py
Original file line number Diff line number Diff line change
@@ -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},
Expand Down Expand Up @@ -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)
4 changes: 2 additions & 2 deletions tests/test_cli_players.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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
26 changes: 24 additions & 2 deletions tests/test_naive_flow_classical.py → tests/test_naive_flow.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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

0 comments on commit 03acda7

Please sign in to comment.