Skip to content

Commit 855425c

Browse files
committed
feat: add basic entities
1 parent d19b22b commit 855425c

File tree

8 files changed

+371
-43
lines changed

8 files changed

+371
-43
lines changed

pyproject.toml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@ classifiers = ["Private :: Do Not Upload"]
1111
requires-python = "==3.13.*"
1212
dependencies = [
1313
"dishka==1.6.0",
14-
"effectt==0.2.0",
1514
"in-memory-db==0.3.0",
1615
"sqlalchemy==2.0.41",
1716
"psycopg[binary]==3.2.9",

src/ttt/entities/core.py

Lines changed: 243 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,243 @@
1+
from dataclasses import dataclass
2+
from enum import Enum, auto
3+
from uuid import UUID
4+
5+
from ttt.entities.math import (
6+
Matrix,
7+
Vector,
8+
)
9+
from ttt.entities.tools import Tracking, assert_, not_none
10+
11+
12+
@dataclass
13+
class User:
14+
_id: int
15+
_number_of_wins: int
16+
_number_of_draws: int
17+
_number_of_defeats: int
18+
_tracking: Tracking
19+
20+
def id(self) -> int:
21+
return self._id
22+
23+
def lose(self) -> None:
24+
self._number_of_defeats += 1
25+
self._tracking.register_mutated(self)
26+
27+
def win(self) -> None:
28+
self._number_of_wins += 1
29+
self._tracking.register_mutated(self)
30+
31+
def be_draw(self) -> None:
32+
self._number_of_draws += 1
33+
self._tracking.register_mutated(self)
34+
35+
36+
def create_user(id_: int, tracking: Tracking) -> User:
37+
user = User(id_, 0, 0, 0, tracking)
38+
tracking.register_new(user)
39+
40+
return user
41+
42+
43+
class AlreadyFilledCellError(Exception): ...
44+
45+
46+
@dataclass
47+
class Cell:
48+
_id: UUID
49+
_game_id: UUID
50+
_board_position: Vector
51+
_filler_id: int | None
52+
_tracking: Tracking
53+
54+
def board_position(self) -> Vector:
55+
return self._board_position
56+
57+
def is_filled(self) -> bool:
58+
return self._filler_id is not None
59+
60+
def filler_id(self) -> int | None:
61+
return self._filler_id
62+
63+
def fill(self, filler_id: int) -> None:
64+
"""
65+
:raises ttt.entities.core.AlreadyFilledCellError:
66+
"""
67+
68+
assert_(not self.is_filled(), else_=AlreadyFilledCellError)
69+
self._filler_id = filler_id
70+
self._tracking.register_mutated(self)
71+
72+
73+
class GameState(Enum):
74+
wait_player1 = auto()
75+
wait_player2 = auto()
76+
completed = auto()
77+
78+
79+
type Board = Matrix[Cell]
80+
81+
82+
def is_board_standard(board: Board) -> bool:
83+
return board.column_size() == board.line_size() == 3 # noqa: PLR2004
84+
85+
86+
@dataclass(frozen=True)
87+
class GameResult:
88+
winner_id: int
89+
90+
91+
class NotStandardBoardError(Exception): ...
92+
93+
94+
class InvalidCellOrderError(Exception): ...
95+
96+
97+
class CompletedGameError(Exception): ...
98+
99+
100+
class NoCellError(Exception): ...
101+
102+
103+
class NotPlayerError(Exception): ...
104+
105+
106+
class NotCurrentPlayerError(Exception): ...
107+
108+
109+
@dataclass
110+
class Game:
111+
"""
112+
:raises ttt.entities.core.NotStandardBoardError:
113+
:raises ttt.entities.core.InvalidCellOrderError:
114+
"""
115+
116+
_id: UUID
117+
_player1: User
118+
_player2: User
119+
_board: Matrix[Cell]
120+
_number_of_unfilled_cells: int
121+
_result: GameResult
122+
_state: GameState
123+
_tracking: Tracking
124+
125+
def __post_init__(self) -> None:
126+
assert_(is_board_standard(self._board), else_=NotStandardBoardError)
127+
128+
is_cell_order_ok = all(
129+
self._board[x, y].board_position() == (x, y)
130+
for x in range(self._board.line_size())
131+
for y in range(self._board.column_size())
132+
)
133+
assert_(is_cell_order_ok, else_=InvalidCellOrderError)
134+
135+
def fill_cell(
136+
self, cell_x: int, cell_y: int, user_id: int,
137+
) -> GameResult | None:
138+
"""
139+
:raises ttt.entities.core.CompletedGameError:
140+
:raises ttt.entities.core.NotPlayerError:
141+
:raises ttt.entities.core.NotCurrentPlayerError:
142+
:raises ttt.entities.core.NoCellError:
143+
:raises ttt.entities.core.AlreadyFilledCellError:
144+
"""
145+
146+
current_player = self._current_player()
147+
current_player = not_none(current_player, else_=CompletedGameError)
148+
149+
assert_(
150+
user_id == self._player1.id() or user_id == self._player2.id(),
151+
else_=NotPlayerError(),
152+
)
153+
assert_(current_player.id() == user_id, else_=NotCurrentPlayerError())
154+
155+
try:
156+
cell = self._board[cell_x, cell_y]
157+
except KeyError as error:
158+
raise NoCellError from error
159+
160+
cell.fill(user_id)
161+
self._number_of_unfilled_cells -= 1
162+
self._tracking.register_mutated(self)
163+
164+
if self._is_player_winner(current_player, cell_x, cell_y):
165+
not_current_player = not_none(self._not_current_player())
166+
167+
current_player.win()
168+
not_current_player.lose()
169+
170+
self._complete(current_player)
171+
return None
172+
173+
if not self._can_continue():
174+
not_current_player = not_none(self._not_current_player())
175+
176+
current_player.be_draw()
177+
not_current_player.be_draw()
178+
179+
self._complete(current_player)
180+
return None
181+
182+
self._wait_next_move()
183+
return None
184+
185+
def _can_continue(self) -> bool:
186+
return self._number_of_unfilled_cells >= 1
187+
188+
def _is_player_winner(self, player: User, cell_x: int, cell_y: int) -> bool:
189+
is_winner = all(
190+
self._board[cell_x, y].filler_id() == player.id()
191+
for y in range(self._board.column_size())
192+
)
193+
is_winner |= all(
194+
int(self._board[x, cell_y].filler_id() == player.id())
195+
for x in range(self._board.line_size())
196+
)
197+
198+
is_winner |= {player.id()} == {
199+
self._board[0, 0].filler_id(),
200+
self._board[1, 1].filler_id(),
201+
self._board[2, 2].filler_id(),
202+
}
203+
is_winner |= {player.id()} == {
204+
self._board[0, 2].filler_id(),
205+
self._board[1, 1].filler_id(),
206+
self._board[2, 0].filler_id(),
207+
}
208+
209+
return is_winner
210+
211+
def _current_player(self) -> User | None:
212+
match self._state:
213+
case GameState.wait_player1:
214+
return self._player1
215+
case GameState.wait_player2:
216+
return self._player2
217+
case GameState.completed:
218+
return None
219+
220+
def _not_current_player(self) -> User | None:
221+
match self._state:
222+
case GameState.wait_player1:
223+
return self._player2
224+
case GameState.wait_player2:
225+
return self._player1
226+
case GameState.completed:
227+
return None
228+
229+
def _wait_next_move(self) -> None:
230+
match self._state:
231+
case GameState.wait_player1:
232+
self._state = GameState.wait_player2
233+
case GameState.wait_player2:
234+
self._state = GameState.wait_player1
235+
case GameState.completed:
236+
raise ValueError
237+
238+
self._tracking.register_mutated(self)
239+
240+
def _complete(self, winner: User) -> None:
241+
self._result = GameResult(winner.id())
242+
self._state = GameState.completed
243+
self._tracking.register_mutated(self)

src/ttt/entities/core/__init__.py

Whitespace-only changes.

src/ttt/entities/core/user.py

Lines changed: 0 additions & 13 deletions
This file was deleted.

src/ttt/entities/math.py

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
from dataclasses import dataclass
2+
from typing import Any
3+
4+
from ttt.entities.tools import assert_
5+
6+
7+
@dataclass(frozen=True)
8+
class InconsistentMatrixError(Exception):
9+
matrix: "Matrix[Any]"
10+
11+
12+
type MatrixSize = tuple[int, int]
13+
type Vector = tuple[int, int]
14+
15+
16+
@dataclass
17+
class Matrix[T]:
18+
"""
19+
:raises ttt.entities.math.InconsistentMatrixError:
20+
"""
21+
22+
_columns: list[list[T]]
23+
24+
def size(self) -> MatrixSize:
25+
return self.line_size(), self.column_size()
26+
27+
def line_size(self) -> int:
28+
if not self._columns:
29+
return 0
30+
31+
return len(self._columns[0])
32+
33+
def column_size(self) -> int:
34+
return len(self._columns)
35+
36+
def __post_init__(self) -> None:
37+
x_size = self.line_size()
38+
39+
assert_(
40+
all(len(column) == x_size for column in self._columns),
41+
else_=InconsistentMatrixError(self),
42+
)
43+
44+
def __setitem__(self, x_and_y: Vector, value: T) -> None:
45+
x, y = x_and_y
46+
self._columns[y][x] = value
47+
48+
def __getitem__(self, x_and_y: Vector) -> T:
49+
x, y = x_and_y
50+
return self._columns[y][x]

src/ttt/entities/time/__init__.py

Whitespace-only changes.

src/ttt/entities/time/time.py

Lines changed: 0 additions & 29 deletions
This file was deleted.

0 commit comments

Comments
 (0)