Исправлены ошибки 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:
raise HTTPException(status_code=404, detail="Participant not found")
# Get active assignment
# Get active assignment (exclude event assignments)
result = await db.execute(
select(Assignment)
.options(
@@ -1059,6 +1059,7 @@ async def organizer_skip_assignment(
.where(
Assignment.participant_id == participant.id,
Assignment.status == AssignmentStatus.ACTIVE.value,
Assignment.is_event_assignment == False,
)
)
assignment = result.scalar_one_or_none()

View File

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

View File

@@ -20,7 +20,7 @@ from sqlalchemy.orm import selectinload
from app.models import (
User, Participant, Marathon, Assignment, AssignmentStatus,
ShopItem, UserInventory, ConsumableUsage, ConsumableType, Game, Challenge,
BonusAssignment, ExiledGame
BonusAssignment, ExiledGame, GameType
)
@@ -262,11 +262,15 @@ class ConsumablesService:
game_id: int,
) -> 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
- 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
@@ -279,9 +283,10 @@ class ConsumablesService:
if assignment.status != AssignmentStatus.ACTIVE.value:
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(
select(Game)
.options(selectinload(Game.challenges))
.where(
Game.id == game_id,
Game.marathon_id == marathon.id,
@@ -292,31 +297,52 @@ class ConsumablesService:
if not game:
raise HTTPException(status_code=400, detail="Game not found in this marathon")
# Get random challenge from this game
result = await db.execute(
select(Challenge)
.where(Challenge.game_id == game_id)
.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")
# Store old assignment info for logging
old_game_id = assignment.game_id
old_challenge_id = assignment.challenge_id
old_is_playthrough = assignment.is_playthrough
# Consume wild card from inventory
item = await self._consume_item(db, user, ConsumableType.WILD_CARD.value)
# Store old assignment info for logging
old_game_id = assignment.game_id
old_challenge_id = assignment.challenge_id
# Delete existing bonus assignments if any
if assignment.bonus_assignments:
for ba in assignment.bonus_assignments:
await db.delete(ba)
# Update assignment with new challenge
assignment.game_id = game_id
assignment.challenge_id = new_challenge.id
# Reset timestamps since it's a 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.challenge_id = None
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.deadline = None # Will be recalculated if needed
assignment.deadline = None
# Log usage
usage = ConsumableUsage(
@@ -328,8 +354,10 @@ class ConsumablesService:
"type": "wild_card",
"old_game_id": old_game_id,
"old_challenge_id": old_challenge_id,
"old_is_playthrough": old_is_playthrough,
"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)
@@ -337,9 +365,11 @@ class ConsumablesService:
return {
"success": True,
"game_id": game_id,
"game_name": game.name,
"challenge_id": new_challenge.id,
"challenge_title": new_challenge.title,
"game_name": game.title,
"game_type": game.game_type,
"is_playthrough": game.game_type == GameType.PLAYTHROUGH.value,
"challenge_id": new_challenge_id,
"challenge_title": new_challenge_title,
}
async def use_lucky_dice(

View File

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