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__
This commit is contained in:
+178
@@ -0,0 +1,178 @@
|
||||
"""
|
||||
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, {})
|
||||
}
|
||||
Reference in New Issue
Block a user