diff --git a/README.md b/README.md index 53992d9..618339d 100644 --- a/README.md +++ b/README.md @@ -14,10 +14,10 @@ variables, arrays, texts, functions, classes, loops, `sleep` and `await`. TrigSc the programs into the map when you save, as the game's own triggers, in under a second and with nothing else to install. A map with a program in it needs **StarCraft: Remastered**. A script with something TrigScript does not take yet is not built, and Problems says why on -the line (see [Use](#use)). The guide's page [TrigScript and -eudplib](https://docs.scmjs.dev/trigscript/trigscript-and-eudplib/) sets TrigScript beside -[eudplib](https://github.com/armoha/eudplib), the compiler behind euddraft: what you write, -the build, speed and size in the game, checking, and libraries. +the line (see [Use](#use)). The guide's page [Coming from +eudplib](https://docs.scmjs.dev/trigscript/coming-from-eudplib/) is for map makers who +build with [eudplib](https://github.com/armoha/eudplib) and euddraft: where each of its +ideas went, and how the two compare in speed and size. The editor is Monaco with a `.d.ts` generated from the open map, so `locations.`, `switches.`, `units.` and `players.` complete to what your map has, and a location passed @@ -391,1677 +391,20 @@ files, each with a tick, so a copy for release can leave the source out. ## The language -A script has two halves. [Triggers](#triggers) are what the script records while it runs: -ordinary triggers, for every version of the game. [Programs](#programs) are code the game -itself runs, for StarCraft: Remastered. [Beside the map](#beside-the-map) is how the editor -and the map work together. [TrigScript beside TypeScript](#trigscript-beside-typescript) -is the comparison in one table. Programs are most of this reference: +The language is documented on the [scmJS documentation site](https://docs.scmjs.dev/), +beside the editor's other guides: -| Section | What is in it | +| Guide | What is in it | | --- | --- | -| [Variables and records](#variables-and-records) | numbers, booleans, texts, units, players; a record is a variable a field; `undefined` | -| [Arrays](#arrays) | fixed and growing arrays, arrays of records, of units, of texts and of arrays; `map`, `filter`, `sort`, `splice` and the rest; patterns and spread; copies | -| [Tables, a `Map` and a `Set`](#tables-a-map-and-a-set) | lists the script has, tables keyed by an id of the game, a `Map` and a `Set` over any number, over units or over a few texts | -| [Numbers and arithmetic](#numbers-and-arithmetic) | `number`, `u32`, `u16`, `u8`; the operators and where they differ from JavaScript | -| [Numbers with a fraction](#numbers-with-a-fraction) | `fixed`, `Math.sqrt`, `Math.sin` and the rest | -| [Control flow and loops](#control-flow-and-loops) | loops run within the frame; the loop that never sleeps; unrolling; labels | -| [Functions](#functions) | inlined or called, what a function returns, functions that call themselves, `game()` functions in other files | -| [Functions as values](#functions-as-values) | functions kept in variables, arrays and fields; closures | -| [Classes](#classes) | instances, methods, `extends`, instances kept by reference, classes in any file | -| [Errors](#errors) | `try`, `catch`, `finally`, `throw` and classes of error | -| [What is worked out when the script is applied](#what-is-worked-out-when-the-script-is-applied) | the line between the script and the program | -| [Reading the game](#reading-the-game) | every condition's quantity as a value; player facts | -| [Units](#units) | loops over units, picks, a unit's fields and what it can be told | -| [Events](#events) | `on.created`, `on.died`, `on.killed`, `on.morphed`, `on.order` | -| [The game's tables and memory](#the-games-tables-and-memory) | `stats()`: costs, weapons, upgrades, colours; `memory.*`; EUD | -| [Keys, the mouse and chat](#keys-the-mouse-and-chat) | `keyPressed`, `keyHeld`, `clicked`, `mouse`, `underMouse`, `chatted`, `on.key`, `on.chat`; `local` and `sync()`; `select()` | -| [Text](#text) | templates with values, `print`, `string` variables and their methods, lines of JSX | -| [Sounds](#sounds) | `playWav`, sounds made when the script is built | -| [Waiting and tasks](#waiting-and-tasks) | `sleep`, `await`, `until`, `start()`, `Promise.all` and `race`; generators; `rose`, `once` | -| [Owners and per-player programs](#owners-and-per-player-programs) | one program for every player, `shared()` | -| [Files the script imports](#files-the-script-imports) | videos, images, sounds and MIDI notes | -| [The standard library](#the-standard-library) | `trigscript/math`, `random`, `timers`, `signals`, `ui`, `cutscene`, `fog`, `screen`, `music` | -| [What a program does not have](#what-a-program-does-not-have) | the parts of TypeScript a program refuses, and what to write instead | - -After those, *Coming from 3.9* and the sections under it say what each version changed. - -### TrigScript beside TypeScript - -Outside `program()` there is nothing to compare: the script *is* TypeScript, checked by the -TypeScript compiler and run as JavaScript when it is applied, so all of the language and -its standard library is there. The table is about the inside of a program, where what is -written has to become something the game can do. **Same** means it is written and behaves -as in TypeScript. What differs is said. What is missing is refused with a message, never -passed over in silence. - -| TypeScript | In a program | What is different | -| --- | --- | --- | -| Types: annotations, `interface`, `type`, unions of literals, tuples, generics, `as`, `!`, `satisfies` | Same | Erased, as in TypeScript. They also decide things: `u8` / `u16` / `u32` are widths, `fixed` is a number with a fraction, and `Map` and `Map` are kept differently. | -| `let`, `const`, `var` | Same | A variable needs a first value (`let n = 0`). A `const` whose value the script already knows is worked out when the script is applied and costs the map nothing. | -| `number` | Different | A whole number of 32 bits, signed, that wraps at its ends as `x \| 0` does. It holds a fraction once one is stored into it, as a 16.16 fixed-point number. No `NaN`, no `Infinity`, no `bigint`. `/` of whole numbers is whole division towards zero, and dividing by 0 gives 0. `u8`, `u16` (stop at their ends) and `u32` are added. | -| `boolean` | Same | Assigned with `=` only: no `\|\|=`, `&&=`. | -| `string` | Mostly | A value with no length to declare. `length`, `s[i]` and `slice` count characters (code points), where JavaScript counts UTF-16 units; they differ only past U+FFFF. A made text holds 1 023 bytes. `toUpperCase` and `toLowerCase` change A to Z only. `replace` and `split` take a text, not a regular expression. | -| `null`, `undefined` | Mostly | `Unit \| null` and an instance that may be none (`first(…)`, `if (u)`, `u?.kill()`). A number or a boolean may be `undefined` where TypeScript says so (`xs.find(…)`, an optional parameter), and `??`, `!== undefined` and `!` read it. | -| Operators: `+ - * / %`, `**`, comparisons, `&&` `\|\|` `!`, `c ? a : b`, `& \| ^ ~ << >> >>>`, `??`, `?.`, `++` `--`, the compound assignments, the comma | Same | `==` and `===` are one thing, since nothing is coerced. A shift by 32 or more leaves nothing, where JavaScript shifts by the remainder. `**` wants a power known when the script is built. `typeof x === "…"` and `"key" in x` work where the answer is known when the script is built, not as tests in the game. Missing: `delete`. | -| `if`, `while`, `do`, `for`, `for…of`, `switch`, `break`, `continue`, labels | Same | A loop runs all its rounds within one frame of the game, so one that never ends must `sleep()` on every path; `while (true)` left only by a `break` needs the sleep too. A `for` over bounds the script knows is unrolled. No `for…in`. | -| Functions: declarations, parameters by value, defaults, optional and rest parameters, recursion, generics | Same | The compiler inlines a function or calls it; the meaning is the same. One that calls itself cannot `sleep()` and has a depth limit. It returns a number, a boolean, a text, a unit, a record, a tuple, an array or an instance. | -| Arrow functions, closures, functions as values | Mostly | A function may be kept in a variable, an array, a field or a `Map`, returned, and called through what holds it; a closure keeps what it captured. An `async` function or a generator is not kept as a value: call it where it is written, or `start()` it. | -| Object literals | Records | A variable a field: nested, passed by reference, spread, taken apart by patterns. The shape is fixed: no methods or getters on a literal (a class has them). | -| Destructuring and spread | Same | `...rest` takes the rest of an array or of a record. `f(...xs)` wants a length the script knows, as TypeScript itself does. | -| Arrays | Mostly | Fixed or growing, of numbers, booleans, texts, units, records, instances, functions and arrays. There: `push`, `pop`, `shift`, `unshift`, `splice`, `at`, `length`, `fill`, `includes`, `indexOf`, `lastIndexOf`, `forEach`, `map`, `filter`, `reduce`, `some`, `every`, `find`…, `sort`, `reverse`, `slice`, `concat`, `toSorted`, `toReversed`, `join`, `Array.from`. Missing: `flat`; `map` into texts or records. `sort` wants its function. An index past the end reads 0 and stores nothing. `const b = a` copies the array, where JavaScript gives it a second name; a row of rows that grow is a *place* in the outer array, and `filter` of rows copies them. | -| Classes: fields, constructor, parameter properties, methods, `get` / `set`, `static`, `private` / `protected` / `#x`, `readonly`, `extends`, `super`, `abstract`, `implements`, `instanceof`, type parameters | Same | Declared in the program or at the top of any file. An array or a variable that holds several classes, or none, keeps its instances by reference, in a table of 1 023 rows; every other instance is its fields' variables. A function gives back an instance only when every `return` gives the same one. No static blocks. | -| `Map`, `Set` | Mostly | Keys are numbers, units, ids of the game, or texts of a union the script knows (`"red" \| "blue"`). Values are numbers, booleans or instances. Order is JavaScript's, the order the keys went in, except for a table keyed by ids of the game, which goes by id. No chained `set()`. | -| `enum`, `const enum`, `namespace` | Same | Their members are numbers and constants known when the script is built. | -| `try` / `catch` / `finally` / `throw` | Mostly | `throw` takes an `Error`, a `RangeError`, a `TypeError` or an instance of a class that extends one. An error nothing catches is said in the game in red, and the program stops. A caught error is not kept in a variable, an array or a field. | -| `async` / `await`, promises, generators | Mostly | `await` of `sleep()`, `until()`, an async call, a task, `Promise.all`, `race` and `any`; `.then`; `function*`, `yield*`, `for await`. A race's losers stop where they are, where JavaScript lets them run on. No `setTimeout` or `setInterval`: `trigscript/timers` has `after` and `every`. | -| Modules | Same | `import` between the script's files, from `"trigscript"`, from the standard library (`"trigscript/math"`, …) and from [libraries other plugins provide](#libraries-from-other-plugins) (`"waves"`), and of videos, images, sounds and MIDI notes the Explorer imported; nothing from npm. | -| The standard library | Some | `Math.min`, `max`, `abs`, `sqrt`, `hypot`, `sin`, `cos`, `tan`, `atan`, `atan2`, `floor`, `ceil`, `round`, `trunc`, `sign`, `log2`, `pow` by a known power; `String(n)`, `n.toString()`, `parseInt`, `isNaN`, `Array.from`, `new Array(n).fill(v)`. For `console.log` there is `print()`, for `Math.random()` there is `random(n)`. Inside a program `Math.random()`, `Date.now()` and `new Date()` are refused, since they would be worked out once when the map is built; game time is `elapsed()` or `memory.game.frame`. No `JSON`, `Date` or `RegExp`. | -| — | Added | What TypeScript has no word for: `sleep`, `rose` and `once`; reads of the game (`minerals(p)`); `Unit` objects and loops over units; events (`on.died`, …); `stats()` and `memory`; `keyPressed`, `keyHeld`, `clicked`, `mouse`, `chatted`, `local` and `sync()`; `shared()` and per-player programs. | - -The sections below say each of these in full, and [What a program does not -have](#what-a-program-does-not-have) lists what to write instead of what is missing. - -### Triggers - -A script is a TypeScript module. The library is available as globals and as the module -`"trigscript"`, so both of these work: +| [TrigScript](https://docs.scmjs.dev/trigscript/) | A quick start, `trigger()` and its argument words, programs in brief, examples, Simulate and tests, the workspace beside the map, troubleshooting | +| [TrigScript language](https://docs.scmjs.dev/trigscript-language/) | Everything a program can say: numbers and fixed numbers, arrays, maps and sets, functions and closures, classes, errors, tasks, units, events, `stats()` and `memory`, keys, the mouse and chat, text and JSX, sounds, and where it differs from TypeScript | +| [TrigScript library](https://docs.scmjs.dev/trigscript-library/) | The standard library's modules, each with an example, and libraries from other plugins | +| [Coming from eudplib](https://docs.scmjs.dev/trigscript/coming-from-eudplib/) | eudplib's and epScript's ideas mapped to TrigScript's, for map makers who build with euddraft | -```ts -import { trigger, bring, displayText, preserve, units, locations, P1, AllPlayers, CurrentPlayer } from "trigscript"; - -trigger(AllPlayers, [ - bring(CurrentPlayer, units.AnyUnit, locations["Beacon Alpha"], ">=", 1), -], [ - displayText("You found it!"), - preserve(), -]); -``` - -```ts -for (const p of [P1, P2, P3]) { - trigger(p, [deaths(p, units.TerranMarine, ">=", 10)], [setDeaths(p, units.TerranMarine, "set", 0), displayText("Ten lost.")]); -} -``` - -`trigger(players, conditions, actions, options?)` records one trigger: - -- `players` is a player or an array of them. -- `conditions` and `actions` are arrays of what the condition and action functions - return. Nested arrays are flattened, and `false`, `null` and `undefined` entries are - skipped, so a helper can return a list and a condition can be `enabled && bring(…)`. -- `options` are the execution flags by name. `{ preserve: true }` is the same as a - `preserve()` action; `disabled`, `ignoreGameEnd` and the rest are there too. - `disabled(condition)` or `disabled(action)` keeps one entry in the trigger but switched - off, as StarEdit can. - -Every condition and action is a function named after StarEdit's, in camel case: `bring`, -`deaths`, `switchIs` (the one whose real name is a reserved word), `commandTheMostAt`, -`createUnit`, `displayText`, `setDeaths`, `runAiScript`, `wait`, `comment`, `victory`. -Arguments come in the order the classic editor shows them. The enumerated ones take short -words (`">="`, `"<="`, `"=="`; `"set"`, `"add"`, `"subtract"`; `"set"`, `"clear"`, -`"toggle"`, `"randomize"`), and StarEdit's own labels (`"At least"`) work as well. -`displayText(text)` always displays; `displayText(text, false)` follows the player's -subtitle setting. A count is a number or `"All"`. `not(condition)` is the opposite of a -condition where one condition can say it: `not(bring(…, ">=", 1))` is "at most 0", a -switch test flips, and `always()` becomes `never()`. "Not exactly 3" has no single -condition and throws. - -A condition is a *value* here, which the game tests later. So `if (bring(…))` outside a -program tests whether the object exists, and `bring(…) && deaths(…)` is just -`deaths(…)`. TypeScript allows both; the compiler reports them as errors and says where -the test belongs: in a trigger's conditions, or in an `if` inside `program()`. - -Because the script runs, anything TypeScript can do at build time works: a class per -base, a table of waves, `Array.from`, template strings, `Math`, a function that returns -the ten triggers a shop needs. Files import each other with `import { x } from "./name"`. -The library is `import { … } from "trigscript"`, `import * as ts from "trigscript"` for -`ts.trigger(…)`, or the globals; the [standard library](#the-standard-library) is -`"trigscript/"`. Nothing else can be imported. What the script records is what the -map gets, and the order of the `trigger()` calls is the order of the triggers. - -Names come from the map. `identifier()` derives an identifier from each display name -(`Terran Marine` becomes `units.TerranMarine`), and the display name itself works as an -index (`units["Terran Marine"]`), as do custom names the map sets, force names under -`players`, and switch names under `switches`. `P1` … `P12`, `CurrentPlayer` and -`AllPlayers` are constants; the other player groups are under `players`. A plain number -works wherever a name does, which is how you reach EUD players and out-of-range unit ids. -Types the tables do not know can be written as `condition(type, …)` or -`action(type, …)`. `memory(address, comparison, value)` and `setMemory(address, modifier, -value)` are the Deaths condition and the Set Deaths action aimed at an address (EUD). - -### Beside the map - -The editor opens two ways. Triggers ▸ TrigScript… is a full-screen window. *Beside the -map* (an icon right of the tabs, or Triggers ▸ TrigScript beside the map) is a panel over -the map that blocks nothing: drag it by its title, resize it by its corner, and keep -placing units while the code sits next to them. Beside the map: - -- **Ctrl+click** on `locations.Beacon` scrolls the map to the location and flashes it. - Hovering the name says where it is and how big. -- **Pick from map** (the target icon, or the editor's right-click menu): click a location - or a unit on the map, and its name (`locations.Beacon`, `units.TerranMarine`) is typed - at the cursor. From the window, it first moves the editor beside the map. -- When the map renames a location or a switch the script mentions (for a switch, its - custom name), a notification offers to **update the references** in every file. The - references are the compiler's, resolved like the code: `locations["Beacon"]`, an alias - from `import { locations as L }` and `ts.locations.Beacon` follow the rename. A comment, - a string, or a parameter that happens to be called `locations` is left alone. - -### Programs - -```ts -const spawnAt = locations.Spawn; -function burst(n: number) { return [createUnit(P2, units.ZergZergling, n, spawnAt), displayText(`Wave of ${n}`)]; } - -program(() => { - let wave = 0; - let alarm = false; - - function next() { - burst(4); - wave += 1; - } - - while (true) { - if (bring(P1, units.AnyUnit, locations.Beacon, ">=", 1) && !alarm) { - alarm = true; - displayText("They are coming."); - } - if (alarm) next(); - if (wave >= 10 || deaths(P1, units.TerranMarine, ">=", 50)) defeat(); - sleep(seconds(2)); - } -}, { owner: P1 }); -``` - -Everything inside the arrow runs in the game. **A program is a coroutine the game -resumes every frame.** Its body runs from where it left off until it reaches a `sleep()`, -an `await` or its end, all within that frame; the next frame it goes on from there. A body -that ends stops for good, and a `return` at the top of the body ends it there -(`if (!hero) return;`). So `while (true) { …; sleep(frames(1)); }` is a game loop that -runs once a frame, and `sleep(seconds(2))` at its end makes it run every two seconds. A -trigger's `preserve()` does nothing in a program, and the editor says so. `disabled(…)` is -refused: a program leaves out an action it does not want. - -#### Variables and records - -**Variables** hold numbers, booleans, texts (*A text is a value*, below), units of the -game (*Units on the map are objects*, below) and players. A `let p = { lives: 3, gold: 0, -alive: true }` is a **record**: a variable per field (`p.lives -= 1`, `if (p.alive)`), -nested records included. Declared types (`let p: { n: u8 } = { n: 0 }`, an `interface`, a -`type`) are honoured, a record passed to a function reaches it by reference, and -`p = { lives: 3, gold: 0, alive: true }` sets every field at once. Variables live in the -game's memory while the map is played and take nothing from the map: no death counters, -no switches, no triggers in the list. - -A number or a boolean may be `undefined` where TypeScript says it may: what `find` gives -when nothing fits, an optional parameter left out, `let n: number | undefined`. `??`, -`!== undefined` and `!` read it. A player in a variable can be changed and compared; the -player of an action is still one the script knows (see [What is worked out when the script -is applied](#what-is-worked-out-when-the-script-is-applied)). - -```ts -program(() => { - const hp = [30, 80, 55]; - const strong = hp.find((h) => h > 50); // a number, or undefined - if (strong !== undefined) print(`${strong}`); // 80 - let leader: Player = P1; - if (hp[0] < 50) leader = P2; - if (leader == P2) print("Player 2 leads"); // Player 2 leads -}); -``` - -#### Arrays - -**Arrays** hold numbers, booleans, texts, units, records, instances of a class, functions, -or other arrays; each kind has its paragraph below. `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`, `hp[i + 1]++`). `.length`, `for (const x of xs)` with -`break` and `continue` (`x` is a copy of the cell, as in TypeScript), `fill(v, start?, -end?)`, `includes(v)`, `indexOf(v)` and `lastIndexOf(v)` (−1 when there is none), -`at(i)` (from the end when `i` is below zero) and `join(", ")` all run within the frame. A -`const` array the body stores into is an array of the program like a `let` one. Cells keep -their type: a `u8[]` stops at both ends, a `number[]` goes below zero. An array handed to a -function is the same array. `const b = a` is a copy, where JavaScript would give the same -array a second name. In a program of every player each player has one; `shared([0, 0, 0])` -is one for everybody. - -A constant index past the end is a compile error. A variable index past the end reads 0 -and stores nothing, in the game without a word. Simulate says where it happened, since it -is always a mistake. - -**An array that something pushes to grows.** `const queue: number[] = []; queue.push(x)`, -`queue.pop()` (`?? 0` or `!`, since TypeScript has a pop of an empty array undefined: here it -is 0), `queue.length`, `queue.length = 0`, `queue[queue.length] = x`, also when the push is -in a function the array was handed to. `shift()`, `unshift(v)` and `splice(i, n, …)` take -values off the front, put them there, and cut or fill the middle. There is no size to -declare. The cells come from one pool the map's programs share, 16 384 of them unless -**Settings** says otherwise: a block that is full is exchanged for 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 (the block after 4 096 cells is 8 192). 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. `let xs = []` wants its type -said (`let xs: number[] = []`). - -```ts -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}`); - } -}); -``` - -```ts -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 -}); -``` - -**An array of records** is an array a field, all growing together: `let waves = [{ count: 4, -delay: 2 }]`, `waves[i].count += 1`, `waves.push({ count: 6, delay: 1 })`, `waves.pop()`, -`waves[i] = { … }`. A record taken out of one (`const w = waves[i]`, the variable of -`for (const w of waves)`, 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, and `w` stays the -record it was when `i` moves on. A field is a number or a boolean (with its declared -width), a unit, a text, an array of numbers, booleans or units, or a record of the same: -`let squads = [{ id: 1, name: "red", leader: first({ owner: P1 }), seen: [] as number[], -at: { x: 0, y: 0 } }]`, `squads[i].seen.push(4)`, `squads[i].name += "!"`. An array in a row -is always one that grows, and the row owns it, as it owns a text that was made. `pop()`, -`length =`, `squads[i] = { … }` and declaring the array again give the blocks back, and -`filter` gives its rows arrays and texts of their own. The line's hint 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()`, `for (const u of squad)`, -`squad.length = 0`. It may be kept across a `sleep()`, which a loop over the game's units may -not. A unit of it that has died since reads 0, takes no order, and is false in an `if`, as -any kept unit is. - -**The methods that take a function** work on all three kinds of array, and the function is -written as it is in TypeScript: `hp.forEach((h, i) => { total += h * i; })`, -`hp.some((h) => h > 20)`, `every`, `findIndex` and `findLastIndex` (−1 when nothing is -found), `hp.reduce((sum, h) => sum + h, 0)`, `hp.map((h) => h * 2)`, -`hp.filter((h) => h > 7)`, `hp.sort((a, b) => a - b)` and `hp.reverse()`, and chains of -them: `cells.filter((c) => c < 20).map((c) => c + 1).reduce((s, c) => s + c, 0)`. The -function is handed the item, its place and the array, as JavaScript hands them. An arrow -written there is copied into the loop the method becomes, so a variable it uses from -outside is that variable, and it costs nothing to write one. A function kept in a variable -works too (`hp.forEach(report)`); see [Functions as values](#functions-as-values). - -```ts -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 -}); -``` - -- `map` makes an array of numbers or of booleans: of the same fixed length when what it runs - over is fixed, one that grows when that grows. `filter` always makes one that grows, of - whatever it ran over: rows of an array of records stay whole, units stay units. Both are - made again each time the line runs and give their block back first, as any declaration - does. A chain makes the arrays in the middle as written. Neither can stand in the - condition of a loop, which is worked out again every turn: make the array before the - loop, or inside it. -- `find` and `findLast` give a unit or none from an array of units, and a number or - `undefined` from an array of numbers: `hp.find((h) => h > 20) ?? -1`. Of records, take - the place with `findIndex` and read `waves[i]`. -- `reduce` wants what it starts from; without it JavaScript throws on an empty array. -- `sort` wants its function: without one JavaScript sorts numbers as text, 10 before 9. It - sorts where the array stands, within the frame, keeping equals in the order they were in, - and gives the same array back. A list nearly in order costs a pass; one in no order costs - its length squared. Dozens of items are nothing; hundreds every frame will be felt, and - the end of the line says *sorts in the frame*. Records are sorted row by row - (`waves.sort((a, b) => a.delay - b.delay)`), units too (`squad.sort((a, b) => a.hp - b.hp)`). -- No `sleep()` inside such a function: the method is one loop within the frame. A `for…of` - over the same array can sleep between its turns. - -The units of the game take them as well: `unitsOf(P1).forEach((u) => u.heal(10))`, `some`, -`every`, `reduce`, `map`, `find` (a unit or null) and `filter`, which is how to keep them: -`const weak = unitsOf(P1, { type: units.TerranMarine }).filter((u) => u.hp < 20)` is an -array of units, which can be sorted and kept across a `sleep()`. A loop over the game's -units has no places, so there is no `sort` until they are in an array. - -```ts -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 too, the loop written out turn by turn as `for…of` over -one is: `waves.forEach((w) => createUnit(P2, w.unit, w.count, at))` is one Create Unit a -wave, and `prices.map((p) => p + bonus)` with `bonus` a variable is an array of the program. -With nothing of the program in it (`[1, 2, 3].map((x) => x * 2)`), it is still the script's -own arithmetic, done when the script is built. - -`new Array(12).fill(0)` is an array of numbers; for booleans, `new Array(12).fill(false)`. - -**Patterns and spread** are TypeScript's. `const { x, y } = mouse(P1)`, -`const { count, delay: wait } = waves[i]`, `const [first, , third = 0, ...tail] = xs`, nested -as deep as they are written, in a declaration, in the variable of a `for…of` -(`for (const { count, delay } of waves)`), and in a parameter of a function -(`function len({ x, y }: Point)`) or of one given to a method -(`waves.forEach(({ count }) => …)`). A name in a pattern is a variable of its own holding a -copy, as a number is copied in JavaScript: changing `x` does not change `p.x`. A record or -an array *inside* what is taken apart stays itself, as an object does: `const [a, b] = waves` -are two rows of `waves`. A default is for what is not there (a field the record has not -got, a place past a fixed array's end); a name with neither is an error, since it would be -undefined. `...tail` is an array of its own, which grows if what it was taken from does. - -Assigned, a pattern is a swap: `[a, b] = [b, a]`, `[hp[i], hp[j]] = [hp[j], hp[i]]`, -`({ x: q.x, y: q.y } = p)`; every value is read before any is stored. `[...xs, v, ...ys]` -copies the cells into a new array (fixed when all of them are, else one that grows), and -`{ ...p, y: 9 }` the fields into a new record or a row: `waves.push({ ...w, count: 9 })`. -`function sum(...ns: number[])` takes the rest of a call's arguments as an array made at -that call; such a function is copied into each call rather than called, since every call -has its own number of them. An array is handed to a function as itself (`total(xs)`), not -spread into its arguments. - -```ts -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, and inside records.** `let grid = [[0, 0, 0], [0, 0, 0]]`, -`new Array(8).fill(0).map(() => new Array(8).fill(0))`, `Array.from({ length: h }, () => …)`, -three deep if you like: `grid[y][x]`, `grid[y][x] += 1`, `grid.length`, `grid[y].length`, -`for (const row of grid)`, `grid.forEach((row, y) => …)`, `some`, `every`, `findIndex`, -`reduce`, `map`. A row is an array like any other: `const row = grid[y]` is the grid's own -cells, as it is in TypeScript, and takes `fill`, `sort`, `includes`, a function's parameter, -a pattern (`const [top, bottom] = grid`); `grid[y] = [a, b, c]` sets one. So does an array -in a record: `let p = { hp: 5, path: [0, 0, 0], seen: [] as number[] }`, `p.path[i]`, -`p.seen.push(v)`. - -How it is kept is the compiler's choice, and the end of the line says which: - -- **Every row the same length, and no row growing**, is *one flat array*, read at - `y * width + x`, no dearer than an array. A row past its own end reads 0 and stores - nothing, so it never reaches into the next row. The outer array may still grow by whole - rows: `const path: number[][] = []; path.push([x, y]); path.pop(); path.length = 0`. -- **Rows of different lengths, or a row something pushes to** (`buckets[i].push(v)`), is - *rows that grow*: each row a block of the heap of its own, found through the outer array, - a few more triggers a read than a flat one. The outer array grows too - (`buckets.push([1, 2], [])`, `buckets.pop()`, `buckets.length = 3`), a row can be given - anew (`buckets[i] = [a, b]`, or an array, whose cells are copied), and a row that goes - (popped, cut off, the whole declared again in a loop) gives its block back first, so - nothing leaks. Two 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. `const last = buckets[2]; buckets.pop(); -buckets.push([7])` leaves `last` reading `[7]`, where JavaScript would still have the row -that was popped. `sort`, `reverse` and `filter` work on an array of arrays, of either kind -(`points.sort((a, b) => a[0] - b[0])`, `buckets.filter((r) => r.length > 2)`), and `filter` -gives rows of their own, as it does of records: what is changed through the rows it made is -not changed in the rows it took them from, which is the one way it is not JavaScript's. - -**Copies.** `xs.slice(1)`, `xs.slice(-2)`, `xs.concat(ys, 7, [a, b])`, `xs.toSorted(f)`, -`xs.toReversed()` and `Array.from(xs)` give a new array that grows, of numbers, booleans or -units, from an array of the program or a list the script has; `[...m.keys()]`, -`Array.from(m.values())` and `[...set]` give a `Map`'s or a `Set`'s as one. `f(...xs)` spreads -an array into a call's arguments when the script knows how many there are: a list it has, or -an array TypeScript knows as a tuple (`let box: [number, number, number] = [1, 1, 1]`), which -is what TypeScript itself asks of a spread argument. An array that grows is handed over as -itself. - -**An array of texts** a program fills is `const names: string[] = []`: `push`, `pop()` (its -value too: `names.pop() ?? ""`), `names[i]` read, written and added to, `length` and `length -=`, `for…of`, `forEach`, `includes`, `indexOf`, `join`. Each text is what a text in a row -is, and the array gives a text's block back when it is replaced, popped or cut off. In a -variable, a record's field or a class's, or handed to a function; not inside a row of an -array of records, where a text is a field of its own. - -```ts -program(() => { - const log: string[] = []; - let wave = 3; - log.push(`wave ${wave} began`); - log.push("boss down"); - print(log.join(" / ")); // wave 3 began / boss down -}); -``` - -#### Tables, a `Map` and a `Set` - -**A list the script made is a table a program can look things up in.** `const price = [50, -100, 150]` outside the program, `price[level]` inside it, is in the map once, however often -it is read, and cannot be written. A list of records is a table a field, such as the wave table: -`const waves = [{ unit: units.ZergZergling, count: 4 }, …]` above the program, -`createUnit(P2, waves[wave].unit, waves[wave].count, at)` inside it. - -**Tables keyed by an id of the game** (a unit type, a player, a location, a switch, a weapon, -an upgrade, a technology) are arrays with a cell for every id, so a key of the game is one -read: - -```ts -const bounty: Record = { [units.ZergZergling]: 5, [units.ZergHydralisk]: 15 }; - -program(() => { - const score: Record = { [P1]: 0 }; - const lost = new Map(); - const seen = new Set(); - for (const u of unitsOf(P2)) { - if (u.hp < 10) { - score[u.owner] += bounty[u.type]; - lost.set(u.type, (lost.get(u.type) ?? 0) + 1); - seen.add(u.type); - u.kill(); - } - } -}); -``` - -A `Record` is read and written by its keys and reads 0 where nothing was stored. A `Map` -has `get` (`?? d` for a key never set), `set`, `has`, `delete`, `clear` and `size`; a `Set` -has `add`, `has`, `delete`, `clear` and `size`. `for (const [key, value] of lost)`, -`for (const key of seen)`, `lost.keys()` and `lost.values()` go through the keys that are -there, in the order of the ids. Every id is looked at, so the editor marks the line with -how many. One made inside the program is the program's, to be written while the map is -played; one made outside it is the script's, and a program only looks things up in it. -`CurrentPlayer` is not a key: in a program of every player a plain variable is already one -per player. A `Map` keyed by a few texts the script knows, `new Map<"red" | "blue", -number>()`, is kept the same way. - -**A `Map` and a `Set` over any number** are for keys that are no id of the game: a place -packed into one number, a unit's id of your own, a score. `new Map()`, -`new Map()`, `new Set()`, empty or with what they start with -(`new Map([[1, 10], [k, 20]])`), in a variable, a record's field or a class's. - -```ts -program(() => { - const owner = new Map(); // a tile, as x + y * 256, to who holds it - const claimed = new Set(); - for (const u of unitsOf(P1, { type: units.TerranMarine })) { - const tile = (u.x >> 5) + (u.y >> 5) * 256; - if (!claimed.has(tile)) { claimed.add(tile); owner.set(tile, 1); } - } - for (const [tile, who] of owner) if (who == 1) print(`tile ${tile % 256}, ${tile >> 8}`); -}); -``` - -`get` (`?? d` for a key it has not got; without it such a key reads 0 or false), `set`, -`has`, `delete` (true when the key was there), `clear`, `size`, `forEach`, and `for…of` over -the table, its `keys()`, `values()` or `entries()`. **They go through their keys in the -order the keys went in, as JavaScript does**: a key set again stays where it was, one -deleted and set again goes to the end, what the loop's body adds is reached and what it -deletes is not. The tests run the same lines in JavaScript and compare. - -What it costs: a key is *looked for*, a few steps to find, set or delete one, where a table -keyed by ids of the game is one read. So where the keys are ids of the game, say so -(`Map`). It also lives in the memory the programs' arrays share: it -starts at eight slots and doubles as it fills, and deleted entries go when it is next made -again. The line's hint says which kind a `Map` became. Values are numbers, booleans or -instances of a class (kept by reference; see [Classes](#classes)). For a text or a record, -keep the place of a row of an array of records. Left out: a text for a key, other than -the few texts of a union, and `m.set(…).set(…)` in a chain. - -**A `Map` or a `Set` keyed by units** is the same table: `const cooldown = new Map()`, `cooldown.set(u, 72)`, `(cooldown.get(u) ?? 0)`, `new Set()` for the units -already dealt with. A unit is found by where it is together with the byte that tells one -unit of that place from the next, so a unit made where a dead one was is *not* the dead -one's key, and reads as absent. A unit that died stays an entry until it is deleted; in a -loop (`for (const [u, n] of cooldown)`) it reads as no unit, which is where to delete it. -`Map` is how TypeScript lets `first(…)` be a key without an `if`. - -#### Numbers and arithmetic - -**Numbers** are whole unless a fraction is stored into them ([Numbers with a -fraction](#numbers-with-a-fraction), below). A whole `number` is what it is in TypeScript -as far as 32 bits go: signed, from −2 147 483 648 to 2 147 483 647. `a - b` is below zero -when `b` is larger, `while (i >= 0) { …; i--; }` ends, `-x`, `Math.abs`, `Math.min`, -`Math.max` and `clamp(x, lo, hi)` mean what they say, and a number below zero is printed -with its minus sign. The one difference from TypeScript is at the ends: a `number` wraps -there, as `x | 0` does (2 147 483 647 + 1 is −2 147 483 648), where TypeScript's would go -on. - -Beside it there are three types for a number that is never below zero: - -| Type | Holds | At its ends | -| --- | --- | --- | -| `number` | −2 147 483 648 … 2 147 483 647 | wraps, as `x \| 0` | -| `u32` | 0 … 4 294 967 295 | wraps, as `x >>> 0` | -| `u16` | 0 … 65 535 | stops: below zero is 0, above is 65 535 | -| `u8` | 0 … 255 | stops: below zero is 0, above is 255 | - -`let lives: u8 = 3` stops at its ends *after* the whole sum, never between its parts. A -`u32` is for bit masks, hashes and a count past two thousand million; everything the game -is asked (`minerals()`, `deaths()`, `u.hp`) is a `number`. A constant its variable cannot -hold is a compile error (`let mask = 0xffffffff` wants `let mask: u32`). - -**A `number` and a `u32` do not mix in arithmetic** without saying which is meant: `h + n` -is an error that names the two ways out, `u32(n)` and `i32(h)`, which read the same 32 -bits the other way and cost nothing (`n >>> 0` says `u32(n)` too, as in JavaScript). A -whole-number constant that fits is either, so `h * 31 + 7` needs nothing. A *comparison* -between the two is no error and is exact: a number below zero is smaller than any `u32`. -Storing one into a variable of the other keeps the bits. - -**Where the game takes nothing below zero** (a `u8` or `u16`, a unit's hit points, an -action's amount or count, a cell of `stats()`, `random(n)`'s bound, a place on the map), a -number below zero goes in as 0: `u.hp -= 1000` kills, and `createUnit(p, unit, n, at)` with -`n` at −3 makes none. - -**Arithmetic**: `+ − * / %` between variables and constants, `*=` `/=` `%=`, `++` `--`, -`Math.min`, `Math.max`, `Math.abs`, `clamp(x, lo, hi)`, and the bitwise `& | ^ ~ << >> >>>` -(with `&=` and the rest) over the 32 bits. `>>` keeps the sign of a `number` and `>>>` -never does, as in JavaScript; a shift by 32 or more leaves nothing but that sign, where -JavaScript would shift by the remainder. `/` of two whole numbers is whole division towards -zero (`-7 / 2` is −3, as `Math.trunc(-7 / 2)`), and `%` takes the sign of what is divided -(`-7 % 2` is −1), as in JavaScript. `Math.floor`, `Math.trunc`, `Math.round` and -`Math.ceil` around a whole division change nothing. `*` wraps, and dividing by a variable -that is 0 in the game gives 0, as `(a / 0) | 0` does. Dividing by a constant 0 is a compile -error. `a ** 2` and `Math.pow(a, 3)` take a power known when the script is built; -`Math.sign(n)` and `Math.log2(n)` (rounded down) work on whole numbers. - -#### Numbers with a fraction - -**A number holds a fraction once one is stored into it**: `let speed = 1.5`, `x += 0.25`, -`const d = Math.cos(a)`. It is then a `fixed` number, 16.16 bits: about four decimal places, -from about −32 768 to 32 767. `fixed(n)` makes a whole number one, and a parameter or a -variable typed `fixed` is one from the start. `u8`, `u16` and `u32` stay whole; a fraction -stored into one is an error. - -- Whole numbers still divide as whole numbers: `7 / 2` is 3, and `7 / fixed(2)` is 3.5. -- Where only a whole number goes (an index, a count, a place, a shift, a unit's field), a - fixed number is an error that names `Math.floor`, `Math.round` and `Math.trunc`. `x | 0` - is `Math.trunc(x)`. -- `Math.sqrt`, `Math.hypot`, `Math.sin`, `Math.cos`, `Math.tan`, `Math.atan` and - `Math.atan2` give fixed numbers. Angles are radians, as in JavaScript. `sin`, `cos` and - `atan2` read tables the build adds to the map, within 1/65 536 of JavaScript's. -- A fixed number in a text is printed as a decimal of up to four places, without trailing - zeros. - -```ts -program(() => { - let speed = 1.5; // a fraction stored into it: a fixed number - speed += 0.25; - const across = Math.hypot(30, 40); - const half = 7 / fixed(2); // 7 / 2 alone is 3, as whole numbers divide - print(`${speed} ${across} ${half} ${Math.floor(speed * 10)}`); // 1.75 50 3.5 17 -}); -``` - -`trigscript/math` adds `lerp`, easing curves, `distance`, `angleTo` and a `Vec` of two -fixed numbers ([The standard library](#the-standard-library)). - -#### Control flow and loops - -`if`/`else`, `while`, `do`, `for`, `switch`, `break`, `continue`, labels and `c ? a : b` -all work. `switch (x)` over a number or a text tests its cases in order and falls through -without `break` as TypeScript does; the case values are known when the script is applied. -`&&`, `||` and `!` short-circuit as in TypeScript: `n >= 1 && once(…)` consumes the edge -only once `n` is 1. Conditions of the game go where a boolean goes: -`if (bring(P1, units.AnyUnit, locations.Beacon, ">=", 1) && !alarm)`. `random()` is a coin -toss, and `random(n)` a whole number from 0 to n − 1 (`n` may be a variable; 0 gives 0). -The game's own randomness seeds them, so two games differ. - -**Loops run to completion within the frame.** `while (i < 10) { …; i++; }` does all ten -rounds at once, as the source says. The consequence: a loop that never ends and never -sleeps would never give the frame back, and the game would freeze. The compiler refuses -such a loop and says where to put a `sleep(frames(1))`. It looks for a loop with no -`sleep()` on some path around it whose condition never mentions a variable the body -changes (or, for a condition or a read of the game, whose body takes no action that could -change it). The simulator has a guard of its own for what the check cannot see. - -A `for` whose start, bound and step are known when the script is applied -(`for (let i = 0; i < 3; i++)`, counting down, stepping by two) is **unrolled**: the body -is compiled once per value, with `i` a value and not a variable, so -`createUnit(P2, unit, i + 1, at)` is an ordinary action, and `break` and `continue` work. -The editor says so at the end of the line: *unrolled ×3*. A `for` over a variable bound, -or one that assigns its variable in the body, is a loop in the game. An unroll of more -than 256 iterations is an error that says how to write it as a loop instead. - -A label on a loop lets `break` and `continue` reach past the loop they are in: - -```ts -program(() => { - const hp = [40, 0, 25, 0, 60, 10]; - let row = 0; - let found = -1; - rows: while (row < 2) { - let col = 0; - while (col < 3) { - if (hp[row * 3 + col] > 50) { found = row * 3 + col; break rows; } - col += 1; - } - row += 1; - } - print(`${found}`); // 4 -}); -``` - -**Lists are unrolled.** `for (const w of waves)` over a list known when the script is -applied compiles the body once per element, with `w` bound to that element, so a wave -table is ordinary data: - -```ts -const waves = [{ unit: units.ZergZergling, n: 6 }, { unit: units.ZergHydralisk, n: 4 }]; -program(() => { - for (const w of waves) { - createUnit(P2, w.unit, w.n, locations.Spawn); - sleep(seconds(20)); - } - victory(); -}); -``` - -#### Functions - -**Functions.** Arguments pass by value, as in TypeScript: `function bump(x: number) { x++; }` -leaves the caller's variable alone. A function may return a number, a boolean, a text, a -unit, a record, a tuple or an array: `function canAfford(price: number) { return gold >= -price; }`, ``function label(n: number) { return `Wave ${n}`; }``, `return { big, half }`, -`return [lo, hi] as const`. It may return an instance of a class under the rule in -*Classes*, below. Parameters take defaults (`function f(x: number, step = 1)`) and may be -optional (`extra?: number`, `undefined` when the call leaves it out). Locals start afresh -at every call. A function declared inside another sees the outer one's parameters and -locals. - -```ts -function split(n: number) { - return { big: n > 4, half: n >> 1 }; -} -function around(n: number) { - return [n - 1, n + 1] as const; -} -function advance(n: number, extra?: number) { - return extra !== undefined ? n + extra : n + 1; -} - -program(() => { - let n = 7; - const { big, half } = split(n); - const [lo, hi] = around(n); - print(`${big} ${half} ${lo}-${hi} ${advance(n)} ${advance(n, 10)}`); // true 3 6-8 8 17 -}); -``` - -A function used once is **inlined**: its body is written where the call stands. A -parameter given a value known when the script is built is that value, and one given a -variable the function never assigns reads that variable directly. A function used a second -time is **called** instead when it can be: one copy of its body in the built map, its -parameters variables that every call sets. The source means the same either way; calling -keeps the map smaller, since ten calls of a function are one body, not ten. The end of the -function's line says which it got: *called ×3*, or *inlined ×3* with the reason when you -hover it. Three things keep a function inlined: - -- It **sleeps**: the program wakes up inside it, which only an inlined body can do. -- A parameter reaches something only a value known when the script is built can fill. - `function pay(p: Player, n: number) { setResources(p, "add", n, "ore"); }` needs its - player when the map is built, so `pay(P2, 4)` stays inlined (the amount could be a - variable; the player cannot). The same goes for an argument that is a list only the - script has. A text written in the script is not such a list: the parameter is a text - variable, set at each call. -- It uses `rose()` or `once()`, which remember what they saw at each place they are - written. - -An array reaches a function as itself (what the function stores, the caller sees), and -which array is settled when the script is built. So a function that takes an array is one -copy *for each array it is passed*: `total(hp)` and `total(shields)` are two copies, five -calls of `total(hp)` one. The hint counts them: *called ×5, 2 copies*. - -**A function may call itself**, directly or through another: `fib(n - 1) + fib(n - 2)`, a -flood fill over an array, two functions that call each other. Such a function is always a -called one, from the call inside itself on, whether or not anything else calls it twice, -and its line says *calls itself*. It means what it means in TypeScript: each run has its -own parameters and locals, an array declared in it is that run's own, and `c ? f(x) : 0` or -`ok && f(x)` makes the call only when TypeScript would. Three things show that it is a map -and not a JavaScript engine: - -- **No `sleep()` or `await` in it.** A function that waits is inlined, and one that calls - itself cannot be; the error says so. -- **A depth limit**: 1 024 calls deep unless the panel's **Settings** says otherwise (16 to - 65 536, kept in the map). A call past it is a `RangeError` that a `catch` can take, as in - JavaScript, when the program throws or catches anything. Otherwise it stops the program - for good, and the game says where: *stack overflow in fill, line 12*. **Simulate** stops - at the same call and lists it with the other faults. A function with no way out at all - (it calls itself on every path) is an error when the script is built. -- **It costs what it keeps.** A function's variables are single cells of the map, so around - each call that may come back they are put on a stack and taken back after. That is a few - dozen triggers a variable a call: nothing for a flood fill or a walk a thousand deep, a - visible pause for tens of thousands of calls within one frame. The fewer variables the - function has, the less a call costs. The stack is only in the built map when some - function calls itself (Settings says how large it comes to for the script as it stands), - and a function that does not call itself costs what it always did. - -A flood fill over a grid, which is both of the last two sections at work: - -```ts -program(() => { - const grid = [ - [0, 0, 1, 0], - [0, 1, 1, 0], - [0, 0, 0, 0], - ]; - function fill(x: number, y: number): number { - if (x < 0 || y < 0 || y >= grid.length || x >= grid[0].length) return 0; - if (grid[y][x] != 0) return 0; - grid[y][x] = 2; - return 1 + fill(x + 1, y) + fill(x - 1, y) + fill(x, y + 1) + fill(x, y - 1); - } - let x = 0; - print(`${fill(x, 0)} cells filled`); // 9 cells filled -}); -``` - -A loop over units (`for (const u of unitsOf(…))`) cannot hold such a call: collect what it -finds into an array, and recurse from a loop over that. - -**Functions the game runs can live in any file.** A function declared at the top of any of -the script's files runs in the game when a program calls it with values of the program. -`game()` marks one that only the game runs: - -```ts -// shop.ts -export const award = game((p: Player, n: number) => { setResources(p, "add", n, "ore"); }); -export const canAfford = game((have: number, price: number) => have >= price); - -// main.ts -import { award, canAfford } from "./shop"; -program(() => { - let gold: u8 = 10; - if (canAfford(gold, 5)) { gold -= 5; award(P2, 3); } -}); -``` - -A `game()` function follows the program's rules (inlined where it is used once, called -where it is used more, as above), and what it does is attributed to its own file and line. -It sees its parameters and what any file sees when the script is applied, not the calling -program's variables. Calling one outside a program is an error: it runs in the game, not -when the script is applied. - -#### Functions as values - -**A function can be kept**: in a variable, an array, a record's or an instance's field, or a -`Map`, and returned by another function. A call through what holds it calls whichever -function is there when the line runs. A closure keeps what it captured, as in JavaScript: -`counter()` below gives a function with its own count. A function given where it is used -(an arrow handed to a method or a parameter) costs nothing to write: it is copied in where -it is called. A kept one is a number in the map, and a call through it chooses among the -functions that could be there, one test each. - -```ts -function heal(hp: number) { return hp + 10; } -function hurt(hp: number) { return hp - 10; } -function counter() { - let n = 0; - return () => { n += 1; return n; }; -} - -program(() => { - const effects = [heal, hurt, (hp: number) => hp * 2]; - const next = counter(); - let hp = 40; - let i = 0; - while (i < effects.length) { hp = effects[i](hp); i = next(); } - print(`${hp}`); // 80 -}); -``` - -An `async` function or a generator is not kept as a value: call it where it is written, or -`start()` it ([Waiting and tasks](#waiting-and-tasks)). `(() => { … })()` runs where it is -written. - -#### Classes - -**Classes** are TypeScript's, declared inside the program or at the top of any of the -script's files. A class declared outside a program is still an ordinary class when the -script is applied, and a program's own wherever a program makes an instance of it. A -method is a function that is handed the instance, so everything above about functions -holds for methods: one used once is inlined, one used more is called, one may call itself, -and one that sleeps is inlined. - -```ts -program(() => { - class Wave { - static made = 0; - left: number; - constructor(public unit: UnitType, public count: number) { this.left = count; Wave.made++; } - spawn(at: Location) { if (this.left > 0) { createUnit(P2, this.unit, 1, at); this.left--; } } - get done() { return this.left == 0; } - } - class Boss extends Wave { - constructor(unit: UnitType) { super(unit, 1); } - spawn(at: Location) { super.spawn(at); displayText("The boss is here"); } - } - const waves = [new Wave(units.ZergZergling, 12), new Wave(units.ZergHydralisk, 6)]; - const boss = new Boss(units.ZergUltralisk); - while (!waves.every((w) => w.done)) { - for (const w of waves) w.spawn(locations.Gate); - sleep(seconds(1)); - } - boss.spawn(locations.Gate); -}); -``` - -Fields with their first values, a constructor, `constructor(public x: number)`, methods, -`get` and `set`, `static` fields and methods, `readonly`, `private`, `protected` and `#x`, -`extends` with `super(…)` and `super.method()`, `abstract`, `implements`, type parameters. -A field holds what a record's does: a number, a boolean, a unit, a text, an array -(`members: Unit[] = []`), a record, another instance (`pos = new Vec(0, 0)`). -`this.members = []` starts the array over. - -**An instance is kept one of two ways**, and the compiler chooses: - -- **As its fields' variables**, when the build knows which instance each name holds. Then - nothing of the class is left when the map is played: an overridden method is found by - what the instance is, `x instanceof Boss` is answered by the compiler, and the branch - that is false is not built. This costs nothing beyond the fields. -- **By reference**, when the program needs it: an array or a variable that may hold - several classes, or none (`let target: Enemy | null`); a variable given another instance; - an instance kept in a variable and pushed to an array; a `Map` or a `Set` of instances. - Those instances are rows of one table of 1 023 rows. A method called through a reference - asks the row's class when classes give it different bodies, and `instanceof` is the same - question. A field read through none is 0, and a write through none does nothing (where - JavaScript would throw). A row nothing holds any more is given back at the next wait; - rows that hold each other in a ring are never given back. - -```ts -class Enemy { - constructor(public hp: number) {} - worth() { return 1; } -} -class Boss extends Enemy { - worth() { return 10; } -} - -program(() => { - const foes: Enemy[] = [new Enemy(5), new Boss(50), new Enemy(5)]; - let target: Enemy | null = null; - let score = 0; - for (const f of foes) { - score += f.worth(); - if (!target || f.hp > target.hp) target = f; - } - print(`${score} ${target?.hp ?? 0}`); // 12 50 -}); -``` - -The limits: - -- **A function may give an instance when every `return` gives the same one**: `return - this` (so `v.add(w).scale(2)` chains, the calls running in the order written), - `return new Wave(n, 10)` (a function that makes one: `const w = make(3)`), or one it was - handed. `pick(a, b)` that returns one or the other is refused. For that, keep the - instances in an array and return the place. -- **No static blocks**: give the static fields their values where they are declared. - -#### Errors - -**`try`, `catch`, `finally` and `throw`** work as in TypeScript. `throw` takes a -`new Error(…)`, a `RangeError`, a `TypeError`, an instance of a class that extends one of -them, or what a `catch` took (`throw e`). Anything else is refused at the `throw`. A class -of error has a `message`, a `name` and fields of its own (numbers, booleans, units, texts), -and `e instanceof OutOfGold` tells the classes apart. `e.stack` is the error's class, -message and the line it was thrown at. - -```ts -class OutOfGold extends Error { - constructor(public need: number) { super(`need ${need} more`); } -} - -program(() => { - let gold = 30; - function buy(cost: number) { - if (cost > gold) throw new OutOfGold(cost - gold); - gold -= cost; - } - try { - buy(20); - buy(50); - } catch (e) { - if (e instanceof OutOfGold) print(`${e.message} (${e.need})`); // need 40 more (40) - else throw e; - } finally { - print(`${gold} left`); // 10 left - } -}); -``` - -- An error goes up through functions, including one that calls itself, to the nearest - `catch`. `finally` runs however the `try` is left: a `return`, a `break`, or an error on - its way out. -- An error nothing catches is said in the game in red, with its line ("TrigScript: - uncaught OutOfGold: need 20 more, line 12."), and Simulate lists it at the `throw`. The - program or task stops there. A handler of an [event](#events) gives up that one turn and - goes on listening. -- A `try` may span `sleep` and `await`. An error in one branch of `Promise.all` or - `Promise.race` leaves the statement, and the other branches stop where they are. -- A stack that runs out is a `RangeError`, "Maximum call stack size exceeded", which a - `catch` takes. -- A caught error is not kept in a variable, an array or a field, and `new Error(m, { cause - })` is refused: put what you need in a field of your own class. -- An index past an array's end and a full heap stay what they were: the game passes over - them, and Simulate and the tests say where. - -#### What is worked out when the script is applied - -**A `const` is what it can be.** `const limit = waves.length` is computed when the script -is applied and inlined; `const next = wave + 1` needs a variable of the program, so it is -one, which TypeScript keeps you from reassigning. A constant is computed when the -compiler reaches its declaration, or earlier if something needs it, and one inside a -branch that is never compiled is never computed at all. - -**What the body reads from outside, with nothing of the program in it, is computed when -the script is applied.** A constant, a helper called with known values, a condition, an -action: each is evaluated once, when the compiler reaches it, and the compiler sees its -value where the expression stood. The editor underlines those parts with dots, so the -boundary is visible as you type, and hovering a variable says what it is. `if (false) …` -and `while (false) …` are pruned, and nothing inside them is evaluated. A helper that -throws is reported at the expression that called it, with a note that it ran when the -script was applied. That is what makes `burst(4)` above work: the helper is ordinary -TypeScript, it returns two actions, and the program runs them. A helper called with a -variable of the program runs in the game instead ([Functions](#functions)), and then it -returns a value, not actions. - -**The one rule to keep in mind**: the player and the location of an action, and the amount -a condition compares with, are known when the script is applied. A program variable -cannot reach them. A parameter of a function that was given a known value does reach them -(such a function is inlined, the parameter being that value), so -`function spawn(p: Player, n: number) { createUnit(p, units.Zergling, n, spawnAt); }` -works with `spawn(P2, 4)`. - -**These parts of an action can follow a variable**: the amount of `setResources`, -`setDeaths`, `setScore` and `setCountdownTimer` (`setResources(P2, "set", wave * 10 + 5, -"gas")`); the count of `createUnit`, `killUnitAt`, `removeUnitAt` and `giveUnits` (that -many units, 0 being none); an action's unit type; and the text of `displayText`, `print`, -the objectives, a leaderboard's label, a transmission and a unit type's name. To compare a -*condition's* amount with a variable, read the value and compare it yourself, as below. - -#### Reading the game - -**Reads: every quantity a condition compares is also a value.** Leave the comparison and -the amount out of the call and it is a read, a number like any other: - -```ts -let price = 50; -if (deaths(P1, units.TerranMarine, ">=", 10)) print("ten lost"); // a condition, as ever -let lost = deaths(P1, units.TerranMarine); // a read -if (minerals(CurrentPlayer) > price * 2) price += lost; // compared with a variable -setResources(P2, "set", minerals(P1) / 2, "ore"); // as an action's amount -let here = bring(P2, units.ZergZergling, locations.Pen); -``` - -That works for `deaths`, `kill`, `bring`, `command`, `accumulate`, `score`, `opponents`, -`countdownTimer` and `elapsedTime`, and there are plainer names for the common ones: -`minerals(p)`, `gas(p)`, `resources(p, "oreAndGas")`, `countUnits(p, unit, location?)`, -`kills(p, unit)`, `countdown()` and `elapsed()`. The last two count the game's own seconds, -sixteen frames each, so at Fastest they run about one and a half times as fast as -`sleep(seconds())`: `elapsed()` reads 21 after fourteen seconds of sleeping. A read means -what the condition means (a force's minerals are the force's sum, `units.Men` counts what -Bring counts), because where the game keeps no table of the value, the program asks the -condition itself, a bit at a time. What to read (the player, the unit, the location) is -known when the script is applied; `CurrentPlayer` is the player the program is running as. -A read is taken when the line runs, each time it runs: `let ore = minerals(P1)` keeps the -number, and `minerals(P1)` written twice reads twice. - -**Player facts** are reads too: `race(p)` against `races.Zerg` / `.Terran` / `.Protoss`, -`slot(p)` against `slots.Human` / `.Computer` / `.Empty` / `.Rescuable` / `.Neutral`, -`isHuman(p)`, `hasLeft(p)` (P1 … P8: a human or computer slot of the map whose player -is gone, which a slot nobody took also is; a computer never leaves), and `supply(p, "used" | -"max" | "provided", race?)` as the top bar shows it, of the race the player plays unless -one is given. - -#### Units - -**Units on the map are objects.** A `Unit` is one of the game's units as it is right now -(the entries of `units.` are `UnitType`s, and `u.type` is one): - -```ts -for (const u of unitsAt(locations.Pen, { owner: P2 })) u.hp = u.maxHp / 2; - -const target = nearest(units.TerranMarine, locations.Beacon, { owner: P1 }); -if (target) target.order("move", locations.Exit); -``` - -- `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?)` pick one - unit, or `null`. `first` and `randomUnit` take a set too: - `first(unitsAt(locations.Base, { owner: P1 }))`. TypeScript wants the `if (target)` - before the unit is used (or `target?.kill()`, `target?.hp ?? 0`). -- A filter is `{ type, owner, at }`, every part known when you build. `units.Men`, - `units.Buildings` and `units.Factories` work as a type, and `CurrentPlayer` as an owner. -- Units come oldest first: the game's unit table in the order the game hands its places - out (slot 0, then 1699 downwards). The walk stops once it has met every unit in use, so - a loop costs what the map holds. A unit made in the place of one that died, once the - game has freed it, comes where the dead one did. A dying unit is passed over, so - `first()` after a `kill()` is the next one. - -A unit has `hp`, `shields` and `energy` in whole points, `maxHp` / `maxShields` of its -type, `owner`, `type`, `x`, `y`, `kills`, `orderId`, `cooldown`, `resources` (a mineral -field's), `buildTime` (frames left of its making; a building finished early keeps the hit -points it had reached, and a Terran one under a third burns, so heal it after), `rank`, `matrix` (frames left of a -defensive matrix), `speed` (how fast it is moving now, in 256ths of a pixel a frame), the -timers `stim` `ensnare` `plague` `lockdown` `maelstrom` `irradiate` `stasis` in the game's -own ticks, and the booleans `invincible`, `hallucinated`, `speedUpgrade`, `cloaked`, -`burrowed`, `underAttack` (the game raised its under-attack alert for it within the last -second or so, which it does for a hit out of the player's view, not one on screen), -`hasTarget` (its order is aimed at a unit) and `inTransport`. -Hit points, shields, energy, kills, the cooldown (the ground weapon's, the air weapon's and -the spells' at once), the resources, the build time, the rank, the matrix (a write gives -250 points of matrix for that many frames; 0 takes it away), the timers, `invincible`, -`hallucinated` and `speedUpgrade` can be written (`u.hp = 0` kills). The rest the game -keeps for itself (a position write ends the game with "EUD not supported", a cloak write -showed nothing), and the declarations have them `readonly`, so the editor says so as you -type. A unit can be told: - -- `order("move" | "patrol" | "attack", location)`, the game's own Order, reaching this - unit alone; -- `give(player)`, `kill()`, `remove()`; -- `damage(n)` and `heal(n)`, or `{ percent: 50 }` of the type's maximum; -- `locate(location)`, which centres a location on it so that `createUnit` and the rest can - happen where the unit is. - -`select(player, unit)` or `select(player, [a, b])` makes what a player has selected that -unit or those units, as if they had clicked them. Functions take units and return them -(`function weakest(): Unit | null`), and `a == b` says whether two are one unit. - -A variable can keep a unit across a `sleep()`. The game gives a dead unit's place to the -next unit made, so every use checks that the unit is still the one that was kept: once it -is gone, its numbers read 0, its booleans false, and writing to it or telling it something -does nothing. `if (u)` asks. The unit of a loop's turn needs no check. A loop over units -runs within one frame, so a `sleep()` or an `await` inside one is an error. To act on one -unit at a time, find it again after each sleep: - -```ts -let u = first({ owner: P2, at: locations.Pen }); -while (u) { u.kill(); sleep(seconds(1)); u = first({ owner: P2, at: locations.Pen }); } -``` - -Each loop and each pick walks the unit table when its line runs (`randomUnit` twice), and -in a per-player program once for each player. Once or a few times a second is nothing. -The editor notes it at the end of the line (`scans units`), so that a scan inside a loop -that runs every frame is a choice and not an accident. - -#### Events - -**`on.*` runs a function each time something happens to a unit**, from the frame it is -written on. `const t = on.died(…)` gives back a task ([Waiting and -tasks](#waiting-and-tasks)), and `t.cancel()` stops listening. An `async` function is -started as a task each time, so it may wait. - -- `on.created(fn)`: every unit that comes onto the map, handed over on the next frame. -- `on.died(fn)`: every unit that dies or is removed, handed over as it was. -- `on.killed((u, by) => …)`: every unit killed in a fight, with the player whose attack - killed it. -- `on.morphed((u, was) => …)`: a unit that turns into another type and stays the same unit, - with the type it was. -- `on.order((u, order, was) => …)`: a unit that takes up a new order, with the one it had. - This compares every unit's order with the last frame's, so it costs a walk of the units. - -Each takes a unit type or a filter first: `on.created(units.ZergZergling, …)`, -`on.died({ owner: P8 }, …)`, `on.killed({ owner: P8, by: P1 }, …)`, -`on.order(units.TerranMarine, orders.Move, …)`. A listener written at the top of a file, -outside any program, listens from the start of the game. `on.key` and `on.chat` are the same -for the players' keys and typed lines ([Keys, the mouse and chat](#keys-the-mouse-and-chat)). - -```ts -program(async () => { - let lost = 0; - on.died({ owner: P1 }, (u) => { - lost += 1; - if (u.type == units.TerranMarine) print(`${lost} lost`); - }); - on.killed((u, by) => { if (by == P1) setResources(P1, "add", 5, "ore"); }); - on.created(units.ZergZergling, (u) => { u.hp = u.maxHp / 2; }); - on.died({ owner: P8 }, async (u) => { - await sleep(seconds(2)); - createUnit(P8, u.type, 1, locations.Spawn); - }); -}); -``` - -#### The game's tables and memory - -**`stats()` is the game's own tables.** What a unit type costs, what a weapon does, a -player's upgrades, as properties a program reads, assigns and `+=`s: - -```ts -stats(units.TerranMarine).minerals = 25; -stats(units.TerranMarine).speed = 6.5; // pixels a frame: a fraction is fine when known when you build -stats(units.ZergZergling).name = "Dog"; -stats(weapons.GaussRifle).damage += 2; -stats(upgrades.TerranInfantryArmor).minerals = 50; -stats(units.TerranGhost).permanentCloak = true; -stats(P3).color = "teal"; // or colors.teal -stats(P1).upgrades[upgrades.TerranInfantryWeapons] = 3; -stats(P1).researched[techs.Lockdown] = true; -if (stats(units.TerranMarine).minerals > minerals(CurrentPlayer)) print("too dear"); -``` - -A unit type has `maxHp`, `maxShields`, `armor`, `minerals`, `gas`, `buildTime` (seconds on -the game's clock), `supplyUsed`, `supplyProvided`, `sight`, `groundWeapon`, `airWeapon` -(`weapons.*`), `size`, `speed`, `name` and the flags `detector`, `permanentCloak`, -`cloakable`, `burrowable`, `regenerates`, `invincible`, `hero`, `organic`, `mechanical`, -`robotic`; a weapon `damage`, `bonus`, `cooldown`, `factor`, `range`, `minRange`; an upgrade -`minerals`, `gas`, `time` and (read only) `maxLevel`; a technology `minerals`, `gas`, -`time`, `energy`; a player `color`, `upgrades[…]` and `researched[…]`. Only what was played -and seen working in StarCraft: Remastered is there, and the hover on each says what it -reaches. Most unit-type fields apply to units made after the write, and a weapon's to -every unit using it at once. `speed` and `name` can be set but not read. A write lasts for -the game. `stats()` wants to see what it is given (`stats(units.TerranMarine)`, `stats(p)` -with `p: Player`), because a table's index is a plain number when the script runs and only -its type says which table. - -**`memory` is the game's other cells** that have no word of their own, each with a hover -that says what Remastered does with it: `memory.game.frame` (frames since the game began), -`memory.map.width` and `memory.map.height` (in tiles), and `memory.vision.countdown` -(set to 1, the next frame puts every tile back in fog, and each unit reveals what it sees -again). A field the game keeps for itself is `readonly`. - -```ts -program(() => { - if (memory.game.frame % 24 == 0) print(`${memory.map.width} by ${memory.map.height}`); - memory.vision.countdown = 1; -}); -``` - -`memory(address, comparison, value)` and `setMemory(address, modifier, value)` reach any -cell of the game's memory by its address (EUD), in a trigger or a program. - -#### Keys, the mouse and chat - -**What the players do is `keyPressed`, `keyHeld`, `clicked`, `mouse` and `chatted`.** A -press, a click and a typed line are true on the one frame they arrive, so a program looks -for them in a loop that runs every frame, or listens with `on.key` and `on.chat`: - -```ts -program(() => { - while (true) { - if (keyPressed(CurrentPlayer, "F8")) createUnit(CurrentPlayer, units.TerranMarine, 1, locations.Base); - - if (clicked(CurrentPlayer, "right")) { - const at = mouse(CurrentPlayer); // map pixels, kept as they are now - centerLocation(locations.Cursor, at.x, at.y); - createUnit(CurrentPlayer, units.TerranMarine, 1, locations.Cursor); - } - - const m = chatted(CurrentPlayer, "-spawn {n} {what:unit}"); - if (m) createUnit(CurrentPlayer, m.what, m.n, locations.Base); - - underMouse(CurrentPlayer, { owner: CurrentPlayer })?.heal(10); - sleep(frames(1)); - } -}, { owner: AllPlayers }); -``` - -- `keyPressed(player, key)` is true once per press, on the frame the press arrives, and - not while the player is typing a message. `keyHeld(player, key)` is true on every frame - the key is down. -- Keys are the letters and digits, `"F1"`, `"F7"` … `"F12"`, `"Space"`, `"Enter"`, - `"Escape"`, `"Tab"`, `"Shift"`, `"Ctrl"`, `"Alt"`, the arrows (`"Left"` …), - `"Backspace"`, `"Delete"`, `"Insert"`, `"Home"`, `"End"`, `"PageUp"`, `"PageDown"` and - `"Numpad0"` … `"Numpad9"`. The game keeps F2 to F6 to itself and reports no press of - them (played and seen; F2 to F4 move the camera), so asking for one is an error. The - game's own use of a key still happens. -- `clicked(player, "left" | "right" | "middle")` is the same as `keyPressed` for a mouse - button. -- `mouse(player)` is where the player's mouse is on the map, `x` and `y` in pixels (32 to a - tile). `const at = mouse(p)` keeps the place, and `centerLocation(location, x, y)` moves a - location there, its size kept (or `centerLocation(location, x, y, width, height)`), so - that `createUnit` and the rest can happen under the cursor. -- `bounds(location)` is where a location is now: `left`, `top`, `right`, `bottom` in - pixels, read in the game (so a location `centerLocation` or Move Location moved is read - where it went) and written one edge at a time — `const b = bounds(locations.Pen); - b.left += 32; b.right += 32;` slides it a tile to the right, its size kept. -- `underMouse(player, filter?)` is the unit nearest the mouse and no farther than `within` - pixels from it (48 unless the filter says), or `null`. -- `on.key("F7", (p) => …)` runs a function each time a player presses the key - (`{ from: P1 }` for one player's), and `on.chat("-give {n}", (p, { n }) => …)` each time - a player sends a line that fits the pattern. - -`chatted(player, pattern)` is `null`, or what the pattern read out of the line the player -sent. The pattern's own text is matched exactly and the whole line has to fit. `{n}` reads -a whole number (up to 1 048 575). `{what:unit}` reads a unit type by its name -(`Terran Marine` or `TerranMarine`, a custom name too); it takes the rest of the line, so -it comes last. `{kind:ore|gas}` reads one of the listed words, giving its place in the list -(0, 1, …). Names and words match whatever the capitals, and a pattern reads up to three -values. The values are typed from the pattern itself (`m.n` is a number, `m.what` a -`UnitType`, `m.amount` an error as you type), and they are the program's numbers: an action -takes a unit type and a count from them at once. Start a pattern with a word of its own -(`-spawn`), so ordinary talk is never taken for it; a line that fits no pattern is just -chat. A game played in single player has no chat: test typed lines in a multiplayer game, -which one person can host alone. - -The player is one of `P1` … `P8` or `CurrentPlayer`; in a per-player program, each -player's own keys, mouse and lines. All of it reaches every player's computer in step, a -few frames after it happens. That is the trip between computers, and it is the same in a -game alone. The map carries it, and only when a program reads input does it take something -from the map: one unit-properties slot, one unit type the map does not use (the Valkyrie, -unless the map uses it), the location the build already keeps for orders, and hidden units -of Player 12 at the map's top left corner, six for each human player and six more when the -mouse is read. No death counter, switch or string is used. Outside a -program these functions are an error, where `if (keyPressed(…))` would otherwise quietly be -true. Simulate presses no keys, so there they read as nothing; a [test](#tests) can press -them. - -**What only one computer knows** is `local`: `local.player` (the player at this computer), -`local.camera.x` and `.y` (where its screen is on the map), `local.mouse.x` and `.y` (the -mouse on the screen), `local.selected` and `local.selectionCount`. A local value may choose -what is shown on this computer. It may not change the game, since every computer would -change it differently and the game would split; the compiler refuses that. `await -sync(player, value)` sends that player's value to every computer and gives it back a few -frames later, the same everywhere. `select(P1, local.selected!)` makes this computer's -selection the player's selection on every computer. - -```ts -program(async () => { - let x = 0; - while (true) { - if (keyHeld(P1, "D")) x += 4; - if (local.player == P1) print(`screen at ${local.camera.x}`); - const cam = await sync(P1, local.camera.x); - if (cam > x) x = cam; - await nextFrame(); - } -}); -``` - -#### Text - -**A text can hold the program's values.** `displayText` takes a template literal (or -texts joined with `+`) with numbers of the program in it, `name(p)` and `color(p)`: - -```ts -let wave = 3; -displayText(`${color(P2)}${name(P2)}\x01 has ${minerals(P2)} ore — wave ${wave + 1}`); -print(`Wave ${wave}`, { to: AllPlayers, position: "center" }); -``` - -`name(p)` is the player's name and `color(p)` the colour code of their colour, both filled -in by the game; the ordinary colour codes work as ever. `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 ("Not enough minerals") appear. A boolean in a -text is `true` or `false`, and a number with a fraction is a decimal of up to four places. -A text with nothing of the program in it stays the plain Display Text action it was. None -of a program's texts enter the map's string table: they go into the built map's. - -**A text is a value.** `string` is a type of a program as `number` is: a variable holds -one, a function takes and returns one, a record has one for a field, and there is nothing -to declare beside it: no length, no capacity. - -```ts -let wave = 3, left = 12, shown = ""; -let title = wave > 10 ? "Late game" : "Early game"; // texts written in the script -let line = `Wave ${wave}: ${left} left`; // a text made in the game -line += "!"; -if (line != shown) { print(line); shown = line; } -setMissionObjectives(`${title} - ${line}`); -``` - -The compiler keeps a text one of two ways, a variable at a time, and the end of the -declaration's line says which: - -- *A text of the map*: a variable that only ever receives texts written in the script - (`"Boss"`, `boss ? "Boss" : "Wave"`, `titles[level]` out of a list the script has). It - holds the text's number in the built map's string table. Assigning it and comparing it - cost what a number's do, and the text of **any** action takes it. -- *A text that is made*: a template with the program's values in it, `a + b`, `s += "!"`, - `String(n)`, a method's result. Its characters are in a block of the memory the - programs' arrays share (the script's Settings set its size), and the variable owns the - block: assigning copies the characters, and what the variable held before goes back. A - text is a value, as it is in TypeScript, so a copy never follows what it was copied - from. A made text holds 1 023 bytes; past that it is cut off, which the game says once - in red and Simulate says on the line. - -What a text can do: `+`, `+=`, templates, `==` `!=` `<` `<=` `>` `>=`, `if (s)` (it is -not empty), `switch (s)`, `length`, `s[i]`, `at()`, `charAt()`, `charCodeAt()`, -`codePointAt()`, `slice()`, `substring()`, `indexOf()`, `includes()`, `startsWith()`, -`endsWith()`, `padStart()`, `padEnd()`, `repeat()`, `concat()`, `trim()`, `trimStart()`, -`trimEnd()`, `toUpperCase()` and `toLowerCase()` (A to Z only), `replace()` and -`replaceAll()` of a text, `split()` at a text into an array of texts, `parseInt()` and -`isNaN()`, `String(n)`, `n.toString()`, and `for (const ch of s)`. Anything else is refused -by name. A text the script has, with nothing of the program in it, takes every method -JavaScript has, since the script simply runs. - -```ts -program(() => { - let wave = 3; - const line = ` Wave ${wave}: zerglings, hydras `; - const parts = line.trim().split(": "); - print(parts[1].toUpperCase().replaceAll(", ", " + ")); // ZERGLINGS + HYDRAS - print(`${parseInt("12 ore") + wave} ${line.includes("hydra")} ${"A".charCodeAt(0)}`); // 15 true 65 -}); -``` - -**A character is a character, not a byte.** The game keeps a text as UTF-8, where a -Korean syllable is three bytes. A program counts what JavaScript counts, so -`"저글링".length` is 3 and `"저글링"[1]` is `글`. The one difference is a character past -U+FFFF (an emoji, a rare ideograph): JavaScript counts two and a program one, and the game -draws a dark square in its place, which a hint on the line says. Since a character has no -fixed size in the game's memory, `s[i]` and `slice()` walk the text from its start; over a -long text, `for (const ch of s)` walks once. That loop runs within the frame, so `sleep()` -inside it is refused. A loop over the places can sleep between turns, which is how a text -is typed out a character at a time: - -```ts -let wave = 3; -let line = `Wave ${wave} is here`; -let typed = ""; -for (let i = 0; i < line.length; i++) { typed += line[i]; print(typed); sleep(frames(2)); } -``` - -`padStart` counts characters, as it does in a browser, and the game's font is -proportional and a colour code has no width: it does not line columns up. - -**A made text outside the chat area.** The objectives, a leaderboard's label, a -transmission's line and a unit type's name (`stats(units.TerranMarine).name = …`) take a -made text. The game looks those texts up by number, so the build keeps a string of its own -for each kind (a player has one objectives text, one leaderboard and one transmission at a -time), and the text is written over it just before the action runs, up to 255 bytes. The -game reads such a string again whenever it draws, so the write happens only on the -computer of the player the action is for: in a program of every player, each sees their -own. What is on the screen is the text as it was when the action ran; changing the -variable afterwards changes nothing until the action runs again. Any other action's text -(`setNextScenario`, a sound's path) is a text of the map or an error that says so. - -A function takes and returns a text like any other value: it is called from its second use -on, its parameter a variable set to a copy at each call. One that calls itself may hold -texts: around each call that may come back, a text's three cells go on the stack with the -rest, so each run has its own. The one thing such a function cannot do is go through a -text with `for…of` around the call; the error says to walk it by place. Left out for now: -regular expressions, and the words a player typed in chat as a text. - -**Lines of JSX.** In a `.tsx` file, `…` is a line of text that may hold the -program's values, `` a bar of squares, and -`` a square of one of the game's text colours. `print` takes one, and -`ui.lines(player, () => …)` keeps the lines in a player's text area and draws them again -when the values in them change. `trigscript/ui` has panels, meters, menus and dialogs built -from them ([The standard library](#the-standard-library)). - -```tsx -program(async () => { - let hp = 0; - ui.lines(P1, () => ( - - Base {hp} - - )); - while (hp < 100) { - hp += 25; - await sleep(seconds(1)); - } - print(Wave {hp / 25}); -}); -``` - -#### Sounds - -`playWav(sound, 0)` plays a sound: one of the game's own files by its path, one of the -map's own sounds by its name (`sounds.Name`, from the Sound Editor's list), or a clip the -script makes when it is built. `tone()`, `noise()`, `pluck()`, `fm()` and `silence()` make -clips; `.fade()`, `.filter()`, `.delay()`, `.reverb()` and `.limit()` change them; -`mixdown()` lays clips over one another into one, and `together()` turns down clips the -game will play at once. The build puts each clip the script plays into the map once, and -a `trigger()` and a transmission take one too. Which clip plays may follow a variable of -the program. In the game, 32 sounds play at once and copies add up with no limiter, so keep -a clip's peak under 1 where copies overlap. - -```ts -const beep = tone("A5", 0.1, { wave: "square", volume: 0.3 }); -const zap = tone("A3", 0.4, { wave: "saw" }).filter("lowpass", [[0, 300], [0.4, 3000]]).reverb({ decay: 0.3 }); - -program(() => { - playWav(beep, 0); - if (random()) playWav(zap, 0); - playWav("sound\\glue\\mousedown2.wav", 0); -}); -``` - -Music played note by note is `trigscript/music` ([The standard library](#the-standard-library)). - -#### Waiting and tasks - -**Time is `sleep`.** `sleep(seconds(15))` gives the frame back and resumes that much -later; other programs and the map's triggers go on meanwhile. `frames(n)` is the game's -own clock, `seconds()` is twenty-four frames at Fastest, and `minutes()` sixty of those. -(`cycles(n)`, from before 3.0, is the same as `frames(n)`.) `wait()` inside a program is -allowed but is a different thing: the game's own Wait holds the rest of its trigger for -the time, and a player has one wait slot, so any other Wait of that player stops until it -is free. Use it for a short pause (a text, then a sound), and `sleep` to pass time. - -**`async` and `await`** are TypeScript's. In a program written `program(async () => …)`, -`await sleep(…)` is the same pause, and `await nextFrame()` waits one frame. An `async` -function may wait anywhere in its body, and `await f()` goes on when it returns, with its -result. `await until(() => n >= 3)` asks once at once and then once a frame, and goes on -in the frame the answer is true; `until(bring(…))` takes a condition of the game as it is. - -**A task runs beside the program.** `start(f())` starts an async call and goes on at once; -the program can `await` the task later, ask `t.running`, or stop it with `t.cancel()`. A -task may outlive the program that started it, and may start tasks of its own. -`start(wave(n), { max: 4 })` lets at most four of that start's tasks run at once. -`Promise.all([…])` waits for every branch, `Promise.race([…])` for the first, and -`Promise.any` is a race. **A race's losers stop where they are**, where JavaScript lets -them run on. `.then((v) => …)` hands on what a promise gives: -`sleep(frames(3)).then(() => 0)` in a race is a time limit with a value. - -```ts -async function wave(n: number) { - createUnit(P2, units.ZergZergling, n * 4, locations.Spawn); - await sleep(frames(n * 5)); - return n; -} - -program(async () => { - let ticks = 0; - const clock = start(async () => { - while (true) { await sleep(frames(1)); ticks += 1; } - }); - const first = await Promise.race([wave(1), wave(3)]); - print(`wave ${first} ended first`); // wave 1 ended first - await until(() => ticks >= 10); - clock.cancel(); - print(`${ticks} ticks, running: ${clock.running}`); // 10 ticks, running: false -}); -``` - -A task that ends on an error throws it where it is awaited. One that fails while nothing -awaits it is said in red, and the program goes on. Something that runs on its own clock is -a task, or another program. A `sleep` or an `await` cannot stand in a loop over units, in a -`for…of` over a text, or in a function that calls itself. - -**Generators** are TypeScript's: a `function*` walked by `for…of` puts the loop's body where -each `yield` is, `yield*` hands on another generator, `break` leaves both, and a class can -be walked with `*[Symbol.iterator]()`. An `async function*` is walked by `for await` and -may wait between its values. - -```ts -function* lanes(n: number) { - let i = 0; - while (i < n) { yield i * 32; i += 1; } -} - -program(() => { - let total = 0; - for (const x of lanes(4)) total += x; - print(`${total}`); // 192 -}); -``` - -**Edges.** `if (rose(bring(P1, units.AnyUnit, locations.Beacon, ">=", 1)))` 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. - -#### Owners and per-player programs - -**A program runs for its `owner`** (default P1), as a trigger would: one player is one -thread running as that player, only while that player is in the game, and -`CurrentPlayer` means that player. `AllPlayers`, a force (`players.Force1`) or a list of -players (`[P1, P2, P3]`) makes a **per-player program**. It runs once each frame for every -human or computer player among them who is in the game, `CurrentPlayer` is that player, -and every variable, array and task is per player, each with their own copy. -`let total = shared(0)` is one value they all share. That is how lives, scores and -cooldowns per player are written once: - -```ts -program(() => { - let lives: u8 = 3; - while (true) { - if (rose(deaths(CurrentPlayer, units.TerranMarine, ">=", 1))) { - lives -= 1; - setDeaths(CurrentPlayer, units.TerranMarine, "set", 0); - if (lives == 0) defeat(); - } - sleep(frames(1)); - } -}, { owner: AllPlayers }); -``` - -The simulator runs such a program as one player at a time. A script may have several -programs, each a thread of its own with its own variables. A program's text -(`displayText("…")`) is put into the built map's string table by the build, so it never -takes a string of the map you edit. A `trigger()`'s text goes into the map's string table -when the script is applied, as it always did. - -#### Files the script imports - -**Import Video…** in the Explorer turns a file into one the script can import, kept in the -map with the script: - -- a video into a `.video` file (`import film from "./film.video"`): its frames, a tile a - pixel, ink or not, for `fog.play` or `fog.light`; -- a picture into an `.image` file: its pixels at the size chosen, which `scale`, `crop`, - `threshold`, `tones` and `halftone` turn into rows for `fog.picture`; -- a sound file into a `.sound` file: a clip for `playWav` and the music library; -- a MIDI file into a `.notes` file: each track's notes, which `music.midi` plays. - -All of it is worked out when the script is built. What reaches the game is what the -script makes of it. `cover(picture, shapes)`, also worked out then, gives the brushes of -your own shapes that draw a picture's ink, for a renderer of your own; `fog.picture` is -the same with the game's sight ranges. - -#### The standard library - -**Modules shipped with TrigScript**, imported as `"trigscript/"`. Each is TypeScript -written for programs, and the hover in the editor documents every name. - -| Module | What it is for | -| --- | --- | -| `trigscript/math` | `lerp`, `remap`, easing curves, `distance`, `angleTo`, `approach`, `wrap`, `PI`, `TAU`, and `Vec`, a point of two fixed numbers | -| `trigscript/random` | `chance(25)`, `range(1, 3)`, `dice(3, 6)`, `pick(list)`, `weighted([70, 25, 5])`, `shuffle(xs)`, and `Random`, the same numbers every game for one seed | -| `trigscript/timers` | `every`, `after`, `Cooldown`, `Countdown`, `Stopwatch`, `now()`, and `over` and `spread` for a loop spread over frames | -| `trigscript/signals` | `Signal`, events of your own: `on`, `once`, `off`, `emit` | -| `trigscript/ui` | for lines of JSX: `Panel`, `Meter`, `MenuList` and `choose` (a menu chosen with W, S and Space), `DialogBox`, `say` and `typewriter`, `log`, `clock`, `thousands` | -| `trigscript/cutscene` | `Camera` (`look`, `pan`, `follow`, `shake`), `walk`, `walkAll`, `charge`, `line` (a portrait that talks), `play` (a scene the player may skip) | -| `trigscript/fog` | the fog of war as a picture: `reveal`, `light`, `rect`, `columns`, `picture`, `play` for a video | -| `trigscript/screen` | `Screen`, a grid of pixels shown on the map as units, `sprite`, and `ScreenView` for the text lines | -| `trigscript/music` | songs played note by note: `synth`, `sampler`, `kit`, `track`, `song`, `arrange`, `midi`, `play`, `loop` | - -```ts -import { chance, range } from "trigscript/random"; -import { every, after, Cooldown, SECOND } from "trigscript/timers"; - -program(async () => { - every(10 * SECOND, (n) => createUnit(P2, units.ZergZergling, range(2, 4), locations.Spawn)); - after(3 * SECOND, () => print("Go!")); - const shot = new Cooldown(12); - while (true) { - if (keyPressed(P1, "Space") && shot.use() && chance(25)) print("a lucky shot"); - await nextFrame(); - } -}); -``` - -`fog` adds a program of its own, which wipes the fog it lit every frame. `fog`, `screen` -and `cutscene` each take one of the map's first 63 locations that the map does not use, -with `reserveLocation(why)`. A script of your own can call it too: the location is chosen -when the script is built, and the reason is what the map's requirements show. - -#### Libraries from other plugins - -Another plugin can provide a library of its own: TrigScript modules a script imports by -the library's name, the way it imports the standard library by `trigscript/…`. -`import { spawnWave } from "waves/spawn"` is the module `spawn` of the library `waves`, and -`import { spawnWave } from "waves"` is its `index`. While the plugin is on, the editor -completes the library's names and its hovers show the library's own source and comments. - -```typescript -import { announce, spawnWave } from "waves"; - -program(() => { - for (let wave = 1; wave <= 5; wave++) { - announce(`Wave ${wave}`); - spawnWave(P2, units.ZergZergling, wave, locations.Spawn); - sleep(seconds(20)); - } -}); -``` - -**The map keeps its own copy.** When the script is applied (saving does it), each library -the script imports is copied into the map, under `trigscript\@libraries\\` beside -the script's files, and from then on the map builds with that copy. A map opened where the -plugin is not installed still builds, and so does a map shared with someone who has a -different version of the plugin. A library the script stops importing leaves the map with -the next apply. - -**A newer version is offered, not taken.** When the plugin installed here has a different -version from the map's copy, the editor says so and offers *Update*, which replaces the -copy. The script then reads as changed, and the next save builds with the new copy. *Keep -the map's copy* leaves it as it is. - -A library is TrigScript like the script's own files, and runs with the same rights: a -library in a map from someone else is no different from that map's own script. A module of -a library joins the build only when something imports it, so a `program()` written at the -top of a module runs only in the maps that import that module. - -#### What a program does not have - -Outside `program()` the script is all of TypeScript, since it simply runs. Inside one, what -is written has to become something the game can do, and these cannot. Each is refused with -a message that says so; none is passed over in silence: - -| Not there | Write instead | -| --- | --- | -| `**` or `Math.pow` by a power that changes while the game runs | multiply in a loop | -| A fraction in a `u8`, `u16` or `u32`, or a fixed number where only a whole one goes (an index, a count) | `Math.floor`, `Math.round` or `Math.trunc` around it | -| `\|\|=` and `&&=` | `flag = flag \|\| other` | -| A variable with no first value (`let n;`) | `let n = 0` | -| `for…in` | `for…of`, or a loop over a list the script knows | -| `delete` | a field that says the value is gone (`hp = 0`), or a `Map` | -| `typeof x` or `"key" in x` as a test while the game runs | a variable of one type, or a `switch` on a number you keep beside it | -| A function that calls itself and waits | a loop that waits: `while (n > 0) { …; await sleep(…); n--; }` | -| An `async` function or a generator kept as a value | call it where it is written, or `start()` it | -| A function that may return one instance or another it did not make | keep the instances in an array and return the place | -| `throw` of something that is not an error; a caught error kept in a variable | `throw new Error(…)` or a class that extends it; copy what you need out of it in the `catch` | -| What `splice` takes out, as a value | read it first: `xs.slice(start, end)` | -| `flat`; `map` into texts or records | `push` in a `for…of` | -| On a made text: regular expressions, and `replace` with a function | `replace` / `replaceAll` / `split` with a text | -| A `Map` whose values are texts or records, or whose keys are texts other than a union the script knows | the place of a row of an array of records as the value | -| Methods or getters on an object literal | a class | -| A static block in a class | the static field's value where it is declared | -| A `for` written out more than 256 times | a `while` loop | -| `Math.random()`, `Date`, `JSON`, `setTimeout`, `bigint` | `random(n)`; `elapsed()` or `memory.game.frame`; `trigscript/timers` | -| A local value (`local.*`) that changes the game | `await sync(player, value)` first | -| Writing a unit's position, cloak or tint | the game refuses these itself (*Units*) | -| Playing on the classic game (1.16.1) | `trigger()` only | +Their sources are `docs/trigscript.md`, `docs/trigscript-language.md` and +`docs/trigscript-library.md` in [scm-js/scm-js](https://github.com/scm-js/scm-js), whose +tests compile every example in them against the TrigScript version the editor ships. A +change to the language goes there. The sections below say what each version changed. ### Coming from 4.0 @@ -2070,7 +413,7 @@ A 4.0 script builds into the same map. What 4.1 changed: - **Libraries from other plugins.** A plugin can provide TrigScript modules that a script imports by id (`import { spawnWave } from "waves"`), and the map keeps a copy of each one it imports, so it still builds where that plugin is missing or at another version - ([Libraries from other plugins](#libraries-from-other-plugins), [Providing a + ([Libraries from other plugins](https://docs.scmjs.dev/trigscript-library/libraries-from-other-plugins/), [Providing a library](#providing-a-library)). `trigscript.build` is service version 2. - **Long lists known when the script is built** are built, such as a video played in the fog: 4.0 refused one of more than 16,384 values wider than six bits ("arrays and stacks @@ -2091,13 +434,13 @@ working. What 4.0 changed around that: game* judges each test twice, and a test's frames carry a cost in milliseconds of Remastered's time. Every address a build touches is checked against what Remastered allows, line by line, before a map is written. -- **The language grew**: [functions as values](#functions-as-values) and closures, [`try` - / `catch` / `throw`](#errors), `async` functions, tasks and `Promise.*` ([Waiting and - tasks](#waiting-and-tasks)), [`on.*` events](#events), [numbers with a - fraction](#numbers-with-a-fraction), [the game's tables and `memory`](#the-games-tables-and-memory), - [`local` reads and `sync()`](#keys-the-mouse-and-chat), programs of a force or a list - of players ([Owners](#owners-and-per-player-programs)), [sounds](#sounds), pictures and - videos the script imports, and a [standard library](#the-standard-library) of nine +- **The language grew**: [functions as values](https://docs.scmjs.dev/trigscript-language/functions/#functions-as-values) and closures, [`try` + / `catch` / `throw`](https://docs.scmjs.dev/trigscript-language/errors/), `async` functions, tasks and `Promise.*` ([Waiting and + tasks](https://docs.scmjs.dev/trigscript-language/waiting-and-tasks/)), [`on.*` events](https://docs.scmjs.dev/trigscript-language/events/), [numbers with a + fraction](https://docs.scmjs.dev/trigscript-language/variables-and-numbers/#numbers-with-a-fraction), [the game's tables and `memory`](https://docs.scmjs.dev/trigscript-language/the-games-tables-and-memory/), + [`local` reads and `sync()`](https://docs.scmjs.dev/trigscript-language/keys-the-mouse-and-chat/), programs of a force or a list + of players ([Owners](https://docs.scmjs.dev/trigscript-language/owners-and-per-player-programs/)), [sounds](https://docs.scmjs.dev/trigscript-language/sounds-and-imported-files/#sounds), pictures and + videos the script imports, and a [standard library](https://docs.scmjs.dev/trigscript-library/) of nine modules. - **Input is the build's own.** Keys, clicks, the mouse and typed lines reach every player's computer in step without MSQC or a chat plugin: `keyPressed`, `keyHeld`, @@ -2269,7 +612,7 @@ as they are) or an object of every file by path. | `trigscript.build(source, { takeOver?, replaceStale? })` | Apply: compile and, when clean, write the `trigger()` records as the block and store the files with the map (the programs are built into the file when the map is saved): a promise of `{ compiled, block, refused?, replaced? }`. The build lands only on the map it was compiled for: `block` is null and `refused` says why when there were `"errors"`, the map `"closed"` or another one `"switched"` to the front while the script ran, or the map's names `"changed"` under it (that one is compiled again, twice at most, before it is reported). Files edited in the archive while the script ran are kept, and the state then reads as unbuilt. When the old block was edited by hand, the new one is appended, or with `replaceStale` put in the old one's place with its unchanged records removed and the edited ones kept after it (`replaced` counts both). `takeOver` replaces the whole trigger list with the script's. A settings-style transaction: not undoable, marks the map modified. | | `trigscript.print(triggers, { imports?, header? })` | Records as `trigger()` calls in the script language: what Import map triggers writes. `imports` starts the text with an import of the names it uses. | | `trigscript.simulate(triggers, cycles, { player? })` | The interpreter: `{ cycles, events, switches }`. | -| `trigscript.libraries()` | The [libraries](#libraries-from-other-plugins) the open map keeps a copy of and the ones plugins provide, side by side: `{ id, label, version, provided, update }`, with `version` the map's copy (null when it keeps none), `provided` what a plugin has now (null when none does), and `update` true when the two differ. | +| `trigscript.libraries()` | The [libraries](https://docs.scmjs.dev/trigscript-library/libraries-from-other-plugins/) the open map keeps a copy of and the ones plugins provide, side by side: `{ id, label, version, provided, update }`, with `version` the map's copy (null when it keeps none), `provided` what a plugin has now (null when none does), and `update` true when the two differ. | | `trigscript.updateLibraries(ids?)` | Replace the map's copies of these libraries (every one with a newer version provided, by default) with what their plugins provide now; the ids it updated. The map is modified and the script reads as changed. | | `trigscript.triggerAt(file, line)` | Which trigger (index in the map's list) a 1-based line of a file generated; null when none did or the block is stale. | | `trigscript.open({ file?, line? })` | Open the editor, on a file and line. | @@ -2309,7 +652,7 @@ Magenta's build rows reach the map this way. ### Providing a library A plugin can give scripts a library to import ([Libraries from other -plugins](#libraries-from-other-plugins)) through the same service, from version 2 of it +plugins](https://docs.scmjs.dev/trigscript-library/libraries-from-other-plugins/)) through the same service, from version 2 of it on: `provideLibrary` takes the library's id, which is what scripts import it by, a label and version for the editor to show, and each module's TrigScript source by its path in the library. diff --git a/tests/readme.test.ts b/tests/readme.test.ts index 877f301..4bb78af 100644 --- a/tests/readme.test.ts +++ b/tests/readme.test.ts @@ -3,6 +3,10 @@ * has the locations the examples name; a block that calls neither `program()` nor `trigger()` is lines of a * program's body, and is compiled inside one. A block of several * files says so with a `// name.ts` line before each (`// tests/name.test.ts` for one in a folder). + * + * The language's own examples left the README for the documentation site (scm-js's docs/trigscript-language.md and + * docs/trigscript-library.md), whose tests compile them against the version the editor ships; the few here are the + * README's own, for the workspace and the tests. */ import { readFileSync } from "node:fs"; import { resolve } from "node:path"; @@ -44,7 +48,7 @@ function files(code: string): Record { describe("the README's examples", () => { const all = blocks(); - it("are there", () => expect(all.length).toBeGreaterThan(20)); + it("are there", () => expect(all.length).toBeGreaterThan(0)); it.each(all.map((b) => [b.line, b.code] as const))("README.md:%i compiles, and makes a trigger or a program", (_line, code) => { const r = compileScript(ts, files(code), NAMES, { lib: LIB }); expect(r.diagnostics.map((d) => `${d.file}:${d.line} ${d.message}`)).toEqual([]);