Skip to content

Models module

Provide the classes for domain model.

This module allows the creation of intances of Marks, Grids, Move and GameState.

The module contains the following class: - Mark - A class that handles user marks. - Grid - A inmutable Class that handles the grid information. - Move - A inmutable data class that handles move information. - GameState - A inmutable data class that handles game state information.

GameState dataclass

An inmutable data Class that is strictly a data transfer object (DTO) whose main purpose is to carry data, consisting of the grid of cells and the starting player's mark

Attributes:

Name Type Description
grid Grid

Grid Represents the grid, 9 elements X, O or space

starting_mark Mark

Mark = Mark("X") Represent the starting mark. Default to X

Methods:

Name Description
current_mark

Cached getter of current mark.

game_not_started

Cached getter if current state is the initial state.

game_over

Cached getter to check if the game is ofver by check if there is a winner or there is a tie.

tie

Cached getter to check if there is a tie by checking if there is a winner or grid is empty.

winner

Cached getter that check if there is a winner by checking winning patterns.

possible_moves

Cached getter of possible moves.

make_random_move

Return possible move based on possible moves.

make_move_to

int) -> Move: Return the move to make based on index.

evaluate_score

Mark) -> int: Returns score based on the result of the move.

Source code in src\backend\logic\models.py
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
@dataclass(frozen=True)
class GameState:
    """An inmutable data Class that is strictly a data transfer object (DTO) whose main purpose
    is to carry data, consisting of the grid of cells and the starting player's mark

    Attributes:
        grid: Grid
            Represents the grid, 9 elements X, O or space
        starting_mark: Mark = Mark("X")
            Represent the starting mark. Default to X

    Methods:
        current_mark(self) -> Mark:
            Cached getter of current mark.
        game_not_started(self) -> bool:
            Cached getter if current state is the initial state.
        game_over(self) -> bool:
            Cached getter to check if the game is ofver by check if there is a winner or
            there is a tie.
        tie(self) -> bool:
            Cached getter to check if there is a tie by checking if
            there is a winner or grid is empty.
        winner(self) -> Mark | None:
            Cached getter that check if there is a winner by checking winning patterns.
        possible_moves(self) -> list[Move]:
            Cached getter of possible moves.
        make_random_move(self) -> Move | None:
            Return possible move based on possible moves.
        make_move_to(self, index: int) -> Move:
            Return the move to make based on index.
        evaluate_score(self, mark: Mark) -> int:
            Returns score based on the result of the move.
    """
    grid: Grid
    starting_mark: Mark = Mark("X")

    def __post_init__(self) -> None:
        """Post instantiation hook that verifies that the gamestate is correct
        """
        validate_game_state(self)

    @cached_property
    def current_mark(self) -> Mark:
        """Cached getter of current mark.

        Returns:
            Mark: Mark of current state.
        """
        if self.grid.x_count == self.grid.o_count:
            return self.starting_mark
        return self.starting_mark.other

    @cached_property
    def game_not_started(self) -> bool:
        """Cached getter if current state is the initial state.

        Returns:
            bool: Rather current turn is the first turn or not
        """
        return self.grid.empty_count == 9

    @cached_property
    def game_over(self) -> bool:
        """Cached getter to check if the game is ofver by check if there is a winner or
        there is a tie.

        Returns:
            bool: Rather the game is over or not.
        """
        return self.winner is not None or self.tie

    @cached_property
    def tie(self) -> bool:
        """Cached getter to check if there is a tie by checking if
        there is a winner or grid is empty.

        Returns:
            bool: Rather the game has a winner or grid is empty
        """
        return self.winner is None and self.grid.empty_count == 0

    @cached_property
    def winner(self) -> Mark | None:
        """Cached getter that check if there is a winner by checking winning patterns.

        Returns:
            Mark | None: Could be X, O or None.
        """
        for pattern in WINNING_PATTERNS:
            for mark in Mark:
                if re.match(pattern.replace("?", mark), self.grid.cells):
                    return mark
        return None

    @cached_property
    def winning_cells(self) -> list[int]:
        """Chaced getter with information of position of marks in winning cell

        Returns:
            list[int]: List of positions of marks in winning cell
        """
        for pattern in WINNING_PATTERNS:
            for mark in Mark:
                if re.match(pattern.replace("?", mark), self.grid.cells):
                    return [
                        match.start()
                        for match in re.finditer(r"\?", pattern)
                    ]
        return []

    @cached_property
    def possible_moves(self) -> list[Move]:
        """Cached getter of possible moves.

        Returns:
            list[Move]: list of possible moves
        """
        moves = []
        if not self.game_over:
            for match in re.finditer(r"\s", self.grid.cells):
                moves.append(self.make_move_to(match.start()))
        return moves

    def make_random_move(self) -> Move | None:
        """Return possible move based on possible moves.

        Returns:
            Move | None: Snapshot of moves.
        """
        try:
            return random.choice(self.possible_moves)
        except IndexError:
            return None

    def make_move_to(self, index: int) -> Move:
        """Return the move to make based on index.

        Args:
            index (int): Position of the move.

        Raises:
            InvalidMove: Exception when a invalid move is selected

        Returns:
            Move: Snapshot of moves
        """
        if self.grid.cells[index] != " ":
            raise InvalidMove("Cell is not empty")
        return Move(
            mark=self.current_mark,
            cell_index=index,
            before_state=self,
            after_state=GameState(
                Grid(
                    self.grid.cells[:index]
                    + self.current_mark
                    + self.grid.cells[index + 1:]
                ),
                self.starting_mark,
            ),
        )

    def evaluate_score(self, mark: Mark) -> int:
        """Returns score based on the result of the move.

        Args:
            mark (Mark): Class that handles user marks.

        Raises:
            UnknownGameScore: Exception when no score can be calculated.

        Returns:
            int: score for the game.
        """
        if self.game_over:
            if self.tie:
                return 0
            if self.winner is mark:
                return 1
            return -1
        raise UnknownGameScore("Game is not over yet")

current_mark: Mark cached property

Cached getter of current mark.

Returns:

Name Type Description
Mark Mark

Mark of current state.

game_not_started: bool cached property

Cached getter if current state is the initial state.

Returns:

Name Type Description
bool bool

Rather current turn is the first turn or not

game_over: bool cached property

Cached getter to check if the game is ofver by check if there is a winner or there is a tie.

Returns:

Name Type Description
bool bool

Rather the game is over or not.

possible_moves: list[Move] cached property

Cached getter of possible moves.

Returns:

Type Description
list[Move]

list[Move]: list of possible moves

tie: bool cached property

Cached getter to check if there is a tie by checking if there is a winner or grid is empty.

Returns:

Name Type Description
bool bool

Rather the game has a winner or grid is empty

winner: Mark | None cached property

Cached getter that check if there is a winner by checking winning patterns.

Returns:

Type Description
Mark | None

Mark | None: Could be X, O or None.

winning_cells: list[int] cached property

Chaced getter with information of position of marks in winning cell

Returns:

Type Description
list[int]

list[int]: List of positions of marks in winning cell

__post_init__()

Post instantiation hook that verifies that the gamestate is correct

Source code in src\backend\logic\models.py
167
168
169
170
def __post_init__(self) -> None:
    """Post instantiation hook that verifies that the gamestate is correct
    """
    validate_game_state(self)

evaluate_score(mark)

Returns score based on the result of the move.

Parameters:

Name Type Description Default
mark Mark

Class that handles user marks.

required

Raises:

Type Description
UnknownGameScore

Exception when no score can be calculated.

Returns:

Name Type Description
int int

score for the game.

Source code in src\backend\logic\models.py
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
def evaluate_score(self, mark: Mark) -> int:
    """Returns score based on the result of the move.

    Args:
        mark (Mark): Class that handles user marks.

    Raises:
        UnknownGameScore: Exception when no score can be calculated.

    Returns:
        int: score for the game.
    """
    if self.game_over:
        if self.tie:
            return 0
        if self.winner is mark:
            return 1
        return -1
    raise UnknownGameScore("Game is not over yet")

make_move_to(index)

Return the move to make based on index.

Parameters:

Name Type Description Default
index int

Position of the move.

required

Raises:

Type Description
InvalidMove

Exception when a invalid move is selected

Returns:

Name Type Description
Move Move

Snapshot of moves

Source code in src\backend\logic\models.py
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
def make_move_to(self, index: int) -> Move:
    """Return the move to make based on index.

    Args:
        index (int): Position of the move.

    Raises:
        InvalidMove: Exception when a invalid move is selected

    Returns:
        Move: Snapshot of moves
    """
    if self.grid.cells[index] != " ":
        raise InvalidMove("Cell is not empty")
    return Move(
        mark=self.current_mark,
        cell_index=index,
        before_state=self,
        after_state=GameState(
            Grid(
                self.grid.cells[:index]
                + self.current_mark
                + self.grid.cells[index + 1:]
            ),
            self.starting_mark,
        ),
    )

make_random_move()

Return possible move based on possible moves.

Returns:

Type Description
Move | None

Move | None: Snapshot of moves.

Source code in src\backend\logic\models.py
254
255
256
257
258
259
260
261
262
263
def make_random_move(self) -> Move | None:
    """Return possible move based on possible moves.

    Returns:
        Move | None: Snapshot of moves.
    """
    try:
        return random.choice(self.possible_moves)
    except IndexError:
        return None

Grid dataclass

An inmutable Class that handles the grid. It is instantiate as a empty grid 9 spaces as default. It runs as Post instantiation hook that verifies that grid composition. Allowed cell position: 9 elements (X, O, or space).

Attributes:

Name Type Description
cells str

str Represents the grid, 9 elements X, O or space.

Methods:

Name Description
x_count

Cached getter of total of X.

o_count

Cached getter of total of O

empty_count

Cached getter of total of spaces

Raises:

Type Description
ValueError

Raises ValueError if

Source code in src\backend\logic\models.py
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
@dataclass(frozen=True)
class Grid:
    """An inmutable Class that handles the grid. It is instantiate as a empty grid 9 spaces as
    default. It runs as Post instantiation hook that verifies that grid composition. Allowed
    cell position: 9 elements (X, O, or space).

    Attributes:
        cells: str
            Represents the grid, 9 elements X, O or space.

    Methods:
        x_count(self) -> int:
            Cached getter of total of X.
        o_count(self) -> int:
            Cached getter of total of O
        empty_count(self) -> int:
            Cached getter of total of spaces

    Raises:
        ValueError: Raises ValueError if
    """
    cells: str = " " * 9

    def __post_init__(self) -> None:
        """Post instantiation hook that verifies that the grid is compose of 9 elements (X, O, or
        space)"""
        validate_grid(self)

    @cached_property
    def x_count(self) -> int:
        """Cached getter of total of X

        Returns:
            int: Total of X
        """
        return self.cells.count("X")

    @cached_property
    def o_count(self) -> int:
        """Cached getter of total of O

        Returns:
            int: Total of Y
        """
        return self.cells.count("O")

    @cached_property
    def empty_count(self) -> int:
        """Cached getter of total of spaces

        Returns:
            int: Total of spaces
        """
        return self.cells.count(" ")

empty_count: int cached property

Cached getter of total of spaces

Returns:

Name Type Description
int int

Total of spaces

o_count: int cached property

Cached getter of total of O

Returns:

Name Type Description
int int

Total of Y

x_count: int cached property

Cached getter of total of X

Returns:

Name Type Description
int int

Total of X

__post_init__()

Post instantiation hook that verifies that the grid is compose of 9 elements (X, O, or space)

Source code in src\backend\logic\models.py
78
79
80
81
def __post_init__(self) -> None:
    """Post instantiation hook that verifies that the grid is compose of 9 elements (X, O, or
    space)"""
    validate_grid(self)

Mark

Bases: StrEnum

A class that handles user marks. it can be CROSS or X, or NAUGHT or O. Extends enum.StrEnum class. It can be CROSS or X, or NAUGHT or O.

Methods:

Name Description
other

Returns the opposite MARK space).

Returns:

Type Description
Mark

CROSS or X, or NAUGHT or O

Source code in src\backend\logic\models.py
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
class Mark(enum.StrEnum):
    """A class that handles user marks. it can be CROSS or X, or NAUGHT or O. Extends enum.StrEnum
        class. It can be CROSS or X, or NAUGHT or O.

    Methods:
        other(self) -> "Mark":
            Returns the opposite MARK
            space).

    Returns:
        (Mark): CROSS or X, or NAUGHT or O
    """
    CROSS = "X"
    NAUGHT = "O"

    @property
    def other(self) -> "Mark":
        """Returns the opposite MARK.

        Returns:
            Mark: can be X or O.
        """
        return Mark.CROSS if self is Mark.NAUGHT else Mark.NAUGHT

other: Mark property

Returns the opposite MARK.

Returns:

Name Type Description
Mark Mark

can be X or O.

Move dataclass

An inmutable data class that is strictly a data transfer object (DTO) whose main purpose is to carry data. Consists of the mark identifying the player who made a move, a numeric zero-based index in the string of cells, and the two states before and after making a move.

Attributes:

Name Type Description
mark Mark

Mark Represent the mark of the player.

cell_index int

int Represent the position to play.

before_state GameState

"GameState" Represent the game state before the move.

after_state GameState

"GameState" Represent the game state after the move.

Source code in src\backend\logic\models.py
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
@dataclass(frozen=True)
class Move:
    """An inmutable data class that is strictly a data transfer object (DTO) whose main purpose
    is to carry data. Consists of the mark identifying the player who made a move, a numeric
    zero-based index in the string of cells, and the two states before and after making a move.

    Attributes:
        mark: Mark
            Represent the mark of the player.
        cell_index: int
            Represent the position to play.
        before_state: "GameState"
            Represent the game state before the move.
        after_state: "GameState"
            Represent the game state after the move.
    """
    mark: Mark
    cell_index: int
    before_state: "GameState"
    after_state: "GameState"