2025-12-15 03:22:29 +07:00
|
|
|
|
from datetime import datetime
|
|
|
|
|
|
from pydantic import BaseModel, Field
|
|
|
|
|
|
from typing import Literal
|
|
|
|
|
|
|
|
|
|
|
|
from app.models.event import EventType
|
|
|
|
|
|
from app.schemas.user import UserPublic
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# Event type literals for Pydantic
|
|
|
|
|
|
EventTypeLiteral = Literal[
|
|
|
|
|
|
"golden_hour",
|
|
|
|
|
|
"common_enemy",
|
|
|
|
|
|
"double_risk",
|
|
|
|
|
|
"jackpot",
|
|
|
|
|
|
"swap",
|
2025-12-15 23:50:37 +07:00
|
|
|
|
"game_choice",
|
2025-12-15 03:22:29 +07:00
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class EventCreate(BaseModel):
|
|
|
|
|
|
type: EventTypeLiteral
|
|
|
|
|
|
duration_minutes: int | None = Field(
|
|
|
|
|
|
None,
|
|
|
|
|
|
description="Duration in minutes. If not provided, uses default for event type."
|
|
|
|
|
|
)
|
|
|
|
|
|
challenge_id: int | None = Field(
|
|
|
|
|
|
None,
|
|
|
|
|
|
description="For common_enemy event - the challenge everyone will get"
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class EventEffects(BaseModel):
|
|
|
|
|
|
points_multiplier: float = 1.0
|
|
|
|
|
|
drop_free: bool = False
|
2025-12-15 23:50:37 +07:00
|
|
|
|
special_action: str | None = None # "swap", "game_choice"
|
2025-12-15 03:22:29 +07:00
|
|
|
|
description: str = ""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class EventResponse(BaseModel):
|
|
|
|
|
|
id: int
|
|
|
|
|
|
type: EventTypeLiteral
|
|
|
|
|
|
start_time: datetime
|
|
|
|
|
|
end_time: datetime | None
|
|
|
|
|
|
is_active: bool
|
|
|
|
|
|
created_by: UserPublic | None
|
|
|
|
|
|
data: dict | None
|
|
|
|
|
|
created_at: datetime
|
|
|
|
|
|
|
|
|
|
|
|
class Config:
|
|
|
|
|
|
from_attributes = True
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class ActiveEventResponse(BaseModel):
|
|
|
|
|
|
event: EventResponse | None
|
|
|
|
|
|
effects: EventEffects
|
|
|
|
|
|
time_remaining_seconds: int | None = None
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class SwapRequest(BaseModel):
|
|
|
|
|
|
target_participant_id: int
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class CommonEnemyLeaderboard(BaseModel):
|
|
|
|
|
|
participant_id: int
|
|
|
|
|
|
user: UserPublic
|
|
|
|
|
|
completed_at: datetime | None
|
|
|
|
|
|
rank: int | None
|
|
|
|
|
|
bonus_points: int
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# Event descriptions and default durations
|
|
|
|
|
|
EVENT_INFO = {
|
|
|
|
|
|
EventType.GOLDEN_HOUR: {
|
|
|
|
|
|
"name": "Золотой час",
|
|
|
|
|
|
"description": "Все очки x1.5!",
|
|
|
|
|
|
"default_duration": 45,
|
|
|
|
|
|
"points_multiplier": 1.5,
|
|
|
|
|
|
"drop_free": False,
|
|
|
|
|
|
},
|
|
|
|
|
|
EventType.COMMON_ENEMY: {
|
|
|
|
|
|
"name": "Общий враг",
|
|
|
|
|
|
"description": "Все получают одинаковый челлендж. Первые 3 получают бонус!",
|
|
|
|
|
|
"default_duration": None, # Until all complete
|
|
|
|
|
|
"points_multiplier": 1.0,
|
|
|
|
|
|
"drop_free": False,
|
|
|
|
|
|
},
|
|
|
|
|
|
EventType.DOUBLE_RISK: {
|
2025-12-15 23:50:37 +07:00
|
|
|
|
"name": "Безопасная игра",
|
2025-12-15 03:22:29 +07:00
|
|
|
|
"description": "Дропы бесплатны, но очки x0.5",
|
|
|
|
|
|
"default_duration": 120,
|
|
|
|
|
|
"points_multiplier": 0.5,
|
|
|
|
|
|
"drop_free": True,
|
|
|
|
|
|
},
|
|
|
|
|
|
EventType.JACKPOT: {
|
|
|
|
|
|
"name": "Джекпот",
|
|
|
|
|
|
"description": "Следующий спин — сложный челлендж с x3 очками!",
|
|
|
|
|
|
"default_duration": None, # 1 spin
|
|
|
|
|
|
"points_multiplier": 3.0,
|
|
|
|
|
|
"drop_free": False,
|
|
|
|
|
|
},
|
|
|
|
|
|
EventType.SWAP: {
|
|
|
|
|
|
"name": "Обмен",
|
|
|
|
|
|
"description": "Можно поменяться заданием с другим участником",
|
|
|
|
|
|
"default_duration": 60,
|
|
|
|
|
|
"points_multiplier": 1.0,
|
|
|
|
|
|
"drop_free": False,
|
|
|
|
|
|
"special_action": "swap",
|
|
|
|
|
|
},
|
2025-12-15 23:50:37 +07:00
|
|
|
|
EventType.GAME_CHOICE: {
|
|
|
|
|
|
"name": "Выбор игры",
|
|
|
|
|
|
"description": "Выбери игру и один из 3 челленджей. Можно заменить текущее задание без штрафа!",
|
|
|
|
|
|
"default_duration": 120,
|
|
|
|
|
|
"points_multiplier": 1.0,
|
|
|
|
|
|
"drop_free": True, # Free replacement of current assignment
|
|
|
|
|
|
"special_action": "game_choice",
|
2025-12-15 03:22:29 +07:00
|
|
|
|
},
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
# Bonus points for Common Enemy top 3
|
|
|
|
|
|
COMMON_ENEMY_BONUSES = {
|
|
|
|
|
|
1: 50,
|
|
|
|
|
|
2: 30,
|
|
|
|
|
|
3: 15,
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class SwapCandidate(BaseModel):
|
|
|
|
|
|
"""Participant available for assignment swap"""
|
|
|
|
|
|
participant_id: int
|
|
|
|
|
|
user: UserPublic
|
|
|
|
|
|
challenge_title: str
|
|
|
|
|
|
challenge_description: str
|
|
|
|
|
|
challenge_points: int
|
|
|
|
|
|
challenge_difficulty: str
|
|
|
|
|
|
game_title: str
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# Two-sided swap confirmation schemas
|
|
|
|
|
|
SwapRequestStatusLiteral = Literal["pending", "accepted", "declined", "cancelled"]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class SwapRequestCreate(BaseModel):
|
|
|
|
|
|
"""Request to swap assignment with another participant"""
|
|
|
|
|
|
target_participant_id: int
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class SwapRequestChallengeInfo(BaseModel):
|
|
|
|
|
|
"""Challenge info for swap request display"""
|
|
|
|
|
|
title: str
|
|
|
|
|
|
description: str
|
|
|
|
|
|
points: int
|
|
|
|
|
|
difficulty: str
|
|
|
|
|
|
game_title: str
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class SwapRequestResponse(BaseModel):
|
|
|
|
|
|
"""Response for a swap request"""
|
|
|
|
|
|
id: int
|
|
|
|
|
|
status: SwapRequestStatusLiteral
|
|
|
|
|
|
from_user: UserPublic
|
|
|
|
|
|
to_user: UserPublic
|
|
|
|
|
|
from_challenge: SwapRequestChallengeInfo
|
|
|
|
|
|
to_challenge: SwapRequestChallengeInfo
|
|
|
|
|
|
created_at: datetime
|
|
|
|
|
|
responded_at: datetime | None
|
|
|
|
|
|
|
|
|
|
|
|
class Config:
|
|
|
|
|
|
from_attributes = True
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class MySwapRequests(BaseModel):
|
|
|
|
|
|
"""User's incoming and outgoing swap requests"""
|
|
|
|
|
|
incoming: list[SwapRequestResponse]
|
|
|
|
|
|
outgoing: list[SwapRequestResponse]
|