From 72089d1b474ad93d24ebb654e6db11c94268d177 Mon Sep 17 00:00:00 2001 From: "mamonov.ep" Date: Fri, 16 Jan 2026 18:10:40 +0300 Subject: [PATCH] =?UTF-8?q?=D0=98=D1=81=D0=BF=D1=80=D0=B0=D0=B2=D0=BB?= =?UTF-8?q?=D0=B5=D0=BD=D1=8B=20=D0=BE=D1=88=D0=B8=D0=B1=D0=BA=D0=B8=20Wil?= =?UTF-8?q?d=20Card=20=D0=B8=20skip-assignment?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 --- backend/app/api/v1/marathons.py | 3 +- backend/app/schemas/game.py | 6 +- backend/app/services/consumables.py | 86 +++++++++++++++++++---------- backend/app/services/points.py | 6 +- 4 files changed, 68 insertions(+), 33 deletions(-) diff --git a/backend/app/api/v1/marathons.py b/backend/app/api/v1/marathons.py index c2ef81a..7543c87 100644 --- a/backend/app/api/v1/marathons.py +++ b/backend/app/api/v1/marathons.py @@ -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() diff --git a/backend/app/schemas/game.py b/backend/app/schemas/game.py index ef73e77..077d87a 100644 --- a/backend/app/schemas/game.py +++ b/backend/app/schemas/game.py @@ -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 diff --git a/backend/app/services/consumables.py b/backend/app/services/consumables.py index 442c5f0..69f160b 100644 --- a/backend/app/services/consumables.py +++ b/backend/app/services/consumables.py @@ -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( diff --git a/backend/app/services/points.py b/backend/app/services/points.py index d16c1af..f5e5ffb 100644 --- a/backend/app/services/points.py +++ b/backend/app/services/points.py @@ -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