forked from zovos/bot_tg
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:
parent
70e7d23064
commit
81af64c8b7
2 changed files with 196 additions and 83 deletions
168
games/economy.py
168
games/economy.py
|
|
@ -1,3 +1,4 @@
|
|||
import random
|
||||
import sqlite3
|
||||
import json
|
||||
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 = {
|
||||
'central_bank_rate': '7.5', # Ставка ЦБ РФ
|
||||
|
|
@ -233,8 +243,14 @@ 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('''
|
||||
|
|
@ -264,7 +280,13 @@ 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'))
|
||||
|
|
@ -303,8 +325,14 @@ 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,))
|
||||
|
|
@ -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()
|
||||
|
|
|
|||
91
main.py
91
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),
|
||||
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,
|
||||
)
|
||||
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:
|
||||
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)
|
||||
|
|
|
|||
Loading…
Reference in a new issue