Skip to content

Commit 02838eb

Browse files
committed
feat(sqlalchemy): add parts for orm
1 parent a40251f commit 02838eb

File tree

7 files changed

+324
-171
lines changed

7 files changed

+324
-171
lines changed

pyproject.toml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,11 @@ dependencies = [
1515
"sqlalchemy==2.0.41",
1616
"psycopg[binary]==3.2.9",
1717
"alembic==1.16.1",
18-
"aiogram==3.20.0.post0",
18+
"alembic-postgresql-enum==1.7.0",
1919
"redis==6.2.0",
2020
"pydantic==2.10.6",
2121
"pydantic-settings[yaml]==2.9.1",
22+
"aiogram==3.20.0.post0",
2223
]
2324

2425
[dependency-groups]

src/ttt/entities/core.py

Lines changed: 75 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -16,23 +16,29 @@ class Player:
1616
number_of_wins: int
1717
number_of_draws: int
1818
number_of_defeats: int
19-
tracking: Tracking
19+
current_game_id: UUID | None
2020

21-
def lose(self) -> None:
21+
def be_in_game(self, game_id: UUID) -> None:
22+
self.current_game_id = game_id
23+
24+
def lose(self, tracking: Tracking) -> None:
25+
self.current_game_id = None
2226
self.number_of_defeats += 1
23-
self.tracking.register_mutated(self)
27+
tracking.register_mutated(self)
2428

25-
def win(self) -> None:
29+
def win(self, tracking: Tracking) -> None:
30+
self.current_game_id = None
2631
self.number_of_wins += 1
27-
self.tracking.register_mutated(self)
32+
tracking.register_mutated(self)
2833

29-
def be_draw(self) -> None:
34+
def be_draw(self, tracking: Tracking) -> None:
35+
self.current_game_id = None
3036
self.number_of_draws += 1
31-
self.tracking.register_mutated(self)
37+
tracking.register_mutated(self)
3238

3339

3440
def create_player(id_: int, tracking: Tracking) -> Player:
35-
player = Player(id_, 0, 0, 0, tracking)
41+
player = Player(id_, 0, 0, 0, None)
3642
tracking.register_new(player)
3743

3844
return player
@@ -47,19 +53,18 @@ class Cell:
4753
game_id: UUID
4854
board_position: Vector
4955
filler_id: int | None
50-
tracking: Tracking
5156

5257
def is_filled(self) -> bool:
5358
return self.filler_id is not None
5459

55-
def fill(self, filler_id: int) -> None:
60+
def fill(self, filler_id: int, tracking: Tracking) -> None:
5661
"""
5762
:raises ttt.entities.core.AlreadyFilledCellError:
5863
"""
5964

6065
assert_(not self.is_filled(), else_=AlreadyFilledCellError)
6166
self.filler_id = filler_id
62-
self.tracking.register_mutated(self)
67+
tracking.register_mutated(self)
6368

6469

6570
class GameState(Enum):
@@ -72,7 +77,7 @@ class GameState(Enum):
7277

7378

7479
def is_board_standard(board: Board) -> bool:
75-
return board.column_size() == board.line_size() == 3 # noqa: PLR2004
80+
return board.width() == board.height() == 3 # noqa: PLR2004
7681

7782

7883
class InvalidCellIDMatrixError(Exception): ...
@@ -91,7 +96,7 @@ def create_empty_board(
9196

9297
board = Matrix([
9398
[
94-
Cell(cell_id_matrix[x, y], game_id, (x, y), None, tracking)
99+
Cell(cell_id_matrix[x, y], game_id, Vector(x, y), None)
95100
for x in range(3)
96101
]
97102
for y in range(3)
@@ -105,6 +110,8 @@ def create_empty_board(
105110

106111
@dataclass(frozen=True)
107112
class GameResult:
113+
id: UUID
114+
game_id: UUID
108115
winner_id: int | None
109116

110117

@@ -120,7 +127,9 @@ class InvalidCellOrderError(Exception): ...
120127
class InvalidNumberOfUnfilledCellsError(Exception): ...
121128

122129

123-
class CompletedGameError(Exception): ...
130+
@dataclass(frozen=True)
131+
class CompletedGameError(Exception):
132+
game_result: GameResult
124133

125134

126135
class NoCellError(Exception): ...
@@ -152,16 +161,15 @@ class Game:
152161
number_of_unfilled_cells: int
153162
result: GameResult | None
154163
state: GameState
155-
tracking: Tracking
156164

157165
def __post_init__(self) -> None:
158166
assert_(self.player1.id != self.player2.id, else_=OnePlayerError)
159167
assert_(is_board_standard(self.board), else_=NotStandardBoardError)
160168

161169
is_cell_order_ok = all(
162170
self.board[x, y].board_position == (x, y)
163-
for x in range(self.board.line_size())
164-
for y in range(self.board.column_size())
171+
for x in range(self.board.width())
172+
for y in range(self.board.height())
165173
)
166174
assert_(is_cell_order_ok, else_=InvalidCellOrderError)
167175

@@ -172,7 +180,11 @@ def __post_init__(self) -> None:
172180
)
173181

174182
def make_move(
175-
self, player_id: int, cell_position: Vector,
183+
self,
184+
player_id: int,
185+
cell_position: Vector,
186+
game_result_id: UUID,
187+
tracking: Tracking,
176188
) -> GameResult | None:
177189
"""
178190
:raises ttt.entities.core.CompletedGameError:
@@ -183,37 +195,39 @@ def make_move(
183195
"""
184196

185197
current_player = self._current_player()
186-
current_player = not_none(current_player, else_=CompletedGameError)
198+
199+
if current_player is None:
200+
raise CompletedGameError(not_none(self.result))
187201

188202
assert_(
189203
player_id in {self.player1.id, self.player2.id},
190204
else_=NotPlayerError(),
191205
)
192206
assert_(current_player.id == player_id, else_=NotCurrentPlayerError())
193207

194-
self._fill_cell(cell_position, player_id)
208+
self._fill_cell(cell_position, player_id, tracking)
195209

196210
if self._is_player_winner(current_player, cell_position):
197211
not_current_player = not_none(self._not_current_player())
198212

199-
current_player.win()
200-
not_current_player.lose()
213+
current_player.win(tracking)
214+
not_current_player.lose(tracking)
201215

202-
return self._complete(current_player)
216+
return self._complete(game_result_id, current_player, tracking)
203217

204218
if not self._can_continue():
205219
not_current_player = not_none(self._not_current_player())
206220

207-
current_player.be_draw()
208-
not_current_player.be_draw()
221+
current_player.be_draw(tracking)
222+
not_current_player.be_draw(tracking)
209223

210-
return self._complete(None)
224+
return self._complete(game_result_id, None, tracking)
211225

212-
self._wait_next_move()
226+
self._wait_next_move(tracking)
213227
return None
214228

215229
def _fill_cell(
216-
self, cell_position: Vector, player_id: int,
230+
self, cell_position: Vector, player_id: int, tracking: Tracking,
217231
) -> GameResult | None:
218232
"""
219233
:raises ttt.entities.core.NoCellError:
@@ -225,9 +239,9 @@ def _fill_cell(
225239
except IndexError as error:
226240
raise NoCellError from error
227241

228-
cell.fill(player_id)
242+
cell.fill(player_id, tracking)
229243
self.number_of_unfilled_cells -= 1
230-
self.tracking.register_mutated(self)
244+
tracking.register_mutated(self)
231245

232246
def _can_continue(self) -> bool:
233247
return not self._is_board_filled()
@@ -240,11 +254,11 @@ def _is_player_winner(self, player: Player, cell_position: Vector) -> bool:
240254

241255
is_winner = all(
242256
self.board[cell_x, y].filler_id == player.id
243-
for y in range(self.board.column_size())
257+
for y in range(self.board.height())
244258
)
245259
is_winner |= all(
246260
int(self.board[x, cell_y].filler_id == player.id)
247-
for x in range(self.board.line_size())
261+
for x in range(self.board.width())
248262
)
249263

250264
is_winner |= {player.id} == {
@@ -278,7 +292,7 @@ def _not_current_player(self) -> Player | None:
278292
case GameState.completed:
279293
return None
280294

281-
def _wait_next_move(self) -> None:
295+
def _wait_next_move(self, tracking: Tracking) -> None:
282296
match self.state:
283297
case GameState.wait_player1:
284298
self.state = GameState.wait_player2
@@ -287,16 +301,25 @@ def _wait_next_move(self) -> None:
287301
case GameState.completed:
288302
raise ValueError
289303

290-
self.tracking.register_mutated(self)
304+
tracking.register_mutated(self)
291305

292-
def _complete(self, winner: Player | None) -> GameResult:
293-
self.result = GameResult(None if winner is None else winner.id)
306+
def _complete(
307+
self, game_result_id: UUID, winner: Player | None, tracking: Tracking,
308+
) -> GameResult:
309+
self.result = GameResult(
310+
game_result_id, self.id, None if winner is None else winner.id,
311+
)
294312
self.state = GameState.completed
295-
self.tracking.register_mutated(self)
313+
tracking.register_mutated(self)
296314

297315
return self.result
298316

299317

318+
@dataclass(frozen=True)
319+
class PlayerAlreadyInGameError(Exception):
320+
players: tuple[Player, ...]
321+
322+
300323
def start_game(
301324
cell_id_matrix: Matrix[UUID],
302325
game_id: UUID,
@@ -305,9 +328,21 @@ def start_game(
305328
tracking: Tracking,
306329
) -> Game:
307330
"""
331+
:raises ttt.entities.core.PlayerAlreadyInGameError:
308332
:raises ttt.entities.core.InvalidCellIDMatrixError:
333+
:raises ttt.entities.core.OnePlayerError:
309334
"""
310335

336+
players_in_game = tuple(
337+
player
338+
for player in (player1, player2)
339+
if player.current_game_id is not None
340+
)
341+
assert_(
342+
not players_in_game,
343+
else_=PlayerAlreadyInGameError(players_in_game),
344+
)
345+
311346
board = create_empty_board(cell_id_matrix, game_id, tracking)
312347

313348
game = Game(
@@ -318,8 +353,10 @@ def start_game(
318353
number_of_unfilled_cells(board),
319354
None,
320355
GameState.wait_player1,
321-
tracking,
322356
)
323357
tracking.register_new(game)
324358

359+
player1.be_in_game(game_id)
360+
player2.be_in_game(game_id)
361+
325362
return game

src/ttt/entities/math.py

Lines changed: 20 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
from collections.abc import Iterable, Iterator
22
from dataclasses import dataclass
3-
from typing import Any
3+
from typing import Any, NamedTuple
44

55
from ttt.entities.tools import assert_
66

@@ -11,7 +11,11 @@ class InconsistentMatrixError(Exception):
1111

1212

1313
type MatrixSize = tuple[int, int]
14-
type Vector = tuple[int, int]
14+
15+
16+
class Vector(NamedTuple):
17+
x: int
18+
y: int
1519

1620

1721
@dataclass
@@ -20,46 +24,46 @@ class Matrix[T]:
2024
:raises ttt.entities.math.InconsistentMatrixError:
2125
"""
2226

23-
columns: list[list[T]]
27+
lines: list[list[T]]
2428

2529
def size(self) -> MatrixSize:
26-
return self.line_size(), self.column_size()
30+
return self.width(), self.height()
2731

28-
def line_size(self) -> int:
29-
if not self.columns:
32+
def width(self) -> int:
33+
if not self.lines:
3034
return 0
3135

32-
return len(self.columns[0])
36+
return len(self.lines[0])
3337

34-
def column_size(self) -> int:
35-
return len(self.columns)
38+
def height(self) -> int:
39+
return len(self.lines)
3640

3741
def __post_init__(self) -> None:
38-
line_size = self.line_size()
42+
line_size = self.width()
3943

4044
assert_(
41-
all(len(column) == line_size for column in self.columns),
45+
all(len(column) == line_size for column in self.lines),
4246
else_=InconsistentMatrixError(self),
4347
)
4448

45-
def __setitem__(self, x_and_y: Vector, value: T) -> None:
49+
def __setitem__(self, x_and_y: tuple[int, int], value: T) -> None:
4650
"""
4751
:raises IndexError:
4852
"""
4953

5054
x, y = x_and_y
51-
self.columns[y][x] = value
55+
self.lines[y][x] = value
5256

53-
def __getitem__(self, x_and_y: Vector) -> T:
57+
def __getitem__(self, x_and_y: tuple[int, int]) -> T:
5458
"""
5559
:raises IndexError:
5660
"""
5761

5862
x, y = x_and_y
59-
return self.columns[y][x]
63+
return self.lines[y][x]
6064

6165
def __iter__(self) -> Iterator[Iterable[T]]:
62-
yield from self.columns
66+
yield from self.lines
6367

6468

6569
def matrix_with_size[T](size: MatrixSize, zero: T) -> Matrix[T]:

0 commit comments

Comments
 (0)