ChessCalcNextTour/README.md
vrubel 282c651821
All checks were successful
CI/CD Pipeline / build-and-deploy (push) Successful in 3s
docs: comprehensive README with player tracking, cache, and jobs
2026-06-20 22:14:38 +00:00

240 lines
12 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# ♟️ chessCalc — калькулятор швейцарской жеребьёвки + авто-отслеживание игроков
Расчёт пар следующего тура шахматного турнира **до официальной публикации**
на chess-results.com. Плюс подписка на игрока по FIDE ID: бот сам находит
турниры, отслеживает результаты и присылает жеребьёвку с подсветкой игрока.
Два Telegram-бота: клиентский и админский.
---
## Возможности
### Клиентский бот
| Команда | Описание |
|---------|----------|
| `/addplayer` | Подписаться на игрока по FIDE ID — бот сам найдёт турнир на chess-results |
| `/myplayers` | Список активных подписок |
| `/removeplayer <N>` | Удалить подписку |
| `/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