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