Увеличен лимит очков до 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:
@@ -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
|
||||||
|
|||||||
@@ -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
242
docs/game-types.md
Normal 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 │
|
||||||
|
│ • Загрузка пруфа │
|
||||||
|
│ • Начисление очков │
|
||||||
|
│ • Увеличение серии │
|
||||||
|
└─────────────────────────────────────────────────┘
|
||||||
|
```
|
||||||
@@ -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>
|
||||||
|
|||||||
Reference in New Issue
Block a user