diff --git a/config.py b/config.py index b0c2354..06840b9 100644 --- a/config.py +++ b/config.py @@ -35,13 +35,7 @@ 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 = { - "epl": "soccer_epl", - "ucl": "soccer_uefa_champs_league", - "laliga": "soccer_spain_la_liga", - "bundesliga": "soccer_germany_bundesliga", - "seriea": "soccer_italy_serie_a", - "ligue1": "soccer_france_ligue_one", - "europa": "soccer_uefa_europa_league", + "dota2": "esports_dota2", } ODDS_CACHE_TTL = 86400 # 24 часа — экономим запросы (500/мес бесплатно) @@ -219,8 +213,8 @@ def get_hint_text(templates_str: str) -> str: "• /talk [текст] — ИИ базарит на фене.\n" "• /penis_casino [ставка] — казино на размер.\n" "• /gadanie [тема] — гадание на фене с мемом.\n" - "• /matches [epl|ucl|laliga|bundesliga|seriea|ligue1|europa] — матчи для ставок.\n" - "• /bet [лига] [номер] [исход] [ставка] — поставить см.\n" + "• /matches — матчи для ставок.\n" + "• /bet [номер] [исход] [ставка] — поставить.\n" "• /mybets — мои активные ставки.\n" "• /bet_history — история ставок за месяц + статистика.\n" "• /sports_debug — диагностика API матчей.\n" diff --git a/games/betting.py b/games/betting.py index 89570d0..5863acb 100644 --- a/games/betting.py +++ b/games/betting.py @@ -1,11 +1,17 @@ import logging import time as _time +import hashlib +import random +import urllib.request +import json +import gzip from datetime import datetime from zoneinfo import ZoneInfo +from bs4 import BeautifulSoup -import aiohttp import psycopg2 import psycopg2.extras +import aiohttp import config from db import get_conn @@ -19,6 +25,7 @@ _last_viewed_sport_by_user: dict[int, str] = {} _DRAW_NAMES = {"draw", "tie", "ничья"} BET_HISTORY_DAYS = 30 +_LIQUIPEDIA_CACHE_TTL = 300 # 5 minutes cache to respect Liquipedia rate limits def _init_bets_db() -> None: @@ -68,91 +75,119 @@ def get_user_sport_context(user_id: int) -> str | None: async def _fetch_available_sports() -> list[dict]: - global _sports_cache + return [{"key": "dota2", "title": "🎮 Dota 2", "active": True}] + + +async def fetch_liquipedia_matches() -> list[dict]: + global _matches_cache now = _time.time() - if _sports_cache and (now - _sports_cache[0]) < 3600: - return _sports_cache[1] - - if not config.ODDS_API_KEY: - return [] - - url = f"{config.ODDS_API_BASE}/sports" - params = {"apiKey": config.ODDS_API_KEY} - - try: - async with aiohttp.ClientSession() as session: - async with session.get(url, params=params, timeout=aiohttp.ClientTimeout(total=15)) as resp: - if resp.status != 200: - body = await resp.text() - logger.warning(f"Sports API returned {resp.status}: {body[:200]}") - return [] - data = await resp.json() - _sports_cache = (now, data) - return data - except Exception: - logger.exception("Failed to fetch sports list") - return [] - - -async def fetch_matches(sport_key: str) -> list[dict]: - now = _time.time() - cached = _matches_cache.get(sport_key) - if cached and (now - cached[0]) < config.ODDS_CACHE_TTL: + cached = _matches_cache.get("liquipedia_all") + if cached and (now - cached[0]) < _LIQUIPEDIA_CACHE_TTL: return cached[1] - if not config.ODDS_API_KEY: - logger.warning("ODDS_API_KEY не задан, матчи недоступны") - return [] - - url = f"{config.ODDS_API_BASE}/sports/{sport_key}/odds" - params = { - "apiKey": config.ODDS_API_KEY, - "regions": "eu", - "markets": "h2h", - "oddsFormat": "decimal", + url = "https://liquipedia.net/dota2/api.php?action=parse&page=Liquipedia:Matches&format=json" + headers = { + "User-Agent": "Dota2BettingTelegramBot/1.0 (contact: danil@example.com)", + "Accept-Encoding": "gzip" } try: async with aiohttp.ClientSession() as session: - async with session.get(url, params=params, timeout=aiohttp.ClientTimeout(total=15)) as resp: + async with session.get(url, headers=headers, timeout=aiohttp.ClientTimeout(total=15)) as resp: if resp.status != 200: - body = await resp.text() - logger.warning(f"Odds API returned {resp.status} for {sport_key}: {body[:300]}") + logger.warning(f"Liquipedia API returned {resp.status}") return cached[1] if cached else [] + data = await resp.json() - logger.info(f"Odds API for {sport_key}: received {len(data)} events") + html_content = data['parse']['text']['*'] + + soup = BeautifulSoup(html_content, "lxml") + match_divs = soup.find_all("div", class_="match-info") + + matches = [] + for idx, div in enumerate(match_divs): + # Tournament + tournament_div = div.find("div", class_="match-info-tournament") + tournament = tournament_div.get_text(strip=True) if tournament_div else "Unknown Tournament" + + # Teams + opponents = div.find_all("div", class_="match-info-header-opponent") + team1 = "TBD" + team2 = "TBD" + winner = None + completed = False + + if len(opponents) >= 2: + o1, o2 = opponents[0], opponents[1] + team1 = o1.get_text(strip=True) + team2 = o2.get_text(strip=True) + + c1 = o1.get("class", []) + c2 = o2.get("class", []) + + has_win_loss = any("winner" in c or "loser" in c for c in c1 + c2) + if has_win_loss: + completed = True + if any("winner" in c for c in c1): + winner = team1 + elif any("winner" in c for c in c2): + winner = team2 + else: + teams = div.find_all("span", class_="block-team") + if len(teams) >= 2: + team1 = teams[0].get_text(strip=True) + team2 = teams[1].get_text(strip=True) + + # Start time + timer_span = div.find("span", class_="timer-object") + timestamp = "" + if timer_span: + timestamp = timer_span.get("data-timestamp", "") + + if not team1 or not team2 or team1 == "TBD" or team2 == "TBD": + continue + + # ID generation + match_id = hashlib.md5(f"lp_{team1}_{team2}_{timestamp}".encode('utf-8')).hexdigest() + + # Deterministic Odds + seed = int(hashlib.md5(match_id.encode('utf-8')).hexdigest(), 16) % 10**8 + r = random.Random(seed) + odds_home = round(r.uniform(1.3, 3.2), 2) + margin = 1.08 + prob_home = 1.0 / odds_home + prob_away = margin - prob_home + if prob_away < 0.1: + prob_away = 0.1 + odds_away = round(1.0 / prob_away, 2) + + matches.append({ + "id": match_id, + "home": team1, + "away": team2, + "commence": datetime.fromtimestamp(int(timestamp)).isoformat() + "Z" if timestamp else "", + "timestamp": timestamp, + "completed": completed, + "winner": winner, + "odds": { + team1: odds_home, + team2: odds_away + }, + "tournament": tournament + }) + + _matches_cache["liquipedia_all"] = (now, matches) + logger.info(f"Scraped and cached {len(matches)} matches from Liquipedia") + return matches except Exception: - logger.exception(f"Failed to fetch odds for {sport_key}") + logger.exception("Failed to fetch matches from Liquipedia") return cached[1] if cached else [] - if not isinstance(data, list): - logger.warning(f"Odds API returned non-list for {sport_key}: {type(data)}") - return cached[1] if cached else [] - matches = [] - for event in data: - best_odds = {} - for bookmaker in event.get("bookmakers", []): - for market in bookmaker.get("markets", []): - if market.get("key") == "h2h": - for outcome in market.get("outcomes", []): - name = outcome.get("name", "") - price = outcome.get("price", 0) - if name and price and (name not in best_odds or price > best_odds[name]): - best_odds[name] = price - - if len(best_odds) >= 2: - matches.append({ - "id": event.get("id", ""), - "home": event.get("home_team", ""), - "away": event.get("away_team", ""), - "commence": event.get("commence_time", ""), - "odds": best_odds, - }) - - _matches_cache[sport_key] = (now, matches) - logger.info(f"Parsed {len(matches)} matches for {sport_key}") - return matches +async def fetch_matches(sport_key: str = "esports_dota2") -> list[dict]: + all_matches = await fetch_liquipedia_matches() + # Return only active (not completed) matches for betting + return [m for m in all_matches if not m["completed"]] def _get_match_outcomes(match: dict) -> list[dict[str, str | float]]: @@ -168,20 +203,7 @@ def _get_match_outcomes(match: dict) -> list[dict[str, str | float]]: 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 @@ -204,11 +226,6 @@ def resolve_match_outcome(match: dict, raw_choice: str) -> str | None: 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"]) @@ -248,67 +265,22 @@ def format_matches(matches: list[dict], sport_alias: str, sport_label: str) -> s lines.append(f"{i}️⃣ {m['home']} vs {m['away']}{start_str}\n{odds_str}\n") - lines.append(f"Ставка: /bet {sport_alias} [номер] [исход] [ставка]") - lines.append(f"Коротко после /matches {sport_alias}: /bet [номер] [исход] [ставка]") - lines.append("Исход: 1/2, для ничьи X. Можно писать ещё название команды или коэффициент.") + lines.append("Ставка: /bet [номер] [исход] [ставка]") + lines.append("Исход: 1/2. Можно писать ещё название команды или коэффициент.") return "\n".join(lines) def _get_sport_label(key: str) -> str: - labels = { - "epl": "⚽ EPL", - "ucl": "⚽ Champions League", - "laliga": "⚽ La Liga", - "bundesliga": "⚽ Bundesliga", - "seriea": "⚽ Serie A", - "ligue1": "⚽ Ligue 1", - "europa": "⚽ Europa League", - } - return labels.get(key, key) + return "🎮 Dota 2" async def get_formatted_matches(sport_alias: str | None) -> str: - if not config.ODDS_API_KEY: - return "⚠️ ODDS_API_KEY не задан. Ставки недоступны." - - if sport_alias and sport_alias in config.ODDS_SPORTS: - sport_key = config.ODDS_SPORTS[sport_alias] - matches = await fetch_matches(sport_key) - if not matches: - sports = await _fetch_available_sports() - valid_keys = {s.get("key") for s in sports} - if sport_key not in valid_keys: - suggestions = [s for s in sports if sport_alias in s.get("key", "").lower() - or sport_alias in s.get("title", "").lower()] - if suggestions: - suggest_text = "\n".join( - f" • {s['key']} — {s.get('title', '')}" - for s in suggestions[:5] - ) - return ( - f"⚠️ Ключ {sport_key} не найден в API.\n" - f"Похожие виды спорта:\n{suggest_text}\n\n" - f"Обнови sport keys в config.py" - ) - return f"⚠️ Ключ {sport_key} не найден в API. Проверь ODDS_SPORTS в config.py" - 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, alias, _get_sport_label(alias))) - - result = "\n\n".join(parts) - if all("матчей не найдено" in p for p in parts): - result += "\n\n💡 Если матчей нет — возможно, sport keys устарели. Проверь /sports_debug" - return result + matches = await fetch_matches() + return format_matches(matches, "dota2", "🎮 Dota 2") 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: - return None - matches = await fetch_matches(sport_key) + matches = await fetch_matches() if 0 < index <= len(matches): return matches[index - 1] return None @@ -495,7 +467,7 @@ async def settle_bets() -> list[str]: try: with get_conn() as conn: cur = conn.cursor(cursor_factory=psycopg2.extras.RealDictCursor) - cur.execute("SELECT DISTINCT match_id, sport FROM bets WHERE status = 'pending'") + cur.execute("SELECT DISTINCT match_id FROM bets WHERE status = 'pending'") pending = cur.fetchall() except psycopg2.Error: return notifications @@ -503,118 +475,60 @@ async def settle_bets() -> list[str]: if not pending: return notifications + all_matches = await fetch_liquipedia_matches() + match_map = {m["id"]: m for m in all_matches} now_ts = int(_time.time()) for row in pending: match_id = row["match_id"] - sport = row["sport"] - sport_key = config.ODDS_SPORTS.get(sport, sport) - - try: - url = f"{config.ODDS_API_BASE}/sports/{sport_key}/scores" - params = {"apiKey": config.ODDS_API_KEY, "daysFrom": 3} - async with aiohttp.ClientSession() as session: - async with session.get(url, params=params, timeout=aiohttp.ClientTimeout(total=15)) as resp: - if resp.status != 200: - logger.warning(f"Scores API returned {resp.status} for {sport_key}") - continue - scores_data = await resp.json() - except Exception: - logger.exception(f"Failed to fetch scores for {sport_key}") + event = match_map.get(match_id) + if not event or not event.get("completed"): continue - for event in scores_data: - if event.get("id") != match_id or not event.get("completed"): - continue + winner = event.get("winner") + if not winner: + continue - scores = event.get("scores") - if not scores: - continue + try: + with get_conn() as conn: + cur = conn.cursor(cursor_factory=psycopg2.extras.RealDictCursor) + cur.execute( + "SELECT * FROM bets WHERE match_id = %s AND status = 'pending'", + (match_id,), + ) + bets = cur.fetchall() - score_values = [(s.get("name", ""), int(s.get("score", 0))) for s in scores] - winner = None - is_draw = False + wcur = conn.cursor() + for bet in bets: + chosen = bet["chosen_team"] + bet_won = (chosen == winner) - if len(score_values) >= 2 and score_values[0][1] == score_values[1][1]: - is_draw = True - winner = "Draw" - else: - max_score = -1 - for name, score_val in score_values: - if score_val > max_score: - max_score = score_val - winner = name - - if not winner: - continue - - try: - with get_conn() as conn: - cur = conn.cursor(cursor_factory=psycopg2.extras.RealDictCursor) - cur.execute( - "SELECT * FROM bets WHERE match_id = %s AND status = 'pending'", - (match_id,), - ) - bets = cur.fetchall() - - wcur = conn.cursor() - for bet in bets: - chosen = bet["chosen_team"] - if is_draw: - bet_won = chosen.casefold() in _DRAW_NAMES or chosen == "Draw" - else: - bet_won = chosen == winner - - if bet_won: - winnings = round(bet["amount"] * bet["odds"], 1) - update_user_length(bet["user_id"], winnings) - wcur.execute( - "UPDATE bets SET status = 'won', payout = %s, resolved_ts = %s WHERE id = %s", - (winnings, now_ts, bet["id"]), - ) - notifications.append( - f"🎉 user_id={bet['user_id']}: выиграл {winnings:.1f} см " - f"({bet['home_team']} vs {bet['away_team']}, {bet['chosen_team']})" - ) - else: - wcur.execute( - "UPDATE bets SET status = 'lost', payout = 0, resolved_ts = %s WHERE id = %s", - (now_ts, bet["id"]), - ) - notifications.append( - f"❌ user_id={bet['user_id']}: проиграл {bet['amount']:.1f} см " - f"({bet['home_team']} vs {bet['away_team']}, {bet['chosen_team']})" - ) - except psycopg2.Error: - logger.exception("Failed to settle bets") + if bet_won: + winnings = round(bet["amount"] * bet["odds"], 1) + update_user_length(bet["user_id"], winnings) + wcur.execute( + "UPDATE bets SET status = 'won', payout = %s, resolved_ts = %s WHERE id = %s", + (winnings, now_ts, bet["id"]), + ) + notifications.append( + f"🎉 user_id={bet['user_id']}: выиграл {winnings:.1f} см " + f"({bet['home_team']} vs {bet['away_team']}, {bet['chosen_team']})" + ) + else: + wcur.execute( + "UPDATE bets SET status = 'lost', payout = 0, resolved_ts = %s WHERE id = %s", + (now_ts, bet["id"]), + ) + notifications.append( + f"❌ user_id={bet['user_id']}: проиграл {bet['amount']:.1f} см " + f"({bet['home_team']} vs {bet['away_team']}, {bet['chosen_team']})" + ) + except psycopg2.Error: + logger.exception("Failed to settle bets") cleanup_old_bets() return notifications async def debug_sports() -> str: - if not config.ODDS_API_KEY: - return "⚠️ ODDS_API_KEY не задан." - - sports = await _fetch_available_sports() - if not sports: - return "❌ Не удалось получить список спортов от API." - - current_keys = set(config.ODDS_SPORTS.values()) - valid_keys = {s.get("key") for s in sports} - - lines = ["🔍 Диагностика sport keys:\n"] - for alias, key in config.ODDS_SPORTS.items(): - status = "✅" if key in valid_keys else "❌ НЕ НАЙДЕН" - lines.append(f" {alias} → {key} {status}") - - lines.append("\n📋 Доступные eSports и футбол:") - for s in sports: - key = s.get("key", "") - title = s.get("title", "") - active = s.get("active", False) - if active and ("esport" in key.lower() or "soccer" in key.lower() or "football" in key.lower()): - marker = " ← ИСПОЛЬЗУЕТСЯ" if key in current_keys else "" - lines.append(f" • {key} — {title}{marker}") - - return "\n".join(lines) + return "🎮 Dota 2 — Scraper Liquipedia активен. Odds API отключен." diff --git a/games/uwu.py b/games/uwu.py index e5ff65e..4def6c1 100644 --- a/games/uwu.py +++ b/games/uwu.py @@ -180,7 +180,7 @@ async def handle_uwu_cmd(message: types.Message): file_url = post["url"] ext = post["ext"] caption = post["caption"] - spoiler = post.get("rating") == "e" # спойлер для explicit + spoiler = post.get("rating") in ("e", "q") # спойлер для explicit и questionable if ext in ("gif", "webm", "mp4"): # Для анимаций: отправляем caption отдельным сообщением, diff --git a/main.py b/main.py index 7e20f49..9bf2f2b 100644 --- a/main.py +++ b/main.py @@ -1461,37 +1461,17 @@ async def handle_gadanie_cmd(message: Message): async def handle_matches_cmd(message: Message): raw_text = message.text or "" parts = raw_text.split(maxsplit=1) - sport_alias = parts[1].strip().lower() if len(parts) > 1 else None - - # Если лига не указана — показать список - if not sport_alias: - labels = { - "epl": "⚽ EPL (Англия)", - "ucl": "⚽ Champions League", - "laliga": "⚽ La Liga (Испания)", - "bundesliga": "⚽ Bundesliga (Германия)", - "seriea": "⚽ Serie A (Италия)", - "ligue1": "⚽ Ligue 1 (Франция)", - "europa": "⚽ Europa League", - } - lines = ["🏆 Доступные лиги для ставок:\n"] - for alias, name in labels.items(): - lines.append(f" {name} → /matches {alias}") - lines.append("\nПример: /matches epl") - await message.reply("\n".join(lines), parse_mode=None) - return + sport_alias = parts[1].strip().lower() if len(parts) > 1 else "dota2" if sport_alias not in config.ODDS_SPORTS: if message.from_user: clear_user_sport_context(message.from_user.id) - available = " / ".join(config.ODDS_SPORTS.keys()) - await message.reply(f"Неизвестная лига. Доступно: {available}", parse_mode=None) + await message.reply("Неизвестная лига. Доступно: dota2", parse_mode=None) return + if message.from_user: - if 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) + remember_user_sport_context(message.from_user.id, sport_alias) + 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) @@ -1504,41 +1484,34 @@ async def handle_bet_cmd(message: Message): parts = raw_text.split() if len(parts) < 4: await message.reply( - "Формат: /bet [спорт] [номер] [исход] [ставка]\n" - "Коротко после /matches cs|dota|football: /bet [номер] [исход] [ставка]\n" - "Исход: 1/2, для ничьи X, можно ещё название команды или коэффициент.\n" - "Пример: /bet cs 1 1 0.5", + "Формат: /bet [номер] [исход] [ставка]\n" + "Или: /bet dota2 [номер] [исход] [ставка]\n" + "Исход: 1/2. Можно писать ещё название команды или коэффициент.\n" + "Пример: /bet 1 1 0.5", parse_mode=None, ) return - sport_alias: str | None - if parts[1].lower() in config.ODDS_SPORTS: + sport_alias: str + if parts[1].lower() == "dota2": if len(parts) < 5: await message.reply( - "Формат: /bet [спорт] [номер] [исход] [ставка]\nПример: /bet cs 1 1 0.5", + "Формат: /bet dota2 [номер] [исход] [ставка]\nПример: /bet dota2 1 1 0.5", parse_mode=None, ) return - sport_alias = parts[1].lower() + sport_alias = "dota2" index_token = parts[2] selection = " ".join(parts[3:-1]).strip() amount_token = parts[-1] else: - sport_alias = get_user_sport_context(message.from_user.id) - if not sport_alias: - await message.reply( - "Сначала открой /matches с нужной лигой (epl, ucl, laliga...).\n" - "Либо пиши полный формат: /bet [спорт] [номер] [исход] [ставка]", - parse_mode=None, - ) - return + sport_alias = "dota2" 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) + await message.reply("Нужен исход: 1, 2, название команды или коэффициент.", parse_mode=None) return try: diff --git a/webapp/api.py b/webapp/api.py index c85a18a..ccde165 100644 --- a/webapp/api.py +++ b/webapp/api.py @@ -174,13 +174,7 @@ async def get_profile(user: dict = Depends(get_current_user)) -> ProfileResponse async def get_leagues() -> list[LeagueInfo]: """Список доступных лиг для ставок.""" labels = { - "epl": "⚽ EPL (Англия)", - "ucl": "⚽ Champions League", - "laliga": "⚽ La Liga (Испания)", - "bundesliga": "⚽ Bundesliga (Германия)", - "seriea": "⚽ Serie A (Италия)", - "ligue1": "⚽ Ligue 1 (Франция)", - "europa": "⚽ Europa League", + "dota2": "🎮 Dota 2", } return [ LeagueInfo(alias=alias, name=labels.get(alias, alias), sport_key=key) diff --git a/webapp/frontend/js/app.js b/webapp/frontend/js/app.js index 3b7fd08..1b26b73 100644 --- a/webapp/frontend/js/app.js +++ b/webapp/frontend/js/app.js @@ -295,13 +295,17 @@ async function showBetHistory() { async function renderBetting(data) { if (data?.league) { - return renderMatches(data.league, data.leagueName); + return renderMatches(data.league, data.leagueName, data.isOnlyLeague); } container.innerHTML = `
⚽ Ставки на матчи
${skeleton(2)}`; try { const leagues = await API.getLeagues(); + if (leagues.length === 1) { + return renderMatches(leagues[0].alias, leagues[0].name, true); + } + container.innerHTML = `
⚽ Выбери лигу
@@ -317,7 +321,7 @@ async function renderBetting(data) { `); item.addEventListener('click', () => { haptic(); - navigate('betting', { league: l.alias, leagueName: l.name }); + navigate('betting', { league: l.alias, leagueName: l.name, isOnlyLeague: false }); }); list.appendChild(item); }); @@ -326,15 +330,22 @@ async function renderBetting(data) { } } -async function renderMatches(league, leagueName) { +async function renderMatches(league, leagueName, isOnlyLeague = false) { + const headerText = isOnlyLeague ? '🎮 Dota 2 Матчи' : leagueName; + const backTarget = isOnlyLeague ? 'home' : 'betting'; + const instructionsHtml = isOnlyLeague + ? `
Ставки на актуальные матчи Dota 2. Выберите исход и укажите сумму ставки в сантиметрах.
` + : ''; + container.innerHTML = `
-
${leagueName}
+
${headerText}
+ ${instructionsHtml} ${skeleton(3)} `; - document.getElementById('back-btn').onclick = () => navigate('betting'); + document.getElementById('back-btn').onclick = () => navigate(backTarget); try { const matches = await API.getMatches(league); @@ -361,10 +372,12 @@ async function renderMatches(league, leagueName) { П1 ${m.odds_home?.toFixed(2) || '—'} + ${m.odds_draw ? ` + ` : ''}