Исправлены ошибки Wild Card и skip-assignment

- Wild Card: исправлен game.name → game.title
- Wild Card: добавлена поддержка игр типа playthrough
- points.py: добавлена проверка на None для challenge_points
- PlaythroughInfo: поля сделаны Optional (description, points, proof_type)
- organizer_skip_assignment: добавлен фильтр is_event_assignment

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-01-16 18:10:40 +03:00
parent 9cfe99ff7e
commit 72089d1b47
4 changed files with 68 additions and 33 deletions

View File

@@ -1049,7 +1049,7 @@ async def organizer_skip_assignment(
if not participant: if not participant:
raise HTTPException(status_code=404, detail="Participant not found") raise HTTPException(status_code=404, detail="Participant not found")
# Get active assignment # Get active assignment (exclude event assignments)
result = await db.execute( result = await db.execute(
select(Assignment) select(Assignment)
.options( .options(
@@ -1059,6 +1059,7 @@ async def organizer_skip_assignment(
.where( .where(
Assignment.participant_id == participant.id, Assignment.participant_id == participant.id,
Assignment.status == AssignmentStatus.ACTIVE.value, Assignment.status == AssignmentStatus.ACTIVE.value,
Assignment.is_event_assignment == False,
) )
) )
assignment = result.scalar_one_or_none() assignment = result.scalar_one_or_none()

View File

@@ -87,7 +87,7 @@ class GameResponse(GameBase):
class PlaythroughInfo(BaseModel): class PlaythroughInfo(BaseModel):
"""Информация о прохождении для игр типа playthrough""" """Информация о прохождении для игр типа playthrough"""
description: str description: str | None = None
points: int points: int | None = None
proof_type: str proof_type: str | None = None
proof_hint: str | None = None proof_hint: str | None = None

View File

@@ -20,7 +20,7 @@ from sqlalchemy.orm import selectinload
from app.models import ( from app.models import (
User, Participant, Marathon, Assignment, AssignmentStatus, User, Participant, Marathon, Assignment, AssignmentStatus,
ShopItem, UserInventory, ConsumableUsage, ConsumableType, Game, Challenge, ShopItem, UserInventory, ConsumableUsage, ConsumableType, Game, Challenge,
BonusAssignment, ExiledGame BonusAssignment, ExiledGame, GameType
) )
@@ -262,11 +262,15 @@ class ConsumablesService:
game_id: int, game_id: int,
) -> dict: ) -> dict:
""" """
Use Wild Card - choose a game and get a random challenge from it. Use Wild Card - choose a game and switch to it.
- Current assignment is replaced For challenges game type:
- New challenge is randomly selected from the chosen game - New challenge is randomly selected from the chosen game
- Game must be in the marathon - Assignment becomes a regular challenge
For playthrough game type:
- Assignment becomes a playthrough of the chosen game
- Bonus assignments are created from game's challenges
Returns: dict with new assignment info Returns: dict with new assignment info
@@ -279,9 +283,10 @@ class ConsumablesService:
if assignment.status != AssignmentStatus.ACTIVE.value: if assignment.status != AssignmentStatus.ACTIVE.value:
raise HTTPException(status_code=400, detail="Can only use wild card on active assignments") raise HTTPException(status_code=400, detail="Can only use wild card on active assignments")
# Verify game is in this marathon # Verify game is in this marathon and load challenges
result = await db.execute( result = await db.execute(
select(Game) select(Game)
.options(selectinload(Game.challenges))
.where( .where(
Game.id == game_id, Game.id == game_id,
Game.marathon_id == marathon.id, Game.marathon_id == marathon.id,
@@ -292,31 +297,52 @@ class ConsumablesService:
if not game: if not game:
raise HTTPException(status_code=400, detail="Game not found in this marathon") raise HTTPException(status_code=400, detail="Game not found in this marathon")
# Get random challenge from this game # Store old assignment info for logging
result = await db.execute( old_game_id = assignment.game_id
select(Challenge) old_challenge_id = assignment.challenge_id
.where(Challenge.game_id == game_id) old_is_playthrough = assignment.is_playthrough
.order_by(func.random())
.limit(1)
)
new_challenge = result.scalar_one_or_none()
if not new_challenge:
raise HTTPException(status_code=400, detail="No challenges available for this game")
# Consume wild card from inventory # Consume wild card from inventory
item = await self._consume_item(db, user, ConsumableType.WILD_CARD.value) item = await self._consume_item(db, user, ConsumableType.WILD_CARD.value)
# Store old assignment info for logging # Delete existing bonus assignments if any
old_game_id = assignment.game_id if assignment.bonus_assignments:
old_challenge_id = assignment.challenge_id for ba in assignment.bonus_assignments:
await db.delete(ba)
# Update assignment with new challenge new_challenge_id = None
new_challenge_title = None
if game.game_type == GameType.PLAYTHROUGH.value:
# Switch to playthrough mode
assignment.game_id = game_id assignment.game_id = game_id
assignment.challenge_id = new_challenge.id assignment.challenge_id = None
# Reset timestamps since it's a new challenge assignment.is_playthrough = True
# Create bonus assignments from game's challenges
for ch in game.challenges:
bonus = BonusAssignment(
main_assignment_id=assignment.id,
challenge_id=ch.id,
)
db.add(bonus)
else:
# Switch to challenge mode - get random challenge
if not game.challenges:
raise HTTPException(status_code=400, detail="No challenges available for this game")
new_challenge = random.choice(game.challenges)
new_challenge_id = new_challenge.id
new_challenge_title = new_challenge.title
assignment.game_id = game_id
assignment.challenge_id = new_challenge_id
assignment.is_playthrough = False
# Reset timestamps since it's a new assignment
assignment.started_at = datetime.utcnow() assignment.started_at = datetime.utcnow()
assignment.deadline = None # Will be recalculated if needed assignment.deadline = None
# Log usage # Log usage
usage = ConsumableUsage( usage = ConsumableUsage(
@@ -328,8 +354,10 @@ class ConsumablesService:
"type": "wild_card", "type": "wild_card",
"old_game_id": old_game_id, "old_game_id": old_game_id,
"old_challenge_id": old_challenge_id, "old_challenge_id": old_challenge_id,
"old_is_playthrough": old_is_playthrough,
"new_game_id": game_id, "new_game_id": game_id,
"new_challenge_id": new_challenge.id, "new_challenge_id": new_challenge_id,
"new_is_playthrough": game.game_type == GameType.PLAYTHROUGH.value,
}, },
) )
db.add(usage) db.add(usage)
@@ -337,9 +365,11 @@ class ConsumablesService:
return { return {
"success": True, "success": True,
"game_id": game_id, "game_id": game_id,
"game_name": game.name, "game_name": game.title,
"challenge_id": new_challenge.id, "game_type": game.game_type,
"challenge_title": new_challenge.title, "is_playthrough": game.game_type == GameType.PLAYTHROUGH.value,
"challenge_id": new_challenge_id,
"challenge_title": new_challenge_title,
} }
async def use_lucky_dice( async def use_lucky_dice(

View File

@@ -66,7 +66,7 @@ class PointsService:
def calculate_drop_penalty( def calculate_drop_penalty(
self, self,
consecutive_drops: int, consecutive_drops: int,
challenge_points: int, challenge_points: int | None,
event: Event | None = None event: Event | None = None
) -> int: ) -> int:
""" """
@@ -80,6 +80,10 @@ class PointsService:
Returns: Returns:
Penalty points to subtract Penalty points to subtract
""" """
# No penalty if no points defined
if challenge_points is None:
return 0
# Double risk event = free drops # Double risk event = free drops
if event and event.type == EventType.DOUBLE_RISK.value: if event and event.type == EventType.DOUBLE_RISK.value:
return 0 return 0