diff --git a/backend/app/schemas/challenge.py b/backend/app/schemas/challenge.py index 3c068ea..56af620 100644 --- a/backend/app/schemas/challenge.py +++ b/backend/app/schemas/challenge.py @@ -19,7 +19,7 @@ class ChallengeBase(BaseModel): description: str = Field(..., min_length=1) type: ChallengeType difficulty: Difficulty - points: int = Field(..., ge=1, le=500) + points: int = Field(..., ge=1, le=1000) estimated_time: int | None = Field(None, ge=1) # minutes proof_type: ProofType proof_hint: str | None = None @@ -34,7 +34,7 @@ class ChallengeUpdate(BaseModel): description: str | None = None type: ChallengeType | None = None difficulty: Difficulty | None = None - points: int | None = Field(None, ge=1, le=500) + points: int | None = Field(None, ge=1, le=1000) estimated_time: int | None = None proof_type: ProofType | None = None proof_hint: str | None = None diff --git a/backend/app/schemas/game.py b/backend/app/schemas/game.py index 9aa3213..9afb5cd 100644 --- a/backend/app/schemas/game.py +++ b/backend/app/schemas/game.py @@ -20,7 +20,7 @@ class GameCreate(GameBase): game_type: GameType = GameType.CHALLENGES # Поля для типа "Прохождение" - playthrough_points: int | None = Field(None, ge=1, le=500) + playthrough_points: int | None = Field(None, ge=1, le=1000) playthrough_description: str | None = None playthrough_proof_type: ProofType | None = None playthrough_proof_hint: str | None = None @@ -46,7 +46,7 @@ class GameUpdate(BaseModel): game_type: GameType | None = None # Поля для типа "Прохождение" - playthrough_points: int | None = Field(None, ge=1, le=500) + playthrough_points: int | None = Field(None, ge=1, le=1000) playthrough_description: str | None = None playthrough_proof_type: ProofType | None = None playthrough_proof_hint: str | None = None diff --git a/docs/game-types.md b/docs/game-types.md new file mode 100644 index 0000000..3957dc3 --- /dev/null +++ b/docs/game-types.md @@ -0,0 +1,242 @@ +# Система типов игр + +## Обзор + +В системе существует два типа игр, определяющих логику выдачи заданий: + +| Тип | Значение | Описание | +|-----|----------|----------| +| **Челленджи** | `challenges` | При спине выдаётся один случайный челлендж из списка | +| **Прохождение** | `playthrough` | Нужно пройти игру целиком, челленджи становятся бонусными | + +--- + +## Модели данных + +### Game + +``` +game_type: str # "challenges" | "playthrough" +playthrough_points: int? # Очки за прохождение (только для playthrough) +playthrough_description: str? # Описание задания +playthrough_proof_type: str? # Тип пруфа: screenshot/video/steam +playthrough_proof_hint: str? # Подсказка для пруфа +``` + +### Assignment + +``` +challenge_id: int? # ID челленджа (для challenges) +game_id: int? # ID игры (для playthrough) +is_playthrough: bool # True если это прохождение +``` + +### BonusAssignment + +``` +main_assignment_id: int # Ссылка на основное задание (playthrough) +challenge_id: int # ID бонусного челленджа +status: str # "pending" | "completed" +proof_path: str? # Путь к файлу пруфа +proof_url: str? # URL пруфа +proof_comment: str? # Комментарий со ссылкой +points_earned: int # Заработанные очки +``` + +--- + +## Логика спина + +### Тип "Челленджи" (challenges) + +``` +1. Выбрать случайную игру из доступных +2. Отфильтровать уже выполненные челленджи этой игры +3. Выбрать случайный невыполненный челлендж +4. Создать Assignment с challenge_id +``` + +**Игра исключается из спина**, если все её челленджи выполнены. + +### Тип "Прохождение" (playthrough) + +``` +1. Выбрать случайную игру из доступных +2. Создать Assignment с game_id и is_playthrough=True +3. Создать BonusAssignment для каждого челленджа игры +4. События (Jackpot, Golden Hour и т.д.) ИГНОРИРУЮТСЯ +``` + +**Игра исключается из спина**, если есть Assignment со статусом COMPLETED или DROPPED. + +--- + +## Завершение заданий + +### Челлендж (challenges) + +``` +POST /marathons/{id}/complete-assignment +``` + +1. Загрузить пруф (файл или комментарий) +2. Начисляются очки челленджа × модификатор события +3. Увеличивается серия участника +4. Статус → COMPLETED + +### Прохождение (playthrough) + +``` +POST /marathons/{id}/complete-assignment +``` + +1. Загрузить пруф прохождения +2. Начисляются очки за прохождение (`playthrough_points`) +3. Бонусные очки добавляются из completed BonusAssignments +4. Увеличивается серия участника +5. Все pending BonusAssignments удаляются (больше нельзя выполнить) +6. Статус → COMPLETED + +### Бонусный челлендж + +``` +POST /marathons/{id}/assignments/{assignment_id}/bonus/{challenge_id}/complete +``` + +1. Доступно только пока основное задание ACTIVE +2. Загрузить пруф бонусного челленджа +3. BonusAssignment.status → COMPLETED +4. Очки накапливаются в BonusAssignment.points_earned +5. **Очки НЕ добавляются сразу** — добавятся при завершении основного задания + +**Исключение:** Если main assignment уже COMPLETED (перепрохождение после диспута), очки добавляются сразу. + +--- + +## Фильтрация игр для спина + +### Функция `get_available_games_for_participant` + +```python +for game in approved_games: + if game.game_type == "playthrough": + # Исключить если есть COMPLETED или DROPPED assignment + if has_finished_playthrough(participant, game): + continue + else: # challenges + # Исключить если ВСЕ челленджи выполнены + if all_challenges_completed(participant, game): + continue + + available.append(game) +``` + +--- + +## Система очков + +### Челлендж + +``` +base_points = challenge.points +modifier = event_modifier (если есть активное событие) +total = base_points × modifier +``` + +### Прохождение + +``` +base_points = game.playthrough_points +bonus_points = sum(bonus.points_earned for bonus in completed_bonuses) +total = base_points + bonus_points +``` + +**События НЕ влияют на очки за прохождение.** + +--- + +## Дроп задания + +### Челлендж + +- Штраф в очках (зависит от настроек марафона) +- Серия обнуляется +- Игра остаётся доступной (можно получить другой челлендж) + +### Прохождение + +- Штраф в очках +- Серия обнуляется +- **Игра исключается из спина навсегда** +- Все BonusAssignments удаляются + +--- + +## Диспуты + +### Оспаривание прохождения + +Если диспут признан недействительным: +1. Assignment → RETURNED +2. Вычитаются все очки (прохождение + бонусы) +3. Серия обнуляется +4. Все BonusAssignments сбрасываются в PENDING + +### Оспаривание бонуса + +Если диспут признан недействительным: +1. BonusAssignment → PENDING +2. Вычитаются очки бонуса +3. Proof данные очищаются +4. Можно попробовать выполнить заново + +--- + +## API эндпоинты + +| Метод | Путь | Описание | +|-------|------|----------| +| POST | `/marathons/{id}/spin` | Крутить колесо | +| POST | `/marathons/{id}/complete-assignment` | Завершить основное задание | +| POST | `/marathons/{id}/assignments/{id}/bonus/{challenge_id}/complete` | Завершить бонус | +| GET | `/marathons/{id}/available-games` | Список доступных игр | +| GET | `/marathons/{id}/available-games-count` | Количество доступных игр | + +--- + +## Схема работы + +``` +┌─────────────────────────────────────────────────────────────────┐ +│ СПИН │ +└─────────────────────────────────────────────────────────────────┘ + │ + ┌───────────────┴───────────────┐ + ▼ ▼ + ┌─────────────────┐ ┌─────────────────┐ + │ PLAYTHROUGH │ │ CHALLENGES │ + └─────────────────┘ └─────────────────┘ + │ │ + ▼ ▼ + ┌─────────────────┐ ┌─────────────────┐ + │ Assignment │ │ Assignment │ + │ game_id = X │ │ challenge_id │ + │ is_playthrough │ │ = X │ + └─────────────────┘ └─────────────────┘ + │ │ + ▼ │ + ┌─────────────────┐ │ + │ BonusAssignment │ │ + │ × N (по числу │ │ + │ челленджей) │ │ + └─────────────────┘ │ + │ │ + ├───────────────────────────────┤ + ▼ ▼ + ┌─────────────────────────────────────────────────┐ + │ COMPLETE │ + │ • Загрузка пруфа │ + │ • Начисление очков │ + │ • Увеличение серии │ + └─────────────────────────────────────────────────┘ +``` diff --git a/frontend/src/pages/LobbyPage.tsx b/frontend/src/pages/LobbyPage.tsx index b52d20b..cdf72c7 100644 --- a/frontend/src/pages/LobbyPage.tsx +++ b/frontend/src/pages/LobbyPage.tsx @@ -949,7 +949,7 @@ export function LobbyPage() { value={editChallenge.points} onChange={(e) => setEditChallenge(prev => ({ ...prev, points: parseInt(e.target.value) || 0 }))} min={1} - max={500} + max={1000} />