diff --git a/games/economy.py b/games/economy.py index 4eca0e5..e5445a8 100644 --- a/games/economy.py +++ b/games/economy.py @@ -1,3 +1,4 @@ +import random import sqlite3 import json import logging @@ -116,6 +117,15 @@ class EconomyManager: UNIQUE(id) ) ''') + + # Кулдауны /penis (длина хранится в user_balances.balance) + cursor.execute(''' + CREATE TABLE IF NOT EXISTS penis_stats ( + user_id INTEGER PRIMARY KEY, + display_name TEXT NOT NULL DEFAULT '', + last_used_ts INTEGER + ) + ''') # Вставка настроек по умолчанию default_settings = { @@ -233,18 +243,24 @@ class EconomyManager: conn = sqlite3.connect(ECONOMY_DB_PATH) cursor = conn.cursor() - # Списываем деньги со счета + # Списываем деньги со счета пользователя → передаём в ЦБ cursor.execute('UPDATE user_balances SET balance = balance - ? WHERE user_id = ?', (amount, user_id)) - + cursor.execute(''' + UPDATE central_bank SET + capital = capital + ?, + last_updated = CURRENT_TIMESTAMP + WHERE id = 1 + ''', (amount,)) + # Создаем вклад cursor.execute(''' INSERT INTO deposits (user_id, amount, interest_rate, matures_at) VALUES (?, ?, ?, ?) ''', (user_id, amount, central_bank_rate, matures_at)) - + conn.commit() conn.close() - + logger.info(f"Deposit created: user {user_id}, amount {amount}, rate {central_bank_rate}%") return True @@ -264,22 +280,28 @@ class EconomyManager: conn = sqlite3.connect(ECONOMY_DB_PATH) cursor = conn.cursor() - # Начисляем деньги на счет + # ЦБ выдаёт кредит: уменьшаем капитал ЦБ, зачисляем пользователю + cursor.execute(''' + UPDATE central_bank SET + capital = capital - ?, + last_updated = CURRENT_TIMESTAMP + WHERE id = 1 + ''', (amount,)) cursor.execute(''' INSERT INTO user_balances (user_id, balance, daily_income, last_daily_reset) VALUES (?, ?, 0.0, date('now')) ON CONFLICT(user_id) DO UPDATE SET balance = balance + ? ''', (user_id, amount, amount)) - + # Создаем кредит cursor.execute(''' INSERT INTO loans (user_id, amount, interest_rate, due_at) VALUES (?, ?, ?, ?) ''', (user_id, amount, loan_rate, due_at)) - + conn.commit() conn.close() - + logger.info(f"Loan created: user {user_id}, amount {amount}, rate {loan_rate}%") return True @@ -303,15 +325,21 @@ class EconomyManager: conn.close() return False - # Списываем деньги + # Списываем деньги у пользователя → возвращаем в ЦБ cursor.execute('UPDATE user_balances SET balance = balance - ? WHERE user_id = ?', (total_repayment, user_id)) - + cursor.execute(''' + UPDATE central_bank SET + capital = capital + ?, + last_updated = CURRENT_TIMESTAMP + WHERE id = 1 + ''', (total_repayment,)) + # Отмечаем кредит как погашенный cursor.execute('UPDATE loans SET is_repaid = 1 WHERE id = ?', (loan_id,)) - + conn.commit() conn.close() - + logger.info(f"Loan repaid: user {user_id}, loan {loan_id}, amount {total_repayment}") return True @@ -629,5 +657,139 @@ class EconomyManager: logger.info(f"Economic stimulus: CB emitted {emission_amount:.2f} cm, tax rate decreased to {new_tax_rate}%") + def play_penis(self, user_id: int, display_name: str, now_ts: int, + start_length: float, cooldown_seconds: int, + min_delta: float, max_delta: float) -> dict: + """ + Возвращает dict: + на кулдауне: {'allowed': False, 'left_seconds': N} + успех: {'allowed': True, 'new_balance': X, 'delta': Y, 'sign': 1 или -1} + """ + conn = sqlite3.connect(ECONOMY_DB_PATH) + try: + conn.row_factory = sqlite3.Row + cursor = conn.cursor() + + # Текущий баланс (он же «длина») — стартуем с start_length для новых + cursor.execute('SELECT balance FROM user_balances WHERE user_id = ?', (user_id,)) + row = cursor.fetchone() + current_balance = row['balance'] if row else start_length + + # Кулдаун из penis_stats + cursor.execute('SELECT last_used_ts, display_name FROM penis_stats WHERE user_id = ?', (user_id,)) + ps_row = cursor.fetchone() + last_used_ts = ps_row['last_used_ts'] if ps_row else None + + if last_used_ts is not None: + next_ts = int(last_used_ts) + cooldown_seconds + if now_ts < next_ts: + if ps_row and ps_row['display_name'] != display_name: + cursor.execute( + 'UPDATE penis_stats SET display_name = ? WHERE user_id = ?', + (display_name, user_id) + ) + conn.commit() + return {'allowed': False, 'left_seconds': next_ts - now_ts} + + delta = round(random.uniform(min_delta, max_delta), 1) + sign = random.choice([-1, 1]) + new_balance = round(max(0.1, current_balance + sign * delta), 1) + + # Обновляем или создаём запись в user_balances + cursor.execute(''' + INSERT INTO user_balances (user_id, balance, daily_income, last_daily_reset) + VALUES (?, ?, 0.0, date('now')) + ON CONFLICT(user_id) DO UPDATE SET balance = ? + ''', (user_id, new_balance, new_balance)) + + sign_label = f'+{delta:.1f}' if sign > 0 else f'-{delta:.1f}' + cursor.execute(''' + INSERT INTO transactions (from_user_id, to_user_id, amount, transaction_type, description) + VALUES (?, ?, ?, 'penis_game', ?) + ''', (user_id, user_id, sign * delta, f'Игра /penis: {sign_label} см')) + + # Обновляем кулдаун + if ps_row is None: + cursor.execute( + 'INSERT INTO penis_stats (user_id, display_name, last_used_ts) VALUES (?, ?, ?)', + (user_id, display_name, now_ts) + ) + else: + cursor.execute( + 'UPDATE penis_stats SET display_name = ?, last_used_ts = ? WHERE user_id = ?', + (display_name, now_ts, user_id) + ) + + conn.commit() + return {'allowed': True, 'new_balance': new_balance, 'delta': delta, 'sign': sign} + except Exception: + conn.rollback() + raise + finally: + conn.close() + + def get_penis_top(self, limit: int) -> list: + """Топ пользователей по балансу (только игравшие в /penis).""" + conn = sqlite3.connect(ECONOMY_DB_PATH) + conn.row_factory = sqlite3.Row + cursor = conn.cursor() + cursor.execute(''' + SELECT ub.user_id, ps.display_name, ub.balance + FROM user_balances ub + INNER JOIN penis_stats ps ON ub.user_id = ps.user_id + ORDER BY ub.balance DESC, ub.user_id ASC + LIMIT ? + ''', (limit,)) + rows = cursor.fetchall() + conn.close() + return [{'user_id': r['user_id'], 'display_name': r['display_name'], 'balance': r['balance']} + for r in rows] + + def process_matured_deposits(self) -> int: + """Выплачивает созревшие вклады из капитала ЦБ. Возвращает кол-во выплат.""" + conn = sqlite3.connect(ECONOMY_DB_PATH) + cursor = conn.cursor() + now = datetime.now().isoformat() + + cursor.execute(''' + SELECT id, user_id, amount, interest_rate + FROM deposits + WHERE is_active = 1 AND matures_at <= ? + ''', (now,)) + matured = cursor.fetchall() + + paid = 0 + for dep_id, user_id, amount, rate in matured: + payout = round(amount * (1 + rate / 100 * 1 / 365 * 30), 2) # ~месячный доход + + # ЦБ выплачивает пользователю основную сумму + проценты + cursor.execute(''' + INSERT INTO user_balances (user_id, balance, daily_income, last_daily_reset) + VALUES (?, ?, 0.0, date('now')) + ON CONFLICT(user_id) DO UPDATE SET balance = balance + ? + ''', (user_id, payout, payout)) + cursor.execute(''' + UPDATE central_bank SET + capital = capital - ?, + last_updated = CURRENT_TIMESTAMP + WHERE id = 1 + ''', (payout,)) + cursor.execute('UPDATE deposits SET is_active = 0 WHERE id = ?', (dep_id,)) + + cursor.execute(''' + INSERT INTO transactions (from_user_id, to_user_id, amount, transaction_type, description) + VALUES (0, ?, ?, 'deposit_payout', ?) + ''', (user_id, payout, f'Выплата вклада #{dep_id}: {amount:.1f} + проценты = {payout:.2f} см')) + + paid += 1 + + conn.commit() + conn.close() + + if paid: + logger.info(f"Matured deposits paid out: {paid}") + return paid + + # Глобальный экземпляр economy = EconomyManager() diff --git a/main.py b/main.py index 667e3b3..5ddc76f 100644 --- a/main.py +++ b/main.py @@ -730,67 +730,29 @@ def play_penis(user_id: int, user_name: str | None = None, now: datetime | None display_name = user_name.strip() if user_name and user_name.strip() else f"user_{user_id}" try: - with sqlite3.connect(config.PENIS_DB_PATH) as conn: - conn.row_factory = sqlite3.Row - row = conn.execute( - "SELECT length, last_used_ts, display_name FROM penis_stats WHERE user_id = ?", - (user_id,), - ).fetchone() - - if row is None: - current_length = config.PENIS_START_LENGTH - last_used_ts = None - else: - current_length = float(row["length"]) - last_used_ts = row["last_used_ts"] - stored_name = row["display_name"] - if not user_name and isinstance(stored_name, str) and stored_name.strip(): - display_name = stored_name.strip() - - if last_used_ts is not None: - next_ts = int(last_used_ts) + config.PENIS_COOLDOWN_SECONDS - if now_ts < next_ts: - if row is not None and row["display_name"] != display_name: - conn.execute( - "UPDATE penis_stats SET display_name = ? WHERE user_id = ?", - (display_name, user_id), - ) - left_seconds = next_ts - now_ts - hours = left_seconds // 3600 - minutes = (left_seconds % 3600) // 60 - conn.commit() - return False, f"Сегодня уже кидал. Попробуй через {hours} ч {minutes} мин." - - delta = round(random.uniform(config.PENIS_MIN_DELTA, config.PENIS_MAX_DELTA), 1) - sign = random.choice([-1, 1]) - new_length = round(max(0.1, current_length + sign * delta), 1) - - if row is None: - conn.execute( - "INSERT INTO penis_stats(user_id, display_name, length, last_used_ts) VALUES(?, ?, ?, ?)", - (user_id, display_name, new_length, now_ts), - ) - else: - conn.execute( - "UPDATE penis_stats SET display_name = ?, length = ?, last_used_ts = ? WHERE user_id = ?", - (display_name, new_length, now_ts, user_id), - ) - conn.commit() - - # Обновляем баланс в экономической системе - if sign > 0: - economy.update_balance(user_id, delta, 'penis_game', f'Выигрыш в игре +{delta} см') - else: - economy.update_balance(user_id, delta, 'penis_game', f'Проигрыш в игре {delta} см') - except sqlite3.Error: + result = economy.play_penis( + user_id, display_name, now_ts, + config.PENIS_START_LENGTH, config.PENIS_COOLDOWN_SECONDS, + config.PENIS_MIN_DELTA, config.PENIS_MAX_DELTA, + ) + except Exception: logger.exception("Failed to process /penis") return False, "Ошибка БД. Попробуй позже." + if not result['allowed']: + left = result['left_seconds'] + hours = left // 3600 + minutes = (left % 3600) // 60 + return False, f"Сегодня уже кидал. Попробуй через {hours} ч {minutes} мин." + + sign = result['sign'] + delta = result['delta'] + new_balance = result['new_balance'] sign_text = "+" if sign > 0 else "-" return ( True, f"📏 Изменение: {sign_text}{delta:.1f} см\n" - f"Текущая длина: {new_length:.1f} см\n" + f"Текущая длина: {new_balance:.1f} см\n" "Следующая попытка через 24 часа.", ) @@ -803,18 +765,8 @@ def format_penis_user_name(user: types.User) -> str: def build_penis_top(limit: int = config.PENIS_TOP_LIMIT) -> str: try: - with sqlite3.connect(config.PENIS_DB_PATH) as conn: - conn.row_factory = sqlite3.Row - rows = conn.execute( - """ - SELECT user_id, display_name, length - FROM penis_stats - ORDER BY length DESC, user_id ASC - LIMIT ? - """, - (limit,), - ).fetchall() - except sqlite3.Error: + rows = economy.get_penis_top(limit) + except Exception: logger.exception("Failed to build /top_penis") return "Не удалось загрузить топ. Ошибка БД." @@ -825,11 +777,10 @@ def build_penis_top(limit: int = config.PENIS_TOP_LIMIT) -> str: lines = ["🏆 Топ по длине:"] for index, row in enumerate(rows, start=1): user_id = int(row["user_id"]) - length = float(row["length"]) - name = row["display_name"] if isinstance(row["display_name"], str) else f"user_{user_id}" - name = name.strip() if name.strip() else f"user_{user_id}" + balance = float(row["balance"]) + name = str(row["display_name"]).strip() or f"user_{user_id}" prefix = medals.get(index, f"{index}.") - lines.append(f"{prefix} {name} — {length:.1f} см") + lines.append(f"{prefix} {name} — {balance:.1f} см") return "\n".join(lines) def _load_polychaetsi_stats() -> dict: @@ -1684,7 +1635,6 @@ def main(): token = os.getenv("BOT_TOKEN") if not token: raise RuntimeError("BOT_TOKEN не задан!") - init_penis_db() init_polychaetsi_storage() bot = Bot(token=token, default=DefaultBotProperties(parse_mode=ParseMode.HTML)) dp = Dispatcher() @@ -1803,6 +1753,7 @@ async def economy_background_tasks(): await asyncio.to_thread(economy.collect_daily_taxes) await asyncio.to_thread(economy.regulate_inflation) + await asyncio.to_thread(economy.process_matured_deposits) if current_hour == 0 and current_minute == 0: users_paid = await asyncio.to_thread(economy.pay_daily_activity_rewards)