# Ментальная модель

> Снимок, команда, квитанция, событие, тик, пачка. Разберитесь в этих шести понятиях — и станет ясно, почему интерфейсы устроены именно так и как писать быстрый код.

Источник: https://war3ai.com/ru/docs/concepts/

## Снимок: чтение без ожидания

Каждые **50 ms** рантайм обходит в игровом потоке весь мир и записывает его в общую память. `g.snapshot()` возвращает **полный и согласованный** мир:

- 16 слотов игроков: золото, древесина, пища, лимит, всего добыто, раса;
- до 1024 юнитов: тип, владелец, координаты, здоровье / мана (с максимумами), текущий приказ и цель приказа, **кого юнит реально атакует** (текущая цель), уровень героя / опыт / очки навыков, видимость юнита для каждого игрока;
- до 256 записей с деталями юнитов: 12 способностей (уровень, оставшаяся перезарядка), 8 баффов, 6 ячеек инвентаря;
- предметы на земле, деревья (обновляются каждые 2 секунды), таблица производства (прогресс найма / исследований / постройки / улучшений), игровые часы, игровое время суток.

Чтение одной копии занимает около **0.4 ms** (разбор на Python) и не ждёт игровой поток. Поэтому: **читайте сколько угодно**. Интерфейсы вроде `g.units()`, `g.my_army()`, `g.cooldown()`, `g.inventory()` берут данные из одного и того же снимка, и сколько бы раз вы их ни вызвали за тик, это дёшево.

> **Совет**
>
> Период публикации настраивается: `g.set_publish_period(ms)`, от 16 до 1000 миллисекунд. Один сбор занимает в игровом потоке около 0.5 ~ 0.9 ms, так что и 33 ms — не проблема. Значение одно на всю машину; действует последнее записанное.

## Команда: запись, около одного кадра

`g.move / attack / gather / build / train / cast …` выполняются в игровом потоке. Рантайм выполняет отправленные клиентом команды пачками внутри **диспетчеризации событий** игрового потока, поэтому одна команда ждёт около **одного кадра** (около 0.1 ms, если попадает в серию событий, иначе — до следующей диспетчеризации).

- Команде можно передать **один юнит или список**; юниты из списка получают приказ в одном кадре;
- `queue='after'` — это очередь через Shift: сначала закончить текущее дело, потом это;
- в командах можно использовать объекты юнитов прямо из снимка — SDK сверяет их по **паре дескрипторов** (адреса переиспользуются новыми юнитами, дескрипторы — нет).

## Квитанция: есть у каждой команды

```python
r = g.build(worker, "hbar", x, y)
if r:                      # движок принял
    ...
else:
    r.reason               # 'rejected（金不够）' (= «не хватает золота»)
    r.verdict              # 8
r.exec_us                  # сколько микросекунд команда выполнялась в игровом потоке
```

Квитанция читается **в том же кадре**: приказ юнита до и после команды, возвращаемое значение функции движка, код причины из проверки выполнимости. Она отвечает на вопрос «принял ли движок команду и если нет, то почему», но **не отвечает** на вопрос «получилось ли в итоге» — это смотрите по снимку и событиям.

Все коды состояния и коды причин см. в разделе [Квитанции и коды причин](https://war3ai.com/ru/docs/reason-codes/).

## Событие: что произошло

`on_event(g, ev)` перед каждым `on_tick` по одному передаёт вам события, накопившиеся с прошлого тика:

| Событие | Значение |
|---|---|
| `unit.appeared` / `unit.died` / `unit.removed` | Юнит появился, погиб, исчез (вход в золотой рудник, обращение, разложение трупа — тоже исчезновение, а не смерть) |
| `unit.damaged` / `order.changed` / `owner.changed` | Получил урон, сменил приказ, сменил владельца |
| `hero.levelup` | Герой получил уровень |
| `item.appeared` / `item.removed` | Предмет на земле появился, подобран или использован |
| `damage` | Уровень движка: **каждый** удар. Юнит-источник, тип атаки, тип урона, фактически снятое здоровье, урон до учёта брони |
| `killed` | Уровень движка: этот удар добил цель, с указанием убийцы |
| `production.done` | Найм / исследование / постройка / улучшение завершены, с четырёхсимвольным кодом и затраченными игровыми секундами. Приходит и для противника |
| `spell.cast` | Юнит применил способность: четырёхсимвольный код способности, уровень, перезарядка в секундах, точка применения |
| `message` | В окне сообщений на экране появилась строка: подсказка игры («Нужно больше ферм»), чат (в `.chat` — отправитель и текст), системное сообщение |
| `selection.changed` / `player.left` | Изменилось выделение локального игрока / игрок вышел или удалён после поражения |
| `game.started` / `game.ended` | Началась новая партия / выход из матча |

События ввода — нажатия кнопок холста, горячие клавиши, клики по земле — описаны в разделе [Интерфейс и ввод](https://war3ai.com/ru/docs/ui-input/).

> **Внимание**
>
> Поток событий **глобальный**: в нём есть и завершённое производство противника, и гибель крипов. Фильтруйте по `ev.owner` или по дескриптору юнита.

## Тик: ритм бота

По умолчанию `on_tick` вызывается 5 раз в секунду (по реальному времени). Время одного тика — это практически только ваши собственные вычисления: снимок читается без ожидания, команда занимает около кадра. Если тик не укладывается в период, следующий автоматически сдвигается, и отставание не накапливается.

- **На скорости 2× не ждите по реальному времени.** Чтобы подождать 3 игровые секунды, смотрите, когда `g.clock()` вырастет на 3, а не вызывайте `sleep(1.5)`.
- **Не вызывайте `sleep` в `on_tick`.** Если нужно «сделать чуть позже», запомните текущее игровое время и проверьте в следующем тике.

## Пачка: десятки команд — одно ожидание

Когда за тик нужно отдать много команд, оберните их в `with g.batch():`:

```python
with g.batch():
    g.attack(melee, target_a)
    g.attack(ranged, target_b)
    g.move(wounded, home.x, home.y)
    g.cast(hero, "thunderclap")
# в конце блока вся пачка отправляется разом: выполняется в одном кадре, игровой поток ждём один раз
```

- Команды внутри блока возвращают `Pending`, который после конца блока превращается в квитанцию; чтение до конца блока выбросит ошибку;
- если внутри блока выброшено исключение, **вся пачка отменяется** (половина команд опаснее, чем ни одной);
- измерено на 8 командах движения: по одной — 68 ~ 99 ms, пачкой — **6.5 ~ 10 ms**.

Тот же подход работает и для запросов: `g.can_do_many([(u, code), ...])`, `g.tech_many([...])` спрашивают о многом за раз.

## Чтение только что записанного

В пределах одного тика снимок ещё не видит отданных вами команд (он догонит их в следующей публикации). Две части логики могут не поделить одного рабочего: одна только что отправила его строить ферму, а другая по снимку считает, что он всё ещё свободен.

Эту проблему решает `g.order_of(u)`: пока снимок не догнал, он берёт новый приказ из квитанции. **Чтобы определить, простаивает ли юнит, используйте `g.order_of(u)`, а не `u.order`.** `g.idle_workers()` уже исключает тех, кому работу дали в этом тике.

## Уровни задержки

| Уровень | Канал | Задержка | Для чего |
|---|---|---|---|
| 0 | push-снимок + поток событий | чтение копии около 0.4 ms; свежие данные каждые 50 ms | все интерфейсы «наблюдения» |
| 1 | быстрая полоса | около 1 кадра; медиана 0.06 ms при 6 параллельных процессах | все команды и запросы (по умолчанию в SDK) |
| 2 | канал управления | 20 ~ 40 ms | запасной путь и немногие операции с интерфейсом (скорость игры, реплики, сообщения) |
| 3 | [шлюз](https://war3ai.com/ru/docs/gateway/) (WebSocket / JSON) | уровень 1 + около 1 ms | любые языки, браузер, LLM, программы на другой машине |

В [каталоге API](https://war3ai.com/ru/api/) для каждого интерфейса указано, через какой уровень он работает.
