Увеличен лимит очков до 1000 и добавлена документация

- Максимум очков за челлендж/прохождение: 500 → 1000
- Добавлена документация по системе типов игр (docs/game-types.md)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2025-12-30 19:49:12 +03:00
parent 89dbe2c018
commit 1e751f7af3
4 changed files with 251 additions and 9 deletions

View File

@@ -19,7 +19,7 @@ class ChallengeBase(BaseModel):
description: str = Field(..., min_length=1) description: str = Field(..., min_length=1)
type: ChallengeType type: ChallengeType
difficulty: Difficulty 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 estimated_time: int | None = Field(None, ge=1) # minutes
proof_type: ProofType proof_type: ProofType
proof_hint: str | None = None proof_hint: str | None = None
@@ -34,7 +34,7 @@ class ChallengeUpdate(BaseModel):
description: str | None = None description: str | None = None
type: ChallengeType | None = None type: ChallengeType | None = None
difficulty: Difficulty | 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 estimated_time: int | None = None
proof_type: ProofType | None = None proof_type: ProofType | None = None
proof_hint: str | None = None proof_hint: str | None = None

View File

@@ -20,7 +20,7 @@ class GameCreate(GameBase):
game_type: GameType = GameType.CHALLENGES 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_description: str | None = None
playthrough_proof_type: ProofType | None = None playthrough_proof_type: ProofType | None = None
playthrough_proof_hint: str | None = None playthrough_proof_hint: str | None = None
@@ -46,7 +46,7 @@ class GameUpdate(BaseModel):
game_type: GameType | None = None 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_description: str | None = None
playthrough_proof_type: ProofType | None = None playthrough_proof_type: ProofType | None = None
playthrough_proof_hint: str | None = None playthrough_proof_hint: str | None = None

242
docs/game-types.md Normal file
View File

@@ -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 │
│ • Загрузка пруфа │
│ • Начисление очков │
│ • Увеличение серии │
└─────────────────────────────────────────────────┘
```

View File

@@ -949,7 +949,7 @@ export function LobbyPage() {
value={editChallenge.points} value={editChallenge.points}
onChange={(e) => setEditChallenge(prev => ({ ...prev, points: parseInt(e.target.value) || 0 }))} onChange={(e) => setEditChallenge(prev => ({ ...prev, points: parseInt(e.target.value) || 0 }))}
min={1} min={1}
max={500} max={1000}
/> />
</div> </div>
<div> <div>
@@ -1109,7 +1109,7 @@ export function LobbyPage() {
value={newChallenge.points} value={newChallenge.points}
onChange={(e) => setNewChallenge(prev => ({ ...prev, points: parseInt(e.target.value) || 0 }))} onChange={(e) => setNewChallenge(prev => ({ ...prev, points: parseInt(e.target.value) || 0 }))}
min={1} min={1}
max={500} max={1000}
/> />
</div> </div>
<div> <div>
@@ -1351,7 +1351,7 @@ export function LobbyPage() {
value={editChallenge.points} value={editChallenge.points}
onChange={(e) => setEditChallenge(prev => ({ ...prev, points: parseInt(e.target.value) || 0 }))} onChange={(e) => setEditChallenge(prev => ({ ...prev, points: parseInt(e.target.value) || 0 }))}
min={1} min={1}
max={500} max={1000}
/> />
</div> </div>
<div> <div>
@@ -1974,7 +1974,7 @@ export function LobbyPage() {
value={playthroughPoints} value={playthroughPoints}
onChange={(e) => setPlaythroughPoints(parseInt(e.target.value) || 50)} onChange={(e) => setPlaythroughPoints(parseInt(e.target.value) || 50)}
min={1} min={1}
max={500} max={1000}
/> />
</div> </div>
<div> <div>
@@ -2151,7 +2151,7 @@ export function LobbyPage() {
value={editPlaythroughPoints} value={editPlaythroughPoints}
onChange={(e) => setEditPlaythroughPoints(parseInt(e.target.value) || 50)} onChange={(e) => setEditPlaythroughPoints(parseInt(e.target.value) || 50)}
min={1} min={1}
max={500} max={1000}
/> />
</div> </div>
<div> <div>