Files
game-marathon/docs/tz-game-types.md
mamonov.ep 89dbe2c018 Улучшение системы оспариваний и исправления
- Оспаривания теперь требуют решения админа после 24ч голосования
  - Можно повторно оспаривать после разрешённых споров
  - Исправлены бонусные очки при перепрохождении после оспаривания
  - Сброс серии при невалидном пруфе
  - Колесо показывает только доступные игры
  - Rate limiting только через backend (RATE_LIMIT_ENABLED)
2025-12-29 22:23:34 +03:00

38 KiB
Raw Permalink Blame History

ТЗ: Типы игр "Прохождение" и "Челленджи"

Описание задачи

Добавить систему типов для игр, которая определяет логику выпадения заданий при спине колеса.

Два типа игр:

Тип Название Поведение при выпадении
playthrough Прохождение Основное задание — пройти игру. Челленджи становятся дополнительными заданиями
challenges Челленджи Выдаётся случайный челлендж из списка челленджей игры (текущее поведение)

Детальное описание логики

Тип "Прохождение" (playthrough)

При создании игры с типом "Прохождение" указываются дополнительные поля:

  • Очки за прохождение (playthrough_points) — количество очков за прохождение игры
  • Описание прохождения (playthrough_description) — описание задания (например: "Пройти основной сюжет игры")
  • Тип пруфа (playthrough_proof_type) — screenshot / video / steam
  • Подсказка для пруфа (playthrough_proof_hint) — опционально (например: "Скриншот финальных титров")

При выпадении игры с типом "Прохождение":

  1. Основное задание: Пройти игру (очки и описание берутся из полей игры)
  2. Дополнительные задания: Все челленджи игры становятся опциональными бонусными заданиями
  3. Пруфы:
    • Требуется отдельный пруф на прохождение игры (тип из playthrough_proof_type)
    • Для каждого бонусного челленджа тоже требуется пруф (по типу челленджа)
    • Прикрепление файла не обязательно — можно отправить только комментарий со ссылкой на видео
  4. Система очков:
    • За основное прохождение — playthrough_points (указанные при создании)
    • За каждый выполненный доп. челлендж — очки челленджа
  5. Завершение: Задание считается выполненным после прохождения основной игры. Доп. челленджи не обязательны — можно выполнять параллельно или игнорировать

Тип "Челленджи" (challenges)

При выпадении игры с типом "Челленджи":

  1. Выбирается один случайный челлендж из списка челленджей игры
  2. Участник выполняет только этот челлендж
  3. Логика остаётся без изменений (текущее поведение системы)

Фильтрация игр при спине

При выборе игры для спина необходимо исключать уже пройденные/дропнутые игры:

Тип игры Условие исключения из спина
playthrough Игра исключается, если участник завершил ИЛИ дропнул прохождение этой игры
challenges Игра исключается, только если участник выполнил все челленджи этой игры

Логика:

Для каждой игры в марафоне:
  ЕСЛИ game_type == "playthrough":
    Проверить: есть ли Assignment с is_playthrough=True для этой игры
              со статусом COMPLETED или DROPPED?
    Если да → исключить игру

  ЕСЛИ game_type == "challenges":
    Получить все челленджи игры
    Получить все завершённые Assignment участника для этих челленджей
    Если количество завершённых == количество челленджей → исключить игру

Важно: Если все игры исключены (всё пройдено), спин должен вернуть ошибку или специальный статус "Все игры пройдены!"

Бонусные челленджи

Бонусные челленджи доступны только пока основное задание активно:

  • После завершения прохождения — бонусные челленджи недоступны
  • После дропа прохождения — бонусные челленджи недоступны
  • Нельзя вернуться к бонусным челленджам позже

Взаимодействие с событиями

Все события игнорируются при выпадении игры с типом playthrough:

Событие Поведение для playthrough
JACKPOT (x3 за hard) Игнорируется
GAME_CHOICE (выбор из 3) Игнорируется
GOLDEN_HOUR (x1.5) Игнорируется
DOUBLE_RISK (x0.5, бесплатный дроп) Игнорируется
COMMON_ENEMY Игнорируется
SWAP Игнорируется

Игрок получает стандартные очки playthrough_points без модификаторов.


Изменения в Backend

1. Модель Game (backend/app/models/game.py)

Добавить поля для типа игры и прохождения:

class GameType(str, Enum):
    PLAYTHROUGH = "playthrough"  # Прохождение
    CHALLENGES = "challenges"    # Челленджи

class Game(Base):
    # ... существующие поля ...

    # Тип игры
    game_type: Mapped[str] = mapped_column(
        String(20),
        default=GameType.CHALLENGES.value,
        nullable=False
    )

    # Поля для типа "Прохождение" (nullable, заполняются только для playthrough)
    playthrough_points: Mapped[int | None] = mapped_column(
        Integer,
        nullable=True
    )
    playthrough_description: Mapped[str | None] = mapped_column(
        Text,
        nullable=True
    )
    playthrough_proof_type: Mapped[str | None] = mapped_column(
        String(20),  # screenshot, video, steam
        nullable=True
    )
    playthrough_proof_hint: Mapped[str | None] = mapped_column(
        Text,
        nullable=True
    )

2. Схемы Pydantic (backend/app/schemas/)

Обновить схемы для Game:

# schemas/game.py
class GameType(str, Enum):
    PLAYTHROUGH = "playthrough"
    CHALLENGES = "challenges"

class GameCreate(BaseModel):
    # ... существующие поля ...
    game_type: GameType = GameType.CHALLENGES

    # Поля для типа "Прохождение"
    playthrough_points: int | None = None
    playthrough_description: str | None = None
    playthrough_proof_type: ProofType | None = None
    playthrough_proof_hint: str | None = None

    @model_validator(mode='after')
    def validate_playthrough_fields(self) -> Self:
        if self.game_type == GameType.PLAYTHROUGH:
            if self.playthrough_points is None:
                raise ValueError('playthrough_points обязателен для типа "Прохождение"')
            if self.playthrough_description is None:
                raise ValueError('playthrough_description обязателен для типа "Прохождение"')
            if self.playthrough_proof_type is None:
                raise ValueError('playthrough_proof_type обязателен для типа "Прохождение"')
            if self.playthrough_points < 1 or self.playthrough_points > 500:
                raise ValueError('playthrough_points должен быть от 1 до 500')
        return self

class GameResponse(BaseModel):
    # ... существующие поля ...
    game_type: GameType
    playthrough_points: int | None
    playthrough_description: str | None
    playthrough_proof_type: ProofType | None
    playthrough_proof_hint: str | None

class GameUpdate(BaseModel):
    """Схема для редактирования игры"""
    title: str | None = None
    download_url: str | None = None
    genre: str | None = None
    game_type: GameType | None = None
    playthrough_points: int | None = None
    playthrough_description: str | None = None
    playthrough_proof_type: ProofType | None = None
    playthrough_proof_hint: str | None = None

    @model_validator(mode='after')
    def validate_playthrough_fields(self) -> Self:
        # Валидация только если меняем на playthrough
        if self.game_type == GameType.PLAYTHROUGH:
            if self.playthrough_points is not None:
                if self.playthrough_points < 1 or self.playthrough_points > 500:
                    raise ValueError('playthrough_points должен быть от 1 до 500')
        return self

3. Миграция Alembic

# Новая миграция
def upgrade():
    # Тип игры
    op.add_column('games', sa.Column(
        'game_type',
        sa.String(20),
        nullable=False,
        server_default='challenges'
    ))

    # Поля для прохождения
    op.add_column('games', sa.Column(
        'playthrough_points',
        sa.Integer(),
        nullable=True
    ))
    op.add_column('games', sa.Column(
        'playthrough_description',
        sa.Text(),
        nullable=True
    ))
    op.add_column('games', sa.Column(
        'playthrough_proof_type',
        sa.String(20),
        nullable=True
    ))
    op.add_column('games', sa.Column(
        'playthrough_proof_hint',
        sa.Text(),
        nullable=True
    ))

def downgrade():
    op.drop_column('games', 'playthrough_proof_hint')
    op.drop_column('games', 'playthrough_proof_type')
    op.drop_column('games', 'playthrough_description')
    op.drop_column('games', 'playthrough_points')
    op.drop_column('games', 'game_type')

4. Логика спина (backend/app/api/v1/wheel.py)

Изменить функцию spin_wheel:

async def get_available_games(
    participant: Participant,
    marathon_games: list[Game],
    db: AsyncSession
) -> list[Game]:
    """Получить список игр, доступных для спина"""
    available = []

    for game in marathon_games:
        if game.game_type == GameType.PLAYTHROUGH.value:
            # Проверяем, прошёл ли участник эту игру
            # Исключаем если COMPLETED или DROPPED
            finished = await db.scalar(
                select(Assignment)
                .where(
                    Assignment.participant_id == participant.id,
                    Assignment.game_id == game.id,
                    Assignment.is_playthrough == True,
                    Assignment.status.in_([
                        AssignmentStatus.COMPLETED.value,
                        AssignmentStatus.DROPPED.value
                    ])
                )
            )
            if not finished:
                available.append(game)

        else:  # GameType.CHALLENGES
            # Проверяем, остались ли невыполненные челленджи
            completed_challenge_ids = await db.scalars(
                select(Assignment.challenge_id)
                .where(
                    Assignment.participant_id == participant.id,
                    Assignment.challenge_id.in_([c.id for c in game.challenges]),
                    Assignment.status == AssignmentStatus.COMPLETED.value
                )
            )
            completed_ids = set(completed_challenge_ids.all())
            all_challenge_ids = {c.id for c in game.challenges}

            if completed_ids != all_challenge_ids:
                available.append(game)

    return available


async def spin_wheel(...):
    # Получаем доступные игры (исключаем пройденные)
    available_games = await get_available_games(participant, marathon_games, db)

    if not available_games:
        raise HTTPException(
            status_code=400,
            detail="Все игры пройдены! Поздравляем!"
        )

    game = random.choice(available_games)

    if game.game_type == GameType.PLAYTHROUGH.value:
        # Для playthrough НЕ выбираем челлендж — основное задание это прохождение
        # Данные берутся из полей игры: playthrough_points, playthrough_description
        challenge = None  # Или создаём виртуальный объект

        # Все челленджи игры становятся дополнительными
        bonus_challenges = list(game.challenges)

        # Создаём Assignment с флагом is_playthrough=True
        assignment = Assignment(
            participant_id=participant.id,
            challenge_id=None,  # Нет привязки к челленджу
            game_id=game.id,    # Новое поле — привязка к игре
            is_playthrough=True,
            status=AssignmentStatus.ACTIVE,
            # ...
        )

    else:  # GameType.CHALLENGES
        # Выбираем случайный НЕВЫПОЛНЕННЫЙ челлендж
        completed_challenge_ids = await db.scalars(
            select(Assignment.challenge_id)
            .where(
                Assignment.participant_id == participant.id,
                Assignment.challenge_id.in_([c.id for c in game.challenges]),
                Assignment.status == AssignmentStatus.COMPLETED.value
            )
        )
        completed_ids = set(completed_challenge_ids.all())

        available_challenges = [c for c in game.challenges if c.id not in completed_ids]
        challenge = random.choice(available_challenges)
        bonus_challenges = []

        assignment = Assignment(
            participant_id=participant.id,
            challenge_id=challenge.id,
            is_playthrough=False,
            status=AssignmentStatus.ACTIVE,
            # ...
        )

    # ... сохранение Assignment ...

5. Модель Assignment (backend/app/models/assignment.py)

Обновить модель для поддержки прохождений:

class Assignment(Base):
    # ... существующие поля ...

    # Для прохождений: привязка к игре вместо челленджа
    game_id: Mapped[int | None] = mapped_column(
        ForeignKey("games.id"),
        nullable=True
    )
    is_playthrough: Mapped[bool] = mapped_column(Boolean, default=False)

    # Relationships
    game: Mapped["Game"] = relationship(back_populates="playthrough_assignments")

# Отдельная таблица для бонусных челленджей
class BonusAssignment(Base):
    __tablename__ = "bonus_assignments"

    id: Mapped[int] = mapped_column(primary_key=True)
    main_assignment_id: Mapped[int] = mapped_column(ForeignKey("assignments.id"))
    challenge_id: Mapped[int] = mapped_column(ForeignKey("challenges.id"))
    status: Mapped[str] = mapped_column(String(20), default="pending")  # pending, completed
    proof_path: Mapped[str | None] = mapped_column(Text, nullable=True)
    proof_url: Mapped[str | None] = mapped_column(Text, nullable=True)
    completed_at: Mapped[datetime | None] = mapped_column(nullable=True)
    points_earned: Mapped[int] = mapped_column(Integer, default=0)

    # Relationships
    main_assignment: Mapped["Assignment"] = relationship(back_populates="bonus_assignments")
    challenge: Mapped["Challenge"] = relationship()

6. API эндпоинты

Добавить/обновить эндпоинты:

# Обновить ответ спина
class PlaythroughInfo(BaseModel):
    """Информация о прохождении (для playthrough игр)"""
    description: str
    points: int

class SpinResult(BaseModel):
    assignment_id: int
    game: GameResponse
    challenge: ChallengeResponse | None  # None для playthrough
    is_playthrough: bool
    playthrough_info: PlaythroughInfo | None  # Заполняется для playthrough
    bonus_challenges: list[ChallengeResponse] = []  # Для playthrough
    can_drop: bool
    drop_penalty: int

# Завершение бонусного челленджа
@router.post("/assignments/{assignment_id}/bonus/{challenge_id}/complete")
async def complete_bonus_challenge(
    assignment_id: int,
    challenge_id: int,
    proof: ProofData,
    db: AsyncSession = Depends(get_db),
    current_user: User = Depends(get_current_user)
) -> BonusAssignmentResponse:
    """Завершить дополнительный челлендж для игры-прохождения"""
    ...

# Получение бонусных челленджей
@router.get("/assignments/{assignment_id}/bonus")
async def get_bonus_assignments(
    assignment_id: int,
    db: AsyncSession = Depends(get_db),
    current_user: User = Depends(get_current_user)
) -> list[BonusAssignmentResponse]:
    """Получить список бонусных челленджей и их статус"""
    ...

# Получение количества доступных игр для спина
@router.get("/marathons/{marathon_id}/available-games-count")
async def get_available_games_count(
    marathon_id: int,
    db: AsyncSession = Depends(get_db),
    current_user: User = Depends(get_current_user)
) -> dict:
    """
    Получить количество игр, доступных для спина.
    Возвращает: { "available": 5, "total": 10 }
    """
    participant = await get_participant(...)
    marathon_games = await get_marathon_games(...)
    available = await get_available_games(participant, marathon_games, db)

    return {
        "available": len(available),
        "total": len(marathon_games)
    }

# Редактирование игры
@router.patch("/marathons/{marathon_id}/games/{game_id}")
async def update_game(
    marathon_id: int,
    game_id: int,
    game_data: GameUpdate,
    db: AsyncSession = Depends(get_db),
    current_user: User = Depends(get_current_user)
) -> GameResponse:
    """
    Редактировать игру.

    Доступно только организатору марафона.
    При смене типа на 'playthrough' необходимо указать playthrough_points и playthrough_description.
    """
    # Проверка прав (организатор)
    # Валидация: если меняем тип на playthrough, проверить что поля заполнены
    # Обновление полей
    ...

Изменения в Frontend

1. Типы (frontend/src/types/index.ts)

export type GameType = 'playthrough' | 'challenges'

export interface Game {
  // ... существующие поля ...
  game_type: GameType
  playthrough_points: number | null
  playthrough_description: string | null
}

export interface PlaythroughInfo {
  description: string
  points: number
}

export interface SpinResult {
  assignment_id: number
  game: Game
  challenge: Challenge | null  // null для playthrough
  is_playthrough: boolean
  playthrough_info: PlaythroughInfo | null
  bonus_challenges: Challenge[]
  can_drop: boolean
  drop_penalty: number
}

export interface BonusAssignment {
  id: number
  challenge: Challenge
  status: 'pending' | 'completed'
  proof_url: string | null
  completed_at: string | null
  points_earned: number
}

export interface GameUpdate {
  title?: string
  download_url?: string
  genre?: string
  game_type?: GameType
  playthrough_points?: number
  playthrough_description?: string
}

2. Форма добавления игры

Добавить выбор типа игры и условные поля:

// components/AddGameForm.tsx
const [gameType, setGameType] = useState<GameType>('challenges')
const [playthroughPoints, setPlaythroughPoints] = useState<number>(100)
const [playthroughDescription, setPlaythroughDescription] = useState<string>('')

return (
  <form>
    {/* ... существующие поля ... */}

    <Select
      label="Тип игры"
      value={gameType}
      onChange={setGameType}
      options={[
        { value: 'challenges', label: 'Челленджи' },
        { value: 'playthrough', label: 'Прохождение' }
      ]}
    />

    {/* Поля только для типа "Прохождение" */}
    {gameType === 'playthrough' && (
      <>
        <Input
          type="number"
          label="Очки за прохождение"
          value={playthroughPoints}
          onChange={setPlaythroughPoints}
          min={1}
          max={500}
          required
        />
        <Textarea
          label="Описание прохождения"
          value={playthroughDescription}
          onChange={setPlaythroughDescription}
          placeholder="Например: Пройти основной сюжет игры"
          required
        />
      </>
    )}
  </form>
)

3. Отображение результата спина

Для типа "Прохождение" показывать:

  • Основное задание с описанием из playthrough_info
  • Очки за прохождение
  • Список дополнительных челленджей (опциональные)
// components/SpinResult.tsx
{result.is_playthrough ? (
  <PlaythroughCard
    game={result.game}
    info={result.playthrough_info}
    bonusChallenges={result.bonus_challenges}
  />
) : (
  <ChallengeCard challenge={result.challenge} />
)}

4. Карточка текущего задания

Для playthrough показывать прогресс по доп. челленджам:

// components/CurrentAssignment.tsx
{assignment.is_playthrough && (
  <div className="mt-4">
    <h4>Дополнительные задания (опционально)</h4>
    <BonusChallengesList
      assignmentId={assignment.id}
      challenges={assignment.bonus_challenges}
      onComplete={handleBonusComplete}
    />
    <p className="text-sm text-gray-500">
      Выполнено: {completedCount} / {totalCount} (+{bonusPoints} очков)
    </p>
  </div>
)}

5. Форма завершения бонусного челленджа

// components/BonusChallengeCompleteModal.tsx
<Modal>
  <h3>Завершить челлендж: {challenge.title}</h3>
  <p>{challenge.description}</p>
  <p>Очки: +{challenge.points}</p>

  <ProofUpload
    proofType={challenge.proof_type}
    onUpload={handleProofUpload}
  />

  <Button onClick={handleComplete}>
    Завершить (+{challenge.points} очков)
  </Button>
</Modal>

6. Редактирование игры

Добавить модалку/страницу редактирования игры:

// components/EditGameModal.tsx
interface EditGameModalProps {
  game: Game
  onSave: (data: GameUpdate) => void
  onClose: () => void
}

const EditGameModal = ({ game, onSave, onClose }: EditGameModalProps) => {
  const [title, setTitle] = useState(game.title)
  const [downloadUrl, setDownloadUrl] = useState(game.download_url)
  const [genre, setGenre] = useState(game.genre)
  const [gameType, setGameType] = useState<GameType>(game.game_type)
  const [playthroughPoints, setPlaythroughPoints] = useState(game.playthrough_points ?? 100)
  const [playthroughDescription, setPlaythroughDescription] = useState(game.playthrough_description ?? '')

  const handleSubmit = () => {
    const data: GameUpdate = {
      title,
      download_url: downloadUrl,
      genre,
      game_type: gameType,
      ...(gameType === 'playthrough' && {
        playthrough_points: playthroughPoints,
        playthrough_description: playthroughDescription,
      }),
    }
    onSave(data)
  }

  return (
    <Modal onClose={onClose}>
      <h2>Редактирование игры</h2>

      <Input label="Название" value={title} onChange={setTitle} />
      <Input label="Ссылка на скачивание" value={downloadUrl} onChange={setDownloadUrl} />
      <Input label="Жанр" value={genre} onChange={setGenre} />

      <Select
        label="Тип игры"
        value={gameType}
        onChange={setGameType}
        options={[
          { value: 'challenges', label: 'Челленджи' },
          { value: 'playthrough', label: 'Прохождение' }
        ]}
      />

      {gameType === 'playthrough' && (
        <>
          <Input
            type="number"
            label="Очки за прохождение"
            value={playthroughPoints}
            onChange={setPlaythroughPoints}
            min={1}
            max={500}
          />
          <Textarea
            label="Описание прохождения"
            value={playthroughDescription}
            onChange={setPlaythroughDescription}
          />
        </>
      )}

      <div className="flex gap-2">
        <Button variant="secondary" onClick={onClose}>Отмена</Button>
        <Button onClick={handleSubmit}>Сохранить</Button>
      </div>
    </Modal>
  )
}

7. Кнопка редактирования в списке игр

// components/GameCard.tsx (или GamesList)
{isOrganizer && (
  <Button
    variant="ghost"
    size="sm"
    onClick={() => setEditingGame(game)}
  >
    Редактировать
  </Button>
)}

8. Счётчик доступных игр

Отображать количество игр, которые ещё могут выпасть при спине:

// components/AvailableGamesCounter.tsx
interface AvailableGamesCounterProps {
  available: number
  total: number
}

const AvailableGamesCounter = ({ available, total }: AvailableGamesCounterProps) => {
  const allCompleted = available === 0

  return (
    <div className="text-sm text-gray-500">
      {allCompleted ? (
        <span className="text-green-600 font-medium">
          Все игры пройдены!
        </span>
      ) : (
        <span>
          Доступно игр: <strong>{available}</strong> из {total}
        </span>
      )}
    </div>
  )
}

// Использование на странице марафона / рядом с колесом
<AvailableGamesCounter available={gamesCount.available} total={gamesCount.total} />

Уточнённые требования

Вопрос Решение
Очки за прохождение Устанавливаются при создании игры (поле playthrough_points)
Обязательность доп. челленджей Не обязательны — можно завершить задание без них
Пруф на прохождение Тип указывается при создании (playthrough_proof_type)
Пруфы на бонусные челленджи Требуются — по типу челленджа (screenshot/video/steam)
Прикрепление файла Не обязательно — можно отправить комментарий со ссылкой
Миграция существующих игр Тип по умолчанию: challenges
Дроп игры (playthrough) Дропнутая игра не выпадает повторно
Бонусные челленджи после завершения Недоступны — только пока задание активно
Счётчик игр Показывать "Доступно игр: X из Y"
События для playthrough Все игнорируются — стандартные очки без модификаторов

План реализации

Этап 1: Backend (модели и миграции)

  • Добавить enum GameType в backend/app/models/game.py
  • Добавить поля game_type, playthrough_points, playthrough_description, playthrough_proof_type, playthrough_proof_hint в модель Game
  • Создать модель BonusAssignment в backend/app/models/bonus_assignment.py
  • Обновить модель Assignment — добавить game_id, is_playthrough
  • Создать миграцию Alembic (020_add_game_types.py)

Этап 2: Backend (схемы и API)

  • Обновить Pydantic схемы для Game (GameCreate, GameResponse)
  • Добавить схему GameUpdate с валидацией
  • Обновить API создания игры
  • Добавить API редактирования игры (PATCH /games/{id})
  • Добавить API счётчика игр (GET /available-games-count)
  • Добавить схемы для BonusAssignment, PlaythroughInfo
  • Добавить эндпоинты для бонусных челленджей

Этап 3: Backend (логика спина)

  • Добавить функцию get_available_games() для фильтрации пройденных игр
  • Обновить логику spin_wheel для обработки типов
  • Для типа challenges — выбирать только невыполненные челленджи
  • Обработать случай "Все игры пройдены"
  • Обновить ответ SpinResult
  • Обновить логику завершения задания для playthrough
  • Добавить логику завершения бонусных челленджей
  • Игнорирование событий для playthrough

Этап 4: Frontend (типы и формы)

  • Обновить типы TypeScript (Game, SpinResult, BonusAssignment, GameUpdate, AvailableGamesCount)
  • Добавить выбор типа в форму создания игры
  • Добавить условные поля "Очки", "Описание", "Тип пруфа", "Подсказка" для типа "Прохождение"
  • Добавить API метод gamesApi.update() и gamesApi.getAvailableGamesCount()
  • Добавить API методы для бонусных челленджей

Этап 5: Frontend (UI)

  • Обновить отображение результата спина для playthrough
  • Обновить карточку текущего задания (PlayPage)
  • Показ бонусных челленджей со статусами
  • Бейдж "Прохождение" на карточках игр в лобби
  • Поддержка пруфа через комментарий для playthrough

Этап 6: Тестирование

  • Тестирование миграции на существующих данных
  • Проверка создания игр обоих типов
  • Проверка редактирования игр (смена типа, обновление полей)
  • Проверка спина для playthrough и challenges
  • Проверка фильтрации пройденных игр (playthrough не выпадает повторно)
  • Проверка фильтрации челленджей (выпадают только невыполненные)
  • Проверка состояния "Все игры пройдены"
  • Проверка завершения основного и бонусных заданий

Схема работы

Создание игры

┌─────────────────────────────────────────────────────────────────┐
│                      СОЗДАНИЕ ИГРЫ                              │
└─────────────────────────────────────────────────────────────────┘
                              │
                              ▼
                    ┌─────────────────┐
                    │  Выбор типа     │
                    └─────────────────┘
                              │
              ┌───────────────┴───────────────┐
              ▼                               ▼
    ┌─────────────────┐             ┌─────────────────┐
    │ "Прохождение"   │             │ "Челленджи"     │
    │                 │             │                 │
    │ Доп. поля:      │             │ Стандартные     │
    │ • Очки          │             │ поля            │
    │ • Описание      │             │                 │
    └─────────────────┘             └─────────────────┘

Спин колеса

┌─────────────────────────────────────────────────────────────────┐
│                         СПИН КОЛЕСА                             │
└─────────────────────────────────────────────────────────────────┘
                              │
                              ▼
                    ┌─────────────────┐
                    │  Выбор игры     │
                    │  (random)       │
                    └─────────────────┘
                              │
              ┌───────────────┴───────────────┐
              ▼                               ▼
    ┌─────────────────┐             ┌─────────────────┐
    │ game_type =     │             │ game_type =     │
    │ "playthrough"   │             │ "challenges"    │
    └─────────────────┘             └─────────────────┘
              │                               │
              ▼                               ▼
    ┌─────────────────┐             ┌─────────────────┐
    │ Основное:       │             │ Случайный       │
    │ playthrough_    │             │ челлендж        │
    │ description     │             │                 │
    │                 │             │ (текущая        │
    │ Очки:           │             │  логика)        │
    │ playthrough_    │             │                 │
    │ points          │             │                 │
    │                 │             │                 │
    │ Доп. задания:   │             │                 │
    │ Все челленджи   │             │                 │
    │ (опционально)   │             │                 │
    └─────────────────┘             └─────────────────┘
              │                               │
              ▼                               ▼
    ┌─────────────────┐             ┌─────────────────┐
    │ Пруф:           │             │ Пруф:           │
    │ На прохождение  │             │ По типу         │
    │ игры            │             │ челленджа       │
    └─────────────────┘             └─────────────────┘
              │                               │
              ▼                               ▼
    ┌─────────────────┐             ┌─────────────────┐
    │ Очки:           │             │ Очки:           │
    │ + За прохождение│             │ + За челлендж   │
    │ + Бонус за доп. │             │                 │
    │   челленджи     │             │                 │
    └─────────────────┘             └─────────────────┘