feat: единая экономика — penis game, вклады/кредиты через ЦБ

- penis_stats переехал в economy.sqlite3, длина хранится в user_balances.balance
- /top_penis читает из user_balances, /penis пишет туда же
- create_deposit: деньги уходят в капитал ЦБ
- create_loan: деньги берутся из капитала ЦБ
- repay_loan: деньги возвращаются в ЦБ
- добавлен process_matured_deposits: ЦБ выплачивает вклады с процентами
- init_penis_db() больше не вызывается

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
q 2026-04-24 13:53:41 +03:00
parent 70e7d23064
commit 81af64c8b7
2 changed files with 196 additions and 83 deletions

View file

@ -1,3 +1,4 @@
import random
import sqlite3 import sqlite3
import json import json
import logging import logging
@ -117,6 +118,15 @@ class EconomyManager:
) )
''') ''')
# Кулдауны /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 = { default_settings = {
'central_bank_rate': '7.5', # Ставка ЦБ РФ 'central_bank_rate': '7.5', # Ставка ЦБ РФ
@ -233,8 +243,14 @@ class EconomyManager:
conn = sqlite3.connect(ECONOMY_DB_PATH) conn = sqlite3.connect(ECONOMY_DB_PATH)
cursor = conn.cursor() cursor = conn.cursor()
# Списываем деньги со счета # Списываем деньги со счета пользователя → передаём в ЦБ
cursor.execute('UPDATE user_balances SET balance = balance - ? WHERE user_id = ?', (amount, user_id)) 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(''' cursor.execute('''
@ -264,7 +280,13 @@ class EconomyManager:
conn = sqlite3.connect(ECONOMY_DB_PATH) conn = sqlite3.connect(ECONOMY_DB_PATH)
cursor = conn.cursor() cursor = conn.cursor()
# Начисляем деньги на счет # ЦБ выдаёт кредит: уменьшаем капитал ЦБ, зачисляем пользователю
cursor.execute('''
UPDATE central_bank SET
capital = capital - ?,
last_updated = CURRENT_TIMESTAMP
WHERE id = 1
''', (amount,))
cursor.execute(''' cursor.execute('''
INSERT INTO user_balances (user_id, balance, daily_income, last_daily_reset) INSERT INTO user_balances (user_id, balance, daily_income, last_daily_reset)
VALUES (?, ?, 0.0, date('now')) VALUES (?, ?, 0.0, date('now'))
@ -303,8 +325,14 @@ class EconomyManager:
conn.close() conn.close()
return False return False
# Списываем деньги # Списываем деньги у пользователя → возвращаем в ЦБ
cursor.execute('UPDATE user_balances SET balance = balance - ? WHERE user_id = ?', (total_repayment, user_id)) 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,)) cursor.execute('UPDATE loans SET is_repaid = 1 WHERE id = ?', (loan_id,))
@ -629,5 +657,139 @@ class EconomyManager:
logger.info(f"Economic stimulus: CB emitted {emission_amount:.2f} cm, tax rate decreased to {new_tax_rate}%") 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() economy = EconomyManager()

93
main.py
View file

@ -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}" display_name = user_name.strip() if user_name and user_name.strip() else f"user_{user_id}"
try: try:
with sqlite3.connect(config.PENIS_DB_PATH) as conn: result = economy.play_penis(
conn.row_factory = sqlite3.Row user_id, display_name, now_ts,
row = conn.execute( config.PENIS_START_LENGTH, config.PENIS_COOLDOWN_SECONDS,
"SELECT length, last_used_ts, display_name FROM penis_stats WHERE user_id = ?", config.PENIS_MIN_DELTA, config.PENIS_MAX_DELTA,
(user_id,), )
).fetchone() except Exception:
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:
logger.exception("Failed to process /penis") logger.exception("Failed to process /penis")
return False, "Ошибка БД. Попробуй позже." 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 "-" sign_text = "+" if sign > 0 else "-"
return ( return (
True, True,
f"📏 Изменение: {sign_text}{delta:.1f} см\n" f"📏 Изменение: {sign_text}{delta:.1f} см\n"
f"Текущая длина: {new_length:.1f} см\n" f"Текущая длина: {new_balance:.1f} см\n"
"Следующая попытка через 24 часа.", "Следующая попытка через 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: def build_penis_top(limit: int = config.PENIS_TOP_LIMIT) -> str:
try: try:
with sqlite3.connect(config.PENIS_DB_PATH) as conn: rows = economy.get_penis_top(limit)
conn.row_factory = sqlite3.Row except Exception:
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:
logger.exception("Failed to build /top_penis") logger.exception("Failed to build /top_penis")
return "Не удалось загрузить топ. Ошибка БД." return "Не удалось загрузить топ. Ошибка БД."
@ -825,11 +777,10 @@ def build_penis_top(limit: int = config.PENIS_TOP_LIMIT) -> str:
lines = ["🏆 Топ по длине:"] lines = ["🏆 Топ по длине:"]
for index, row in enumerate(rows, start=1): for index, row in enumerate(rows, start=1):
user_id = int(row["user_id"]) user_id = int(row["user_id"])
length = float(row["length"]) balance = float(row["balance"])
name = row["display_name"] if isinstance(row["display_name"], str) else f"user_{user_id}" name = str(row["display_name"]).strip() or f"user_{user_id}"
name = name.strip() if name.strip() else f"user_{user_id}"
prefix = medals.get(index, f"{index}.") prefix = medals.get(index, f"{index}.")
lines.append(f"{prefix} {name}{length:.1f} см") lines.append(f"{prefix} {name}{balance:.1f} см")
return "\n".join(lines) return "\n".join(lines)
def _load_polychaetsi_stats() -> dict: def _load_polychaetsi_stats() -> dict:
@ -1684,7 +1635,6 @@ def main():
token = os.getenv("BOT_TOKEN") token = os.getenv("BOT_TOKEN")
if not token: raise RuntimeError("BOT_TOKEN не задан!") if not token: raise RuntimeError("BOT_TOKEN не задан!")
init_penis_db()
init_polychaetsi_storage() init_polychaetsi_storage()
bot = Bot(token=token, default=DefaultBotProperties(parse_mode=ParseMode.HTML)) bot = Bot(token=token, default=DefaultBotProperties(parse_mode=ParseMode.HTML))
dp = Dispatcher() dp = Dispatcher()
@ -1803,6 +1753,7 @@ async def economy_background_tasks():
await asyncio.to_thread(economy.collect_daily_taxes) await asyncio.to_thread(economy.collect_daily_taxes)
await asyncio.to_thread(economy.regulate_inflation) await asyncio.to_thread(economy.regulate_inflation)
await asyncio.to_thread(economy.process_matured_deposits)
if current_hour == 0 and current_minute == 0: if current_hour == 0 and current_minute == 0:
users_paid = await asyncio.to_thread(economy.pay_daily_activity_rewards) users_paid = await asyncio.to_thread(economy.pay_daily_activity_rewards)