# ♟️ chessCalc — калькулятор швейцарской жеребьёвки + авто-отслеживание игроков Расчёт пар следующего тура шахматного турнира **до официальной публикации** на chess-results.com. Плюс подписка на игрока по FIDE ID: бот сам находит турниры, отслеживает результаты и присылает жеребьёвку с подсветкой игрока. Два Telegram-бота: клиентский и админский. --- ## Возможности ### Клиентский бот | Команда | Описание | |---------|----------| | `/addplayer` | Подписаться на игрока по FIDE ID — бот сам найдёт турнир на chess-results | | `/myplayers` | Список активных подписок | | `/removeplayer ` | Удалить подписку | | `/cancel` | Отменить текущую операцию | | `URL турнира` | Прислать ссылку chess-results.com — получить жеребьёвку следующего тура | **Авто-отслеживание:** - Каждые **5 минут** — проверка новых результатов подписанных игроков - При завершении тура — расчёт и отправка жеребьёвки следующего тура (±5 пар вокруг игрока, `✅` зелёная галочка) - Результат каждого тура: `♟ победа/ничья/поражение` ### Админский бот - `/stat` — уникальные пользователи, всего запросов, запросов за сегодня --- ## Как запустить ```bash # 1. Токены cp .env.example .env # отредактировать токены # 2. Запуск docker compose up -d ``` ### CLI (отладка) ```bash # Пары следующего тура docker compose run --rm client-bot python3 -m swiss_calc 'URL' # JSON docker compose run --rm client-bot python3 -m swiss_calc --json 'URL' # Положение docker compose run --rm client-bot python3 -m swiss_calc --standings 'URL' ``` --- ## Архитектура ``` chessCalc/ ├── Dockerfile # python:3.11-slim + bbpPairings ├── docker-compose.yml # client-bot + admin-bot + volume bot-data ├── .env.example # шаблон токенов ├── .env # реальные токены (gitignored) ├── bbpPairings.exe # FIDE-движок Dutch System (C++, 2.4 MB) ├── requirements.txt # requests, bs4, python-telegram-bot, rich ├── swiss_calc/ # Ядро — парсинг + расчёт │ ├── __main__.py # CLI entry point │ ├── parser.py # Парсинг chess-results.com (art=2/4/5) │ ├── trf_generator.py # Генерация TRF-16 для bbpPairings │ ├── bbp_wrapper.py # Вызов bbpPairings.exe через subprocess │ ├── swiss.py # Оркестрация + fallback Swiss-алгоритм │ └── display.py # Форматирование вывода (CLI) ├── bots/ # Telegram-боты │ ├── client_bot.py # Клиентский бот │ ├── admin_bot.py # Админский бот (/stat) │ ├── tracker.py # Отслеживание игроков, кэш турниров, джобы │ └── stats.py # Статистика (SQLite) └── TASKS.md # Журнал изменений ``` ### Поток данных — жеребьёвка ``` URL турнира │ ▼ ┌─────────────────────────────────────────┐ │ parser.fetch_tournament() │ │ 1. art=4 — положение │ │ 2. art=5 — стартовый список │ │ 3. Сопоставление имён → SNo │ │ 4. art=2&rd=1..N — пары каждого тура │ └─────────────────────────────────────────┘ │ tournament_data ▼ ┌─────────────────────────────────────────┐ │ swiss.calculate_next_round() │ │ 1. Генерация TRF-16 │ │ 2. bbpPairings --dutch (FIDE 2025) │ │ └→ fallback: упрощённый Swiss │ └─────────────────────────────────────────┘ │ пары (доски) ▼ ┌─────────────────────────────────────────┐ │ format_pairings() → _render_chunks() │ │ MarkdownV2 таблица в code-блоке │ └─────────────────────────────────────────┘ ``` ### Поток данных — подписка на игрока ``` FIDE ID (напр. 55867510) │ ▼ ┌─────────────────────────────────────────┐ │ tracker.fetch_fide_player() │ │ Парсинг ratings.fide.com/profile/{ID} │ │ → имя, рейтинг, FED, каноничный FIDE ID│ └─────────────────────────────────────────┘ │ player dict ▼ ┌─────────────────────────────────────────┐ │ tracker.scan_for_player() │ │ SQL-запрос к tnr_cache: │ │ SELECT tnr FROM tnr_cache │ │ WHERE players_json LIKE '%{fide_id}%' │ │ → список TNR с этим игроком │ └─────────────────────────────────────────┘ │ tournaments list ▼ ┌─────────────────────────────────────────┐ │ Авто-подписка + уведомление │ │ → запись в subscriptions │ │ → сообщение пользователю │ └─────────────────────────────────────────┘ ``` --- ## Локальный кэш турниров (`tnr_cache`) Таблица SQLite с полным списком игроков каждого турнира: ``` tnr_cache ├── tnr INTEGER PRIMARY KEY — номер турнира на chess-results ├── name TEXT — название турнира ├── start_date TEXT — дата начала ├── end_date TEXT — дата окончания └── players_json TEXT — JSON: {fide_id: {sno, name, rating, fed}} ``` - При первом фетче art=0 страницы турнира данные сохраняются в кэш - **1979+ турниров** в кэше (1.4M кластер, 2025-2026) - Поиск игрока — **один SQL LIKE-запрос, ~0.015 секунды**, без HTTP ### Прогрев кэша (`warmup_cache`) - При старте: полный скан кластера 1.4M с шагом 1 (~50 минут) - Сохраняет прогресс в `tnr_state` — при перезапуске продолжает - После завершения (`warmup_tnr = done`) — пропускает, если кэш > 1000 записей - Каждые 6 часов — перепроверка кластера --- ## Фоновые задачи | Задача | Интервал | Что делает | |--------|----------|------------| | `check_all_subscriptions` | **5 мин** | Проверяет новые результаты, считает и отправляет жеребьёвку | | `rescan_new_tournaments` | **1 час** | Ищет новые TNR > `max_tnr_seen`, авто-подписывает | | `rescan_existing_tournaments` | **2 часа** | Рефетчит art=0 для всех TNR (мимо кэша), находит новых игроков | | `warmup_cache` | **6 часов** | Обновляет tnr_cache (если нужен) | --- ## База данных (SQLite) Таблицы в `/app/data/tournaments.db` (Docker volume `bot-data`): | Таблица | Назначение | |---------|------------| | `subscriptions` | Активные подписки: user_id, fide_id, player_name, tournament_url, ... | | `tnr_cache` | Кэш турниров: tnr, name, players_json | | `tnr_state` | Состояние: max_tnr_seen, warmup_tnr, warmup progress | | `users` / `requests` | Статистика использования (админский бот) | Авто-миграция схемы: недостающие колонки добавляются при каждом `_get_conn()`. --- ## Источники данных | Источник | URL | Что даёт | |----------|-----|----------| | FIDE профиль | `ratings.fide.com/profile/{ID}` | Имя, рейтинг, федерация, каноничный FIDE ID | | chess-results art=0 | `chess-results.com/tnr{N}.aspx?lan=11&art=0` | Список всех игроков турнира с FIDE ID | | chess-results art=4 | `…&art=4` | Положение (имена, очки, тайбрейки) | | chess-results art=5 | `…&art=5` | Стартовый список (SNo, рейтинги) | | chess-results art=2 | `…&art=2&rd=N` | Пары тура N | Парсер поддерживает 12-колоночный (новые турниры) и 10-колоночный (старые) форматы. --- ## Движок жеребьёвки **Основной:** [bbpPairings](https://github.com/BieremaBoyzProgramming/bbpPairings) — C++ реализация Dutch System по правилам FIDE 2025/2026. **Резервный:** упрощённый швейцарский алгоритм на Python (fold + перебор offset). --- ## Точность жеребьёвки Проверено на турнирах Первенства России (2026): | Турнир | Уч. | Точность | |--------|-----|----------| | 1393121 | 39 | **100%** | | 1393124 | 93 | **100%** | | 1393133 | 96 | **100%** | | 1393137 | 14 | **100%** | | 1393131 | 112 | **85%** * | \* Расхождения только в нижних досках из-за ручных bye/forfeit-ов. --- ## Ограничения 1. **Ручные bye/forfeit-ы** — решение арбитра, алгоритм не предсказывает 2. **10-колоночный формат** — старые турниры без SNo, точность ~80% 3. **Круговые турниры** — не поддерживаются 4. **TNR-пространство не сплошное** — кластеры с пропусками, требуется кэш 5. **Латиница/кириллица** — имена на chess-results могут отличаться от FIDE, матчинг по SNo