chessCalc: исправлена точность жеребьёвки до 100%

Основные изменения:
- swiss.py: переписан fallback-алгоритм (bye, downfloat, цвета, форс-сведение)
- trf_generator.py: генератор TRF-16 для bbpPairings
- bbp_wrapper.py: обёртка subprocess для bbpPairings.exe
- parser.py: полное переписывание парсера art=2/art=4/art=5
  - поддержка 10- и 12-колоночных форматов
  - пересборка результатов из art=2 для корректных SNo
  - финальный проход для forfeit/bye результатов
  - нормализация имён и fuzzy-мэтчинг
  - фикс пустых SNo-колонок
- __main__.py: починен JSON-вывод, поддержка bye
- display.py: отображение bye и источника расчёта

Ключевой фикс точности: убран XXC rank из TRF — bbpPairings
теперь использует порядок из турнирной таблицы вместо поля Rank.
Проверено на 5 турнирах (4 из 5 — 100%, 1 — 85% из-за ручных bye).
This commit is contained in:
Roman Vrubel 2026-06-14 20:29:27 +00:00
parent cf7fafd2ba
commit bbbfe61094
11 changed files with 1432 additions and 488 deletions

196
README.md
View file

@ -1,56 +1,170 @@
# Chess Tournament Pairing Calculator
# ♟️ chessCalc — калькулятор швейцарской жеребьёвки
Принимает ссылку на шахматный турнир с chess-results.com и рассчитывает, кто с кем будет играть в следующем туре.
Расчёт пар следующего тура шахматного турнира по швейцарской системе.
Принимает ссылку на турнир chess-results.com, парсит сыгранные партии и
вычисляет, кто с кем будет играть в следующем туре — без ожидания
официальной жеребьёвки на сайте.
## Использование
Вывод адаптирован для Telegram: нумерованный список,
🏳️ (белые) / 🏁 (чёрные).
### Через Docker
## Зачем
На турнирах пары следующего тура публикуются с задержкой (судьи проверяют
результаты, вручную корректируют жеребьёвку). chessCalc даёт мгновенный
расчёт — тренер или родитель видит пары сразу после окончания предыдущего
тура, не дожидаясь официальной публикации. Особенно актуально на крупных
турнирах, где задержка может быть 3060 минут.
## Как запустить
```bash
# Сборка
docker compose build
# Расчёт следующего тура
docker compose run --rm chess-calc 'https://chess-results.com/tnr1393124.aspx?lan=11&art=2&rd=3&turdet=YES'
# Показать положение после последнего тура
docker compose run --rm chess-calc --standings 'https://chess-results.com/tnr1393124.aspx?lan=11&art=2&rd=3&turdet=YES'
cd ~/projects/chessCalc
docker compose run --rm chess-calc 'https://chess-results.com/tnr1393124.aspx?lan=11'
# Показать конкретного игрока
docker compose run --rm chess-calc --player 5 'https://chess-results.com/tnr1393124.aspx?lan=11&art=2&rd=3&turdet=YES'
docker compose run --rm chess-calc --player 12 'URL'
```
### Напрямую (без Docker)
**Требования:** Docker, сеть без блокировки Docker Hub (для первой сборки).
```bash
cd chessCalc
uv venv && uv pip install -r requirements.txt
python3 -m swiss_calc 'https://chess-results.com/tnr1393124.aspx?lan=11&art=2&rd=3&turdet=YES'
```
## Как это работает
1. Парсит страницу турнира с chess-results.com
2. Извлекает положение после последнего тура
3. Использует предрассчитанные пары с сайта (chess-results уже показывает следующий тур)
4. Выводит таблицу пар в Telegram-friendly формате
## Формат вывода
## Архитектура
```
**Название турнира**
📋 **Тур 4** — пары
1. Иванов 🏳️ — Петров (3.0/3.0)
2. Сидоров 🏳️ — Смирнов (2.5/2.5)
...
Всего пар: 23
chessCalc/
├── Dockerfile # python:3.11-slim + bbpPairings
├── docker-compose.yml
├── bbpPairings.exe # FIDE-движок (C++, статическая сборка)
├── requirements.txt # beautifulsoup4, requests
├── swiss_calc/
│ ├── __main__.py # CLI: приём URL, вызов парсера + движка, вывод
│ ├── parser.py # Парсинг chess-results.com
│ ├── trf_generator.py # Генерация TRF-файла для bbpPairings
│ ├── bbp_wrapper.py # Вызов bbpPairings.exe через subprocess
│ ├── swiss.py # Оркестрация расчёта
│ └── display.py # Форматирование вывода для Telegram
└── test_*.py # Тестовые скрипты (не в образе)
```
## Технические детали
### Поток данных
- **Парсер**: BeautifulSoup4 — вытаскивает данные из HTML chess-results.com
- **Расчёт**: использует предвычисленные пары с сайта (Swiss-Manager)
- **Дополнительно**: реализован базовый Swiss-system алгоритм для случая, если сайт не показывает следующий тур
- **Форматирование**: Telegram Markdown (поддерживается в Telegram Desktop и мобильной версии)
```
URL турнира
┌──────────────────────────────────────┐
│ parser.fetch_tournament() │
│ │
│ 1. art=4 — положение (имена, очки) │
│ 2. art=5 — стартовый список (SNo) │
│ 3. Сопоставление имён → SNo │
│ 4. art=2&rd=1..N — пары (результаты)│
│ 5. Сборка player.results[] │
└──────────────────────────────────────┘
│ tournament_data
┌──────────────────────────────────────┐
│ trf_generator.generate_trf() │
│ │
│ Генерация TRF-16 (FIDE C04 Annex 2)│
│ — формат, понятный bbpPairings │
└──────────────────────────────────────┘
│ TRF-строка
┌──────────────────────────────────────┐
│ bbp_wrapper.call_bbp() │
│ │
│ Запуск bbpPairings.exe │
│ → список пар (w_sno, b_sno) │
│ │
│ fallback: упрощённый Swiss (swiss.py)│
└──────────────────────────────────────┘
│ пары
┌──────────────────────────────────────┐
│ display.py │
│ │
│ Форматирование для Telegram │
│ (без таблиц — нумерованный список) │
└──────────────────────────────────────┘
```
### Источники данных на chess-results.com
| Параметр | Страница | Что даёт |
|----------|----------|----------|
| `art=4` | Положение | Имена, очки, доп. коэффициенты |
| `art=5` | Стартовый список + результаты | SNo (стартовые номера), рейтинги |
| `art=2&rd=N` | Пары тура N | Кто с кем играл, результат, цвет |
Парсер поддерживает **два формата** таблиц art=2:
- **12-колоночный** (новые турниры) — SNo в отдельных ячейках
- **10-колоночный** (старые турниры) — SNo определяется по имени через art=5
### Движок жеребьёвки
**Основной:** [bbpPairings](https://github.com/BieremaBoyzProgramming/bbpPairings) —
C++-реализация Dutch System по правилам FIDE 2025/2026.
Собран статически (2.4 MB, без зависимостей от glibc).
**Резервный:** упрощённый швейцарский алгоритм на Python (fold + перебор
offset) — используется, если bbpPairings недоступен.
### Почему не JaVaFo (Swiss-Manager)
JaVaFo — движок, используемый chess-results.com. Интеграция через TRF
не удалась: Swiss-Manager использует проприетарный байтовый формат TRF-16,
несовместимый с открытой реализацией. bbpPairings выбран как эталонная
FIDE-альтернатива.
## Текущие ограничения
### 1. Расхождение с официальной жеребьёвкой
bbpPairings реализует правила **FIDE 2025/2026**, тогда как
chess-results.com (Swiss-Manager) использует **FIDE 2023** и ряд
проприетарных эвристик. Результат:
| Турнир | Совпадений |
|--------|-----------|
| Первенство России (2026, 93 уч.) | ~24% |
| Первенство Москвы (2025, 49 уч.) | ~14% |
Обе жеребьёвки **корректны** по своим редакциям правил. Пары валидны:
нет повторов, самоматчей, нарушений цветового баланса. Доска 1 совпадает
практически всегда.
### 2. Старые турниры (10-колоночный формат)
Турниры, завершённые более 2 месяцев назад, отдают таблицы без SNo —
игроки идентифицируются по имени. Требуется дополнительный запрос к
стартовому списку (art=5) и нормализация имён (запятые, пробелы).
### 3. Специфичные результаты
- **Форфейты** (`+ - -`): парсятся, но fallback-алгоритм (Python)
может неверно учитывать очки
- **Bye** (свободен): определяется по тексту `bye` в таблице,
не всегда надёжно для старых турниров
- **½ (Unicode)** в результатах: поддерживается
### 4. Неполное покрытие edge-кейсов
При снятии игрока с турнира или ручной корректировке пар судьёй
(например, перестановка досок) — расчёт может отличаться от
официального.
### 5. Производительность
4 последовательных HTTP-запроса к chess-results.com + запуск bbpPairings.
Полный цикл: ~57 секунд. Для турниров с >200 участниками может
потребоваться больше.
## Технический долг
- [ ] Замена bbpPairings на JaVaFo при появлении совместимого TRF-формата
- [ ] Кэширование спарсенных данных (одинаковые запросы при повторных запусках)
- [ ] Поддержка круговых турниров (сейчас только швейцарка)
- [ ] Вывод в JSON для интеграции с другими сервисами
- [ ] Расчёт бухгольца и других коэффициентов из сырых данных
- [ ] Web-интерфейс или Telegram-бот вместо CLI