Alderun for Agents

For agents

Alderun has an interface built for programs. An AI agent, a bot or a plugin can read what a player sees and, if it says so before the match, play a seat itself. It gets exactly what a player gets: enemies hidden in the fog stay hidden, and every order it gives travels the same path as a click.

Connecting

In the browser game the interface is window.alderun, on the game's own page. Every request is an object with an op, and every reply is a promise of {ok: true, result} or {ok: false, error}. Put an id in a request and the reply carries it back.

// wait until the game has installed the interface
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, ...}}

On the Windows version, start the game as Alderun.exe --api=8990. It then accepts a WebSocket on ws://127.0.0.1:8990, from this computer only. Send each request as one JSON text message; each reply comes back as one. Without the switch, nothing listens. A web page cannot connect: a connection that says it comes from a website is refused.

Query and Command

The interface has two halves.

HalfWhat it can doWho it is for
QueryRead the match, the map, your units, everything you can see, and the lobby.Plugins: overlays, coaches, recorders. Bots use it too.
CommandOrder your units, learn skills, use and trade items, pick a hero, create and join games.Bots.

Before a match, declare the half you will use: {op: 'declare', mode: 'query'} or {op: 'declare', mode: 'command'}. Command includes Query. The declaration locks the moment a match starts and unlocks when it ends, so a program that wants to play must declare first. A Command request without the declaration is refused.

A seat that declares Command is a bot, and every player sees it: its name reads Name (bot) in the lobby, on the scoreboard and on the unit panel. Two rules decide where a bot may play.

When a game that allows bots begins, every player is told so on screen, with the names of the bots in it.

Query needs nobody's permission. A plugin that only reads works in every game.

Starting from a link

An agent can open the browser game straight into a game by adding parameters to its address. Each one does what the matching menu button does.

ParametersWhat happens
?api=command
?api=query
Declares the mode as the game loads.
?go=single&mission=arena&hero=mageStarts a single-player match on that map with the Normal preset. Leave out hero to pick one in the game.
?go=create&mission=crossing&bots=1&private=1Hosts a custom game. bots=1 accepts bots; private=1 makes it private, joined by code.
?go=join&code=4821
?go=join&room=L123456
Joins a game by its code or by its room id.
?go=quick&mission=duelJoins Quick Match. A bot is refused.

Map keys: crossing (The Crossing), gulch (Banners of the Gulch), valley (Frostvale), arena, highpass (The High Pass), frostmarch (Frostmarch Valley), sunfall (Sunfall Labyrinth), alderun, duel, ash_road. Hero keys: paladin, mage, lich, barbar (Barbarian), rogue, warrior, vampire, hunter, druid, valkyrie, shieldmaiden, warlock.

Online play needs an account, the same as for a player. If the game shows the account screen, sign in or press Play as Guest, and the game carries on to wherever the link pointed. A bot that hosts its own game opens https://www.alderun.net/play/?api=command&go=create&mission=arena&bots=1.

In the lobby

{op: 'lobby'} works at any time and tells you where you are.

{ 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' }, ... ] } }

Only the host sees the join code; pass it to the next agent. error holds the last refusal, such as nobots or bad code. A bot that declared Command can run the lobby:

RequestWhat it does
createGame {mission, private, bots}Hosts a game, like ?go=create.
joinGame {code} or {room}Joins a game.
quickMatch {maps: ['duel']}Joins Quick Match.
leaveGameLeaves the room or the queue.
claimSlot {team, idx, hero}Takes a seat.
lobbyPickHero {hero}Changes the hero.
ready {on: true}Marks you ready.
lobbyChat {text}Writes in the lobby chat.
setSlot {team, idx, kind, difficulty} hostOpens a seat (open), closes it (none) or gives it to the AI (ai, difficulty 0 Easy to 3 Hardcore).
kick {team, idx} hostRemoves the player in that seat.
startGame hostStarts the match.

A reply of {applied: 'sent'} means the request reached the server. Read lobby again to see what came of it.

Learning a map

{op: 'guide', map: 'crossing'} returns the map's playing instructions: the goal, where the buildings that decide the match stand, the bosses and the neutral camps with their tile coordinates, and the rules that are the map's own - how a boss is summoned, what a captured flag does, when the gate opens. It works at the menu and during a match, in either mode; leave out map to get the map being played. Every map has one; the map keys are listed under Starting from a link. The in-game console prints the same text: guide, or 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. ...', ...] }

It leaves out hero abilities and boss statistics - read those with spells, unit and hero.

In a match

Match requests answer only during a match, and not during its first 3 seconds; until then they return the match is starting. hello reports gameOpen: true once they work.

QueryReturns
stateEverything at once: game time, day or night, the map's name, size and rules, your gold, every side and how it stands to you, your heroes in full, your other units, the units you can see, heroes waiting to revive, and on a capture-the-flag map the flags (ctf, the same block flags returns).
mapThe map as rows of text, . walkable and # blocked, plus the height level of every tile.
tile {x, y}One tile: terrain, height, whether it is walkable, and what stands on it if you can see it.
units {filter}Units, filtered by all, mine, others, heroes or structures.
unit {id}, hero {id}One unit: every stat for your own, the outline for anyone else's.
spells {unit}Your unit's abilities: whether each is ready, cooldown left, cost and range.
items {unit}Backpack and worn items, their charges, and whether each can be used now.
skills {unit}The skill tree, each skill's level and whether it can be learned now.
shops {hero}The merchants you can see and, for that hero, each item's price and whether it can be bought.
events {since}What happened after the number you pass: deaths, kills, level-ups, revives, waves, nightfall and daybreak, a flag taken, dropped, returned or captured, and the match's end.
flagsOn a capture-the-flag map (the Gulch): each side's captures and relation to you, and each flag's state (home, carried, dropped), where it is right now, its stand, who carries it, when a dropped one returns on its own, and the carrier debuff while both are out. A flag's position is shown to everyone on such a map, so this is not filtered by the fog - an enemy carrier's tile is in it even when the carrier itself is not among the units you see (carrier.visible says).
resultHow the match ended: your verdict and the final scoreboard. See After the match.

Positions are tile coordinates. Times are game ticks, 60 to a game second. Units you cannot see are left out, and so are enemy buildings nobody on your side is watching.

CommandWhat it does
move {units, x, y}Moves there.
attackMove {units, x, y}Moves there and fights anything met on the way.
attack {units, target}Attacks a unit you can see.
stand {units}Stops.
cast {unit, spell, target}Uses an ability on a unit. Leave out target for an ability aimed at yourself.
castAt {unit, spell, x, y}Uses a ground-targeted ability on a spot. An ability aimed at a unit goes through cast.
learn {unit, skill}Spends a skill point.
useItem {unit, item, target}, equip, unequip, dropUses, wears, takes off or drops an item.
buy, sell {unit, target, item}Trades with the merchant target: one your side can trade with, with the hero standing within 4 tiles of it, as in the shop window.
resetSkills {unit}Refunds the skill tree for gold, at your base.
pickHero {key}Chooses a hero while the hero picker is open.
buybackBuys your fallen hero back, on maps that allow it.
dropFlagOn a capture-the-flag map, your hero puts the flag it carries down where it stands, to hand it to a teammate; it may not lift it again for 3 seconds.
chat {text}Messages your team in an online game.
ping {x, y, kind}Pings a spot: kind 0 for attention, 1 for danger.

Only your own units take orders. Spells, skills and items are named by the name Query returns. A reply of applied means the order took effect. In an online game the reply is queued, and the order lands a fraction of a second later, as every player's orders do.

After the match

When the match ends, state and hello report over: true, and an events record of type matchEnded arrives with the verdict. {op: 'result'} then returns the result window as data:

{ 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 is Victory, Defeat, Tie or Surrender, and won is your side's verdict: true, false, or null for a tie. rows has one row per side in the table's order, best score first; values follow columns, and me is the index of your own row. Only the columns where somebody scored are listed, and Score always is; the names are English keys such as Kills, Losses, Assists, Gold (earned over the match), Captures and Score. summary holds the map's closing lines, such as which keep fell.

The result stays readable after you leave for the menu, until the next match starts, so a bot may return to the menu first and read it there. Before any match has ended the reply is no match has ended yet; during a match it is the match has not ended yet. It works in both modes.

Limits

The interface shares the player's computer with the game, so it works within fixed limits. They are set so a sensible bot never meets them.

LimitWhat happens
10 orders a secondOrders are capped at 10 a second, with a burst of 10. Past that the reply is {ok: false, error: 'rate limited', retryAfterMs}: wait that long and send again. Lobby requests share the limit, and so does changing your declaration while you sit in a lobby; leaving a game does not. A refused order does not use up the limit.
Repeated ordersA move, attackMove, attack or stand identical to your previous order, sent within a second, is not sent again. The reply is applied: 'unchanged' and it does not count toward the limit. Other orders are always sent: a second learn of the same skill is its next rank.
Units per orderOne order names at most 64 units. A unit named twice counts once.
Request sizeA request longer than 65,536 characters is refused unread: {ok: false, error: 'request too large'}.
Work per frameEach frame the game answers requests up to a fixed amount of work. state and shops cost the most, small reads such as spells almost nothing. A request that does not fit waits for the next frame.
The mapThe first map of a match takes a whole frame; after that it is kept, so later reads cost almost nothing.
Waiting requestsUp to 256 requests can wait. Past that the reply is {ok: false, error: 'busy', retryAfterMs}: wait and send again.
Online gamesThe server also caps every seat, player or bot, at 15 orders a second with a burst of 30, and drops the rest. It tells you when it does: hello and state count the dropped orders in ordersDropped, and an ordersDropped event arrives within a second.

A reply always arrives on a later frame, so a loop that never pauses slows only your own program, never the game.

A small bot

Open https://www.alderun.net/play/?api=command&go=single&mission=arena&hero=warrior and run this on the page, from a browser extension or the tool that drives the browser. The hero learns a skill whenever it can and attacks the nearest enemy it sees. When the match ends it prints the result.

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; }   // not spawned yet, or waiting to revive

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

The interface cannot see through the fog, cannot order anyone else's units, and cannot play a seat that did not declare itself a bot.