Add events
This commit is contained in:
227
backend/app/services/events.py
Normal file
227
backend/app/services/events.py
Normal file
@@ -0,0 +1,227 @@
|
||||
from datetime import datetime, timedelta
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.orm import selectinload
|
||||
from sqlalchemy.orm.attributes import flag_modified
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.models import Event, EventType, Marathon, Challenge, Difficulty
|
||||
from app.schemas.event import EventEffects, EVENT_INFO, COMMON_ENEMY_BONUSES
|
||||
|
||||
|
||||
class EventService:
|
||||
"""Service for managing marathon events"""
|
||||
|
||||
async def get_active_event(self, db: AsyncSession, marathon_id: int) -> Event | None:
|
||||
"""Get currently active event for marathon"""
|
||||
now = datetime.utcnow()
|
||||
result = await db.execute(
|
||||
select(Event)
|
||||
.options(selectinload(Event.created_by))
|
||||
.where(
|
||||
Event.marathon_id == marathon_id,
|
||||
Event.is_active == True,
|
||||
Event.start_time <= now,
|
||||
)
|
||||
.order_by(Event.start_time.desc())
|
||||
)
|
||||
event = result.scalar_one_or_none()
|
||||
|
||||
# Check if event has expired
|
||||
if event and event.end_time and event.end_time < now:
|
||||
await self.end_event(db, event.id)
|
||||
return None
|
||||
|
||||
return event
|
||||
|
||||
async def can_start_event(self, db: AsyncSession, marathon_id: int) -> bool:
|
||||
"""Check if we can start a new event (no active event exists)"""
|
||||
active = await self.get_active_event(db, marathon_id)
|
||||
return active is None
|
||||
|
||||
async def start_event(
|
||||
self,
|
||||
db: AsyncSession,
|
||||
marathon_id: int,
|
||||
event_type: str,
|
||||
created_by_id: int | None = None,
|
||||
duration_minutes: int | None = None,
|
||||
challenge_id: int | None = None,
|
||||
) -> Event:
|
||||
"""Start a new event"""
|
||||
# Check no active event
|
||||
if not await self.can_start_event(db, marathon_id):
|
||||
raise ValueError("An event is already active")
|
||||
|
||||
# Get default duration if not provided
|
||||
event_info = EVENT_INFO.get(EventType(event_type), {})
|
||||
if duration_minutes is None:
|
||||
duration_minutes = event_info.get("default_duration")
|
||||
|
||||
now = datetime.utcnow()
|
||||
end_time = now + timedelta(minutes=duration_minutes) if duration_minutes else None
|
||||
|
||||
# Build event data
|
||||
data = {}
|
||||
if event_type == EventType.COMMON_ENEMY.value and challenge_id:
|
||||
data["challenge_id"] = challenge_id
|
||||
data["completions"] = [] # Track who completed and when
|
||||
|
||||
event = Event(
|
||||
marathon_id=marathon_id,
|
||||
type=event_type,
|
||||
start_time=now,
|
||||
end_time=end_time,
|
||||
is_active=True,
|
||||
created_by_id=created_by_id,
|
||||
data=data if data else None,
|
||||
)
|
||||
db.add(event)
|
||||
await db.commit()
|
||||
await db.refresh(event)
|
||||
|
||||
# Load created_by relationship
|
||||
if created_by_id:
|
||||
await db.refresh(event, ["created_by"])
|
||||
|
||||
return event
|
||||
|
||||
async def end_event(self, db: AsyncSession, event_id: int) -> None:
|
||||
"""End an event"""
|
||||
result = await db.execute(select(Event).where(Event.id == event_id))
|
||||
event = result.scalar_one_or_none()
|
||||
if event:
|
||||
event.is_active = False
|
||||
if not event.end_time:
|
||||
event.end_time = datetime.utcnow()
|
||||
await db.commit()
|
||||
|
||||
async def consume_jackpot(self, db: AsyncSession, event_id: int) -> None:
|
||||
"""Consume jackpot event after one spin"""
|
||||
await self.end_event(db, event_id)
|
||||
|
||||
def get_event_effects(self, event: Event | None) -> EventEffects:
|
||||
"""Get effects of an event"""
|
||||
if not event:
|
||||
return EventEffects(description="Нет активного события")
|
||||
|
||||
event_info = EVENT_INFO.get(EventType(event.type), {})
|
||||
|
||||
return EventEffects(
|
||||
points_multiplier=event_info.get("points_multiplier", 1.0),
|
||||
drop_free=event_info.get("drop_free", False),
|
||||
special_action=event_info.get("special_action"),
|
||||
description=event_info.get("description", ""),
|
||||
)
|
||||
|
||||
async def get_random_hard_challenge(
|
||||
self,
|
||||
db: AsyncSession,
|
||||
marathon_id: int
|
||||
) -> Challenge | None:
|
||||
"""Get a random hard challenge for jackpot event"""
|
||||
result = await db.execute(
|
||||
select(Challenge)
|
||||
.join(Challenge.game)
|
||||
.where(
|
||||
Challenge.game.has(marathon_id=marathon_id),
|
||||
Challenge.difficulty == Difficulty.HARD.value,
|
||||
)
|
||||
)
|
||||
challenges = result.scalars().all()
|
||||
if not challenges:
|
||||
# Fallback to any challenge
|
||||
result = await db.execute(
|
||||
select(Challenge)
|
||||
.join(Challenge.game)
|
||||
.where(Challenge.game.has(marathon_id=marathon_id))
|
||||
)
|
||||
challenges = result.scalars().all()
|
||||
|
||||
if challenges:
|
||||
import random
|
||||
return random.choice(challenges)
|
||||
return None
|
||||
|
||||
async def record_common_enemy_completion(
|
||||
self,
|
||||
db: AsyncSession,
|
||||
event: Event,
|
||||
participant_id: int,
|
||||
user_id: int,
|
||||
) -> tuple[int, bool, list[dict] | None]:
|
||||
"""
|
||||
Record completion for common enemy event.
|
||||
Returns: (bonus_points, event_closed, winners_list)
|
||||
- bonus_points: bonus for this completion (top 3 get bonuses)
|
||||
- event_closed: True if event was auto-closed (3 completions reached)
|
||||
- winners_list: list of winners if event closed, None otherwise
|
||||
"""
|
||||
if event.type != EventType.COMMON_ENEMY.value:
|
||||
return 0, False, None
|
||||
|
||||
data = event.data or {}
|
||||
completions = data.get("completions", [])
|
||||
|
||||
# Check if already completed
|
||||
if any(c["participant_id"] == participant_id for c in completions):
|
||||
return 0, False, None
|
||||
|
||||
# Add completion
|
||||
rank = len(completions) + 1
|
||||
completions.append({
|
||||
"participant_id": participant_id,
|
||||
"user_id": user_id,
|
||||
"completed_at": datetime.utcnow().isoformat(),
|
||||
"rank": rank,
|
||||
})
|
||||
|
||||
# Update event data - need to flag_modified for SQLAlchemy to detect JSON changes
|
||||
event.data = {**data, "completions": completions}
|
||||
flag_modified(event, "data")
|
||||
|
||||
bonus = COMMON_ENEMY_BONUSES.get(rank, 0)
|
||||
|
||||
# Auto-close event when 3 players completed
|
||||
event_closed = False
|
||||
winners_list = None
|
||||
if rank >= 3:
|
||||
event.is_active = False
|
||||
event.end_time = datetime.utcnow()
|
||||
event_closed = True
|
||||
winners_list = completions[:3] # Top 3
|
||||
|
||||
await db.commit()
|
||||
|
||||
return bonus, event_closed, winners_list
|
||||
|
||||
async def get_common_enemy_challenge(
|
||||
self,
|
||||
db: AsyncSession,
|
||||
event: Event
|
||||
) -> Challenge | None:
|
||||
"""Get the challenge for common enemy event"""
|
||||
if event.type != EventType.COMMON_ENEMY.value:
|
||||
return None
|
||||
|
||||
data = event.data or {}
|
||||
challenge_id = data.get("challenge_id")
|
||||
if not challenge_id:
|
||||
return None
|
||||
|
||||
result = await db.execute(
|
||||
select(Challenge)
|
||||
.options(selectinload(Challenge.game))
|
||||
.where(Challenge.id == challenge_id)
|
||||
)
|
||||
return result.scalar_one_or_none()
|
||||
|
||||
def get_time_remaining(self, event: Event | None) -> int | None:
|
||||
"""Get remaining time in seconds for an event"""
|
||||
if not event or not event.end_time:
|
||||
return None
|
||||
|
||||
remaining = (event.end_time - datetime.utcnow()).total_seconds()
|
||||
return max(0, int(remaining))
|
||||
|
||||
|
||||
event_service = EventService()
|
||||
Reference in New Issue
Block a user