Programs
Everything inside program(() => { … }) runs in the game:
program(() => {
let wave = 0;
let alarm = false;
while (true) {
if (bring(P1, units.AnyUnit, locations.Beacon, ">=", 1) && !alarm) {
alarm = true;
displayText("They are coming.");
}
if (alarm) {
createUnit(P8, units.ZergZergling, 4, locations.Spawn);
wave += 1;
}
if (wave >= 10) defeat();
sleep(seconds(20));
}
}, { owner: P1 });
A program runs every frame, from where it left off. Its body runs until it reaches a
sleep() or its end, all within one frame of the game, and the next frame it carries on
from there. A body that ends stops for good. So the loop above is a game loop: it looks
at the beacon, sends a wave if the alarm is up, and sleeps twenty seconds.
Variables hold numbers, booleans, texts and units of the game, and a
let p = { lives: 3, gold: 0 } is a variable per field. They live in the game while the map is played and take nothing from
the map: no death counters, no switches, no triggers in the list. Numbers are whole and
signed, as in TypeScript: a - b is below zero when b is larger, while (i >= 0) ends,
and a number below zero is shown with its minus sign. A number runs from −2 147 483 648 to
2 147 483 647 and wraps at either end. A u8 or u16 variable (let lives: u8 = 3) stays
within 0 … 255 or 0 … 65 535 and stops at both ends, which is what lives and cooldowns
want; a u32 runs from 0 to 4 294 967 295, for bit masks and hashes, and is kept apart
from a plain number in arithmetic unless u32(x) or i32(x) says which is meant. +,
-, *, / and % work between variables, with Math.min, Math.max, Math.abs and
clamp(), and so do the bitwise &, |, ^, <<, >> and >>>; division is whole and
towards zero, and dividing by a variable that is 0 gives 0. Where the game takes nothing
below zero — hit points, an amount, a unit count — a number below zero goes in as 0.
Arrays hold numbers, booleans, texts, units, records or other arrays: let hp = [10, 20, 30], new Array(12).fill(3), an
index that is a constant or a variable (hp[i] += 7), .length, for (const x of xs),
fill, includes and indexOf. An array that something pushes to grows —
const queue: number[] = []; queue.push(x); queue.pop() ?? 0 — with no size to declare: the
cells come from a pool the map's programs share, and the workspace's Settings view
(Ctrl+,) sets how large it is, a setting kept in the map. An index past the end reads 0 and
stores nothing, and Simulate says where. An array of records — let waves = [{ count: 4, delay: 2 }], waves[i].count += 1, waves.push({ … }) — hands out its records by
reference, as TypeScript does, and an array of units (const squad: Unit[] = [],
squad.push(u)) may be kept across a sleep(). A record in an array may hold a unit, a text, an array of its own
(squads[i].members.push(u)) or another record. An array of arrays is a grid —
let grid = [[0, 0, 0], [0, 0, 0]], grid[y][x] += 1 — or rows that each grow
(buckets[i].push(v)), and const names: string[] = [] is an array of texts. A list made
outside the program — const price = [50, 100, 150], or a wave table of records — can be
looked up with a variable: price[level], waves[wave].count.
The array methods that take a function are there, written as in TypeScript: forEach,
some, every, find, findIndex, reduce, map, filter, sort and reverse, and
chains of them. filter over the units of the game is how to keep some of them:
program(() => {
while (true) {
// The three most hurt units at the base get 20 hit points, every two seconds.
const hurt = unitsAt(locations.Base, { owner: CurrentPlayer }).filter((u) => u.hp < u.maxHp);
hurt.sort((a, b) => a.hp - b.hp);
for (const u of hurt.slice(0, 3)) u.heal(20);
sleep(seconds(2));
}
}, { owner: AllPlayers });
The function is written into the loop the method becomes, so it costs nothing and sees the
program's variables — and for the same reason it cannot be kept in a variable: write it
where it is used, or name a function. sort wants its function ((a, b) => a - b), runs
within the frame, and is nothing for dozens of items and felt for hundreds every frame; the
end of the line says sorts in the frame. slice, concat, toSorted, toReversed and
Array.from make copies. Patterns and spread work as well: const { x, y } = mouse(P1),
for (const { count, delay } of waves), [a, b] = [b, a], [...xs, 7],
waves.push({ ...w, count: 9 }).
Tables keyed by an id of the game are arrays with a cell for every id, so a key of the
game is one read: const bounty: Record<UnitType, number> = { [units.ZergZergling]: 5 } and
bounty[u.type], new Map<UnitType, number>() with get (?? 0 for a key never set),
set, has, delete, clear and size, new Set<UnitType>() with add and the same;
for (const [key, value] of lost) goes through the keys that are there.
The keys are unit types, players, locations, switches, weapons, upgrades or technologies.
A Map and a Set over any number, or over units, are for keys that are no id of the
game: new Map<number, number>() for a tile packed into one number, new Map<Unit, number>() for a cooldown a unit, new Set<Unit>() for the units already dealt with. They
go through their keys in the order the keys went in, as JavaScript's do. A key is looked
for, a few steps where a table keyed by ids of the game is one read, and the values are
numbers or booleans. A unit that died stays an entry until it is deleted; in a loop over
the map it reads as no unit, which is the moment to delete it.
Control flow is what it says. if/else, while, do, for, switch, break,
continue and c ? a : b all work. Conditions go in an if or a while; actions stand
as statements. A loop runs all its rounds at once, within the frame, which has one
consequence to keep in mind: a loop that never ends and never sleeps would freeze the
game. The editor refuses one and says where to put sleep(frames(1)). A for whose
bounds are known when the script is applied is unrolled, and the editor says so at the
end of the line.
Time is sleep. sleep(seconds(15)) gives the frame back and carries on that much
later, while other programs and the map's triggers go on. frames(n) is the game's own
clock, a second is twenty-four frames at Fastest, and minutes() is there too.
Something that runs on its own clock is another program — one program per concurrent
activity. The game's own wait() is allowed but is a different thing: it stalls every
trigger of that player, so use it for a short pause (a text, then a sound) and sleep
to pass time.
Edges. if (rose(bring(…))) is true on the frame the condition becomes true and not
again until it has been false in between; once(…) is true the first time only.
random() is a coin toss, and random(n) a whole number from 0 to n − 1.
Reads: what the game holds is a value. Every condition that compares a quantity is also a read of it when the comparison and the amount are left out, and a read goes wherever a number goes:
program(() => {
let price = 50;
while (true) {
if (minerals(CurrentPlayer) >= price * 2 && bring(CurrentPlayer, units.AnyUnit, locations.Beacon) >= 1) {
setResources(CurrentPlayer, "subtract", price, "ore");
createUnit(CurrentPlayer, units.TerranMarine, 1, locations.Spawn);
price += deaths(CurrentPlayer, units.TerranMarine);
}
sleep(seconds(1));
}
}, { owner: AllPlayers });
deaths(p, unit), kill(p, unit), bring(p, unit, location), command(p, unit),
accumulate(p, resource), score(p, kind), countdownTimer() and elapsedTime() all
read this way, and the common ones have plainer names: minerals(p), gas(p),
countUnits(p, unit, location?), kills(p, unit), countdown(), elapsed() — those two
in the game's own seconds, which at Fastest pass about one and a half times as fast as
the seconds of sleep. A read means what its condition means — a force's minerals are the force's sum, units.Men
counts what Bring counts — and it is taken when the line runs, so let ore = minerals(P1) keeps the number and minerals(P1) written twice reads twice. What to read
— the player, the unit, the location — is fixed when the script is applied. About the
players themselves there are race(p) (compare it with races.Zerg, .Terran,
.Protoss), slot(p) (slots.Human, .Computer, .Empty), isHuman(p),
hasLeft(p) and supply(p, "used" | "max" | "provided") as the top bar shows it.
The units on the map are objects. A loop runs over the ones that match, a pick finds one, and a unit has properties and things it can be told:
program(() => {
while (true) {
// Everything Player 2 has on the hill is worn down to half health.
for (const u of unitsAt(locations.Hill, { owner: P2 })) u.hp = u.maxHp / 2;
// The Marine nearest the beacon is sent to the base; nobody else moves.
const scout = nearest(units.TerranMarine, locations.Beacon, { owner: P1 });
if (scout) scout.order("move", locations.Base);
sleep(seconds(5));
}
});
unitsAt(location, filter?), unitsOf(player, filter?) and allUnits(filter?) are what
a for…of runs over; first(filter?), nearest(type, location, filter?) and
randomUnit(filter?) give one unit, or null when nothing matches — so the if (scout)
is required, and the editor says so when it is missing. A filter is { type, owner, at };
units.Men, units.Buildings and units.Factories work as a type. A unit has hp,
shields and energy in whole points, maxHp and maxShields, owner, type, x,
y, kills, cooldown, resources, the spell timers (stim, lockdown, stasis, …)
and invincible, burrowed, cloaked, hallucinated, underAttack. Hit points,
shields, energy, kills, the cooldown, the timers and invincible can be written; the
position, the owner and the rest are read only, and the editor marks a write to one as
you type. A unit can be told order("move" | "patrol" | "attack", location),
give(player), kill(), remove(), damage(n), heal(n) — or { percent: 50 } — and
locate(location), which centres a location on the unit so that the ordinary actions can
happen where it stands.
A variable can keep a unit across a sleep. Units die, and the game hands a dead unit's
place to the next one made, so every use checks that the unit is still the same one:
once it is gone its numbers read 0 and nothing written or told to it has any effect, and
if (u) asks whether it is still there. A loop over units runs within one frame, so a
sleep inside one is an error; to take units one at a time, find the next after each
sleep. Each loop and each pick looks through all of the game's 1700 unit slots when its
line runs, which is nothing a few times a second and worth a thought every frame: the
editor writes scans units at the end of such a line.
stats() reaches the game's own tables: what a unit type costs, what a weapon does,
which upgrades a player has.
program(() => {
stats(units.TerranMarine).minerals = 25;
stats(units.TerranMarine).speed = 6.5; // pixels a frame; a Marine walks at 4
stats(units.ZergZergling).name = "Dog";
stats(weapons.GaussRifle).damage += 2;
stats(P1).upgrades[upgrades.TerranInfantryWeapons] = 3;
stats(P3).color = "teal";
});
A field reads as a number (or true / false) and takes = and +=. Unit types, weapons
(weapons.), upgrades (upgrades.), technologies (techs.) and players each have their
own fields, and the completion list after the dot is the list: only what was played and
seen working in Remastered is offered, and the hover on each field says what it reaches —
most unit-type fields apply to units made after the write, a weapon's to every unit
using it, a colour at once. A write lasts for the game.
What the players do is there to read: a key, a mouse button, where the mouse is, and what a player types.
program(() => {
while (true) {
if (keyPressed(CurrentPlayer, "F8")) createUnit(CurrentPlayer, units.TerranMarine, 1, locations.Anywhere);
if (clicked(CurrentPlayer, "right")) {
const at = mouse(CurrentPlayer);
displayText(`You clicked at ${at.x}, ${at.y}.`);
}
const m = chatted(CurrentPlayer, "-spawn {n} {what:unit}");
if (m) createUnit(CurrentPlayer, m.what, m.n, locations.Anywhere);
underMouse(CurrentPlayer, { owner: CurrentPlayer })?.heal(10);
sleep(frames(1));
}
}, { owner: AllPlayers });
A key, a click and a typed line are true on the one frame they arrive, so look for them
in a loop that sleeps one frame at a time. keyPressed is true once per press — not
while the key is held, and not while the player is typing a message. mouse(p) is the
place on the map under the player's cursor, in pixels (32 to a tile);
centerLocation(location, x, y) moves a location there, so that a unit can be created
under the cursor, and underMouse(p) is the unit nearest the cursor, or null.
chatted(p, pattern) is null until the player sends a line that fits the pattern, and
then holds what the pattern read out of it. The pattern's own words are matched exactly
and the whole line has to fit; {n} reads a whole number, {what:unit} a unit type by
its name (the rest of the line, so it comes last) and {kind:ore|gas} one of the listed
words, as its place in the list. The editor knows the names in the pattern: after m. it
offers n and what, and nothing else. A pattern starts with a word of its own, such as
-spawn, so ordinary talk is left alone. A game played in single player has no chat, so
try typed lines in a multiplayer game — hosting one alone is enough.
All of this reaches every player's computer in step, a few frames after it happens. It costs the map a little, and only when a program reads input: one free location among the first 63 (nine when the mouse is read; the script is told when there is no room), and the Valkyrie and Player 12, which carry the input between computers and must be left alone.
A text can hold the program's numbers. Write it as a template literal:
program(() => {
let wave = 0;
while (true) {
wave += 1;
displayText(`Wave ${wave}: you have ${minerals(CurrentPlayer)} ore, ${name(CurrentPlayer)}.`);
print(`${color(P1)}${name(P1)}\x01 leads with ${kills(P1, units.AnyUnit)} kills`, { to: AllPlayers, position: "center" });
sleep(seconds(30));
}
}, { owner: AllPlayers });
name(p) is the player's name and color(p) the colour code of their colour, filled in
by the game. displayText shows the text to the current player, as it always has;
print(text, { to, position }) shows it to someone else — a player, AllPlayers, a
force — or, with position: "center", on the line in the middle of the screen where the
game's own messages appear.
A text is a value. A string variable holds one, a function takes and returns one, a
record has one for a field, and there is no length to declare:
program(() => {
let shown = "";
while (true) {
const slain = kills(P1, units.AnyUnit);
const rank = slain >= 50 ? "Veteran" : slain >= 10 ? "Soldier" : "Recruit";
const line = `${rank}: ${slain} kills, ${Math.max(50 - slain, 0)} to go`;
if (line != shown) { setMissionObjectives(line); shown = line; }
sleep(seconds(1));
}
});
Texts take +, +=, templates, the comparisons, length, s[i], slice, indexOf,
includes, startsWith, endsWith, padStart, padEnd, repeat and
for (const ch of s); a character is a character, so "저글링".length is 3. The
objectives, a leaderboard's label, a transmission's line and a unit type's name
(stats(units.ZergZergling).name = …) take a text the program made, up to 255 bytes; what
is on the screen is the text as it was when the action ran, so run the action again when
the text changes, as above. A made text holds 1 023 bytes and is cut off past that, which
the game says once in red. split, replace, trim and parseInt are not there: keep a
number beside the text it was made into.
Functions declared inside the program, or made with game() in any file, run in the
game too. Arguments pass by value, they may return a number, a boolean, a text or a unit —
function canAfford(price: number) { return gold >= price; } — and they may sleep. A
function used once is written into the program where it is called; one used more than
once is a single copy in the built map that every call runs, which keeps a script with
helpers small. The end of the function's line says which it got (called ×3, inlined
×2), and hovering it says why: a function that sleeps, or whose parameter has to be
known when the map is built (the player of setResources(p, …)), stays inlined. A
function may call itself — fib(n - 1) + fib(n - 2), a flood fill over an array — as long
as it does not sleep: each run has its own parameters and locals, as in TypeScript. It may
go 1 024 calls deep unless the workspace's Settings says otherwise; a call past that stops
the program, and the game says where. Thousands of such calls within one frame make the
game pause, since each keeps its function's variables while it runs.
Classes are TypeScript's, declared inside the program: fields, a constructor, methods,
get and set, static, private, extends with super.
program(() => {
class Wave {
left: number;
constructor(public unit: UnitType, count: number) { this.left = count; }
get done() { return this.left == 0; }
send() {
createUnit(P8, this.unit, 1, locations.Spawn);
this.left--;
}
}
const waves = [new Wave(units.ZergZergling, 12), new Wave(units.ZergHydralisk, 6)];
for (const w of waves) {
print(`${w.left} are on their way`, { to: AllPlayers });
while (!w.done) { w.send(); sleep(seconds(1)); }
sleep(seconds(30));
}
victory();
});
An instance is a record and a method a function handed the instance, so what holds for
functions holds for methods. Which class an instance is, is settled when the script is
applied — nothing of a class is left in the game — and the limits follow from that: an
array of instances holds one class, a function can give back an instance only when every
return gives the same one (return this, so calls chain, or return new Wave(…)), and a
class a program writes to is declared inside it.
A program runs for its owner (Player 1 unless { owner: … } says otherwise), as that
player, while that player is in the game. AllPlayers, a force or a list of players
makes a per-player program: the same code runs for each of those players who is in
the game, computers included, CurrentPlayer is that player, and every variable is per
player, each with their own copy — which is how lives, scores and cooldowns are written
once. let total = shared(0) is one value they all share.
Everything the body reads from outside is computed when the script is applied. A
constant, a helper, a condition, an action: each is worked out once, when the script
runs, and the editor underlines those parts with dots so the boundary is visible as you
type. That is what lets a helper written outside the program supply actions inside it.
It is also the one rule to keep in mind: a program variable cannot reach a condition, an
action or a helper, because those were computed before the game started. The exceptions
are the amount of setResources, setDeaths, setScore and setCountdownTimer, the
unit count of createUnit, killUnitAt, removeUnitAt and giveUnits and an action's
unit type, which can be variables or expressions over them, and a text: displayText and
print take any text the program made, and so do the objectives, a leaderboard's label, a
transmission and a unit type's name.