2025-12-14 02:38:35 +07:00
|
|
|
|
import json
|
|
|
|
|
|
from openai import AsyncOpenAI
|
|
|
|
|
|
|
|
|
|
|
|
from app.core.config import settings
|
|
|
|
|
|
from app.schemas import ChallengeGenerated
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class GPTService:
|
|
|
|
|
|
"""Service for generating challenges using OpenAI GPT"""
|
|
|
|
|
|
|
|
|
|
|
|
def __init__(self):
|
|
|
|
|
|
self.client = AsyncOpenAI(api_key=settings.OPENAI_API_KEY)
|
|
|
|
|
|
|
|
|
|
|
|
async def generate_challenges(
|
|
|
|
|
|
self,
|
|
|
|
|
|
game_title: str,
|
|
|
|
|
|
game_genre: str | None = None
|
|
|
|
|
|
) -> list[ChallengeGenerated]:
|
|
|
|
|
|
"""
|
|
|
|
|
|
Generate challenges for a game using GPT.
|
|
|
|
|
|
|
|
|
|
|
|
Args:
|
|
|
|
|
|
game_title: Name of the game
|
|
|
|
|
|
game_genre: Optional genre of the game
|
|
|
|
|
|
|
|
|
|
|
|
Returns:
|
|
|
|
|
|
List of generated challenges
|
|
|
|
|
|
"""
|
|
|
|
|
|
genre_text = f" (жанр: {game_genre})" if game_genre else ""
|
|
|
|
|
|
|
2025-12-16 03:41:34 +07:00
|
|
|
|
prompt = f"""Ты — эксперт по видеоиграм. Сгенерируй 6 КОНКРЕТНЫХ челленджей для игры "{game_title}"{genre_text}.
|
|
|
|
|
|
|
|
|
|
|
|
ВАЖНО: Челленджи должны быть СПЕЦИФИЧНЫМИ для этой игры!
|
|
|
|
|
|
- Используй РЕАЛЬНЫЕ названия локаций, боссов, персонажей, миссий, уровней из игры
|
|
|
|
|
|
- Основывайся на том, какие челленджи РЕАЛЬНО делают игроки в этой игре (спидраны, no-hit боссов, сбор коллекционных предметов и т.д.)
|
|
|
|
|
|
- НЕ генерируй абстрактные челленджи типа "пройди уровень" или "убей 10 врагов"
|
|
|
|
|
|
|
|
|
|
|
|
Примеры ХОРОШИХ челленджей:
|
|
|
|
|
|
- Dark Souls: "Победи Орнштейна и Смоуга без призыва" / "Пройди Чумной город без отравления"
|
|
|
|
|
|
- GTA V: "Получи золото в миссии «Ювелирное дело»" / "Выиграй уличную гонку на Vinewood"
|
|
|
|
|
|
- Hollow Knight: "Победи Хорнет без получения урона" / "Найди все грибные споры в Грибных пустошах"
|
|
|
|
|
|
- Minecraft: "Убей Дракона Края за один визит в Энд" / "Построй работающую ферму железа"
|
|
|
|
|
|
|
|
|
|
|
|
Требования по сложности:
|
|
|
|
|
|
- 2 лёгких (15-30 мин): простые задачи, знакомство с игрой
|
|
|
|
|
|
- 2 средних (1-2 часа): требуют навыка или исследования
|
|
|
|
|
|
- 2 сложных (3+ часа): серьёзный челлендж, достижения, полное прохождение
|
|
|
|
|
|
|
|
|
|
|
|
Формат ответа — JSON:
|
|
|
|
|
|
- title: название на русском (до 50 символов), конкретное и понятное
|
|
|
|
|
|
- description: что именно сделать (1-2 предложения), с деталями из игры
|
|
|
|
|
|
- type: completion | no_death | speedrun | collection | achievement | challenge_run
|
|
|
|
|
|
- difficulty: easy | medium | hard
|
|
|
|
|
|
- points: easy=20-40, medium=45-75, hard=90-150
|
|
|
|
|
|
- estimated_time: время в минутах
|
|
|
|
|
|
- proof_type: screenshot | video | steam
|
|
|
|
|
|
- proof_hint: ЧТО КОНКРЕТНО должно быть видно на скриншоте/видео (экран победы, достижение, локация и т.д.)
|
|
|
|
|
|
|
|
|
|
|
|
Ответь ТОЛЬКО JSON:
|
2025-12-14 02:38:35 +07:00
|
|
|
|
{{"challenges": [{{"title": "...", "description": "...", "type": "...", "difficulty": "...", "points": 50, "estimated_time": 30, "proof_type": "...", "proof_hint": "..."}}]}}"""
|
|
|
|
|
|
|
|
|
|
|
|
response = await self.client.chat.completions.create(
|
2025-12-16 03:41:34 +07:00
|
|
|
|
model="gpt-5-mini",
|
2025-12-14 02:38:35 +07:00
|
|
|
|
messages=[{"role": "user", "content": prompt}],
|
|
|
|
|
|
response_format={"type": "json_object"},
|
2025-12-16 03:41:34 +07:00
|
|
|
|
temperature=0.8,
|
|
|
|
|
|
max_tokens=2500,
|
2025-12-14 02:38:35 +07:00
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
content = response.choices[0].message.content
|
|
|
|
|
|
data = json.loads(content)
|
|
|
|
|
|
|
|
|
|
|
|
challenges = []
|
|
|
|
|
|
for ch in data.get("challenges", []):
|
|
|
|
|
|
# Validate and normalize type
|
|
|
|
|
|
ch_type = ch.get("type", "completion")
|
|
|
|
|
|
if ch_type not in ["completion", "no_death", "speedrun", "collection", "achievement", "challenge_run"]:
|
|
|
|
|
|
ch_type = "completion"
|
|
|
|
|
|
|
|
|
|
|
|
# Validate difficulty
|
|
|
|
|
|
difficulty = ch.get("difficulty", "medium")
|
|
|
|
|
|
if difficulty not in ["easy", "medium", "hard"]:
|
|
|
|
|
|
difficulty = "medium"
|
|
|
|
|
|
|
|
|
|
|
|
# Validate proof_type
|
|
|
|
|
|
proof_type = ch.get("proof_type", "screenshot")
|
|
|
|
|
|
if proof_type not in ["screenshot", "video", "steam"]:
|
|
|
|
|
|
proof_type = "screenshot"
|
|
|
|
|
|
|
2025-12-16 03:06:26 +07:00
|
|
|
|
# Validate points based on difficulty
|
|
|
|
|
|
points = ch.get("points", 30)
|
2025-12-14 02:38:35 +07:00
|
|
|
|
if not isinstance(points, int) or points < 1:
|
2025-12-16 03:06:26 +07:00
|
|
|
|
points = 30
|
|
|
|
|
|
# Clamp points to expected ranges
|
|
|
|
|
|
if difficulty == "easy":
|
|
|
|
|
|
points = max(20, min(40, points))
|
|
|
|
|
|
elif difficulty == "medium":
|
|
|
|
|
|
points = max(45, min(75, points))
|
|
|
|
|
|
elif difficulty == "hard":
|
|
|
|
|
|
points = max(90, min(150, points))
|
2025-12-14 02:38:35 +07:00
|
|
|
|
|
|
|
|
|
|
challenges.append(ChallengeGenerated(
|
|
|
|
|
|
title=ch.get("title", "Unnamed Challenge")[:100],
|
|
|
|
|
|
description=ch.get("description", "Complete the challenge"),
|
|
|
|
|
|
type=ch_type,
|
|
|
|
|
|
difficulty=difficulty,
|
|
|
|
|
|
points=points,
|
|
|
|
|
|
estimated_time=ch.get("estimated_time"),
|
|
|
|
|
|
proof_type=proof_type,
|
|
|
|
|
|
proof_hint=ch.get("proof_hint"),
|
|
|
|
|
|
))
|
|
|
|
|
|
|
|
|
|
|
|
return challenges
|