From 107a0647d104039596de3f8d4033157df522e3a2 Mon Sep 17 00:00:00 2001 From: q Date: Fri, 20 Mar 2026 12:24:05 +0300 Subject: [PATCH] fix --- README.md | 4 +- config.py | 2 +- games/betting.py | 118 ++++++++++++++++++++++++++++++++++++++++------- main.py | 103 ++++++++++++++++++++++++++++++++--------- 4 files changed, 184 insertions(+), 43 deletions(-) diff --git a/README.md b/README.md index fe95fe2..d586f05 100644 --- a/README.md +++ b/README.md @@ -56,7 +56,7 @@ docker compose down - `/gadanie <тема>` — ИИ-гадание на фене + авто-мем. - `/ebalnik` — вкл/выкл авто-ответы бота в чате. - `/matches [cs|dota|football]` — ближайшие матчи с коэффициентами. -- `/bet <спорт> <номер> <команда> <ставка>` — поставить см на матч. +- `/bet [спорт] <номер> <исход> <ставка>` — поставить см на матч. - `/mybets` — мои активные ставки. - `/svodka` — сводка СВО. @@ -86,7 +86,7 @@ docker compose down - `/talk` шлёт запрос к llama.cpp серверу с контекстом 100 сообщений (хранятся в SQLite). - `/penis_casino` — слоты с множителем x1.2–x3 (`games/casino.py`). - `/gadanie` — ИИ-гадание + авто-мем через `generate_meme` (`games/fortune.py`). -- `/matches` + `/bet` — ставки на реальные матчи, коэффициенты через The-Odds-API (`games/betting.py`). +- `/matches` + `/bet` — ставки на реальные матчи, коэффициенты через The-Odds-API (`games/betting.py`). После `/matches cs|dota|football` можно короче: `/bet <номер> <исход> <ставка>`. ## ИИ-модуль (`AI/talk_handler.py`) diff --git a/config.py b/config.py index c8c68c1..901d0e2 100644 --- a/config.py +++ b/config.py @@ -206,7 +206,7 @@ def get_hint_text(templates_str: str) -> str: "• /penis_casino [ставка] — казино на размер.\n" "• /gadanie [тема] — гадание на фене с мемом.\n" "• /matches [cs|dota|football] — матчи для ставок.\n" - "• /bet <спорт> <номер> <команда> <ставка> — поставить см.\n" + "• /bet [спорт] [номер] [исход] [ставка] — поставить см.\n" "• /mybets — мои ставки.\n" "• /ebalnik — включить/выключить автоответы в чате.\n" "• /polychaetsi — топ по слову «получается».\n" diff --git a/games/betting.py b/games/betting.py index c5da421..9aaaea1 100644 --- a/games/betting.py +++ b/games/betting.py @@ -13,6 +13,8 @@ from games.casino import get_user_length, update_user_length logger = logging.getLogger(__name__) _matches_cache: dict[str, tuple[float, list]] = {} +_last_viewed_sport_by_user: dict[int, str] = {} +_DRAW_NAMES = {"draw", "tie", "ничья"} def _init_bets_db() -> None: @@ -43,6 +45,19 @@ def _init_bets_db() -> None: _init_bets_db() +def remember_user_sport_context(user_id: int, sport_alias: str) -> None: + if sport_alias in config.ODDS_SPORTS: + _last_viewed_sport_by_user[user_id] = sport_alias + + +def clear_user_sport_context(user_id: int) -> None: + _last_viewed_sport_by_user.pop(user_id, None) + + +def get_user_sport_context(user_id: int) -> str | None: + return _last_viewed_sport_by_user.get(user_id) + + async def fetch_matches(sport_key: str) -> list[dict]: now = _time.time() cached = _matches_cache.get(sport_key) @@ -96,16 +111,87 @@ async def fetch_matches(sport_key: str) -> list[dict]: return matches -def format_matches(matches: list[dict], sport_label: str) -> str: - if not matches: - return f"🏟 {sport_label} — матчей не найдено." +def _get_match_outcomes(match: dict) -> list[dict[str, str | float]]: + odds = match.get("odds", {}) + outcomes: list[dict[str, str | float]] = [] + added: set[str] = set() - lines = [f"🏟 {sport_label} — ближайшие матчи:\n"] + def append_outcome(code: str, name: str) -> None: + if name in odds and name not in added: + outcomes.append({"code": code, "name": name, "odds": odds[name]}) + added.add(name) + + home = match.get("home", "") + away = match.get("away", "") + append_outcome("1", home) + + draw_name = next((name for name in odds if name.casefold() in _DRAW_NAMES), None) + if draw_name: + append_outcome("X", draw_name) + + append_outcome("2", away) + + next_code = 3 + for name in odds: + if name in added: + continue + append_outcome(str(next_code), name) + next_code += 1 + + return outcomes + + +def describe_match_outcomes(match: dict) -> str: + return ", ".join( + f"{item['code']}={item['name']} x{item['odds']:.2f}" + for item in _get_match_outcomes(match) + ) + + +def resolve_match_outcome(match: dict, raw_choice: str) -> str | None: + choice = raw_choice.strip() + if not choice: + return None + + normalized = choice.casefold() + outcomes = _get_match_outcomes(match) + + for item in outcomes: + if normalized == str(item["code"]).casefold(): + return str(item["name"]) + + if normalized in _DRAW_NAMES or normalized == "x": + for item in outcomes: + if str(item["code"]).casefold() == "x": + return str(item["name"]) + + for item in outcomes: + if normalized == str(item["name"]).casefold(): + return str(item["name"]) + + try: + choice_odds = round(float(choice.replace(",", ".")), 2) + except ValueError: + return None + + for item in outcomes: + if round(float(item["odds"]), 2) == choice_odds: + return str(item["name"]) + + return None + + +def format_matches(matches: list[dict], sport_alias: str, sport_label: str) -> str: + if not matches: + return f"🏟 {sport_label} [{sport_alias}] — матчей не найдено." + + lines = [f"🏟 {sport_label} [{sport_alias}] — ближайшие матчи:\n"] for i, m in enumerate(matches[:10], 1): - odds_parts = [] - for team, coeff in m["odds"].items(): - odds_parts.append(f"{team}: {coeff:.2f}") - odds_str = " | ".join(odds_parts) + outcomes = _get_match_outcomes(m) + odds_str = "\n".join( + f" {item['code']}) {item['name']}: {item['odds']:.2f}" + for item in outcomes + ) start_str = "" if m["commence"]: @@ -116,9 +202,11 @@ def format_matches(matches: list[dict], sport_label: str) -> str: except Exception: pass - lines.append(f"{i}️⃣ {m['home']} vs {m['away']}\n {odds_str}{start_str}\n") + lines.append(f"{i}️⃣ {m['home']} vs {m['away']}{start_str}\n{odds_str}\n") - lines.append("Ставка: /bet <номер> <команда> <сумма>") + lines.append(f"Ставка: /bet {sport_alias} [номер] [исход] [ставка]") + lines.append(f"Коротко после /matches {sport_alias}: /bet [номер] [исход] [ставка]") + lines.append("Исход: 1/2, для ничьи X. Можно писать ещё название команды или коэффициент.") return "\n".join(lines) @@ -131,18 +219,15 @@ async def get_formatted_matches(sport_alias: str | None) -> str: if sport_alias and sport_alias in config.ODDS_SPORTS: sport_key = config.ODDS_SPORTS[sport_alias] matches = await fetch_matches(sport_key) - return format_matches(matches, _get_sport_label(sport_alias)) + return format_matches(matches, sport_alias, _get_sport_label(sport_alias)) parts = [] for alias, sport_key in config.ODDS_SPORTS.items(): matches = await fetch_matches(sport_key) - parts.append(format_matches(matches, _get_sport_label(alias))) + parts.append(format_matches(matches, alias, _get_sport_label(alias))) return "\n\n".join(parts) -_last_fetched_matches: dict[str, list[dict]] = {} - - async def get_match_by_index(sport_alias: str, index: int) -> dict | None: sport_key = config.ODDS_SPORTS.get(sport_alias) if not sport_key: @@ -172,8 +257,7 @@ def place_bet(user_id: int, match: dict, sport: str, chosen_team: str, amount: f team_odds = match["odds"].get(chosen_team) if team_odds is None: - available = ", ".join(match["odds"].keys()) - return f"Команда не найдена. Доступные: {available}" + return f"Исход не найден. Доступные: {describe_match_outcomes(match)}" new_length = update_user_length(user_id, -amount) if new_length is None: diff --git a/main.py b/main.py index a30a13a..6a2d653 100644 --- a/main.py +++ b/main.py @@ -22,7 +22,18 @@ 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 games.betting import get_formatted_matches, get_match_by_index, place_bet, get_user_bets, settle_bets +from games.betting import ( + clear_user_sport_context, + describe_match_outcomes, + get_formatted_matches, + get_match_by_index, + get_user_bets, + get_user_sport_context, + place_bet, + remember_user_sport_context, + resolve_match_outcome, + settle_bets, +) from zparser import get_military_data import config @@ -931,7 +942,7 @@ def build_polychaetsi_top(limit: int = config.POLYCHAETSI_TOP_LIMIT) -> str: # --- Хендлеры aiogram --- async def handle_help(message: Message): - await message.answer(config.get_hint_text(templates_list_text())) + await message.answer(config.get_hint_text(templates_list_text()), parse_mode=None) async def handle_start(message: Message): await message.answer("Соси") @@ -1185,7 +1196,7 @@ async def handle_penis_casino_cmd(message: Message): return parts = message.text.split(maxsplit=1) if len(parts) < 2: - await message.reply("Формат: /penis_casino <ставка>\nПример: /penis_casino 0.5") + await message.reply("Формат: /penis_casino [ставка]\nПример: /penis_casino 0.5", parse_mode=None) return try: bet = round(float(parts[1].replace(",", ".")), 1) @@ -1217,8 +1228,19 @@ async def handle_gadanie_cmd(message: Message): await message.reply("Бля, карты рассыпались, попробуй позже.") async def handle_matches_cmd(message: Message): - parts = message.text.split(maxsplit=1) + raw_text = message.text or "" + parts = raw_text.split(maxsplit=1) sport_alias = parts[1].strip().lower() if len(parts) > 1 else None + if sport_alias and sport_alias not in config.ODDS_SPORTS: + if message.from_user: + clear_user_sport_context(message.from_user.id) + await message.reply("Неизвестный спорт. Доступно: cs / dota / football", parse_mode=None) + return + if message.from_user: + if sport_alias and sport_alias in config.ODDS_SPORTS: + remember_user_sport_context(message.from_user.id, sport_alias) + else: + clear_user_sport_context(message.from_user.id) await message.bot.send_chat_action(chat_id=message.chat.id, action="typing") result = await get_formatted_matches(sport_alias) await message.reply(result, parse_mode=None) @@ -1227,35 +1249,70 @@ async def handle_bet_cmd(message: Message): if not message.from_user: await message.answer("Команда доступна только пользователям.") return - parts = message.text.split() + raw_text = message.text or "" + parts = raw_text.split() if len(parts) < 4: await message.reply( - "Формат: /bet <номер> <команда> <ставка>\n" - "Пример: /bet cs 1 NAVI 0.5\n" - "Спорт: cs / dota / football" + "Формат: /bet [спорт] [номер] [исход] [ставка]\n" + "Коротко после /matches cs|dota|football: /bet [номер] [исход] [ставка]\n" + "Исход: 1/2, для ничьи X, можно ещё название команды или коэффициент.\n" + "Пример: /bet cs 1 1 0.5", + parse_mode=None, ) return - # /bet <спорт> <номер> <команда> <ставка> - if len(parts) >= 5: + + sport_alias: str | None + if parts[1].lower() in config.ODDS_SPORTS: + if len(parts) < 5: + await message.reply( + "Формат: /bet [спорт] [номер] [исход] [ставка]\nПример: /bet cs 1 1 0.5", + parse_mode=None, + ) + return sport_alias = parts[1].lower() - try: - index = int(parts[2]) - except ValueError: - await message.reply("Номер матча должен быть числом.") - return - team = parts[3] - try: - amount = round(float(parts[4].replace(",", ".")), 1) - except ValueError: - await message.reply("Ставка должна быть числом.") - return + index_token = parts[2] + selection = " ".join(parts[3:-1]).strip() + amount_token = parts[-1] else: - await message.reply("Формат: /bet <спорт> <номер> <команда> <ставка>") + sport_alias = get_user_sport_context(message.from_user.id) + if not sport_alias: + await message.reply( + "Сначала открой /matches cs, /matches dota или /matches football.\n" + "Либо пиши полный формат: /bet [спорт] [номер] [исход] [ставка]", + parse_mode=None, + ) + return + index_token = parts[1] + selection = " ".join(parts[2:-1]).strip() + amount_token = parts[-1] + + if not selection: + await message.reply("Нужен исход: 1, 2, X, название команды или коэффициент.", parse_mode=None) + return + + try: + index = int(index_token) + except ValueError: + await message.reply("Номер матча должен быть числом.", parse_mode=None) + return + + try: + amount = round(float(amount_token.replace(",", ".")), 1) + except ValueError: + await message.reply("Ставка должна быть числом.", parse_mode=None) return match = await get_match_by_index(sport_alias, index) if not match: - await message.reply("Матч не найден. Посмотри /matches") + await message.reply(f"Матч не найден. Посмотри /matches {sport_alias}", parse_mode=None) + return + + team = resolve_match_outcome(match, selection) + if not team: + await message.reply( + f"Исход не найден. Доступные: {describe_match_outcomes(match)}", + parse_mode=None, + ) return result = await asyncio.to_thread(place_bet, message.from_user.id, match, sport_alias, team, amount)