"""ガチャシステムのロジック""" import random import uuid from datetime import datetime from typing import Optional, Tuple from sqlalchemy.ext.asyncio import AsyncSession from app.core.config import settings from app.models.card import Card, CardRarity from app.repositories.user import UserRepository from app.repositories.card import CardRepository, UniqueCardRepository from app.db.models import DrawHistory class GachaService: """ガチャシステムのサービスクラス""" # カード基本情報(ai.jsonから) CARD_INFO = { 0: {"name": "ai", "base_cp_range": (10, 100)}, 1: {"name": "dream", "base_cp_range": (20, 120)}, 2: {"name": "radiance", "base_cp_range": (30, 130)}, 3: {"name": "neutron", "base_cp_range": (40, 140)}, 4: {"name": "sun", "base_cp_range": (50, 150)}, 5: {"name": "night", "base_cp_range": (25, 125)}, 6: {"name": "snow", "base_cp_range": (15, 115)}, 7: {"name": "thunder", "base_cp_range": (60, 160)}, 8: {"name": "ultimate", "base_cp_range": (80, 180)}, 9: {"name": "sword", "base_cp_range": (70, 170)}, 10: {"name": "destruction", "base_cp_range": (90, 190)}, 11: {"name": "earth", "base_cp_range": (35, 135)}, 12: {"name": "galaxy", "base_cp_range": (65, 165)}, 13: {"name": "create", "base_cp_range": (75, 175)}, 14: {"name": "supernova", "base_cp_range": (100, 200)}, 15: {"name": "world", "base_cp_range": (85, 185)}, } def __init__(self, session: AsyncSession): self.session = session self.user_repo = UserRepository(session) self.card_repo = CardRepository(session) self.unique_repo = UniqueCardRepository(session) async def draw_card(self, user_did: str, is_paid: bool = False) -> Tuple[Card, bool]: """ カードを抽選する Args: user_did: ユーザーのDID is_paid: 課金ガチャかどうか Returns: (Card, is_unique): 抽選されたカードとuniqueかどうか """ # Get or create user user = await self.user_repo.get_or_create(user_did) # レアリティ抽選 rarity = self._determine_rarity(is_paid) # カード種類を選択 card_id = self._select_card_id(rarity) # CPを決定 cp = self._calculate_cp(card_id, rarity) # uniqueカードチェック is_unique = False if rarity == CardRarity.UNIQUE: # uniqueカードの場合、利用可能かチェック is_available = await self.unique_repo.is_card_available(card_id) if not is_available: # 利用不可の場合はキラカードに変更 rarity = CardRarity.KIRA else: is_unique = True # データベースにカードを保存 user_card = await self.card_repo.create_user_card( user_id=user.id, card_id=card_id, cp=cp, status=rarity, skill=self._get_skill_for_card(card_id, rarity), is_unique=is_unique ) # 抽選履歴を保存 draw_history = DrawHistory( user_id=user.id, card_id=card_id, status=rarity, cp=cp, is_paid=is_paid ) self.session.add(draw_history) # API用のCardモデルに変換 card = Card( id=card_id, cp=cp, status=rarity, skill=user_card.skill, owner_did=user_did, obtained_at=user_card.obtained_at, is_unique=is_unique, unique_id=str(user_card.unique_id) if user_card.unique_id else None ) # atproto PDSに同期(非同期で実行) try: from app.services.card_sync import CardSyncService sync_service = CardSyncService(self.session) await sync_service.sync_card_to_pds(user_card, user_did) except Exception: # 同期失敗してもガチャは成功とする pass return card, is_unique def _determine_rarity(self, is_paid: bool) -> CardRarity: """レアリティを抽選する""" rand = random.random() * 100 if is_paid: # 課金ガチャは確率アップ if rand < settings.prob_unique * 2: # 0.0002% return CardRarity.UNIQUE elif rand < settings.prob_kira * 2: # 0.2% return CardRarity.KIRA elif rand < 0.5: # 0.5% return CardRarity.SUPER_RARE elif rand < 5: # 5% return CardRarity.RARE else: # 通常ガチャ if rand < settings.prob_unique: return CardRarity.UNIQUE elif rand < settings.prob_kira: return CardRarity.KIRA elif rand < settings.prob_super_rare: return CardRarity.SUPER_RARE elif rand < settings.prob_rare: return CardRarity.RARE return CardRarity.NORMAL def _select_card_id(self, rarity: CardRarity) -> int: """レアリティに応じてカードIDを選択""" if rarity in [CardRarity.UNIQUE, CardRarity.KIRA]: # レアカードは特定のIDに偏らせる weights = [1, 1, 2, 2, 3, 1, 1, 3, 5, 4, 5, 2, 3, 4, 6, 5] else: # 通常は均等 weights = [1] * 16 return random.choices(range(16), weights=weights)[0] def _calculate_cp(self, card_id: int, rarity: CardRarity) -> int: """カードのCPを計算""" base_range = self.CARD_INFO[card_id]["base_cp_range"] base_cp = random.randint(*base_range) # レアリティボーナス multiplier = { CardRarity.NORMAL: 1.0, CardRarity.RARE: 1.5, CardRarity.SUPER_RARE: 2.0, CardRarity.KIRA: 3.0, CardRarity.UNIQUE: 5.0, }[rarity] return int(base_cp * multiplier) def _get_skill_for_card(self, card_id: int, rarity: CardRarity) -> Optional[str]: """カードのスキルを取得""" if rarity in [CardRarity.KIRA, CardRarity.UNIQUE]: # TODO: スキル情報を返す return f"skill_{card_id}_{rarity.value}" return None