Skip to content

Commit ed7d382

Browse files
committed
feat(infrastructure & presentation): make io (#7)
1 parent 385aeea commit ed7d382

File tree

19 files changed

+376
-15
lines changed

19 files changed

+376
-15
lines changed
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
from abc import ABC, abstractmethod
2+
from uuid import UUID
3+
4+
from ttt.entities.core.game.game import Game
5+
6+
7+
class GameAiGateway(ABC):
8+
@abstractmethod
9+
async def next_move_cell_number_int(
10+
self, game: Game, ai_id: UUID, /,
11+
) -> int | None: ...

src/ttt/application/game/common/ports/game_views.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,3 +61,8 @@ async def render_waiting_for_game_view(
6161
async def render_double_waiting_for_game_view(
6262
self, location: UserLocation,
6363
) -> None: ...
64+
65+
@abstractmethod
66+
async def render_waiting_for_ai_type_to_start_game_with_ai_view(
67+
self, location: UserLocation,
68+
) -> None: ...

src/ttt/application/game/game/make_move_in_game.py

Lines changed: 35 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
from ttt.application.common.ports.randoms import Randoms
66
from ttt.application.common.ports.transaction import Transaction
77
from ttt.application.common.ports.uuids import UUIDs
8+
from ttt.application.game.common.ports.game_ai_gateway import GameAiGateway
89
from ttt.application.game.common.ports.game_views import GameViews
910
from ttt.application.game.common.ports.games import Games
1011
from ttt.application.user.common.ports.users import Users
@@ -28,6 +29,7 @@ class MakeMoveInGame:
2829
users: Users
2930
uuids: UUIDs
3031
randoms: Randoms
32+
ai_gateway: GameAiGateway
3133
transaction: Transaction
3234

3335
async def __call__(
@@ -54,7 +56,7 @@ async def __call__(
5456

5557
try:
5658
tracking = Tracking()
57-
game.make_move(
59+
move = game.make_move(
5860
location.user_id,
5961
cell_number_int,
6062
game_result_id,
@@ -76,7 +78,35 @@ async def __call__(
7678
location, game,
7779
)
7880
else:
79-
await self.map_(tracking)
80-
await self.game_views.render_game_view_with_locations(
81-
locations, game,
82-
)
81+
if move.next_move_ai_id is not None:
82+
await self.game_views.render_game_view_with_locations(
83+
locations, game,
84+
)
85+
86+
game_result_id = await self.uuids.random_uuid()
87+
free_cell_random = await self.randoms.random()
88+
player_win_random = await self.randoms.random()
89+
ai_move_cell_number_int = (
90+
await self.ai_gateway.next_move_cell_number_int(
91+
game,
92+
move.next_move_ai_id,
93+
)
94+
)
95+
game.make_ai_move(
96+
move.next_move_ai_id,
97+
ai_move_cell_number_int,
98+
game_result_id,
99+
free_cell_random,
100+
player_win_random,
101+
tracking,
102+
)
103+
104+
await self.map_(tracking)
105+
await self.game_views.render_game_view_with_locations(
106+
locations, game,
107+
)
108+
else:
109+
await self.map_(tracking)
110+
await self.game_views.render_game_view_with_locations(
111+
locations, game,
112+
)

src/ttt/application/game/game_with_ai/__init__.py

Whitespace-only changes.
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
from dataclasses import dataclass
2+
3+
from ttt.application.common.ports.emojis import Emojis
4+
from ttt.application.common.ports.map import Map
5+
from ttt.application.common.ports.randoms import Randoms
6+
from ttt.application.common.ports.transaction import Transaction
7+
from ttt.application.common.ports.uuids import UUIDs
8+
from ttt.application.game.common.ports.game_views import GameViews
9+
from ttt.application.game.common.ports.games import Games
10+
from ttt.application.game.common.ports.waiting_locations import WaitingLocations
11+
from ttt.application.user.common.ports.user_views import UserViews
12+
from ttt.application.user.common.ports.users import Users
13+
from ttt.entities.core.game.ai import AiType
14+
from ttt.entities.core.game.game import start_game_with_ai
15+
from ttt.entities.core.user.location import UserLocation
16+
from ttt.entities.core.user.user import UserAlreadyInGameError
17+
from ttt.entities.tools.tracking import Tracking
18+
19+
20+
@dataclass(frozen=True, unsafe_hash=False)
21+
class StartGameWithAi:
22+
map_: Map
23+
uuids: UUIDs
24+
emojis: Emojis
25+
randoms: Randoms
26+
users: Users
27+
user_views: UserViews
28+
games: Games
29+
game_views: GameViews
30+
waiting_locations: WaitingLocations
31+
transaction: Transaction
32+
33+
async def __call__(self, location: UserLocation, ai_type: AiType) -> None:
34+
game_id = await self.uuids.random_uuid()
35+
ai_id = await self.uuids.random_uuid()
36+
cell_id_matrix = await self.uuids.random_uuid_matrix((3, 3))
37+
user_emoji = await self.emojis.random_emoji()
38+
ai_emoji = await self.emojis.random_emoji()
39+
player_order_random = await self.randoms.random()
40+
41+
async with self.transaction:
42+
user = await self.users.user_with_id(location.user_id)
43+
44+
if user is None:
45+
await self.user_views.render_user_is_not_registered_view(
46+
location,
47+
)
48+
return
49+
50+
try:
51+
tracking = Tracking()
52+
game = start_game_with_ai(
53+
cell_id_matrix,
54+
game_id,
55+
user,
56+
user_emoji,
57+
location.chat_id,
58+
ai_id,
59+
ai_type,
60+
ai_emoji,
61+
player_order_random,
62+
tracking,
63+
)
64+
except UserAlreadyInGameError:
65+
await self.game_views.render_user_already_in_game_views(
66+
[location],
67+
)
68+
else:
69+
await self.map_(tracking)
70+
await (
71+
self.game_views
72+
.render_started_game_view_with_locations(
73+
[location.game(game.id)], game,
74+
)
75+
)
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
from dataclasses import dataclass
2+
3+
from ttt.application.game.common.ports.game_views import GameViews
4+
from ttt.entities.core.user.location import UserLocation
5+
6+
7+
@dataclass(frozen=True, unsafe_hash=False)
8+
class WaitAiTypeToStartGameWithAi:
9+
game_views: GameViews
10+
11+
async def __call__(self, location: UserLocation) -> None:
12+
await (
13+
self.game_views
14+
.render_waiting_for_ai_type_to_start_game_with_ai_view(location)
15+
)

src/ttt/entities/core/game/game.py

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ class GameCancellationResult:
5555

5656
@dataclass(frozen=True)
5757
class UserMove:
58-
is_game_waiting_ai_move: bool
58+
next_move_ai_id: UUID | None
5959

6060

6161
@dataclass(frozen=True)
@@ -221,11 +221,12 @@ def make_move(
221221
)
222222

223223
next_move_player = self._current_player()
224-
225-
return UserMove(
226-
is_game_waiting_ai_move=isinstance(next_move_player, Ai),
224+
next_move_ai_id = (
225+
next_move_player.id if isinstance(next_move_player, Ai) else None
227226
)
228227

228+
return UserMove(next_move_ai_id=next_move_ai_id)
229+
229230
def make_ai_move( # noqa: PLR0913, PLR0917
230231
self,
231232
ai_id: UUID,
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 cast
3+
from uuid import UUID
4+
5+
from ttt.application.game.common.ports.game_ai_gateway import GameAiGateway
6+
from ttt.entities.core.game.ai import Ai, AiType
7+
from ttt.entities.core.game.game import Game
8+
from ttt.infrastructure.openai.gemini import Gemini
9+
from ttt.infrastructure.openai.promt import next_move_cell_number_promt
10+
11+
12+
@dataclass(frozen=True, unsafe_hash=False)
13+
class GeminiGameAiGateway(GameAiGateway):
14+
_gemini: Gemini
15+
16+
async def next_move_cell_number_int(
17+
self, game: Game, ai_id: UUID, /,
18+
) -> int | None:
19+
if game.player1.id == ai_id:
20+
ai = cast(Ai, game.player1)
21+
elif game.player2.id == ai_id:
22+
ai = cast(Ai, game.player2)
23+
else:
24+
raise ValueError
25+
26+
match ai.type:
27+
case AiType.gemini_2_0_flash:
28+
model = "gemini-2.0-flash"
29+
30+
response = await self._gemini.chat.completions.create(
31+
model=model,
32+
messages=[
33+
{
34+
"role": "user",
35+
"content": next_move_cell_number_promt(game, ai_id),
36+
},
37+
],
38+
temperature=1.5,
39+
)
40+
content = response.choices[0].message.content
41+
42+
if content is None:
43+
return None
44+
45+
content = content.strip()
46+
47+
try:
48+
return int(content)
49+
except ValueError:
50+
return None
Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,13 @@
1-
from openai import OpenAI
1+
from typing import NewType
22

3+
from openai import AsyncOpenAI
34

4-
def gemini(api_key: str, base_url: str) -> OpenAI:
5-
return OpenAI(
5+
6+
Gemini = NewType("Gemini", AsyncOpenAI)
7+
8+
9+
def gemini(api_key: str, base_url: str) -> Gemini:
10+
return Gemini(AsyncOpenAI(
611
api_key=api_key,
712
base_url=f"{base_url}/v1beta/openai/",
8-
)
13+
))
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
from uuid import UUID
2+
3+
from ttt.entities.core.game.game import Game
4+
from ttt.entities.math.vector import Vector
5+
6+
7+
type Promt = str
8+
9+
10+
def next_move_cell_number_promt(game: Game, ai_id: UUID) -> Promt:
11+
line1, line2, line3 = (
12+
"".join(
13+
_next_move_cell_number_promt_cell(game, ai_id, (x, y))
14+
for x in range(3)
15+
)
16+
for y in range(3)
17+
)
18+
19+
return f"""
20+
You are a tic-tac-toe user.
21+
You play for 1, the enemy for 0, _ - empty cells.
22+
Each number is a position on the board:
23+
123
24+
456
25+
789
26+
27+
What position will your move be if the board is:
28+
{line1}
29+
{line2}
30+
{line3}
31+
32+
Return only the position number
33+
"""
34+
35+
36+
def _next_move_cell_number_promt_cell(
37+
game: Game,
38+
ai_id: UUID,
39+
cell_position: Vector,
40+
) -> str:
41+
cell = game.board[cell_position]
42+
filler_id = cell.filler_id()
43+
44+
if filler_id == ai_id:
45+
return "1"
46+
if filler_id is None:
47+
return "_"
48+
49+
return "0"

0 commit comments

Comments
 (0)