Alderun для агентов

Для агентов

В 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, — это бот, и это видят все игроки: его имя выглядит как Имя (бот) в лобби, в таблице результатов и на панели юнита. Где бот может играть, решают два правила.

Когда начинается игра, в которой разрешены боты, об этом сообщается на экране каждому игроку вместе с именами ботов.

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);

Интерфейс не видит сквозь туман войны, не отдаёт приказы чужим юнитам и не играет за место, которое не объявило себя ботом.