1
0
card/api/app/services/gacha.py
2025-06-01 21:39:53 +09:00

182 lines
6.5 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""ガチャシステムのロジック"""
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