Skip to content

Commit 604ed6f

Browse files
committed
feat(services): add game service to modify game
1 parent d975ada commit 604ed6f

File tree

4 files changed

+177
-11
lines changed

4 files changed

+177
-11
lines changed
Lines changed: 52 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
from dataclasses import dataclass
22
from typing import Optional
33

4-
from mastermind.core.controllers.players import Player
4+
from mastermind.core.controllers.players import PlayerRole
55

66

77
@dataclass
@@ -12,9 +12,57 @@ class GameState:
1212
Attributes:
1313
game_started (bool): A flag indicating if the game has started.
1414
game_over (bool): A flag indicating if the game has ended.
15-
winner (Player): The player who won the game, if any. Only top level player (CodeSetter or Codebreaker) is allowed.
15+
winner (PlayerRole): The player who won the game, if any. Only top level player (CodeSetter or Codebreaker) is allowed.
1616
"""
1717

1818
game_started: bool = False
19-
game_over: bool = False
20-
winner: Optional[Player] = None
19+
winner: Optional[PlayerRole] = None
20+
21+
@property
22+
def game_over(self) -> bool:
23+
"""Returns a boolean indicating if the game has ended.
24+
25+
Returns:
26+
bool: True if the game has ended, False otherwise.
27+
28+
Examples:
29+
>>> game_state = GameState(game_started=True, winner=None)
30+
>>> game_state.game_over
31+
False
32+
>>> game_state = GameState(game_started=True, winner=PlayerRole.CODE_SETTER)
33+
>>> game_state.game_over
34+
True
35+
"""
36+
return self.winner is not None
37+
38+
39+
def get_winner(
40+
num_attempts: int,
41+
max_attempts: int,
42+
last_feedback: tuple[int, int],
43+
number_of_dots: int,
44+
) -> Optional[PlayerRole]:
45+
"""Determines the winner of the game based on the number of attempts and the last feedback.
46+
47+
Args:
48+
num_attempts (int): The number of attempts made by the player.
49+
max_attempts (int): The maximum number of attempts allowed.
50+
last_feedback (tuple[int, int]): The feedback received from the last game round.
51+
number_of_dots (int): The number of dots in the code.
52+
53+
Returns:
54+
Optional[PlayerRole]: The winner of the game, if any.
55+
56+
Examples:
57+
>>> get_winner(num_attempts=5, max_attempts=5, last_feedback=(1, 0), number_of_dots=4) == PlayerRole.CODE_SETTER
58+
True
59+
>>> get_winner(num_attempts=5, max_attempts=5, last_feedback=(4, 0), number_of_dots=4) == PlayerRole.CODE_BREAKER
60+
True
61+
>>> get_winner(num_attempts=4, max_attempts=5, last_feedback=(1, 0), number_of_dots=4) == None
62+
True
63+
"""
64+
65+
if last_feedback == (number_of_dots, 0):
66+
return PlayerRole.CODE_BREAKER
67+
68+
return PlayerRole.CODE_SETTER if num_attempts >= max_attempts else None

src/mastermind/core/models/gameboard.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ def guesses(self) -> Generator[Tuple[int, ...], None, None]:
3939
Returns a generator of all guesses made in the game to allow for easy iteration.
4040
4141
Returns:
42-
Generator[Tuple[int, ...], None, None]: _description_
42+
Generator[Tuple[int, ...], None, None]: A generator of all guesses made in the game.
4343
4444
Examples:
4545
>>> game_board = GameBoard(game_rounds=[GameRound(GUESS=(1, 2, 3, 4), FEEDBACK=(1, 0)), GameRound(GUESS=(3, 4, 5, 6), FEEDBACK=(2, 1))])
Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
from mastermind.core.models.game import Game
2+
from mastermind.core.models.game_state import get_winner
3+
from mastermind.core.services.gameboard_service import GameboardService
4+
5+
6+
class GameEndedException(Exception):
7+
pass
8+
9+
10+
class GameNotStartedException(Exception):
11+
pass
12+
13+
14+
class GameService:
15+
def __init__(self, game: Game) -> None:
16+
"""Initializes a new game service with the given game.
17+
18+
Args:
19+
game (Game): The game to be managed.
20+
"""
21+
22+
self._game_board = game.game_board
23+
self._game_configuration = game.game_configuration
24+
self._game_entities = game.game_entities
25+
self._game_state = game.game_state
26+
self._gameboard_service = GameboardService(self._game_board)
27+
28+
def add_round(self, guess: tuple[int, ...], feedback: tuple[int, int]) -> None:
29+
"""Adds a new game round with the player's guess and corresponding feedback.
30+
31+
Appends the round to game rounds and clears the undo stack to prevent branching.
32+
This method should be called from the GameController, not directly by the player.
33+
34+
Args:
35+
guess (tuple[int, ...]): A tuple representing the player's current guess.
36+
feedback (tuple[int, int]): A tuple containing the number of correct and misplaced pegs.
37+
38+
Examples:
39+
>>> game = create_new_game(GameConfiguration(NUMBER_OF_COLORS=3, NUMBER_OF_DOTS=4, ATTEMPTS_ALLOWED=5, GAME_MODE=GameMode.PVP))
40+
>>> service = GameService(game)
41+
>>> service.add_round((1, 2, 3, 4), (1, 0))
42+
>>> service.game_rounds
43+
[GameRound(GUESS=(1, 2, 3, 4), FEEDBACK=(1, 0))]
44+
45+
Raises:
46+
GameEndedException: When trying to add a round to a game that has ended.
47+
"""
48+
49+
if self._game_state.game_over:
50+
raise GameEndedException("Cannot add round to game that has ended.")
51+
52+
self._gameboard_service.add_round(guess, feedback)
53+
self._game_state.game_started = True
54+
self._game_state.winner = get_winner(
55+
num_attempts=len(self._game_board),
56+
max_attempts=self._game_configuration.ATTEMPTS_ALLOWED,
57+
last_feedback=self._gameboard_service.game_rounds[-1].FEEDBACK,
58+
number_of_dots=self._game_configuration.NUMBER_OF_DOTS,
59+
)
60+
61+
def undo(self) -> None:
62+
"""Undo the most recent game round.
63+
64+
Examples:
65+
>>> game = create_new_game(GameConfiguration(NUMBER_OF_COLORS=3, NUMBER_OF_DOTS=4, ATTEMPTS_ALLOWED=5, GAME_MODE=GameMode.PVP))
66+
>>> service = GameService(game)
67+
>>> service.add_round((1, 2, 3, 4), (1, 0))
68+
>>> game.game_board.game_rounds
69+
[GameRound(GUESS=(1, 2, 3, 4), FEEDBACK=(1, 0))]
70+
>>> service.undo()
71+
>>> game.game_board.game_rounds
72+
[]
73+
74+
Raises:
75+
GameNotStartedException: When trying to undo game rounds before the game has started.
76+
GameEndedException: When trying to undo game rounds after the game has ended.
77+
"""
78+
79+
if not self._game_state.game_started:
80+
raise GameNotStartedException(
81+
"Cannot undo game rounds before game has started."
82+
)
83+
84+
if not self._game_state.game_over:
85+
raise GameEndedException("Cannot undo game rounds after game has ended.")
86+
87+
self._gameboard_service.undo()
88+
89+
def redo(self) -> None:
90+
"""Restores the most recently undone game round.
91+
92+
Examples:
93+
>>> game = create_new_game(GameConfiguration(NUMBER_OF_COLORS=3, NUMBER_OF_DOTS=4, ATTEMPTS_ALLOWED=5, GAME_MODE=GameMode.PVP))
94+
>>> service = GameService(game)
95+
>>> service.add_round((1, 2, 3, 4), (1, 0))
96+
>>> service.undo()
97+
>>> service.redo()
98+
>>> service.game_rounds
99+
[GameRound(GUESS=(1, 2, 3, 4), FEEDBACK=(1, 0))]
100+
101+
Raises:
102+
GameNotStartedException: When trying to redo game rounds before the game has started.
103+
"""
104+
105+
if not self._game_state.game_started:
106+
raise GameNotStartedException(
107+
"Cannot redo game rounds before game has started."
108+
)
109+
110+
self._gameboard_service.redo()

src/mastermind/core/services/gameboard_service.py

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,22 @@
66

77

88
class GameboardService:
9+
"""This class manages the game board and provides methods for adding and undoing game rounds.
10+
11+
It should be called from the GameService, not directly by the player.
12+
13+
Attributes:
14+
game_rounds (Deque[GameRound]): A deque of GameRound instances, each representing a round of the game.
15+
undo_stack (Deque[GameRound]): A deque of GameRound instances, representing the undo stack.
16+
"""
17+
918
def __init__(self, gameboard: GameBoard) -> None:
1019
"""Initializes a new game board service with the current game board state.
1120
1221
The service prepares game rounds and an undo stack for tracking game progress.
1322
1423
Args:
15-
gameboard: The current game board to be managed.
24+
gameboard (GameBoard): The current game board to be managed.
1625
"""
1726

1827
self.game_rounds: Deque[GameRound] = gameboard.game_rounds
@@ -49,20 +58,19 @@ def redo(self) -> None:
4958

5059
self.game_rounds.append(self.undo_stack.pop())
5160

52-
def _add_round(self, guess: tuple[int, ...], feedback: tuple[int, int]) -> None:
61+
def add_round(self, guess: tuple[int, ...], feedback: tuple[int, int]) -> None:
5362
"""Adds a new game round with the player's guess and corresponding feedback.
5463
5564
Appends the round to game rounds and clears the undo stack to prevent branching.
56-
This method should be called from the GameService, not directly by the player.
5765
5866
Args:
59-
guess: A tuple representing the player's current guess.
60-
feedback: A tuple containing the number of correct and misplaced pegs.
67+
guess (tuple[int, ...]): A tuple representing the player's current guess.
68+
feedback (tuple[int, int]): A tuple containing the number of correct and misplaced pegs.
6169
6270
Examples:
6371
>>> gameboard = GameBoard(game_rounds=[GameRound(GUESS=(3, 4, 5, 6), FEEDBACK=(2, 1))])
6472
>>> service = GameboardService(gameboard)
65-
>>> service._add_round((1, 2, 3, 4), (1, 0))
73+
>>> service.add_round((1, 2, 3, 4), (1, 0))
6674
>>> service.game_rounds
6775
[GameRound(GUESS=(3, 4, 5, 6), FEEDBACK=(2, 1)), GameRound(GUESS=(1, 2, 3, 4), FEEDBACK=(1, 0))]
6876
"""

0 commit comments

Comments
 (0)