Skip to content
Open
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
23 changes: 12 additions & 11 deletions codenames/duet/score.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
from __future__ import annotations

from pydantic import BaseModel, ConfigDict
from enum import Enum

from pydantic import BaseModel

from codenames.generic.state import TeamScore

Expand All @@ -21,14 +23,13 @@ def target_reached(self) -> bool:
return self.main.revealed == self.main.total


class GameResult(BaseModel):
model_config = ConfigDict(frozen=True)
win: bool
reason: str

class GameResult(Enum):
def __init__(self, win: bool, reason: str):
self.win = win
self.reason = reason

TARGET_REACHED = GameResult(win=True, reason="Target score reached")
ASSASSIN_HIT = GameResult(win=False, reason="Assassin card was hit")
GAME_QUIT = GameResult(win=False, reason="Team quit the game")
TIMER_TOKENS_DEPLETED = GameResult(win=False, reason="Timer tokens depleted")
MISTAKE_LIMIT_REACHED = GameResult(win=False, reason="Mistake limit reached")
TARGET_REACHED = (True, "Target score reached")
ASSASSIN_HIT = (False, "Assassin card was hit")
GAME_QUIT = (False, "The game was quit")
TIMER_TOKENS_DEPLETED = (False, "Timer tokens depleted")
MISTAKE_LIMIT_REACHED = (False, "Mistake limit reached")
29 changes: 14 additions & 15 deletions codenames/duet/state.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,7 @@

from codenames.duet.board import DuetBoard
from codenames.duet.card import DuetColor
from codenames.duet.score import (
ASSASSIN_HIT,
GAME_QUIT,
MISTAKE_LIMIT_REACHED,
TARGET_REACHED,
TIMER_TOKENS_DEPLETED,
GameResult,
Score,
)
from codenames.duet.score import GameResult, Score
from codenames.duet.team import DuetTeam
from codenames.duet.types import DuetCard, DuetGivenClue, DuetGivenGuess
from codenames.generic.board import WordGroup
Expand Down Expand Up @@ -103,6 +95,13 @@ def spymaster_state(self) -> DuetSpymasterState:
def operative_state(self) -> DuetOperativeState:
return self.get_operative_state(None)

@field_validator("game_result", mode="before")
@classmethod
def parse_game_result(cls, v: Any) -> GameResult | None:
if v is None:
return None
return GameResult(*v)

def process_clue(self, clue: Clue) -> DuetGivenClue | None:
if self.is_game_over:
raise GameIsOver()
Expand Down Expand Up @@ -197,18 +196,18 @@ def _end_turn(self):
self.current_player_role = self.current_player_role.other

def _quit(self):
self.game_result = GAME_QUIT
self.game_result = GameResult.GAME_QUIT
self._end_turn()

def _update_score(self, card_color: DuetColor):
if card_color == DuetColor.NEUTRAL:
return
if card_color == DuetColor.ASSASSIN:
self.game_result = ASSASSIN_HIT
self.game_result = GameResult.ASSASSIN_HIT
return
game_ended = self.score.add_point()
if game_ended:
self.game_result = TARGET_REACHED
self.game_result = GameResult.TARGET_REACHED


class DuetSide(StrEnum):
Expand Down Expand Up @@ -244,9 +243,9 @@ def from_boards(cls, board_a: DuetBoard, board_b: DuetBoard) -> DuetGameState:
def game_result(self) -> GameResult | None:
# If the timer runs out, the game is lost
if self.timer_tokens < 0:
return TIMER_TOKENS_DEPLETED
return GameResult.TIMER_TOKENS_DEPLETED
if self.allowed_mistakes == 0:
return MISTAKE_LIMIT_REACHED
return GameResult.MISTAKE_LIMIT_REACHED
result_a, result_b = self.side_a.game_result, self.side_b.game_result
# If no side has a result, the game is still ongoing
if not result_a and not result_b:
Expand All @@ -261,7 +260,7 @@ def game_result(self) -> GameResult | None:
return None
# Otherwise, both sides, finished, no one lost, means the game is won
assert result_a.win and result_b.win
return TARGET_REACHED
return GameResult.TARGET_REACHED

@property
def is_sudden_death(self) -> bool:
Expand Down
6 changes: 3 additions & 3 deletions codenames/mini/state.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import logging

from codenames.duet.score import MISTAKE_LIMIT_REACHED, TIMER_TOKENS_DEPLETED
from codenames.duet.score import GameResult
from codenames.duet.state import DuetSideState
from codenames.duet.types import DuetGivenGuess
from codenames.generic.move import Guess
Expand Down Expand Up @@ -35,12 +35,12 @@ def _update_tokens(self, mistake: bool) -> None:
log.info("Timer tokens depleted! Entering sudden death")
self.current_player_role = PlayerRole.OPERATIVE
elif self.timer_tokens < 0:
self.game_result = TIMER_TOKENS_DEPLETED
self.game_result = GameResult.TIMER_TOKENS_DEPLETED
log.info("Timer tokens depleted (after sudden death)!")
if not mistake:
return
self.allowed_mistakes -= 1
if self.allowed_mistakes == 0:
log.info("Mistake limit reached!")
self.game_result = MISTAKE_LIMIT_REACHED
self.game_result = GameResult.MISTAKE_LIMIT_REACHED
return
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@ minor_tags = [
"🔥",
"🐲",
"🎡",
"🏞️",
]
patch_tags = [
"📝",
Expand Down
12 changes: 4 additions & 8 deletions tests/duet/test_game_runner.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,7 @@

from codenames.duet.board import DuetBoard
from codenames.duet.runner import DuetGameRunner
from codenames.duet.score import (
MISTAKE_LIMIT_REACHED,
TARGET_REACHED,
TIMER_TOKENS_DEPLETED,
)
from codenames.duet.score import GameResult
from codenames.duet.state import (
DuetGameState,
DuetOperativeState,
Expand Down Expand Up @@ -102,7 +98,7 @@ def test_tokens_run_out_game(board_10_state: DuetGameState):
runner = DuetGameRunner(players=players, state=board_10_state)
runner.run_game()

assert runner.state.game_result == TIMER_TOKENS_DEPLETED
assert runner.state.game_result == GameResult.TIMER_TOKENS_DEPLETED


def test_mistakes_run_out_game(board_10_state: DuetGameState):
Expand All @@ -121,7 +117,7 @@ def test_mistakes_run_out_game(board_10_state: DuetGameState):
runner = DuetGameRunner(players=players, state=board_10_state)
runner.run_game()

assert runner.state.game_result == MISTAKE_LIMIT_REACHED
assert runner.state.game_result == GameResult.MISTAKE_LIMIT_REACHED


def test_sudden_death(board_10_state: DuetGameState):
Expand All @@ -144,6 +140,6 @@ def test_sudden_death(board_10_state: DuetGameState):
runner = DuetGameRunner(players=players, state=board_10_state)
runner.run_game()

assert runner.state.game_result == TARGET_REACHED
assert runner.state.game_result == GameResult.TARGET_REACHED
assert runner.state.timer_tokens == 0
assert runner.state.is_sudden_death
6 changes: 3 additions & 3 deletions tests/duet/test_game_state.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
import pytest

from codenames.duet.board import DuetBoard
from codenames.duet.score import TARGET_REACHED
from codenames.duet.score import GameResult
from codenames.duet.state import DuetGameState, DuetSide
from codenames.generic.exceptions import InvalidGuess
from codenames.generic.move import PASS_GUESS, Clue, Guess
Expand Down Expand Up @@ -111,7 +111,7 @@ def test_game_state_flow(board_10: DuetBoard, board_10_dual: DuetBoard):
assert game_state.current_side_state.score.main.revealed == 2
assert game_state.timer_tokens == 6
assert game_state.side_a.is_game_over
assert game_state.side_a.game_result == TARGET_REACHED
assert game_state.side_a.game_result == GameResult.TARGET_REACHED
assert not game_state.side_b.is_game_over
assert game_state.side_b.game_result is None
assert not game_state.is_game_over
Expand All @@ -136,4 +136,4 @@ def test_game_state_flow(board_10: DuetBoard, board_10_dual: DuetBoard):

assert game_state.timer_tokens == 4
assert game_state.is_game_over
assert game_state.game_result == TARGET_REACHED
assert game_state.game_result == GameResult.TARGET_REACHED
21 changes: 14 additions & 7 deletions tests/duet/test_side_state.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
import pytest

from codenames.duet.board import DuetBoard
from codenames.duet.score import ASSASSIN_HIT, GAME_QUIT, TARGET_REACHED
from codenames.duet.score import GameResult
from codenames.duet.state import DuetSideState
from codenames.generic.exceptions import GameIsOver, InvalidGuess, InvalidTurn
from codenames.generic.move import PASS_GUESS, QUIT_GAME, Clue, Guess
Expand All @@ -13,15 +13,22 @@

def test_side_state_json_serialization_and_load(board_10: DuetBoard):
side_state = DuetSideState.from_board(board=board_10)
side_state.process_clue(clue=Clue(word="A", card_amount=2))
side_state.process_clue(clue=Clue(word="A", card_amount=3))
side_state.process_guess(guess=Guess(card_index=0))
side_state.process_guess(guess=Guess(card_index=1))

side_state_json = side_state.model_dump_json()
side_state_dict = json.loads(side_state_json)
side_state_from_json = DuetSideState.model_validate(side_state_dict)
assert side_state_from_json == side_state
assert side_state_dict == side_state.model_dump()
assert side_state_dict == side_state.model_dump(mode="json")

side_state.process_guess(guess=Guess(card_index=QUIT_GAME))
side_state_json = side_state.model_dump_json()
side_state_dict = json.loads(side_state_json)
side_state_from_json = DuetSideState.model_validate(side_state_dict)
assert side_state_from_json == side_state
assert side_state_dict == side_state.model_dump(mode="json")


def test_side_state_win_flow(board_10: DuetBoard):
Expand Down Expand Up @@ -79,7 +86,7 @@ def test_side_state_win_flow(board_10: DuetBoard):
side_state.process_guess(guess=Guess(card_index=3)) # Green - Correct, win
assert side_state.score.main.revealed == 4
assert len(get_side_moves(side_state)) == 9
assert side_state.game_result == TARGET_REACHED
assert side_state.game_result == GameResult.TARGET_REACHED


def test_side_state_assassin_flow(board_10: DuetBoard):
Expand Down Expand Up @@ -122,7 +129,7 @@ def test_side_state_assassin_flow(board_10: DuetBoard):
assert side_state.score.main.revealed == 3
assert len(get_side_moves(side_state)) == 7
assert side_state.is_game_over
assert side_state.game_result == ASSASSIN_HIT
assert side_state.game_result == GameResult.ASSASSIN_HIT

with pytest.raises(GameIsOver):
side_state.process_clue(clue=Clue(word="Clue 3", card_amount=1))
Expand All @@ -144,7 +151,7 @@ def test_side_state_spymaster_quit(board_10: DuetBoard):
# Round 2
side_state.process_clue(clue=Clue(word="Clue 2", card_amount=QUIT_GAME))
assert side_state.is_game_over
assert side_state.game_result == GAME_QUIT
assert side_state.game_result == GameResult.GAME_QUIT


def test_side_state_operator_quit(board_10: DuetBoard):
Expand All @@ -157,4 +164,4 @@ def test_side_state_operator_quit(board_10: DuetBoard):
assert not side_state.is_game_over
side_state.process_guess(guess=Guess(card_index=QUIT_GAME))
assert side_state.is_game_over
assert side_state.game_result == GAME_QUIT
assert side_state.game_result == GameResult.GAME_QUIT
12 changes: 4 additions & 8 deletions tests/mini/test_game_runner.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,5 @@
from codenames.duet.board import DuetBoard
from codenames.duet.score import (
MISTAKE_LIMIT_REACHED,
TARGET_REACHED,
TIMER_TOKENS_DEPLETED,
)
from codenames.duet.score import GameResult
from codenames.duet.state import DuetSide
from codenames.generic.move import PASS_GUESS, Clue
from codenames.mini.runner import MiniGameRunner
Expand All @@ -25,7 +21,7 @@ def test_happy_flow(board_10: DuetBoard):
runner = MiniGameRunner(players=players.team_a, state=state)
runner.run_game()

assert runner.state.game_result == TARGET_REACHED
assert runner.state.game_result == GameResult.TARGET_REACHED
assert runner.state.timer_tokens == 2
assert runner.state.allowed_mistakes == 3
assert len(runner.state.given_clues) == 3
Expand All @@ -46,7 +42,7 @@ def test_timer_token_depleted(board_10: DuetBoard):
runner = MiniGameRunner(players=players.team_a, state=state)
runner.run_game()

assert runner.state.game_result == TIMER_TOKENS_DEPLETED
assert runner.state.game_result == GameResult.TIMER_TOKENS_DEPLETED
assert runner.state.timer_tokens == -1
assert runner.state.allowed_mistakes == 2
assert len(runner.state.given_clues) == 2
Expand All @@ -67,7 +63,7 @@ def test_mistake_limit_reached(board_10: DuetBoard):
runner = MiniGameRunner(players=players.team_a, state=state)
runner.run_game()

assert runner.state.game_result == MISTAKE_LIMIT_REACHED
assert runner.state.game_result == GameResult.MISTAKE_LIMIT_REACHED
assert runner.state.timer_tokens == 2
assert runner.state.allowed_mistakes == 0
assert len(runner.state.given_clues) == 3
Expand Down