import json import random import math from datetime import datetime, date from typing import Dict, Optional, Tuple from sqlalchemy import select, func from sqlalchemy.ext.asyncio import AsyncSession from database import User, UserRPU, InventoryItem from promo_phase import get_promo_effect_for_user, get_current_phase BASE_RARITY_WEIGHTS = { "Consumer Grade": 10000, "Industrial Grade": 5000, "Mil-Spec": 2500, "Mil-Spec Grade": 2500, "Restricted": 625, "Classified": 125, "Covert": 25, "Rare Special Item": 5, "Extraordinary": 5 } RARITY_TO_FIELD = { "Consumer Grade": "consumer_multiplier", "Industrial Grade": "industrial_multiplier", "Mil-Spec": "mil_spec_multiplier", "Mil-Spec Grade": "mil_spec_multiplier", "Restricted": "restricted_multiplier", "Classified": "classified_multiplier", "Covert": "covert_multiplier", "Rare Special Item": "rare_special_multiplier", "Extraordinary": "rare_special_multiplier" } RARITY_RANK = { "Consumer Grade": 0, "Industrial Grade": 1, "Mil-Spec": 2, "Mil-Spec Grade": 2, "Restricted": 3, "Classified": 4, "Covert": 5, "Rare Special Item": 6, "Extraordinary": 6 } CEILING_BASE_MULTIPLIER = 3.0 CEILING_MIN = 1000.0 CEILING_SESSION_HISTORY = {0: 5.0, 1: 4.0, 2: 3.5, -1: 3.0} BUDGET_MAX = 100.0 BUDGET_CONSUMPTION_RATE = 0.001 BUDGET_REGEN_PER_OPEN = 0.5 BUDGET_LOW_THRESHOLD = 20.0 COMEBACK_TRIGGER_COUNT = 5 COMEBACK_TRIGGER_VALUE_RATIO = 0.3 COMEBACK_OPENINGS = 5 COMEBACK_MULTIPLIER_MAX = 3.0 HOT_THRESHOLD_SPENT = 10000 HOT_THRESHOLD_WITHDRAWAL = 0.1 COLD_DAYS_INACTIVE = 7 COLD_SPENT_PER_DAY = 100 _BATCH_MODE = False _BATCH_WEIGHTS_CACHE = {} def _rpu_commit_or_flush(db): if _BATCH_MODE: db.flush() else: db.commit() async def _rpu_commit_or_flush_async(db: AsyncSession): if _BATCH_MODE: await db.flush() else: await db.commit() def _batch_cache_get(user_id: int): if _BATCH_MODE and user_id in _BATCH_WEIGHTS_CACHE: return _BATCH_WEIGHTS_CACHE[user_id] return None def _batch_cache_set(user_id: int, weights: dict): if _BATCH_MODE: _BATCH_WEIGHTS_CACHE[user_id] = weights def _batch_cache_clear(user_id: int = None): if user_id is not None: _BATCH_WEIGHTS_CACHE.pop(user_id, None) else: _BATCH_WEIGHTS_CACHE.clear() async def get_user_rpu(db: AsyncSession, user_id: int) -> UserRPU: result = await db.execute(select(UserRPU).where(UserRPU.user_id == user_id)) rpu = result.scalar_one_or_none() if not rpu: rpu = UserRPU(user_id=user_id) db.add(rpu) await _rpu_commit_or_flush_async(db) await db.refresh(rpu) return rpu async def check_and_reset_session(db: AsyncSession, user_id: int): rpu = await get_user_rpu(db, user_id) today = date.today() if rpu.session_reset_date is None: rpu.session_reset_date = datetime.utcnow() rpu.luck_budget = BUDGET_MAX rpu.session_spent = 0; rpu.session_won = 0 rpu.ceiling_break_session = 0 rpu.comeback_active = False; rpu.comeback_openings_left = 0 rpu.comeback_multiplier = 1.0; rpu.consecutive_loss_count = 0 rpu.consecutive_loss_value = 0.0 await _rpu_commit_or_flush_async(db) return last_reset = rpu.session_reset_date.date() if last_reset < today: rpu.session_reset_date = datetime.utcnow() rpu.luck_budget = BUDGET_MAX rpu.session_spent = 0; rpu.session_won = 0 rpu.ceiling_break_session = 0 rpu.comeback_active = False; rpu.comeback_openings_left = 0 rpu.comeback_multiplier = 1.0; rpu.consecutive_loss_count = 0 rpu.consecutive_loss_value = 0.0 await _rpu_commit_or_flush_async(db) async def calculate_ceiling(db: AsyncSession, user_id: int) -> float: result = await db.execute(select(User).where(User.id == user_id)) user = result.scalar_one_or_none() if not user: return CEILING_MIN total_dep = max(0.0, user.total_deposited) rpu = await get_user_rpu(db, user_id) session_count = 0 if rpu.session_reset_date and user.created_at: days_since = (datetime.utcnow() - user.created_at).days session_count = max(0, days_since) mult = CEILING_SESSION_HISTORY.get(session_count, CEILING_SESSION_HISTORY[-1]) mult *= rpu.ceiling_multiplier ceiling = total_dep * mult return max(CEILING_MIN, ceiling) async def get_total_inventory_value(db: AsyncSession, user_id: int) -> float: result = await db.execute( select(func.coalesce(func.sum(InventoryItem.price_rub), 0)) .where(InventoryItem.user_id == user_id) ) return float(result.scalar() or 0) async def get_user_total_value(db: AsyncSession, user_id: int) -> float: result = await db.execute(select(User).where(User.id == user_id)) user = result.scalar_one_or_none() balance = user.balance if user else 0 inv = await get_total_inventory_value(db, user_id) return balance + inv async def get_ceiling_luck_factor(db: AsyncSession, user_id: int) -> float: result = await db.execute(select(User).where(User.id == user_id)) user = result.scalar_one_or_none() if not user: return 1.0 ceiling = await calculate_ceiling(db, user_id) inv_value = await get_total_inventory_value(db, user_id) withdrawn = user.total_withdrawn or 0 extracted_value = inv_value + withdrawn if ceiling <= 0: return 1.0 ratio = extracted_value / ceiling if ratio < 0.8: return 1.0 if ratio >= 1.0: return 0.3 return 1.0 - (ratio - 0.8) / 0.2 * 0.7 async def check_ceiling_breach(db: AsyncSession, user_id: int, item_price: float, case_price: float) -> bool: result = await db.execute(select(User).where(User.id == user_id)) user = result.scalar_one_or_none() if not user: return False ceiling = await calculate_ceiling(db, user_id) total_value = user.balance + await get_total_inventory_value(db, user_id) if total_value < ceiling: return False if item_price < ceiling * 0.5: return False return True async def apply_ceiling_breach(db: AsyncSession, user_id: int, item_price: float): rpu = await get_user_rpu(db, user_id) rpu.ceiling_multiplier = min(3.0, rpu.ceiling_multiplier * 1.5) rpu.ceiling_break_count += 1 rpu.ceiling_break_session += 1 await _rpu_commit_or_flush_async(db) async def apply_ceiling_withdrawal_penalty(db: AsyncSession, user_id: int): rpu = await get_user_rpu(db, user_id) rpu.ceiling_multiplier = max(0.5, rpu.ceiling_multiplier * 0.7) await _rpu_commit_or_flush_async(db) async def consume_luck_budget(db: AsyncSession, user_id: int, item_price: float): rpu = await get_user_rpu(db, user_id) rpu.luck_budget = max(0.0, rpu.luck_budget - item_price * BUDGET_CONSUMPTION_RATE) await _rpu_commit_or_flush_async(db) async def regen_luck_budget(db: AsyncSession, user_id: int): rpu = await get_user_rpu(db, user_id) rpu.luck_budget = min(BUDGET_MAX, rpu.luck_budget + BUDGET_REGEN_PER_OPEN) await _rpu_commit_or_flush_async(db) def get_budget_factor(rpu: UserRPU) -> float: if rpu.luck_budget >= BUDGET_LOW_THRESHOLD: return 1.0 return max(0.3, rpu.luck_budget / BUDGET_LOW_THRESHOLD) async def update_comeback_tracking(db: AsyncSession, user_id: int, spent: float, received_value: float): rpu = await get_user_rpu(db, user_id) result = await db.execute(select(User).where(User.id == user_id)) user = result.scalar_one_or_none() if not user: return if received_value < spent * 0.8: rpu.consecutive_loss_count += 1 rpu.consecutive_loss_value += (spent - received_value) else: rpu.consecutive_loss_count = 0 rpu.consecutive_loss_value = 0.0 total_dep = max(1.0, user.total_deposited) loss_ratio = rpu.consecutive_loss_value / total_dep if (rpu.consecutive_loss_count >= COMEBACK_TRIGGER_COUNT or loss_ratio >= COMEBACK_TRIGGER_VALUE_RATIO): if not rpu.comeback_active: rpu.comeback_active = True rpu.comeback_openings_left = COMEBACK_OPENINGS rpu.comeback_multiplier = COMEBACK_MULTIPLIER_MAX await _rpu_commit_or_flush_async(db) async def apply_comeback(db: AsyncSession, user_id: int) -> float: rpu = await get_user_rpu(db, user_id) if not rpu.comeback_active or rpu.comeback_openings_left <= 0: return 1.0 mult = rpu.comeback_multiplier rpu.comeback_openings_left -= 1 if rpu.comeback_openings_left <= 0: rpu.comeback_active = False rpu.comeback_multiplier = 1.0 rpu.consecutive_loss_count = 0 rpu.consecutive_loss_value = 0.0 await _rpu_commit_or_flush_async(db) return mult async def analyze_hot_cold(db: AsyncSession, user_id: int) -> Tuple[bool, bool, float]: result = await db.execute(select(User).where(User.id == user_id)) user = result.scalar_one_or_none() if not user: return False, True, 0.0 total_dep = max(0.0, user.total_deposited) total_wd = max(0.0, user.total_withdrawn) wd_ratio = total_wd / max(1.0, total_dep) is_hot = (total_dep >= HOT_THRESHOLD_SPENT and wd_ratio < HOT_THRESHOLD_WITHDRAWAL) rpu = await get_user_rpu(db, user_id) is_cold = False if rpu.last_activity_date: days_since = (datetime.utcnow() - rpu.last_activity_date).days if days_since >= COLD_DAYS_INACTIVE: is_cold = True else: is_cold = True days_active = max(1, (datetime.utcnow() - user.created_at).days) spent_per_day = total_dep / days_active if spent_per_day < COLD_SPENT_PER_DAY and total_dep > 0: is_cold = True hot_score = total_dep - (total_wd * 2) rpu.hot_score = hot_score rpu.last_activity_date = datetime.utcnow() await _rpu_commit_or_flush_async(db) return is_hot, is_cold, wd_ratio async def check_withdrawal_penalty(db: AsyncSession, user_id: int): result = await db.execute(select(User).where(User.id == user_id)) user = result.scalar_one_or_none() if not user: return total_dep = max(1.0, user.total_deposited) total_wd = max(0.0, user.total_withdrawn) wd_ratio = total_wd / total_dep if wd_ratio > 0.5: rpu = await get_user_rpu(db, user_id) rpu.luck_multiplier = max(0.3, rpu.luck_multiplier - wd_ratio * 0.3) rpu.ceiling_multiplier = max(0.5, rpu.ceiling_multiplier * 0.7) await _rpu_commit_or_flush_async(db) def calculate_rpu_level(rpu: UserRPU) -> float: multipliers = [ rpu.consumer_multiplier, rpu.industrial_multiplier, rpu.mil_spec_multiplier, rpu.restricted_multiplier, rpu.classified_multiplier, rpu.covert_multiplier, rpu.rare_special_multiplier ] avg = sum(multipliers) / len(multipliers) if avg >= 1.0: level = 50 - ((avg - 1.0) / 2.0) * 100 else: level = 50 + ((1.0 - avg) / 0.9) * 50 return max(0, min(100, level)) def get_rpu_description(level: float) -> str: if level < 20: return "🍀 Очень везучий" elif level < 35: return "😊 Везучий" elif level < 45: return "🙂 Немного везучий" elif level < 55: return "😐 Обычный" elif level < 70: return "🙁 Немного невезучий" elif level < 85: return "😟 Невезучий" else: return "💀 Проклятый" async def get_adjusted_weights(db: AsyncSession, user_id: int) -> Dict[str, float]: cached = _batch_cache_get(user_id) if cached is not None: return cached await check_and_reset_session(db, user_id) rpu = await get_user_rpu(db, user_id) level = calculate_rpu_level(rpu) is_hot, is_cold, wd_ratio = await analyze_hot_cold(db, user_id) promo = get_promo_effect_for_user(user_id, rpu.hot_score, wd_ratio, is_cold) comeback_mult = await apply_comeback(db, user_id) budget_factor = get_budget_factor(rpu) ceiling_factor = await get_ceiling_luck_factor(db, user_id) adjusted = {} for rarity, base_weight in BASE_RARITY_WEIGHTS.items(): field = RARITY_TO_FIELD.get(rarity) base_multiplier = getattr(rpu, field, 1.0) if field else 1.0 rarity_index = RARITY_RANK.get(rarity, 0) if level < 50: luck_factor = 1.0 + ((50 - level) / 50) * rarity_index * 0.3 else: luck_factor = 1.0 - ((level - 50) / 50) * rarity_index * 0.2 luck_factor = max(0.1, min(3.0, luck_factor)) final = base_multiplier * rpu.luck_multiplier * luck_factor final *= (1.0 + promo["luck_modifier"]) final *= budget_factor final *= comeback_mult final *= ceiling_factor if rarity_index >= 3: final *= (1.0 + promo.get("rare_chance_modifier", 0)) final = max(0.05, min(5.0, final)) adjusted[rarity] = base_weight * final _batch_cache_set(user_id, adjusted) return adjusted async def auto_adjust_rpu(db: AsyncSession, user_id: int): rpu = await get_user_rpu(db, user_id) if not rpu.auto_adjust: return result = await db.execute(select(User).where(User.id == user_id)) user = result.scalar_one_or_none() if not user: return if rpu.total_opened > 0 and rpu.total_spent > 0: roi = rpu.total_value_received / rpu.total_spent if roi < 0.4: boost = (0.4 - roi) * 2 rpu.luck_multiplier = min(2.5, rpu.luck_multiplier + boost) for r in ['restricted_multiplier', 'classified_multiplier', 'covert_multiplier', 'rare_special_multiplier']: setattr(rpu, r, min(2.0, getattr(rpu, r) + boost * 0.3)) elif roi > 2.0: penalty = (roi - 2.0) * 0.5 rpu.luck_multiplier = max(0.3, rpu.luck_multiplier - penalty) for r in ['restricted_multiplier', 'classified_multiplier', 'covert_multiplier', 'rare_special_multiplier']: setattr(rpu, r, max(0.3, getattr(rpu, r) - penalty * 0.5)) streak = rpu.current_streak if streak <= -5: boost = min(0.3, abs(streak) * 0.03) rpu.luck_multiplier = min(2.5, rpu.luck_multiplier + boost) rpu.classified_multiplier = min(2.0, rpu.classified_multiplier + boost * 0.5) rpu.covert_multiplier = min(1.8, rpu.covert_multiplier + boost * 0.4) rpu.rare_special_multiplier = min(1.5, rpu.rare_special_multiplier + boost * 0.3) if streak >= 5: penalty = min(0.2, streak * 0.02) rpu.luck_multiplier = max(0.3, rpu.luck_multiplier - penalty) for r in ['classified_multiplier', 'covert_multiplier', 'rare_special_multiplier']: setattr(rpu, r, max(0.2, getattr(rpu, r) - penalty * 0.5)) if rpu.total_opened > 50: rpu.classified_multiplier = min(1.5, rpu.classified_multiplier + 0.05) rpu.covert_multiplier = min(1.3, rpu.covert_multiplier + 0.03) for field in ['consumer_multiplier', 'industrial_multiplier', 'mil_spec_multiplier', 'restricted_multiplier', 'classified_multiplier', 'covert_multiplier', 'rare_special_multiplier']: setattr(rpu, field, max(0.1, min(3.0, getattr(rpu, field)))) rpu.luck_multiplier = max(0.1, min(3.0, rpu.luck_multiplier)) rpu.last_adjustment = datetime.utcnow() await _rpu_commit_or_flush_async(db) async def update_rpu_stats(db: AsyncSession, user_id: int, spent: float, opened: int = 1, received_value: float = 0.0, rarity: str = None, is_rare: bool = False): rpu = await get_user_rpu(db, user_id) result = await db.execute(select(User).where(User.id == user_id)) user = result.scalar_one_or_none() rpu.total_spent += spent rpu.total_opened += opened rpu.total_value_received += received_value rpu.session_spent += spent rpu.session_won += received_value if is_rare or received_value > spent * 3: rpu.current_streak = min(20, rpu.current_streak + 1) else: rpu.current_streak = max(-20, rpu.current_streak - 1) if rpu.current_streak > rpu.best_streak: rpu.best_streak = rpu.current_streak if rpu.current_streak < rpu.worst_streak: rpu.worst_streak = rpu.current_streak await consume_luck_budget(db, user_id, received_value) await regen_luck_budget(db, user_id) await update_comeback_tracking(db, user_id, spent, received_value) if user and is_rare and received_value > 0: if await check_ceiling_breach(db, user_id, received_value, spent): await apply_ceiling_breach(db, user_id, received_value) await analyze_hot_cold(db, user_id) await _rpu_commit_or_flush_async(db) if rpu.auto_adjust: await auto_adjust_rpu(db, user_id) async def get_rpu_v2_info(db: AsyncSession, user_id: int) -> dict: await check_and_reset_session(db, user_id) rpu = await get_user_rpu(db, user_id) result = await db.execute(select(User).where(User.id == user_id)) user = result.scalar_one_or_none() if not user: return {} ceiling = await calculate_ceiling(db, user_id) inv_value = await get_total_inventory_value(db, user_id) total_value = user.balance + inv_value is_hot, is_cold, wd_ratio = await analyze_hot_cold(db, user_id) session_count = max(0, (datetime.utcnow() - user.created_at).days) if user.created_at else 0 return { "rpu_level": round(calculate_rpu_level(rpu)), "description": get_rpu_description(calculate_rpu_level(rpu)), "luck_multiplier": rpu.luck_multiplier, "luck_budget": round(rpu.luck_budget, 1), "ceiling": round(ceiling, 2), "ceiling_multiplier": round(rpu.ceiling_multiplier, 2), "ceiling_breach_total": rpu.ceiling_break_count, "ceiling_breach_session": rpu.ceiling_break_session, "total_value": round(total_value, 2), "balance": round(user.balance, 2), "inventory_value": round(inv_value, 2), "total_deposited": round(user.total_deposited, 2), "total_withdrawn": round(user.total_withdrawn, 2), "session": { "number": session_count, "spent": round(rpu.session_spent, 2), "won": round(rpu.session_won, 2), }, "comeback": { "active": rpu.comeback_active, "openings_left": rpu.comeback_openings_left, "multiplier": round(rpu.comeback_multiplier, 2), }, "streak": { "current": rpu.current_streak, "best": rpu.best_streak, "worst": rpu.worst_streak, }, "hot_cold": { "is_hot": is_hot, "is_cold": is_cold, "hot_score": round(rpu.hot_score, 2), "withdrawal_ratio": round(wd_ratio, 3), }, "promo_phase": get_current_phase(), }