-
Notifications
You must be signed in to change notification settings - Fork 2
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
7 changed files
with
196 additions
and
36 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,90 @@ | ||
from __future__ import annotations | ||
|
||
import logging | ||
|
||
from codenames.duet.board import DuetBoard | ||
from codenames.duet.score import GameResult | ||
from codenames.generic.exceptions import InvalidGuess | ||
from codenames.generic.move import GivenGuess | ||
from codenames.generic.player import Operative, PlayerRole, Spymaster | ||
from codenames.generic.runner import ( | ||
SEPARATOR, | ||
ClueGivenSubscriber, | ||
GuessGivenSubscriber, | ||
TeamPlayers, | ||
) | ||
from codenames.mini.state import MiniGameState | ||
|
||
log = logging.getLogger(__name__) | ||
|
||
|
||
class MiniGameRunner: | ||
def __init__(self, players: TeamPlayers, state: MiniGameState | None = None, board: DuetBoard | None = None): | ||
self.players = players | ||
if (not state and not board) or (state and board): | ||
raise ValueError("Exactly one of state or board must be provided.") | ||
self.state = state or MiniGameState.from_board(board=board) # type: ignore[arg-type] | ||
self.clue_given_subscribers: list[ClueGivenSubscriber] = [] | ||
self.guess_given_subscribers: list[GuessGivenSubscriber] = [] | ||
|
||
@property | ||
def spymaster(self) -> Spymaster: | ||
return self.players.spymaster | ||
|
||
@property | ||
def operative(self) -> Operative: | ||
return self.players.operative | ||
|
||
@property | ||
def current_role(self) -> PlayerRole: | ||
return self.state.current_player_role | ||
|
||
def run_game(self) -> GameResult: | ||
self._notify_game_starts() | ||
result = self._run_rounds() | ||
suffix = "win!" if result.win else "lose!" | ||
log.info(f"{SEPARATOR}{result.reason}, you {suffix}") | ||
return result | ||
|
||
def _notify_game_starts(self): | ||
self.spymaster.on_game_start(board=self.state.board) | ||
self.operative.on_game_start(board=self.state.board.censored) | ||
|
||
def _run_rounds(self) -> GameResult: | ||
while not self.state.is_game_over: | ||
self._run_turn() | ||
return self.state.game_result # type: ignore | ||
|
||
def _run_turn(self): | ||
if not self.state.is_sudden_death: | ||
self._get_clue_from(spymaster=self.spymaster) | ||
while not self.state.is_game_over and self.current_role == PlayerRole.OPERATIVE: | ||
self._get_guess_from(operative=self.operative) | ||
|
||
def _get_clue_from(self, spymaster: Spymaster): | ||
clue = spymaster.give_clue(game_state=self.state.spymaster_state) | ||
for subscriber in self.clue_given_subscribers: | ||
subscriber(spymaster, clue) | ||
given_clue = self.state.process_clue(clue=clue) | ||
if given_clue is None: | ||
return | ||
for player in self.players: | ||
player.on_clue_given(given_clue=given_clue) | ||
|
||
def _get_guess_from(self, operative: Operative): | ||
given_guess = self._get_guess_until_valid(operative) | ||
if given_guess is None: | ||
return | ||
for player in self.players: | ||
player.on_guess_given(given_guess=given_guess) | ||
|
||
def _get_guess_until_valid(self, operative: Operative) -> GivenGuess | None: | ||
while True: | ||
guess = operative.guess(game_state=self.state.operative_state) | ||
try: | ||
given_guess = self.state.process_guess(guess=guess) | ||
except InvalidGuess: | ||
continue | ||
for subscriber in self.guess_given_subscribers: | ||
subscriber(operative, guess) | ||
return given_guess |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Empty file.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
import pytest | ||
|
||
from codenames.duet.board import DuetBoard | ||
from tests.duet.utils import constants | ||
|
||
|
||
@pytest.fixture() | ||
def board_10() -> DuetBoard: | ||
return constants.board_10() |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,74 @@ | ||
from codenames.duet.board import DuetBoard | ||
from codenames.duet.score import ( | ||
MISTAKE_LIMIT_REACHED, | ||
TARGET_REACHED, | ||
TIMER_TOKENS_DEPLETED, | ||
) | ||
from codenames.duet.state import DuetSide | ||
from codenames.generic.move import PASS_GUESS, Clue | ||
from codenames.mini.runner import MiniGameRunner | ||
from codenames.mini.state import MiniGameState | ||
from tests.duet.utils.runner import build_players | ||
from tests.utils.players.dictated import DictatedTurn | ||
|
||
|
||
def test_happy_flow(board_10: DuetBoard): | ||
turns_by_side = { | ||
DuetSide.SIDE_A: [ | ||
DictatedTurn(clue=Clue(word="A", card_amount=3), guesses=[0, 1, 4]), # Green, Green, Neutral | ||
DictatedTurn(clue=Clue(word="B", card_amount=2), guesses=[2, PASS_GUESS]), # Green, pass | ||
DictatedTurn(clue=Clue(word="C", card_amount=2), guesses=[3]), # Green | ||
] | ||
} | ||
players = build_players(turns_by_side=turns_by_side) | ||
state = MiniGameState.from_board(board=board_10) | ||
runner = MiniGameRunner(players=players.team_a, state=state) | ||
runner.run_game() | ||
|
||
assert runner.state.game_result == TARGET_REACHED | ||
assert runner.state.timer_tokens == 2 | ||
assert runner.state.allowed_mistakes == 3 | ||
assert len(runner.state.given_clues) == 3 | ||
assert len(runner.state.given_guesses) == 5 | ||
|
||
|
||
def test_timer_token_depleted(board_10: DuetBoard): | ||
turns_by_side = { | ||
DuetSide.SIDE_A: [ | ||
DictatedTurn(clue=Clue(word="A", card_amount=3), guesses=[4]), # Neutral | ||
DictatedTurn(clue=Clue(word="B", card_amount=2), guesses=[PASS_GUESS]), # pass | ||
DictatedTurn(clue=Clue(word="NONE", card_amount=2), guesses=[4, 1, 5]), # Green, Neutral | ||
] | ||
} | ||
players = build_players(turns_by_side=turns_by_side) | ||
state = MiniGameState.from_board(board=board_10) | ||
state.timer_tokens = 2 | ||
runner = MiniGameRunner(players=players.team_a, state=state) | ||
runner.run_game() | ||
|
||
assert runner.state.game_result == TIMER_TOKENS_DEPLETED | ||
assert runner.state.timer_tokens == -1 | ||
assert runner.state.allowed_mistakes == 2 | ||
assert len(runner.state.given_clues) == 2 | ||
assert len(runner.state.given_guesses) == 3 | ||
|
||
|
||
def test_mistake_limit_reached(board_10: DuetBoard): | ||
turns_by_side = { | ||
DuetSide.SIDE_A: [ | ||
DictatedTurn(clue=Clue(word="A", card_amount=3), guesses=[4]), # Neutral | ||
DictatedTurn(clue=Clue(word="B", card_amount=2), guesses=[PASS_GUESS]), # pass | ||
DictatedTurn(clue=Clue(word="C", card_amount=2), guesses=[0, 1, 5]), # Green, Green, Neutral | ||
] | ||
} | ||
players = build_players(turns_by_side=turns_by_side) | ||
state = MiniGameState.from_board(board=board_10) | ||
state.allowed_mistakes = 2 | ||
runner = MiniGameRunner(players=players.team_a, state=state) | ||
runner.run_game() | ||
|
||
assert runner.state.game_result == MISTAKE_LIMIT_REACHED | ||
assert runner.state.timer_tokens == 2 | ||
assert runner.state.allowed_mistakes == 0 | ||
assert len(runner.state.given_clues) == 3 | ||
assert len(runner.state.given_guesses) == 4 |