Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 10 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -435,7 +435,7 @@ with a message, never passed over in silence.
| `try` / `catch` / `throw` | Missing | The game has no exceptions. What is always a mistake — an index past the end, a full heap, a stack overflow — is said in the game in red, or listed by Simulate. |
| `async` / `await`, promises, generators, timers | Missing | `sleep()` is how a program waits: it gives the frame back and carries on later, and may stand anywhere but in a function that calls itself or a loop over units. Something on its own clock is another `program()`. |
| Modules | Same | `import` between the script's files and from `"trigscript"`; nothing from npm. A function another file's program calls is made with `game()`. |
| The standard library | A little | `Math.min`, `Math.max`, `Math.abs` (and the rounding ones, which change nothing), `String(n)`, `n.toString()`, `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`, `RegExp`, `Object.*`, and no `Math.sqrt` or `**` on a variable. |
| The standard library | A little | `Math.min`, `Math.max`, `Math.abs` (and the rounding ones, which change nothing), `String(n)`, `n.toString()`, `Array.from`, `new Array(n).fill(v)`; `Math.sqrt`, `Math.sign`, `Math.log2` (of a whole number: rounded down), `~`, and `**` / `Math.pow` by a power known when the script is built. 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`, `RegExp`, `Object.*`, and no `**` by a variable. |
| — | Added | What TypeScript has no word for: `sleep`, `rose` and `once`, reads of the game (`minerals(p)`), `Unit` objects and loops over units, `stats()`, `keyPressed` / `clicked` / `mouse` / `chatted`, `shared()`, per-player programs. |

The sections below say each of these in full, and [What a program does not
Expand Down Expand Up @@ -553,8 +553,11 @@ program(() => {
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()`
or its end, all within that frame; the next frame it goes on from there. A body that ends
stops for good. `while (true) { …; sleep(frames(1)); }` is therefore a game loop running
once per frame, and `sleep(seconds(2))` at its end makes it one every two seconds.
stops for good, and a `return` at the top of the body ends it there (`if (!hero) return;`).
`while (true) { …; sleep(frames(1)); }` is therefore a game loop running once per frame,
and `sleep(seconds(2))` at its end makes it one every two seconds. A trigger's `preserve()`
does nothing in a program, which is said as a remark; `disabled(…)` is refused, since a
program tests its conditions and runs its actions rather than skipping them.

#### Variables and records

Expand All @@ -571,7 +574,7 @@ played and cost the map nothing: no death counters, no switches, no triggers in
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)`, `includes(v)` and `indexOf(v)`
(`x` is a copy of the cell, as in TypeScript), `fill(v, start?, end?)`, `includes(v)` and `indexOf(v)`
(−1 when there is none) 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. In a program of
Expand Down Expand Up @@ -1199,8 +1202,9 @@ 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`, and TypeScript wants the `if (target)` before
the unit is used (or `target?.kill()`). A filter is `{ type, owner, at }`, every part known
`randomUnit(filter?)` pick one unit or `null` — `first` and `randomUnit` take a set too,
`first(unitsAt(locations.Base, { owner: P1 }))` — and 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 stopping once it has met
Expand Down
13 changes: 11 additions & 2 deletions compiler/backend/build.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,8 @@ export interface OwnBuildStats {
mapTriggers: number;
mapTriggerCopies: number;
hyperTriggersLeftOut: number;
/** The map's own triggers for a player group the game runs no list for (Players 9 to 12, Foes, Allies, Neutral, Non-Allied Victory Players): laid out in no list, as in the game. */
mapTriggersUnlisted: number;
startUpTriggers: number;
input: { keys: number; mouse: boolean; syncs: number; carrier: number; ring: number; rings: number; humans: number[]; cuwp: number; bits: number } | null;
/** How many strings the map had before, and has now. */
Expand All @@ -77,6 +79,8 @@ export interface OwnBuild {
stats: OwnBuildStats;
/** With `maps`: where the triggers and the variables are, and the same as lines of a build's log. */
maps?: OwnMaps;
/** What the map maker should hear of beyond the log: triggers of the map's own that are left out or never run. */
warnings: string[];
}

export type OwnBuildOutcome = OwnBuild | { refused: string };
Expand Down Expand Up @@ -164,6 +168,7 @@ export function buildOwnMap(input: OwnBuildInput): OwnBuildOutcome {
mapTriggers: lowered.triggers.length,
mapTriggerCopies: lowered.triggers.reduce((n, t) => n + t.slots.length, 0),
hyperTriggersLeftOut: lowered.hyperTriggersLeftOut,
mapTriggersUnlisted: lowered.triggers.filter((t) => t.slots.length === 0).length,
startUpTriggers: own.trig.length / 2400,
input: lowered.input ? { keys: lowered.input.keys.length, mouse: lowered.input.mouse, syncs: lowered.input.syncs, carrier, ring: lowered.input.ring, rings: lowered.input.rings, humans: lowered.input.humans, cuwp, bits: lowered.input.bits } : null,
stringsBefore,
Expand All @@ -174,8 +179,12 @@ export function buildOwnMap(input: OwnBuildInput): OwnBuildOutcome {
};
const maps = input.maps ? ownMaps(lowered, own, input.compiled.ir, strings.length, textBase) : undefined;
if (stats.input) lines.push(`input: ${stats.input.keys} keys, ${stats.input.mouse ? "the mouse, " : ""}${stats.input.syncs} sync slots; carriers of type ${carrier}, ${stats.input.ring} a ring, ${stats.input.rings} rings, for players ${stats.input.humans.map((h) => h + 1).join(", ")}; unit-properties slot ${cuwp}; ${stats.input.bits} bits a value`);
lines.push(`${stats.runs} run${stats.runs === 1 ? "" : "s"} (${stats.runNames.join(", ")}), ${stats.graphTriggers} triggers of the programs, payload ${stats.payloadBytes} bytes at 0x${stats.address.toString(16)}, ${stats.mapTriggers} trigger${stats.mapTriggers === 1 ? "" : "s"} of the map's own (${stats.mapTriggerCopies} copies${stats.hyperTriggersLeftOut ? `, ${stats.hyperTriggersLeftOut} hyper triggers left out` : ""}), start-up ${stats.startUpTriggers} triggers, ${stats.stringsAfter - stats.stringsBefore} strings added`);
return { chk: s.bytes(), lines, stats, ...(maps ? { maps } : {}) };
lines.push(`${stats.runs} run${stats.runs === 1 ? "" : "s"} (${stats.runNames.join(", ")}), ${stats.graphTriggers} triggers of the programs, payload ${stats.payloadBytes} bytes at 0x${stats.address.toString(16)}, ${stats.mapTriggers} trigger${stats.mapTriggers === 1 ? "" : "s"} of the map's own (${stats.mapTriggerCopies} copies${stats.hyperTriggersLeftOut ? `, ${stats.hyperTriggersLeftOut} hyper triggers left out` : ""}${stats.mapTriggersUnlisted ? `, ${stats.mapTriggersUnlisted} in no list` : ""}), start-up ${stats.startUpTriggers} triggers, ${stats.stringsAfter - stats.stringsBefore} strings added`);
// What the map maker should hear of, beyond the log: the editor shows each as a notice.
const warnings: string[] = [];
if (stats.hyperTriggersLeftOut) warnings.push(`${stats.hyperTriggersLeftOut} hyper trigger${stats.hyperTriggersLeftOut === 1 ? "" : "s"} of the map's own ${stats.hyperTriggersLeftOut === 1 ? "was" : "were"} left out: a map with a program in it goes through its triggers every frame as it is, and with hyper triggers in a list the game would hold it to every second frame.`);
if (stats.mapTriggersUnlisted) warnings.push(`${stats.mapTriggersUnlisted} trigger${stats.mapTriggersUnlisted === 1 ? "" : "s"} of the map's own ${stats.mapTriggersUnlisted === 1 ? "is" : "are"} for a player group the game runs no triggers for (Players 9 to 12, Foes, Allies, Neutral, Non-Allied Victory Players): ${stats.mapTriggersUnlisted === 1 ? "it" : "they"} never run, in this build as in the game.`);
return { chk: s.bytes(), lines, stats, warnings, ...(maps ? { maps } : {}) };
}

export const isRefused = (o: OwnBuildOutcome): o is { refused: string } => "refused" in o;
12 changes: 9 additions & 3 deletions compiler/backend/emit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1125,7 +1125,8 @@ class Scope {
}
return true;
}
case "return": return (this.native && this.returning.length > 0 && this.returned(s)) || this.island(s);
// (A `return` at the top of a program ends it: nothing to give back.)
case "return": return (this.native && (this.returning.length > 0 ? this.returned(s) : s.value === undefined)) || this.island(s);
// (A map of our own: the slots `planTasks` made — a listener's, `on.*`'s, as any other's.)
case "start": {
const slots = this.native ? this.slots.get(s) : undefined;
Expand Down Expand Up @@ -2680,6 +2681,8 @@ class Emitter {
/** A `return`: the value into the result of the function or inlined call around it, and on to its end. */
ret(s: Extract<Stmt, { kind: "return" }>): void {
const frame = this.frames[this.frames.length - 1];
// At the top of the program: its end, for good.
if (!frame) { this.goto(END); return; }
const r = frame.result;
if (r && r.kind === "boolean" && s.value !== undefined) this.setBool(r.id, s.value as BoolExpr);
else if (r) this.write(r.id, s.value === undefined ? { c: 0, terms: [] } : linear(s.value as NumExpr)!);
Expand Down Expand Up @@ -4757,8 +4760,11 @@ class Emitter {
// product of the target — and stays one of its own.)
// (Or a field that holds it plus a constant — a store of `xs[i] + 1` — when nothing tests the scratch cell or
// routes into it: the scratch's 0 to start from is that constant.)
// (A map of our own only: a cell trigger of its own adds its number in, where the Python side's reads may set it.)
const fromConst = (p: Place) => this.scope.native && p.scale === 1 && !p.base && !tested.get(t.copy!.from.slice(1))?.length && !routedInto.get(t.copy!.from.slice(1))?.length;
// (A map of our own only: a cell trigger of its own adds its number in, where the Python side's reads may set it.
// Not when a masked write works the scratch cell — `&`, `|`, `^` by a constant: a `set 0` under a mask would
// become a set of the constant under it, and put the constant's bits there: `(v & 0xffff) − 1025` came out as
// v's low half under −1025's high half, step 12's Open list.)
const fromConst = (p: Place) => this.scope.native && p.scale === 1 && !p.base && !tested.get(t.copy!.from.slice(1))?.length && !routedInto.get(t.copy!.from.slice(1))?.length && !(scratch.get(t.copy!.from.slice(1)) ?? []).some((a) => a.mask !== undefined);
const lonePlace = !apart && ps.length === 1 && ps[0].dword && plainPlace(ps[0]) ? ps[0]
: !apart && ps.length === 2 && (this.own.has(target) || !!this.scope.decls.get(target)?.scoped) && (plainPlace(ps[1]) || fromConst(ps[1])) && ps[0].dword && !cellsRead.has(JSON.stringify(ps[0].dword)) ? ps[1]
: undefined;
Expand Down
6 changes: 3 additions & 3 deletions compiler/declarations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -397,12 +397,12 @@ ${kw}function unitsAt(location: Location, filter?: Omit<UnitFilter, "at">): Unit
${kw}function unitsOf(player: Player, filter?: Omit<UnitFilter, "owner">): UnitSet;
/** Every unit on the map the filter matches. */
${kw}function allUnits(filter?: UnitFilter): UnitSet;
/** The first unit the filter matches, or null. */
${kw}function first(filter?: UnitFilter): Unit | null;
/** The first unit the filter matches, or null; a set of units — \`first(unitsAt(locations.Base, { owner: P1 }))\` — is its filter. */
${kw}function first(filter?: UnitFilter | UnitSet): Unit | null;
/** The unit of a type nearest to the centre of a location, or null; units.AnyUnit for any type. */
${kw}function nearest(unit: UnitType, location: Location, filter?: Omit<UnitFilter, "type">): Unit | null;
/** One of the units the filter matches, picked by the game, or null. */
${kw}function randomUnit(filter?: UnitFilter): Unit | null;
${kw}function randomUnit(filter?: UnitFilter | UnitSet): Unit | null;
/**
* The game's own tables, inside program() only: what a unit type costs, what a weapon does, a player's
* upgrades. Read a field as a number, assign to it, += it: \`stats(units.TerranMarine).minerals = 25;\`
Expand Down
5 changes: 3 additions & 2 deletions compiler/eud.ts
Original file line number Diff line number Diff line change
Expand Up @@ -267,8 +267,9 @@ function assigned(body: Stmt[], functions: Map<string, FuncDecl>, into = new Set
const f = c.fn ? functions.get(c.fn) : undefined;
if (f && !followed.has(f.id)) { followed.add(f.id); f.body.forEach(stmt); pops(f.body); }
};
// A pop() whose value is used — `const i = queue.pop()!` — is an expression, and shortens the array as the statement does.
const pops = (stmts: Stmt[]) => expressions(stmts, (e) => { if (e.kind === "pop") into.add(whole(e.array)); });
// A pop() whose value is used — `const i = queue.pop()!` — is an expression, and shortens the array as the statement
// does; a call used as a value — `xs[n++] = 1`, whose index is the update's call — assigns what its body does.
const pops = (stmts: Stmt[]) => expressions(stmts, (e) => { if (e.kind === "pop") into.add(whole(e.array)); else if (e.kind === "call") call(e.call); });
body.forEach(stmt);
pops(body);
return into;
Expand Down
Loading
Loading