47c7f17c90
— 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__
179 lines
6.0 KiB
Python
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, {})
|
|
}
|