# ♟️ 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
