State is represented using frozen dataclasses. State classes are understood as a representation of system state in a given point in time. To better represent state mutations immutable dataclasses are used so an evolution in the state implies a new name making concepts clearer
from dataclasses import dataclass, field, replace
from typing import List, Optional
from domain.games.tic_tac_toe.board import TicTacToeBoard
from domain.games.tic_tac_toe.types import GameStage
from domain.games.types import GameId, UserId
from scuti.domain.model.identifiable.identifiable_entity import IdentifiableEntity
Each state stage can have its own properties and methods to derive state or mutate
@dataclass(frozen=True)
class GameWaitingForPlayers(IdentifiableEntity[GameId]):
id: GameId
players: List[UserId] = field(default_factory=list)
@dataclass(frozen=True)
class GameInProgress(IdentifiableEntity[GameId]):
id: GameId
board: TicTacToeBoard
stage: GameStage
winner: Optional[UserId] = field(default=None)
waiting_for_player: Optional[UserId] = field(default=None)
players: List[UserId] = field(default_factory=list)
def place(self, player: UserId, x: int, y: int):
next_board = self.board.place(x=x, y=y, player_id=player)
stage = self.__next_stage(next_board)
winner = next_board.any_player_has_three_in_a_row()
next_player = self.players[0] if self.waiting_for_player == self.players[1] else self.players[1]
return replace(self,
waiting_for_player=next_player,
winner=winner,
stage=stage,
board=next_board)
def cancel_game(self):
return replace(self, stage=GameStage.GAME_ABORTED)
def __next_stage(self, next_board: TicTacToeBoard):
if next_board.any_player_has_three_in_a_row():
return GameStage.PLAYER_WON
elif next_board.is_full():
return GameStage.DRAW
else:
return GameStage.IN_PROGRESS