Files
dodep-simulator/promo_phase.py
T
sasheg 47c7f17c90 RPU v2, promo/bank система, админка, оптимизации
— RPU v2: бюджет удачи, потолок (ceiling), комбек после
  проигрышей, hot/cold анализ, интеграция с promo-фазами
— Bank API: mock-банк для карты (депозит/вывод с балансом 1000₽)
— Promo-сервис: автогенерация промокодов каждый час, типы
  (card_to_site, luck_boost, ceiling_boost, free_case, reset_streak)
— Promo-фазы: LUCKY/NEUTRAL/UNLUCKY, переключение каждые 1-6ч
— Admin: история транзакций (user_history), управление промокодами,
  управление promo-фазами, API фильтров/коллекций, RPU info
— Database: WAL mode, новые поля RPU v2, TransactionLog,
  карточный баланс, статистики (streaks, ceiling, budget)
— Frontend: /deposit, /promo/activate, /withdraw, /card-balance,
  /online-count, /rpu-info, lifespan вместо startup event
— UI: user_history.html, улучшения профиля, кейсов, навигации
— Cases: обновление cases.json (1892 строки новых данных)
— Achievements: кэш коллекций, оптимизация is_custom проверок
— Sounds: дополнительные звуковые эффекты
— Upgrade: интеграция с RPU (множители, streaks)
— .gitignore: исключены .db, .bak, __pycache__
2026-07-25 13:11:08 +00:00

179 lines
6.0 KiB
Python

"""
Promo Phase — глобальное скрытое состояние "промо-фазы".
Влияет на то, какой эффект дают промокоды (везёт / не везёт / нейтрально).
Переключается случайно каждые 1-6 часов.
"""
import json
import random
from datetime import datetime, timedelta
from pathlib import Path
PHASE_FILE = Path("promo_phase.json")
PHASE_CONFIG = {
"LUCKY": {
"weight": 0.4,
"min_hours": 1,
"max_hours": 4,
"effect": "boost",
"boost_range": (0.15, 0.35), # +15-35% к luck_multiplier
"rare_chance_boost": 0.05, # +5% к шансу редкого
},
"UNLUCKY": {
"weight": 0.4,
"min_hours": 1,
"max_hours": 4,
"effect": "penalty",
"penalty_range": (0.10, 0.25), # -10-25% к luck_multiplier
"ceiling_penalty": 0.10, # -10% к потолку
},
"NEUTRAL": {
"weight": 0.2,
"min_hours": 1,
"max_hours": 3,
"effect": "none",
}
}
def load_phase():
"""Загружает текущую промо-фазу из файла"""
if PHASE_FILE.exists():
try:
with open(PHASE_FILE, 'r') as f:
return json.load(f)
except:
pass
return {
"phase": "NEUTRAL",
"until": datetime.utcnow().isoformat(),
"started_at": datetime.utcnow().isoformat()
}
def save_phase(state: dict):
"""Сохраняет промо-фазу"""
with open(PHASE_FILE, 'w') as f:
json.dump(state, f, indent=2)
def pick_next_phase() -> str:
"""Выбирает следующую фазу случайно, с учётом весов"""
phases = list(PHASE_CONFIG.keys())
weights = [PHASE_CONFIG[p]["weight"] for p in phases]
return random.choices(phases, weights=weights, k=1)[0]
def get_random_duration(phase: str) -> timedelta:
"""Случайная длительность фазы"""
config = PHASE_CONFIG.get(phase, PHASE_CONFIG["NEUTRAL"])
min_h = config["min_hours"]
max_h = config["max_hours"]
hours = random.uniform(min_h, max_h)
return timedelta(hours=hours)
def ensure_phase():
"""Проверяет и при необходимости переключает фазу"""
state = load_phase()
until = datetime.fromisoformat(state["until"])
if datetime.utcnow() >= until:
new_phase = pick_next_phase()
duration = get_random_duration(new_phase)
state["phase"] = new_phase
state["until"] = (datetime.utcnow() + duration).isoformat()
state["started_at"] = datetime.utcnow().isoformat()
save_phase(state)
return state["phase"]
def get_current_phase() -> str:
"""Возвращает текущую фазу"""
return ensure_phase()
def get_promo_effect_for_user(user_id: int, hot_score: float = 0.0,
withdrawal_ratio: float = 0.0,
is_cold: bool = False) -> dict:
"""
Возвращает индивидуальный эффект промо-фазы для пользователя.
Одинаковая фаза → разный эффект в зависимости от hot/cold/withdrawal.
"""
phase = get_current_phase()
config = PHASE_CONFIG.get(phase, {})
effect_type = config.get("effect", "none")
result = {
"phase": phase,
"effect": effect_type,
"luck_modifier": 0.0, # additive к luck_multiplier
"ceiling_modifier": 0.0, # additive к ceiling
"rare_chance_modifier": 0.0, # additive к шансу редкого
"description": ""
}
if effect_type == "boost":
boost = random.uniform(*config["boost_range"])
# Холодные игроки получают БОЛЬШЕ буста (реактивация)
if is_cold:
boost *= 1.5
# Кто много вывел — получает МЕНЬШЕ
boost *= max(0.5, 1.0 - withdrawal_ratio * 0.5)
result["luck_modifier"] = boost
result["rare_chance_modifier"] = config.get("rare_chance_boost", 0)
result["description"] = "Промо-удача 🍀"
elif effect_type == "penalty":
penalty = random.uniform(*config["penalty_range"])
# Горячие (много потратили, 0 выводов) почти не чувствуют пенальти
if hot_score > 10000 and withdrawal_ratio < 0.1:
penalty *= 0.3
# Кто вывел много — получает ПОЛНУЮ открутку
if withdrawal_ratio > 0.5:
penalty *= 1.5
result["luck_modifier"] = -penalty
result["ceiling_modifier"] = -config.get("ceiling_penalty", 0)
result["description"] = "Промо-открутка 💀"
return result
def get_promo_phase_info() -> dict:
"""Возвращает инфо о фазе: phase, effect, next_switch, remaining_minutes."""
state = load_phase()
phase = state["phase"]
until = datetime.fromisoformat(state["until"])
config = PHASE_CONFIG.get(phase, {})
remaining = (until - datetime.utcnow()).total_seconds()
return {
"phase": phase,
"effect": config.get("effect", "none"),
"next_switch": until.isoformat(),
"remaining_minutes": max(0, int(remaining // 60)),
}
def get_phase_debug_info() -> dict:
"""Для админки — отладка промо-фаз"""
state = load_phase()
phase = state["phase"]
until = datetime.fromisoformat(state["until"])
started = datetime.fromisoformat(state["started_at"])
remaining = (until - datetime.utcnow()).total_seconds()
return {
"current_phase": phase,
"started_at": started.isoformat(),
"until": until.isoformat(),
"remaining_seconds": max(0, int(remaining)),
"config": PHASE_CONFIG.get(phase, {})
}