initial
This commit is contained in:
4
backend/app/services/__init__.py
Normal file
4
backend/app/services/__init__.py
Normal file
@@ -0,0 +1,4 @@
|
||||
from app.services.points import PointsService
|
||||
from app.services.gpt import GPTService
|
||||
|
||||
__all__ = ["PointsService", "GPTService"]
|
||||
96
backend/app/services/gpt.py
Normal file
96
backend/app/services/gpt.py
Normal file
@@ -0,0 +1,96 @@
|
||||
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 ""
|
||||
|
||||
prompt = f"""Для видеоигры "{game_title}"{genre_text} сгенерируй 6 челленджей для игрового марафона.
|
||||
|
||||
Требования:
|
||||
- 2 лёгких челленджа (15-30 минут игры)
|
||||
- 2 средних челленджа (1-2 часа игры)
|
||||
- 2 сложных челленджа (3+ часов или высокая сложность)
|
||||
|
||||
Для каждого челленджа укажи:
|
||||
- title: короткое название на русском (до 50 символов)
|
||||
- description: что нужно сделать на русском (1-2 предложения)
|
||||
- type: один из [completion, no_death, speedrun, collection, achievement, challenge_run]
|
||||
- difficulty: easy/medium/hard
|
||||
- points: очки (easy: 30-50, medium: 60-100, hard: 120-200)
|
||||
- estimated_time: примерное время в минутах
|
||||
- proof_type: screenshot/video/steam (что лучше подойдёт для проверки)
|
||||
- proof_hint: что должно быть на скриншоте/видео для подтверждения на русском
|
||||
|
||||
Ответь ТОЛЬКО валидным JSON объектом с ключом "challenges" содержащим массив челленджей.
|
||||
Пример формата:
|
||||
{{"challenges": [{{"title": "...", "description": "...", "type": "...", "difficulty": "...", "points": 50, "estimated_time": 30, "proof_type": "...", "proof_hint": "..."}}]}}"""
|
||||
|
||||
response = await self.client.chat.completions.create(
|
||||
model="gpt-4o-mini",
|
||||
messages=[{"role": "user", "content": prompt}],
|
||||
response_format={"type": "json_object"},
|
||||
temperature=0.7,
|
||||
max_tokens=2000,
|
||||
)
|
||||
|
||||
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"
|
||||
|
||||
# Validate points
|
||||
points = ch.get("points", 50)
|
||||
if not isinstance(points, int) or points < 1:
|
||||
points = 50
|
||||
|
||||
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
|
||||
55
backend/app/services/points.py
Normal file
55
backend/app/services/points.py
Normal file
@@ -0,0 +1,55 @@
|
||||
class PointsService:
|
||||
"""Service for calculating points and penalties"""
|
||||
|
||||
STREAK_MULTIPLIERS = {
|
||||
0: 0.0,
|
||||
1: 0.0,
|
||||
2: 0.1,
|
||||
3: 0.2,
|
||||
4: 0.3,
|
||||
}
|
||||
MAX_STREAK_MULTIPLIER = 0.4
|
||||
|
||||
DROP_PENALTIES = {
|
||||
0: 0, # First drop is free
|
||||
1: 10,
|
||||
2: 25,
|
||||
}
|
||||
MAX_DROP_PENALTY = 50
|
||||
|
||||
def calculate_completion_points(
|
||||
self,
|
||||
base_points: int,
|
||||
current_streak: int
|
||||
) -> tuple[int, int]:
|
||||
"""
|
||||
Calculate points earned for completing a challenge.
|
||||
|
||||
Args:
|
||||
base_points: Base points for the challenge
|
||||
current_streak: Current streak before this completion
|
||||
|
||||
Returns:
|
||||
Tuple of (total_points, streak_bonus)
|
||||
"""
|
||||
multiplier = self.STREAK_MULTIPLIERS.get(
|
||||
current_streak,
|
||||
self.MAX_STREAK_MULTIPLIER
|
||||
)
|
||||
bonus = int(base_points * multiplier)
|
||||
return base_points + bonus, bonus
|
||||
|
||||
def calculate_drop_penalty(self, consecutive_drops: int) -> int:
|
||||
"""
|
||||
Calculate penalty for dropping a challenge.
|
||||
|
||||
Args:
|
||||
consecutive_drops: Number of drops since last completion
|
||||
|
||||
Returns:
|
||||
Penalty points to subtract
|
||||
"""
|
||||
return self.DROP_PENALTIES.get(
|
||||
consecutive_drops,
|
||||
self.MAX_DROP_PENALTY
|
||||
)
|
||||
Reference in New Issue
Block a user