Add events
This commit is contained in:
@@ -10,13 +10,15 @@ from app.api.deps import DbSession, CurrentUser
|
||||
from app.core.config import settings
|
||||
from app.models import (
|
||||
Marathon, MarathonStatus, Game, Challenge, Participant,
|
||||
Assignment, AssignmentStatus, Activity, ActivityType
|
||||
Assignment, AssignmentStatus, Activity, ActivityType,
|
||||
EventType, Difficulty
|
||||
)
|
||||
from app.schemas import (
|
||||
SpinResult, AssignmentResponse, CompleteResult, DropResult,
|
||||
GameResponse, ChallengeResponse, GameShort, UserPublic, MessageResponse
|
||||
)
|
||||
from app.services.points import PointsService
|
||||
from app.services.events import event_service
|
||||
|
||||
router = APIRouter(tags=["wheel"])
|
||||
|
||||
@@ -69,46 +71,91 @@ async def spin_wheel(marathon_id: int, current_user: CurrentUser, db: DbSession)
|
||||
if active:
|
||||
raise HTTPException(status_code=400, detail="You already have an active assignment")
|
||||
|
||||
# Get all games with challenges
|
||||
result = await db.execute(
|
||||
select(Game)
|
||||
.options(selectinload(Game.challenges))
|
||||
.where(Game.marathon_id == marathon_id)
|
||||
)
|
||||
games = [g for g in result.scalars().all() if g.challenges]
|
||||
# Check active event
|
||||
active_event = await event_service.get_active_event(db, marathon_id)
|
||||
|
||||
if not games:
|
||||
raise HTTPException(status_code=400, detail="No games with challenges available")
|
||||
game = None
|
||||
challenge = None
|
||||
|
||||
# Random selection
|
||||
game = random.choice(games)
|
||||
challenge = random.choice(game.challenges)
|
||||
# Handle special event cases
|
||||
if active_event:
|
||||
if active_event.type == EventType.JACKPOT.value:
|
||||
# Jackpot: Get hard challenge only
|
||||
challenge = await event_service.get_random_hard_challenge(db, marathon_id)
|
||||
if challenge:
|
||||
# Load game for challenge
|
||||
result = await db.execute(
|
||||
select(Game).where(Game.id == challenge.game_id)
|
||||
)
|
||||
game = result.scalar_one_or_none()
|
||||
# Consume jackpot (one-time use)
|
||||
await event_service.consume_jackpot(db, active_event.id)
|
||||
|
||||
# Create assignment
|
||||
elif active_event.type == EventType.COMMON_ENEMY.value:
|
||||
# Common enemy: Everyone gets same challenge (if not already completed)
|
||||
event_data = active_event.data or {}
|
||||
completions = event_data.get("completions", [])
|
||||
already_completed = any(c["participant_id"] == participant.id for c in completions)
|
||||
|
||||
if not already_completed:
|
||||
challenge = await event_service.get_common_enemy_challenge(db, active_event)
|
||||
if challenge:
|
||||
game = challenge.game
|
||||
|
||||
# Normal random selection if no special event handling
|
||||
if not game or not challenge:
|
||||
result = await db.execute(
|
||||
select(Game)
|
||||
.options(selectinload(Game.challenges))
|
||||
.where(Game.marathon_id == marathon_id)
|
||||
)
|
||||
games = [g for g in result.scalars().all() if g.challenges]
|
||||
|
||||
if not games:
|
||||
raise HTTPException(status_code=400, detail="No games with challenges available")
|
||||
|
||||
game = random.choice(games)
|
||||
challenge = random.choice(game.challenges)
|
||||
|
||||
# Create assignment (store event_type for jackpot multiplier on completion)
|
||||
assignment = Assignment(
|
||||
participant_id=participant.id,
|
||||
challenge_id=challenge.id,
|
||||
status=AssignmentStatus.ACTIVE.value,
|
||||
event_type=active_event.type if active_event else None,
|
||||
)
|
||||
db.add(assignment)
|
||||
|
||||
# Log activity
|
||||
activity_data = {
|
||||
"game": game.title,
|
||||
"challenge": challenge.title,
|
||||
}
|
||||
if active_event:
|
||||
activity_data["event_type"] = active_event.type
|
||||
|
||||
activity = Activity(
|
||||
marathon_id=marathon_id,
|
||||
user_id=current_user.id,
|
||||
type=ActivityType.SPIN.value,
|
||||
data={
|
||||
"game": game.title,
|
||||
"challenge": challenge.title,
|
||||
},
|
||||
data=activity_data,
|
||||
)
|
||||
db.add(activity)
|
||||
|
||||
await db.commit()
|
||||
await db.refresh(assignment)
|
||||
|
||||
# Calculate drop penalty
|
||||
drop_penalty = points_service.calculate_drop_penalty(participant.drop_count)
|
||||
# Calculate drop penalty (considers active event for double_risk)
|
||||
drop_penalty = points_service.calculate_drop_penalty(participant.drop_count, active_event)
|
||||
|
||||
# Get challenges count (avoid lazy loading in async context)
|
||||
challenges_count = 0
|
||||
if 'challenges' in game.__dict__:
|
||||
challenges_count = len(game.challenges)
|
||||
else:
|
||||
challenges_count = await db.scalar(
|
||||
select(func.count()).select_from(Challenge).where(Challenge.game_id == game.id)
|
||||
)
|
||||
|
||||
return SpinResult(
|
||||
assignment_id=assignment.id,
|
||||
@@ -119,7 +166,7 @@ async def spin_wheel(marathon_id: int, current_user: CurrentUser, db: DbSession)
|
||||
download_url=game.download_url,
|
||||
genre=game.genre,
|
||||
added_by=None,
|
||||
challenges_count=len(game.challenges),
|
||||
challenges_count=challenges_count,
|
||||
created_at=game.created_at,
|
||||
),
|
||||
challenge=ChallengeResponse(
|
||||
@@ -246,9 +293,41 @@ async def complete_assignment(
|
||||
participant = assignment.participant
|
||||
challenge = assignment.challenge
|
||||
|
||||
total_points, streak_bonus = points_service.calculate_completion_points(
|
||||
challenge.points, participant.current_streak
|
||||
# Get marathon_id for activity and event check
|
||||
result = await db.execute(
|
||||
select(Challenge).options(selectinload(Challenge.game)).where(Challenge.id == challenge.id)
|
||||
)
|
||||
full_challenge = result.scalar_one()
|
||||
marathon_id = full_challenge.game.marathon_id
|
||||
|
||||
# Check active event for point multipliers
|
||||
active_event = await event_service.get_active_event(db, marathon_id)
|
||||
|
||||
# For jackpot/rematch: use the event_type stored in assignment (since event may be over)
|
||||
# For other events: use the currently active event
|
||||
effective_event = active_event
|
||||
|
||||
# Handle assignment-level event types (jackpot, rematch)
|
||||
if assignment.event_type in [EventType.JACKPOT.value, EventType.REMATCH.value]:
|
||||
# Create a mock event object for point calculation
|
||||
class MockEvent:
|
||||
def __init__(self, event_type):
|
||||
self.type = event_type
|
||||
effective_event = MockEvent(assignment.event_type)
|
||||
|
||||
total_points, streak_bonus, event_bonus = points_service.calculate_completion_points(
|
||||
challenge.points, participant.current_streak, effective_event
|
||||
)
|
||||
|
||||
# Handle common enemy bonus
|
||||
common_enemy_bonus = 0
|
||||
common_enemy_closed = False
|
||||
common_enemy_winners = None
|
||||
if active_event and active_event.type == EventType.COMMON_ENEMY.value:
|
||||
common_enemy_bonus, common_enemy_closed, common_enemy_winners = await event_service.record_common_enemy_completion(
|
||||
db, active_event, participant.id, current_user.id
|
||||
)
|
||||
total_points += common_enemy_bonus
|
||||
|
||||
# Update assignment
|
||||
assignment.status = AssignmentStatus.COMPLETED.value
|
||||
@@ -261,25 +340,53 @@ async def complete_assignment(
|
||||
participant.current_streak += 1
|
||||
participant.drop_count = 0 # Reset drop counter on success
|
||||
|
||||
# Get marathon_id for activity
|
||||
result = await db.execute(
|
||||
select(Challenge).options(selectinload(Challenge.game)).where(Challenge.id == challenge.id)
|
||||
)
|
||||
full_challenge = result.scalar_one()
|
||||
|
||||
# Log activity
|
||||
activity_data = {
|
||||
"challenge": challenge.title,
|
||||
"points": total_points,
|
||||
"streak": participant.current_streak,
|
||||
}
|
||||
# Log event info (use assignment's event_type for jackpot/rematch, active_event for others)
|
||||
if assignment.event_type in [EventType.JACKPOT.value, EventType.REMATCH.value]:
|
||||
activity_data["event_type"] = assignment.event_type
|
||||
activity_data["event_bonus"] = event_bonus
|
||||
elif active_event:
|
||||
activity_data["event_type"] = active_event.type
|
||||
activity_data["event_bonus"] = event_bonus
|
||||
if common_enemy_bonus:
|
||||
activity_data["common_enemy_bonus"] = common_enemy_bonus
|
||||
|
||||
activity = Activity(
|
||||
marathon_id=full_challenge.game.marathon_id,
|
||||
marathon_id=marathon_id,
|
||||
user_id=current_user.id,
|
||||
type=ActivityType.COMPLETE.value,
|
||||
data={
|
||||
"challenge": challenge.title,
|
||||
"points": total_points,
|
||||
"streak": participant.current_streak,
|
||||
},
|
||||
data=activity_data,
|
||||
)
|
||||
db.add(activity)
|
||||
|
||||
# If common enemy event auto-closed, log the event end with winners
|
||||
if common_enemy_closed and common_enemy_winners:
|
||||
from app.schemas.event import EVENT_INFO, COMMON_ENEMY_BONUSES
|
||||
event_end_activity = Activity(
|
||||
marathon_id=marathon_id,
|
||||
user_id=current_user.id, # Last completer triggers the close
|
||||
type=ActivityType.EVENT_END.value,
|
||||
data={
|
||||
"event_type": EventType.COMMON_ENEMY.value,
|
||||
"event_name": EVENT_INFO.get(EventType.COMMON_ENEMY, {}).get("name", "Общий враг"),
|
||||
"auto_closed": True,
|
||||
"winners": [
|
||||
{
|
||||
"user_id": w["user_id"],
|
||||
"rank": w["rank"],
|
||||
"bonus_points": COMMON_ENEMY_BONUSES.get(w["rank"], 0),
|
||||
}
|
||||
for w in common_enemy_winners
|
||||
],
|
||||
},
|
||||
)
|
||||
db.add(event_end_activity)
|
||||
|
||||
await db.commit()
|
||||
|
||||
return CompleteResult(
|
||||
@@ -314,9 +421,13 @@ async def drop_assignment(assignment_id: int, current_user: CurrentUser, db: DbS
|
||||
raise HTTPException(status_code=400, detail="Assignment is not active")
|
||||
|
||||
participant = assignment.participant
|
||||
marathon_id = assignment.challenge.game.marathon_id
|
||||
|
||||
# Calculate penalty
|
||||
penalty = points_service.calculate_drop_penalty(participant.drop_count)
|
||||
# Check active event for free drops (double_risk)
|
||||
active_event = await event_service.get_active_event(db, marathon_id)
|
||||
|
||||
# Calculate penalty (0 if double_risk event is active)
|
||||
penalty = points_service.calculate_drop_penalty(participant.drop_count, active_event)
|
||||
|
||||
# Update assignment
|
||||
assignment.status = AssignmentStatus.DROPPED.value
|
||||
@@ -328,14 +439,20 @@ async def drop_assignment(assignment_id: int, current_user: CurrentUser, db: DbS
|
||||
participant.drop_count += 1
|
||||
|
||||
# Log activity
|
||||
activity_data = {
|
||||
"challenge": assignment.challenge.title,
|
||||
"penalty": penalty,
|
||||
}
|
||||
if active_event:
|
||||
activity_data["event_type"] = active_event.type
|
||||
if active_event.type == EventType.DOUBLE_RISK.value:
|
||||
activity_data["free_drop"] = True
|
||||
|
||||
activity = Activity(
|
||||
marathon_id=assignment.challenge.game.marathon_id,
|
||||
marathon_id=marathon_id,
|
||||
user_id=current_user.id,
|
||||
type=ActivityType.DROP.value,
|
||||
data={
|
||||
"challenge": assignment.challenge.title,
|
||||
"penalty": penalty,
|
||||
},
|
||||
data=activity_data,
|
||||
)
|
||||
db.add(activity)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user