1
0
syui 9f9208e160
Integrate ai.card Rust API with ai.gpt MCP and implement daily limit system
### Major Changes:
- **Rust Migration**: Move api-rs to root directory, rename binary to 'aicard'
- **MCP Integration**: Add card tools to ai.gpt MCP server (get_user_cards, draw_card, get_draw_status)
- **Daily Limit System**: Implement 2-day interval card drawing limits in API and iOS
- **iOS Enhancements**: Add DrawStatusView, backup functionality, and limit integration

### Technical Details:
- ai.gpt MCP now has 20 tools including 3 card-related tools
- ServiceClient enhanced with missing card API methods
- iOS app includes daily limit UI and atproto OAuth backup features
- Database migration for last_draw_date field
- Complete feature parity between web and iOS implementations

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-06-08 10:35:43 +09:00

180 lines
6.4 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
from app.services.card_master import card_master_service
class GachaService:
"""ガチャシステムのサービスクラス"""
def __init__(self, session: AsyncSession):
self.session = session
self.user_repo = UserRepository(session)
self.card_repo = CardRepository(session)
self.unique_repo = UniqueCardRepository(session)
# Load card info from external source
self.CARD_INFO = card_master_service.get_card_info()
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かどうか
Raises:
ValueError: If user cannot draw due to daily limit
"""
# Check if user can draw (daily limit)
can_draw = await self.user_repo.can_draw_card(user_did, settings.draw_limit_days)
if not can_draw:
next_draw_time = await self.user_repo.get_next_draw_time(user_did, settings.draw_limit_days)
if next_draw_time:
raise ValueError(f"カードを引けるのは{next_draw_time.strftime('%Y-%m-%d %H:%M:%S')}以降です。")
else:
raise ValueError("現在カードを引くことができません。")
# 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)
# Update user's last draw date
await self.user_repo.update_last_draw_date(user.id)
# 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