# Live-цикл и реалтайм

Источник: https://docs.novapoker.ru/dev/live/
Дорожка: разработка
Сверено с кодом: Mon Aug 24
Код: repos/poker-api/apps/server/src/live, repos/poker-api/apps/server/src/live/seating.ts, repos/poker-api/apps/server/src/live/balance.ts

---

Модуль `live` в `poker-api`. Все операции ведут `owner` и `td`. Особенность
раздела в том, что почти каждая операция защищена от конкурентного нажатия —
пульт стоит в зале, и по нему бьют пальцем, а не курсором.

## Старт

Требует минимум двух `checked_in`, иначе `409 NOT_ENOUGH_PLAYERS`. В одной
операции:

- оставшиеся `registered` → `no_show` плюс страйк;
- рассадка;
- `chipsInPlay = startingStack × число вошедших`;
- часы с нулевого отрезка.

Переход в `running` — **атомарный guarded-update**; повторный старт получает
`409 TOURNAMENT_ALREADY_STARTED`.

## Рассадка

`src/live/seating.ts`, `SEATS_PER_TABLE = 9`. Число столов — `⌈игроков / 9⌉`,
перемешивание Фишера–Йейтса с **инъектируемым `rng`** (поэтому рассадка
тестируема), раздача по кругу:

```
tableNo = (i mod столов) + 1
seatNo  = ⌊i / столов⌋ + 1
```

Отсюда разница размеров столов не больше одного и места подряд с первого.

## Часы

`pause`, `resume`, `level` (`next`/`prev`). Все три — **compare-and-swap**:
конкурентные нажатия не задваивают аудит, не шлют лишние broadcast'ы и не теряют
уровень. Смена уровня на паузе начинает новый отрезок «в момент паузы».

Источник истины — серверные поля `Tournament`: `currentLevelIdx`,
`levelStartedAt`, `pausedAt`. Клиент считает остаток сам, но от этих значений.

## Вылеты

`POST /eliminations` гасит место (`TableSeat.active = false`) и штампует
`eliminatedAt` **строго возрастающими** метками.

**Место = число активных игроков до вылета**: первый вылетевший из N получает
место N.

Рядом три операции:

| Операция | Что делает |
|---|---|
| `undo` | отменяет последний вылет; если исходное место занято, сажает на первое свободное |
| `reorder` | перераспределяет **уже снятые** метки времени между игроками, новых не создаёт |
| `reseat` | ручная пересадка |

`reorder` именно перераспределяет, а не переписывает номера: `(tournamentId, place)`
в `Result` уникальна, и набор мест обязан остаться прежним.

## Балансировка

`src/live/balance.ts`. Подсказка считается после каждого вылета и **приходит в
ответе на вылет**, а не отдельным запросом:

- разница между самым большим и самым маленьким живым столом ≥ 2 → одна
  пересадка: игрок с максимальным `seatNo` большого стола на первое свободное
  место маленького (при равенстве размеров выигрывает стол с меньшим номером);
- активных ≤ 9 → `finalTable`, пакет пересадок на самый большой стол;
- иначе `null`.

Под капотом кнопки «Пересадить» — те же вызовы `reseat`. Сервер сам никого не
двигает.

## Подтверждение результатов

Разрешено только для `running` и когда активных игроков осталось не больше одного.

`N` (размер поля) = число чек-инувшихся; **неявки в поле не входят**. Порядок мест:
не выбывший — место 1, дальше по убыванию времени вылета, тай-брейки по
`createdAt` и `id`.

Гейт атомарный: конкурентный второй вызов получает
`409 RESULTS_ALREADY_CONFIRMED` **до** начисления, а не после.

## Пересчёт

`POST /admin/tournaments/{id}/recount`, только для `finished`. Удаляет и
пересоздаёт `Result`, **сохраняя исходные `createdAt`**, учитывает уже
заклеймленных гостей и пересобирает профили всех задетых игроков по хронологии.

Сохранение `createdAt` здесь не косметика: сила считается экспоненциальным
сглаживанием, то есть зависит от порядка турниров. Пересчёт не последнего турнира
без этого сдвинул бы его в конец истории и дал бы дрейф силы.

## LiveState

Что получают клиенты: статус, часы (индекс, старт отрезка, пауза, полный список
уровней), `remaining`, `entrants`, `chipsInPlay`, `avgStack`, столы с местами и
именами, список вылетов с местами.

**Телефоны наружу не отдаются.**

Подписка — namespace `/live` через socket.io, полный `LiveState` рассылается в
комнату турнира после каждого изменения. Handshake требует JWT.

## Что рядом

- [Старт, рассадка и баланс](https://docs.novapoker.ru/product/start-and-seating.md) — то же словами продукта.
- [Рейтинг: пакет формул](https://docs.novapoker.ru/dev/rating.md) — что считается при подтверждении.
- [Результаты и аудит](https://docs.novapoker.ru/dev/results.md) — что пишется на финише.
