Files
dodep-simulator/rpu.py
T

511 lines
18 KiB
Python

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(),
}