From 7035a9065606ff97eb37b43c431073a7539231c6 Mon Sep 17 00:00:00 2001 From: Danil Date: Sat, 7 Mar 2026 15:19:46 +0300 Subject: [PATCH 1/2] =?UTF-8?q?=D0=9E=D0=B1=D0=BD=D0=BE=D0=B2=D0=BB=D0=B5?= =?UTF-8?q?=D0=BD=D0=B8=D0=B5=20=D1=82=D1=80=D0=B8=D0=B3=D0=B3=D0=B5=D1=80?= =?UTF-8?q?=D0=BE=D0=B2=20=D0=B0=D0=B2=D1=82=D0=BE-=D0=BE=D1=82=D0=B2?= =?UTF-8?q?=D0=B5=D1=82=D0=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- config.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config.py b/config.py index 545b5a4..183f263 100644 --- a/config.py +++ b/config.py @@ -80,7 +80,7 @@ KEYWORDS = {'крип', 'радиохэд', 'creep', 'radiohead'} # --- Авто-ответы ИИ --- AUTOREPLY_CHANCE = 0.10 AUTOREPLY_COOLDOWN = 120 -AUTOREPLY_TRIGGERS = {'бот', 'фраер', 'кент', 'братуха', 'пацаны', 'чё как', 'базар', 'ебать', 'пизда', 'хуй', 'бля', 'сука', 'говно', 'пидор', 'пиздабол', 'пиздатый', 'пиздатая', 'пиздатое', 'пиздатые', 'пиздато', 'пиздато', 'пиздато', 'пиздато', 'ваномас'} +AUTOREPLY_TRIGGERS = {'бот', 'фраер', 'кент', 'братуха', 'пацаны', 'чё как', 'базар', 'ебать', 'пизда', 'хуй', 'бля', 'сука', 'говно', 'пидор', 'пиздабол', 'пиздатый', 'пиздатая', 'пиздатое', 'пиздатые', 'пиздато', 'ваномас'} def get_hint_text(templates_str: str) -> str: return ( From 9f6a7464d1591d52c33d39fbc9d08084eaa2be13 Mon Sep 17 00:00:00 2001 From: Danil Date: Sat, 7 Mar 2026 15:52:49 +0300 Subject: [PATCH 2/2] =?UTF-8?q?=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB=D0=B5?= =?UTF-8?q?=D0=BD=20=D0=BA=D0=B0=D0=B7=D0=B8=D0=BA=20=D0=B8=20=D0=B3=D0=B0?= =?UTF-8?q?=D0=B4=D0=B0=D0=BD=D0=B8=D0=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 22 ++++++-- config.py | 5 ++ games/__init__.py | 0 games/casino.py | 127 ++++++++++++++++++++++++++++++++++++++++++++++ games/fortune.py | 57 +++++++++++++++++++++ main.py | 43 ++++++++++++++++ 6 files changed, 249 insertions(+), 5 deletions(-) create mode 100644 games/__init__.py create mode 100644 games/casino.py create mode 100644 games/fortune.py diff --git a/README.md b/README.md index a2e96c5..4528060 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,11 @@ - вписывает текст шрифтом Impact (белый с чёрной обводкой) в макеты (`/maket`); - делает мемы из присланных фото (`/gen_mem Up:... Down:...`); - подсказывает, сколько минут до перекура по заданному расписанию (`/break`); -- умеет базарить (`/talk`). +- умеет базарить (`/talk`) с контекстом 100 сообщений; +- авто-ответы в чате (10% шанс + триггер-слова); +- миниигра `/penis` + `/top_penis`; +- казино на размер (`/penis_casino`); +- гадание на фене с мемом (`/gadanie`). ## Запуск @@ -22,7 +26,13 @@ docker-compose up --build - `/maket <текст>` — вписать текст в макет. Доступные: `1 — Владимир Владимирович Путин самый лучший президент` (737x414, точка 104,240). - `/gen_mem Up:верх Down:низ` — прислать с фото (подпись) или ответом на фото. Можно указывать только Up или только Down. - `/break` — показывает, сколько минут до ближайшего перекура/идёт ли перекур. -- `/talk <сообщение>` — побазарить с ИИ. Работает через llama.cpp сервер. +- `/talk <сообщение>` — побазарить с ИИ (контекст 100 сообщений). +- `/penis` — миниигра, 1 раз в 24 часа. +- `/top_penis` — топ по длине. +- `/penis_casino <ставка>` — казино на размер (макс 1 см, казино всегда в выигрыше). +- `/gadanie <тема>` — ИИ-гадание на фене + авто-мем. +- `/ebalnik` — вкл/выкл авто-ответы бота в чате. +- `/svodka` — сводка СВО. ## Макеты и шрифт @@ -39,12 +49,14 @@ docker-compose up --build - `/maket` подбирает размер текста, чтобы уместить его в область макета. - `/gen_mem` центрирует текст по ширине, масштабирует под изображение, рисует белый текст с чёрной обводкой. -- `/break` считает по расписанию в `main.py` (словарь `SCHEDULE`). -- `/talk` шлёт запрос к llama.cpp серверу (`LLAMA_API_URL`), системный промпт задаёт стиль фени(Можно изменить). +- `/break` считает по расписанию в `config.py`. +- `/talk` шлёт запрос к llama.cpp серверу с контекстом 100 сообщений (хранятся в SQLite). +- `/penis_casino` — слоты с множителем x1.2–x3 (`games/casino.py`). +- `/gadanie` — ИИ-гадание + авто-мем через `generate_meme` (`games/fortune.py`). ## ИИ-модуль (`AI/talk_handler.py`) -Отдельный модуль, не трогает основной код. Подключается к llama.cpp серверу через OpenAI-совместимый API (`/v1/chat/completions`). +Подключается к llama.cpp серверу через OpenAI-совместимый API (`/v1/chat/completions`). Хранит контекст 100 сообщений в SQLite. Настройка сервера: 1. Скачать модель: `wget https://huggingface.co/Qwen/Qwen2.5-1.5B-Instruct-GGUF/resolve/main/qwen2.5-1.5b-instruct-q4_k_m.gguf` diff --git a/config.py b/config.py index 26bcebb..dad297b 100644 --- a/config.py +++ b/config.py @@ -18,6 +18,9 @@ PENIS_COOLDOWN = timedelta(hours=24) PENIS_COOLDOWN_SECONDS = int(PENIS_COOLDOWN.total_seconds()) PENIS_TOP_LIMIT = 10 +# --- Казино --- +CASINO_MAX_BET = 1.0 + # --- Конфигурация макетов --- TEMPLATES = { "1": { @@ -97,6 +100,8 @@ def get_hint_text(templates_str: str) -> str: "• /penis — миниигра 1 раз в 24 часа.\n" "• /top_penis — топ по длине.\n" "• /talk [текст] — ИИ базарит на фене.\n" + "• /penis_casino [ставка] — казино на размер.\n" + "• /gadanie [тема] — гадание на фене с мемом.\n" "• /ebalnik — включить/выключить автоответы в чате.\n" "• /prices /fetch — цены.\n" "Подсказка: в группах пиши /команда@username_бота." diff --git a/games/__init__.py b/games/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/games/casino.py b/games/casino.py new file mode 100644 index 0000000..6c4a534 --- /dev/null +++ b/games/casino.py @@ -0,0 +1,127 @@ +import random +import sqlite3 +import logging + +import config + +logger = logging.getLogger(__name__) + +# Символы слотов и их веса (чем выше вес — тем чаще выпадает) +SLOT_SYMBOLS = ["🍒", "🍋", "🔔", "💎", "7️⃣", "🍀"] +SLOT_WEIGHTS = [30, 25, 20, 12, 8, 5] # 🍒 чаще всего, 🍀 реже всего + +# Множители для выигрышей: {кол-во совпадений: (мин, макс)} +MULTIPLIERS = { + 2: (1.2, 1.8), # две одинаковых + 3: (2.0, 3.0), # три одинаковых (джекпот, редко) +} + +CASINO_WIN_CHANCE = 0.35 + + +def spin_slots() -> tuple[list[str], int]: + reels = random.choices(SLOT_SYMBOLS, weights=SLOT_WEIGHTS, k=3) + + if random.random() > CASINO_WIN_CHANCE: + while len(set(reels)) < 3: + reels = random.choices(SLOT_SYMBOLS, weights=SLOT_WEIGHTS, k=3) + return reels, 0 + + counts = {} + for s in reels: + counts[s] = counts.get(s, 0) + 1 + max_match = max(counts.values()) + return reels, max_match + + +def get_user_length(user_id: int) -> float | None: + try: + with sqlite3.connect(config.PENIS_DB_PATH) as conn: + row = conn.execute( + "SELECT length FROM penis_stats WHERE user_id = ?", + (user_id,), + ).fetchone() + return float(row[0]) if row else None + except sqlite3.Error: + logger.exception("casino: failed to get length") + return None + + +def update_user_length(user_id: int, delta: float) -> float | None: + try: + with sqlite3.connect(config.PENIS_DB_PATH) as conn: + row = conn.execute( + "SELECT length FROM penis_stats WHERE user_id = ?", + (user_id,), + ).fetchone() + if row is None: + return None + new_length = round(float(row[0]) + delta, 1) + conn.execute( + "UPDATE penis_stats SET length = ? WHERE user_id = ?", + (new_length, user_id), + ) + conn.commit() + return new_length + except sqlite3.Error: + logger.exception("casino: failed to update length") + return None + + +def play_casino(user_id: int, bet: float) -> str: + current = get_user_length(user_id) + if current is None: + return "Ты ещё ни разу не крутил /penis, братуха. Сначала заведи счёт." + + # Проверяем отрицательный баланс + if current <= 0: + return "🚫 С кредитом в казино не впускают, братуха. Иди наращивай через /penis." + + # Проверяем максимум ставки + if bet > config.CASINO_MAX_BET: + return f"Максимальная ставка — {config.CASINO_MAX_BET} см, не жадничай." + + if bet <= 0: + return "Ставка должна быть больше нуля, фраер." + + # Проверяем хватает ли + if bet > current: + return f"У тебя {current:.1f} см, а ставишь {bet:.1f}. Столько нет, фраер." + + # Крутим + reels, matches = spin_slots() + slots_display = " | ".join(reels) + + if matches >= 2: + min_mult, max_mult = MULTIPLIERS[matches] + multiplier = round(random.uniform(min_mult, max_mult), 1) + winnings = round(bet * multiplier, 1) + new_length = update_user_length(user_id, winnings) + if new_length is None: + return "Ошибка БД. Попробуй позже." + + if matches == 3: + return ( + f"🎰 {slots_display}\n\n" + f"🎉 ДЖЕКПОТ!!! Три одинаковых!\n" + f"Множитель: x{multiplier}\n" + f"Выигрыш: +{winnings:.1f} см\n" + f"📏 Текущий размер: {new_length:.1f} см" + ) + return ( + f"🎰 {slots_display}\n\n" + f"✅ Выигрыш! Две совпали.\n" + f"Множитель: x{multiplier}\n" + f"Выигрыш: +{winnings:.1f} см\n" + f"📏 Текущий размер: {new_length:.1f} см" + ) + else: + new_length = update_user_length(user_id, -bet) + if new_length is None: + return "Ошибка БД. Попробуй позже." + return ( + f"🎰 {slots_display}\n\n" + f"❌ Мимо, братуха.\n" + f"Проигрыш: -{bet:.1f} см\n" + f"📏 Текущий размер: {new_length:.1f} см" + ) diff --git a/games/fortune.py b/games/fortune.py new file mode 100644 index 0000000..7ac5c71 --- /dev/null +++ b/games/fortune.py @@ -0,0 +1,57 @@ +import logging +import aiohttp +import os + +logger = logging.getLogger(__name__) + +LLAMA_API_URL = os.getenv("LLAMA_API_URL", "https://mirror.porno4free.ru/zovos-ai/") + +FORTUNE_PROMPT = ( + "Ты — уличный гадатель с района, базаришь на фене. " + "Пользователь хочет погадать на тему. Выдай шуточное гадание, " + "коротко и дерзко, 2-3 предложения максимум. " + "В конце ОБЯЗАТЕЛЬНО напиши на отдельной строке в формате:\n" + "MEM_UP: <текст сверху для мема, максимум 4 слова>\n" + "MEM_DOWN: <текст снизу для мема, максимум 4 слова>\n" + "Тексты для мема должны быть смешными и короткими." +) + + +async def generate_fortune(topic: str) -> tuple[str, str, str]: + url = f"{LLAMA_API_URL.rstrip('/')}/v1/chat/completions" + payload = { + "messages": [ + {"role": "system", "content": FORTUNE_PROMPT}, + {"role": "user", "content": f"Погадай мне на: {topic}"}, + ], + "max_tokens": 200, + "temperature": 0.9, + "top_p": 0.9, + } + + async with aiohttp.ClientSession() as session: + async with session.post(url, json=payload, timeout=aiohttp.ClientTimeout(total=120)) as resp: + data = await resp.json() + text = data["choices"][0]["message"]["content"].strip() + + up_text = "" + down_text = "" + fortune_lines = [] + + for line in text.split("\n"): + stripped = line.strip() + if stripped.upper().startswith("MEM_UP:"): + up_text = stripped.split(":", 1)[1].strip() + elif stripped.upper().startswith("MEM_DOWN:"): + down_text = stripped.split(":", 1)[1].strip() + else: + fortune_lines.append(line) + + fortune_text = "\n".join(fortune_lines).strip() + + if not up_text: + up_text = "Гадание" + if not down_text: + down_text = "по фене" + + return fortune_text, up_text, down_text diff --git a/main.py b/main.py index 3ed8c4f..6c8936a 100644 --- a/main.py +++ b/main.py @@ -19,6 +19,8 @@ from aiogram.client.default import DefaultBotProperties # Локальные импорты from AI.talk_handler import handle_talk, generate_autoreply, push_message +from games.casino import play_casino +from games.fortune import generate_fortune from zparser import get_military_data import config @@ -711,6 +713,43 @@ async def handle_keywords(message: Message): except Exception: logger.exception("Autoreply failed") +async def handle_penis_casino_cmd(message: Message): + if not message.from_user: + await message.answer("Команда доступна только пользователям.") + return + parts = message.text.split(maxsplit=1) + if len(parts) < 2: + await message.reply("Формат: /penis_casino <ставка>\nПример: /penis_casino 0.5") + return + try: + bet = round(float(parts[1].replace(",", ".")), 1) + except ValueError: + await message.reply("Ставка должна быть числом, фраер. Пример: /penis_casino 0.5") + return + result = await asyncio.to_thread(play_casino, message.from_user.id, bet) + await message.reply(result, parse_mode=None) + +async def handle_gadanie_cmd(message: Message): + parts = message.text.split(maxsplit=1) + if len(parts) < 2 or not parts[1].strip(): + await message.reply("Напиши на что гадать, кент. Пример: /gadanie любовь") + return + topic = parts[1].strip() + await message.bot.send_chat_action(chat_id=message.chat.id, action="typing") + try: + fortune_text, up_text, down_text = await generate_fortune(topic) + await message.reply(f"🔮 Гадание на «{topic}»:\n\n{fortune_text}", parse_mode=None) + photo_path = config.BASE_DIR / "maket" / "vape.jpg" + if photo_path.exists(): + img = Image.open(photo_path).convert("RGB") + meme_output = await asyncio.to_thread(generate_meme, img, up_text, down_text) + await message.reply_photo( + BufferedInputFile(meme_output.getvalue(), filename="gadanie.jpg") + ) + except Exception: + logger.exception("Gadanie failed") + await message.reply("Бля, карты рассыпались, попробуй позже.") + # --- Запуск --- def main(): @@ -735,6 +774,8 @@ def main(): dp.message.register(handle_talk, Command("talk")) dp.message.register(handle_svodka_cmd, Command("svodka")) dp.message.register(handle_ebalnik_cmd, Command("ebalnik")) + dp.message.register(handle_penis_casino_cmd, Command("penis_casino")) + dp.message.register(handle_gadanie_cmd, Command("gadanie")) dp.message.register(handle_gen_mem, Command("gen_mem")) dp.message.register(handle_keywords, F.text) @@ -754,6 +795,8 @@ def main(): BotCommand(command="zvetok", description="Цветянский бля"), BotCommand(command="talk", description="Побазарить"), BotCommand(command="ebalnik", description="вкл/выкл автответов"), + BotCommand(command="penis_casino", description="казино на размер"), + BotCommand(command="gadanie", description="гадание на фене"), BotCommand(command="svodka", description="СВО: итоги"), ] scopes = (