# Агент — `$.agent`

Мост между игрой и программой, которая ей управляет. Низкий уровень (команды
`eval`/`state`/`step` по stdin/stdout) описан в [AGENT_API.md](AGENT_API);
задача `$.agent` — превратить мир в понятный снимок и дать игре проверять себя.

```js
$.agent.expose('hero', () => ({
    hp: $('#hero').hp(), x: Math.round($('#hero').pos().x),
}));
$.test.truthy($('#hero').hp() > 0, 'жив');
$.test.near($('#hero').pos().x, 100, 6, 'дошёл');
```

Проверки живут **не** у `$.agent`, а у `$.test`
([HIGH_LEVEL_API.md](HIGH_LEVEL_API) §25): `check/equal/near/truthy/falsy`
и итог `reset/results/report`.

Утверждения в понятиях мира — `$.expect(селектор)`: `exists()`, `count(n)`,
`empty()`, `hp(n)`, `prop(имя, значение)`, `positionNear(x, y, eps)`,
`state(значение)`.

```js
$.expect('#door').state('open');
$.expect('.enemy').count(5);
$.expect('#hero').positionNear(100, 300, 1);
$.test.reset();          // перед прогоном
$.test.report();         // «Все проверки пройдены (N)»
```

Каждое утверждение идёт через `$.test.check`, поэтому попадает и в общий
счётчик, и в снимок агента. Провал приходит не только строкой, но и структурной
деталью — `$.test.results().details[i]` и `state.tests.details`:

```json
{ "message": "#hero: hp = 1", "subject": "#hero",
  "prop": "hp", "expected": 1, "actual": 100 }
```

Это и есть артефакт падающего теста: агент видит, **что** именно не совпало, и
не разбирает текст лога. `state()` читает **свободный атрибут** `state`
(`$('#door').attr('state', 'open')`), а не свойство узла: у анимации клипами своё
`state`, путать их нельзя.

---

## 1. Методы

| Вызов | Смысл |
|---|---|
| `snapshot()` | снимок мира: кадр, время, узлы, физика |
| `expose(name, fn)` | добавить своё поле в снимок |
| `install()` | зарегистрировать снимок в движке (команда `state`) |
| `describe()` | строка состояния |
| `frame()` / `time()` / `node(sel)` | быстрый доступ к данным снимка |
| `nodes(sel, limit?)` | список описаний узлов; `limit` обрезает список |
| `active` / `headless` / `seed` | параметры запуска (см. §3) |

`node()` и `nodes()` — тот же код, что обслуживает команды протокола `inspect`
и `query` (а режим `count` — команду `profile`): DevTools, агент и игра видят
одинаковые описания узлов, второй реализации поиска нет
([AGENT_API.md](AGENT_API) §3.3.1–3.3.3).

## 2. Узлы в снимке

`snapshot()` отдаёт узлы как **краткое описание** (`nodeBrief`): тег, id, классы,
позиция, размер, угол, видимость, здоровье (`hp`, `max_hp`), `team`, `alive`,
тело, признак `ui` и **семантику `aria`**
(`$.ui.aria`) — она нужна, чтобы доступность интерфейса проверялась тестом.
Текста и значений произвольных игровых полей там **нет** — их добавляйте
через `expose`. Мир и интерфейс идут разными разделами: `entities` и `ui`.

## 3. Активация

`active` / `headless` / `seed` — параметры запуска. В обычном запуске агент
неактивен, и `describe()` показывает нули.

## 4. Ограничения

* **снимок беден по умолчанию**: текст, инвентарь и игровые поля появляются
  только через `expose`; из «боевого» `nodeBrief` в снимке уже есть `hp`,
  `max_hp`, `team` и `alive`, а также позиция, размер, угол и видимость;
* **проверки не бросают исключений**: результат копится в `results()`, падение
  теста решает программа-агент;
* **реплеи есть, но вне агента**: запись и воспроизведение ввода делает
  `$.replay` (replay.md), а команд протокола для них нет — сценарий
  разыгрывается командами `key`/`touch`/`pad`;
* **сетевых команд нет**: `net`/`net-peer` не реализованы, сетевые сценарии
  разыгрываются двумя процессами движка
  ([net_loopback_test.py](https://github.com/Nikide/russiano2d/blob/main/tests/agent/net_loopback_test.py)).
