# Реплеи — `$.replay`

Запись ввода по кадрам и воспроизведение. Детерминизм в движке уже есть
(`--seed`, `--fixed-dt`, свои генераторы из `$.random`), но записать и проиграть
сессию было нечем. Реплей превращает баг-репорт в одну строку данных, а
регресс-тест — в «проиграй запись и проверь, что мир пришёл туда же».

Требования к записи на уровне движка — CLI `--record`/`--replay`, версия
формата, чекпоинты, версия движка в заголовке — описаны в
[RECORD_REPLAY.md](RECORD_REPLAY); здесь — текущее API `$.replay`.

```js
// Ввод кадра собирает ИГРА: готового «сними весь ввод» в $.input нет.
const sampler = () => ({ ax: $.input.axis('left', 'right'),
                         jump: $.input.down('jump') });
// Применение ввода — ОДНА функция на запись и на проигрывание.
const apply = (in_) => {
    hero.move(in_.ax * 200 * $.time.delta(), 0);
    if (in_.jump) hero.jump();
};

$.replay.record(sampler, apply);   // запись «из коробки»: обвязку ставит движок
// ... играем ...
$.fs.write('replay.json', $.replay.toText());
$.replay.stop();

$.replay.load($.fs.readText('replay.json'));
$.replay.play();                   // если apply не передать, возьмётся прежний
```

Одна и та же пара `sampler`/`apply` работает в обе стороны — это не
формальность: если записать одним способом, а применить другим, реплей
разойдётся, и виноват будет уже не движок.

---

## 1. Методы

| Вызов | Смысл |
|---|---|
| `record(sampler, apply)` | запись «из коробки»: обвязку `$.update` ставит движок |
| `play(apply?)` | воспроизведение «из коробки»; `apply` — та же функция, что при записи |
| `verify(sampler)` | сравнить текущий ввод с записью: `{ same, count, first }` |
| `start(extra?)` | начать запись вручную (старая стирается); `extra` — поля в заголовок |
| `record(value, frame?)` | записать кадр вручную (низкий уровень; см. `_push`) |
| `stop()` | остановить запись или проигрывание |
| `play(from?)` | начать воспроизведение (можно с кадра) |
| `tick()` | ввод текущего кадра проигрывания; `null` — записи нет или кадр пуст |
| `skip(count)` | перемотать вперёд без проигрывания |
| `load(data)` / `toText()` / `size()` | загрузка и текст для файла |
| `clear()` | стереть запись |
| `mode()` / `isRecording()` / `isPlaying()` | режим |
| `length()` / `position()` / `dropped()` | сколько кадров, где курсор, сколько потеряно |
| `header()` / `frames()` / `at(i)` | заголовок, кадры, кадр по индексу |
| `onEnd` | ваш обработчик конца воспроизведения |

Чистые функции: `createReplay(header)`, `compareReplays(a, b)` — вторая
возвращает `{ same, count, first }`, где `first` — номер первого расхождения
(на этом стоит регресс-проверка).

## 2. Заголовок

В заголовке лежит то, без чего воспроизведение не совпадёт: **зерно запуска**
(`engine.seed`) и **шаг времени** (`engine.fixedDt`), плюс всё, что игра передала
в `start({ ... })` (уровень, сложность). Сравнивайте заголовки перед
воспроизведением: другое зерно — другой мир.

## 3. Что записывать

Записывайте **вход в симуляцию**, а не состояние:

* нажатия и оси — своим компактным объектом (готового «снимка всего ввода» в
  `$.input` нет, и это осознанно: игра знает, что именно влияет на симуляцию);
* команды ИИ, если они приходят извне;
* ничего, что можно вычислить заново.

Объект превращается в JSON. Кадр больше 4096 байт не пишется (это уже не ввод),
циклический объект становится пустым кадром — пустой кадр **сохраняется**, иначе
съехало бы соответствие «кадр записи ↔ кадр проигрывания».

## 4. Ограничения (честно)

* **реплей воспроизводит ровно то, что вы записали.** Если симуляция зависит от
  чего-то вне записи (настенные часы, `Date.now`, сеть, положение мыши без
  записи, `Math.random` вместо `$.random`), воспроизведение разойдётся — движок
  об этом не догадается;
* **предел записи — 36 000 кадров** (10 минут при 60 к/с). Дальше кадры
  считаются потерянными в `dropped()`, а не молча теряются;
* **нумерация кадров не сверяется при проигрывании**: `tick()` отдаёт кадры по
  порядку записи, а не по `engine.frame`. Если игра пишет не каждый кадр,
  соответствие держит игра;
* **состояние мира не пишется**: реплей — это ввод, а не снимок мира. Для
  снимка используйте `$.prefab`/`$.save`;
* **нет сжатия**: текст — это JSON; для долгих записей сохраняйте реже или
  округляйте значения.

## 5. Проверка

```bash
# ядро без движка: запись, проигрывание, текст, сравнение
build/_deps/quickjs-build/qjs tests/js/replay_test.mjs
# в движке: номер кадра, зерно запуска, шаг времени, текст записи
python3 tests/agent/highlevel_replay_test.py
```
