Files
game-marathon/backend/app/services/event_scheduler.py
2025-12-15 03:50:04 +07:00

151 lines
4.8 KiB
Python

"""
Event Scheduler for automatic event launching in marathons.
"""
import asyncio
import random
from datetime import datetime, timedelta
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
from app.models import Marathon, MarathonStatus, Event, EventType
from app.services.events import EventService
# Configuration
CHECK_INTERVAL_SECONDS = 300 # Check every 5 minutes
EVENT_PROBABILITY = 0.1 # 10% chance per check to start an event
MIN_EVENT_GAP_HOURS = 4 # Minimum hours between events
# Events that can be auto-triggered (excluding common_enemy which needs a challenge_id)
AUTO_EVENT_TYPES = [
EventType.GOLDEN_HOUR,
EventType.DOUBLE_RISK,
EventType.JACKPOT,
EventType.REMATCH,
]
class EventScheduler:
"""Background scheduler for automatic event management."""
def __init__(self):
self._running = False
self._task: asyncio.Task | None = None
async def start(self, session_factory) -> None:
"""Start the scheduler background task."""
if self._running:
return
self._running = True
self._task = asyncio.create_task(self._run_loop(session_factory))
print("[EventScheduler] Started")
async def stop(self) -> None:
"""Stop the scheduler."""
self._running = False
if self._task:
self._task.cancel()
try:
await self._task
except asyncio.CancelledError:
pass
print("[EventScheduler] Stopped")
async def _run_loop(self, session_factory) -> None:
"""Main scheduler loop."""
while self._running:
try:
async with session_factory() as db:
await self._process_events(db)
except Exception as e:
print(f"[EventScheduler] Error in loop: {e}")
await asyncio.sleep(CHECK_INTERVAL_SECONDS)
async def _process_events(self, db: AsyncSession) -> None:
"""Process events - cleanup expired and potentially start new ones."""
# 1. Cleanup expired events
await self._cleanup_expired_events(db)
# 2. Maybe start new events for eligible marathons
await self._maybe_start_events(db)
async def _cleanup_expired_events(self, db: AsyncSession) -> None:
"""End any events that have expired."""
now = datetime.utcnow()
result = await db.execute(
select(Event).where(
Event.is_active == True,
Event.end_time < now,
)
)
expired_events = result.scalars().all()
for event in expired_events:
event.is_active = False
print(f"[EventScheduler] Ended expired event {event.id} ({event.type})")
if expired_events:
await db.commit()
async def _maybe_start_events(self, db: AsyncSession) -> None:
"""Potentially start new events for eligible marathons."""
# Get active marathons with auto_events enabled
result = await db.execute(
select(Marathon).where(
Marathon.status == MarathonStatus.ACTIVE.value,
Marathon.auto_events_enabled == True,
)
)
marathons = result.scalars().all()
event_service = EventService()
for marathon in marathons:
# Skip if random chance doesn't hit
if random.random() > EVENT_PROBABILITY:
continue
# Check if there's already an active event
active_event = await event_service.get_active_event(db, marathon.id)
if active_event:
continue
# Check if enough time has passed since last event
result = await db.execute(
select(Event)
.where(Event.marathon_id == marathon.id)
.order_by(Event.end_time.desc())
.limit(1)
)
last_event = result.scalar_one_or_none()
if last_event:
time_since_last = datetime.utcnow() - last_event.end_time
if time_since_last < timedelta(hours=MIN_EVENT_GAP_HOURS):
continue
# Start a random event
event_type = random.choice(AUTO_EVENT_TYPES)
try:
event = await event_service.start_event(
db=db,
marathon_id=marathon.id,
event_type=event_type.value,
created_by_id=None, # null = auto-started
)
print(
f"[EventScheduler] Auto-started {event_type.value} for marathon {marathon.id}"
)
except Exception as e:
print(
f"[EventScheduler] Failed to start event for marathon {marathon.id}: {e}"
)
# Global scheduler instance
event_scheduler = EventScheduler()