Arrays
Arrays hold numbers, booleans, texts, units, records, instances of a class, functions or
other arrays. let hp = [10, 20, 30], let xs = [a, a * 2, 0],
let lives: u8[] = new Array(12).fill(3). An index is a constant or a variable
(hp[i] += 7). .length, for (const x of xs) with break and continue, fill,
includes, indexOf and lastIndexOf (−1 when there is none), at(i) (from the end
when i is below zero) and join(", ") all run within the frame. Cells keep their type:
a u8[] stops at both ends. An array handed to a function is the same array.
Two things are not JavaScript's:
- An index past the end reads 0 and stores nothing. A constant index past the end is an error. A variable one is passed over in the game without a word, and Simulate says where it happened, since it is always a mistake.
const b = ais a copy, where JavaScript would give the same array a second name.
In a program of every player, each player has their own copy of an array;
shared([0, 0, 0]) is one for everybody.
Arrays that grow#
An array that something pushes to grows, with no size to declare.
const queue: number[] = [], queue.push(x), queue.pop(), queue.length = 0,
shift(), unshift(v) and splice(i, n, …) all work. pop() of an empty array is 0
here, where TypeScript says undefined, so write ?? 0 or ! after it. An empty array
wants its type said: let xs: number[] = [].
program(() => {
const hp = [10, 20, 30]; // three cells, for good
const queue: number[] = []; // starts empty and grows as it is pushed to
let total = 0;
for (const h of hp) total += h;
for (let i = 0; i < hp.length; i++) if (hp[i] > 15) queue.push(i);
while (queue.length > 0) {
const i = queue.pop()!;
hp[i] -= 5;
print(`cell ${i} is now ${hp[i]} of ${total}`);
}
});
program(() => {
const queue = [5, 6, 7];
queue.unshift(4);
const head = queue.shift() ?? 0;
queue.splice(1, 1, 60, 61);
print(`${head} ${queue.join(",")} ${queue.at(-1) ?? 0} ${queue.lastIndexOf(61)}`); // 4 5,60,61,7 7 2
});
The cells of every growing array come from one pool that the map's programs share: 16 384 cells, unless the workspace's Settings view (Ctrl+,) says otherwise. An array that fills its block is moved to one twice the size, and a block given back serves the next array that wants that size. An array declared again, in a loop or in a function called again, first gives back the block it held, so a scratch array made every round holds one block at a time. A single array can reach between a quarter and a half of the pool. When the pool has no block left, nothing more is pushed and the game says so once, in red. Simulate counts blocks as the game does, so it runs out at the same push.
Arrays of records, units and texts#
An array of records is an array per field, all growing together:
let waves = [{ count: 4, delay: 2 }], waves[i].count += 1,
waves.push({ count: 6, delay: 1 }). A record taken out of one (const w = waves[i], the
variable of a for…of, a record handed to a function) is the array's own, as an object in
a TypeScript array is a reference: w.delay = 9 writes the array. A field is a number, a
boolean, a unit, a text, an array of numbers, booleans or units, or a record of the same:
squads[i].seen.push(4), squads[i].name += "!". The hint at the end of the line says
how many cells a row comes to.
An array of units keeps the units themselves: const squad: Unit[] = [],
squad.push(u), squad[i].hp = 10, squad.pop()?.kill(). Unlike a loop over the game's
units, it may be kept across a sleep(). A unit in it that has died since reads 0, takes
no order, and is false in an if.
An array of texts is const names: string[] = [], with push, pop() (?? ""),
names[i] read, written and added to, length, for…of, forEach, includes,
indexOf and join.
program(() => {
const log: string[] = [];
let wave = 3;
log.push(`wave ${wave} began`);
log.push("boss down");
print(log.join(" / ")); // wave 3 began / boss down
});
The methods that take a function#
forEach, some, every, find, findLast, findIndex, findLastIndex, reduce,
map, filter, sort and reverse work on every kind of array, and chains of them do
too. The function is written as in TypeScript and is handed the item, its place and the
array. An arrow written there is copied into the loop the method becomes, so it costs
nothing to write one, and it sees the program's variables.
program(() => {
const scores = [12, 7, 30, 7];
let bonus = 5;
const raised = scores.map((s) => s + bonus);
const best = raised.reduce((m, s) => Math.max(m, s), 0);
const below = raised.filter((s) => s < best);
below.sort((a, b) => b - a);
print(`best ${best}, ${below.length} below it, the next is ${below[0]}`); // best 35, 3 below it, the next is 17
});
mapmakes an array of numbers or booleans.filtermakes one that grows, of whatever it ran over: records stay whole, units stay units. Neither can stand in a loop's condition, which is worked out again every round: make the array before the loop.findgives a unit ornullfrom an array of units, and a number orundefinedfrom an array of numbers. For records, take the place withfindIndexand readwaves[i].reducewants the value it starts from.sortwants its function: without one, JavaScript sorts numbers as text, 10 before 9. It sorts in place, within the frame, and keeps equal items in order. Dozens of items cost nothing. Hundreds every frame will be felt, and the line's hint says sorts in the frame.- A function handed to a method cannot
sleep(): the method is one loop within the frame. Afor…ofover the same array can sleep between its rounds.
The game's units take these methods too: unitsOf(P1).forEach((u) => u.heal(10)), some,
every, reduce, map, find and filter. filter is how to keep some of them, as an
array that can be sorted and kept across a sleep():
program(() => {
const squad = unitsOf(P1, { type: units.TerranMarine }).filter((u) => u.hp < 20);
squad.sort((a, b) => a.hp - b.hp);
for (const u of squad) { u.heal(10); sleep(frames(4)); } // the weakest first, one every four frames
});
A list the script made takes them as well. With a variable of the program in the function,
prices.map((p) => p + bonus) is an array of the program. With nothing of the program in
it, [1, 2, 3].map((x) => x * 2) is the script's own arithmetic, done when the script is
built.
Patterns and spread#
Destructuring and spread are TypeScript's: const { x, y } = mouse(P1),
const [first, , third = 0, ...tail] = xs, for (const { count, delay } of waves), and
in a function's parameters. A name in a pattern holds a copy of a number, so changing x
does not change p.x. A record or an array inside what is taken apart stays itself.
Assigned, a pattern is a swap: [a, b] = [b, a], [hp[i], hp[j]] = [hp[j], hp[i]].
[...xs, v] and { ...p, y: 9 } copy into a new array or record.
function sum(...ns: number[]) takes the rest of a call's arguments as an array.
program(() => {
const waves = [{ count: 4, delay: 2 }, { count: 6, delay: 1 }];
let a = 1;
let b = 2;
[a, b] = [b, a];
for (const { count, delay } of waves) print(`${count} every ${delay}`);
const xs = [a, b, 3];
const [head, ...rest] = xs;
waves.push({ ...waves[0], count: 9 });
print(`${head}, then ${rest.length} more; ${waves.length} waves`); // 2, then 2 more; 3 waves
});
Arrays inside arrays#
let grid = [[0, 0, 0], [0, 0, 0]],
new Array(8).fill(0).map(() => new Array(8).fill(0)) or
Array.from({ length: h }, () => …) make a grid: grid[y][x] += 1, grid[y].length,
for (const row of grid). A row is an array like any other, and const row = grid[y] is
the grid's own cells, as in TypeScript. An array inside a record works the same way:
p.path[i], p.seen.push(v).
The compiler chooses how to keep it, and the line's hint says which:
- Every row the same length, and no row growing, is one flat array, no slower to read than a plain one. A row past its own end reads 0 and never reaches into the next row. The outer array may still grow by whole rows.
- Rows of different lengths, or a row something pushes to (
buckets[i].push(v)), are rows that grow, each a block of the pool. A row that goes gives its block back, so nothing leaks. Two levels deep; not three.
One thing is not JavaScript's: a row of rows that grow, kept in a const, is that place
in the outer array, not the array that was there. After
const last = buckets[2]; buckets.pop(); buckets.push([7]), last reads [7]. And
filter of rows gives copies, so a change through the rows it made does not reach the
rows it took them from.
Copies#
xs.slice(1), xs.concat(ys, 7), xs.toSorted(f), xs.toReversed() and
Array.from(xs) give a new array that grows. [...m.keys()] and [...set] give a
Map's or a Set's as one. f(...xs) spreads an array into a call when the script knows
how many there are: a list it has, or a tuple
(let box: [number, number, number] = [1, 1, 1]).