diff --git a/codenames/duet/score.py b/codenames/duet/score.py index bf923ff..909c35b 100644 --- a/codenames/duet/score.py +++ b/codenames/duet/score.py @@ -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 @@ -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") diff --git a/codenames/duet/state.py b/codenames/duet/state.py index 70fc576..06939ce 100644 --- a/codenames/duet/state.py +++ b/codenames/duet/state.py @@ -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 @@ -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() @@ -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): @@ -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: @@ -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: diff --git a/codenames/mini/state.py b/codenames/mini/state.py index edea1c0..4007541 100644 --- a/codenames/mini/state.py +++ b/codenames/mini/state.py @@ -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 @@ -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 diff --git a/pyproject.toml b/pyproject.toml index 54d8082..bd98a28 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -76,6 +76,7 @@ minor_tags = [ "🔥", "🐲", "🎡", + "🏞️", ] patch_tags = [ "📝", diff --git a/tests/duet/test_game_runner.py b/tests/duet/test_game_runner.py index 09ee3a0..1bda8cb 100644 --- a/tests/duet/test_game_runner.py +++ b/tests/duet/test_game_runner.py @@ -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, @@ -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): @@ -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): @@ -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 diff --git a/tests/duet/test_game_state.py b/tests/duet/test_game_state.py index 126d93a..884a358 100644 --- a/tests/duet/test_game_state.py +++ b/tests/duet/test_game_state.py @@ -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 @@ -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 @@ -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 diff --git a/tests/duet/test_side_state.py b/tests/duet/test_side_state.py index 044a6c5..e63ea44 100644 --- a/tests/duet/test_side_state.py +++ b/tests/duet/test_side_state.py @@ -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 @@ -13,7 +13,7 @@ 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)) @@ -21,7 +21,14 @@ def test_side_state_json_serialization_and_load(board_10: DuetBoard): 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): @@ -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): @@ -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)) @@ -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): @@ -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 diff --git a/tests/mini/test_game_runner.py b/tests/mini/test_game_runner.py index 759c075..dc2e5dc 100644 --- a/tests/mini/test_game_runner.py +++ b/tests/mini/test_game_runner.py @@ -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 @@ -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 @@ -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 @@ -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