scmJS docs

Testing and Simulate

TrigScript can run a script without starting the game, in a simulator built into the editor. Simulate shows what the script does in its first twenty seconds. Tests are checks you write that run after every change. Neither adds anything to the map.

The simulator models death counts, switches, preserve, the order of the trigger list, the game's arithmetic and the map's players. It has units: the ones placed on the map, then whatever createUnit makes, giveUnits hands over, moveUnit moves and killUnitAt kills, which bring and command count. It leaves out what it cannot know without the game: nothing walks, nothing fights and nothing is built, so no unit dies unless the script (or a test) kills it. Scores and the countdown read 0, and keys, clicks, the mouse and chat read as nothing. The picture sets the two lists side by side.

What Simulate and the tests see, and what only the game has

The simulator keeps the game's time, so the frames it lists are the frames the game would act on: the first pass on frame 2, then one every 31 frames, or every frame in a map with a program. A Wait holds its trigger for its time at Fastest. See How triggers run.

Simulate#

Simulate (Ctrl+F5 in the TrigScript window) runs the script for 480 frames, twenty seconds of the game at Fastest. It lists, under the code, every action that ran with its frame and its line in the script, and the final value of every program variable. Triggers and programs run side by side, so a death count a program sets is seen by a trigger. A program or a trigger of a force runs for each of its players, and each line says whose it is.

Simulate: the script's actions, each with its frame and line

Simulate is a check on the logic. A script that waits for the enemy to die waits for ever here; for the fight, write a test that kills the units, or play the map.

Tests#

A test is ordinary TypeScript that runs in the simulator after every change that compiles. It never runs in the game and adds nothing to the map. If you have used Vitest or Jest, the names are the same: test, describe, beforeEach, test.only, test.skip, test.each and expect, imported from "trigscript".

A test of a trigger:

import { test, expect } from "trigscript";

trigger(P1, [bring(P1, units.TerranCivilian, locations.Shop, ">=", 1)], [
  moveUnit(P1, units.TerranCivilian, 1, locations.Shop, locations.Spawn),
  createUnit(P1, units.TerranMarine, 4, locations.Spawn),
], { preserve: true });

test("a civilian at the shop buys four marines", (sim) => {
  sim.place(P1, units.TerranCivilian, locations.Shop);
  sim.seconds(3);
  expect(sim.count(P1, units.TerranMarine, locations.Spawn)).toBe(4);
});

A test of a program, which reads the program's variables by name:

import { test, expect } from "trigscript";

// A Marine on the beacon calls the next wave, each bigger than the last.
program(() => {
  let wave = 0;
  while (true) {
    if (countUnits(P1, units.TerranMarine, locations.Beacon) > 0) {
      wave += 1;
      createUnit(P8, units.ZergZergling, wave * 4, locations.Spawn);
      print(`Wave ${wave}`);
      killUnitAt(P1, units.TerranMarine, "All", locations.Beacon);
    }
    sleep(frames(1));
  }
}, { name: "waves" });

test("the second wave is bigger", (sim) => {
  sim.place(P1, units.TerranMarine, locations.Beacon);
  sim.until(() => sim.program("waves").wave === 1);
  sim.place(P1, units.TerranMarine, locations.Beacon);
  sim.until(() => sim.program("waves").wave === 2);
  expect(sim.count(P8, units.ZergZergling, locations.Spawn)).toBe(12);
  expect(sim).toHavePrinted("Wave 2");
});

Every test gets sim, a world of its own: the map's placed units, locations and players, the script's programs and triggers, and the same random() results every run. Time in a test is the game's: nothing runs on frame 1, the first pass is on frame 2, and sim.frames(3) gives a program two runs.

What a test does With
Change the world sim.place(player, type, location, count?), sim.kill(unit), sim.remove(unit), sim.give(unit, to), sim.move(unit, to)
Let time pass sim.frames(n), sim.seconds(n), sim.until(() => …)
Act as a player sim.press("F2"), sim.click(), sim.type("-give 100"), sim.moveMouse(x, y)
Read the world sim.count(player, type, location?), sim.units(filter?), sim.resources(player), sim.deaths(player, type), sim.switch(n)
Read a program sim.program("waves").wave
See what was shown sim.printed(), expect(sim).toHavePrinted("Wave 2")

A test fails when an expect does not hold, when it throws, and when a program does something that is always a mistake and that the game would pass over in silence, such as reading past the end of an array. A test that means to check for that says expect(sim).toHaveFaulted().

Tests live beside what they test, as above, or in files whose names end in .test.ts, in any folder. A test file can import the script's own functions and test them as plain TypeScript. A test file is never part of the map: main.ts cannot import one, and a trigger() or a program() inside one is an error.

A failing test: the mark in the margin, what was expected at the end of the line, and the Testing view

In the editor, every test( has a mark in the margin (passed, failed or not run), and a click on the mark runs that test. A failure is shown where it happened, as expected 7, got 6 at the end of the line.

A failing test is a warning, not an error: the map still saves and builds. The Tests setting in the workspace's Settings view, kept in the map, makes a failing test stop the build instead.

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.