Change rematch event to change game

This commit is contained in:
2025-12-15 23:50:37 +07:00
parent 07e02ce32d
commit 339a212e57
14 changed files with 428 additions and 257 deletions

View File

@@ -640,129 +640,173 @@ async def cancel_swap_request(
return MessageResponse(message="Swap request cancelled")
@router.post("/marathons/{marathon_id}/rematch/{assignment_id}", response_model=MessageResponse)
async def rematch_assignment(
# ==================== Game Choice Event Endpoints ====================
class GameChoiceChallengeResponse(BaseModel):
"""Challenge option for game choice event"""
id: int
title: str
description: str
difficulty: str
points: int
estimated_time: int | None
proof_type: str
proof_hint: str | None
class GameChoiceChallengesResponse(BaseModel):
"""Response with available challenges for game choice"""
game_id: int
game_title: str
challenges: list[GameChoiceChallengeResponse]
class GameChoiceSelectRequest(BaseModel):
"""Request to select a challenge during game choice event"""
challenge_id: int
@router.get("/marathons/{marathon_id}/game-choice/challenges", response_model=GameChoiceChallengesResponse)
async def get_game_choice_challenges(
marathon_id: int,
assignment_id: int,
game_id: int,
current_user: CurrentUser,
db: DbSession,
):
"""Retry a dropped assignment (during rematch event)"""
"""Get 3 random challenges from a game for game choice event"""
from app.models import Game
from sqlalchemy.sql.expression import func
await get_marathon_or_404(db, marathon_id)
participant = await require_participant(db, current_user.id, marathon_id)
# Check active rematch event
# Check active game_choice event
event = await event_service.get_active_event(db, marathon_id)
if not event or event.type != EventType.REMATCH.value:
raise HTTPException(status_code=400, detail="No active rematch event")
if not event or event.type != EventType.GAME_CHOICE.value:
raise HTTPException(status_code=400, detail="No active game choice event")
# Check no current active assignment
# Get the game
result = await db.execute(
select(Assignment).where(
Assignment.participant_id == participant.id,
Assignment.status == AssignmentStatus.ACTIVE.value,
)
select(Game).where(Game.id == game_id, Game.marathon_id == marathon_id)
)
if result.scalar_one_or_none():
raise HTTPException(status_code=400, detail="You already have an active assignment")
game = result.scalar_one_or_none()
if not game:
raise HTTPException(status_code=404, detail="Game not found")
# Get the dropped assignment
# Get 3 random challenges from this game
result = await db.execute(
select(Challenge)
.where(Challenge.game_id == game_id)
.order_by(func.random())
.limit(3)
)
challenges = result.scalars().all()
if not challenges:
raise HTTPException(status_code=400, detail="No challenges available for this game")
return GameChoiceChallengesResponse(
game_id=game.id,
game_title=game.title,
challenges=[
GameChoiceChallengeResponse(
id=c.id,
title=c.title,
description=c.description,
difficulty=c.difficulty,
points=c.points,
estimated_time=c.estimated_time,
proof_type=c.proof_type,
proof_hint=c.proof_hint,
)
for c in challenges
],
)
@router.post("/marathons/{marathon_id}/game-choice/select", response_model=MessageResponse)
async def select_game_choice_challenge(
marathon_id: int,
data: GameChoiceSelectRequest,
current_user: CurrentUser,
db: DbSession,
):
"""Select a challenge during game choice event (replaces current assignment if any)"""
await get_marathon_or_404(db, marathon_id)
participant = await require_participant(db, current_user.id, marathon_id)
# Check active game_choice event
event = await event_service.get_active_event(db, marathon_id)
if not event or event.type != EventType.GAME_CHOICE.value:
raise HTTPException(status_code=400, detail="No active game choice event")
# Get the challenge
result = await db.execute(
select(Challenge)
.options(selectinload(Challenge.game))
.where(Challenge.id == data.challenge_id)
)
challenge = result.scalar_one_or_none()
if not challenge:
raise HTTPException(status_code=404, detail="Challenge not found")
# Verify challenge belongs to this marathon
if challenge.game.marathon_id != marathon_id:
raise HTTPException(status_code=400, detail="Challenge does not belong to this marathon")
# Check for current active assignment (non-event)
result = await db.execute(
select(Assignment)
.options(selectinload(Assignment.challenge))
.where(
Assignment.id == assignment_id,
Assignment.participant_id == participant.id,
Assignment.status == AssignmentStatus.DROPPED.value,
Assignment.status == AssignmentStatus.ACTIVE.value,
Assignment.is_event_assignment == False,
)
)
dropped = result.scalar_one_or_none()
if not dropped:
raise HTTPException(status_code=404, detail="Dropped assignment not found")
current_assignment = result.scalar_one_or_none()
# Create new assignment for the same challenge (with rematch event_type for 50% points)
# If there's a current assignment, replace it (free drop during this event)
old_challenge_title = None
if current_assignment:
old_challenge_title = current_assignment.challenge.title
# Mark old assignment as dropped (no penalty during game_choice event)
current_assignment.status = AssignmentStatus.DROPPED.value
current_assignment.completed_at = datetime.utcnow()
# Create new assignment with chosen challenge
new_assignment = Assignment(
participant_id=participant.id,
challenge_id=dropped.challenge_id,
challenge_id=data.challenge_id,
status=AssignmentStatus.ACTIVE.value,
event_type=EventType.REMATCH.value,
event_type=EventType.GAME_CHOICE.value,
)
db.add(new_assignment)
# Log activity
activity_data = {
"game": challenge.game.title,
"challenge": challenge.title,
"event_type": EventType.GAME_CHOICE.value,
}
if old_challenge_title:
activity_data["replaced_challenge"] = old_challenge_title
activity = Activity(
marathon_id=marathon_id,
user_id=current_user.id,
type=ActivityType.REMATCH.value,
data={
"challenge": dropped.challenge.title,
"original_assignment_id": assignment_id,
},
type=ActivityType.SPIN.value, # Treat as a spin activity
data=activity_data,
)
db.add(activity)
await db.commit()
return MessageResponse(message="Rematch started! Complete for 50% points")
class DroppedAssignmentResponse(BaseModel):
id: int
challenge: ChallengeResponse
dropped_at: datetime
class Config:
from_attributes = True
@router.get("/marathons/{marathon_id}/dropped-assignments", response_model=list[DroppedAssignmentResponse])
async def get_dropped_assignments(
marathon_id: int,
current_user: CurrentUser,
db: DbSession,
):
"""Get dropped assignments that can be rematched"""
await get_marathon_or_404(db, marathon_id)
participant = await require_participant(db, current_user.id, marathon_id)
result = await db.execute(
select(Assignment)
.options(
selectinload(Assignment.challenge).selectinload(Challenge.game)
)
.where(
Assignment.participant_id == participant.id,
Assignment.status == AssignmentStatus.DROPPED.value,
)
.order_by(Assignment.started_at.desc())
)
dropped = result.scalars().all()
return [
DroppedAssignmentResponse(
id=a.id,
challenge=ChallengeResponse(
id=a.challenge.id,
title=a.challenge.title,
description=a.challenge.description,
type=a.challenge.type,
difficulty=a.challenge.difficulty,
points=a.challenge.points,
estimated_time=a.challenge.estimated_time,
proof_type=a.challenge.proof_type,
proof_hint=a.challenge.proof_hint,
game=GameShort(
id=a.challenge.game.id,
title=a.challenge.game.title,
cover_url=None,
),
is_generated=a.challenge.is_generated,
created_at=a.challenge.created_at,
),
dropped_at=a.completed_at or a.started_at,
)
for a in dropped
]
if old_challenge_title:
return MessageResponse(message=f"Задание заменено! Теперь у вас: {challenge.title}")
else:
return MessageResponse(message=f"Задание выбрано: {challenge.title}")
@router.get("/marathons/{marathon_id}/swap-candidates", response_model=list[SwapCandidate])

View File

@@ -307,12 +307,12 @@ async def complete_assignment(
# 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 jackpot: 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]:
# Handle assignment-level event types (jackpot)
if assignment.event_type == EventType.JACKPOT.value:
# Create a mock event object for point calculation
class MockEvent:
def __init__(self, event_type):
@@ -353,8 +353,8 @@ async def complete_assignment(
"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]:
# Log event info (use assignment's event_type for jackpot, active_event for others)
if assignment.event_type == EventType.JACKPOT.value:
activity_data["event_type"] = assignment.event_type
activity_data["event_bonus"] = event_bonus
elif active_event: