# War3AI / OpenWar3 Полная документация
> Источник: https://war3ai.com/ru — открытый интерфейс Warcraft III 1.27 для ИИ-агентов. При написании бота используйте только методы Game, перечисленные в «Каталоге API» в конце файла.
---
# Обзор документации
> Документация OpenWar3: что это и что умеет; быстрый старт, первый бот, LLM пишет бота, API и протокол, шлюз и MCP — с какой страницы начать чтение в вашей ситуации.
**OpenWar3** — открытый интерфейсный слой War3AI: рантайм, внедряемый в Warcraft III 1.27, плюс Python SDK.
- Каждые **50 ms** рантайм выкладывает в общую память полное состояние всей карты: ресурсы и пищу всех игроков; здоровье и ману каждого юнита, его приказ, кого он сейчас атакует, перезарядку способностей, баффы и инвентарь; предметы на земле, деревья, очереди производства, время суток. Плюс **поток событий**: юниты появляются и гибнут, каждый отдельный удар, завершение производства…
- Внешняя программа отдаёт **семантические команды** с задержкой **около одного кадра**: движение, атака, добыча, строительство, тренировка, применение и изучение способностей, воскрешение, использование предметов, покупки… На каждую команду приходит **квитанция**: принял ли её движок, а если нет — код причины.
- Вы говорите только «что сделать»: юниты задаются четырёхсимвольными кодами, способности — строками приказов, как в самой игре. «Как это сделать» — забота рантайма.
Поэтому LLM не нужны никакие низкоуровневые знания и не нужно смотреть на экран. Прочитав документацию, модель пишет бота, который умеет вести экономику и воевать, а после выхода на карту сама дорабатывает его по квитанциям и событиям.
И не только бои: [холст](https://war3ai.com/ru/docs/canvas/) рисует поверх игрового экрана ваши собственные панели и пометки, с [интерфейсом и вводом](https://war3ai.com/ru/docs/ui-input/) нарисованные кнопки нажимаются, а горячие клавиши срабатывают, [JASS-канал](https://war3ai.com/ru/docs/jass/) позволяет извне вызывать 1291 функцию игры, а на RPG-картах можно взять с собой [ИИ-компаньона](https://war3ai.com/ru/docs/companion/). Готовый ИИ можно оформить как [схему](https://war3ai.com/ru/docs/schemes/) — переключение в один клик, экспорт для обмена; целый новый геймплей можно написать как [игровой мод](https://war3ai.com/ru/docs/mods/).
Подключиться можно и без Python: [шлюз](https://war3ai.com/ru/docs/gateway/) позволяет вызывать те же методы по WebSocket / JSON из любого языка и со страницы в браузере, а [MCP-сервер](https://war3ai.com/ru/docs/mcp/) даёт агентам вроде Claude Code напрямую вызывать инструменты — смотреть обстановку и отдавать команды.
- [Быстрый старт](https://war3ai.com/ru/docs/quickstart/): Настройте окружение, запустите матч одной командой и посмотрите, как управление берёт пример бота.
- [Пишем бота с помощью LLM](https://war3ai.com/ru/docs/ai-bot/): Программировать не обязательно: скопируйте промпт, опишите стиль игры и отдайте агенту.
- [Ментальная модель](https://war3ai.com/ru/docs/concepts/): Снимки, команды, квитанции, события, тик. Пять минут перед тем, как писать бота.
- [Каталог API](https://war3ai.com/ru/api/): Все методы: у каждого указаны статус проверки, класс задержки и механизм под капотом.
## Выберите путь под свою ситуацию
| Кто вы | С чего начать | Что дальше |
|---|---|---|
| Играете в Warcraft, но не программируете | [Быстрый старт](https://war3ai.com/ru/docs/quickstart/) → [Пишем бота с помощью LLM](https://war3ai.com/ru/docs/ai-bot/) | Если возникнут вопросы — [частые вопросы](https://war3ai.com/ru/docs/faq/) |
| Знаете Python | [Первый бот](https://war3ai.com/ru/docs/first-bot/) → [Ментальная модель](https://war3ai.com/ru/docs/concepts/) → [Пятнадцать правил](https://war3ai.com/ru/docs/rules/) | [Приёмы профи](https://war3ai.com/ru/docs/cookbook/), [Примеры ботов](https://war3ai.com/ru/docs/examples/) |
| Делаете coding-агента или автоматизацию | [Автономные итерации агента](https://war3ai.com/ru/docs/agent-loop/) | [Квитанции и коды причин](https://war3ai.com/ru/docs/reason-codes/), [`llms-full.txt`](https://war3ai.com/ru/llms-full.txt) |
| Хотите, чтобы LLM принимала решения по ходу матча | [LLM как советник](https://war3ai.com/ru/docs/llm-coach/) | [Реплики и локальные модели](https://war3ai.com/ru/docs/speech/) |
| Хотите, чтобы агент сам управлял игрой (Claude Code и т. п.) | [LLM вызывает инструменты (MCP)](https://war3ai.com/ru/docs/mcp/) | [Интерфейс и ввод](https://war3ai.com/ru/docs/ui-input/) |
| Пишете на другом языке (JS, C#, Go, Rust…) | [Шлюз](https://war3ai.com/ru/docs/gateway/) | Уровнем ниже: [протокол W3P](https://war3ai.com/ru/docs/protocol/) |
| Хотите стравить ИИ разных авторов | [Честный режим](https://war3ai.com/ru/docs/fair-mode/) | [Арена](https://war3ai.com/ru/arena/) |
| Хотите придумать свой геймплей на RPG / пользовательской карте | [Игровые моды](https://war3ai.com/ru/docs/mods/) | [Интерфейс и ввод](https://war3ai.com/ru/docs/ui-input/), [Холст](https://war3ai.com/ru/docs/canvas/), [JASS-канал](https://war3ai.com/ru/docs/jass/), [RPG-компаньон](https://war3ai.com/ru/docs/companion/) |
| Хотите поделиться своим ИИ с другими | [ИИ-схемы](https://war3ai.com/ru/docs/schemes/) | [Консоль Farsight](https://war3ai.com/ru/docs/console/) |
## Что лежит в репозитории
```text
start.bat Единственная точка входа: развёртывание с нуля + открытие Farsight; stop.bat полностью останавливает всё
sdk/python/ Интерфейсный слой. openwar3/ — внешний фасад (Game + Bot), начинайте отсюда
brains/ Слой принятия решений
examples/ hello_bot (экономика) → rush_bot (армия) → macro_bot (макро) → micro_bot (микро + крипинг); buddy (RPG-компаньон);
mod_hero_roguelike / mod_endless_defense (игровые моды)
xwar3/ Эталонный мозг: стратегический слой (секунды) + рефлекторный слой (4 процесса) + модель шансов на победу
console/ Веб-консоль Farsight (FastAPI + React)
gateway/ Шлюз (WebSocket / JSON) + JS-клиент + демо-страница для браузера
director/ Автооператор камеры, полоски здоровья над юнитами
speech/ Облачка реплик над юнитами + локальная LLM
runtime/ Оркестрация нескольких экземпляров (каждый матч перезапускается по настройкам)
data/ order-ids.txt; инструменты для извлечения данных из вашей собственной игры
schemes/ Ваши ИИ-схемы (mine/) и схемы, которыми поделились другие (installed/); в репозиторий не попадают
tools/ play.py (матч одной командой), run_scheme.py (запуск схем), war3_mcp.py (MCP-сервер), run_tests.py, скрипты проверки в реальной игре
docs/ Каталог API api.json (генерируется из кода), протокол, руководство
```
Между рантаймом и вашим кодом — только версионированный [протокол W3P](https://war3ai.com/ru/docs/protocol/): проще всего работать через Python SDK, но можно подключиться по протоколу и с любого другого языка.
## Что означает «статус проверки» метода
В каталоге API у каждого метода указан один из трёх статусов:
- **Проверено в реальных играх**: низкоуровневый путь (номер действия, форма аргументов, считанный результат) проверен в настоящих матчах, и его охраняют скрипты проверки.
- **Экспериментальный**: новый метод, уже работает на тестовом экземпляре, проверка в реальной игре идёт по пунктам. Пользоваться можно, но детали интерфейса ещё могут измениться.
- **Выведено / не полностью проверено**: механизм повторяет то, как это делает сам движок (например, эквивалентная функция JASS), но ещё не проверен в матчах пункт за пунктом. Перед использованием смотрите на квитанцию.
> **Примечание**
>
> Сейчас поддерживается только **Warcraft III 1.27** (The Frozen Throne). Версии 1.24 ~ 1.28 устроены на одном и том же движке; поддержка нескольких версий запланирована на этапе P4 [дорожной карты](https://war3ai.com/ru/roadmap/). Начиная с 1.29, а также Reforged — это другой движок, и их поддержку мы не обещаем.
---
# Быстрый старт
> Дважды щёлкните start.bat — всё установится само; укажите папку игры в Farsight и запустите партию, чтобы посмотреть, как управление берёт пример бота. Около 15 минут.
## Что понадобится
| | Требование | Пояснение |
|---|---|---|
| Система | Windows 10 / 11, 64-битная | Пока поддерживается только Windows |
| Игра | Warcraft III **1.27a** (The Frozen Throne, `Game.dll` 1.27.0.52240) | Ваш собственный легально приобретённый клиент; никакие файлы игры на диске не изменяются |
**Больше ничего заранее ставить не нужно.** `start.bat` скачивает только одно: Python 3.13 (официальный портативный пакет, около 14 МБ) — в папку `bin\env\` репозитория, права администратора не нужны, системный PATH не меняется, в китайских сетях автоматически выбираются зеркала; если на компьютере уже есть рабочий Python, используется он. PowerShell берётся тот, что уже есть в Windows; веб-интерфейс Farsight поставляется вместе с репозиторием уже собранным, Node.js не нужен.
## Установка
1. **Получите код**
```bash
git clone https://github.com/OPENXXAI/OpenWar3AI.git
```
Или скачайте [архив](https://github.com/OPENXXAI/OpenWar3AI/archive/refs/heads/main.zip) и распакуйте его. Рантайм (внедряемая DLL и загрузчик) поставляется вместе с репозиторием, отдельно скачивать его не нужно.
2. **Дважды щёлкните `start.bat`**
При первом запуске он сам:
- скачает Python 3.13;
- установит пакеты Python, проверит файлы рантайма и установит их;
- скачает данные AMAI и сгенерирует из них стратегические данные для эталонного мозга (у AMAI собственная лицензия, сгенерированное в git не попадает; сбой затронет только эталонный мозг);
- откроет главную страницу Farsight «Центр управления» `http://127.0.0.1:8866`.
Результат каждого шага выводится на экран, а для неудавшегося шага скрипт подскажет, как это исправить. В дальнейшем каждый двойной щелчок — лишь проверка на одну-две секунды, после чего открывается Farsight.
Чёрное окно через несколько секунд закроется само: Farsight продолжает работать в фоне и не остановится, даже если закрыть браузер.
3. **Укажите папку игры в «Центре управления»**
В самом верху «Центра управления» можно нажать «Найти автоматически» или выбрать папку Warcraft III самостоятельно через «Обзор…». Farsight проверит версию игры и **извлечёт данные из вашей собственной копии игры** (таблицы юнитов, способности, предметы, таблица контр… файлы Blizzard вместе с кодом не распространяются).
Если версия не 1.27a, вы увидите предупреждение. Карты и настройки следующего матча задаются относительно этой папки (можно выбрать карту из любой подпапки `<папка игры>\Maps`); сменить папку позже можно на странице «Настройки».
4. **Запустите партию и отдайте управление примеру бота**
Проще всего — на странице Farsight «Экземпляры и запуск» отметить номер экземпляра, выбрать ИИ-схему и нажать «Начать тест». Можно и из командной строки:
```bash
python tools/play.py --bot brains/examples/hello_bot.py
```
Эта команда запускает экземпляр игры, внедряет рантайм, автоматически начинает партию и затем запускает бота. **Если крестьяне пошли на рудник, а ратуша начала нанимать крестьян — всё работает.**
`python` в команде — это интерпретатор, записанный в `openwar3.json`; тот, что установил сам `start.bat`, лежит в `bin\env\python\python.exe`.
## start.bat и stop.bat
```bash
start.bat # проверка развёртывания + открыть Farsight
start.bat setup # полная проверка: переустановить пакеты Python, повторить AMAI
start.bat restart # перезапустить только бэкенд Farsight (игры и службы не затрагиваются)
start.bat node # заодно установит Node.js (нужен только для предпросмотра сайта, обычно не нужен)
start.bat 5 6 # заодно начать тест на экземплярах 5 и 6 (игра + эталонный мозг)
stop.bat # полностью остановить всё; stop.bat --keep-llm оставляет локальную модель в видеопамяти
```
Шлюз, облачка реплик и локальная LLM тоже запускаются и останавливаются в «Центре управления» Farsight — искать другие скрипты не нужно. **Полная остановка**: дважды щёлкните `stop.bat` или нажмите «Остановить всё» в правом верхнем углу «Центра управления» — по очереди остановятся экземпляры игры, ИИ, шлюз, облачка, локальная модель, которую использует система, и бэкенд Farsight. MCP-сервер принадлежит клиентам вроде Claude и не останавливается.
> **Файл конфигурации**
>
> `openwar3.json` автоматически записывают `start.bat` и Farsight; в нём хранятся только локальные пути, в git он не попадает. Чтобы изменить порты, адрес локальной LLM или имя модели, укажите по образцу `openwar3.example.json` только то, что от него отличается.
## Параметры play.py
```bash
python tools/play.py --bot my_bot.py --inst 9 --race 2 --enemy-race 1 --difficulty 3 --speed 200
python tools/play.py --bot my_bot.py --inst 9 --attach # игра уже запущена, только подключаем Bot
python tools/play.py --bot my_bot.py --fair # честный режим: видно только то, что в зоне обзора
```
| Параметр | По умолчанию | Описание |
|---|---|---|
| `--bot` | обязателен | Путь к файлу бота (в файле должен быть подкласс `Bot`) |
| `--inst` | `9` | Номер экземпляра. Не занимайте номер уже работающего экземпляра (какие номера заняты, видно на странице Farsight «Экземпляры и запуск») |
| `--race` | `1` | Наша раса: 1 Люди, 2 Орда, 3 Нежить, 4 Ночные эльфы |
| `--enemy-race` | `0` | Раса противника |
| `--difficulty` | `2` | Сложность компьютерного противника: 2 лёгкая, 3 обычная, 4 безумная |
| `--speed` | `100` | Скорость игры (в процентах, 200 = скорость 2×) |
| `--map` | `default_map` из конфигурации | Карта |
| `--attach` | | Не запускать игру, только подключиться к уже работающему экземпляру |
| `--hz` | `5` | Сколько раз в секунду вызывать `on_tick` |
| `--minutes` | `60` | Максимальная длительность в минутах (реального времени) |
| `--fair` | | [Честный режим](https://war3ai.com/ru/docs/fair-mode/) |
| `--player` | | От имени игрока с каким номером командовать (для матчей AI против AI) |
> **Внимание**
>
> Не запускайте игру с `--minimize`: **когда окно свёрнуто, симуляция игры стоит** (часы не идут), и бот так и не дождётся начала партии.
Можно обойтись и без `play.py` и подключиться к уже работающему экземпляру через командную строку SDK:
```bash
python -m openwar3 run brains/examples/hello_bot.py --inst 5 # запустить Bot
python -m openwar3 status --inst 5 # подключиться и вывести состояние снимка / быстрой полосы
python -m openwar3 catalog # вывести каталог API
```
## Когда всё заработало
- [Пишем первого бота](https://war3ai.com/ru/docs/first-bot/): Начинаем с минимального бота на 10 строк и шаг за шагом добавляем найм войск и атаку.
- [Пусть его напишет LLM](https://war3ai.com/ru/docs/ai-bot/): Скопируйте шаблон промпта и опишите стратегию простыми словами.
## Самопроверка
```bash
python tools/run_tests.py # SDK / эталонный мозг / рефлекторный слой / консоль / реплики / примеры — каждый набор в отдельном подпроцессе
```
Офлайн-тестам запущенная игра не нужна. В «Центре управления» Farsight тоже есть проверка окружения — там видно, все ли части установлены.
---
# Первый бот
> Начинаем с минимального бота на 10 строк, добавляем найм крестьян, постройку ферм, армию, героя и атаку, а в конце разбираемся с квитанциями.
Бот — это класс, унаследованный от `openwar3.Bot`. Вам нужно переопределить только нужные хуки, а `g` (`Game`) отвечает за то, чтобы «видеть» и «делать».
## Минимальный бот
```python title="my_bot.py"
from openwar3 import Bot
class MyBot(Bot):
def on_start(self, g): # вызывается один раз после входа в партию
g.message("Я здесь")
def on_tick(self, g): # около 5 раз в секунду
for w in g.idle_workers():
g.gather(w, g.nearest(g.gold_mines(), w))
```
```bash
python tools/play.py --bot my_bot.py
```
Простаивающие крестьяне пойдут на ближайший золотой рудник. Четыре хука:
| Хук | Когда вызывается |
|---|---|
| `on_start(g)` | Один раз после входа в партию, до первого тика |
| `on_tick(g)` | Каждый тик (по умолчанию 5 раз в секунду). Если тик не уложился, следующий автоматически сдвигается, и отставание не накапливается |
| `on_event(g, ev)` | Перед каждым `on_tick` по одному передаёт события, накопившиеся с прошлого тика |
| `on_end(g, reason)` | Один раз по окончании партии (процесс игры исчез / у нас не осталось юнитов / ручная остановка) |
> **Совет**
>
> Исключение в `on_tick` не прерывает партию: запускатель печатает стек вызовов и продолжает со следующего тика; остановка происходит только после **20 тиков подряд с ошибкой**.
## Добавляем экономику: крестьяне и фермы
```python
from openwar3 import Bot
class Economy(Bot):
def on_tick(self, g):
res = g.resources() # если не удалось прочитать — None, а не 0
halls = g.my_buildings({"htow", "hkee", "hcas"})
if res is None or not halls:
return
home = halls[0]
# 1. простаивающие крестьяне идут добывать золото
for w in g.idle_workers():
mine = g.nearest(g.gold_mines(), w)
if mine:
g.gather(w, mine)
# 2. нанимаем крестьян: в очереди только 1 (полная очередь запирает в ней деньги)
if len(g.my_workers()) < 15 and not g.queue(home):
g.train(home, "hpea")
# 3. пища почти кончилась: берём крестьянина, который сейчас не строит, и ставим ферму у ратуши
if res["food_cap"] - res["food_used"] <= 6:
builder = next((w for w in g.my_workers() if not g.is_constructing(w)), None)
if builder:
g.build_near(builder, "hhou", home.x, home.y)
```
Три приёма, на которые стоит обратить внимание:
- **Нанимаем, только когда `g.queue(home)` пуста.** Если отдавать приказ найма каждый тик, очередь из 7 ячеек заполнится и запрёт деньги (измерено: в ратуше стояли в очереди 4 крестьянина, 300 золота заперты в очереди, старт заметно замедлился).
- **`build_near` вместо жёстко заданных координат.** Он сам ищет подходящее место от ближнего к дальнему и отслеживает результат между тиками; если не хватает денег, ничего не делает. Жёстко заданные координаты вполне могут оказаться прямо в лесу.
- **Не берём крестьянина, который уже строит.** Ферма Людей строится 35 секунд; если увести рабочего посреди стройки, фундамент встанет.
Полная версия, работающая за все четыре расы, — `brains/examples/hello_bot.py`: по 5 рабочих на рудник, при заполненном руднике — на лес, достройка замороженных фундаментов.
## Добавляем казарму, героя и атаку
```python
from openwar3 import Bot
WAVE = 8
class Rush(Bot):
def on_start(self, g):
self.attacking = False
def on_tick(self, g):
halls = g.my_buildings({"htow", "hkee", "hcas"})
if not halls:
return
home = halls[0]
# герой: есть алтарь, нет героя -> сначала воскрешаем, не вышло — нанимаем (герой уникален, повторный найм погибшего отклонят)
altars = g.my_buildings({"halt"})
if altars and not g.my_heroes():
if not g.revive(altars[0]):
g.train(altars[0], "Hpal")
for h in g.my_heroes():
info = g.hero_info(h)
if info and info["skill_points"]:
g.learn(h, "AHhb") # Свет небес
# казармы непрерывно нанимают пехотинцев (в очереди только 1)
for b in g.my_buildings({"hbar"}):
if not g.queue(b):
g.train(b, "hfoo")
# набрали волну — атакуем; армию потрепали — домой
army = g.my_army()
if len(army) >= WAVE:
self.attacking = True
elif len(army) < WAVE // 2:
self.attacking = False
if self.attacking:
target = g.nearest([e for e in g.enemies() if g.is_building(e)], home)
if target:
idle = [u for u in army if not g.order_of(u)] # приказываем только бездельникам
g.attack_move(idle, target.x, target.y)
```
Полная версия — `brains/examples/rush_bot.py` (наследуется от `hello_bot` и строит казарму / алтарь, если их нет).
## Разбираемся с квитанциями
Каждая команда возвращает квитанцию. `if r:` означает «движок принял»; если не принял, в `r.reason` указано почему:
```python
r = g.train(barracks, "hfoo")
if not r:
print(r.reason) # rejected(人口不够) (= «не хватает пищи»)
print(r.verdict) # 3
```
Частые коды причин: `3` не хватает пищи, `8` не хватает золота, `9` не хватает древесины, `32` очередь заполнена, `183` нет нужного здания или технологии, `221` такого нет / уже строится / герой уже есть, `1001` цель не видна. Полная таблица — в разделе [Квитанции и коды причин](https://war3ai.com/ru/docs/reason-codes/).
> **«Принята» ≠ «выполнена»**
>
> Квитанция сообщает лишь, что «движок принял команду». Точку постройки в лесу движок тоже примет сразу, а неудача случится, только когда рабочий дойдёт до места; заклинание могут прервать. Результат смотрите по снимку и событиям: для построек используйте `build_near` (он следит, появился ли фундамент), для заклинаний проверяйте, ушло ли оно на перезарядку, через `g.cooldown()`.
## Что дальше
- [Ментальная модель](https://war3ai.com/ru/docs/concepts/): Снимок, команды, события, тик, пачка — почему всё устроено именно так.
- [Книга рецептов: приёмы профи](https://war3ai.com/ru/docs/cookbook/): 21 приём: полная загрузка добычи, без упора в лимит пищи, фокус огня, отвод раненых, крипинг ночью…
---
# Пишем бота с помощью LLM
> Можно и без навыков программирования: вы объясняете, как бот должен играть, а LLM пишет код. Скопируйте шаблон промпта, опишите стратегию, запустите — и просите доработать.
Подходит тем, кто играет в Warcraft III, но не умеет программировать, а также разработчикам, которые хотят сэкономить время. Весь процесс — это диалог: **вы описываете стратегию → модель пишет код → вы играете партию → рассказываете модели, что увидели → она исправляет**.
> **Совет**
>
> Сначала настройте окружение по [Быстрому старту](https://war3ai.com/ru/docs/quickstart/) и добейтесь, чтобы `hello_bot` заработал (крестьяне пошли добывать золото). Тогда при проблемах вы сможете отличить ошибку окружения от ошибки в боте.
## 1. Подготовьте материалы для модели
Насколько хорошо модель напишет код, на восемьдесят процентов зависит от того, прочитала ли она правильные материалы. Выберите вариант под свой инструмент:
| Что вы используете | Как передать материалы |
|---|---|
| **Coding Agent с доступом к репозиторию** (Claude Code, Cursor, Codex и т. п.) | Откройте его в папке репозитория и попросите сначала прочитать `docs/BOT_HANDBOOK_ZH.md`, `docs/api.json` и один пример (для экономики — `brains/examples/macro_bot.py`, для боя — `micro_bot.py`) |
| **Чат-модель с доступом в интернет** | Попросите её сначала прочитать [`https://war3ai.com/llms-full.txt`](https://war3ai.com/ru/llms-full.txt) — вся документация сайта собрана в этом одном файле |
| **Веб-чат без доступа в интернет** | Вставьте руководство, [`api.json`](https://war3ai.com/ru/api.json) и один файл-пример после промпта |
| **Локальная модель** (LM Studio, Ollama) | То же самое. Контекстное окно лучше от 32K токенов, иначе руководство и каталог API не поместятся |
Если нужен какой-то приём профи, добавьте ещё соответствующий рецепт из [Книги рецептов](https://war3ai.com/ru/docs/cookbook/).
## 2. Скопируйте этот промпт
Замените последний блок «Стратегия, которую я хочу» своими словами — чем конкретнее, тем лучше:
```text
Напиши AI (на Python) для Warcraft III 1.27. Используй только методы Game, перечисленные в api.json,
не выдумывай несуществующие методы. Пиши по образцу rush_bot.py: наследуйся от openwar3.Bot, реализуй on_start(g) и on_tick(g).
Правила:
- on_tick вызывается примерно 5 раз в секунду и должен быть быстрым (никаких sleep внутри).
- Значение, которое не удалось прочитать, — это None, а не 0; сначала проверяй, потом используй.
- Команда возвращает квитанцию (Receipt): `if r:` означает «движок принял»; если не принял, в `r.reason` указана причина
(не хватает пищи, не хватает золота, цель не видна, такой герой уже есть…) — попробуй снова в следующий тик или иначе.
- Атаковать конкретного врага — g.attack(юниты, враг); враг должен быть в зоне обзора, невидимого отклонят.
- Погибшего героя воскрешают через g.revive(алтарь), нанять второго такого же нельзя.
- Здания строй через g.build_near(рабочий, код_здания, x, y): он сам найдёт место, куда здание влезет, и отследит результат; если не хватает денег, ничего не делает.
- Чтобы знать, «что только что произошло» (кто погиб, кто получил урон, герой получил уровень, выпал предмет), реализуй on_event(g, ev).
- Не отдавай одному и тому же юниту одну и ту же команду каждый тик (это прерывает то, что он делает); приказывай «бездельникам».
- На добычу отправляй только рабочих из idle_workers(). На одном золотом руднике не больше 5 рабочих.
- В очереди найма держи только 1 юнит (следующего ставь, когда g.queue(здание) пуста); упёрлись ли в пищу — смотри g.production(здание).blocked.
- Если за один тик нужно отдать много команд, оберни их в with g.batch(): (игровой поток ждём только один раз).
- Кого бить, выбирай через g.time_to_kill(моя_группа, враг) (учитывает контры и броню); куда идти — через g.path_distance (если не дойти, вернёт None).
- В честном режиме видно только то, что в зоне обзора; врагов, которых видели раньше, бери из g.last_seen().
- Юниты обозначаются четырёхсимвольными кодами (крестьянин Людей hpea, пехотинец hfoo, казарма hbar…), заклинания — строками приказов (thunderbolt — Молот бурь,
blizzard — Снежная буря, holybolt — Свет небес…, полный список в data/order-ids.txt), изучение способностей — четырёхсимвольными кодами (AHtb, AHbz…).
Стратегия, которую я хочу:
<напиши здесь простыми словами, например:
"Люди, в начале 5 крестьян на золото и 1 на лес; первый герой — Архимаг; две казармы, пехотинцы и стрелки;
набрав 12 юнитов, идём с героем на экспансию противника; если здоровье героя ниже 30% — отступаем домой;
на крипинге сначала лагеря поближе к базе.">
```
### Как понятно описать стратегию
Больше всего модель боится расплывчатых требований. Вместо «играй агрессивнее» полезнее такие сведения:
- **Раса и герой**: какого героя брать первым, порядок прокачки (например, Архимаг: Элементаль воды, Снежная буря, Элементаль воды…).
- **Порядок постройки**: на каком крестьянине ставить казарму, когда улучшать ратушу, сколько нужно казарм.
- **Состав армии**: пехотинцы + стрелки? С какого количества выходить?
- **Условия атаки и отступления**: с какой армией нападать, при каком здоровье героя отступать, если армию потрепали — домой, собирать заново.
- **Крипинг**: крипить ли, когда (после наступления темноты?), только лагеря, которые по силам?
- **Честность**: если потом собираетесь на Арену, укажите «использовать только врагов, которых видно в зоне обзора».
## 3. Запустите
Сохраните код от модели как `brains/my_bot.py`, затем:
```bash
python tools/play.py --bot brains/my_bot.py --race 1 --difficulty 2
```
Чтобы увидеть результат быстрее, добавьте `--speed 200` (скорость игры 2×).
## 4. Просите исправить
- **Ошибка**: вставьте модели **весь текст ошибки** как есть и попросите «исправь».
- **Играет плохо**: описывайте, **что вы видите в игре**, а не свои догадки о причине. Например: «герой всё время стоит на базе», «войска идут в бой по одному», «все крестьяне столпились на одном руднике».
- **Хотите новый приём**: добавляйте по одной вещи за раз, сыграйте партию, убедитесь, что ничего не сломалось, и только потом добавляйте следующую.
> **Примечание**
>
> Coding Agent, который умеет сам запускать команды, может взять на себя и шаги 3 и 4: сыграть партию, прочитать логи и квитанции, поправить код, запустить снова. Как дать ему достаточно информации, см. в разделе [Автономные итерации агента](https://war3ai.com/ru/docs/agent-loop/).
## 5. Частые проблемы
| Симптом | Скорее всего |
|---|---|
| Ничего не двигается | Неверный номер экземпляра (`--inst`) или игра ещё не вошла в партию |
| Крестьяне не добывают золото | Приказ отдан уже занятым крестьянам; отправляйте только `idle_workers()` |
| Здания никак не строятся | Используйте `build_near`, не прописывайте координаты жёстко; проверьте `reason` в квитанции — может, не хватает денег |
| Герой не появляется | Смотрите квитанцию `train`: не хватает пищи? Или герой погиб (нужен `revive`)? |
| Герой не применяет заклинания | Способность не изучена (`learn`) или нет маны; после применения проверьте через `cooldown()`, ушла ли она на перезарядку |
| Войска дёргаются каждый тик | Команды отдаются заново каждый тик; приказывайте только бездельничающим юнитам |
| Войска не нанимаются, деньги копятся | Упёрлись в лимит пищи: смотрите `g.production(казарма).blocked` |
| Модель использует несуществующие методы | Ещё раз подчеркните в промпте «только методы из api.json» и вставьте api.json целиком |
## Что дальше
- Все интерфейсы и механизм каждого из них: [каталог API](https://war3ai.com/ru/api/);
- эталонный мозг (`brains/xwar3/strategy`) — это полноценный AI, который занимает экспансии, крипует и атакует; можно дать модели изучить его подход, но он использует более низкоуровневые интерфейсы, поэтому копировать его напрямую не советуем;
- на [Арене](https://war3ai.com/ru/arena/) будут видны только враги в зоне обзора — добавьте `--fair` уже сейчас, чтобы потом ничего не переделывать.
---
# Автономные итерации агента
> Пусть Coding Agent сам играет партии, читает результаты, правит код и запускает снова. Для этого ему нужны команда, работающая без присмотра, структурированный отчёт о партии и чёткая цель.
В разделе [Пишем бота с помощью LLM](https://war3ai.com/ru/docs/ai-bot/) шаг «сыграть партию → посмотреть, что происходит → рассказать модели» выполняете вы. Coding Agent, умеющий запускать команды (Claude Code, Codex, режим Agent в Cursor и т. п.), может взять на себя и этот шаг, и цикл замкнётся:
```text
правка кода ──► партия (без присмотра) ──► чтение отчёта ──► поиск главной проблемы ──┐
▲ │
└──────────────────────────────────────────────────────────────────────────────────┘
```
Чтобы такой цикл действительно сходился, агенту нужны три вещи.
## 1. Команда, работающая без присмотра
```bash
python tools/play.py --bot brains/my_bot.py --speed 200 --minutes 10 --fair
```
- `--minutes` гарантирует, что партия закончится (в минутах реального времени), и агент не застрянет в ней;
- `--speed 200` экономит время за счёт скорости 2× — но в боте **ждите по игровым часам** (`g.clock()`), а не через `sleep` по реальному времени;
- `--fair` с первого дня заставляет писать по правилам Арены: видно только то, что в зоне обзора;
- по окончании запуска терминал печатает причину завершения, например `我方没有单位了` («у нас не осталось юнитов») или `到时间了` («время вышло»); всё, что бот выводит через `print`, тоже попадает в терминал.
> **Внимание**
>
> Когда окно свёрнуто, симуляция игры стоит. Пусть агент запускает игру в оконном режиме по умолчанию и не занимает номер экземпляра, которым пользуетесь вы (`--inst`).
## 2. Структурированный отчёт о партии
Вывод терминала рассчитан на человека. Агенту нужен JSON: что произошло, что не получилось и почему. SDK уже даёт всё сырьё — у квитанций есть коды причин, в потоке событий есть завершение производства и потери. Остаётся только это собрать:
```python title="recorder.py"
import collections, json, time
from openwar3 import Bot
class Recorder(Bot):
"""Добавляет Bot отчёт о партии. Унаследуйтесь от него и вызывайте super() в своих on_start / on_event."""
def on_start(self, g):
self.rejects = collections.Counter() # "train hfoo: rejected(人口不够)" -> число раз
self.timeline = [] # [игровые секунды, категория, четырёхсимвольный код]: найм / исследование / постройка / улучшение завершены
self.lost = collections.Counter() # что потеряли мы
self.killed = collections.Counter() # что убили мы
def check(self, r, what):
"""Оборачивает команду и записывает причину отказа: self.check(g.train(b, "hfoo"), "train hfoo")"""
if r is not None and not r:
self.rejects[f"{what}: {r.reason}"] += 1
return r
def on_event(self, g, ev):
me = g.me()
if ev.kind == "production.done" and ev.owner == me:
self.timeline.append([round(ev.clock), ev.done_kind, ev.done_code])
elif ev.kind == "unit.died":
(self.lost if ev.owner == me else self.killed)[ev.type] += 1
def on_end(self, g, reason):
report = {"reason": reason, "timeline": self.timeline, "lost": self.lost,
"killed": self.killed, "rejects": self.rejects.most_common(10)}
try: # игра могла уже закрыться — не прочитали, и ладно
report |= {"clock": g.clock(), "resources": g.resources(),
"army": len(g.my_army()), "workers": len(g.my_workers())}
except Exception:
pass
with open(f"run_{int(time.time())}.json", "w", encoding="utf-8") as f:
json.dump(report, f, ensure_ascii=False, indent=1)
```
На какие вопросы отвечает этот отчёт:
| Сигнал | Откуда | Что видно |
|---|---|---|
| Самые частые причины отказа | `reason` / `verdict` в квитанции | Постоянно упираемся в пищу (3), приказываем без денег (8 / 9), бьём цели в тумане войны (1001), нанимаем героя, хотя он погиб (221) |
| Хронология производства | события `production.done` (со временем в игровых секундах) | На какой секунде первый герой, на какой — улучшение ратуши, непрерывно ли работают казармы; можно сравнить с дебютами профессиональных игроков |
| Потери обеих сторон | события `unit.died` | Не сливаем ли войска, сколько раз погиб герой, окупился ли крипинг |
| Причина завершения | `on_end(g, reason)` | `我方没有单位了` («у нас не осталось юнитов») = поражение; `到时间了` («время вышло») = победитель не определён |
| Итоговая армия и ресурсы | снимок, прочитанный в `on_end` | Деньги копятся и не тратятся = производство не поспевает; мало рабочих = экономика не развилась |
> **Примечание**
>
> Программное определение победы — один из фундаментальных экспериментов [Арены](https://war3ai.com/ru/arena/), он пока в дорожной карте. Сейчас поражение можно определять по «у нас не осталось юнитов», а победу приблизительно — по «все видимые здания противника уничтожены».
## 3. Чёткая цель и несколько ограничений
Передайте агенту следующий текст, подправив цель под себя:
```text
Цель: добиться, чтобы brains/my_bot.py стабильно побеждал компьютер на «лёгком» уровне сложности на карте Echo Isles (Люди против случайной расы).
Каждый раунд:
1. Запусти python tools/play.py --bot brains/my_bot.py --speed 200 --minutes 10 --fair
2. Прочитай вывод терминала и самый свежий run_*.json: причину завершения, хронологию производства, самые частые причины отказа, потери обеих сторон
3. Найди «одну» проблему, сильнее всего влияющую на результат, и исправь только её; в комментарии к коду запиши причину правки и данные, на которые опирался
4. Вернись к шагу 1. Если 3 партии подряд нет прогресса — остановись и сообщи мне отчёт и свои выводы
Ограничения:
- Используй только методы из docs/api.json, не выдумывай интерфейсы
- Не отдавай одному и тому же юниту одну и ту же команду каждый тик; приказывай только бездельничающим юнитам
- Сохраняй --fair (только враги, которых видно в зоне обзора)
- Перед правкой кода запусти python tools/run_tests.py и убедись, что примеры не сломаны
```
## Привычки, которые ускоряют сходимость цикла
- **Меняйте что-то одно за раз.** Если поменять три вещи сразу, то при победе неизвестно, что сработало, а при поражении — что сломалось.
- **Сравнивайте на достаточном числе партий.** В одной и той же ситуации случайность велика; по двум партиям видна только очень большая разница. Чтобы судить о прогрессе, смотрите на тренд хотя бы по нескольким партиям.
- **Сначала чините отказы, потом настраивайте стратегию.** Самая частая причина отказа в квитанциях — зачастую и есть главный баг бота.
- **Записывайте выводы в комментарии.** Агент в следующем раунде (или в следующем диалоге) поймёт из комментариев, почему код написан именно так, и не откатит уже исправленное.
- **Страхуйтесь офлайн-тестами.** Напишите для ключевой логики модульные тесты, которым не нужна запущенная игра (тесты примеров ботов лежат в `brains/examples/tests/`), и пусть агент прогоняет их после каждой правки.
---
# LLM как стратегический советник
> Отдайте LLM вопросы «на что копить, куда ставить рабочих, атаковать или выжидать в эту минуту», а слою правил оставьте только исполнение и право вето. Эталонный мозг уже работает так; на этой странице — сам паттерн и подводные камни.
Когда бот дорастает до определённого размера, вы замечаете, что правила экономики наслаиваются одно на другое: правило про число лесорубов, правило «5 рабочих на рудник», правило «урезать добычу древесины вдвое, если её слишком много», правило «отправить больше людей на золото, если золота мало, а древесины много»… Каждое правило по отдельности верно, но вместе они порождают ситуации вроде «на руднике не хватает рабочих, а все крестьяне рубят лес» — ситуации, **за которые не отвечает ни одно правило**.
Решения вида «оценить всю картину и расставить приоритеты» изначально плохо ложатся на `if / else`, зато это ровно то, в чём сильны LLM. Эталонный мозг (`brains/xwar3/strategy/brain/coach.py`) использует следующее разделение на слои.
## Слои
```text
LLM (советник) Раз в 20 игровых секунд, асинхронно, никогда не блокирует тик
Вход: снимок партии на одну страницу (ресурсы, пища, распределение рабочих, рудники, войска, технологии, герои, разведданные о противнике, недавние события)
Выход: строгий JSON — диагноз в одну фразу + распределение рабочих + что строить первым + позиция на эту минуту + чего не делать
│
▼ белый список + ограничение min/max + вето
Слой правил (Bot, каждый тик) Переводит советы в «смещения» уже имеющихся возможностей: распределение рабочих, приоритет постройки / найма, атакующая позиция
│
▼
Слой исполнения (SDK / рефлекторный слой) Отдаёт приказы, читает квитанции, микроконтроль
```
## Контракт вывода
Заставьте модель выдавать JSON с фиксированным набором полей — ничего не добавлять и не опускать:
```json
{
"diagnosis": "Одна фраза: главная проблема в партии; должна подтверждаться входными данными",
"workers": { "gold": 10, "lumber": 6 },
"priority": ["hpea", "hhou", "hbar"],
"posture": "creep",
"avoid": ["Не исследовать улучшение брони первым, когда не хватает древесины"]
}
```
| Поле | Как его использует слой правил | Ограничение в эталонном мозге |
|---|---|---|
| `workers` | Целевое число рабочих на золоте и на древесине | Золото 2 ~ 25, древесина 1 ~ 20; сумма не больше общего числа крестьян |
| `priority` | Порядок приоритетов для найма / постройки / исследований | Не больше 4; принимаются только четырёхсимвольные коды из таблицы «допустимых кодов» |
| `posture` | Позиция на эту минуту | Только одно из `attack` `defend` `creep` `expand` `recover` `hold` |
| `avoid` | Чего не делать в эту минуту | Не больше 2 пунктов |
| `diagnosis` | Только для логов и отображения в консоли | — |
Для каждой расы — свой промпт, и в нём только компромиссы, характерные для этой расы (совместное строительство и Ополчение у Людей, Норы у Орды, Haunted Gold Mine у Нежити, Entangled Gold Mine у Ночных эльфов…). Общие правила вынесите в общую часть — не копируйте их четыре раза.
## Четыре жёстких ограничения
Каждое из них эталонный мозг усвоил на собственных ошибках:
1. **Советник никогда не отдаёт приказы юнитам напрямую.** Он не видит происходящего в масштабе 150 ms и к тому же галлюцинирует. Он меняет только цели и приоритеты; кто куда идёт и кого атакует, по-прежнему решают слой правил и рефлекторный слой — у командования может быть только один хозяин.
2. **Асинхронность.** Один вызов советника занимает около 1 секунды и выполняется в фоновом потоке; действует последний результат, и он **никогда не блокирует тик**. Если модель не запущена, не уложилась в тайм-аут или ответила чепухой, ведите себя так, будто этого слоя нет, и возвращайтесь к чистым правилам. Слишком старый совет (старше 3 интервалов) тоже не используется.
3. **Белый список + ограничение значений.** Каждое поле должно отображаться на уже существующую возможность, а числа ограничиваются разумным диапазоном. Всё нераспознанное **подсчитывается и отбрасывается**, а не игнорируется молча.
4. **Считайте всё.** Сколько раз спросили, сколько успешных ответов, сколько тайм-аутов, сколько раз сработало ограничение, сколько раз было принято каждое поле — публикуйте всё это вместе с последним входом, отправленным модели. Иначе на вопрос «а этот слой вообще что-то даёт?» ответить невозможно.
> **Что умеет безопасно деградировать, легче всего деградирует незаметно**
>
> Советник устроен так, что «сбой = этого слоя нет», поэтому, когда сервис модели не запущен, бот ведёт себя в точности как на чистых правилах, и снаружи ничего не заметно. Однажды у эталонного мозга советник целый день не мог подключиться ни на одном из 6 экземпляров — и никто этого не заметил. Обязательно публикуйте «время последнего успешного вызова» и «причину последнего сбоя» — именно для этого в [консоли Farsight](https://war3ai.com/ru/docs/console/) есть страница «Стратегический советник».
## Реализация в вашем боте
Ниже — минимальный каркас, который работает с любым OpenAI-совместимым API (LM Studio, Ollama или облачный API) и использует только стандартную библиотеку:
```python title="coached_bot.py"
import collections, json, threading, urllib.request
from openwar3 import Bot
BASE = "http://127.0.0.1:1234/v1" # LM Studio / Ollama / любой OpenAI-совместимый сервис
MODEL = "your-model"
POSTURES = {"attack", "defend", "creep", "expand", "recover", "hold"}
SYSTEM = """Ты тренер по экономике в Warcraft III. Ты отвечаешь только за экономику и стратегию, не за микроконтроль.
Выводи только JSON с фиксированными полями: {"diagnosis": одна фраза, "workers": {"gold": целое, "lumber": целое},
"priority": [четырёхсимвольные коды, не больше 4, только из allowed], "posture": одно из шести, "avoid": [не больше 2]}
Опирайся только на переданные данные о партии; не выдумывай того, чего в данных нет."""
def ask(state: dict) -> dict:
body = {"model": MODEL, "temperature": 0.3, "max_tokens": 260,
"messages": [{"role": "system", "content": SYSTEM},
{"role": "user", "content": json.dumps(state, ensure_ascii=False)}]}
req = urllib.request.Request(f"{BASE}/chat/completions", json.dumps(body).encode(),
{"Content-Type": "application/json"})
with urllib.request.urlopen(req, timeout=8) as r:
text = json.load(r)["choices"][0]["message"]["content"]
return json.loads(text[text.index("{"): text.rindex("}") + 1])
class CoachedBot(Bot):
EVERY = 20.0 # игровые секунды: экономические решения живут в масштабе минут, спрашивать каждый тик не нужно
allowed = {"hpea", "hfoo", "hrif", "hkni", "hhou", "hbar", "hbla", "Rhme", "Rhar"}
def on_start(self, g):
self.plan, self.plan_at, self.asked_at, self.busy = {}, -1e9, -1e9, False
self.stats = collections.Counter()
def summary(self, g) -> dict: # снимок читаем в основном потоке; фоновый поток g не трогает
res = g.resources() or {}
return {"clock": round(g.clock() or 0), "gold": res.get("gold"), "lumber": res.get("lumber"),
"food": [res.get("food_used"), res.get("food_cap")],
"workers": len(g.my_workers()), "idle_workers": len(g.idle_workers()),
"army": collections.Counter(u.type for u in g.my_army()),
"enemy_seen": collections.Counter(u.type for u, _t, _age in g.last_seen(max_age=90)),
"night": g.is_night(), "allowed": sorted(self.allowed)}
def consult(self, state, now):
try:
p = ask(state)
self.stats["ok"] += 1
posture = p.get("posture")
if posture not in POSTURES:
self.stats["bad_posture"] += 1 # посчитать и отбросить, не молча
posture = "hold"
self.plan = {"gold": min(25, max(2, int(p["workers"]["gold"]))), # ограничение
"lumber": min(20, max(1, int(p["workers"]["lumber"]))),
"priority": [c for c in p.get("priority", []) if c in self.allowed][:4],
"posture": posture}
self.plan_at = now
except Exception as e: # тайм-аут / чепуха в ответе: этого слоя нет
self.stats[f"error:{type(e).__name__}"] += 1
finally:
self.busy = False
def on_tick(self, g):
now = g.clock() or 0.0
if not self.busy and now - self.asked_at >= self.EVERY:
self.busy, self.asked_at = True, now
threading.Thread(target=self.consult, args=(self.summary(g), now), daemon=True).start()
plan = self.plan if now - self.plan_at <= 3 * self.EVERY else {} # слишком старый совет не используем
# ↓ слой правил: при пустом plan — правила по умолчанию; с plan меняем только распределение, приоритеты и позицию, конкретные приказы по-прежнему решают правила
...
```
## Как выбрать модель
| Ситуация | Рекомендация |
|---|---|
| Локально, нужна скорость | MoE-модели (за один вызов активируется лишь малая часть параметров) намного быстрее плотных моделей того же размера. Эталонный мозг использует Qwen3.6-35B-A3B (LM Studio, Q4): медиана **1.09 s**, самый медленный ответ 1.45 s, 5/5 ответов напрямую разбираются через `json.loads` |
| Локальные «думающие» модели | **Обязательно отключите блок размышлений**, иначе все токены уйдут на размышления и ни одного JSON вы не получите. LM Studio игнорирует `/no_think`; эталонный мозг перешёл на `/v1/completions`, сам собирает ChatML и заранее подставляет пустой `` и `{` |
| Облачные модели | Задержка обычно выше, но эта схема слоёв асинхронна по своей природе; экономические решения измеряются минутами, так что несколько секунд задержки допустимы |
> **Примечание**
>
> Та же модель может озвучивать ваших юнитов: см. [Реплики и локальные модели](https://war3ai.com/ru/docs/speech/). Если хотите, чтобы модель сама отдавала приказы каждый тик (а не работала советником), дождитесь JSON-шлюза [Арены](https://war3ai.com/ru/arena/).
---
# LLM вызывает инструменты (MCP)
> tools/war3_mcp.py — это MCP-сервер. Подключите его к Claude Code, Claude Desktop или любому клиенту с поддержкой MCP, и LLM сможет сама смотреть на обстановку, отдавать команды, писать игроку на экране, задавать ему вопросы карточками и делать скриншоты — без заранее написанного кода.
`tools/war3_mcp.py` — это **MCP-сервер** (stdio). Подключите его к Claude Code, Claude Desktop, агентному фреймворку для локальных моделей — к любому клиенту с поддержкой MCP, — и LLM сможет **напрямую** смотреть на обстановку, отдавать команды, писать игроку на игровом экране, задавать ему вопросы и делать скриншоты, не написав заранее ни строчки кода.
Помимо написания ботов, роли советника и озвучки юнитов, это ещё один способ подключения: **LLM сама пользуется инструментами**.
## Подключение
```bash
claude mcp add war3 -- python <репозиторий>\tools\war3_mcp.py --inst 9 # Claude Code; <репозиторий> замените на свою папку openwar3
```
Для других клиентов конфигурация пишется в том же формате:
```json
{"mcpServers": {"war3": {"command": "python", "args": ["<репозиторий>\\tools\\war3_mcp.py", "--inst", "9"]}}}
```
К игре сервер подключается только при первом вызове инструмента, так что игру можно запустить и позже; если игру закрыть и запустить снова, следующий вызов переподключится автоматически. Через `--role` можно ограничить, что разрешено LLM:
| Роль | Что доступно |
|---|---|
| `dev` (по умолчанию) | Все инструменты, включая `war3_jass` |
| `player --player N` | Командовать только юнитами игрока N и видеть только его обзор (честный режим); без JASS |
| `observer` | Только чтение, рисовать на экране и заставлять юнитов говорить нельзя; рантайм сразу отклоняет его команды |
У роли `player` те же ограничения, что и в [шлюзе](https://war3ai.com/ru/docs/gateway/): нельзя завершить игру, сменить скорость или поставить паузу, недоступны методы, раскрывающие чужие карты, а запросы с номером игрока работают только для своего игрока.
Несколько лимитов: результат инструмента — не больше 200 000 символов, лишнее обрезается с подсказкой, как сузить запрос; `war3_ask_player` ждёт не дольше 120 секунд; `scale` у скриншота — от 0.1 до 1.
## Инструменты
| Инструмент | Что делает |
|---|---|
| `war3_overview` | Обстановка на одной странице: время, ресурсы, пища, число наших юнитов по типам, герои (здоровье, мана, уровень, перезарядка), видимые типы вражеских юнитов, производство. **Вызывайте первым** |
| `war3_units` | Список юнитов (`owner`: me / enemy / creep / all, фильтр `types`); `addr` нужен для команд |
| `war3_events` | Что произошло с прошлого вызова: гибель, повышение уровня, применение способностей, завершение производства, чат, нажатия игрока на кнопки… (по умолчанию без нескольких самых шумных видов) |
| `war3_call` | Вызвать любой публичный метод (`move`, `attack_move`, `train`, `build`, `cast`, `learn`, `ui.button`, `canvas.text`…); юнит задаётся как `{"unit": addr}` |
| `war3_api` | Поиск по API: по ключевому слову в именах и описаниях |
| `war3_toast` / `war3_say` | Строка текста вверху экрана / реплика над головой юнита |
| `war3_ask_player` | Показать игроку посреди экрана несколько карточек, дождаться клика и вернуть выбранный вариант (игру можно поставить на паузу) |
| `war3_screenshot` | Скриншот игры (PNG; снимается, даже если окно перекрыто, фокус не перехватывается) |
| `war3_jass` | Выполнить фрагмент JASS (только dev; менять мир — только в одиночной игре) |
Что с этим можно сделать:
- **Напарник / тренер**: `war3_overview` — посмотреть обстановку, `war3_toast` — дать совет на экране;
- **Спрашивать игрока по ходу игры**: `war3_ask_player` показывает три карточки, и LLM действует по той, которую выбрал игрок;
- **Комментирование**: `war3_events` — узнать, что произошло, `war3_say` — пусть юниты сами об этом расскажут;
- **Командовать отрядом напрямую**: роль `player` + `war3_call`, двигать можно только свои юниты;
- **Отлаживать интерфейс по картинке**: `war3_screenshot` — сделать снимок и проверить, правильно ли стоят нарисованные кнопки.
## Как примерно выглядит диалог
```text
Вы: Посмотри, что сейчас на карте, и спроси меня на экране: что дальше — экспансия, армия или улучшение ратуши?
→ war3_overview {}
← Обстановка: игровое время, золото 500, пища 10/12, наши htow 1 · hpea 5 · Hpal 1, врагов не видно, производства нет
→ war3_ask_player {"question": "Что дальше?", "options": ["Армия", "Экспансия", "Улучшение ратуши"], "pause": true}
← {"picked": 1, "option": "Экспансия"}
Модель: Вы выбрали экспансию. Сначала через war3_units найду свободного крестьянина, потом посмотрю, где ближайший золотой рудник…
```
## Замеры
2026-09-25:
- Собственный MCP-клиент на живом матче, 7/7: рукопожатие → список инструментов (10) → `war3_overview` (`htow` 1, `hpea` 5) → `war3_units` → `war3_toast` → `war3_screenshot` (PNG около 200 000 байт) → `war3_ask_player` (три карточки, имитация клика по второй → `{"picked": 1, "option": "开矿"}`, то есть «экспансия»).
- Реальное подключение к Claude Code 2.1: он сам запускает сервер и проходит рукопожатие, статус `connected`, все 10 инструментов появляются в его списке инструментов как `mcp__war3__*`.
## Реализация
- JSON-RPC 2.0 с разделением по строкам (`initialize` / `tools/list` / `tools/call` / `ping`), версия протокола 2025-06-18, совместимость с 2025-03-26 и 2024-11-05.
- Ошибки инструментов по правилам MCP возвращаются в результате (`isError: true`), соединение не рвётся.
- Со [шлюзом](https://war3ai.com/ru/docs/gateway/) общие белые списки ролей, формат аргумента-юнита и «обстановка на одной странице».
- Логи идут в stderr, в stdout — только протокол.
---
# Ментальная модель
> Снимок, команда, квитанция, событие, тик, пачка. Разберитесь в этих шести понятиях — и станет ясно, почему интерфейсы устроены именно так и как писать быстрый код.
## Снимок: чтение без ожидания
Каждые **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/) для каждого интерфейса указано, через какой уровень он работает.
---
# Пятнадцать правил
> Каждое выстрадано в реальных матчах. Сверяйтесь с ними, когда пишете бота, — это сэкономит большую часть времени на поиск ошибок.
> **Совет**
>
> Отдайте эту страницу LLM вместе с [`api.json`](https://war3ai.com/ru/api.json) — бот, которого она напишет, обойдёт множество ловушек.
## Чтение состояния
### 1. Нет данных — это `None`, а не 0
`resources()`, `time_of_day()`, `production()`, `cooldown()` могут вернуть `None` (идёт загрузка, у юнита нет подробных данных, здание ничего не производит…). Сначала проверьте, потом используйте:
```python
res = g.resources()
if res is None:
return
```
### 2. Узнавайте юнита по дескриптору, а не по адресу
Адреса переиспользуются: старый адрес может указывать на только что появившегося юнита. Чтобы помнить юнита между тиками, храните `u.handle` и находите его через `g.unit(handle)`.
### 3. Поток событий — глобальный
В `production.done` и `unit.died` попадают и события соперника, и крипов. Фильтруйте по `ev.owner` (или по дескриптору здания):
```python
if ev.kind == "production.done" and ev.owner == g.me():
...
```
### 4. Рабочих внутри рудника нет в снимке
Рабочий, зашедший в золотой рудник, в этот момент исчезает из снимка (`unit.removed`, а не гибель). Чтобы знать, сколько рабочих на каждом руднике, **ведите учёт сами** и не урезайте его по снимку — иначе будете отправлять лишних в уже заполненный рудник.
## Отдача команд
### 5. «Принята» в квитанции ≠ «выполнена»
Точку строительства в лесу движок тоже сразу принимает, а срывается всё, когда рабочий дойдёт до места; способность могут прервать. Результат смотрите по снимку и событиям: для строительства используйте `build_near` (он следит, появился ли фундамент), для способностей проверяйте по `g.cooldown()`, началась ли перезарядка.
### 6. По невидимым целям бить нельзя
Целевой приказ по врагу в тумане войны будет отклонён с кодом причины **1001**. Чтобы преследовать врага в тумане, сделайте `attack_move` в место, где его видели последний раз.
### 7. Приказывайте только свободным юнитам
Если каждый тик повторять юниту один и тот же приказ, вы его сбиваете: бойцы дёргаются на месте, а у крестьянина обнуляется цикл добычи. Свободен ли юнит, проверяйте через `g.order_of(u)` (он учитывает и то, что вы приказали в этом тике), а не через `u.order` из снимка (снимок ещё не догнал).
### 8. Shift умеет только «вставить после текущего»
В движке нет «добавить в конец»: если подряд отправить B и C с `queue='after'`, получится A, C, B. Чтобы пройти цепочку точек по порядку, используйте `g.path(units, список_точек)`, чтобы один рабочий построил несколько зданий подряд — `g.build_queue(worker, план)`: они вставляют команды в обратном порядке и всё сделают за вас.
### 9. Команды одного тика — одной пачкой
Если отправлять десятки команд по одной, придётся десятки раз ждать игровой поток; внутри `with g.batch():` — только один раз.
## Экономика и производство
### 10. Не больше 5 рабочих на рудник
Больше — доход не растёт. Целевое число рабочих зависит от числа рудников: по 5 на золоте на каждый рудник плюс несколько на лесе.
### 11. В очереди тренировки — только 1 юнит
Заполненная очередь из 7 мест замораживает деньги (в тесте главное здание поставило в очередь 4 крестьян, 300 золота оказались заперты, и старт сильно затянулся). Ставьте следующего, когда `g.queue(b)` опустеет.
### 12. Упёрлись в пищу — смотрите таблицу производства
`g.production(b).blocked` = в очереди что-то есть, но производство не начинается; чаще всего не хватает пищи. Это на шаг раньше, чем «строить, когда пища почти кончилась»: потеряли в бою пачку юнитов, начали добирать, очередь встала — вы сразу это видите.
### 13. Герой уникален; главное здание не улучшить, пока его очередь не пуста
- Погибшего героя можно только воскресить через `g.revive(алтарь)`, повторная тренировка будет отклонена (221); воскрешение тоже требует пищи (герой занимает 5).
- Пока в очереди главного здания что-то есть, улучшить его нельзя (код причины 185, «здание занято»).
## Время и пространство
### 14. На скорости 2× не ждите по настенным часам
Чтобы подождать 3 игровые секунды, дождитесь, пока `g.clock()` вырастет на 3, а не делайте `sleep(1.5)`. На повышенной скорости часы движка идут быстрее настенных.
### 15. На островных и лесных картах не мерьте по прямой
Выбирайте лагеря крипов и места для второй базы через `g.path_distance(a, b)` (A* по земле в обход леса, обрывов и зданий); если дойти нельзя, вернётся `None`. Ближайшая по прямой точка может оказаться за морем.
## И ещё одно: пишите под честный режим
С `--fair` видны только юниты, предметы, производство и события в пределах обзора — именно по этим правилам играют на Арене. Пишите под честный режим уже сейчас, и при переходе на [Арену](https://war3ai.com/ru/arena/) ничего менять не придётся. Подробнее — в разделе [Честный режим](https://war3ai.com/ru/docs/fair-mode/).
---
# Честный режим
> Клиент с внедрённым рантаймом видит всю карту. Честный режим оставляет боту только то, что в пределах обзора, — как у живого игрока и как по правилам Арены.
Возможности наблюдения в этом проекте держатся на том, что «клиент хранит состояние всех игроков»: в снимке есть все юниты карты, включая врагов в тумане войны. Для отладки это удобно, но для соревнований нечестно.
**Честный режим** заставляет SDK фильтровать данные по вашему обзору:
```bash
python tools/play.py --bot my_bot.py --fair
python -m openwar3 run my_bot.py --inst 5 --fair
```
```python
from openwar3 import Game, run
g = Game(inst=5, fair=True) # напрямую через Game
run(MyBot, inst=5, fair=True) # или через раннер
```
## Что фильтруется
| Данные | В честном режиме |
|---|---|
| Юниты | Все свои + вражеские и нейтральные, которые мы видим прямо сейчас |
| Предметы на земле | Только в пределах обзора наших юнитов (дневной и ночной обзор считаются отдельно по таблицам данных) |
| Таблица производства | Только видимые здания (что тренирует соперник, не видно) |
| События | Свои; видимые (или бывшие видимыми в течение последней секунды); урон, нанесённый нами |
## Откуда берётся обзор
- В снимке у каждого юнита есть **маска видимости**: бит p = игрок p видит его прямо сейчас (учитываются только игроки 0 ~ 11 с юнитами на карте; свои юниты для себя видимы всегда). `u.visible_to(g.me())` читает её напрямую, без ожидания.
- Произвольная точка: `g.visible(x, y)` спрашивает движок (видно / туман войны / чёрная маска) через быструю полосу, около кадра на вызов. Когда за тик нужно проверить много юнитов, используйте `u.visible_to()` из снимка, а не вызывайте `g.visible()` для каждого.
## Память о врагах: `last_seen`
Живой игрок помнит: «только что вон там была пачка налётчиков». SDK помнит за вас: при каждом обновлении снимка он записывает вражеских юнитов и крипов, которых мы видим в этот момент (последнюю позицию, здоровье, время); если видел их гибель — удаляет, при смене матча — очищает.
```python
for u, t, age in g.last_seen(max_age=60): # враги, замеченные за последние 60 игровых секунд
print(u.type, u.x, u.y, f"{age:.0f} с назад")
heroes = [r for r in g.last_seen() if r[0].is_hero] # где последний раз видели вражеских героев
camps = g.last_seen(owner="creep") # замеченные крипы
```
В честном режиме это ваш единственный «источник сведений о сопернике» — как у живого игрока. Обычный режим тоже запоминает по обзору, так что код можно использовать один и тот же.
## От имени какого игрока командовать
```bash
python tools/play.py --bot my_bot.py --player 1 --attach
```
`--player N` (или `Game(player=N)`) позволяет боту командовать от имени игрока N — и только юнитами игрока N. Бой двух ИИ — это два таких канала в одном матче.
> **В локальном режиме честность — договорённость, а не граница безопасности**
>
> На вашем компьютере ничем нельзя помешать программе читать всю карту. `--fair` — ограничение, которое вы накладываете на себя сами; настоящие соревнования обеспечивает процесс-судья [Арены](https://war3ai.com/ru/arena/): бот никогда не касается общей памяти, получает только наблюдения, отфильтрованные судьёй по обзору, может лишь отправлять действия, и у каждого действия сначала проверяется, кому принадлежит юнит.
## Почему стоит включить его уже сейчас
- На Арене правила будут именно такими: пишите под честный режим сейчас — и потом не придётся менять ни строчки;
- Только без информации обо всей карте становится ясно, насколько хорош ваш бот на самом деле (эталонный мозг сейчас сильно опирается на информацию обо всей карте, например на целевую точку компьютерного капитана, — это как раз хорошая проверка);
- Разведка, память и оценка обстановки, написанные под честный режим, — вот по-настоящему ценные способности ИИ.
---
# Книга рецептов: приёмы профи
> Преимущество сильных игроков по большей части складывается из десятков «мелких привычек». На этой странице типичные приёмы профессиональной игры по одному переложены на код SDK — каждый фрагмент можно сразу скопировать в on_tick.
Соглашения: `g` — это `Game`, `home` — наша главная база (`g.my_buildings({"htow", "hkee", "hcas"})[0]`), `now = g.clock()`. Подробности об интерфейсах — в [каталоге API](https://war3ai.com/ru/api/), полные рабочие примеры — в разделе [Примеры ботов](https://war3ai.com/ru/docs/examples/).
> **Совет**
>
> Когда просите LLM добавить какой-то приём, вставьте ему соответствующий рецепт вместе с кодом — это работает гораздо лучше, чем просьба «играть более профессионально».
## I. Экономика
### 1. Рабочие никогда не простаивают, по 5 на рудник
```python
for w in g.idle_workers(): # отправляем только простаивающих (повторный приказ занятым прервёт добычу)
mine = g.nearest([m for m in g.gold_mines() if crew[m.addr] < 5], w)
g.gather(w, mine) if mine else g.gather(w, g.trees(w.x, w.y, limit=1)[0])
```
Сколько рабочих отправлено на каждый рудник, считайте сами (`crew`): рабочие внутри золотого рудника в снимок не попадают. Полный пример — `hello_bot.py`.
### 2. В очереди только 1 юнит — деньги не замораживаются
```python
for b in g.my_buildings({"hbar"}):
if not g.queue(b): # следующего ставим, только когда очередь пуста
g.train(b, "hfoo")
```
### 3. Никогда не упираться в лимит пищи
```python
stuck = any(p.blocked for _b, p in g.all_production("me")) # в очереди есть, но не началось = не хватает пищи
res = g.resources()
if stuck or res["food_cap"] - res["food_used"] <= 6:
g.build_near(builder, "hhou", home.x, home.y)
```
`blocked` срабатывает на шаг раньше, чем «почти заполнено»: потеряли в бою пачку войск, начали доукомплектовываться — и как только очередь встала, вы сразу об этом узнаёте.
### 4. Порядок постройки + возврат на рудник после стройки (возврат через Shift)
```python
spot = g.build_near(w, "hbar", home.x, home.y)
if spot:
g.gather(w, mine, queue="after") # достроит и вернётся добывать — искать его в следующий тик не нужно
```
Один крестьянин строит несколько зданий подряд: `g.build_queue(w, [("hhou", x1, y1), ("hhou", x2, y2)])`. Деньги списываются только в момент начала стройки.
### 5. Когда улучшать ратушу, улучшения атаки/брони
```python
if not g.queue(hall) and g.can_do(hall, "hkee") in (0, 220): # улучшать можно, только когда очередь ратуши пуста (иначе 185)
g.upgrade(hall, "hkee")
p = g.production(hall) # прогресс улучшения ратуши
if p and p.kind == "upgrade":
print(f"Ратуше осталось {p.remaining:.0f} с")
for sm in g.my_buildings({"hbla"}):
if not g.queue(sm):
ok = [u for u, v in zip(UPS, g.can_do_many([(sm, u) for u in UPS])) if v in (0, 220)]
if ok:
g.research(sm, ok[0])
```
### 6. Экспансия: выбираем ближайший рудник по пешему расстоянию
```python
mines = [m for m in g.gold_mines() if g.dist(m, home) > 1500 and not taken(m)]
best = min(mines, key=lambda m: g.path_distance(home, m) or 1e9) # для рудника на острове вернётся None -> в конец списка
```
## II. Разведка и информация
### 7. Что делает противник
```python
for b, p in g.all_production("enemy"): # что нанимают / исследуют / улучшают видимые здания противника
print(b.type, p.kind, p.queue, f"{p.progress:.0%}" if p.progress is not None else "застряло")
```
Вместе с событиями: `ev.kind == "production.done" and ev.owner != g.me()` — что противник только что получил.
### 8. Помнить всё, что видели (туман войны)
```python
for u, t, age in g.last_seen(max_age=60): # враги, замеченные за последние 60 игровых секунд (последняя позиция и здоровье)
...
hero_seen = [r for r in g.last_seen() if r[0].is_hero] # где в последний раз был вражеский герой
```
В честном режиме это ваш единственный источник сведений о противнике — как и у живого игрока.
### 9. Куда собирается атаковать компьютер (только против компьютерного AI)
```python
plan = g.enemy_ai_plan(some_enemy_soldier) # куда направляется его компьютерный капитан
```
Компьютер выбирает точку атаки ещё до выхода из базы — заранее стяните туда войска.
## III. Крипинг
### 10. Крипинг ночью
```python
if g.is_night(): # с 18:00 до 6:00 крипы спят (можно ударить первым и не попасть в окружение), обзор у всех короче
...
wait = g.seconds_until(18) # сколько игровых секунд до темноты (сутки = 480 с)
```
### 11. Нападать только на лагеря, которые вам по силам
```python
from openwar3 import combat
mine = [g.stats(u) for u in army]
def ttk(target): return combat.time_to_kill(mine, g.stats(target), target_hp=target.hp) or 1e9
camp = [c for c in g.creeps() if g.dist(c, center) < 600]
ours = max(ttk(c) for c in camp) # сколько времени уйдёт на весь лагерь (грубо)
theirs = g.time_to_kill(camp, weakest_of_mine) or 1e9 # за сколько они убьют нашего самого слабого
if ours < theirs and g.reachable(center, camp[0]):
g.attack_move(army, camp[0].x, camp[0].y)
```
Полный пример: `_maybe_creep` в `micro_bot.py`.
## IV. Микроконтроль
### 12. Фокус огня: бить того, кто умрёт быстрее всех, а не ближайшего
```python
target = min(visible_enemies, key=lambda e: g.time_to_kill(fighters, e) or 1e9)
g.attack([u for u in fighters if (g.current_target(u) or target).handle != target.handle], target)
```
Приказ отдаётся только тем, кто «ещё не бьёт эту цель» (`current_target`) — не прерывайте тех, кто уже атакует.
### 13. Отводить раненых
```python
for u in army:
if u.hp < u.hp_max * 0.35:
g.move(u, *toward(home, u, 500)) # отойти на 500 в сторону базы; не отводить повторно в течение 3 секунд
```
Как понять, что юнит под фокусом: в событиях `damage` один и тот же юнит за короткое время получает урон от нескольких источников = его окружили.
### 14. Берегите героев, не дарите опыт
```python
for h in g.my_heroes():
if h.hp < h.hp_max * 0.4:
g.move(h, home.x, home.y)
g.use_item(h, slot_of(h, "phea")) # зелье лечения: номер ячейки ищите через inventory(h)
```
### 15. Контры: нужные войска — по нужным целям
```python
s = g.stats(u)
best = max(enemies, key=lambda e: s.dps_vs(g.stats(e))) # Стрелки по всадникам на грифонах (колющий по лёгкой броне ×2), грифоны по пехоте (магический по тяжёлой броне ×2)
```
Таблица контр берётся из игровых данных: `combat.damage_multiplier("pierce", "small") == 2.0`.
### 16. Окружение и маршруты: обходим башни
```python
route = g.walk_path(army_center, target) # точки поворота кратчайшего наземного пути
g.path(army, route, attack=True) # атака с движением через каждую точку по порядку
```
Чтобы обойти башни, пометьте область вокруг них в сетке поиска пути как непроходимую и посчитайте маршрут заново:
```python
grid = g.grid().copy()
for t in towers:
grid.block_area(t.x, t.y, 800) # дальность башни 700 + запас
route = grid.path((army_x, army_y), (target.x, target.y))
```
### 17. Команды одного тика — одной пачкой
```python
with g.batch():
g.attack(melee, target_a)
g.attack(ranged, target_b)
g.move(wounded, *home_xy)
g.cast(hero, "thunderclap")
```
Десятки команд ждут игровой поток всего один раз (измерено: 8 команд движения — 68 ms → 6.5 ms).
### 18. Осада: артиллерия бьёт по земле
```python
g.attack_ground(mortars, tower.x, tower.y) # мортиры / катапульты стреляют по участку земли (за лесом, по невидимым юнитам)
```
## V. Герои
### 19. Порядок прокачки
```python
SKILLS = {"Hamg": ["AHwe", "AHbz", "AHwe", "AHbz", "AHwe", "AHmt"]} # Элементаль воды, Снежная буря… на 6-м уровне ульта — Массовая телепортация
info = g.hero_info(h)
if info and info["skill_points"]:
g.learn(h, SKILLS[h.type][learned_count]) # отказ (уровня не хватает для ульты) — ждём следующего уровня
```
### 20. Сработало ли заклинание
```python
r = g.cast(h, "thunderbolt", target=enemy_hero)
# в следующий тик:
if g.cooldown(h, "AHtb"): # ушло на перезарядку = действительно применено; «принята» ≠ «применено»
...
```
### 21. Покупка зелий, возврат на базу
```python
g.buy(shop, "phea") # герой стоит рядом с магазином
g.use_item(hero, slot, x=home.x, y=home.y) # свиток телепортации на базу (предмет, применяемый в точку)
```
## VI. Разбор партии
- Каждый тик пишите решения в лог (`print` попадает в окно запуска), а вместе с `g.say(юнит, "Отходим")` смотрите их прямо в игре;
- событие `production.done` содержит «сколько секунд заняло» — соберите свою хронологию построек (на какой секунде первый герой, на какой секунде улучшение ратуши) и сравните с сильными игроками;
- пусть агент сам разбирает партии: см. отчёт о партии в разделе [Автономные итерации агента](https://war3ai.com/ru/docs/agent-loop/).
---
# Примеры ботов
> Четыре примера от простого к сложному: каждый запускается как есть, каждый фрагмент логики соответствует одной возможности SDK. Плюс полноценный эталонный мозг.
Все примеры лежат в `brains/examples/`; каждый следующий наследует предыдущий и добавляет только новое. Читать лучше по порядку:
| Пример | Чему учит | Запуск |
|---|---|---|
| `hello_bot.py` | Добыча (5 рабочих на рудник, рудник заполнен — на лес), найм рабочих (в очереди только 1), постройка зданий для пищи, достройка брошенных фундаментов; работает за все четыре расы | `python tools/play.py --bot brains/examples/hello_bot.py` |
| `rush_bot.py` | Казарма и алтарь (если их нет — строятся через `build_near`), сначала герой (погиб — воскрешение), очки навыков тратятся сразу, накопили волну — атака с движением (attack-move) | `… --bot brains/examples/rush_bot.py` |
| `macro_bot.py` | Порядок постройки + после стройки рабочий сам возвращается на рудник (Shift), упёрлись в пищу — сразу строить, в очереди казармы 1 юнит, улучшения атаки и брони, переход на следующий тир и продвинутые войска, цель выбирается по **реальному пути по земле**, движение по маршруту | `… --bot brains/examples/macro_bot.py --speed 200` |
| `micro_bot.py` | Поверх экономики берёт на себя бой: фокус на том, кого можно убить быстрее всех, отвод раненых, спасение героя, ночью — лагеря крипов по силам, возврат на защиту, когда враг у базы; команды одного тика уходят одной пачкой | `… --bot brains/examples/micro_bot.py --fair` |
> **Примечание**
>
> В комментариях `hello_bot` и `rush_bot` записаны грабли, на которые наступили в реальных играх, например: «строить ферму каждый раз посылали первого же рабочего — в итоге все 3 фермы остались недостроенными фундаментами», «жёстко заданные координаты казармы пришлись ровно на лес — за 3 минуты не построено ни одной». Комментарии дают больше, чем сам код.
## hello_bot: экономика
```python
# раса -> (рабочий, главные здания, здание для пищи)
RACES = {
"h": ("hpea", {"htow", "hkee", "hcas"}, "hhou"),
"o": ("opeo", {"ogre", "ostr", "ofrt"}, "otrb"),
"u": ("uaco", {"unpl", "unp1", "unp2"}, "uzig"),
"e": ("ewsp", {"etol", "etoa", "etoe"}, "emow"),
}
MINE_CAP = 5 # не больше 5 рабочих на рудник (больше — доход не растёт)
LUMBER_CREW = 5 # лесорубы: 5 на золото с каждого рудника + столько на лес = целевое число рабочих
```
Три задачи: свободных рабочих — на золото (бот сам ведёт учёт, сколько рабочих на каждом руднике; рудник заполнен — на лес); рабочих не хватает — нанимать (в очереди только 1); пища почти кончилась — найти рабочего, который сейчас ничего не строит, и поставить рядом с главным зданием здание для пищи (у Людей и Орды бот ещё и отправляет рабочих достраивать брошенные фундаменты).
## rush_bot: армия и атака
Поверх `hello_bot` добавляются три вещи: нет казармы и алтаря — построить; в алтаре нанять героя (**погиб — сначала воскресить**, герой уникален), есть очки навыков — изучать; накопилось 8 бойцов — всей армией attack-move к вражескому главному зданию, потрепали — домой копить заново. Приказы отдаются только свободным бойцам, чтобы не сбивать бой каждый тик.
## macro_bot: основы макро
```python
TECH = {
"h": dict(order=["halt", "hbar", "hbla", "hlum"], altar="halt", hero="Hamg", skills=["AHwe", "AHbz", "AHab"],
barracks="hbar", soldiers=["hfoo", "hrif", "hkni"], smith="hbla", upgrades=["Rhme", "Rhar", "Rhra", "Rhla"],
tiers=["hkee", "hcas"]),
...
}
```
То, что профи делает каждую игру, — и каждому пункту соответствует возможность SDK: таблица порядка постройки + `gather(..., queue="after")`, чтобы после стройки вернуться на рудник; `production().blocked` показывает, что упёрлись в пищу; `g.queue` гарантирует, что в очереди казармы всего 1 юнит; `can_do` спрашивает у движка, можно ли исследовать следующий уровень атаки или брони; переход на следующие тиры и продвинутые войска (урок из реальной игры: бот застрял на первом тире и на 23-й минуте был снесён рыцарями и грифонами соперника с третьего тира); цель выбирается по `path_distance`, движение по точкам поворота — через `path()`.
## micro_bot: когда начинается бой
```python
def _fight(self, g, army, foes, home, now):
...
visible = [e for e in foes if e.visible_to(me)] # невидимую цель движок отклонит (1001)
atk = [s for s in (g.stats(u) for u in fighters) if s]
target = min(visible, key=lambda e: _ttk(g, atk, e)) # того, кого быстрее убить, а не ближайшего
idle_or_other = [u for u in fighters if g.current_target(u) is None
or g.current_target(u).handle != target.handle]
if idle_or_other:
g.attack(idle_or_other, target)
```
В реальной игре: 5 минут, 1497 тиков, 3023 команды, 0 ошибок.
## Эталонный мозг: полноценный ИИ
`brains/xwar3/` — полноценный ИИ, который ставит вторую базу, крипует и атакует. Он состоит из трёх слоёв:
| Слой | Где | Ритм | Что делает |
|---|---|---|---|
| Стратегический слой | `strategy/` | секунды | Выбор и смена стратегий в духе AMAI, таблицы постройки, контр-юниты, выбор героев; опционально — [LLM-советник по стратегии](https://war3ai.com/ru/docs/llm-coach/) |
| Рефлекторный слой | `reflex/` (4 отдельных процесса) | ~100 ms | Спасение юнитов, применение способностей, фокус огня, подбор предметов |
| Модель шансов на победу | `worldmodel/` | — | Выиграем ли бой (подмножество для инференса) |
Несколько процессов делят юнитов через **таблицу захватов** и по приоритету решают, чьё слово главное: ручное управление 95 > спасение 90 > уклонение от способностей 85 > применение способностей 80 > подбор предметов 70 > … > стратегия 50 > распределение рабочих 45. Ваш собственный бот записан в таблице как `bot` с приоритетом 50 по умолчанию.
> **Внимание**
>
> Эталонный мозг напрямую использует низкоуровневую часть SDK (`w3cmd` / `act`) и сильно опирается на информацию обо всей карте. Он хорош как источник идей, но давать LLM копировать его один в один не стоит. Ему нужны данные AMAI: при первом развёртывании `start.bat` скачивает их из публичного репозитория AMAI и генерирует нужные файлы (у AMAI собственная лицензия, сгенерированные файлы в git не попадают; если не получилось, повторите через `start.bat setup`).
Проще всего запустить эталонный мозг через [консоль Farsight](https://war3ai.com/ru/docs/console/): на странице «Экземпляры и запуск» отметьте номер экземпляра и нажмите «Начать тест».
---
# Отладка и производительность
> Почему тик медленный, почему команда не сработала, почему игра стоит. Ищите по симптомам, а затем подтверждайте встроенными скриптами проверки в реальной игре.
## Смотрите на квитанцию
Квитанция каждой команды — информация из первых рук:
```python
r = g.cast(hero, "blizzard", x=tx, y=ty)
if not r:
print(r.reason, r.verdict) # rejected(…) и код причины
print(r.exec_us, r.engine_us) # сколько микросекунд команда выполнялась в игровом потоке / сколько из них заняла сама функция приказа движка
```
Обычно команда занимает в игровом потоке от нескольких до нескольких сотен микросекунд. После завершения блока пачки в `g.last_receipts` лежат квитанции всех команд этой пачки.
## Смотрите прямо в игре
```python
g.say(unit, "Отходим") # над юнитом появляется облачко чата (на игру не влияет)
g.message("Начинаем крипинг") # строка в области сообщений внизу слева (видна только на этом компьютере)
```
Вывод `print` появляется в терминале, где запущен бот. Печатайте ключевые решения каждого тика и добавьте облачка над юнитами — так разобраться гораздо быстрее, чем читая код.
## Тик слишком медленный
Сначала проверьте, не ваш ли это случай:
| Причина | Решение |
|---|---|
| Команды отправляются по одной, и каждая ждёт кадр | Оберните их в `with g.batch():` — десятки команд ждут только один раз |
| `g.visible()` / `g.can_do()` вызываются по одному (каждый вызов идёт через быструю полосу и ждёт кадр) | Видимость берите из снимка через `u.visible_to()`; выполнимость спрашивайте пачкой через `g.can_do_many([...])` |
| `sleep` или ожидание внутри `on_tick` | Запомните игровое время и проверьте условие на следующем тике |
| Дорогие вычисления каждый тик (поиск пути, сканирование всей карты) | Кэшируйте результат и пересчитывайте раз в несколько тиков. У `g.grid()` встроенный кэш на 2 секунды, уровни технологий в `g.stats()` кэшируются на 5 секунд |
## Игра стоит / бот не дожидается начала матча
| Симптом | Скорее всего |
|---|---|
| Бесконечное «ожидание матча» | Неверный номер экземпляра; или окно игры **свёрнуто** — пока оно свёрнуто, симуляция стоит (часы не идут) |
| Игра идёт, а на приказы бота нет реакции | Приказы идут чужим юнитам (квитанция `not_owner`); или бот подключён как observer (`forbidden`) |
| Команды возвращают `held` | Юнита держит слой с более высоким приоритетом (рефлекторный слой эталонного мозга, ручной приказ из консоли), команда не отправлена |
| На паузе команды всё равно проходят | Это нормально: на паузе часы движка стоят, но диспетчеризация событий работает и команды выполняются как обычно |
## Подключиться и посмотреть статус
```bash
python -m openwar3 status --inst 5
```
Выводит состояние подключения: pid игры, период публикации мира и время каждого сбора, счётчики быстрой полосы, идёт ли матч, число юнитов, игровые часы.
## Скрипты проверки в реальной игре
Запустите тестовый экземпляр и по пунктам проверьте, что возможности SDK работают на вашей машине:
```bash
python tools/sdk_live_check.py --inst 20 # всё
python tools/sdk_live_check.py --inst 20 --only prod # только один раздел
```
Разделы: пачки, время, производство, очередь приказов, боевые характеристики, поиск пути, честный режим. Каждый раздел отдаёт команды в настоящем матче, считывает результат и печатает число пройденных проверок.
Для офлайн-тестов игра не нужна:
```bash
python tools/run_tests.py
```
## Частые «вроде бы баги»
- **Квитанция на строительство — «принята», а фундамента всё нет**: точку в лесу движок тоже сразу принимает, а срывается всё, когда рабочий дойдёт до места. Используйте `build_near`: он следит за результатом и на время заносит неудачные точки в чёрный список.
- **Квитанция на способность — «принята», а способность не применилась**: её прервали или не хватило маны. На следующем тике после применения проверьте по `g.cooldown()`, началась ли перезарядка.
- **Приказ атаки принят, а бойцы бьют кого-то другого**: для атаки конкретной цели используйте `g.attack(боец, враг)` (семантика правого клика). Сырой приказ атаки на цель меняет только приказ, но не запоминает цель — и юнит пойдёт бить кого-то рядом.
- **Не сходится число рабочих**: рабочих внутри рудника нет в снимке.
- **Погибшего героя не удаётся натренировать**: герой уникален, нужен `g.revive(алтарь)`; воскрешение требует пищи и доступно лишь примерно через 3 игровые секунды после гибели.
---
# RPG-компаньон
> ИИ-компаньон для игрока в RPG и пользовательских картах: ходит за вами, помогает бить крипов, лечит, когда у вас мало здоровья, и разговаривает с вами. Четыре режима; унаследуйте один класс, поменяйте пару атрибутов — и компаньон уже ваш.
Не только для обычных матчей. В RPG и пользовательских картах можно завести себе **ИИ-компаньона**: он ходит за вами, помогает бить крипов, лечит, когда у вас мало здоровья, а когда делать нечего — перекидывается с вами парой фраз. Реплики можно брать даже из локальной LLM.
**Как этим пользоваться — решаете вы.** Всё разбито на три слоя API, снизу вверх, и любой из них можно использовать напрямую:
| Слой | Что это | Для чего |
|---|---|---|
| **JASS-канал** `g.jass` | Все функции JASS, доступные авторам карт (их 1291), вызываются напрямую по имени (создать юнита, настроить союз, выдать предмет, переименовать, показать текст, воскресить героя…) | Придумать свой геймплей |
| **Удобные методы** | `g.spawn`, `g.set_alliance`, `g.player_slots`, `g.show_text`, `g.map_data`: частые задачи уже обёрнуты | Писать свои вспомогательные скрипты |
| **Фреймворк компаньона** | `openwar3.companion.Companion` + `openwar3.talk.Talk`: унаследуйте класс, поменяйте пару атрибутов — и у вас компаньон, который ходит следом, помогает в бою, лечит и болтает | Просто нужен компаньон |
> **Внимание**
>
> Только для **одиночных игр и игр по локальной сети, созданных вами**. Создание юнитов, настройка союзов и тому подобное — это одностороннее изменение мира на вашем компьютере: в одиночной игре (против компьютера) это нормально, а в многопользовательской у остальных игроков начнётся рассинхронизация. Поэтому в многопользовательской игре JASS-канал пропускает только функции для чтения, а компаньон автоматически переходит в режим «только разговор».
## Быстрый старт: в Farsight в один клик
1. **Выберите карту**: в Farsight откройте страницу «Экземпляры и запуск» → «Следующий матч» → карта и выберите RPG-карту (в списке всё, что лежит в `Scenario` и `Download` папки `Maps` в каталоге игры, например `(4)WarChasers`).
2. **Выберите схему**: в выпадающем списке «ИИ-схема» на карточке экземпляра выберите **Пример компаньона (buddy)** → «Выбрать».
3. **«Начать тест»**: когда игра запустится, **играйте сами в окне игры**. Компаньон — паладин по имени «Светик» — появится рядом с вами.
Можно и из командной строки:
```bash
python tools/play.py --bot brains/examples/buddy.py --inst 20 --rpg --map "<каталог игры>\Maps\Scenario\(4)WarChasers.w3m"
```
`--rpg` (в манифесте схемы — `"judge": false`) означает, что победа и поражение не определяются по правилам обычного матча: в RPG герои воскресают, и правила «не осталось зданий — проиграл» там тоже нет. Многие RPG-карты после загрузки останавливаются на экране «Нажмите любую клавишу, чтобы продолжить»; когда SDK видит, что «матч идёт, а игровые часы всё ещё на нуле», он сам нажимает пробел (`g.press_to_continue()` — только отправляет окну игры сообщение о нажатии клавиши и не перехватывает фокус).
## Пишем своего компаньона
```python
from openwar3.companion import Companion
from openwar3.talk import Talk
class MyBuddy(Companion):
mode = "ally" # режим, см. таблицу ниже
unit = "Hpal" # кого создать: любой четырёхсимвольный код, в том числе заданный картой
nickname = "Светик"
heal = ("holybolt", "AHhb", 0.55) # (приказ заклинания, способность для изучения, лечить, когда здоровье хозяина ниже этой доли); None = не лечить
follow_distance = 350
talk = Talk(persona="Жизнерадостный юный паладин, любит подбадривать хозяина")
```
### Четыре режима
| mode | Кто компаньон | Пояснение |
|---|---|---|
| `ally` (по умолчанию) | Занимает свободный слот игрока и становится вашим **союзником** | Свой цвет и имя (в таблице счёта и на панели союзников показывается `nickname`); командовать им вы не можете, он воюет сам. Союз и общий обзор фреймворк настраивает автоматически |
| `own` | Создаётся **под вашим управлением** | Вы в любой момент можете командовать им вручную; когда вы им не занимаетесь, за вас им управляет ИИ |
| `adopt` | Берёт под контроль юнита, **который уже есть** на карте | Переопределите `adopt(g)`, чтобы он возвращал этого юнита (питомца или спутника, которого вам даёт карта) |
| `voice` | Юнита не создаёт, **только говорит** | Болтовня и напоминания; мир не меняет, поэтому работает и в многопользовательской игре |
Если свободного слота нет, `ally` автоматически переходит в `own`; в многопользовательской игре или если юнита создать не удалось — в `voice`.
> **Примечание**
>
> Компаньон в режиме `ally` ориентируется на «лучшего юнита в этом слоте прямо сейчас» (герои в приоритете), а не держится за одного конкретного юнита. В реальной проверке одна карта приняла компаньона за живого игрока, удалила паладина и выдала ему героя карты — компаньон просто взял этого героя под контроль и изучил способности, которые ему задала карта. Если герой погибает, компаньон по возможности воскрешает его на месте; если карта воскрешает героя сама — продолжает им пользоваться.
### Что он делает каждый тик
Проверяет условия по порядку и выполняет первое сработавшее:
| Порядок | Действие | Условие | Настройка |
|---|---|---|---|
| 1 | Отступление | Своё здоровье ниже 25% и рядом враги: отойти за спину хозяина | `retreat_at` |
| 2 | Лечение | Здоровье хозяина ниже порога, способность перезарядилась, расстояние не больше 900 | `heal` (None — выключить) |
| 3 | Помощь в бою | Рядом с хозяином враги: **кто бьёт хозяина > кого бьёт хозяин > ближайший** | `assist_radius` или переопределить `pick_target` |
| 4 | Следование | Слишком далеко от хозяина — догнать; если совсем далеко — бежать обратно, не ввязываясь в бой | `follow_distance`, `leash` |
| 5 | Болтовня | Когда врагов нет — одна фраза раз в 1 ~ 2.5 минуты | Таблица реплик |
«Враги» определяются по союзным отношениям в игре (обновляются раз в 20 секунд). В RPG-картах часто несколько союзных игроков, поэтому нельзя просто считать врагами всех игроков, кроме себя.
Переопределяемые хуки: `find_master` (кто хозяин; по умолчанию герой самого высокого уровня у локального игрока), `adopt`, `pick_target`, `on_poke` (хозяин щёлкнул по компаньону правой кнопкой), а также методы бота `on_start` / `on_tick` / `on_event` / `on_end`. Сколько раз компаньон лечил, помогал в бою, убивал, следовал, отступал, говорил и воскрешал, записывается в `self.stats` и выводится в конце.
### Как к нему обратиться
- **Команды чата**: наберите в чате `-follow` (за мной), `-stay` (стой здесь), `-heal` (вылечи сейчас) или `-hi` (поздороваться). Список команд задаётся в `commands`, реакции меняются переопределением `on_command`.
- **Правый клик по компаньону**: вызывает `on_poke`. В примере реакция такая: если у хозяина не полное здоровье — подлечить, иначе сказать что-нибудь.
- **Диалог с портретом**: приветствие, гибель хозяина, повышение уровня хозяина и возвращение компаньона звучат через встроенный в игру диалог с портретом (портрет внизу меняется на компаньона, на экране появляются субтитры); остальное — облачком над головой.
- **Панель состояния**: панель в левой части экрана с полоской здоровья компаньона, тем, чем он сейчас занят, настроением (радость / азарт / тревога / страх / грусть), числом убийств и лечений. Она рисуется на [холсте](https://war3ai.com/ru/docs/canvas/), поэтому безопасна и в многопользовательской игре.
### Реплики и локальные LLM
`Talk` подбирает реплики по событиям и показывает их облачком над головой; в режиме `voice` или когда облачко показать нельзя — в левом нижнем углу экрана. Каждая реплика также пишется в лог схемы, так что потом можно посмотреть, что он говорил.
| Событие | Когда | Событие | Когда |
|---|---|---|---|
| `hello` | Только появился | `master_low` | У хозяина мало здоровья |
| `poke` | Хозяин щёлкнул по нему правой кнопкой | `master_levelup` | Хозяин повысил уровень |
| `fight` | Начался бой | `master_died` / `master_back` | Хозяин пал / воскрес |
| `kill` | Убил крипа (называет его имя) | `buddy_low` / `buddy_died` / `buddy_back` | У самого компаньона мало здоровья / он пал / вернулся |
| `healed` | Подлечил хозяина | `idle` / `item` | Болтовня / подобрал предмет |
В репликах можно использовать подстановки `{master}`, `{me}`, `{map}`, `{enemy}`, `{level}`, `{item}`; сами реплики правятся прямо в `talk.lines`, перезарядка — в `talk.cooldown`.
**Подключение локальной LLM**: `Talk(llm=LocalLLM(url, model))`, подходит любой OpenAI-совместимый API (LM Studio, Ollama…). Модель отвечает в фоновом потоке, и реплика звучит, только когда ответ пришёл; если модель не запущена, не уложилась по времени или вернула ошибку, звучит заготовленная реплика — игра не подвисает. Запросы уходят только на указанный вами локальный адрес, а содержат они то, что произошло в игре (как зовут хозяина, каких крипов он убил).
## Имена юнитов в пользовательских картах
Юниты, предметы и герои RPG-карт в основном созданы самой картой (четырёхсимвольные коды вроде `HC07`, `I00A`), и во встроенной таблице имён их нет. `g.map_data` читает файл карты текущего матча напрямую:
```python
md = g.map_data
md.name_of("HC07") # 'Optimus Primo' — приоритет у имён, изменённых картой
md.hero_names("HC07") # список собственных имён героя
md.hero_skills("OC10") # способности, которые карта задала этому герою
md.tooltip("I00A") # текст подсказки
```
Защищённые и оптимизированные карты (а таковы многие популярные RPG) не содержат стандартных файлов данных объектов — тогда имена читаются из текстовых данных карты. В проверке на этом компьютере все 38 RPG- и пользовательских карт разобраны успешно, в 37 из них найдены имена юнитов.
## Поделиться как схемой
Компаньон — это обычный подкласс `openwar3.Bot`, так что его можно оформить как [ИИ-схему](https://war3ai.com/ru/docs/schemes/) и поделиться с другими. В манифест добавляются ещё два поля:
```json
{"id": "my-buddy", "name": "Мой компаньон", "entry": "my_buddy.py", "fair": false, "judge": false}
```
`"fair": false` — нужно, чтобы пользоваться JASS-каналом (создавать юнитов, настраивать союзы); `"judge": false` — не определять победу и поражение по правилам обычного матча.
## Результаты проверки
2026-09-24, тестовый экземпляр, карта WarChasers, скорость 2×:
- Все 18 проверок JASS-канала пройдены: слоты игроков, преобразование юнитов в дескрипторы и обратно, вещественные возвращаемые значения, строковые аргументы, создание юнита в свободном слоте, настройка союза, переименование, удаление юнита; вызов из полосы игрока и неверное число аргументов корректно отклоняются.
- Компаньон: сам прошёл экран «Нажмите любую клавишу» → появился рядом с хозяином и поздоровался → вслед за хозяином зашёл в круг выбора героя, получил от карты героя и взял его под контроль → следовал за хозяином (на расстоянии 200 ~ 400) → бил крипов, после убийства сказал «Красиво!» → при малом запасе здоровья отступил → погиб, был воскрешён картой и продолжил следовать за хозяином.
## Что ещё не сделано
1. **Произвольный текст, который игрок пишет в чат, прочитать нельзя.** Фиксированные команды чата уже работают; чтобы компаньон по-настоящему свободно болтал с вами, нужно получать сам текст.
2. **Компаньон не понимает геймплей конкретной карты** (задания, магазины, сюжет). Он умеет общее: следовать, помогать в бою, лечить. Чтобы он понимал конкретную карту, опишите её в своём подклассе — `g.map_data` подскажет имена, `g.jass` вызовет любую функцию. Именно эта часть оставлена на ваше усмотрение.
---
# Холст
> Текстовые блоки, панели, полоски прогресса, изображения, круги на земле и маршруты со стрелками поверх игровой картинки. Рантайм сам рисует их каждый кадр и не меняет состояние игры, поэтому это безопасно и в многопользовательской игре; работать можно через Python, HTTP или напрямую через общую память.
Внешняя программа может рисовать поверх игровой картинки **текстовые блоки, панели, полоски прогресса, изображения, круги на земле и маршруты по земле (со стрелкой)** — рантайм сам отрисовывает их каждый кадр. Подходит для собственного HUD, вспомогательных линий, подсказок, учебных пометок, информационных плашек для стрима.
## Холст или визуальные функции JASS
| | Холст (эта страница) | [Визуальные функции JASS](https://war3ai.com/ru/docs/jass/) |
|---|---|---|
| Кто рисует | Сам рантайм | Сама игра (всплывающий текст, эффекты, панели, диалог с портретом…) |
| Многопользовательская игра | **Безопасно**: рисуется только на вашем экране, игровые объекты не создаются, состояние игры не меняется | Только одиночная игра |
| Оформление | Свободное: любые шрифты (включая китайские), скруглённые углы, полупрозрачность, рамки, любые цвета, локальные изображения | Родной стиль игры |
| Привязка | К юниту, к координатам мира, к позиции на экране; круги на земле повторяют рельеф | Зависит от функции |
| Затраты | Замер: 0.2 ~ 0.35 ms за кадр (9 элементов) | Около 13 ms на вызов |
Оба пути можно совмещать: эффекты в родном стиле игры — через JASS, собственные панели, вспомогательные линии и подсказки — через холст.
## Python
```python
c = g.canvas # при первом обращении рантайм ставит хук отрисовки (около 0.1 s)
c.text("title", "Привет, это холст", screen=(40, 110), color=(255, 220, 80),
bg=(0, 0, 0, 170), border="#C49C40", size=22, bold=True)
c.panel("status", "Компаньон · Светик", ["Настроение: радость", "Убийств: 12"], screen=(16, 330))
c.bar("hp", 0.62, unit=hero, lift=260, width=100, height=12, color=(80, 220, 80), text="62%") # следует за юнитом
c.text("tag", "Босс готовит ульту!", unit=boss, lift=320, color=(255, 80, 80), size=22, bold=True)
c.circle("danger", (x, y), 300, color=(255, 60, 60), fill=(255, 60, 60, 60), width=3) # опасная зона на земле
c.circle("aura", hero, 450, color=(80, 200, 255, 220)) # круг, следующий за юнитом
c.path("route", [(x1, y1), (x2, y2), (x3, y3)], color=(255, 220, 0), width=5, arrow=True)
c.image("icon", "icon.png", screen=(40, 170), width=64, height=64)
c.remove("danger"); c.hide("tag"); c.clear() # clear убирает только то, что нарисовали вы
c.expire("tag", 5) # исчезнет сам через 5 s
with c.batch(): ... # много изменений разом — одна запись в общую память
c.stats() # drawnFrames растёт = отрисовка действительно идёт
```
Каждый элемент обозначается ключом `key`: повторная отрисовка с тем же key — это обновление.
**Кликабельность**: добавьте текстовому блоку или панели `clickable=True` (цвет при наведении задаётся через `hover=`) — при клике в поток событий приходит `ui.click`, `ev.key` равен этому key, а сам клик по элементу до игры не доходит. Готовые кнопки, карточки выбора, горячие клавиши и клики по земле — в разделе [Интерфейс и ввод](https://war3ai.com/ru/docs/ui-input/).
**Позиция** (у каждого элемента одна):
- `screen=(x, y)`: пиксели экрана, отрицательные значения отсчитываются от правого / нижнего края; `center=True` — выравнивание по центру;
- `frac=(0.5, 0.1)`: доли экрана;
- `world=(x, y)`: координаты мира;
- `unit=юнит`: следовать за юнитом. Текст и полоски в мире и над юнитом привязываются к точке серединой нижнего края, `lift` поднимает их выше.
Элементы в мире и над юнитами по умолчанию не заходят на нижнюю панель управления и шар дня и ночи вверху (`over_ui=True` — рисовать поверх них). **Цвет** можно задать как `(r, g, b)`, `(r, g, b, a)`, `"#RRGGBB"` или `"#RRGGBBAA"`.
| Метод | Что рисует | Основные параметры |
|---|---|---|
| `text(key, текст, ...)` | Текстовый блок, несколько строк — через `\n` | `color`, `bg` фон (не задан — прозрачный), `border`, `size`, `bold`, `shadow`, `width` (перенос по этой ширине), `radius` скругление углов |
| `panel(key, заголовок, [строки...], ...)` | Панель (тёмный полупрозрачный фон, золотая рамка) | Как у `text` |
| `bar(key, 0..1, ...)` | Полоска прогресса: здоровье, перезарядка, произнесение | `width`, `height`, `color`, `bg`, `border`, `text` |
| `image(key, путь, ...)` | Локальное изображение (png / jpg / bmp / gif) | `width`, `height` (не заданы — исходный размер) |
| `circle(key, юнит или точка, радиус, ...)` | Круг на земле, повторяет рельеф | `color` цвет линии, `fill` заливка (с прозрачностью), `width` толщина линии |
| `path(key, [точки...], ...)` | Ломаная на земле | `color`, `width`, `arrow` стрелка на конце; точки — координаты или юниты |
## HTTP (любой язык)
Бэкенд Farsight (слушает только локальный адрес):
```http
POST /api/instances/20/canvas
{"set": [
{"key": "banner", "kind": "text", "text": "Холст из HTTP", "frac": [0.5, 0.12], "center": true,
"color": "#FFDC50", "bg": [0, 0, 0, 180]},
{"key": "hp", "kind": "bar", "value": 0.8, "unit": 596125988, "lift": 260, "text": "80%"},
{"key": "zone", "kind": "circle", "center": 596125988, "radius": 600, "color": [255, 200, 0], "width": 4},
{"key": "route", "kind": "path", "points": [[-4587, -9092], [-5387, -8792]], "color": "#50C8FF"}
],
"remove": ["old"], "clear": false}
GET /api/instances/20/canvas какие элементы сейчас нарисованы + сколько кадров отрисовано
```
`kind` — это имя метода в Python, имена параметров те же; юнит задаётся адресом `addr` из снимка.
## Напрямую через общую память
Можно обойтись без Python и Farsight: один раз отправьте семантическую команду `canvas_enable` (код операции W3P 73), и рантайм создаст блок общей памяти `Local\War3Canvas_`: заголовок 64 байта + 256 записей × 112 байт + пул текста / точек на 64 KB. Запись идёт по seqlock (номер становится нечётным → записываются элементы и пул → номер становится чётным); рантайм читает блок раз в кадр, а если попадает на наполовину записанные данные, повторяет предыдущий кадр. Обратно он записывает число отрисованных кадров, число элементов и счётчик сбоев. Эталонная реализация на Python — `sdk/python/w3canvas.py`, структуры описаны в заголовочном файле протокола, см. [Протокол W3P](https://war3ai.com/ru/docs/protocol/).
## Несколько программ рисуют одновременно
Моды, Farsight, MCP и шлюз могут одновременно рисовать в одной и той же игре, а холст всего один. Правило: **каждая программа трогает только свои элементы**.
- перед записью взять именованную блокировку, прочитать текущие элементы, оставить чужие, заменить свои и записать всё обратно;
- каждый элемент помнит, кто его нарисовал (идентификатор процесса + порядковый номер внутри процесса); если нарисовавшая его программа завершилась, элемент попутно убирается при следующей записи, кто бы её ни делал, а его кнопки больше не перехватывают клики;
- номера элементов выдаёт общий счётчик, так что они не совпадают.
Python SDK уже так и работает, и `clear()` тоже убирает только свои элементы. Если пишете в общую память напрямую, следуйте этим правилам, иначе затрёте чужое. Подробности о раскладке памяти — см. [Протокол W3P](https://war3ai.com/ru/docs/protocol/).
## Замеры и замечания
- Замер 2026-09-25 (1920×1080, скорость 2×): 9 элементов — 0.27 ~ 0.34 ms за кадр, около 63 кадров в секунду, 0 сбоев; запись 9 элементов — 6 ms; когда герой двигается, круг, текст и полоска здоровья, привязанные к юниту, не отстают. Текстура перерисовывается, только когда меняется содержимое; при простом перемещении перерисовки нет.
- Холст рисуется после интерфейса игры и перед указателем мыши: он перекрывает собственные полоски здоровья игры, юнитов и интерфейс, а указатель мыши рисуется поверх него. Нижнюю панель управления и шар дня и ночи он обходит, но **собственные панели карты не обходит** (таблицу лидеров и таймер в правом верхнем углу) — свои панели в правый верхний угол не ставьте.
- Вне матча (в главном меню, на экране итогов) элементы, привязанные к координатам мира и к юнитам, не рисуются, а привязанные к позиции на экране рисуются как обычно.
- Круг на земле строится так: каждая из 64 точек окружности отдельно проецируется на землю, поэтому на неровном рельефе форма круга следует за ним. Так и должно быть: круг нарисован на настоящей поверхности.
- При первом включении нужно поставить хук и прогреть шрифты — около 1 секунды; в это время текстовые элементы не рисуются, а круги и линии рисуются.
- Если при отрисовке случился хотя бы один сбой, до конца сеанса холст больше не рисует (та же защита, что у облачков над головой), а `faults` в `stats()` становится 1.
- Текст, пути к изображениям и точки — всего 64 KB, не больше 256 элементов; путь к изображению должен быть локальным путём, доступным процессу игры.
Панель состояния [ИИ-компаньона](https://war3ai.com/ru/docs/companion/) нарисована именно на холсте: полоска здоровья, текущее занятие, настроение, число убийств и лечений.
---
# Интерфейс и ввод
> Кнопки и карточки выбора на холсте нажимаются и подсвечиваются при наведении; можно регистрировать горячие клавиши, выбирать точку кликом по земле, узнавать, куда указывает мышь и кого выбрал локальный игрок. Клики, горячие клавиши, применение способностей, полный текст чата, уход игроков — всё попадает в поток событий.
То, что рисует [холст](https://war3ai.com/ru/docs/canvas/), теперь **можно нажимать**. Рантайм перехватывает ввод окна игры, и внешняя программа может:
| Возможность | Коротко | Получает ли это игра |
|---|---|---|
| **Кликабельные элементы холста** | Кнопки, карточки выбора, панели: клик отправляет `ui.click`, при наведении — автоматическая подсветка | Клик по кнопке **до игры не доходит** |
| **Горячие клавиши** | Регистрируете сочетания вроде `F5` или `ctrl+shift+Q`, нажатие отправляет `hotkey` | Можно поглотить (вместе с символом, который порождает нажатие) |
| **Клик по земле** | Клик по миру отправляет `mouse.world` с координатами на земле | Можно поглотить («кликните, где поставить башню») |
| **Позиция мыши** | Обновляется каждый кадр: пиксели экрана, точка на земле под курсором, элемент холста под курсором | — |
| **Выделение** | Кого выбрал локальный игрок; при любом изменении приходит `selection.changed` | — |
Всё это — **локальный ввод + локальная отрисовка**: в поток приказов ничего не попадает, поэтому безопасно и в многопользовательской игре. Но если обработчик меняет мир (создаёт юнитов, меняет характеристики), это по-прежнему только для одиночной игры.
## Python: g.ui
```python
ui = g.ui # при первом обращении рантайм перехватывает ввод окна
ui.button("shop", "Купить зелье (50 золота)", screen=(40, 300), on_click=lambda g, ev: buy(g))
c = ui.choice("Новый уровень! Выберите награду", [("Сила +5", "крепче"), ("Скорость атаки +20%", "бьёт чаще"), ("Призвать волка", "ещё один помощник")],
pause=True, on_pick=lambda g, i: give(g, i)) # ряд карточек посреди экрана; pause=True — пока выбирают, игра на паузе
i = c.wait(timeout=30) # можно и ждать с блокировкой (события при этом обрабатываются и не теряются)
ui.hotkey("F5", lambda g, ev: g.say(hero, "Понял!")) # по умолчанию нажатие поглощается
ui.hotkey("ctrl+shift+Q", on_press=..., swallow=False)
ui.mouse(on_click, capture=True, buttons=("left", "right")) # ловить клики по земле: сообщать о левой и правой кнопке и поглощать их
xy = ui.pick_point("Кликните по земле: где поставить башню?") # блокирующая версия: следующий левый клик по земле -> (x, y); Esc или тайм-аут -> None
ui.cursor() # {'screen': (x, y), 'world': (x, y, z) или None, 'hover': 'shop'}
ui.toast("Идёт 3-я волна!", seconds=3)
ui.close() # убрать свои элементы и горячие клавиши; ввод окну возвращается, только если им не пользуется другая программа
g.close() # или отключиться целиком (можно и так: with Game(...) as g:)
```
Обработчики получают `(g, ev)` и срабатывают, когда вы вызываете `g.events()`, — раннеры ботов и [игровых модов](https://war3ai.com/ru/docs/mods/) делают это каждый тик. Клики, для которых обработчик не задан, попадают в `ui.clicks`. Исключение в обработчике только записывается в лог и не мешает другим обработчикам и событиям.
Можно работать и напрямую с холстом: `g.canvas.text(..., clickable=True, hover=цвет)`; клики приходят из потока событий, а `ev.key` — это key, заданный при отрисовке. Если нарисовать кликабельный элемент, перехват ввода включается автоматически — трогать `g.ui` заранее не нужно.
**Запись горячих клавиш**: `F1` ~ `F24`, `A` ~ `Z`, `0` ~ `9`, `numpad0` ~ `numpad9`, `space enter esc tab backspace insert delete home end pageup pagedown left up right down`; впереди можно добавить `ctrl+`, `shift+`, `alt+`.
> **Внимание**
>
> Буквы и цифры без модификаторов конфликтуют с вводом в чат и горячими клавишами самой игры. Лучше берите клавиши, которые игра не использует, вроде F5 ~ F8, или сочетания.
## Новые события
В `g.events()` появились такие события (все поля — в [протоколе W3P](https://war3ai.com/ru/docs/protocol/)):
| kind | Когда | Удобные поля |
|---|---|---|
| `ui.click` | Нажат интерактивный элемент холста | `.key` — key на холсте, `.button` (`'left'` / `'right'`), `.mods` — модификаторы |
| `ui.hover` | Мышь зашла на элемент холста / ушла с него | `.key` (при уходе — `None`) |
| `hotkey` | Нажата зарегистрированная горячая клавиша | `.key` — запись клавиши, `.mods` |
| `mouse.world` | Клик по миру, если включён перехват кликов по земле | `.x .y` — координаты на земле, `.button`, `.value` (1 = клик поглощён) |
| `selection.changed` | Изменилось выделение локального игрока | Юнитов берите через `g.selection()` |
| `spell.cast` | Юнит применил способность (способность ушла на перезарядку) | `.spell` — четырёхсимвольный код, `.b` — уровень, `.value` — перезарядка в секундах, `.x .y` — точка применения |
| `message` | В окне сообщений на экране появилась строка | `.text` — полный текст, `.frame` — в каком окне, `.chat` (если это чат) |
| `player.left` | Игрок вышел или удалён после поражения | `.player` |
| `game.ended` | Выход из матча | — |
## Чат и экранные сообщения
То, что игрок пишет в чат, читается прямо из поля `.chat` события `message`:
```python
for ev in g.events():
if ev.kind == "message" and ev.chat and ev.chat["text"] == "-follow":
... # ev.chat = {'channel': 'Всем', 'sender': 'имя игрока', 'text': '-follow'}
```
У `g.messages()` свой, независимый курсор, и в нём есть ещё и подсказки игры («Нужно больше ферм», «Здесь строить нельзя»). В боте по ним видно, почему команда не выполнилась.
## Из других языков
- **Шлюз**: методы `ui.button`, `ui.choice`, `ui.hotkey`, `ui.mouse`, `ui.cursor` доступны в [шлюзе](https://war3ai.com/ru/docs/gateway/) под теми же именами. Передать функцию-обработчик удалённо нельзя, поэтому клики и горячие клавиши приходят в потоке событий (событие `ui.click` содержит `key`).
- **Прямая запись в общую память**: сначала отправьте семантическую команду `input_enable` (код операции W3P 74), и рантайм начнёт перехватывать ввод; в блоке ввода `Local\War3Input_` вы записываете таблицу горячих клавиш и переключатели мыши, а рантайм записывает обратно позицию мыши, точку на земле под курсором и элемент под курсором. Флаг `0x40` у элемента холста означает «интерактивный». Раскладка — в [протоколе W3P](https://war3ai.com/ru/docs/protocol/).
## Несколько программ одновременно
Моды, Farsight, MCP и каждый сеанс шлюза могут одновременно ставить кнопки и регистрировать горячие клавиши в одной игре, не мешая друг другу:
- каждая программа регистрирует свои горячие клавиши и свой переключатель кликов по земле, а SDK сводит всё в одну таблицу и передаёт её рантайму. Каждая клавиша попадает в таблицу один раз, событие получают все, и каждый узнаёт свои горячие клавиши по клавише;
- `ui.close()` убирает только своё, а ввод окну возвращается, лишь когда уходит последняя программа;
- если программу завершили принудительно и она не успела прибраться, рантайм раз в 2 секунды проверяет, живы ли зарегистрированные программы; когда все они завершились, он убирает оставленные ими горячие клавиши и перехват кликов по земле, а их кнопки больше не перехватывают клики.
## Замеры
2026-09-25, проверка в реальной игре на тестовом экземпляре, 16/16:
- клик по кнопке → `ui.click` + обработчик, счётчик перехватов в рантайме +1 (игра этот клик не получила); клик мимо кнопки ничего не вызывает;
- F6 → `hotkey`; клик по земле → `mouse.world` (поглощён);
- создать паладина и выбрать его → `selection.changed`, `g.selection()` совпадает; применить «Божественный щит» → `spell.cast('AHds', 1, 35.0)`;
- текст карты → `message`; чат → `message`, в `.chat` разобраны отправитель и текст;
- поражение компьютерного игрока → `player.left`; завершение матча → `game.ended`.
Клики по кнопкам настоящей мышью и подсветку при наведении тоже проверили по одному.
## Ограничения и замечания
- **Позиция берётся от настоящей мыши**: игра сама читает позицию по системному курсору, поэтому наведение и `cursor()` отражают настоящую мышь. Перехват касается только нажатий.
- **Рисуется под указателем мыши**: Warcraft каждый кадр рисует указатель как часть картинки. Холст и облачки над головой рисуются до того шага, на котором игра рисует указатель: они перекрывают интерфейс игры, а указатель перекрывает их. Только если в этом кадре указатель не рисуется (скрыт или идёт заставка), они рисуются последним шагом.
- **Системное масштабирование**: если в своих тестах вы отправляете клики оконными сообщениями, координаты от процесса без поддержки DPI система увеличивает (при масштабе 150% замерено ×1.5). Объявите в тестовой программе поддержку DPI. На клики живого человека это не влияет.
- **В первый раз нужно прогреть шрифты** — около 1 секунды. Пока кнопки ещё не нарисованы, нажать их нельзя.
- **Вне матча клики по земле не сообщаются**: в главном меню и на экране итогов `mouse.world` не отправляется и не поглощается.
- Если нажатие было поглощено, а до отпускания вы переключились на другую программу или увели мышь за пределы окна, состояние всё равно сбрасывается, и следующее отпускание уже не поглощается.
- В 1.27 нет функций для создания новых фреймов интерфейса игры (они появились только в 1.31): кнопки и карточки здесь рисует рантайм, стиль может быть любым, но в собственной иерархии меню игры они не появляются.
---
# JASS-канал
> 1291 функцию JASS, доступную авторам карт, теперь можно вызывать по имени извне игры: создавать юнитов, менять свойства, эффекты, панели, диалоги, звук, камеру, туман войны… Четыре способа: консоль Farsight, командная строка, HTTP и Python.
**Все native-функции JASS (их 1291)**, которыми авторы карт пользуются в скриптах карт, теперь можно вызывать по имени извне игры: создавать юнитов, менять свойства, рисовать эффекты, показывать панели и диалоги, играть звуки, двигать камеру, менять туман войны… Это путь к дальнейшей настройке игры под себя: помощники для RPG, [ИИ-компаньон](https://war3ai.com/ru/docs/companion/), собственные мини-режимы, инструменты отладки.
| Способ | Для чего | Где |
|---|---|---|
| **Страница «JASS-консоль» в Farsight** | Пробовать вручную, смотреть и править на ходу | Левая панель «Система → JASS-консоль»: пишете скрипт и нажимаете «Выполнить», справа — функции по категориям, щелчок вставляет функцию в скрипт |
| **Командная строка** | Пробовать вручную или запускать файл-скрипт снова и снова | `python -m openwar3 jass --inst 20` (интерактивно), `-e "код"`, `my_script.j`, `--list слово` |
| **HTTP** | Внешние программы на любом языке | `POST /api/instances/{n}/jass` и др. (см. ниже), бэкенд Farsight слушает только локальный адрес |
| **Python** | Схемы, компаньоны, инструменты | `g.jass.ЛюбаяФункция(...)`; частые визуальные эффекты и взаимодействие обёрнуты в `openwar3.visual` |
> **Внимание**
>
> Три ограничения, и все продиктованы механикой:
>
> - Менять мир можно только в **одиночной игре** (против компьютера на вашей машине). Если ваш компьютер в одностороннем порядке создаёт объекты и меняет юнитов, в многопользовательской игре у остальных игроков начнётся рассинхронизация, — поэтому там пропускаются только функции для чтения (`Get*`, `Is*`, `Count*`…).
> - Только для ваших локальных инструментов: вызовы через подключение от имени игрока (`Game(player=N)`) или в честном режиме отклоняются.
> - Только для одиночных игр и игр по локальной сети, созданных вами.
>
> Чтобы добавить что-то на экран в многопользовательской игре, используйте [холст](https://war3ai.com/ru/docs/canvas/): его рисует сам рантайм, состояние игры не меняется.
## Как писать скрипты
Консоль, командная строка и HTTP используют один и тот же язык скриптов. Одна строка — одна инструкция; **можно вставлять JASS как есть** (`call` / `set` / `local`, `true` / `false` / `null`, четырёхсимвольные коды вида `'Hpal'`, комментарии `//`), а можно писать в стиле Python:
```text
set h = hero() // встроенная: главный герой нашей стороны
local texttag t = CreateTextTag()
call SetTextTagText(t, "|cffffcc00+128 Крит!|r", 0.024)
call SetTextTagPosUnit(t, h, 60)
call SetTextTagVelocity(t, 0, 0.03)
call SetTextTagPermanent(t, false)
call SetTextTagLifespan(t, 4)
call SetTextTagVisibility(t, true)
call PingMinimapEx(h.x, h.y + 300, 3, 255, 0, 0, false)
set u = CreateUnit(Player(0), 'hfoo', h.x + 200, h.y, 270)
print("Создан", u, "уровень героя", GetHeroLevel(h))
```
- **Переменные сохраняются**: в том же экземпляре и в том же матче переменные, заданные через `set` в одном фрагменте, доступны в следующем; при смене матча они очищаются автоматически, можно очистить их и вручную.
- **Встроенные функции**: `hero()` — главный герой нашей стороны, `me()` — локальный игрок, `unit('hfoo')` — найти юнита, `unit_at(x, y)`, `wait(секунды)`, `print(...)`. У юнита можно читать `.x`, `.y`, `.hp`, `.hp_max`, `.mana`, `.type`, `.owner`, `.level`; поддерживаются арифметика и сравнения.
- **Не поддерживаются** `if`, `loop`, `function` — для логики используйте `g.jass` в Python (это обычные вызовы функций) или оформите всё как [схему](https://war3ai.com/ru/docs/schemes/).
- При ошибке вы узнаете номер строки и причину (нет такой функции, неверное число аргументов, переменная не определена…); инструкции до ошибки уже выполнены.
Параметры и возвращаемые значения:
| В сигнатуре | Что передавать | Пояснение |
|---|---|---|
| Целое | Число; четырёхсимвольный код `'Hpal'` преобразуется автоматически | |
| Вещественное | Число | Рантайм переводит его в формат, нужный движку |
| Логическое | `true` / `false` | |
| Строка | `"..."` | Поддерживаются китайские символы и цветовые коды игры; строки, которые игра сохраняет у себя (всплывающий текст, панели, кнопки, команды чата), копируются в момент вызова — это безопасно |
| Дескриптор | Дескриптор из переменной или юнит (например, `hero()` автоматически превращается в дескриптор) | |
| Функция (code) | Только `null` | Функцию JASS извне передать нельзя; вызов вида `TimerStart(t, 60, false, null)` работает |
| Возвращаемая строка | — | Движок возвращает номер в таблице строк, текст прочитать нельзя. Имена юнитов — через `g.map_data.name_of` |
## Категории
Функции разбиты на категории по именам; по ним же устроены правая панель консоли и `--list`:
| Категория | Число | Примеры |
|---|---|---|
| Визуальные эффекты | 80 | Всплывающий текст, молнии между юнитами, спецэффекты, изображения на земле, отпечатки на земле, цвет / масштаб / анимация юнита |
| Панели интерфейса | 146 | Многострочные панели, таблица лидеров, окна таймера, диалоги, задания, текст на экране, сигналы на миникарте, диалог с портретом, полноэкранные фильтры |
| Камера | 44 | Поля камеры, панорамирование, тряска камеры |
| Звук и музыка | 50 | Создание и воспроизведение звуков, музыка |
| Туман войны и обзор | 25 | Области видимости, включение и выключение тумана |
| Предметы / герои / юниты | 63 / 32 / 161 | Создать предмет, задать уровень героя, сменить владельца, добавить способность |
| Игроки / союзы / ресурсы | 71 | Настроить союз, изменить золото и древесину |
| Триггеры / события / таймеры | 62 | Создание триггеров, регистрация событий, таймеры |
| Рельеф / погода / разрушаемые объекты | 45 | Погодные эффекты, изменение рельефа, создание разрушаемых объектов |
| Ход игры | 57 | Скорость игры, пауза, время суток |
| Прочее | … | Группы юнитов и области, хранилище, скрипты компьютерного ИИ, преобразование типов и математика, ответы на события… |
На 2026-09-24 в реальной игре по одной вызваны и проверены глазами **94 функции**; остальные работают по тому же пути, просто их эффект не проверялся для каждой по отдельности.
> **Примечание**
>
> Функции категории «ответы на события» (`GetTriggerUnit`, `GetClickedButton`…) имеют значение только в момент срабатывания триггера; при вызове извне они возвращают 0 или пустое значение. Чтобы узнать, «случилось ли событие», используйте счётчики событий, описанные ниже.
## HTTP
Бэкенд Farsight (по умолчанию `127.0.0.1:8866`, слушает только локальный адрес):
```http
GET /api/jass/natives?q=TextTag&cat=visual
POST /api/instances/20/jass {"code": "set h = hero()\ncall PingMinimapEx(h.x, h.y, 3, 255, 0, 0, false)"}
-> {"ok": true, "rows": [...], "printed": [...], "vars": {...}}
-> ошибка: {"ok": false, "error": "第 2 行:...", "line": 2}
POST /api/instances/20/jass/call {"name": "SetUnitScale", "args": [{"unit": 599669636}, 1.4, 1.4, 1.4]}
POST /api/instances/20/jass/reset сбросить запомненные переменные
```
Параметр-юнит записывается как `{"unit": адрес}`, где адрес — это `addr` юнита из снимка. Замер: 60 ~ 90 ms на запрос.
## Python: g.jass и openwar3.visual
```python
j = g.jass
t = j.CreateTextTag()
j.SetTextTagText(t, "Привет", 0.024) # правила для параметров те же, что в скриптах; юниты и предметы из снимка можно передавать как есть
j.signature("CreateImage") # узнать сигнатуру
```
`openwar3.visual.Visual(g)` оборачивает проверенные частые визуальные эффекты — по одной строке на эффект (каждый тик вызывайте `v.tick()`: он удаляет истёкшее и передвигает линии и круги, привязанные к юнитам; `v.clear()` удаляет всё):
| Метод | Эффект |
|---|---|
| `float_text(текст, юнит или точка, ...)` | Всплывающий текст: числа урона, подсказки над головой; китайские символы и цвета поддерживаются |
| `link(a, b, kind)` | Линия между двумя юнитами, следует за ними: привязь / духовная связь / похищение жизни / волна исцеления |
| `effect(модель, юнит или точка, ...)` | Модель эффекта: над головой, под ногами или однократно (взрыв, столб света) |
| `ring(юнит или точка, радиус, color)` | Круг на земле: радиус способности, опасная зона, точка сбора; может следовать за юнитом |
| `ping(точка, color)` | Сигнал на миникарте |
| `board(заголовок, строки...)` | Многострочная панель в правом верхнем углу (с иконками), ячейки можно менять по одной |
| `countdown(заголовок, секунды)` | Окно таймера в правом верхнем углу, отсчёт ведёт сама игра |
| `scene(имя, реплика, portrait)` | Диалог с портретом: портрет внизу меняется на говорящего юнита, на экране появляется субтитр «имя: реплика» |
| `screen_tint(color, alpha)` | Полноэкранный фильтр (по умолчанию красноватые края: предупреждение о малом здоровье) |
| `sound(путь)` / `reveal(точка, радиус, секунды)` / `look(юнит, ...)` | Звук / рассеять туман на участке / перекрасить юнита, увеличить, проиграть анимацию, мигнуть |
## Взаимодействие: что сделал игрок — без функций JASS
Чтобы реагировать на действия игрока, в JASS нужно писать функции триггеров, а передать функцию извне нельзя. Выход такой: **создать пустой триггер без условий и действий, только зарегистрировать событие — и считать, сколько раз он сработал.** Проверено: пустой триггер тоже ведёт счёт.
| Метод | Назначение |
|---|---|
| `chat_commands(["-follow", "-stay"])` → `.poll()` | Команды, которые игрок набрал в чате (точное совпадение или по началу строки) |
| `menu(заголовок, [кнопки...])` → `.clicked()` | Меню из кнопок посреди экрана: какую нажали |
| `hotkeys(("left", "right", "up", "down", "esc"))` → `.poll()` | Сколько раз нажаты стрелки и Esc |
| `on("TriggerRegister...Event", аргументы...)` → `.poll()` | Сколько раз произошло любое событие JASS: гибель юнита, вход в область, получение урона, таймер… |
Ограничение: известно только, «сколько раз это произошло», но не «кто это был и что именно напечатал». Чтобы различать, кто это был, заведите по счётчику на каждый объект. Именно так подключены команды чата у [ИИ-компаньона](https://war3ai.com/ru/docs/companion/).
## Замечания
- **Созданное удаляйте сами**: всплывающий текст, линии, изображения, панели, триггеры… Если не удалить, они так и останутся (`Visual.clear()` удаляет то, что создал сам). Одновременно в игре может быть не больше ~100 всплывающих текстов.
- **BJ-функции — не native**: `CreateTextTagUnitBJ` и подобные собраны из native в скриптах карт, здесь их нет — вызывайте native так, как это сделано в их реализации.
- **Некоторые константы нужно сначала преобразовать**: например, `ConvertPlayerColor(1)`, `ConvertFogState(4)` (значения см. в common.j).
- Один вызов — около 13 ms (включая преобразование дескрипторов); на уровне протокола это коды операций W3P 70 ~ 72, см. [Протокол W3P](https://war3ai.com/ru/docs/protocol/).
---
# Игровые моды
> Схема — это не только ИИ, который играет за вас, но и набор правил: вы сами играете в окне игры, а мод расставляет всё на старте, выпускает волны врагов, выдаёт награды, показывает вам на экране кнопки и карточки выбора и определяет исход. Унаследуйте openwar3.Mod — один файл, один геймплей.
[ИИ-схемы](https://war3ai.com/ru/docs/schemes/) бывают двух видов: `kind: bot` — это ИИ, который играет за вас; `kind: mod` — это **набор правил**: вы сами играете в окне игры, а мод ставит задачи — как всё расставить на старте, когда выпускать волны врагов (по времени или по событиям), какие давать награды, какие кнопки и карточки показать вам на экране, когда засчитать победу.
Мод использует только готовые возможности: [интерфейс и ввод](https://war3ai.com/ru/docs/ui-input/) (кнопки, карточки, горячие клавиши, клики по земле), [холст](https://war3ai.com/ru/docs/canvas/) (панели, полоски прогресса, маршруты), [JASS-канал](https://war3ai.com/ru/docs/jass/) (создание юнитов, изменение характеристик, выдача предметов), поток событий (гибель, повышение уровня, применение способностей, чат).
## Два примера
Выбираются в Farsight: «ИИ-схемы» → «Встроенные»:
| Мод | Как играть | Что использует |
|---|---|---|
| **Рогалик с героем** `builtin/hero-roguelike` | У вас один паладин, а враги волна за волной наступают со всех сторон; с каждым новым уровнем посреди экрана — выбор одного улучшения из трёх (пока выбираете, игра на паузе); продержались 10 волн — победа, герой погиб — поражение | `g.ui.choice` (кликабельные карточки + пауза), события `hero.levelup` / `killed` / `spell.cast`, команда чата `-help`, изменение характеристик героя и выдача предметов через JASS |
| **Бесконечная оборона** `builtin/endless-defense` | Враги появляются на стартовой точке напротив и по красной линии на земле бегут к вашей ратуше; за каждую отбитую волну — золото; кнопка на экране или F7 досрочно вызывает следующую волну, награда ×1.5; F8, а затем левый клик по земле — бесплатная сторожевая башня (правый клик — отмена) | `g.ui.button`, `g.ui.hotkey`, `g.ui.mouse` (перехват кликов по земле), панель / полоска прогресса / маршрут на холсте, создание врагов и выдача золота через JASS |
Каждый пример — примерно 150 строк, код лежит в `brains/examples/mod_hero_roguelike.py` и `brains/examples/mod_endless_defense.py`.
```bash
python tools/play.py --bot brains/examples/mod_hero_roguelike.py --inst 9 # запустить матч: управление берёт мод, вы играете в окне игры
```
## Пишем мод
```python
from openwar3 import Mod
class Survive(Mod):
name = "survive"
def on_start(self, g):
super().on_start(g) # проверка одиночной игры + нейтрализация компьютерного противника
self.foe = self.wave_player(g) # пустой слот как «игрок волн»: ни с кем не в союзе, без компьютерного ИИ
self.every(30, self.wave) # волна каждые 30 игровых секунд (на паузе таймер стоит)
g.ui.hotkey("F7", lambda g, ev: self.wave(g))
def wave(self, g):
self.spawn_ring(g, self.foe, "ugho", 6, self.home(g), 1400, attack_to=self.home(g))
def on_event(self, g, ev):
if ev.kind == "unit.died" and ev.type == "htow":
self.finish("loss", "Ратуша разрушена")
```
`Mod` добавляет к `Bot` следующее:
| Метод / атрибут | Описание |
|---|---|
| `on_start / on_tick / on_event / on_end` | Как у Bot; переопределяя `on_start` / `on_tick`, не забудьте сначала вызвать `super()` |
| `every(секунды, fn, first=)` / `after(секунды, fn)` | Таймеры по **игровому времени**, обработчик — `fn(g)` |
| `finish(result, reason)` | Завершить матч (`'win'` / `'loss'` / `'unknown'`): раннер останавливается на следующем тике, посреди экрана рисуется панель с результатом, по нему же записываются результаты схемы |
| `wave_player(g)` | Первый игрок в пустом слоте — для роли «игрока волн» |
| `spawn_ring(g, игрок, юнит, количество, центр, радиус, attack_to=)` | Создаёт юнитов по кругу — даже волна из нескольких десятков не вызывает подтормаживаний; возвращает дескрипторы JASS |
| `alive_of(g, игрок)` / `attack_move_all(g, игрок, точка)` | Живые юниты игрока / всех — в атаку с движением к точке (вызывайте раз в несколько секунд, и враги будут преследовать) |
| `home(g)` / `hud(g, заголовок, строки)` | Позиция нашего главного здания / информационная панель в правом верхнем углу |
| `neutralize_ai = True` | Нейтрализовать компьютерного противника на старте: его юниты ставятся на паузу каждые 5 секунд, золото и древесина обнуляются. На картах для обычных матчей компьютерный противник есть всегда, а когда правила задаёт мод, он только мешает |
| `single_player_only = True` | Не запускаться, если в игре есть другие живые игроки (JASS, меняющий мир, вызовет у них рассинхронизацию) |
| `linger_s = 6` | Сколько секунд показывать экран результата после определения исхода, прежде чем завершить |
`finish()` доступен и в `Bot`: обычный бот тоже может сам объявить конец матча.
## Оформить как схему и поделиться
В `scheme.json` укажите `"kind": "mod"`, а в файле входа определите подкласс `Mod`:
```json
{"id": "survive", "name": "Продержаться 10 волн", "kind": "mod", "entry": "survive.py", "class": "Survive"}
```
Мод всегда работает **без честного режима** (он судья, который ставит задачи: ему нужно видеть всю карту и менять мир) и **не определяет исход по правилам обычного матча** (исход сообщает `finish`); `fair` / `judge` в манифесте на него не действуют. Экспорт в zip, импорт, доверие и результаты — точно так же, как у схем-ботов, см. [ИИ-схемы](https://war3ai.com/ru/docs/schemes/). Мод — тоже код, поэтому перед первым запуском чужого мода нужно так же подтвердить доверие.
## Замеры
2026-09-25, тестовый экземпляр:
- **Рогалик с героем**: появилась первая волна, панель в правом верхнем углу обновляется; герой доведён до 3-го уровня → посреди экрана появились карточки, игровые часы остановились; два клика по карточкам → оба улучшения применились (сила 22 → 27), часы снова пошли.
- **Бесконечная оборона**: панель, маршрут на земле и кнопка на месте; F8 + клик по земле → рядом с ратушей появилась сторожевая башня; нажатие кнопки, пока волна ещё не зачищена → подсказка «Эта волна ещё не зачищена».
## Ограничения
- **Только одиночная игра**: создание юнитов и изменение характеристик идут через JASS-канал, а в многопользовательской игре это вызывает рассинхронизацию. Так устроена модель lockstep; многопользовательским режимам придётся дождаться канала синхронизации (см. [дорожную карту](https://war3ai.com/ru/roadmap/)).
- Мод видит всю карту — он ставит задачи, а не играет.
- Компьютерный противник на картах для обычных матчей только «нейтрализован», но не удалён (удаление запустило бы проверку победы по правилам обычного матча).
---
# Консоль Farsight
> Локальная веб-консоль и единственная точка входа: папка игры, запуск и остановка служб и экземпляров игры, настройка следующего матча, мысли ИИ, ручные приказы, режиссура, история матчей.
Farsight — веб-консоль, работающая на вашем компьютере; она **слушает только 127.0.0.1**. Это и единственная точка входа во всю систему: запуск матчей, смена ИИ, шлюз, облачка реплик, локальная LLM — всё здесь, искать другие скрипты не нужно.
```bash
start.bat # проверить развёртывание, затем открыть Farsight http://127.0.0.1:8866
start.bat 5 6 # заодно начать тест на экземплярах 5 и 6 (игра + эталонный мозг)
start.bat restart # перезапустить только бэкенд Farsight (после изменения серверного кода; игры и службы не затрагиваются)
stop.bat # полностью остановить всё
```
Порт меняется в `ports.console` файла `openwar3.json` (по умолчанию 8866).
## Центр управления
Главная страница Farsight.
- **Папка игры**: найти автоматически или выбрать вручную; проверка версии игры, извлечение данных из вашей игры.
- **Локальные службы**: [шлюз](https://war3ai.com/ru/docs/gateway/), [облачка реплик](https://war3ai.com/ru/docs/speech/), локальная LLM (LM Studio), локальный предпросмотр сайта — на каждой карточке можно запустить, остановить, перезапустить службу и посмотреть логи; кроме того, видно, подключил ли клиент [MCP](https://war3ai.com/ru/docs/mcp/)-сервер.
- **Проверка окружения**: установлены ли Python, файлы рантайма, игровые данные, данные AMAI и остальные части.
- **«Остановить всё»** (в правом верхнем углу): по очереди останавливает экземпляры игры, ИИ, шлюз, облачка, локальную модель, которую использует система, и бэкенд Farsight — то же самое, что двойной щелчок по `stop.bat`. MCP-сервер принадлежит клиентам вроде Claude и не останавливается; сама программа LM Studio тоже не закрывается.
## Страницы
| Группа | Страница | Назначение |
|---|---|---|
| Главное | Центр управления | См. предыдущий раздел |
| Матч | Обзор | Сводка по матчу текущего экземпляра |
| | Командование боем | Вид карты; можно отдавать приказы вручную (ручные команды имеют в таблице захватов наивысший приоритет: 95) |
| | Данные юнитов | Приказ, текущая цель, мана, уровень и опыт героя, перезарядка способностей, инвентарь каждого юнита |
| | Решения ИИ / боевые решения | О чём эталонный мозг думает в этот тик, подробности каждого боевого решения |
| | Советник по стратегии | Состояние [LLM-советника](https://war3ai.com/ru/docs/llm-coach/): работает ли сервис модели, подключён ли каждый экземпляр, последний совет и входные данные, которые он видел |
| | Режиссёрский пульт | Автооператор камеры, полоски здоровья над юнитами |
| | Облачка реплик | Заставить юнитов говорить, разговор с локальной моделью, посиделки крестьян, диалоги в кадре, реакция на события, настройки модели. См. [Реплики и локальные модели](https://war3ai.com/ru/docs/speech/) |
| | Скорость команд | APM и пропускная способность команд |
| | События и ввод | Что произошло в этом матче: применение способностей, чат и экранные сообщения, нажатия кнопок, горячие клавиши, клики по земле, выделение, уход игроков — с фильтром по категориям; рядом — позиция мыши, элемент под курсором и локальное выделение. См. [Интерфейс и ввод](https://war3ai.com/ru/docs/ui-input/) |
| Записи | Логи / история матчей | Источники логов каждого экземпляра; итог, длительность и пиковая численность армии в каждом матче |
| | Заметки о проблемах | Нажмите в игре Pause/Break, чтобы поставить паузу и отметить момент, а описание добавьте здесь позже |
| Система | Экземпляры и запуск | Запуск и остановка экземпляров; для **следующего матча** — карта (карта для боёв, а можно выбрать и RPG / пользовательскую), расы сторон, сложность и скорость игры; для каждого экземпляра — своя ИИ-схема; «Начать тест» — игра + ИИ в один клик |
| | ИИ-схемы | Импорт, экспорт, копирование, доверие, удаление схем; переключение схемы на экземпляре (даже в идущем матче управление сразу перехватит другой ИИ); результаты каждой схемы. См. [ИИ-схемы](https://war3ai.com/ru/docs/schemes/) |
| | Консоль JASS | Пишете JASS-скрипт и нажимаете «Выполнить»; справа — 1291 функция по категориям, щелчок вставляет функцию в скрипт; переменные сохраняются до конца матча. См. [JASS-канал](https://war3ai.com/ru/docs/jass/) |
| | Подключение и расширения | Состояние шлюза и запуск в один клик; адреса подключения для каждой роли (разработчик / игрок / наблюдатель), команды и конфигурация для подключения MCP; примеры на JS и Python. См. [Шлюз](https://war3ai.com/ru/docs/gateway/) и [MCP](https://war3ai.com/ru/docs/mcp/) |
| | Данные и диск | Сколько места на диске занимают записи, история матчей, логи и другие рабочие данные, сколько добавилось за последние сутки и что можно удалить (Farsight сам ничего не удаляет) |
| | Настройки | Папка игры, язык интерфейса, оформление (тёмная / светлая тема, современный стиль / стиль Warcraft) и т. п. |
| | Отзывы и предложения | Столкнулись с проблемой или есть идея — отправьте её нам прямо отсюда; диагностическая информация прикладывается, только если вы отметите флажок, и перед отправкой её можно просмотреть |
Нажмите Ctrl + K, чтобы открыть палитру команд: переход по страницам, смена экземпляра, завершение текущего матча, запуск нового мозга.
Раздел «Что нового» внизу боковой панели показывает, что недавно появилось в Farsight и в платформе. При запуске (и затем каждые 6 часов) Farsight спрашивает War3AI.com, не вышла ли новая версия, и если вышла — сообщает вам. Когда Farsight обновляется, вверху страницы появляется баннер: сохраните то, что вводите, и нажмите «Обновить».
## Несколько экземпляров
За запуск нескольких копий отвечает `runtime/farm.py` (Farsight вызывает его за вас, когда запускает и останавливает экземпляры): оригинальный загрузчик `War3.exe` копируется под именем `War3-.exe` (никакие файлы игры не изменяются), у каждого экземпляра свой номер и свой каталог (`bin/inst/`). После окончания матча следующий запускается автоматически по `next_game.json` (именно его редактирует страница «Экземпляры и запуск» в консоли).
> **Совет**
>
> Ваш бот подключается к нужному экземпляру через `--inst N`. На странице «Экземпляры и запуск» видно, какие номера заняты, — не берите номер, на котором работает эталонный мозг.
## Страница для стрима
`http://127.0.0.1:8866/live` — страница с прокручивающимся логом, которую удобно добавить в OBS как источник типа «Браузер»: на ней видны решения ИИ и ход боя.
## API
Серверная часть консоли — набор локальных REST- и WebSocket-методов (состояние экземпляров, настройки следующего матча, подробности по юнитам, ручные приказы, логи, история матчей, режиссура, ИИ-схемы, вызовы JASS, холст…), а веб-страница — лишь один из клиентов: вызывать их напрямую может программа на любом языке. Список методов приведён в шапке файла `console/server/app.py`; как работать с группами схем, JASS и холста, описано на страницах [ИИ-схемы](https://war3ai.com/ru/docs/schemes/), [JASS-канал](https://war3ai.com/ru/docs/jass/) и [Холст](https://war3ai.com/ru/docs/canvas/).
---
# ИИ-схемы
> Одна схема — один полноценный ИИ. Переключение в Farsight в один клик, причём даже в идущем матче управление сразу перейдёт к новому ИИ; экспорт в zip, чтобы поделиться, импорт чужих схем для тестов и автоматическая статистика результатов по каждой схеме.
**Схема** = один полноценный ИИ: папка + манифест `scheme.json` + код. Каждому экземпляру игры назначается одна схема; в Farsight она переключается в один клик, и **даже в идущем матче управление сразу переходит к новой схеме**.
Схемы, которыми поделились другие, импортируются в **отдельный раздел** и никак не затрагивают ваши; чтобы изменить такую схему, нажмите «Копировать в мои».
```text
schemes/
mine// мои схемы: написанные вами или скопированные из других для правки (меняйте как угодно, действует со следующего матча)
installed// установленные: сюда распаковываются zip-архивы, которыми поделились другие (перед первым запуском нужно подтвердить доверие)
brains/xwar3/ встроенные: эталонный мозг (полноценный ИИ)
brains/examples/ встроенные: четыре учебных примера hello / rush / macro / micro, пример компаньона buddy, два игровых мода («Рогалик с героем», «Бесконечная оборона»)
```
Схема — не обязательно ИИ, который играет за вас. Схема с `kind: mod` — это **набор правил игры**: играете вы, а она ставит задачи; см. [Игровые моды](https://war3ai.com/ru/docs/mods/).
## Использование в Farsight
Страница «ИИ-схемы» (левая панель «Система → ИИ-схемы»):
| Действие | Что делает |
|---|---|
| Импорт схемы (zip) | Устанавливает схему в `installed/`; если схема с таким id уже установлена, спросит, заменить ли её (после замены доверие нужно подтвердить заново) |
| Применить к экземпляру… | Выбор экземпляра + «Сразу» (текущий ИИ останавливается, новая схема подхватывает этот матч) или «Со следующего „Начать тест“» |
| Копировать в мои | Копия в `mine/`: автором записываетесь «я», версия 0.1.0, и запоминается, из какой версии какой схемы сделана копия |
| Экспорт в zip | Упаковывает в `-<версия>.zip` — отправьте файл другому, и вы поделились схемой |
| Открыть папку | Открывает каталог схемы в Проводнике, чтобы сразу править код |
| Доверять | Для чужих схем обязательно перед первым запуском (см. ниже «Доверие и безопасность») |
| Последние результаты | Победа или поражение, длительность и причина завершения каждого матча этой схемы |
| Удалить | Удалять можно только «Мои» и «Установленные»; схему, которую сейчас использует какой-либо экземпляр, удалить нельзя |
На карточке экземпляра тоже появилась строка «ИИ-схема»: выберите схему в выпадающем списке → «Переключить (сразу)». Если экземпляр не запущен, кнопка называется «Выбрать» — при следующем «Начать тест» ИИ запустится по этой схеме.
## Манифест scheme.json
```json
{
"format": 1,
"id": "fast-rush",
"name": "Раш за три минуты",
"version": "1.2.0",
"author": "Некто",
"description": "Одной фразой: какую стратегию играет этот ИИ",
"entry": "rush_bot.py",
"class": "RushBot",
"fair": true,
"hz": 5,
"races": ["human", "orc"],
"license": "MIT"
}
```
| Поле | Обязательно | Пояснение |
|---|---|---|
| `id` | ✔ | Строчные латинские буквы, цифры, `-`, `_`; от 2 до 41 символа |
| `entry` | ✔ | Файл `.py` внутри каталога схемы (абсолютные пути и `..` запрещены) |
| `kind` | | По умолчанию `bot` (подкласс `openwar3.Bot`, играет за вас); `mod` = [игровой мод](https://war3ai.com/ru/docs/mods/) (подкласс `openwar3.Mod`; всегда без честного режима, исход не определяется по правилам обычного матча) |
| `class` | | Имя подкласса Bot (или Mod) в файле входа; если не задано, берётся последний подкласс `openwar3.Bot` в этом файле |
| `fair` | | По умолчанию `true`: видно только то, что в пределах обзора, — те же правила, что на Арене. `false` = видна вся карта, и только в этом случае доступен [JASS-канал](https://war3ai.com/ru/docs/jass/) (он нужен компаньону) |
| `judge` | | По умолчанию `true`: победа и поражение определяются по правилам обычного матча. Для RPG-схем и схем компаньонов — `false` |
| `hz` | | Сколько раз в секунду вызывается `on_tick`, по умолчанию 5 |
| `format` | | Версия формата манифеста, сейчас 1; если она новее, чем поддерживает ваш OpenWar3, схема отклоняется с предложением обновиться |
| Остальные | | `name`, `version`, `author`, `description`, `races`, `license`, `homepage`, `forked_from` — только для отображения |
Каталог схемы добавляется в путь поиска модулей Python, так что файл входа может импортировать (`import`) другие файлы из того же каталога. Сторонние пакеты (numpy, torch…) автоматически не устанавливаются — укажите в `description`, что нужно.
**Минимальной схеме хватает двух файлов**:
```python
# my_bot.py
from openwar3 import Bot
class MyBot(Bot):
def on_tick(self, g):
for w in g.idle_workers():
mine = g.nearest(g.gold_mines(), w)
if mine:
g.gather(w, mine)
```
```json
{"id": "my-first", "name": "Мой первый ИИ", "entry": "my_bot.py"}
```
Положите их в `schemes/mine/my-first/` и обновите Farsight — схема появится. Начать ещё проще так: выберите пример во «Встроенных» и нажмите «Копировать в мои».
## Запуск и результаты
Схемы запускает **раннер схем** (кнопки «Начать тест» / «Переключить» в Farsight запускают именно его):
```bash
python tools/run_scheme.py --inst 20 --scheme builtin/micro --hours 6
```
- На каждый экземпляр — один постоянный процесс-супервизор, а **на каждый матч — отдельный дочерний процесс** со схемой: если код схемы упадёт, супервизор не пострадает; если вы поменяли код «моей» схемы, со следующего матча автоматически используется новый.
- По окончании каждого матча записывается строка результатов: схема, версия, автор, победа или поражение, причина, длительность игры, число ошибок. По этим строкам Farsight считает процент побед.
Как определяется исход:
| Ситуация | Записывается как |
|---|---|
| У соперника не осталось зданий | Победа |
| У нас не осталось зданий (даже если войска живы — в обычном матче поражение засчитывается именно так) | Поражение |
| У нас не осталось юнитов | Поражение |
| Матч завершён / остановлен вручную в Farsight | Не определено |
| При переключении матч уже шёл больше 60 игровых секунд (схема подхватила его на полпути) | Учитывается отдельно, **в процент побед не входит** |
| Игровые часы долго не идут | Не определено |
Когда исход определён, раннер закрывает экран итогов, запускает следующий матч по настройкам «Следующий матч», и схема снова берёт управление — можно оставить всё на ночь копить статистику. Пауза — не конец матча: во время паузы бот работает как обычно, стоят только игровые часы.
## Доверие и безопасность
**Схема — это код, и при запуске у неё те же права, что и у вас** (читать и записывать файлы, выходить в сеть). Поэтому:
- схемы в `installed/` по умолчанию **не считаются доверенными**: Farsight и раннер отказываются их запускать, пока вы не нажмёте «Доверять»;
- установка поверх схемы с тем же id **сбрасывает доверие** (новая версия — это новый код);
- при импорте проверяется: zip не больше 50 MB и не больше 2000 файлов; абсолютные пути и `..` запрещены (чтобы ничего не записалось за пределы каталога схемы); некорректный манифест или отсутствующий файл входа — сразу отказ.
> **Внимание**
>
> Прежде чем доверять схеме, нажмите «Открыть папку» и прочитайте код. Берите схемы только у тех, кому доверяете.
## API (для скриптов)
| Метод | Описание |
|---|---|
| `GET /api/schemes` | Список схем + результаты + выбранная и запущенная схема каждого экземпляра |
| `GET /api/schemes/results?ref=` | Последние 30 матчей одной схемы |
| `POST /api/schemes/import` | Импорт zip |
| `GET /api/schemes/export?ref=` | Скачать zip |
| `POST /api/schemes/fork` | Копировать в мои |
| `POST /api/schemes/trust` | Доверять |
| `DELETE /api/schemes?ref=` | Удалить (отказ, если схему использует экземпляр) |
| `POST /api/instances/{n}/scheme` | Сменить схему экземпляра: сразу подхватить текущий матч или со следующего «Начать тест» |
В Python можно напрямую использовать библиотеку: `from openwar3 import schemes` (`list_schemes`, `install_zip`, `export_zip`, `fork`, `trust`, `stats`…).
## Дальше: сайт схем
Экспортированный zip — это и есть единица обмена, сайту остаётся лишь добавить слой поверх: загрузка из Farsight в один клик; скачивание с сайта с теми же проверками, что при «Импорте схемы», и тем же обязательным подтверждением доверия; по желанию — отправка результатов, а сайт сводит процент побед по версиям. Место для кнопки «Поделиться на сайте схем» в Farsight уже предусмотрено. Как идёт работа — в [дорожной карте](https://war3ai.com/ru/roadmap/).
---
# Реплики и локальные модели
> Любой юнит в игре может заговорить от любого имени — в облачке над головой. Подключите локальную LLM: фраза на входе, ответ появляется над юнитом.
Облачки — зрелищный слой: на исход игры они не влияют и подходят для стримов, комментирования и отладки.
- говорить может любой юнит от любого имени; несколько юнитов могут говорить одновременно;
- размер шрифта, цвет, ширина, хвостик, прозрачность и скорость печати настраиваются для каждого облачка отдельно;
- можно напрямую подключить локальную LLM (LM Studio) с потоковым выводом: облачко обновляется по мере генерации.
## Из бота
Проще всего — встроенный в SDK метод `say`:
```python
g.say(hero, "За мной, в атаку!", seconds=4)
```
## Запуск и интерфейс
**Проще всего — главная страница Farsight «Центр управления»**: сначала нажмите «Локальная LLM → Запустить и загрузить модель» (локальный сервер LM Studio + загрузка настроенной модели в видеопамять), затем «Облачка реплик → Запустить». На карточках можно смотреть логи, останавливать и перезапускать.
Интерфейс — на странице «Облачка реплик» в левой панели Farsight: заставить юнита говорить (выбрать юнита, написать текст, настроить стиль, поговорить с моделью), посиделки крестьян, диалоги в кадре, реакция на события, настройки модели; всё применяется к экземпляру, выбранному в верхней панели.
Можно и из командной строки:
```bash
python speech/speak_launch.py # запустить сервис локальной модели + загрузить и прогреть модель + поднять API облачков
python speech/speak_launch.py --restart # перезапустить API после изменения кода
python speech/speak_launch.py --stop # остановить API и выгрузить модель из видеопамяти
```
Каждый шаг работает по принципу «уже запущено — пропустить», поэтому повторный запуск ничего не ломает.
## HTTP API
По умолчанию `http://127.0.0.1:8872/` (порт задаётся в `ports.speech` файла `openwar3.json`); вызывать его может любая программа.
### Заставить юнита говорить `POST /api/say`
```json
{
"inst": 16,
"bubbles": [
{ "unit": "0x14A12614", "name": "Горный король", "text": "За мной, в атаку!" },
{ "unit": "0x14A12924", "name": "Архимаг", "text": "Сейчас будет снежная буря.",
"style": { "font_px": 26, "text_color": "#FFE080", "bg_color": "#C0102040" } },
{ "screen": [960, 110], "key": 1, "name": "Рассказчик", "text": "Первая волна орков прибудет через 30 секунд.",
"style": { "tail": false, "type_ms": 0 } },
{ "world": [-4684, 2644], "key": 2, "text": "Точка сбора", "style": { "font_px": 16 } }
]
}
```
| Поле | Описание |
|---|---|
| `unit` / `world` / `screen` | Одно из трёх: следовать за юнитом (если у него есть полоска здоровья — прямо над ней) / координаты карты / пиксели экрана (для закадрового текста) |
| `name` | Имя говорящего в первой строке — любое, не обязательно имя этого юнита |
| `text` | Текст реплики, переносится автоматически |
| `duration_ms` | Сколько показывать; 0 = автоматически 3 ~ 5 секунд |
| `key` | Номер облачка в мире / на экране: новое сообщение с тем же key заменяет старое |
| `update` | Если такое облачко уже есть — заменить только текст, не сбрасывая таймер (для потокового вывода) |
| `style` | `font_px`, `max_width_px`, `text_color`, `bg_color`, `border_color`, `tail`, `side_px`, `opacity`, `type_ms`, `font`… |
Одновременно — не больше 32 облачков; накладные расходы — в среднем около 0.1 ~ 0.2 ms за кадр.
### Разговор с локальной моделью `POST /api/chat`
```json
{
"inst": 16, "unit": "0x14A12614", "name": "Горный король",
"persona": "Ты играешь Мурадина, Горного короля из Warcraft: прямолинейный, любит выпить. Одна-две разговорные фразы, не длиннее 40 символов.",
"message": "Впереди толпа огров. Атакуем или нет?",
"stream": true
}
```
Возвращается `{"reply": "...", "first_token_ms": 283, "total_ms": 342}`, и к этому моменту ответ уже висит над этим юнитом. Каждый юнит помнит последние 6 обменов репликами.
### Прочее
| Метод | Описание |
|---|---|
| `GET /api/instances` | Запущенные игры |
| `GET /api/units?inst=16&mine=true&heroes=true` | Список юнитов (с китайскими названиями, координатами, здоровьем) |
| `POST /api/clear` | Убрать одно или все облачка |
| `GET /api/llm`, `POST /api/llm` | Посмотреть / изменить настройки модели (`base_url`, `model`, `max_tokens`, `temperature`) |
| `POST /api/banter` | Посиделки крестьян: рабочие на базе по очереди ворчат в образе своих персонажей, плюс объявление в начале матча (все данные о ходе игры — реальные) |
| `POST /api/camtalk` | Диалоги в кадре: герои и их свита, попавшие в кадр, разговаривают в соответствии со своими ролями |
| `POST /api/events` | Реакция на события: начало боя, конец боя, гибель героя, переход на следующий тир, потеря здания… говорят, только когда что-то случилось |
## Как выбрать локальную модель
Замеры на одной RTX 5090 (5 игровых реплик):
| Модель | Видеопамять | Скорость | Одна реплика | Вывод |
|---|---|---|---|---|
| **Qwen3.6-35B-A3B** (MoE, за раз активно только 3B), Q4, thinking выключен | 20.6 GB | ≈142 token/s | **≈0.3 s** (первый токен ≈0.27 s) | Рекомендуем: быстро, естественно отыгрывает роли на китайском |
| gpt-oss-20b (MXFP4), reasoning low | 11.3 GB | ≈280 token/s | 0.3 ~ 0.8 s | Если мало видеопамяти; китайский чуть более пресный |
| Qwen3.6-27B (плотная), Q4 | 17.2 GB | ≈39 token/s | через 5.5 s всё ещё думает | Не подходит для диалога в реальном времени |
- **Скорость определяется числом параметров, активных за раз, а не общим числом**: MoE на 35B активирует только 3B и работает в 3 ~ 4 раза быстрее плотной 27B.
- **«Размышления» обязательно выключить**: иначе все токены уходят на размышления, и до ответа дело не доходит.
- Облачко печатает около 22 символов в секунду, так что скорость генерации — уже не узкое место; по-настоящему на ощущения влияет **задержка первого токена**.
> **Чтобы реплики звучали «по-настоящему»**
>
> Передавайте модели только реальные данные о ходе игры (число матчей, победы и поражения, численность армии, запасы) и явно пишите «используй только эти факты». Проверено: без этого ограничения модель выдумывает бои, которых не было.
---
# Шлюз
> Шлюз WebSocket / JSON: публичный API, доступный из Python SDK, можно вызывать из JS, C#, Go, Rust, со страницы в браузере или из программы на другой машине. Три роли, в комплекте JS-клиент и демо-страница для браузера; задержка — быстрая полоса плюс около 1 ms.
Шлюз оборачивает быструю полосу и публикуемое состояние в **WebSocket / JSON**. Публичные методы Python SDK из [каталога API](https://war3ai.com/ru/api/) можно вызывать из JS, C#, Go, Rust, со страницы в браузере, из программы на другой машине и из LLM — с теми же именами методов и параметрами. Задержка — быстрая полоса плюс около 1 ms.
**Проще всего: главная страница Farsight «Центр управления» → Шлюз → «Запустить»** (остановка, перезапуск, логи и открытие демо-страницы — на той же карточке). Из командной строки:
```bash
python gateway/server.py # ws://127.0.0.1:8870/ws (порт — ports.gateway в openwar3.json)
python gateway/server.py --open # то же самое, а когда порт начнёт слушать, открывает демо-страницу http://127.0.0.1:8870/demo
python gateway/server.py --host 0.0.0.0 # для локальной сети: токен требуется автоматически (bin/gateway/token.txt)
python gateway/server.py --allow-origin http://localhost:5173 # чтобы подключаться могла и ваша собственная веб-страница
```
## Подключение и роли
Адрес подключения: `ws://127.0.0.1:8870/ws?inst=9&role=dev` (вместо `inst=` можно указать `pid=`; если нужен токен, добавьте `&token=`).
| Роль | Что можно вызывать | Для чего |
|---|---|---|
| `dev` | Всё: наблюдение, команды, управление игрой, песочница (JASS, меняющий мир), отрисовка интерфейса | Локальные инструменты, [игровые моды](https://war3ai.com/ru/docs/mods/), компаньоны |
| `player` (с `&player=N`) | Наблюдение, команды юнитам игрока N, отрисовка интерфейса; **по умолчанию честный режим** — видно только то, что в обзоре игрока N (`&fair=0` — выключить) | Бот или LLM, играющие за конкретного игрока |
| `observer` | Только чтение (рантайм сразу отклоняет его команды) | Наблюдение за матчем, комментирование, сбор данных |
Роли `player` недоступны: управление игрой — завершение игры, смена скорости, пауза; `players` и `enemy_ai_plan`, которые раскрывают чужие карты; `canvas.image`, из-за которого процесс игры открыл бы локальный файл; и JASS. Запросы с номером игрока, такие как `resources`, `tech` и `stats`, работают только для своего игрока.
Одно подключение — один сеанс, он занимает одну быструю полосу (всего в рантайме их 16). Одновременно шлюз держит не больше 12 сеансов, чтобы несколько полос оставалось ботам, модам и Farsight. При отключении убирается только то, что нарисовал сам этот сеанс, и его горячие клавиши; нарисованное другими программами остаётся.
## Сообщения
Сразу после подключения приходит `hello`: версия протокола, роль, идентификатор процесса игры и список методов, доступных этой роли. Дальше в каждом запросе передаётся `id`, а ответ приходит с тем же `id`:
```json
→ {"id": 1, "op": "call", "method": "units", "args": ["me"]}
← {"id": 1, "ok": true, "result": [{"addr": 123456, "type": "hpea", "owner": 0, "x": -4480, "y": 2120, "hp": 220, "hp_max": 220}]}
→ {"id": 2, "op": "call", "method": "move", "args": [[{"unit": 123456}], 100, 200]}
→ {"id": 3, "op": "call", "method": "ui.button", "args": ["shop", "Купить зелье"], "kwargs": {"screen": [40, 300]}}
→ {"id": 4, "op": "subscribe", "state": {"hz": 4, "units": "mine"}, "events": true}
← {"type": "state", ...} {"type": "events", ...} дальше приходят постоянно
→ {"id": 5, "op": "overview"} обстановка на одной странице: ресурсы, число юнитов по типам, герои, видимые враги, производство
→ {"id": 6, "op": "jass", "code": "call PingMinimapEx(0, 0, 3, 255, 0, 0, false)"} только для dev
→ {"id": 7, "op": "api"} каталог методов (есть ещё ping / unsubscribe)
```
- **Юнит в аргументах** задаётся как `{"unit": адрес}`, где адрес — это `addr` из JSON юнита; можно добавить `"handle": [lo, hi]`, чтобы проверить, что этот адрес не занял уже другой юнит.
- **Имена методов** — это публичные методы Game, а также `ui.*` (button / choice / toast / hotkey / mouse / cursor…), `canvas.*` (text / panel / bar / image / circle / path / remove…) и `jass.<имя_функции>` (только для dev).
- Передать функцию-обработчик удалённо нельзя: клики и горячие клавиши приходят в потоке событий, событие `ui.click` содержит `key`. См. [Интерфейс и ввод](https://war3ai.com/ru/docs/ui-input/).
- Ошибка в одном вызове возвращается только для этого вызова (`ok: false` и `error`), соединение не рвётся; то же самое, если прислали не JSON.
- Поля событий в JSON совпадают с [протоколом W3P](https://war3ai.com/ru/docs/protocol/), плюс удобные поля (`spell`, `key`, `text`, `chat`, `button`, `player`, `mods`).
Работает и HTTP — удобно для разовых вызовов и curl: `GET /api?role=player` выдаёт каталог методов, `POST /call` с `inst`, `role`, `method`, `args`, `kwargs` выполняет один вызов. `/call` переиспользует сеансы: если игру перезапустили и сменился процесс, автоматически открывается новый сеанс, а сеансы, простаивающие 10 минут, закрываются.
## Клиенты
**JS** (браузер или Node 22+, без зависимостей): `gateway/clients/js/openwar3.mjs`
```js
import { OpenWar3, unit } from "./openwar3.mjs";
const ow = new OpenWar3("ws://127.0.0.1:8870/ws", { inst: 9, role: "dev" });
await ow.connect();
const mine = await ow.api.units("me");
await ow.api.move(mine.slice(0, 3).map(unit), 100, 200);
await ow.api.ui.button("hi", "Нажми меня", { screen: [40, 300] }); // последний простой объект = именованные аргументы
ow.on("event:ui.click", (e) => console.log("Нажато:", e.key));
await ow.subscribe({ state: { hz: 2, units: "mine" }, events: true });
```
В Node 20 / 21 нужен флаг `--experimental-websocket`. Полный пример — в `gateway/clients/js/example.mjs`.
**Демо-страница в браузере** `http://127.0.0.1:8870/demo`: обстановка, таблица наших юнитов, кнопка, которую можно поставить в игру, поток событий — всё на одной странице.
**Другие языки**: хватит любой библиотеки WebSocket и JSON, показанного выше, — трогать общую память не нужно.
**LLM**: используйте сразу [MCP-сервер](https://war3ai.com/ru/docs/mcp/) — частые задачи в нём уже оформлены как готовые инструменты.
## Замеры
2026-09-25, по пунктам на живом матче, 16/16 (шлюз — 9 пунктов, MCP — 7): рукопожатие (у роли dev — 121 метод), `units('me')`, обстановка на одной странице, подсказка на экране, кнопка; после подписки клик по этой кнопке в игре → `ui.click` доходит до клиента; JASS; неверный юнит даёт ошибку только для этого вызова; HTTP `/call` (роль observer).
JS-клиент (Node) и демо-страница в браузере тоже проверены: по кнопке, поставленной с веб-страницы, кликнули в игре, и журнал событий страницы получил `ui.click`.
## Безопасность
- По умолчанию шлюз слушает только локальный `127.0.0.1` и токен не требует (как и Farsight). Если `--host` — не локальный адрес, токен требуется автоматически; с `--auth` токен нужен и локально.
- **Другие сайты в браузере подключиться не могут**: у любого подключения из браузера есть источник (`Origin`), а шлюз принимает только свою демо-страницу и адреса, переданные через `--allow-origin`; программы вроде Python, Node или curl источник не передают и подключаются как обычно. Когда шлюз слушает только локально, он ещё проверяет `Host` и так блокирует атаки, при которых внешний домен резолвится на локальную машину.
- Роль клиент объявляет сам при подключении: в локальном режиме это соглашение, а не граница безопасности. На Арене роли будет раздавать процесс-судья, см. [Арена](https://war3ai.com/ru/arena/).
---
# Протокол W3P
> Полный контракт между рантаймом и внешними программами: восемь блоков общей памяти, чтение состояния мира, чтение событий, отправка команд, квитанции, роли полос, холст, интерфейс и ввод. Читайте эту страницу, если подключаетесь не из Python.
Рантайм и внешние программы обмениваются данными **только через общую память**. Ниже описано всё, что для этого нужно.
- Эталонная реализация — на Python: `sdk/python/w3world.py` (чтение) и `sdk/python/w3fast.py` (запись); размер и смещения каждой структуры заданы там и закреплены тестами;
- **Протокол описывает только семантику и не зависит от версии игры.** При смене версии игры рантайм адаптируется сам, а протокол не меняется; новые поля только дописываются в конец блока, так что старые клиенты продолжают работать.
> **Примечание**
>
> Большинству эта страница не нужна — просто используйте Python SDK. Она понадобится, только если вы хотите подключаться напрямую из C++ / C# / Rust / Go или другого языка либо хотите знать, что происходит под капотом SDK.
## 1. Восемь блоков общей памяти
`` — идентификатор процесса игры.
| Имя | Направление | Содержимое | Синхронизация |
|---|---|---|---|
| `Local\War3World_` | рантайм → вы | Состояние мира: заголовок + 16 игроков + до 1024 юнитов + 256 записей с деталями юнитов + 256 предметов на земле + область расширений + таблица производства | seqlock |
| `Local\War3Trees_` | рантайм → вы | До 4096 разрушаемых объектов (деревья и т. п.), обновляется каждые 2 секунды | seqlock |
| `Local\War3Events_` | рантайм → вы | Кольцо событий, 8192 записи | у каждой записи свой порядковый номер |
| `Local\War3Map_` | рантайм → вы | Карта: клетки рельефа (128 на клетку, до 256×256) + границы игровой области + стартовые позиции; вычисляется порциями в первые секунды партии | seqlock (после вычисления больше не меняется) |
| `Local\War3Fast_` | в обе стороны | Полосы команд: 16 полос × 16 слотов; в каждом слоте одна команда + квитанция; у каждой полосы есть роль | в каждом слоте один писатель и один читатель |
| `Local\War3Canvas_` | вы → рантайм | [Холст](https://war3ai.com/ru/docs/canvas/): заголовок 64 байта + 256 элементов × 112 байт + пул текста / точек 64 KB; создаётся только после одного вызова `canvas_enable` | seqlock (пишете вы, рантайм читает каждый кадр) |
| `Local\War3Msgs_` | рантайм → вы | Кольцо экранных сообщений: полный текст подсказок игры, чата и системных сообщений, 128 записей × 256 байт | у каждой записи свой порядковый номер |
| `Local\War3Input_` | в обе стороны | [Интерфейс и ввод](https://war3ai.com/ru/docs/ui-input/): рантайм записывает обратно позицию мыши, точку на земле под курсором и элемент под курсором; вы записываете таблицу горячих клавиш и переключатели мыши; рантайм начинает перехватывать ввод только после одного вызова `input_enable` | таблица горячих клавиш — seqlock |
**Несколько клиентов одновременно работают с холстом и вводом**: каждый из этих блоков существует в единственном экземпляре, и если клиенты пишут в них независимо, они затирают друг друга. Соглашения такие, и в своём клиенте их тоже нужно соблюдать:
- **Холст**: чтение — изменение — запись под именованным мьютексом `Local\War3CanvasMutex_`; заменяйте только свои элементы, чужие оставляйте как есть (с перестройкой смещений в пуле); элементы, чей процесс-владелец завершился, и элементы без владельца удаляйте. В элементе `reserved[1]` = идентификатор процесса-владельца, `reserved[2]` = порядковый номер внутри процесса; номера элементов выдаёт счётчик по смещению 60 в заголовке блока (начиная с `0x10000`).
- **Ввод**: каждый клиент регистрирует свои горячие клавиши и переключатели мыши в `Local\War3InputClients_` (заголовок 16 байт + 16 клиентов × 528 байт), под `Local\War3InputMutex_` обновляет свою запись, а затем объединяет живых клиентов и записывает результат в блок ввода: горячие клавиши дедуплицируются по паре «код клавиши + модификаторы», переключатели мыши объединяются. События получают все клиенты, и каждый узнаёт свои горячие клавиши по паре «код клавиши + модификаторы». Пока в таблице регистрации есть другие живые клиенты, не отправляйте `input_enable 0`.
- **Рантайм**: кликабельные элементы, чей процесс-владелец завершился, больше не перехватывают клики; раз в 2 секунды рантайм просматривает таблицу регистрации и, если все зарегистрированные клиенты завершились, обнуляет таблицу горячих клавиш и переключатели мыши в блоке ввода.
## 2. Чтение состояния мира (seqlock)
```text
loop:
s1 = block.seq (смещение 8, int32)
if s1 нечётное: повторить (рантайм сейчас пишет)
скопировать заголовок + players + units[unitCount] + details[detailCount] + items[itemCount]
if block.seq != s1: повторить
```
- **Заголовок**: счётчик публикаций (не растёт = публикация остановилась), игровые часы движка, epoch (+1 за каждую партию), номер своего игрока, идёт ли партия, скорость игры, период публикации, сколько микросекунд ушло на сбор этой копии в игровом потоке, порядковый номер событий, время по этапам. Клиент может записать `requestedPeriodMs`, чтобы запросить период публикации (16 ~ 1000 ms).
- **Юнит** (112 байт): пара дескрипторов (**опознавайте юниты по паре дескрипторов** — адреса переиспользуются), четырёхсимвольный код типа, владелец, флаги, координаты, здоровье / мана (с максимумами), текущий приказ + цель приказа, текущая цель (кого юнит реально атакует), уровень героя / опыт / очки навыков, индекс деталей, `visibleTo` (бит p = игрок p видит юнит прямо сейчас).
- **Детали** (288 байт; герои > юниты игроков > крипы; до 256): 12 способностей (код / уровень / флаги / оставшаяся перезарядка в секундах), 8 кодов баффов, 6 ячеек инвентаря.
- **Расширения в конце блока** (только дописываются, прежние смещения не сдвигаются, старые клиенты продолжают работать): область расширений `EXT1` (игровое время суток, скорость смены дня и ночи, число записей в таблице производства) и таблица производства `prods[128]` (здания, которые сейчас нанимают / исследуют / строят / улучшают, очередь, общая длительность, прошедшее время, не застряло ли). **Используйте, только если совпадает magic.**
## 3. Чтение событий
```text
head = ring.writeSeq (смещение 8)
for seq in (cursor, head]:
e = ring.events[(seq - 1) % 8192]
if e.seq > seq: одно событие потеряно (перезаписано, потому что вы читали слишком медленно)
elif e.seq != seq: ещё не дописано, прочитать в следующий раз
else: обработать e
```
Структура события — 64 байта: `seq kind clockMs addr handleLo handleHi typeId owner a b x y value extra`.
- Получаются сравнением двух соседних публикаций (точность = период публикации): `unit.appeared / unit.died / unit.removed / unit.damaged / order.changed / hero.levelup / owner.changed / item.appeared / item.removed / game.started`;
- уровня движка (рантайм записывает их прямо в игровом потоке в момент удара, так что событие есть на **каждый удар**): `damage` (источник, тип урона, тип атаки, позиция, фактически снятое здоровье, урон до учёта брони), `killed` (убийца);
- получается отслеживанием таблицы производства: `production.done` (четырёхсимвольный код готового, категория, сколько игровых секунд заняло; приходит и для противника);
- проверяются рантаймом при каждой публикации: `spell.cast` (способность ушла на перезарядку: `a` — четырёхсимвольный код способности, `b` — уровень, `value` — перезарядка в секундах, `x/y` — точка применения), `player.left` (`a` — номер игрока, `b` — новое состояние слота), `selection.changed` (выделение локального игрока; полный список — в области расширений блока мира), `game.ended` (выход из матча);
- экранные сообщения: `message` (`a` = порядковый номер сообщения, полный текст ищите в общей памяти `Local\War3Msgs_`: 128 записей × 256 байт, там и подсказки игры, и чат, и системные сообщения; `b` = номер окна сообщений);
- интерфейс и ввод (после `input_enable`): `ui.click` (`a` — id элемента холста, `b` — 1 левая кнопка / 2 правая), `ui.hover`, `hotkey` (`a` — id горячей клавиши, `b` — код виртуальной клавиши), `mouse.world` (`x/y` — координаты на земле, `value` = 1 означает, что клик поглощён); модификаторы всегда в `extra`.
## 4. Отправка команд
1. **Один клиентский объект занимает одну полосу**: захватите `Local\War3FastMutex_`, найдите свободную полосу (или полосу, процесс-владелец которой умер) и запишите роль, номер игрока и свой pid. Если одному процессу нужны две роли, откройте две полосы;
2. заполните слоты: флаг семантической команды, код операции, `args[11]`, крайний срок `deadlineMs`;
3. когда все слоты записаны, пометьте их отправленными и увеличьте `submitSeq` полосы на 1;
4. дождитесь события `Local\War3FastDone__` (или опрашивайте), прочитайте квитанции и верните слоты.
Рантайм выполняет команды пачками внутри диспетчеризации событий игрового потока: бюджет времени на одно опустошение — **4 ms** (реальный высокоточный таймер); всё, что не уложилось, остаётся до следующей диспетчеризации. **Слоты с истёкшим крайним сроком не выполняются** — ситуации «после снятия паузы старые команды выполнились ещё раз» не бывает.
Индексы `args`: `0..2` юнит (адрес, handle lo, handle hi), `3` номер приказа или четырёхсимвольный код, `4..6` цель, `7/8` x / y (биты float), `9` extra (номер игрока / номер ячейки / переключатель / флаг очереди), `10` mode (0 без цели / 1 в точку / 2 в цель).
### Коды операций
| Код операции | Имя | Описание |
|---|---|---|
| 1 | `point` | Приказ юниту в точку (движение / атака с движением / патруль / атака земли / заклинание в точку). extra bit0 = в очередь (после текущего приказа) |
| 2 | `target` | Приказ юниту на цель (атака правым кликом / добыча / ремонт / заклинание на цель / подбор предмета); цель должна быть видна |
| 3 | `immediate` | Команда без цели (стоп / удерживать позицию / нанять / исследовать / улучшить / заклинание без цели) |
| 4 | `build` | Рабочий строит здание (координаты выравниваются по 32) |
| 5 | `learn` | Герой изучает способность |
| 6 | `use_item` | Использовать ячейку инвентаря с номером extra |
| 7 | `revive` | Воскресить героя в алтаре |
| 8 | `rally` | Точка сбора (в точку / на цель) |
| 9 | `buy` | Магазин продаёт предмет стоящему рядом герою |
| 10 | `item_drop` | Предмет покидает инвентарь: передать союзнику, продать в магазин (`code` = юнит-получатель) или бросить на землю |
| 20 ~ 25 | Запросы | `q_tech` счётчик технологий, `q_feasible` выполнимость, `q_visible` видимость, `q_mine_gold` остаток золота в руднике, `q_captain` капитан компьютерного AI, `q_dead_heroes` список погибших героев |
| 30 | `pause` | Пауза / продолжить |
| 40 ~ 50 | Камера | Прочитать состояние камеры, задать поле, навести на точку, следовать, сбросить, повернуть, границы, сглаживание, показ/скрытие интерфейса, чистая картинка, туман |
| 60 ~ 63 | HUD | Текст кнопки заданий, заголовок и описание панели заданий, обновить, прочитать, открыта ли панель |
| 70 | `jass` | Вызов JASS native по имени (всего 1291): имя и строковые аргументы кладутся в дополнительную область слота, остальные аргументы — в `args` согласно сигнатуре; результат возвращается в `value[0]`. Только для полосы локальных инструментов; вызовы с аргументами-функциями или приостанавливающие поток скрипта всегда отклоняются. См. [JASS-канал](https://war3ai.com/ru/docs/jass/) |
| 71 / 72 | `jass_handle_of` / `jass_unit_of` | Преобразование между юнитом из снимка и дескриптором JASS в обе стороны (пара дескрипторов в снимке — это не дескриптор JASS) |
| 73 | `canvas_enable` | Создаёт общую память холста и ставит перехват отрисовки; отправить может любая полоса (холст рисуется только на экране этой машины). В первый раз ставится перехват — задайте тайм-аут от 2 секунд |
| 74 | `input_enable` | `extra` = 1 — перехватывать ввод окна игры (клики / наведение на элементы холста, горячие клавиши, клики по земле), 0 — вернуть. Блок ввода `Local\War3Input_`: заголовок 128 байт + 32 горячие клавиши × 16 байт; вы записываете таблицу горячих клавиш и переключатели мыши, рантайм записывает обратно позицию мыши, точку на земле под курсором и элемент под курсором. Отправить может любая полоса (влияет только на локальный ввод). См. [Интерфейс и ввод](https://war3ai.com/ru/docs/ui-input/) |
## 5. Квитанции
Квитанция занимает 52 байта (+8 байт замеров времени): `status`, `engineReturn`, `verdict` (код причины отказа), `orderBefore / orderAfter` (приказ юнита, прочитанный в том же кадре), `value[8]` (результаты запроса), `execUs` (сколько микросекунд команда выполнялась в игровом потоке), `engineUs` (из них — сама функция приказа движка).
Все коды состояния и коды причин см. в разделе [Квитанции и коды причин](https://war3ai.com/ru/docs/reason-codes/).
## 6. Роли полос
| Роль | Что может |
|---|---|
| `dev` | Локальные инструменты: семантические команды (управление юнитами локального игрока) + JASS-канал |
| `player` | Только семантические команды и только для юнитов игрока, которому принадлежит полоса (чужие = `not_owner`) |
| `observer` | Только запросы, камера, чтение состояния панели HUD, включение холста и локального ввода; всё остальное — `forbidden` |
Два AI играют друг против друга = две полосы `player` в одной партии (player 0 / player 1).
> **Внимание**
>
> В локальном режиме роль объявляет сам клиент (это соглашение, а не граница безопасности). На [Арене](https://war3ai.com/ru/arena/) полосы создаёт процесс-арбитр и выдаёт каждому участнику только полосу `player`.
## 7. Семантика, проверенная в реальных партиях
- Правый клик (smart) по врагу = атаковать **именно его** (и цель приказа, и текущая цель — этот юнит); «сырой» приказ атаки, отправленный как команда на цель, только переключает приказ на атаку, не запоминая цель, и юнит уходит бить кого-то другого поблизости;
- движок не принимает команды на цель по невидимым юнитам: с наступлением ночи дальние лагеря уходят в туман войны, и любой правый клик отклоняется (1001);
- «принята» у постройки означает лишь, что рабочий принял приказ: точка в лесу тоже принимается сразу, а неудача случается, только когда рабочий дойдёт до места; явно занятые точки отклоняются сразу;
- героя можно воскресить лишь примерно через 3 игровые секунды после смерти; при нехватке пищи тоже будет отказ (герои занимают пищу);
- предметы в инвентаре не считаются предметами на земле; при подборе приходит `item.removed`;
- на паузе часы движка стоят, но команды отдавать можно;
- у игры, запущенной свёрнутой, симуляция стоит (часы не идут).
---
# Квитанции и коды причин
> Квитанция каждой команды содержит код статуса и код причины. На них опирается самокоррекция ботов и агентов: «почему не получилось» превращается в машиночитаемое число.
```python
r = g.train(barracks, "hfoo")
bool(r) # False
r.status # 1 -> rejected
r.verdict # 3 -> не хватает пищи
r.reason # 'rejected(人口不够)' (= «не хватает пищи»)
r.exec_us # сколько микросекунд команда выполнялась в игровом потоке
```
`if r:` эквивалентно `r.status == 0` (движок принял команду).
## Код статуса `status`
| Код | Имя | Значение | Типичная причина |
|---|---|---|---|
| 0 | `accepted` | Движок принял команду | — (но «принята» ≠ «выполнена», см. ниже) |
| 1 | `rejected` | Движок отклонил команду | Смотрите `verdict` |
| 2 | `bad_unit` | Юнита нет или дескриптор не совпадает | Юнит уже погиб; использован устаревший объект юнита |
| 3 | `not_owner` | Это не ваш юнит | Командуете чужим юнитом от имени `player` |
| 4 | `fault` | Исключение при выполнении (рантайм его перехватил, игра не упадёт) | Сообщите нам, приложив шаги воспроизведения |
| 5 | `bad_args` | Неверные аргументы | Ошибка в координатах, номере ячейки или четырёхсимвольном коде |
| 6 | `unsupported` | Не поддерживается | В этой версии рантайма нет такой возможности |
| 7 | `bad_target` | Недопустимая цель | Цели уже нет; неподходящий тип цели |
| 8 | `forbidden` | Роль полосы этого не позволяет | Приказ от имени `observer` |
| 97 | `cancelled` | В блоке пачки возникло исключение, вся пачка не отправлена | Ошибка в коде внутри блока `with g.batch():` |
| 98 | `held` | Юнита удерживает слой с более высоким приоритетом, команда не отправлена | Этого юнита держит рефлекторный слой эталонного мозга или ручной приказ из консоли |
| 99 | `timeout` | Тайм-аут | Игра на паузе или подвисла, и дедлайн истёк (просроченные команды уже не выполняются) |
## Код причины `verdict`
При отклонении рантайм объясняет причину с помощью собственной проверки выполнимости движка. Можно и не отдавать приказ, а сначала спросить: `g.can_do(юнит, код)` возвращает те же коды.
| Код | Значение | Что делать |
|---|---|---|
| 0 / 220 | Можно | — |
| 3 | Не хватает пищи | Постройте здание для пищи; `g.production(b).blocked` покажет проблему заранее |
| 8 | Не хватает золота | Подождите; перед приказом проверяйте `g.can_afford(code)` |
| 9 | Не хватает древесины | Отправьте больше рабочих на лес |
| 32 | Очередь тренировки заполнена (7 мест) | Держите в очереди 1 юнит: ставьте следующего, когда `g.queue(b)` опустеет |
| 183 | Нет требуемой технологии / здания | Сначала постройте требуемое здание или перейдите на следующий тир |
| 185 | Здание занято | Алтарь воскрешает героя; главное здание нельзя улучшить, пока его очередь не пуста |
| 221 | Такого пункта нет / строится / улучшается / уже существует | Герой уже есть (погибшего нужно `revive`); этот магазин такое не продаёт |
| 89 | Товар ещё не поступил в магазин | В начале матча товары появляются по времени из таблицы предметов; у только что построенного магазина отсчёт идёт с момента завершения постройки |
| 1001 | Цель не видна | Цель в тумане войны или под чёрной маской; сделайте `attack_move` в её позицию |
## «Принята» ≠ «выполнена»
Квитанция лишь сообщает, что «движок принял команду», и считывается в том же кадре. Что произойдёт потом, она не отражает:
| Команда | Что может сорваться уже после принятия | Как проверить |
|---|---|---|
| Строительство | Точку в лесу движок тоже сразу принимает, а срывается всё, когда рабочий дойдёт до места | Используйте `build_near` (он следит, появился ли фундамент) или ждите `production.done` |
| Применение способности | Прервали, не хватило маны | На следующем тике проверьте, ушла ли способность на перезарядку: `g.cooldown(u, способность)` |
| Тренировка | Встала в очередь, но пищи не хватает, и она не начинается | `g.production(b).blocked` |
| Движение / атака | Перебита другой логикой (или слоем с более высоким приоритетом) | `g.current_target(u)`, `g.order_of(u)` |
## Методы-запросы
Эти методы не отдают приказов, а только спрашивают движок; результат тоже кладётся в поле `value` квитанции (SDK возвращает само значение):
| Метод | Возвращает |
|---|---|
| `g.can_do(u, code)` / `g.can_do_many([(u, code), ...])` | Код причины из таблицы выше |
| `g.tech(code, player=None)` / `g.tech_many([...])` | Уровень исследования / число построенных зданий (с учётом цепочки улучшений) |
| `g.visible(x, y)` | Видна ли эта точка нашей стороне |
| `g.gold_left(mine)` | Сколько золота осталось в руднике |
| `g.enemy_ai_plan(вражеский_юнит)` | Куда капитан компьютерного противника поведёт армию (работает только для компьютерного ИИ) |
---
# Откуда данные
> Источник и точность каждого вида данных. Если что-то «выглядит не так», начните с этой страницы.
| Данные | Источник | Точность |
|---|---|---|
| Юниты, ресурсы, приказы, способности, баффы, инвентарь | Блок мира, который рантайм публикует каждые 50 ms | Период публикации (можно уменьшить до 16 ms) |
| События урона и убийств | Рантайм записывает прямо в игровом потоке, каждый удар | Мгновенно |
| Прочие события (появление, гибель, смена приказа, повышение уровня…) | Сравнение двух соседних публикаций | Период публикации |
| Таблица производства (тренировка / исследование / строительство / улучшение) | Поля таймера в способностях производства движка + прошедшее время, которое накапливает рантайм | Около ±0.2 игровой секунды |
| Боевые характеристики, таблица бонусов типов атаки и брони | Встроенные таблицы данных игры (извлекаются из вашей локальной копии игры) | Без учёта модификаторов от предметов, аур и баффов |
| Поиск пути | Проходимость рельефа в движке (клетка 128) + деревья + площадь зданий, A* на стороне SDK | Одна клетка; проходы уже клетки считаются непроходимыми |
| Игровое время суток | JASS `GetFloatGameState(GAME_STATE_TIME_OF_DAY)` | Период публикации |
| Видимость | Маска видимости, которую рантайм считает для каждого юнита по каждому игроку | Период публикации |
| Счётчики технологий, выполнимость, остаток золота в руднике | Запрос через быструю полосу напрямую к движку | Мгновенно |
## Игровые данные не распространяются с кодом
Таблицы юнитов, способности, предметы, герои, баффы, таблица бонусов урона и т. п. взяты из игровых файлов Blizzard и **в репозиторий не входят**. Когда вы укажете папку игры в «Центре управления» Farsight, они будут автоматически извлечены из вашей собственной игры; можно запустить извлечение и вручную:
```bash
python data/tools/extract_game_data.py
```
Результат извлечения лежит в `data/game/` (в git не попадает): исходные `.slk` / `.txt`, а также подготовленные `units.json`, `names.json`, `skills.json`, `items.json`, `heroes.json`, `buffs.json`.
## Несколько конкретных чисел
| Величина | Значение |
|---|---|
| Сутки | 480 игровых секунд (день и ночь — по 240 секунд), один час = 20 игровых секунд; матч начинается в 8 утра |
| День | 6:00 ~ 18:00 |
| Коэффициент брони | 0.06 (из таблиц данных игры) |
| Ёмкость блока мира | 16 игроков, 1024 юнита, 256 наборов подробных данных юнитов, 256 предметов на земле, 128 записей производства |
| Деревья | До 4096 разрушаемых объектов, обновление каждые 2 секунды |
| Кольцо событий | 8192 записи; при слишком медленном чтении события теряются (SDK это обнаруживает) |
| Клетки карты | 128 игровых единиц на клетку, максимум 256 × 256 |
## Примеры, откалиброванные измерениями
- Время производства: крестьянин 14.9, ферма 34.9, улучшение Iron Forged Swords 59.9 игровой секунды — совпадает с тем, что публикует рантайм (расхождение меньше 0.2 секунды);
- Боевые характеристики сверены с игровой панелью: Паладин — 650 здоровья, 255 маны, броня 3.9, атака 24 ~ 34; пехотинец с первым улучшением атаки — 13 ~ 15;
- Урон до учёта брони в событиях урона движка (14 / 15 / 15) попадает в диапазон, рассчитанный `stats()`;
- Поиск пути: Echo Isles — 116 × 88 клеток, путь по земле до главного здания соперника 10642 (по прямой 9856), построение сетки 18 ms, один проход A* ≈1 ms.
---
# Частые вопросы
> Это чит? Какие версии поддерживаются? Что ИИ видит и что умеет? Можно ли обойтись без программирования?…
## Это чит?
Нет. Это интерфейс для разработчиков, созданный для исследований ИИ и развлечения. Он работает только с **клиентом, которым вы владеете на законных основаниях**: на вашем компьютере, в локальной сети или в собственных играх — против компьютера или других ИИ. Его **нельзя использовать в Battle.net и на любых серверах с античитом**, и в нём нет никаких функций для игр против живых людей. Подробнее — в [условиях использования](https://war3ai.com/ru/docs/legal/).
## Какие версии игры поддерживаются?
Сейчас только **Warcraft III 1.27** (The Frozen Throne). Версии 1.24 ~ 1.28 построены на одном и том же движке; поддержка нескольких версий (таблица символов под каждую версию, поиск по сигнатурам как запасной вариант, самопроверка при запуске со списком доступных возможностей) запланирована на этапе P4 [дорожной карты](https://war3ai.com/ru/roadmap/). Версии начиная с 1.29 и Reforged — другой движок, их нужно адаптировать отдельно; пока мы этого не обещаем.
## Изменяются ли мои файлы игры?
Нет. Рантайм внедряется во время работы игры и **не изменяет Game.dll на диске** и любые другие файлы игры. Для запуска нескольких копий оригинальный загрузчик `War3.exe` просто копируется под другим именем. Игровые данные (таблицы юнитов и т. п.) извлекаются из вашей собственной игры и вместе с кодом не распространяются.
## Что видит ИИ?
По сути всё, что хотел бы знать профессиональный игрок, с обновлением каждые 50 ms:
- золото, древесину и пищу всех игроков; позицию, здоровье и ману, текущий приказ, **кого юнит сейчас атакует**, уровень и опыт каждого юнита;
- уровни способностей и оставшуюся перезарядку у героев и юнитов, баффы на них, инвентарь;
- что тренирует / исследует / строит / улучшает каждое здание, какой прогресс и не застряло ли производство из-за нехватки пищи;
- предметы на земле, деревья, сетку проходимости и пригодности для застройки, стартовые точки, игровое время (день и ночь);
- поток событий: появление и гибель юнитов, **каждый удар** (кто ударил, тип атаки, урон до учёта брони), убийства, завершение производства, повышение уровня героя…
- Кроме того, можно напрямую спросить движок: можно ли сейчас что-то сделать и почему нельзя; какой уровень у технологии; видна ли точка; сколько золота осталось в руднике; куда компьютерный противник поведёт армию.
Поверх этого SDK уже считает боевые характеристики (бонусы типов атаки и брони, броню, улучшения атаки и брони), «за сколько секунд его убить» и поиск пути по земле. Все методы — в [каталоге API](https://war3ai.com/ru/api/).
## Что умеет ИИ?
Практически всё, что может игрок: движение, атака с движением, атака конкретной цели, стоп, удержание позиции, патруль, атака по земле, добыча, ремонт, строительство (с автоматическим поиском места), тренировка / исследование / улучшение, отмена, изучение способностей, применение способностей (на юнита / в точку / без цели), точка сбора, воскрешение героев, поднять / использовать / выбросить / передать / продать предмет, покупки, призыв к оружию; очередь через Shift, марш по точкам маршрута, постройка нескольких зданий одним рабочим подряд; а ещё скорость игры, пауза и облачка реплик над юнитами. На каждую команду приходит квитанция.
Помимо действий игрока, можно рисовать поверх игрового экрана собственные панели и пометки ([холст](https://war3ai.com/ru/docs/canvas/)), а в одиночной игре — вызывать 1291 функцию JASS, доступную авторам карт ([JASS-канал](https://war3ai.com/ru/docs/jass/)).
## Можно ли использовать на RPG / пользовательских картах?
Да. Выберите RPG-карту, назначьте экземпляру схему «Пример компаньона» (buddy), и после начала матча играйте сами — рядом с вами будет ИИ-напарник, который помогает в бою, лечит вас и болтает с вами; см. [RPG-компаньон](https://war3ai.com/ru/docs/companion/). `g.map_data` читает имена пользовательских юнитов карты; [JASS-канал](https://war3ai.com/ru/docs/jass/) умеет создавать юнитов, настраивать союзы, открывать панели… Как играть — решать вам. Действия, меняющие мир, доступны только в одиночной игре (в многопользовательской возникнет рассинхронизация), а холст безопасен и в многопользовательской игре.
## Можно ли пользоваться без программирования?
Да. Настройте окружение по [быстрому старту](https://war3ai.com/ru/docs/quickstart/), затем откройте страницу [Пишем бота с помощью LLM](https://war3ai.com/ru/docs/ai-bot/): вы простыми словами описываете стиль игры, а LLM пишет код. Если что-то не работает, передайте ей текст ошибки или опишите, что видите в игре, — и попросите исправить.
## Только Python?
SDK написан на Python. Между рантаймом и внешней программой есть только протокол общей памяти ([W3P](https://war3ai.com/ru/docs/protocol/)), так что подключиться может любой язык, умеющий читать и писать общую память Windows. Проще всего — через [шлюз](https://war3ai.com/ru/docs/gateway/) (WebSocket / JSON): те же методы можно вызывать из JS, C#, Go, Rust, со страницы в браузере и из программы на другой машине; LLM-агента можно подключить напрямую через [MCP](https://war3ai.com/ru/docs/mcp/).
## Какая LLM лучше?
Подойдёт любая популярная модель, умеющая писать код. Главное — не модель, а **правильные материалы** (руководство + `api.json` + один пример) и требование использовать только методы из каталога API. Решения в реальном времени по ходу матча (советник, озвучка) чувствительны к задержке — локальные MoE-модели справляются с ними очень хорошо, см. [LLM как советник](https://war3ai.com/ru/docs/llm-coach/) и [Реплики и локальные модели](https://war3ai.com/ru/docs/speech/).
## Не замедлит ли это игру?
Один сбор состояния мира занимает в игровом потоке в медиане 0.5 ~ 0.9 ms (100 ~ 120 юнитов) и выполняется раз в 50 ms. Одна команда занимает в игровом потоке несколько микросекунд; на одну обработку очереди отводится бюджет 4 ms, а всё, что не успело выполниться, переносится на следующую, так что игру это не тормозит. Все вызовы в игру защищены обработкой исключений: если бот упал, останавливается только его сторона, а игра продолжает работать.
## Можно ли запустить несколько игр одновременно?
Да. Оркестрацией нескольких экземпляров занимается `runtime/farm.py`, у каждого экземпляра свой номер; запускать и останавливать их можно из [консоли Farsight](https://war3ai.com/ru/docs/console/). Ваш бот подключается к нужному экземпляру через `--inst N`.
## Можно ли стравить двух ИИ?
Откройте в одном матче два канала `player` (`--player 0` / `--player 1`) — это и есть ИИ против ИИ. В локальном режиме честность держится на договорённости; официальные бои с судьёй, фильтрацией по обзору и проверкой владения юнитами будут на [Арене](https://war3ai.com/ru/arena/) (этап P6).
## Есть ли поддержка Mac / Linux?
Пока поддерживаются только Windows 10 / 11.
## Какая лицензия?
Лицензия будет объявлена вместе с официальным релизом. Сторонние компоненты сохраняют свои лицензии (например, MinHook — BSD-2); у AMAI собственная лицензия, производные от неё данные с проектом не распространяются, а генерируются при установке из публичного репозитория AMAI.
## Куда сообщать о проблемах?
Канал обратной связи откроется после официального релиза. В сообщении укажите номер экземпляра, вывод `python -m openwar3 status` и шаги воспроизведения. Но сначала загляните в [Отладку и производительность](https://war3ai.com/ru/docs/debugging/) — возможно, ответ уже там.
---
# Условия использования
> Что можно и чего нельзя, статистика посещений этого сайта, а также сведения о товарных знаках и сторонних лицензиях. Используя проект, вы соглашаетесь соблюдать эти условия.
## Можно
- Использовать проект с **клиентом Warcraft III 1.27, которым вы владеете на законных основаниях**;
- на своём компьютере, офлайн, в локальной сети или в собственных играх устраивать бои ИИ против компьютерного противника или других ИИ;
- исследовать, обучать, развлекаться, стримить матчи своего ИИ;
- разрабатывать собственные проекты на основе SDK, эталонного мозга, примеров и инструментов, соблюдая их лицензии.
## Нельзя
- **Использовать проект в Battle.net и на любых серверах и платформах с античитом**, а также одновременно с активной сессией античита;
- использовать его для получения нечестного преимущества в играх против живых людей;
- распространять игровые файлы Blizzard или извлечённые из них данные (проект их тоже не распространяет: игровые данные пользователь извлекает из своей игры сам);
- нарушать условия лицензии на использование рантайма.
## Наши технические обязательства
- Мы не изменяем `Game.dll` на диске и любые другие файлы игры; все изменения происходят только во время выполнения;
- для запуска нескольких копий оригинальный загрузчик `War3.exe` лишь копируется под другим именем;
- в проекте нет кода или игровых файлов Blizzard.
## Ваша ответственность
Законы об обратной разработке и модификации игр в разных странах различаются. **Пользователь сам определяет, законно ли использование проекта там, где он находится, и сам несёт ответственность за последствия.** Проект предоставляется «как есть», без каких-либо явных или подразумеваемых гарантий.
## Статистика посещений этого сайта
Этот сайт (war3ai.com) собирает статистику посещений с помощью Microsoft Clarity: какие страницы просматривают, откуда приходят, сколько времени проводят, где кликают и докуда прокручивают, а также анонимные записи сеансов и тепловые карты. Мы используем её только для того, чтобы улучшать документацию и страницы.
- Регистрация не нужна, и мы не собираем идентифицирующие данные вроде имени или адреса электронной почты; текст в полях ввода по умолчанию скрыт и не записывается;
- Clarity сохраняет в браузере cookie, чтобы отличать повторные визиты одного и того же посетителя; данные обрабатывает Microsoft, см. [Заявление о конфиденциальности Microsoft](https://privacy.microsoft.com/privacystatement);
- Если не хотите попадать в статистику: один раз откройте любой адрес этого сайта, добавив в конец `?stats=off`, — и в этом браузере статистика больше собираться не будет (`?stats=on` включает её снова); можно также заблокировать `clarity.ms` функцией защиты от отслеживания в браузере — сайт при этом работает как обычно.
Локальные Farsight, SDK и рантайм такой статистики не содержат. Farsight обращается к war3ai.com только в двух случаях: при запуске и затем каждые 6 часов он читает список версий, чтобы узнать, не вышла ли новая; когда вы нажимаете «Отправить» на странице «Отзывы и предложения», отправляются ваш отзыв и идентификатор этого компьютера (солёный хеш системного номера: восстановить по нему исходное значение нельзя, он нужен для защиты от спама); диагностическая информация прикладывается, только если вы отметите соответствующий флажок, и перед отправкой её можно просмотреть.
Когда эти два вида запросов поступают на war3ai.com, сервер записывает IP-адрес, определённую Cloudflare страну или регион и версию клиента (User-Agent) — это применяется для защиты от злоупотреблений и подсчёта количества используемых экземпляров Farsight. Записи о проверке версии автоматически удаляются через 90 дней; отзывы вместе с этой информацией хранятся до тех пор, пока сопровождающие проекта не обработают их и не удалят. Эти данные видны только сопровождающим проекта в административной панели и не передаются кому-либо ещё.
## Товарные знаки
Warcraft® и 魔兽争霸® являются товарными знаками или зарегистрированными товарными знаками Blizzard Entertainment, Inc. War3AI / OpenWar3 — независимый проект сообщества: он не связан с Blizzard Entertainment, не одобрен и не спонсируется ею. Прочие упомянутые названия продуктов (Claude, GPT, Gemini, Qwen и т. д.) принадлежат их владельцам и используются только для указания совместимости.
## Сторонние компоненты и данные
| Компонент / данные | Лицензия | Как используется |
|---|---|---|
| MinHook | BSD-2-Clause | Используется вместе с рантаймом, уведомление о лицензии сохраняется |
| AMAI | Собственная лицензия | С проектом не распространяется; при развёртывании `start.bat` скачивает его из публичного репозитория AMAI, после чего инструменты генерируют данные для эталонного мозга |
| Игровые данные (юниты, способности, предметы и т. п.) | Blizzard | С проектом не распространяются; пользователь извлекает их из своей игры |
| Фактические данные из публичных записей матчей (места постройки зданий, дебютные порядки) | — | Только фактические данные, используются эталонным мозгом |
---
# Каталог API (api.json)
Статус: verified = низкоуровневый путь проверен в реальных играх; experimental = новый метод, уже работает, проверка в реальных играх идёт по пунктам; inferred = выведено / не полностью проверено. Задержка:Push-снимок(Чтение общей памяти без ожидания игрового потока (≈ 0.05 ms)); Быстрая полоса(≈ 1 кадр: пачки исполняются в игровом потоке); Канал управления(20~40 ms (старый путь для операций с интерфейсом)); Прямая запись(Без очереди к игровому потоку: запись в общую память (холст) или сообщение окну игры); Локальный расчёт(Чистые вычисления или чтение файла, игру не трогает)
## Наблюдение
Чтение состояния без изменения игры. Почти всё читается напрямую из push-снимка, без ожидания.
- `snapshot(max_age: 'float' = 0.05)` [verified] [Push-снимок] Полное состояние всей карты (WorldState): .units .players .items .clock .me; повторные вызовы в пределах max_age секунд возвращают одну и ту же копию.
⚠ Рабочих внутри золотого рудника в списке нет; по умолчанию видна вся карта (в модели lockstep локально есть всё), фильтрация по зоне обзора — только при Game(fair=True). (Механизм: Блок мира W3P Local\War3World_ (рантайм публикует каждые 50 ms, seqlock))
- `last_seen(owner: 'str | int' = 'enemy', max_age: 'float | None' = None) -> 'list'` [verified] [Push-снимок] Последние замеченные юниты противника (или крипов — 'creep', или игрока с заданным номером): [(как юнит выглядел тогда, игровые часы в тот момент, сколько секунд прошло)], новые первыми.
Если видели, как юнит погиб, он удаляется из списка. И в честном, и в обычном режиме запись ведётся по принципу «что мы видим прямо сейчас» — это та самая карта в голове игрока:
разведанные силы, где последний раз видели вражеского героя, когда противник занял экспансию. max_age — только записи не старше указанного числа игровых секунд. (Механизм: visibleTo из push-снимка (при каждом обновлении снимка запоминаются видимые юниты противника и крипы))
- `map()` [verified] [Push-снимок] Таблица рельефа этой партии MapInfo: .walkable(x,y) .buildable(x,y) .at(x,y) .bounds (игровая область) .starts (стартовые позиции) .cells (bit0 — непроходимо, bit1 — нельзя строить).
После начала партии вычисляется несколько секунд; пока не готово, возвращает None. Деревьев здесь нет (используйте trees()). (Механизм: Блок карты W3P Local\War3Map_ (рантайм вычисляет порциями после начала партии; IsTerrainPathable для прохода/постройки))
- `me() -> 'int | None'` [verified] [Push-снимок] Номер нашего игрока (0~11). (Механизм: Заголовок блока мира)
- `resources(player: 'int | None' = None) -> 'dict | None'` [verified] [Push-снимок] {'gold','lumber','food_used','food_cap','gold_gathered','lumber_gathered'}; player по умолчанию — мы, читать можно любого игрока.
Если прочитать не удалось, возвращает None — не принимайте это за 0. (Механизм: Блок мира players[16])
- `players() -> 'list'` [verified] [Push-снимок] Все 16 слотов игроков: Player(id, gold, lumber, food_used, food_cap, gold_gathered, lumber_gathered, race, known). (Механизм: Блок мира players[16])
- `units(owner: 'str | int' = 'all', types=None, alive: 'bool' = True) -> 'list'` [verified] [Push-снимок] Фильтр юнитов по владельцу/типу. owner: 'me' / 'enemy' / 'creep' / 'all' / номер игрока. types: множество четырёхсимвольных кодов. (Механизм: Блок мира units[])
- `unit(handle) -> 'object | None'` [verified] [Push-снимок] Найти юнит по паре дескрипторов (lo, hi) (цель приказа, текущая цель и события отдают именно пары дескрипторов). (Механизм: Блок мира by_handle)
- `is_building(u) -> 'bool'` [verified] [Push-снимок] Является ли юнит зданием (включая башни). Определяется по нулевой скорости передвижения в таблице юнитов; у главного здания Нежити площадь застройки равна 0, так что по площади не определяйте. (Механизм: Снимок + units.json (spd==0 = здание))
- `my_workers() -> 'list'` [verified] [Push-снимок] Наши рабочие (крестьяне/батраки/послушники/огоньки). (Механизм: Push-снимок)
- `idle_workers() -> 'list'` [verified] [Push-снимок] Рабочие без дела: нет ни приказа, ни задания (те, кому вы дали работу в этом тике, не считаются).
⚠ Повторный приказ добычи рабочему, у которого есть задание, прерывает цикл добычи (доход падает до нуля). (Механизм: Push-снимок (слот приказа + слот задания))
- `my_heroes() -> 'list'` [verified] [Push-снимок] Наши живые герои (погибшие — в списке воскрешения алтаря, см. revive). (Механизм: Push-снимок)
- `my_army() -> 'list'` [verified] [Push-снимок] Наши боевые юниты: не рабочие и не здания. (Механизм: Push-снимок + units.json)
- `my_buildings(types=None) -> 'list'` [verified] [Push-снимок] Наши здания (включая башни и строящиеся фундаменты); через types можно запросить только некоторые виды, например {'hbar'}. (Механизм: Push-снимок)
- `is_constructing(worker) -> 'bool'` [verified] [Push-снимок] Строит ли этот рабочий здание прямо сейчас (или идёт строить / помогает чинить; включая назначенных в этом тике). Выбирая строителя, пропускайте таких, иначе предыдущий фундамент встанет. (Механизм: Push-снимок (приказ = четырёхсимвольный код здания либо приказ строительства/ремонта))
- `under_construction(building) -> 'bool'` [verified] [Push-снимок] Здание ещё не достроено (здоровье не полное). ⚠ У повреждённого здания здоровье тоже не полное — в начале партии этого хватает, а после начала боёв учитывайте ещё и время. (Механизм: Push-снимок (здоровье фундамента растёт от очень малого до полного))
- `gold_mines() -> 'list'` [verified] [Push-снимок] Золотые рудники на карте. ⚠ У Entangled Gold Mine Ночных эльфов и у нейтрального рудника в одной и той же точке по отдельному юниту — на добычу отправляйте к своему. (Механизм: Push-снимок (ngol/egol/ugol))
- `enemies(fighters_only: 'bool' = False) -> 'list'` [verified] [Push-снимок] Юниты вражеских игроков (без крипов). fighters_only: без рабочих и зданий. (Механизм: Push-снимок)
- `creeps() -> 'list'` [verified] [Push-снимок] Крипы (нейтрально-враждебные). ⚠ Ночью обзор короче: когда дальний лагерь уходит в туман войны, команды на цель по нему отклоняются (код причины 1001). (Механизм: Push-снимок (owner 12 = нейтрально-враждебные))
- `life_mana(u) -> 'dict | None'` [verified] [Push-снимок] {'hp','hp_max','mana','mana_max'} (float, исходные значения движка). В u можно передать юнит прямо из снимка (он будет заменён на самую свежую копию). (Механизм: Юнит блока мира: hp/hpMax/mana/manaMax)
- `hero_info(hero) -> 'dict | None'` [verified] [Push-снимок] {'level','xp','skill_points'}. (Механизм: Юнит блока мира: level/xp/skillPoints)
- `abilities(u) -> 'list'` [verified] [Push-снимок] [{code, level, cooldown, flags}]; баффы — в buffs(u). Есть только у юнитов «с деталями» (герои > юниты игроков > крипы, не больше 256). (Механизм: Детали блока мира: способности (код/уровень/флаги/оставшаяся перезарядка))
- `buffs(u) -> 'list'` [verified] [Push-снимок] Коды баффов на юните (например, 'BHds' — Божественный щит, 'Bslo' — Замедление). Какому эффекту соответствует код, см. data/game/buffs.json. (Механизм: Детали блока мира: объекты способностей, начинающиеся на B)
- `cooldown(u, ability: 'str') -> 'float | None'` [verified] [Push-снимок] Сколько ещё секунд (игровых) перезаряжается способность; 0 = можно применять; если такой способности нет (или у юнита нет деталей), возвращает None. (Механизм: Детали блока мира: оставшаяся перезарядка способности (таймер способности))
- `inventory(hero) -> 'list | None'` [verified] [Push-снимок] Четырёхсимвольные коды предметов в 6 ячейках (пустая ячейка — None); если инвентаря нет, возвращает None. (Механизм: Детали блока мира: 6 ячеек инвентаря)
- `current_order(u) -> 'dict | None'` [verified] [Push-снимок] {'order','target','x','y'}: приказ, который юнит выполняет сейчас (order — 0x000D00xx или четырёхсимвольный код здания, 0 = простой).
target — пара дескрипторов; превратить её в юнит можно через g.unit(target). (Механизм: Юнит блока мира: order / цель приказа / точка цели приказа)
- `current_target(u)` [verified] [Push-снимок] Юнит, которого этот юнит **реально атакует/преследует** (если такого нет — None).
⚠ После приказа атаки слот приказа быстро пустеет, а атака висит на задании — чтобы понять, «кого бьёт» юнит, используйте это, а не current_order. (Механизм: Юнит блока мира: текущая цель (задание))
- `clock() -> 'float | None'` [verified] [Push-снимок] Игровые часы движка (игровые секунды, во время загрузки 0). На повышенной скорости игры идут быстрее реального времени. (Механизм: Заголовок блока мира clockMs (игровые часы движка))
- `production(building)` [verified] [Push-снимок] Что сейчас производит здание: Production(kind, queue, duration, elapsed, blocked, progress, remaining…); если ничего — None.
kind 'queue' (найм/исследование/герой, в queue до 7 ячеек, [0] — то, что делается сейчас) / 'construction' (строится) / 'upgrade' (улучшение ратуши/башни);
blocked = в очереди есть, но не началось (чаще всего не хватает пищи — пора строить ферму); progress 0..1.
Здания противника тоже видны (в честном режиме — только видимые здания). (Механизм: Таблица производства блока мира (объекты способностей Aque/ABnP/AUnP + прошедшее время, которое отслеживает рантайм; измеренная погрешность < 0.2 игровой секунды))
- `queue(building) -> 'list'` [verified] [Push-снимок] Четырёхсимвольные коды в очереди найма/исследований ([0] — делается сейчас); если здание простаивает или не производит ничего — []. (Механизм: Таблица производства блока мира)
- `all_production(owner: 'str | int' = 'me') -> 'list'` [verified] [Push-снимок] Всё текущее производство [(здание, Production)]. owner — как в units(): 'me' / 'enemy' / номер игрока / 'all'.
Как используют профи: смотреть, каких юнитов нанимает противник, что исследует и когда улучшает ратушу (если его здания разведаны). (Механизм: Таблица производства блока мира)
- `path_distance(a, b) -> 'float | None'` [verified] [Push-снимок] Длина пути наземного юнита от a до b (a и b — юниты или (x,y)); если не дойти — None. На островных картах, чтобы понять, «можно ли дойти по земле до этого лагеря крипов/экспансии», используйте это —
надёжнее расстояния по прямой (обходит лес, обрывы, здания). Точность — одна клетка 128; проход уже одной клетки считается непроходимым. (Механизм: Блок карты (IsTerrainPathable движка) + блок деревьев + площадь зданий, A* на стороне SDK (клетка 128))
- `reachable(a, b) -> 'bool | None'` [verified] [Push-снимок] Можно ли дойти по земле (блок карты ещё не вычислен = None). (Механизм: То же, что выше)
- `walk_path(a, b) -> 'list | None'` [verified] [Push-снимок] Точки поворота пути [(x,y)...] (последняя — b); вместе с path(units, список_точек) ведёт отряд этим маршрутом (в обход башен, обходными тропами). (Механизм: То же, что выше)
- `upkeep(player: 'int | None' = None) -> 'dict | None'` [inferred] [Push-снимок] Уровень содержания: {'level': 'none'/'low'/'high', 'income': 1.0/0.7/0.4, 'next_at': пища, с которой начинается следующий уровень (нет = None)}.
Азбука профи: пока улучшаете ратушу до третьего уровня / исследуете улучшения атаки/брони, держитесь на 50 пищи, а до 80 поднимайтесь только перед решающим боем. (Механизм: Фиксированное правило 1.27: 0~50 пищи — без налога, 51~80 — доход ×0.7, 81~100 — ×0.4)
- `xp_to_next(hero) -> 'int | None'` [verified] [Push-снимок] Сколько опыта не хватает герою до следующего уровня (на 10 уровне = 0). (Механизм: level/xp блока мира + формула NeedHeroXP из MiscGame)
- `creep_camps(link: 'float' = 600.0) -> 'list'` [verified] [Push-снимок] Группирует (видимых) крипов на поле в лагеря: [{'x','y','units','level','hp','max_level'}], от ближних к нашей главной базе к дальним.
level = суммарный уровень лагеря (обычная мера сложности крипинга), hp = суммарное здоровье. Выбирайте лагерь вместе с time_to_kill / path_distance. (Механизм: Push-снимок (крипы объединяются в группу в радиусе 600) + уровни из units.json)
- `buff_info(code: 'str') -> 'dict | None'` [verified] [Локальный расчёт] Что означает код баффа: {'ability','effect','dur','hero_dur','targets'} (например, 'Bslo' -> Замедление). Если коду соответствует несколько строк, возвращается первая. (Механизм: data/game/buffs.json (BuffID из AbilityData.slk -> способность/эффект/длительность))
- `stats(u, player: 'int | None' = None)` [verified] [Push-снимок] Боевые характеристики юнита combat.UnitStats: максимум здоровья/маны, броня (с учётом улучшений атаки/брони и ловкости героя), тип брони, скорость передвижения, дневной/ночной обзор,
оружие (по каким целям бьёт, дальность, интервал атаки, диапазон урона, тип атаки, урон по площади). u — юнит (автоматически берутся технологии его владельца и уровень героя) или четырёхсимвольный код (player по умолчанию — мы).
Дальше — .dps_vs(противник) / .hits_to_kill(противник) / combat.time_to_kill(группа, противник). ⚠ Предметы, ауры и баффы не учитываются. (Механизм: Таблицы данных (UnitBalance/UnitWeapons/UpgradeData/MiscGame) + текущие уровни технологий + уровень героя)
- `time_to_kill(attackers, target) -> 'float | None'` [verified] [Push-снимок] За сколько игровых секунд эта группа юнитов вместе убьёт target (по текущему здоровью target; учитываются контры, броня, улучшения атаки/брони; не учитываются перемещение, урон по площади, лечение).
Как используют профи: фокус огня сначала на того, кто «умрёт быстрее всех» (минимальный time_to_kill), а не на ближайшего. Не могут атаковать = None. (Механизм: stats() + текущее здоровье)
- `time_of_day() -> 'float | None'` [verified] [Push-снимок] Игровое время суток (часы, 0~24). Партия начинается в 8 утра; полные сутки = 480 игровых секунд (день и ночь по 240 секунд, масштабируются скоростью смены дня и ночи).
Если прочитать не удалось (старый рантайм / не в партии), возвращает None. (Механизм: Область расширений блока мира: GetFloatGameState(GAME_STATE_TIME_OF_DAY))
- `is_night() -> 'bool | None'` [verified] [Push-снимок] Ночь ли сейчас (18:00~6:00). Приём профи: ночью крипы спят (можно ударить первым и не попасть в окружение), у всех юнитов обзор короче (хорошее время для внезапной атаки),
часовые/юниты Ночных эльфов ночью становятся невидимыми у деревьев. Если прочитать не удалось, возвращает None. (Механизм: Область расширений блока мира (день — с 6 до 18))
- `seconds_until(hour: 'float') -> 'float | None'` [verified] [Push-снимок] Сколько игровых секунд осталось до игрового времени hour (например, seconds_until(18) = сколько осталось до темноты; удобно для планирования ночного крипинга). (Механизм: Область расширений блока мира + сутки по 480 секунд (измерено: 20 игровых секунд в час))
- `items_on_ground() -> 'list'` [verified] [Push-снимок] Предметы на земле [Item(addr, handle_lo, handle_hi, type, x, y, life)]. При подборе/использовании приходит событие item.removed. (Механизм: Блок мира items[] (только лежащие на земле: дескриптор владельца — все FF))
- `trees(x: 'float | None' = None, y: 'float | None' = None, limit: 'int' = 60) -> 'list'` [verified] [Push-снимок] Живые деревья (из DestructableData, у которых targType содержит tree); если задано (x,y), сортируются по расстоянию от ближних к дальним, не больше limit штук.
Каждое — Tree(addr, handle_lo, handle_hi, type, x, y, life), его можно сразу передать в gather для рубки леса. (Механизм: Блок деревьев Local\War3Trees_ (обновляется каждые 2 секунды))
- `events() -> 'list'` [verified] [Push-снимок] Что произошло с прошлого вызова: unit.appeared / unit.died / unit.removed / unit.damaged / order.changed /
hero.levelup / owner.changed / item.appeared / item.removed / game.started (получаются сравнением публикаций, точность = период публикации 50 ms),
а также damage / killed уровня движка (рантайм записывает их прямо в игровом потоке в момент удара, событие есть на **каждый удар**):
damage: handle = кого ударили, .source_addr = кто ударил (превратить в юнит — snapshot().unit_by_addr), .value = фактически снятое здоровье,
.raw_damage = урон до учёта брони, .attack_type (normal/pierce/siege/magic/chaos/hero/spell), .damage_type
killed: этот удар добил цель, .source_addr = убийца
а также production.done, который рантайм получает, отслеживая таблицу производства (точность = период публикации): юнит = здание, .done_code = четырёхсимвольный код готового,
.done_kind = 'training' (войска/герой/воскрешение) / 'research' / 'construction' (здание построено) / 'upgrade' (улучшение ратуши/башни), .value = сколько игровых секунд заняло
Добавлено 09-25:
spell.cast: юнит = заклинатель, .spell — четырёхсимвольный код способности, b — уровень, value — перезарядка в секундах, x,y — точка применения (распознаётся, когда способность уходит на перезарядку; точность = период публикации)
player.left: .player — номер игрока, который вышел / удалён после поражения; game.ended: выход из матча
selection.changed: изменилось выделение локального игрока (юнитов берите через g.selection())
message: строка в окне сообщений на экране (подсказка игры, чат, системное сообщение): .text — полный текст, .frame — номер окна сообщений,
.chat = {'channel', 'sender', 'text'} (если это чат; то, что игрок пишет в чат, читается именно отсюда)
ui.click / ui.hover / hotkey / mouse.world: интерфейс и ввод (g.ui), .key — key на холсте / запись горячей клавиши
Каждое — Event(seq, kind, clock, addr, handle, type, owner, a, b, x, y, value, extra).
В честном режиме (fair=True) приходят только: события своих юнитов, события юнитов, видимых прямо сейчас (или ещё видимых в течение последней 1 секунды), урон по нам и урон, нанесённый нами,
а также локальные события интерфейса / сообщений / матча. (Механизм: Кольцо событий Local\War3Events_ (сравнение публикаций + события урона, которые фиксирует рантайм))
- `selection() -> 'list'` [verified] [Push-снимок] Юниты, которые сейчас выбраны у локального игрока (главный юнит — первым; не больше 12). При изменении выделения приходит событие selection.changed. (Механизм: Область расширений блока мира W3P, selAddrs (рантайм при каждой публикации добавляет выделение локального игрока))
- `messages() -> 'list'` [verified] [Push-снимок] Новые строки в окнах сообщений на экране с прошлого вызова: [{'text', 'frame', 'repeat', 'seq', 'game_ms'}].
Здесь и подсказки игры («Нужно больше ферм», «Здесь строить нельзя»), и чат, и системные сообщения; frame показывает, в каком окне сообщений появилась строка.
Это те же сообщения, что и события message в потоке событий (у каждого свой курсор). (Механизм: Общая память Local\War3Msgs_ (экранные сообщения, которые фиксирует рантайм))
- `tech(code: 'str', player: 'int | None' = None) -> 'int | None'` [verified] [Быстрая полоса] Уровень исследования / число построенных зданий (цепочка улучшений учитывается: Замок тоже считается как htow). player по умолчанию — мы, запрашивать можно любого игрока. (Механизм: Запрос W3P q_tech (счётчик технологий игрока в движке))
- `can_do(u, code: 'str') -> 'int | None'` [verified] [Быстрая полоса] Вердикт движка о выполнимости: 0/220 — можно; 3 пища, 8 не хватает золота, 9 не хватает древесины, 32 очередь заполнена, 183 нет нужного здания или технологии, 185 алтарь воскрешает, 221 такого нет/строится.
⚠ Для постройки здания рабочим всегда 221 — выбирать место этим нельзя (используйте build_near). (Механизм: Запрос W3P q_feasible (проверка выполнимости в движке))
- `can_do_many(pairs) -> 'list'` [verified] [Быстрая полоса] Много can_do за раз: pairs = [(юнит, четырёхсимвольный_код), ...], возвращает список кодов вердикта в том же порядке (где ответа нет — None).
Планируя, что строить/нанимать в этом тике, сначала спросите всё разом — это в N раз быстрее, чем can_do по одному (эталонный мозг 09-23: планирование построек 76 -> 25 ms). (Механизм: Запрос W3P q_feasible × N, отправка одной пачкой)
- `tech_many(codes, player: 'int | None' = None) -> 'dict'` [verified] [Быстрая полоса] Много счётчиков технологий/зданий за раз: {четырёхсимвольный_код: количество или None}. (Механизм: Запрос W3P q_tech × N, отправка одной пачкой)
- `visible(x: 'float', y: 'float') -> 'bool | None'` [verified] [Быстрая полоса] Видим ли мы эту точку сейчас (не в тумане войны/чёрной маске). Bot в честном режиме должен использовать только видимых врагов. (Механизм: Запрос W3P q_visible (видно / туман войны / чёрная маска))
- `gold_left(mine) -> 'int | None'` [inferred] [Быстрая полоса] Сколько золота осталось в руднике. (Механизм: Запрос W3P q_mine_gold (остаток золота в руднике по данным движка))
- `enemy_ai_plan(enemy_unit) -> 'dict | None'` [verified] [Быстрая полоса] Капитан компьютерного AI: куда он ведёт войска (ещё до выхода известно, какую часть вашей базы он будет атаковать). Работает только против компьютерного противника; если юнит не следует за капитаном — None. (Механизм: Запрос W3P q_captain (компьютерный капитан, за которым следует вражеский юнит))
- `order_of(u) -> 'int | None'` [verified] [Push-снимок] Текущий приказ юнита, **включая отданный вами в этом тике** (пока снимок не догнал, берётся новый приказ из квитанции).
⚠ 09-23, реальная игра: hello_bot только что отправил крестьянина строить ферму, а в том же тике rush_bot увидел в снимке, что тот «свободен», и отправил его строить казарму — ферма раз за разом бросалась на полпути.
Выбирая «свободные/не занятые стройкой» юниты, используйте это, а не u.order. (Механизм: Приказ из снимка + команды, только что принятые в этом процессе (квитанции))
- `can_afford(code: 'str') -> 'bool'` [verified] [Push-снимок] Хватает ли сейчас золота/древесины на code (юниты, здания; по ценам из units.json). Всё, чего нет в таблице цен, считается доступным.
⚠ У четырёхсимвольных кодов улучшения ратуши в таблице накопленная цена, так что оценка здесь будет консервативной; окончательное слово — за квитанцией движка. (Механизм: Наши ресурсы из push-снимка + цены из units.json)
- `map_data()` [verified] [Локальный расчёт] Данные карты текущей партии (openwar3.mapdata.MapData): name_of('HC07') — имена пользовательских юнитов/предметов/способностей, hero_names, tooltip.
Большинство юнитов на RPG-картах созданы самой картой, во встроенной таблице имён их нет; если игра запущена не лаунчером (файл карты не найден), возвращает None. (Механизм: Файл карты (путь из --map лаунчера): w3u/w3t/w3a + wts; у защищённых карт читаются TXT внутри карты)
## Команды
Приказы юнитам. Исполняются примерно за кадр, у каждой есть квитанция.
- `batch() -> 'Batch'` [verified] [Быстрая полоса] Объединяет команды одного тика в пачку:
with g.batch() as b:
g.attack(archers, target) # возвращает Pending, квитанцией становится после конца блока
g.move(wounded, *home)
g.cast(hero, "thunderclap")
print(b.sent, b.wait_ms, [r.reason for r in b.receipts])
Каждая команда, отправленная по отдельности, ждёт одной обработки в игровом потоке (около 10 ms); пачка ждёт один раз — эталонный мозг 09-23 за счёт этого сократил раунд 48 -> 26 ms.
* Арбитраж по-прежнему проходит по каждой команде отдельно (удерживаемый юнит сразу получает квитанцию held и в пачку не попадает);
* команды внутри блока возвращают Pending: чтение .ok до конца блока выбрасывает ошибку (квитанции ещё нет), после конца блока он используется как Receipt;
* исключение внутри блока = вся пачка отменяется (status 97 cancelled), удерживаемые юниты освобождаются;
* запросы (can_do / tech / visible …), а также build_near и buy в пачку не попадают и выполняются сразу — их результат нужен немедленно;
чтобы спросить много за раз, используйте can_do_many / tech_many;
* вложенный with g.batch() сливается с самой внешней пачкой; больше 16 команд рантайм автоматически делит на несколько частей (по одному ожиданию на часть). (Механизм: Команды внутри блока копятся в пачку и отправляются разом в конце блока (выполняются в одном кадре, игровой поток ждём один раз))
- `move(units, x: 'float', y: 'float', queue: 'str | None' = None)` [verified] [Быстрая полоса] Идти в (x,y), не атакуя по пути (для отступления). Можно передать один юнит или список (приказ всем в одном кадре).
queue='after': сначала закончить текущее дело (вставить после текущего приказа). В квитанции values[0] = сколько приказов у юнита в очереди после команды (включая выполняемый). (Механизм: W3P point: move (биты extra = способ постановки в очередь))
- `attack_move(units, x: 'float', y: 'float', queue: 'str | None' = None)` [verified] [Быстрая полоса] Атака с движением (A по земле): атакует врагов, встреченных по пути. queue — как в move. (Механизм: W3P point: attack в точку)
- `attack(units, target, force: 'bool' = False, queue: 'str | None' = None)` [verified] [Быстрая полоса] Атаковать target. По умолчанию — правым кликом (по врагу = атаковать именно его; 09-23 измерено: и цель приказа, и текущая цель — он).
⚠ Цель должна быть в зоне обзора, невидимую отклонят (код причины 1001).
force=True использует приказ атаки 0x0F (нужен, чтобы бить своих/нейтральных зверьков) — по измерениям он лишь подменяет приказ на атаку и не запоминает цель,
и юнит уходит бить других врагов поблизости; для атаки конкретной цели его не используйте. (Механизм: W3P target: команда на цель (правый клик smart))
- `stop(units)` [verified] [Быстрая полоса] Прекратить всё (номер приказа 0x000D0004), очередь приказов тоже очищается. (Механизм: W3P immediate: stop)
- `hold(units, queue: 'str | None' = None)` [verified] [Быстрая полоса] Удерживать позицию (не преследовать, атаковать только в пределах дальности). (Механизм: W3P immediate: holdposition)
- `patrol(units, x: 'float', y: 'float', queue: 'str | None' = None)` [inferred] [Быстрая полоса] Патрулировать между текущей позицией и (x,y). (Механизм: W3P point: patrol)
- `attack_ground(units, x: 'float', y: 'float', queue: 'str | None' = None)` [verified] [Быстрая полоса] Атака земли: артиллерия стреляет по участку (по невидимым юнитам, по тем, кто за лесом, чтобы перекрыть проход). Принимают только юниты, способные атаковать землю. (Механизм: W3P point: attackground (осадные юниты / мортиры / катапульты))
- `cancel(building)` [verified] [Быстрая полоса] Отмена: последняя ячейка очереди найма/исследований (деньги возвращаются), строящееся здание (возврат 75%), улучшающаяся ратуша. (Механизм: W3P immediate: cancel)
- `path(units, points, attack: 'bool' = False)` [verified] [Быстрая полоса] Пройти по цепочке точек по порядку (точки через Shift: маршрутные точки, обход башен, маршрут разведки). attack=True — каждый отрезок как атака с движением.
Отправляется за один раз; квитанций — по одной на точку (в порядке points). (Механизм: Пачка: первый отрезок выполняется сразу, остальные вставляются в обратном порядке через queue='after' (движок умеет вставлять только после текущего))
- `gather(workers, target, queue: 'str | None' = None)` [verified] [Быстрая полоса] Добыча золота/древесины (target — золотой рудник или дерево из trees()). ⚠ Отправляйте только свободных рабочих (idle_workers): повторный приказ рабочему с заданием прерывает цикл добычи.
Как используют профи: вернуться на рудник после стройки = после build(...) вызвать gather(worker, mine, queue='after'). (Механизм: W3P target: harvest (золотой рудник или дерево))
- `repair(workers, building, queue: 'str | None' = None)` [verified] [Быстрая полоса] Ремонт / помощь в строительстве (стройка Людей и Орды без строителя замирает). (Механизм: W3P target: repair)
- `build(worker, code: 'str', x: 'float', y: 'float', queue: 'str | None' = None)` [verified] [Быстрая полоса] Рабочий строит code в (x,y) (координаты выравниваются по сетке 32). Квитанция «принята» = приказ рабочего уже — это здание (или приказ начала стройки);
при queue='after' = поставлено в очередь приказов рабочего (values[0] в квитанции — длина очереди).
⚠ «Принята» ≠ «построено»: точку в лесу движок тоже принимает сразу, а неудача наступает, когда рабочий дойдёт (измерено 09-23); если деньги потрачены в другом месте, фундамент тоже не появится.
Если не знаете, где есть место, используйте build_near (он отслеживает результат и заносит неудачные точки в чёрный список). Для нескольких зданий подряд — build_queue. (Механизм: W3P build: приказ постройки, приказ рабочего перечитывается в том же кадре для подтверждения)
- `build_queue(worker, plan)` [verified] [Быстрая полоса] Один рабочий строит несколько зданий подряд (стройка через Shift): plan = [(четырёхсимвольный_код, x, y), ...]. Отправляется за один раз; квитанции — в порядке plan.
⚠ Деньги списываются только в момент начала стройки (не при постановке в очередь) — если поставили 3 здания, а денег хватает на 1, последние два сорвутся, когда рабочий до них дойдёт. (Механизм: Пачка: первое здание сразу, остальные в обратном порядке через queue='after')
- `build_near(worker, code: 'str', x: 'float', y: 'float', min_r: 'float' = 450, max_r: 'float' = 1500, max_tries: 'int' = 24)` [verified] [Быстрая полоса] Ищет вокруг (x,y) от ближнего к дальнему место, куда влезет code, и строит. **Не блокирует**, можно вызывать каждый тик:
* для этого здания уже идёт попытка (рабочий в пути) -> возвращает ту же точку, приказ не повторяется;
* прошлая попытка удалась (появился фундамент) -> при необходимости ищет новую точку;
* прошлая попытка провалилась (рабочий дошёл и обнаружил, что места нет; движок снял приказ, фундамента нет) -> точка попадает в чёрный список на 45 секунд, берётся следующая;
* не хватает денег -> сразу возвращает None (не пробует и в чёрный список не заносит); все точки перепробованы — возвращает None.
⚠ Зачем отслеживать: 09-23, реальная игра — точку в лесу движок **сразу принимает**, а неудача случается, только когда рабочий дойдёт (по квитанции того же кадра этого не понять);
а проверка места в движке для постройки рабочим всегда возвращает 221, так что «сначала проверить, потом строить» тоже не выйдет. Сразу отклоняются только явно занятые точки (центр ратуши). (Механизм: Команда build по точкам + отслеживание (появился фундамент = успех; рабочий бросил приказ, а фундамента нет = точка в чёрный список))
- `train(building, code: 'str')` [verified] [Быстрая полоса] Нанять юнит / исследовать технологию / улучшить ратушу (улучшение = приказ самой ратуше с четырёхсимвольным кодом целевого здания, например 'hkee').
При отказе reason в квитанции объяснит почему (не хватает пищи, золота, древесины, очередь заполнена, нет нужного здания или технологии…). (Механизм: W3P immediate: четырёхсимвольный код; при отказе — с кодом причины из проверки выполнимости)
- `learn(hero, ability: 'str')` [verified] [Быстрая полоса] Герой изучает способность (четырёхсимвольный код, например 'AHbz' — Снежная буря). (Механизм: W3P learn: изучено, только если уменьшились очки навыков)
- `cast(u, spell, target=None, x: 'float | None' = None, y: 'float | None' = None)` [verified] [Быстрая полоса] Применить способность. spell — строка приказа ('thunderbolt' — Молот бурь, 'blizzard', 'holybolt' — Свет небес…, см. data/order-ids.txt) или номер приказа.
Задан target — на юнит; заданы x,y — на землю; ничего не задано — без цели (Раскат грома, Божественный щит, призыв Элементаля воды).
«Принята» в квитанции означает лишь, что движок принял команду; применилась ли способность, смотрите по тому, ушла ли она на перезарядку в cooldown() и появился ли бафф в buffs(). (Механизм: W3P target / point / immediate (выбирается по аргументам))
- `rally(building, x: 'float | None' = None, y: 'float | None' = None, target=None)` [verified] [Быстрая полоса] Задать точку сбора (в точку либо на юнит/золотой рудник). (Механизм: W3P rally)
- `revive(altar, hero=None)` [verified] [Быстрая полоса] Воскресить погибшего героя в алтаре (если hero не задан — первого в списке).
Частые причины отказа (будут в reason квитанции): не хватает пищи (герой тоже занимает пищу), не хватает денег, герой погиб слишком недавно (воскрешать можно примерно через 3 игровые секунды после смерти),
воскрешение уже идёт (при принятии движок сразу очищает этот слот). (Механизм: W3P revive: список погибших героев -> алтарь применяет воскрешение к погибшему герою)
- `pick_up(hero, item)` [verified] [Быстрая полоса] Герой идёт подбирать предмет с земли (item — из items_on_ground). После подбора предмет появляется в инвентаре, а для земли приходит событие item.removed. (Механизм: W3P target: правый клик по предмету)
- `use_item(hero, slot: 'int', target=None, x: 'float | None' = None, y: 'float | None' = None)` [verified] [Быстрая полоса] Использовать предмет из ячейки инвентаря slot (0~5); можно указать целевой юнит или целевую точку.
⚠ При использовании предмета в точку (например, Башни из слоновой кости) движок и при успехе возвращает 0, так что квитанция всегда «принята» — проверяйте, опустела ли ячейка. (Механизм: W3P use_item (по номеру ячейки))
- `drop_item(hero, slot: 'int', x: 'float', y: 'float')` [verified] [Быстрая полоса] Выложить предмет из ячейки slot в (x,y) (герой подходит и кладёт). (Механизм: W3P item_drop (копия JASS UnitDropItemPoint: dropitem 0xD0021 в точку + предмет как мгновенная цель))
- `give_item(hero, slot: 'int', to)` [verified] [Быстрая полоса] Отдать предмет из ячейки slot юниту to (другому герою / юниту — подойти и передать). Отдать магазину = продать (см. sell_item). (Механизм: W3P item_drop (копия JASS UnitDropItemTarget: dropitem на юнит))
- `sell_item(hero, slot: 'int', shop)` [verified] [Быстрая полоса] Продать предмет из ячейки slot магазину (герой должен подойти к магазину; принимаются только продаваемые предметы, возвращается половина цены). (Механизм: Как give_item, но цель — магазин (измерено: Staff of Sanctuary продаётся за 125 золота))
- `move_item(hero, slot: 'int', to_slot: 'int')` [verified] [Быстрая полоса] Переложить предмет в инвентаре (из ячейки slot в ячейку to_slot; если заняты обе — поменять местами). Для раскладки под горячие клавиши. (Механизм: W3P target: приказ 0xD0022+номер ячейки, цель = предмет (копия JASS UnitDropItemSlot))
- `buy(shop, item_code: 'str')` [inferred] [Быстрая полоса] Купить предмет в магазине (для героя, стоящего рядом с магазином). Если не хватает требуемой технологии, движок возвращает 0 и деньги не списывает. (Механизм: W3P buy: магазин продаёт предмет стоящему рядом герою)
- `call_to_arms(hall, on: 'bool' = True)` [verified] [Быстрая полоса] Призыв к оружию у Людей: крестьяне превращаются в Ополчение (у Ратуши первого уровня этой способности нет — работает только у Крепости/Замка). (Механизм: W3P immediate: townbellon/off)
## Управление игрой
Скорость игры, пауза, период публикации, облачка реплик, холст, интерфейс и ввод, сообщения.
- `ui()` [verified] [Прямая запись] Интерфейс и ввод (openwar3.ui.UI): кликабельные кнопки и карточки выбора, горячие клавиши, выбор точки кликом по земле, куда указывает мышь.
Клик по кнопке до игры не доходит; только локальный ввод + локальная отрисовка, поэтому безопасно и в многопользовательской игре. (Механизм: W3P 74 input_enable + общая память Local\War3Input_ (рантайм получает ввод окна игры))
- `set_speed(percent: 'int') -> 'bool'` [verified] [Канал управления] Скорость игры (100 = обычная). (Механизм: Действие 47 (25~800%))
- `pause(on: 'bool' = True)` [verified] [Быстрая полоса] Поставить игру на паузу / снять с паузы. На паузе часы движка стоят, но через быструю полосу приказы отдавать можно (диспетчеризация событий продолжает работать). (Механизм: W3P pause)
- `set_publish_period(ms: 'int') -> 'None'` [verified] [Push-снимок] Период публикации состояния мира (16~1000 миллисекунд, по умолчанию 50). Один сбор — около 0.5 ms, так что и 33 ms не проблема; значение одно на всю машину, действует последнее записанное. (Механизм: requestedPeriodMs блока мира)
- `say(u, text: 'str', seconds: 'float' = 4.0) -> 'bool'` [verified] [Канал управления] Показать облачко чата над юнитом (для трансляций/отладки, на игру не влияет). Если облачко не появилось, возвращает False; причина — в g.last_say_error. (Механизм: Действие 56)
- `message(text: 'str') -> 'bool'` [inferred] [Канал управления] Вывести строку в области сообщений в левом нижнем углу игры (видно только на этой машине). Сначала игра должна сама показать хотя бы одну подсказку (DLL перехватывает окно сообщений именно в тот момент). (Механизм: Действие 45)
- `end_game() -> 'bool'` [verified] [Канал управления] Завершить этот процесс игры (farm.py --keep автоматически начнёт следующую партию согласно next_game.json). (Механизм: Действие 22)
- `canvas()` [verified] [Прямая запись] Холст: рисует поверх игрового экрана текстовые блоки, панели, индикаторы прогресса, изображения, круги и маршруты на земле (openwar3.canvas.Canvas).
Рисует сам рантайм, не создаёт игровых дескрипторов и не меняет состояние игры — поэтому безопасен и в многопользовательской игре; стиль любой (китайский текст, скругления, полупрозрачность). (Механизм: W3P 73 canvas_enable + общая память Local\War3Canvas_ (рантайм рисует каждый кадр перед тем, как игра рисует указатель мыши; указатель перекрывает холст))
- `press_to_continue() -> 'bool'` [verified] [Прямая запись] Нажать пробел на экране загрузки «Нажмите любую клавишу, чтобы продолжить». Многие RPG / сюжетные карты после загрузки ждут нажатия клавиши (проверено 09-24 на WarChasers:
без нажатия игра так и стоит на экране загрузки, игровые часы на 0, быстрая полоса не опустошается). openwar3.run нажимает сам, пока ждёт входа в партию, так что вызывать вручную обычно не нужно. (Механизм: PostMessage WM_KEYDOWN/UP пробела окну игры (фокус не перехватывается))
## Песочница
JASS-канал: создание юнитов, союзы, смена имён, вывод текста… Для RPG-помощников и компаньонов; менять мир можно только в одиночной игре и в локальных инструментах.
- `jass()` [verified] [Быстрая полоса] Вызвать любую JASS native по имени: g.jass.CreateUnit(g.jass.Player(1), "Hpal", x, y, 270.0).
Аргументы I/R/B/S/H преобразуются автоматически (объекты юнитов/предметов передаются как есть); в многопользовательской игре можно вызывать только функции чтения. Подробнее — openwar3/jass.py и docs/COMPANION_ZH.md. (Механизм: W3P 70 jass (рантайм ищет native по имени в таблице, всего 1291))
- `player_slots() -> 'list[dict]'` [verified] [Быстрая полоса] 16 слотов игроков: controller (user — живой игрок / computer / neutral…), state (empty / playing / left), human, me, ally (союзник ли нам).
На RPG-картах через него ищут свободный слот для компаньона и определяют, одиночная ли это игра. (Механизм: JASS GetPlayerController / GetPlayerSlotState / IsPlayerAlly)
- `spawn(code: 'str', x: 'float', y: 'float', player: 'int | None' = None, facing: 'float' = 270.0)` [verified] [Быстрая полоса] Создать юнит в (x,y) (player по умолчанию — локальный игрок) и вернуть юнит из снимка (после следующей публикации мира, ≈ 50 ms); если создать не удалось, возвращает None.
У возвращённого юнита есть дополнительное свойство jass_handle. ⚠ Работает только в одиночной игре (в многопользовательской — рассинхронизация). (Механизм: JASS CreateUnit + W3P 72 дескриптор -> юнит)
- `set_alliance(a: 'int', b: 'int', allied: 'bool' = True, vision: 'bool' = True, control: 'bool' = False, xp: 'bool' = False, both: 'bool' = True) -> 'None'` [verified] [Быстрая полоса] Задать отношение игрока a к игроку b: allied = не атакуют друг друга + приходят друг другу на помощь; vision — общий обзор; control — общее управление юнитами (b может командовать юнитами a);
xp — общий опыт. both=True задаёт оба направления сразу (control — только a -> b). (Механизм: JASS SetPlayerAlliance)
- `set_player_name(player: 'int', name: 'str') -> 'None'` [verified] [Быстрая полоса] Сменить имя игрока (то, что видно в таблице счёта, в чате и на панели союзников). Нужно, чтобы дать имя компаньону. (Механизм: JASS SetPlayerName)
- `show_text(text: 'str', seconds: 'float' = 6.0, player: 'int | None' = None) -> 'None'` [verified] [Быстрая полоса] Показать строку текста в левом нижнем углу экрана (такой текст выводят триггеры карты), по умолчанию — локальному игроку. Поддерживаются цветовые коды |cffRRGGBB. (Механизм: JASS DisplayTimedTextToPlayer)
## Подключение и утилиты
Состояние подключения и чисто вычислительные утилиты.
- `status() -> 'dict'` [verified] [Локальный расчёт] Состояние подключения: pid, публикация мира (период, время сбора), счётчики быстрой полосы. (Механизм: Блок мира + быстрая полоса + таблица захватов)
- `nearest(candidates, to)` [verified] [Локальный расчёт] Ближайший к to (юнит или (x,y)); если кандидатов нет — None. (Механизм: Чистое вычисление)