Change rematch event to change game
This commit is contained in:
@@ -0,0 +1,41 @@
|
||||
"""Rename rematch event type to game_choice
|
||||
|
||||
Revision ID: 008_rename_to_game_choice
|
||||
Revises: 007_add_event_assignment_fields
|
||||
Create Date: 2024-12-15
|
||||
|
||||
"""
|
||||
from alembic import op
|
||||
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = "008_rename_to_game_choice"
|
||||
down_revision = "007_add_event_assignment_fields"
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
# Update event type from 'rematch' to 'game_choice' in events table
|
||||
op.execute("UPDATE events SET type = 'game_choice' WHERE type = 'rematch'")
|
||||
|
||||
# Update event_type in assignments table
|
||||
op.execute("UPDATE assignments SET event_type = 'game_choice' WHERE event_type = 'rematch'")
|
||||
|
||||
# Update activity data that references rematch event
|
||||
op.execute("""
|
||||
UPDATE activities
|
||||
SET data = jsonb_set(data, '{event_type}', '"game_choice"')
|
||||
WHERE data->>'event_type' = 'rematch'
|
||||
""")
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
# Revert event type from 'game_choice' to 'rematch'
|
||||
op.execute("UPDATE events SET type = 'rematch' WHERE type = 'game_choice'")
|
||||
op.execute("UPDATE assignments SET event_type = 'rematch' WHERE event_type = 'game_choice'")
|
||||
op.execute("""
|
||||
UPDATE activities
|
||||
SET data = jsonb_set(data, '{event_type}', '"rematch"')
|
||||
WHERE data->>'event_type' = 'game_choice'
|
||||
""")
|
||||
@@ -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])
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -19,7 +19,7 @@ class ActivityType(str, Enum):
|
||||
EVENT_START = "event_start"
|
||||
EVENT_END = "event_end"
|
||||
SWAP = "swap"
|
||||
REMATCH = "rematch"
|
||||
GAME_CHOICE = "game_choice"
|
||||
|
||||
|
||||
class Activity(Base):
|
||||
|
||||
@@ -12,7 +12,7 @@ class EventType(str, Enum):
|
||||
DOUBLE_RISK = "double_risk" # дропы бесплатны, x0.5 очков
|
||||
JACKPOT = "jackpot" # x3 за сложный челлендж
|
||||
SWAP = "swap" # обмен заданиями
|
||||
REMATCH = "rematch" # реванш проваленного
|
||||
GAME_CHOICE = "game_choice" # выбор игры (2-3 челленджа на выбор)
|
||||
|
||||
|
||||
class Event(Base):
|
||||
|
||||
@@ -13,7 +13,7 @@ EventTypeLiteral = Literal[
|
||||
"double_risk",
|
||||
"jackpot",
|
||||
"swap",
|
||||
"rematch",
|
||||
"game_choice",
|
||||
]
|
||||
|
||||
|
||||
@@ -32,7 +32,7 @@ class EventCreate(BaseModel):
|
||||
class EventEffects(BaseModel):
|
||||
points_multiplier: float = 1.0
|
||||
drop_free: bool = False
|
||||
special_action: str | None = None # "swap", "rematch"
|
||||
special_action: str | None = None # "swap", "game_choice"
|
||||
description: str = ""
|
||||
|
||||
|
||||
@@ -85,7 +85,7 @@ EVENT_INFO = {
|
||||
"drop_free": False,
|
||||
},
|
||||
EventType.DOUBLE_RISK: {
|
||||
"name": "Двойной риск",
|
||||
"name": "Безопасная игра",
|
||||
"description": "Дропы бесплатны, но очки x0.5",
|
||||
"default_duration": 120,
|
||||
"points_multiplier": 0.5,
|
||||
@@ -106,13 +106,13 @@ EVENT_INFO = {
|
||||
"drop_free": False,
|
||||
"special_action": "swap",
|
||||
},
|
||||
EventType.REMATCH: {
|
||||
"name": "Реванш",
|
||||
"description": "Можно переделать проваленный челлендж за 50% очков",
|
||||
"default_duration": 240,
|
||||
"points_multiplier": 0.5,
|
||||
"drop_free": False,
|
||||
"special_action": "rematch",
|
||||
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",
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@@ -21,7 +21,7 @@ AUTO_EVENT_TYPES = [
|
||||
EventType.GOLDEN_HOUR,
|
||||
EventType.DOUBLE_RISK,
|
||||
EventType.JACKPOT,
|
||||
EventType.REMATCH,
|
||||
EventType.GAME_CHOICE,
|
||||
]
|
||||
|
||||
|
||||
|
||||
@@ -25,7 +25,7 @@ class PointsService:
|
||||
EventType.GOLDEN_HOUR.value: 1.5,
|
||||
EventType.DOUBLE_RISK.value: 0.5,
|
||||
EventType.JACKPOT.value: 3.0,
|
||||
EventType.REMATCH.value: 0.5,
|
||||
# GAME_CHOICE uses 1.0 multiplier (default)
|
||||
}
|
||||
|
||||
def calculate_completion_points(
|
||||
|
||||
Reference in New Issue
Block a user