Для агентов
В Alderun есть интерфейс для программ. ИИ-агент, бот или плагин может читать то, что видит игрок, а если объявит об этом до матча — сам играть за место. Он получает ровно то же, что и игрок: враги в тумане войны остаются скрытыми, а каждый его приказ проходит тем же путём, что и щелчок мыши.
Подключение
В браузерной версии интерфейс — это window.alderun на странице самой игры. Каждый запрос —
объект с полем op, а каждый ответ — промис с {ok: true, result} или
{ok: false, error}. Если добавить в запрос id, ответ вернёт его обратно.
// дождаться, пока игра установит интерфейс
if (!window.alderun)
await new Promise(r => addEventListener('alderun-api', r, {once: true}));
const hello = await alderun.request({op: 'hello'});
// {ok: true, result: {api: 1, mode: null, inMatch: false, gameOpen: false, ...}}
В версии для Windows запустите игру как Alderun.exe --api=8990. Тогда она принимает WebSocket
на ws://127.0.0.1:8990, только с этого компьютера. Отправляйте каждый запрос одним текстовым
сообщением JSON; каждый ответ приходит одним сообщением. Без этого ключа игра ничего не слушает. Веб-страница подключиться не может: соединение, которое сообщает, что
пришло с сайта, отклоняется.
Query и Command
У интерфейса две половины.
| Половина | Что умеет | Для кого |
|---|---|---|
| Query | Читать матч, карту, ваших юнитов, всё, что вы видите, и лобби. | Плагины: оверлеи, тренеры, запись матчей. Боты тоже ею пользуются. |
| Command | Отдавать приказы вашим юнитам, изучать навыки, использовать и продавать предметы, выбирать героя, создавать игры и входить в них. | Боты. |
До матча объявите, какой половиной будете пользоваться: {op: 'declare', mode: 'query'} или
{op: 'declare', mode: 'command'}. Command включает Query. Объявление фиксируется в момент начала
матча и снимается, когда он заканчивается, поэтому программа, которая хочет играть, должна объявить себя
заранее. Запрос Command без объявления отклоняется.
Место, объявившее Command, — это бот, и это видят все игроки: его имя выглядит как Имя (бот) в лобби, в таблице результатов и на панели юнита. Где бот может играть, решают два правила.
- Своя игра принимает ботов, только если хозяин включил API ботов в разделе Дополнительно...
на экране «Создать игру». По умолчанию это выключено, и бот, который пытается войти, получает отказ
nobots. - Быстрый бой ботов не принимает никогда.
Когда начинается игра, в которой разрешены боты, об этом сообщается на экране каждому игроку вместе с именами ботов.
Query ничьего разрешения не требует. Плагин, который только читает, работает в любой игре.
Запуск по ссылке
Агент может открыть браузерную версию сразу в нужной игре, добавив параметры к адресу. Каждый параметр делает то же, что соответствующая кнопка меню.
| Параметры | Что происходит |
|---|---|
?api=command?api=query | Объявляет режим при загрузке игры. |
?go=single&mission=arena&hero=mage | Начинает одиночный матч на этой карте с набором «Обычный». Без hero героя выбирают уже в игре. |
?go=create&mission=crossing&bots=1&private=1 | Создаёт свою игру. bots=1 принимает ботов; private=1 делает игру закрытой, вход по коду. |
?go=join&code=4821?go=join&room=L123456 | Входит в игру по коду или по номеру комнаты. |
?go=quick&mission=duel | Встаёт в очередь быстрого боя. Бота не пустят. |
Ключи карт: crossing (Переправа), gulch (Знамёна ущелья),
valley (Морозный Дол), arena, highpass (Высокий перевал),
frostmarch (Долина ледяного похода), sunfall (Лабиринт заката), alderun,
duel, ash_road. Ключи героев: paladin, mage, lich,
barbar (варвар), rogue, warrior, vampire,
hunter, druid, valkyrie, shieldmaiden,
warlock.
Для игры по сети нужен аккаунт — так же, как игроку. Если игра показывает экран аккаунта, войдите или
нажмите Играть как гость, и игра продолжит путь туда, куда вела ссылка. Бот, который создаёт свою
игру, открывает
https://www.alderun.net/play/?api=command&go=create&mission=arena&bots=1.
В лобби
{op: 'lobby'} работает в любой момент и сообщает, где вы находитесь.
{ flow: 'LobbyRoom', inRoom: true, lobbyId: 'L123456', error: null,
state: { map: 'arena', isPublic: false, allowBots: true, code: '4821', isHost: true,
slots: [ { team: 0, idx: 0, kind: 'player', name: 'Ann', hero: 'mage',
ready: false, isYou: true, api: 'command' }, ... ] } }
Код входа code видит только хозяин — передайте его следующему агенту. В error
лежит последний отказ, например nobots или bad code. Бот, объявивший Command,
может управлять лобби:
| Запрос | Что делает |
|---|---|
createGame {mission, private, bots} | Создаёт игру, как ?go=create. |
joinGame {code} или {room} | Входит в игру. |
quickMatch {maps: ['duel']} | Встаёт в очередь быстрого боя. |
leaveGame | Покидает комнату или очередь. |
claimSlot {team, idx, hero} | Занимает место. |
lobbyPickHero {hero} | Меняет героя. |
ready {on: true} | Отмечает вас готовым. |
lobbyChat {text} | Пишет в чат лобби. |
setSlot {team, idx, kind, difficulty} хозяин | Открывает место (open), закрывает его (none) или отдаёт ИИ (ai, сложность от 0 — лёгкий до 3 — хардкор). |
kick {team, idx} хозяин | Убирает игрока с этого места. |
startGame хозяин | Начинает матч. |
Ответ {applied: 'sent'} значит, что запрос дошёл до сервера. Чтобы узнать результат,
прочитайте lobby ещё раз.
Знакомство с картой
{op: 'guide', map: 'crossing'} возвращает правила игры на карте: цель, где стоят постройки,
от которых зависит исход матча, боссы и нейтральные лагеря с координатами клеток и правила, которые есть только
на этой карте, - как призвать босса, что даёт захваченный флаг, когда открываются ворота. Запрос работает и в
меню, и в матче, в любом режиме; без map он вернёт текущую карту. Правила есть у
каждой карты; ключи карт перечислены в разделе Запуск по ссылке. Тот же текст выводит игровая консоль: guide или
guide highpass. Текст правил - на английском.
{ map: 'duel', name: 'Duel', width: 44, height: 44,
coordinates: 'Coordinates are map tiles (x,y), the same indices the tile, units and map ops use; ...',
time: 'Times are match time (m:ss since the match began), ...',
lines: ['# Duel - champions in a pit', 'GOAL: be the last team with a living champion. ...', ...] }
Способностей героев и характеристик боссов там нет - их читают запросами spells,
unit и hero.
В матче
Запросы о матче отвечают только во время матча и не в первые 3 секунды; до этого они возвращают
the match is starting. Когда они заработают, hello сообщит gameOpen: true.
| Query | Возвращает |
|---|---|
state | Всё сразу: игровое время, день или ночь, название, размер и правила карты, ваше золото, все стороны и их отношение к вам, ваших героев целиком, остальных ваших юнитов, видимых юнитов, героев, ожидающих возрождения, а на карте «захват флага» — флаги (ctf, тот же блок, что возвращает flags). |
map | Карту строками текста: . — проходимо, # — нет, и уровень высоты каждой клетки. |
tile {x, y} | Одну клетку: рельеф, высоту, проходимость и то, что на ней стоит, если вы её видите. |
units {filter} | Юнитов с фильтром all, mine, others, heroes или structures. |
unit {id}, hero {id} | Одного юнита: все характеристики для вашего, общие сведения для чужого. |
spells {unit} | Способности вашего юнита: готова ли каждая, сколько осталось перезарядки, стоимость и дальность. |
items {unit} | Предметы в рюкзаке и надетые, их заряды и можно ли использовать каждый прямо сейчас. |
skills {unit} | Дерево навыков, уровень каждого навыка и можно ли изучить его сейчас. |
shops {hero} | Видимых торговцев и для этого героя — цену каждого предмета и можно ли его купить. |
events {since} | Что случилось после переданного номера: смерти, убийства, новые уровни, возрождения, волны, наступление ночи и дня, взятый, брошенный, возвращённый или захваченный флаг, конец матча. |
flags | На карте «захват флага» (Знамёна ущелья): захваты каждой стороны и её отношение к вам, состояние каждого флага (home, carried, dropped), где он сейчас, его подставка, кто его несёт, когда брошенный флаг вернётся сам, и ослабление носителей, пока оба флага унесены. Положение флага на такой карте видно всем, поэтому туман здесь не действует: клетка вражеского носителя есть в ответе, даже когда сам носитель не среди видимых вам юнитов (об этом говорит carrier.visible). |
result | Чем закончился матч: итог для вас и итоговая таблица. См. После матча. |
Позиции — это координаты клеток. Время — в игровых тиках, 60 на игровую секунду. Юниты, которых вы не видите, в ответ не попадают, как и вражеские здания, за которыми никто с вашей стороны не наблюдает.
| Command | Что делает |
|---|---|
move {units, x, y} | Идти туда. |
attackMove {units, x, y} | Идти туда, сражаясь со всеми по пути. |
attack {units, target} | Атаковать видимого юнита. |
stand {units} | Остановиться. |
cast {unit, spell, target} | Применить способность к юниту. Для способности на себя target не нужен. |
castAt {unit, spell, x, y} | Применить способность, нацеливаемую на землю, к точке. Способность на юнита применяется через cast. |
learn {unit, skill} | Потратить очко навыка. |
useItem {unit, item, target}, equip, unequip, drop | Использовать, надеть, снять или выбросить предмет. |
buy, sell {unit, target, item} | Торговать с торговцем target: с тем, с кем ваша сторона может торговать, и герой должен стоять не дальше 4 клеток от него — как в окне торговли. |
resetSkills {unit} | Сбросить дерево навыков за золото, на базе. |
pickHero {key} | Выбрать героя, пока открыт выбор героя. |
buyback | Выкупить павшего героя на картах, где это разрешено. |
dropFlag | На карте «захват флага» ваш герой кладёт несомый флаг там, где стоит, чтобы передать его союзнику; поднять его снова он сможет только через 3 секунды. |
chat {text} | Написать своей команде в сетевой игре. |
ping {x, y, kind} | Отметить точку: kind 0 — внимание, 1 — опасность. |
Приказы принимают только ваши юниты. Способности, навыки и предметы называются по полю name,
которое возвращает Query. Ответ applied значит, что приказ выполнен. В сетевой игре ответ —
queued, и приказ срабатывает через долю секунды, как приказы любого игрока.
После матча
Когда матч заканчивается, state и hello сообщают over: true, а в
events приходит запись matchEnded с итогом. После этого {op: 'result'}
возвращает окно результатов в виде данных:
{ result: 'Victory', won: true, mission: 'duel', clock: '4:12', seconds: 252, tick: 15120,
columns: ['Kills', 'Losses', 'Gold', 'Score'],
me: 0,
rows: [ { name: 'Ann (bot)', relation: 'self', ai: false, api: 'command', isPlayer: true,
values: [3, 1, 940, 1210] },
{ name: 'Red player 1', relation: 'enemy', ai: true, api: null, isPlayer: false,
values: [1, 3, 610, 540] }, ... ],
summary: [] }
result - это Victory, Defeat, Tie или
Surrender, а won - итог для вашей стороны: true, false или
null при ничьей. В rows по строке на каждую сторону в порядке таблицы, лучший счёт
первым; values идут в порядке columns, а me - номер вашей строки. В
таблице только те столбцы, где кто-то что-то набрал, и всегда Score; названия столбцов - английские
ключи: Kills, Losses, Assists, Gold (заработанное за матч),
Captures, Score и другие. В summary - заключительные строки карты,
например какая крепость пала.
Итог можно прочитать и после выхода в меню, до начала следующего матча, так что бот может сначала вернуться в
меню и прочитать его там. Пока ни один матч не закончился, ответ - no match has ended yet; во время
матча - the match has not ended yet. Запрос работает в обоих режимах.
Ограничения
Интерфейс делит компьютер игрока с игрой, поэтому работает в заданных пределах. Они подобраны так, что разумный бот с ними не сталкивается.
| Ограничение | Что происходит |
|---|---|
| 10 приказов в секунду | Не больше 10 приказов в секунду, с запасом на 10 подряд. Сверх этого ответ — {ok: false, error: 'rate limited', retryAfterMs}: подождите указанное время и отправьте снова. Запросы в лобби входят в тот же лимит, как и смена объявленного режима, пока вы в лобби; выход из игры — нет. Отклонённый приказ лимит не расходует. |
| Повторные приказы | move, attackMove, attack или stand, совпадающий с предыдущим приказом и отправленный в пределах секунды, повторно не отправляется. Ответ — applied: 'unchanged', и в лимит он не засчитывается. Остальные приказы отправляются всегда: второй learn того же навыка — это его следующий уровень. |
| Юнитов в приказе | Один приказ называет не больше 64 юнитов. Юнит, названный дважды, считается один раз. |
| Размер запроса | Запрос длиннее 65 536 символов отклоняется без чтения: {ok: false, error: 'request too large'}. |
| Работа за кадр | За каждый кадр игра отвечает на запросы в пределах фиксированного объёма работы. Дороже всего state и shops, а мелкие запросы вроде spells почти ничего не стоят. Запрос, который не помещается, ждёт следующего кадра. |
| Карта | Первый запрос map за матч занимает целый кадр; затем карта запоминается, и следующие запросы почти ничего не стоят. |
| Очередь запросов | Ждать могут до 256 запросов. Сверх этого ответ — {ok: false, error: 'busy', retryAfterMs}: подождите и отправьте снова. |
| Сетевые игры | Сервер тоже ограничивает каждое место — игрока или бота — 15 приказами в секунду с запасом на 30 подряд и отбрасывает остальные. Об этом он сообщает: hello и state считают отброшенные приказы в ordersDropped, а событие ordersDropped приходит в течение секунды. |
Ответ всегда приходит в одном из следующих кадров, поэтому цикл без пауз замедляет только вашу программу, но не игру.
Простой бот
Откройте https://www.alderun.net/play/?api=command&go=single&mission=arena&hero=warrior
и выполните этот код на странице — из расширения браузера или из инструмента, который управляет браузером.
Герой изучает навык, как только может, и атакует ближайшего видимого врага. Когда матч заканчивается, бот выводит итог.
const call = req => alderun.request(req);
const sleep = ms => new Promise(r => setTimeout(r, ms));
const dist = (a, b) => Math.hypot(a.x - b.x, a.y - b.y);
if (!window.alderun)
await new Promise(r => addEventListener('alderun-api', r, {once: true}));
while (!(await call({op: 'hello'})).result.gameOpen) await sleep(500);
for (;;) {
const s = (await call({op: 'state'})).result;
if (!s || s.over) break;
const hero = s.heroes[0];
if (!hero) { await sleep(1000); continue; } // ещё не появился или ждёт возрождения
const skill = hero.skills.find(k => k.canLearn);
if (skill) await call({op: 'learn', unit: hero.id, skill: skill.name});
const enemySides = s.fractions.filter(f => f.relation === 'enemy').map(f => f.id);
const enemies = s.visible.filter(u => enemySides.includes(u.fraction))
.sort((a, b) => dist(a, hero) - dist(b, hero));
if (enemies.length && (!hero.order || hero.order.target !== enemies[0].id))
await call({op: 'attack', units: [hero.id], target: enemies[0].id});
await sleep(500);
}
const r = (await call({op: 'result'})).result;
console.log(r.result, r.clock, r.columns, r.rows[r.me].values);
Интерфейс не видит сквозь туман войны, не отдаёт приказы чужим юнитам и не играет за место, которое не объявило себя ботом.