scmJS docs

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:

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

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:

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]).

scmJS 0.8.5 · Generated from the repository. The source is under the MIT license, which does not cover what ATTRIBUTION.md lists. StarCraft and Brood War are trademarks of Blizzard Entertainment; this project ships none of their data.