diff --git a/README.md b/README.md index 58454bd..9e449b6 100644 --- a/README.md +++ b/README.md @@ -63,6 +63,7 @@ docker compose down - `/bet [спорт] <номер> <исход> <ставка>` — поставить см на матч. - `/mybets` — мои активные ставки. - `/svodka` — сводка СВО. +- `/uwu [теги]` — поиск случайной пикчи/гифки на e621 (по умолчанию sfw, можно указать свои теги). ## Экономика ZOV OS 💰 diff --git a/config.py b/config.py index 7e1461f..1a49985 100644 --- a/config.py +++ b/config.py @@ -1,205 +1,232 @@ -import os -from pathlib import Path -from datetime import timedelta, time -from zoneinfo import ZoneInfo - -# --- Базовые пути --- -BASE_DIR = Path(__file__).parent -FONT_PATH = BASE_DIR / "impact.ttf" -DEFAULT_TZ = os.getenv("TZ", "Europe/Moscow") -DATABASE_URL = os.getenv("DATABASE_URL", "postgresql://bot:botpass@postgres:5432/botdb") - -# --- Настройки /penis --- -PENIS_START_LENGTH = 10.0 -PENIS_MIN_DELTA = 0.1 -PENIS_MAX_DELTA = 7.0 -PENIS_COOLDOWN = timedelta(hours=24) -PENIS_COOLDOWN_SECONDS = int(PENIS_COOLDOWN.total_seconds()) -PENIS_TOP_LIMIT = 10 - -# --- Настройки "получается" --- -POLYCHAETSI_STATS_PATH = Path(os.getenv("POLYCHAETSI_STATS_PATH", "/db/polychaetsi_stats.json")) -POLYCHAETSI_TARGET = "получается" -POLYCHAETSI_TOP_LIMIT = 10 -POLYCHAETSI_IMAGE_PATH = BASE_DIR / "maket" / "cvetok.jpg" - -# --- Казино --- -CASINO_MAX_BET = 1.0 - -# --- E621 API --- -E621_LOGIN = os.getenv("E621_LOGIN", "") -E621_API_KEY = os.getenv("E621_API_KEY", "") - -# --- Ставки на матчи --- -ODDS_API_KEY = os.getenv("ODDS_API_KEY", "") -ODDS_API_BASE = "https://api.the-odds-api.com/v4" -BET_MAX_AMOUNT = 3.0 -ODDS_SPORTS = { - "dota2": "esports_dota2", -} -ODDS_CACHE_TTL = 86400 # 24 часа — экономим запросы (500/мес бесплатно) - -# --- Nude --- -NUDE_HISTORY_PATH = Path(os.getenv("NUDE_HISTORY_PATH", "/db/nude_history.json")) -NUDE_POSTS_PER_PAGE = int(os.getenv("NUDE_POSTS_PER_PAGE", "100")) -NUDE_POSTS_POOL_TARGET = int(os.getenv("NUDE_POSTS_POOL_TARGET", "1200")) -NUDE_MAX_POSTS_TO_SCAN = int(os.getenv("NUDE_MAX_POSTS_TO_SCAN", "2000")) -NUDE_MIN_UNSEEN_POOL = int(os.getenv("NUDE_MIN_UNSEEN_POOL", "100")) -NUDE_HISTORY_MAX_SIZE = int(os.getenv("NUDE_HISTORY_MAX_SIZE", "5000")) - -# --- Magnit API --- -MAGNIT_API_BASE_URL = os.getenv("MAGNIT_API_BASE_URL", "https://mirror.porno4free.ru/magnit") -MAGNIT_STORE_CODE = os.getenv("MAGNIT_STORE_CODE", "618224") -MAGNIT_REQUEST_TIMEOUT = int(os.getenv("MAGNIT_REQUEST_TIMEOUT", "15")) -# Можно указывать просто строку с названием или словарь с query/label/store_code. -MAGNIT_PRODUCTS = [ - "RedBull", -] - -# --- Конфигурация макетов --- -TEMPLATES = { - "1": { - "path": BASE_DIR / "maket" / "1.png", - "anchor": (104, 240), - "padding": 20, - "label": "Владимир Владимирович Путин самый лучший президент", - }, - "2": { - "path": BASE_DIR / "maket" / "2.png", - "anchor": (40, 40), - "box_size": (582, 346), - "label": "Макет 2", - }, -} - -# --- Расписание --- -def make_time(h: int, m: int) -> time: - return time(hour=h, minute=m) - -# Если у тебя наоборот, просто поменяй на False. -UPPER_WEEK_IS_ODD = False - -PAIR_TIMES = { - 1: (make_time(8, 0), make_time(9, 35)), - 2: (make_time(9, 50), make_time(11, 25)), - 3: (make_time(11, 55), make_time(13, 30)), - 4: (make_time(13, 45), make_time(15, 20)), - 5: (make_time(15, 50), make_time(17, 25)), -} - -# Расписание 2 курс, гр. 27 (25ВТ-09.03.03.01-о2), осень 2026-27. -# Источник: Raspis_osen_2_kurs_2026-27_g.pdf (черновик, водяной знак "Проект", возможны изменения). -# Разделения на верхнюю/нижнюю неделю в источнике нет — обе недели одинаковые. -_MON = [ - {"pair": 2, "title": "Экономика и право (практика), Индустриев М.А. до 06.11 / Васильева Е.В. с 07.11, ауд. 504"}, - {"pair": 3, "title": "Экономика и право (лекция), Туманян Ю.Р. до 06.11 / Васильева Е.В. с 07.11, ауд. 510"}, - {"pair": 4, "title": "Геоинформационные системы (лекция), Ушканова Е.В., ауд. 403а (НИИМ и ПМ)"}, -] -_TUE = [ - {"pair": 3, "title": "Специальные разделы математики (практика), Вакулов Б.Г., ауд. 504"}, - {"pair": 4, "title": "Геоинформационные системы (лаб), Ушканова Е.В., ауд. 403а (НИИМ и ПМ)"}, -] -_WED = [ - {"pair": 2, "title": "Технология программирования (лекция), Губский Д.С., ауд. 510"}, - {"pair": 3, "title": "1 подгруппа: Основы функционирования вычислительной техники (лаб), Дацюк О.В., ауд. 606 | 2 подгруппа: Геоинформационные системы (лаб), Ушканова Е.В., ауд. 403а (НИИМ и ПМ)"}, - {"pair": 4, "title": "1 подгруппа: Технология программирования (лаб), Губский Д.С., ауд. 609 | 2 подгруппа: Технология программирования (лаб), Иванова И.Н., ауд. 609а"}, - {"pair": 5, "title": "Основы функционирования вычислительной техники (лаб), Дацюк О.В., ауд. 606"}, -] -_THU = [ - {"pair": 2, "title": "Основы функционирования вычислительной техники (лекция), Дацюк О.В., ауд. 510"}, - {"pair": 3, "title": "1 подгруппа: Основы функционирования вычислительной техники (практика), Дацюк О.В., ауд. 818 | 2 подгруппа: Специальные разделы математики (практика), Вакулов Б.Г., ауд. 507"}, - {"pair": 4, "title": "Специальные разделы математики (лекция), Каплицкий В.М., ауд. 510"}, -] -_FRI = [ - {"pair": None, "title": "Иностранный язык (время уточняется)"}, -] -_SAT = [ - {"pair": 1, "title": "Философия (лекция, совместно с гр. 26), Новохатько А.Г., MS Teams"}, -] - -_LESSON_WEEK = { - 0: _MON, - 1: _TUE, - 2: _WED, - 3: _THU, - 4: _FRI, - 5: _SAT, -} - -LESSON_SCHEDULE = { - "upper": _LESSON_WEEK, - "lower": _LESSON_WEEK, -} - -# SCHEDULE — тайминг перекуров для /break, построен из фактических пар в -# _LESSON_WEEK, а не задаётся руками, чтобы не рассинхронизироваться при -# правках расписания (раньше здесь была отдельная захардкоженная таблица, -# которая не обновилась вместе с LESSON_SCHEDULE и врала на днях с занятиями). -# Каждая пара (95 мин) состоит из двух академ. часов по 45 мин с 5-минутным -# перекуром между ними; между парами перекур — фактический зазор по PAIR_TIMES. -def _time_to_minutes(t: time) -> int: - return t.hour * 60 + t.minute - - -def _minutes_to_time(m: int) -> time: - return make_time(m // 60, m % 60) - - -def _build_day_schedule(pairs: list[int]) -> list[tuple[time, time, int | None]]: - entries = [] - for idx, pair in enumerate(pairs): - start_t, end_t = PAIR_TIMES[pair] - start_m = _time_to_minutes(start_t) - end_m = _time_to_minutes(end_t) - mid_m = start_m + 45 - second_start_m = mid_m + 5 - entries.append((start_t, _minutes_to_time(mid_m), 5)) - if idx == len(pairs) - 1: - entries.append((_minutes_to_time(second_start_m), end_t, None)) - else: - next_start_t, _ = PAIR_TIMES[pairs[idx + 1]] - gap = _time_to_minutes(next_start_t) - end_m - entries.append((_minutes_to_time(second_start_m), end_t, gap)) - return entries - - -def _pairs_for_day(day_lessons: list[dict]) -> list[int]: - return sorted({item["pair"] for item in day_lessons if item.get("pair") is not None}) - - -SCHEDULE = { - day: _build_day_schedule(_pairs_for_day(lessons)) - for day, lessons in _LESSON_WEEK.items() -} - -# --- Остальное --- -KEYWORDS = {'крип', 'радиохэд', 'creep', 'radiohead'} - -# --- Авто-ответы ИИ --- -AUTOREPLY_CHANCE = 0.10 -AUTOREPLY_COOLDOWN = 120 -AUTOREPLY_TRIGGERS = {'бот', 'фраер', 'кент', 'братуха', 'пацаны', 'чё как', 'базар', 'ебать', 'пизда', 'хуй', 'бля', 'сука', 'говно', 'пидор', 'пиздабол', 'пиздатый', 'пиздатая', 'пиздатое', 'пиздатые', 'пиздато', 'ваномас'} - -def get_hint_text(templates_str: str) -> str: - return ( - "Команды:\n" - f"• /maket «id» «текст» — доступные: {templates_str}.\n" - "• /gen_mem Up:верх Down:низ — отправь с фото.\n" - "• /break — узнать время до перекура.\n" - "• /schedule [пн|mon] — расписание; без аргумента еще текущая и следующая пара.\n" - "• /penis — миниигра 1 раз в 24 часа.\n" - "• /top_penis — топ по длине.\n" - "• /talk [текст] — ИИ базарит на фене.\n" - "• /penis_casino [ставка] — казино на размер.\n" - "• /gadanie [тема] — гадание на фене с мемом.\n" - "• /matches — матчи для ставок.\n" - "• /bet [номер] [исход] [ставка] — поставить.\n" - "• /mybets — мои активные ставки.\n" - "• /bet_history — история ставок за месяц + статистика.\n" - "• /sports_debug — диагностика API матчей.\n" - "• /ebalnik — включить/выключить автоответы в чате.\n" - "• /uwu [теги] — случайная картинка с e621.\n" - "• /polychaetsi — топ по слову «получается».\n" - "• /prices /fetch — цены и остатки Магнита.\n" - "Подсказка: в группах пиши /команда@username_бота." - ) +import os +from pathlib import Path +from datetime import timedelta, time +from zoneinfo import ZoneInfo + +# --- Базовые пути --- +BASE_DIR = Path(__file__).parent +FONT_PATH = BASE_DIR / "impact.ttf" +DEFAULT_TZ = os.getenv("TZ", "Europe/Moscow") +DATABASE_URL = os.getenv("DATABASE_URL", "postgresql://bot:botpass@postgres:5432/botdb") + +# --- Настройки /penis --- +PENIS_START_LENGTH = 10.0 +PENIS_MIN_DELTA = 0.1 +PENIS_MAX_DELTA = 7.0 +PENIS_COOLDOWN = timedelta(hours=24) +PENIS_COOLDOWN_SECONDS = int(PENIS_COOLDOWN.total_seconds()) +PENIS_TOP_LIMIT = 10 + +# --- Настройки "получается" --- +POLYCHAETSI_STATS_PATH = Path(os.getenv("POLYCHAETSI_STATS_PATH", "/db/polychaetsi_stats.json")) +POLYCHAETSI_TARGET = "получается" +POLYCHAETSI_TOP_LIMIT = 10 +POLYCHAETSI_IMAGE_PATH = BASE_DIR / "maket" / "cvetok.jpg" + +# --- Казино --- +CASINO_MAX_BET = 1.0 + +# --- E621 API --- +E621_LOGIN = os.getenv("E621_LOGIN", "") +E621_API_KEY = os.getenv("E621_API_KEY", "") + +# --- Ставки на матчи --- +ODDS_API_KEY = os.getenv("ODDS_API_KEY", "") +ODDS_API_BASE = "https://api.the-odds-api.com/v4" +BET_MAX_AMOUNT = 3.0 +ODDS_SPORTS = { + "dota2": "esports_dota2", +} +ODDS_CACHE_TTL = 86400 # 24 часа — экономим запросы (500/мес бесплатно) + +# --- Nude --- +NUDE_HISTORY_PATH = Path(os.getenv("NUDE_HISTORY_PATH", "/db/nude_history.json")) +NUDE_POSTS_PER_PAGE = int(os.getenv("NUDE_POSTS_PER_PAGE", "100")) +NUDE_POSTS_POOL_TARGET = int(os.getenv("NUDE_POSTS_POOL_TARGET", "1200")) +NUDE_MAX_POSTS_TO_SCAN = int(os.getenv("NUDE_MAX_POSTS_TO_SCAN", "2000")) +NUDE_MIN_UNSEEN_POOL = int(os.getenv("NUDE_MIN_UNSEEN_POOL", "100")) +NUDE_HISTORY_MAX_SIZE = int(os.getenv("NUDE_HISTORY_MAX_SIZE", "5000")) + +# --- Magnit API --- +MAGNIT_API_BASE_URL = os.getenv("MAGNIT_API_BASE_URL", "https://mirror.porno4free.ru/magnit") +MAGNIT_STORE_CODE = os.getenv("MAGNIT_STORE_CODE", "618224") +MAGNIT_REQUEST_TIMEOUT = int(os.getenv("MAGNIT_REQUEST_TIMEOUT", "15")) +# Можно указывать просто строку с названием или словарь с query/label/store_code. +MAGNIT_PRODUCTS = [ + "RedBull", +] + +# --- Конфигурация макетов --- +TEMPLATES = { + "1": { + "path": BASE_DIR / "maket" / "1.png", + "anchor": (104, 240), + "padding": 20, + "label": "Владимир Владимирович Путин самый лучший президент", + }, + "2": { + "path": BASE_DIR / "maket" / "2.png", + "anchor": (40, 40), + "box_size": (582, 346), + "label": "Макет 2", + }, +} + +# --- Расписание --- +def make_time(h: int, m: int) -> time: + return time(hour=h, minute=m) + +# Если у тебя наоборот, просто поменяй на False. +UPPER_WEEK_IS_ODD = False + +PAIR_TIMES = { + 1: (make_time(8, 0), make_time(9, 35)), + 2: (make_time(9, 50), make_time(11, 25)), + 3: (make_time(11, 55), make_time(13, 30)), + 4: (make_time(13, 45), make_time(15, 20)), + 5: (make_time(15, 50), make_time(17, 25)), +} + +LESSON_SCHEDULE = { + "upper": { + 0: [ + {"pair": 1, "title": "Нет пары"}, + {"pair": 2, "title": "Экономика и право (практика), пр. Индустриев М.А. (до 6.11.25) / Васильева Е.В. (с 7.11.25), а. 504"}, + {"pair": 3, "title": "Экономика и право (лекция), пр. Туманян Ю.Р. (до 6.11.25) / Васильева Е.В. (с 7.11.25), а. 510"}, + {"pair": 4, "title": "Геоинформационные системы (лекция), пр. Ушкалова Е.В., а. 403а (НИИМ и ПМ)"}, + ], + 1: [ + {"pair": 1, "title": "Нет пары"}, + {"pair": 2, "title": "Нет пары"}, + {"pair": 3, "title": "Специальные разделы математики (практика), пр. Вакулин Б.Г., а. 504"}, + {"pair": 4, "title": "1-я подгруппа: Геоинформационные системы (лаб), пр. Ушкалова Е.В., а. 403а (НИИМ и ПМ)"}, + ], + 2: [ + {"pair": 1, "title": "Нет пары"}, + {"pair": 2, "title": "Технологии программирования (лекция), пр. Губский Д.С., а. 510"}, + {"pair": 3, "title": "1-я подгруппа: Основы функционирования ВТ (лаб), пр. Дядик О.В., а. 606. 2-я подгруппа: Геоинформационные системы (лаб), пр. Ушкалова Е.В., а. 403а (НИИМ и ПМ)"}, + {"pair": 4, "title": "1-я подгруппа: Технологии программирования (лаб), пр. Губский Д.С., а. 609. 2-я подгруппа: Технологии программирования (лаб), пр. Иванова Н.Н., а. 609 а"}, + {"pair": 5, "title": "2-я подгруппа: Основы функционирования вычислительной техники (лаб), пр. Дядик О.В., а. 606"}, + ], + 3: [ + {"pair": 1, "title": "Нет пары"}, + {"pair": 2, "title": "Специальные разделы математики (прак), пр. Вакулин Б.Г., а. 502"}, + {"pair": 3, "title": "Основы функционирования вычислительной техники (прак), пр. Дядик О.В., а. 818"}, + {"pair": 4, "title": "Специальные разделы математики (лекция), пр. Котлярский В.М., а. 510"}, + ], + 4: [ + {"pair": None, "title": "Иностранный язык"}, + ], + 5: [ + {"pair": 1, "title": "Философия (лекция), пр. Новосёлова А.Г., MS Teams"}, + ], + }, + "lower": { + 0: [ + {"pair": 1, "title": "Нет пары"}, + {"pair": 2, "title": "Экономика и право (практика), пр. Индустриев М.А. (до 6.11.25) / Васильева Е.В. (с 7.11.25), а. 504"}, + {"pair": 3, "title": "Экономика и право (лекция), пр. Туманян Ю.Р. (до 6.11.25) / Васильева Е.В. (с 7.11.25), а. 510"}, + {"pair": 4, "title": "Геоинформационные системы (лекция), пр. Ушкалова Е.В., а. 403а (НИИМ и ПМ)"}, + ], + 1: [ + {"pair": 1, "title": "Нет пары"}, + {"pair": 2, "title": "Нет пары"}, + {"pair": 3, "title": "Специальные разделы математики (практика), пр. Вакулин Б.Г., а. 504"}, + {"pair": 4, "title": "1-я подгруппа: Геоинформационные системы (лаб), пр. Ушкалова Е.В., а. 403а (НИИМ и ПМ)"}, + ], + 2: [ + {"pair": 1, "title": "Нет пары"}, + {"pair": 2, "title": "Технологии программирования (лекция), пр. Губский Д.С., а. 510"}, + {"pair": 3, "title": "1-я подгруппа: Основы функционирования ВТ (лаб), пр. Дядик О.В., а. 606. 2-я подгруппа: Геоинформационные системы (лаб), пр. Ушкалова Е.В., а. 403а (НИИМ и ПМ)"}, + {"pair": 4, "title": "1-я подгруппа: Технологии программирования (лаб), пр. Губский Д.С., а. 609. 2-я подгруппа: Технологии программирования (лаб), пр. Иванова Н.Н., а. 609 а"}, + {"pair": 5, "title": "2-я подгруппа: Основы функционирования вычислительной техники (лаб), пр. Дядик О.В., а. 606"}, + ], + 3: [ + {"pair": 1, "title": "Нет пары"}, + {"pair": 2, "title": "Основы функционирования вычислительной техники (лекция), пр. Дядик О.В., а. 514"}, + {"pair": 3, "title": "Нет пары"}, + {"pair": 4, "title": "Специальные разделы математики (лекция), пр. Котлярский В.М., а. 510"}, + ], + 4: [ + {"pair": None, "title": "Иностранный язык"}, + ], + # В PR отсутствовали пятница/суббота на нижней неделе — этих пар нет + # только в исходном коммите; иняз и философия по субботам идут + # независимо от чётности недели, так что дублируем из "upper". + 5: [ + {"pair": 1, "title": "Философия (лекция), пр. Новосёлова А.Г., MS Teams"}, + ], + }, +} + +SCHEDULE = { + 0: [ # Понедельник + (make_time(9, 50), make_time(10, 35), 5), + (make_time(10, 40), make_time(11, 25), 30), + (make_time(11, 55), make_time(12, 40), 5), + (make_time(12, 45), make_time(13, 30), 15), + (make_time(13, 45), make_time(14, 30), 5), + (make_time(14, 35), make_time(15, 20), None), + ], + 1: [ # Вторник + (make_time(11, 55), make_time(12, 40), 5), + (make_time(12, 45), make_time(13, 30), 15), + (make_time(13, 45), make_time(14, 30), 5), + (make_time(14, 35), make_time(15, 20), None), + ], + 2: [ # Среда + (make_time(9, 50), make_time(10, 35), 5), + (make_time(10, 40), make_time(11, 25), 30), + (make_time(11, 55), make_time(12, 40), 5), + (make_time(12, 45), make_time(13, 30), 15), + (make_time(13, 45), make_time(14, 30), 5), + (make_time(14, 35), make_time(15, 20), 30), + (make_time(15, 50), make_time(16, 35), 5), + (make_time(16, 40), make_time(17, 25), None), + ], + 3: [ # Четверг + (make_time(9, 50), make_time(10, 35), 5), + (make_time(10, 40), make_time(11, 25), 30), + (make_time(11, 55), make_time(12, 40), 5), + (make_time(12, 45), make_time(13, 30), 15), + (make_time(13, 45), make_time(14, 30), 5), + (make_time(14, 35), make_time(15, 20), None), + ], + 4: [], # Пятница + 5: [ # Суббота + (make_time(8, 0), make_time(8, 45), 5), + (make_time(8, 50), make_time(9, 35), None), + ], +} + +# --- Остальное --- +KEYWORDS = {'крип', 'радиохэд', 'creep', 'radiohead'} + +# --- Авто-ответы ИИ --- +AUTOREPLY_CHANCE = 0.10 +AUTOREPLY_COOLDOWN = 120 +AUTOREPLY_TRIGGERS = {'бот', 'фраер', 'кент', 'братуха', 'пацаны', 'чё как', 'базар', 'ебать', 'пизда', 'хуй', 'бля', 'сука', 'говно', 'пидор', 'пиздабол', 'пиздатый', 'пиздатая', 'пиздатое', 'пиздатые', 'пиздато', 'ваномас'} + +def get_hint_text(templates_str: str) -> str: + return ( + "Команды:\n" + f"• /maket «id» «текст» — доступные: {templates_str}.\n" + "• /gen_mem Up:верх Down:низ — отправь с фото.\n" + "• /break — узнать время до перекура.\n" + "• /schedule [пн|mon] — расписание; без аргумента еще текущая и следующая пара.\n" + "• /penis — миниигра 1 раз в 24 часа.\n" + "• /top_penis — топ по длине.\n" + "• /talk [текст] — ИИ базарит на фене.\n" + "• /penis_casino [ставка] — казино на размер.\n" + "• /gadanie [тема] — гадание на фене с мемом.\n" + "• /matches — матчи для ставок.\n" + "• /bet [номер] [исход] [ставка] — поставить.\n" + "• /mybets — мои активные ставки.\n" + "• /bet_history — история ставок за месяц + статистика.\n" + "• /sports_debug — диагностика API матчей.\n" + "• /ebalnik — включить/выключить автоответы в чате.\n" + "• /uwu [теги] — случайная картинка с e621.\n" + "• /polychaetsi — топ по слову «получается».\n" + "• /prices /fetch — цены и остатки Магнита.\n" + "Подсказка: в группах пиши /команда@username_бота." + ) diff --git a/games/betting.py b/games/betting.py index c444cd2..03fb315 100644 --- a/games/betting.py +++ b/games/betting.py @@ -286,26 +286,26 @@ async def get_match_by_index(sport_alias: str, index: int) -> dict | None: return None -def place_bet(user_id: int, match: dict, sport: str, chosen_team: str, amount: float) -> str: +def place_bet(user_id: int, match: dict, sport: str, chosen_team: str, amount: float) -> tuple[bool, str]: current = get_user_length(user_id) if current is None: - return "Сначала заведи счёт через /penis, братуха." + return False, "Сначала заведи счёт через /penis, братуха." if current <= 0: - return "🚫 С кредитом ставки не принимаем." + return False, "🚫 С кредитом ставки не принимаем." if amount <= 0: - return "Ставка должна быть больше нуля." + return False, "Ставка должна быть больше нуля." if amount > config.BET_MAX_AMOUNT: - return f"Максимальная ставка — {config.BET_MAX_AMOUNT} см." - if amount > current: - return f"У тебя {current:.1f} см, а ставишь {amount:.1f}. Не хватает, фраер." + return False, f"Максимальная ставка — {config.BET_MAX_AMOUNT} см." + if round(amount, 2) > round(current, 2): + return False, f"У тебя {current:.1f} см, а ставишь {amount:.1f}. Не хватает, фраер." team_odds = match["odds"].get(chosen_team) if team_odds is None: - return f"Исход не найден. Доступные: {describe_match_outcomes(match)}" + return False, f"Исход не найден. Доступные: {describe_match_outcomes(match)}" new_length = update_user_length(user_id, -amount) if new_length is None: - return "Ошибка БД." + return False, "Ошибка БД." now_ts = int(_time.time()) try: @@ -323,12 +323,11 @@ def place_bet(user_id: int, match: dict, sport: str, chosen_team: str, amount: f except psycopg2.Error: logger.exception("Failed to place bet") update_user_length(user_id, amount) - return "Ошибка БД." + return False, "Ошибка БД." potential = round(amount * team_odds, 1) - return ( + return True, ( f"✅ Ставка принята!\n" - f"🏟 {match['home']} vs {match['away']}\n" f"📌 {chosen_team} (x{team_odds:.2f})\n" f"💰 Ставка: {amount:.1f} см\n" f"🎯 Возможный выигрыш: {potential:.1f} см\n" diff --git a/games/casino.py b/games/casino.py index 140ba6d..341dcb5 100644 --- a/games/casino.py +++ b/games/casino.py @@ -75,7 +75,7 @@ def play_casino(user_id: int, bet: float) -> str: return f"Максимальная ставка — {config.CASINO_MAX_BET} см, не жадничай." if bet <= 0: return "Ставка должна быть больше нуля, фраер." - if bet > current: + if round(bet, 2) > round(current, 2): return f"У тебя {current:.1f} см, а ставишь {bet:.1f}. Столько нет, фраер." reels, matches = spin_slots() diff --git a/games/economy.py b/games/economy.py index 3a796e8..ef68a6c 100644 --- a/games/economy.py +++ b/games/economy.py @@ -179,7 +179,8 @@ class EconomyManager: return False from_balance = self.get_user_balance(from_user_id) - if amount > from_balance * 0.2 or from_balance < amount: + limit = round(from_balance * 0.2, 2) + if round(amount, 2) > limit or round(from_balance, 2) < round(amount, 2): return False with get_conn() as conn: @@ -201,15 +202,17 @@ class EconomyManager: logger.info(f"Transfer: {amount} from user {from_user_id} to user {to_user_id}") return True - def create_deposit(self, user_id: int, amount: float, days: int) -> bool: + def create_deposit(self, user_id: int, amount: float, days: int) -> tuple[bool, str]: if amount <= 0: - return False + return False, "Сумма вклада должна быть больше нуля." balance = self.get_user_balance(user_id) min_deposit = float(self.get_setting('deposit_min_amount')) - if amount < min_deposit or balance < amount: - return False + if round(amount, 2) < round(min_deposit, 2): + return False, f"Минимальная сумма вклада — {min_deposit:.1f} см. Твой баланс: {balance:.1f} см." + if round(balance, 2) < round(amount, 2): + return False, f"Недостаточно средств. Твой баланс: {balance:.1f} см, а сумма вклада: {amount:.1f} см." central_bank_rate = float(self.get_setting('central_bank_rate')) matures_at = datetime.now() + timedelta(days=days) @@ -230,7 +233,7 @@ class EconomyManager: ''', (user_id, amount, central_bank_rate, matures_at)) logger.info(f"Deposit created: user {user_id}, amount {amount}, rate {central_bank_rate}%") - return True + return True, "Вклад успешно открыт!" def create_loan(self, user_id: int, amount: float, days: int) -> bool: if amount <= 0: @@ -239,7 +242,7 @@ class EconomyManager: max_loan = float(self.get_setting('loan_max_amount')) loan_rate = float(self.get_setting('loan_interest_rate')) - if amount > max_loan: + if round(amount, 2) > round(max_loan, 2): return False with get_conn() as conn: @@ -251,13 +254,13 @@ class EconomyManager: (user_id,), ) outstanding = float(cur.fetchone()['total']) - if outstanding + amount > max_loan: + if round(outstanding + amount, 2) > round(max_loan, 2): return False # ЦБ должен иметь достаточно капитала cur.execute("SELECT capital FROM central_bank WHERE id=1") cb_capital = float(cur.fetchone()['capital']) - if cb_capital < amount: + if round(cb_capital, 2) < round(amount, 2): return False due_at = datetime.now() + timedelta(days=days) @@ -293,10 +296,10 @@ class EconomyManager: if not loan or loan['is_repaid']: return False - total_repayment = loan['amount'] * (1 + loan['interest_rate'] / 100) + total_repayment = round(loan['amount'] * (1 + loan['interest_rate'] / 100), 2) balance = self.get_user_balance(user_id) - if balance < total_repayment: + if round(balance, 2) < total_repayment: return False wcur = conn.cursor() @@ -479,7 +482,7 @@ class EconomyManager: logger.info(f"Central bank updated: {capital_change} ({transaction_type})") def emit_money(self, amount: float, reason: str = "") -> bool: - if self.get_central_bank_stats()['capital'] < amount: + if round(self.get_central_bank_stats()['capital'], 2) < round(amount, 2): return False self.update_central_bank(-amount, 'emission', reason) logger.info(f"Money emitted: {amount} cm ({reason})") diff --git a/main.py b/main.py index 0840ecc..31b3750 100644 --- a/main.py +++ b/main.py @@ -1263,12 +1263,12 @@ async def handle_deposit_cmd(message: Message): await message.reply("Сумма и дни должны быть положительными.", parse_mode=None) return - success = await asyncio.to_thread(economy.create_deposit, message.from_user.id, amount, days) + success, error_msg = await asyncio.to_thread(economy.create_deposit, message.from_user.id, amount, days) if success: rate = economy.get_setting('central_bank_rate') await message.reply(f"✅ Вклад открыт!\n💰 Сумма: {amount:.1f} см\n📅 Срок: {days} дней\n📈 Ставка: {rate}% годовых", parse_mode=None) else: - await message.reply("❌ Не удалось открыть вклад. Проверь баланс и минимальную сумму.", parse_mode=None) + await message.reply(f"❌ Не удалось открыть вклад. {error_msg}", parse_mode=None) async def handle_loans_cmd(message: Message): """Показать кредиты""" @@ -1562,7 +1562,7 @@ async def handle_bet_cmd(message: Message): ) return - result = await asyncio.to_thread(place_bet, message.from_user.id, match, sport_alias, team, amount) + success, result = await asyncio.to_thread(place_bet, message.from_user.id, match, sport_alias, team, amount) await message.reply(result, parse_mode=None) async def handle_mybets_cmd(message: Message): diff --git a/webapp/api.py b/webapp/api.py index ee29af3..12ab236 100644 --- a/webapp/api.py +++ b/webapp/api.py @@ -253,7 +253,9 @@ async def create_bet( raise HTTPException(status_code=400, detail="Неверный исход ставки") # 3. Разместить ставку (синхронная функция) - result = await asyncio.to_thread(place_bet, user_id, match, req.league, team, req.amount) + success, result = await asyncio.to_thread(place_bet, user_id, match, req.league, team, req.amount) + if not success: + raise HTTPException(status_code=400, detail=result) return {"message": result} @@ -294,7 +296,7 @@ async def play_casino_api( raise HTTPException(status_code=400, detail=f"Макс ставка: {config.CASINO_MAX_BET} см") if req.bet <= 0: raise HTTPException(status_code=400, detail="Ставка > 0") - if req.bet > current: + if round(req.bet, 2) > round(current, 2): raise HTTPException(status_code=400, detail=f"У тебя {current:.1f} см") reels, match_count = await asyncio.to_thread(spin_slots)