From 08cc73a83d73e39b4e75dd395f240075021c538c Mon Sep 17 00:00:00 2001 From: Jeany Date: Mon, 19 Oct 2026 23:23:59 -0600 Subject: [PATCH 1/6] A map of our own says what it refuses: the part the emitter never takes, and its line MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Most refusals read "a assign it does not lower (no reason given)": the checks' reasons were dropped or kept from an earlier statement, so `a * b`, `a / 4`, `random(6)` and `minerals(P1)` were refused in the same words. Now each statement refused keeps its own reason, and `unlowered` names the first part of it the emitter never takes, in a script's words — "a product of two values, line 51", "the action Set Resources", "a write of unit.sight", "a unit's hp", "an event handled (on.killed)", "an error thrown or caught (try, throw)" — preferring such a part over a reason of how things are put together. build-own's checks move into emitOwn (compiler/backend/own.ts), which the ledger asks too. A print to the owner by its slot is taken; a program owned by P9 to P12, whose triggers the game does not run, is refused. The maps build-own makes of the five own probes are byte for byte as before. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01T1CM9pLuTuAn3mgTEkjgVg --- compiler/backend/emit.ts | 216 +++++++++++++++++++++++++++++++++++++-- compiler/backend/own.ts | 33 ++++++ scripts/build-own.mts | 26 +---- 3 files changed, 247 insertions(+), 28 deletions(-) create mode 100644 compiler/backend/own.ts diff --git a/compiler/backend/emit.ts b/compiler/backend/emit.ts index f17e87c..ad37d48 100644 --- a/compiler/backend/emit.ts +++ b/compiler/backend/emit.ts @@ -21,6 +21,7 @@ import { STACK_DEPTH, type ArrayDecl, type BoolExpr, type Call, type FuncDecl, type NumExpr, type Program, type Stmt, type TextPart, type VarDecl, type At } from "../ir"; import { PACK_FROM } from "../eud"; import { PlayerGroup } from "../../vendor/triggers"; +import { actionDef, conditionDef } from "../../vendor/triggerDefs"; import { END, MOST_ACTS, MOST_CONDS, STACK, checkGraph, nextOf, type Act, type Compare, type Cond, type Dword, type FixedFn, type Graph, type Num, type Region, type Trigger } from "./graph"; /** The game's tick (frames since the game started), and the fields of the trigger a map of our own wakes by. */ @@ -105,6 +106,184 @@ function side(e: NumExpr): Side | null { return null; } +// ── what a map of our own refuses, said ── + +/** Why an array is not one a map of our own keeps (`nativeArray`), in a script's words. */ +function arrayWhy(a: ArrayDecl): string | undefined { + if (a.texts) return "an array of texts"; + if (a.dynamic || a.through) return "an array that grows"; + if (a.task !== undefined) return "an array of a task's"; + if (a.bits) return `a u${a.bits} array`; + if (a.kind !== "number" && a.kind !== "boolean") return `an array of ${a.kind}s`; + return undefined; +} + +/** + * The first thing in a statement a map of our own refused that its emitter never takes, in a script's words: the + * reason a refusal gives when the checks that refused it gave none ("a product of two values", "the action Create + * Unit"). Inner parts before the whole, in the order they are written; undefined when every part is one the emitter + * takes somewhere, and the reason lies in how they are put together. + */ +export function unlowered(s: Stmt, decls: Map, arrays: Map): string | undefined { + const variable = (id: string): string | undefined => { + const d = decls.get(id); + if (!d) return undefined; + if (d.kind === "unit") return "a unit kept in a variable"; + // (A program with `try` or `throw` keeps the message of the error in flight: what it is refused for is the error.) + if (d.kind === "text") return d.name === "(error message)" ? "an error thrown or caught (try, throw)" : d.name.startsWith("(") ? "a text made while the game runs" : "a text kept in a variable"; + if (d.bits) return `a u${d.bits} variable`; + if (d.task !== undefined) return "a variable of a task's"; + if (d.held) return "a variable a closure keeps"; + return undefined; + }; + const array = (id: string): string | undefined => { + const a = arrays.get(id); + return a && arrayWhy(a); + }; + const plainSide = (e: NumExpr): boolean => linear(e) !== null || e.kind === "element"; + const isConst = (e: NumExpr): boolean => linear(e)?.terms.length === 0; + const binary = (e: Extract): string | undefined => { + if (linear(e) || !plainSide(e.left) || !plainSide(e.right)) return undefined; + const byConst = isConst(e.right); + switch (e.op) { + case "*": return "a product of two values"; + case "/": return byConst ? "a quotient by a constant" : "a quotient of two values"; + case "%": return byConst ? "a remainder by a constant" : "a remainder of two values"; + case "&": case "|": case "^": return byConst ? `${e.op} with a constant, as a value` : `${e.op} of two values`; + case "<<": case ">>": case ">>>": return byConst ? `a shift by a constant (${e.op}) as a value` : `a shift by a value (${e.op})`; + default: return undefined; + } + }; + const boolOnly = new Set(["compare", "and", "or", "not", "test", "cond", "random", "running", "unitAlive", "unitSame", "unitFlag", "edge", "textCompare", "textTest"]); + // Expressions: what each kind is, when the emitter never takes it. + const node = (o: Record): string | undefined => { + switch (o.kind) { + case "binary": return binary(o as Extract); + case "intrinsic": { + const name = o.name as string; + if (name === "fmul") return "a product of fixed numbers"; + if (name === "fdiv") return "a quotient of fixed numbers"; + return name === "min" || name === "max" || name === "abs" ? undefined : `Math.${name}`; + } + case "read": { + const r = o.read as { source: string; record?: { type: number }; fact?: string; of?: string }; + if (r.source === "condition") return `a read of the game (${conditionDef(r.record!.type)?.name ?? `condition ${r.record!.type}`})`; + if (r.source === "player") return r.fact === "left" ? "whether a player has left" : `a player's ${r.fact}`; + return `a player's supply (${r.of})`; + } + case "cond": { + const r = o.record as { type: number; player: number }; + if (r.type === DEATHS_CONDITION && r.player > LAST_GROUP) return "a read of memory (memory())"; + return `the condition ${conditionDef(r.type)?.name ?? r.type}`; + } + case "tableRead": return `a read of ${(o.cell as { name: string }).name}`; + case "randomInt": return "a random number"; + case "random": return "a random boolean"; + case "input": return `the players' input (${(o.input as { source: string }).source})`; + case "length": return array(o.array as string) ? "the length of an array that grows" : undefined; + case "pop": return "pop()"; + case "running": return "a task's running"; + case "taskHandle": return "a task"; + case "edge": return `${o.edge as string}()`; + case "unitField": return `a unit's ${o.field as string}`; + case "unitPart": return "a unit's place in the game's memory"; + case "unitAlive": return "whether a unit is alive"; + case "unitSame": return "two units compared"; + case "unitFlag": return `a unit's flag ${o.flag as string}`; + case "unitNull": case "unitVar": case "pick": case "unitAt": case "selected": return "a unit"; + case "textLength": return "a text's length"; + case "textIndexOf": return "a search in a text"; + case "textCode": return "a character's code"; + case "textCompare": return "two texts compared"; + case "textTest": return `a text's ${o.test as string}`; + case "textSlice": return "a part of a text (slice)"; + case "textPad": return "a text padded (padStart, padEnd)"; + case "textRepeat": return "a text repeated"; + case "textTernary": return "a ?: of texts"; + case "template": return "a text made while the game runs"; + case "textCall": { + // (A method of a text the front end writes as a function of its own — `trim()` — or a function of the script's.) + const name = (o.call as { name: string }).name; + if (!TEXT_METHODS.has(name.split(".").pop()!)) return `a function that gives back a text (${name})`; + return name.includes(".") ? `${name}()` : `a text's ${name}()`; + } + case "textVar": case "textOf": case "textAt": case "built": + return "a text of the program's"; + case "ternary": { + const t = o.whenTrue as { kind: string; value?: unknown; id?: string }; + const boolean = boolOnly.has(t.kind) || typeof t.value === "boolean" || (t.kind === "var" && decls.get(t.id!)?.kind === "boolean"); + return boolean ? "a ?: of booleans" : undefined; + } + case "var": return typeof o.id === "string" ? variable(o.id) : undefined; + case "element": return array(o.array as string); + default: return undefined; + } + }; + // Statements: what each kind is, before what is in it. + const statement = (t: Stmt): string | undefined => { + switch (t.kind) { + case "action": { + if (t.record.type === SET_DEATHS_ACTION && t.record.player > LAST_GROUP) return "a write of memory (setMemory())"; + const name = `the action ${actionDef(t.record.type)?.name ?? t.record.type}`; + return t.variables?.length ? `${name}, its fields from values` : t.text ? `${name}, its text the program's` : name; + } + case "tableWrite": return `a write of ${t.cell.name}`; + case "centerLocation": return "a location moved (centerLocation)"; + case "select": return "select()"; + case "syncSend": return "sync()"; + case "push": return "push()"; + case "pop": return "pop()"; + case "setLength": return "an array's length set"; + case "assignText": case "storeText": case "releaseText": return "a text of the program's"; + case "textLoop": return "a loop over a text"; + case "buildText": return "JSX made while the game runs"; + case "assignUnit": return "a unit kept in a variable"; + case "unitLoop": return t.source ? `units an event gives (${t.source})` : "a loop over units"; + case "unitWrite": return `a unit's ${t.field} written`; + case "unitDo": return `a unit's verb (${t.verb.do})`; + case "start": { + // (`on.*` is a task that listens: say which.) + const on = t.listen?.source ? { created: "on.created", died: "on.died", killed: "on.killed", ordered: "on.order", morphed: "on.morphed" }[t.listen.source] : t.listen?.inputs?.length ? (t.listen.inputs[0].source === "chat" ? "on.chat" : "on.key") : undefined; + return on ? `an event handled (${on})` : "a task started"; + } + case "cancel": return "a task cancelled"; + case "parallel": return t.mode === "race" ? "Promise.race or Promise.any" : "Promise.all"; + case "try": return "try"; + case "throw": return "throw"; + case "uncaught": return "an error nothing catches"; + case "print": return t.position === "center" ? "a text at the centre of the screen" : undefined; + case "declare": case "assign": case "assignBool": return variable(t.kind === "declare" ? t.decl.id : t.target); + case "declareArray": case "store": return array(t.array); + default: return undefined; + } + }; + let found: string | undefined; + const walk = (x: unknown): void => { + if (found || !x || typeof x !== "object") return; + if (Array.isArray(x)) { x.forEach(walk); return; } + const o = x as Record; + // (A statement says what it is before what is in it; an expression after, so that the innermost part is named.) + const isStmt = "label" in o && typeof o.kind === "string" && STATEMENT_KINDS.has(o.kind); + if (isStmt) found = statement(o as unknown as Stmt); + // (What is read of a unit says more than the unit: `u.hp` is "a unit's hp", not "a unit kept in a variable".) + else if (typeof o.kind === "string" && UNIT_READS.has(o.kind)) found = node(o); + for (const [k, v] of Object.entries(o)) if (k !== "at" && k !== "original" && v && typeof v === "object") walk(v); + if (!found && !isStmt && typeof o.kind === "string") found = node(o); + }; + walk(s); + return found; +} + +/** What is read of a unit, and what is done to a text: named before the unit or the text it is done to. */ +const UNIT_READS = new Set(["unitField", "unitFlag", "unitAlive", "unitSame", "unitPart", "textLength", "textIndexOf", "textCode", "textCompare", "textTest", "textSlice", "textPad", "textRepeat", "textTernary", "textCall"]); +/** The methods of a text the front end writes as functions of its own (`textCall`). */ +const TEXT_METHODS = new Set(["toUpperCase", "toLowerCase", "toLocaleUpperCase", "toLocaleLowerCase", "trim", "trimStart", "trimEnd", "replace", "replaceAll", "split", "join", "at", "charAt", "concat", "parseInt", "Number"]); +/** Deaths and Set Deaths, which `memory()` and `setMemory()` are when their player is past the groups (26). */ +const DEATHS_CONDITION = 15, SET_DEATHS_ACTION = 45, LAST_GROUP = 26; + +/** The kinds of statement, told apart from the expressions of the same name (`pop`, `call`). */ +const STATEMENT_KINDS = new Set(["declare", "assignText", "storeText", "releaseText", "textLoop", "assign", "declareArray", "store", "push", "setLength", "assignBool", "assignUnit", "unitLoop", "unitWrite", "unitDo", "tableWrite", "if", "while", "do", "for", "unrolled", "switch", "break", "continue", "return", "sleep", "parallel", "start", "cancel", "action", "syncSend", "select", "centerLocation", "print", "buildText", "block", "try", "throw", "uncaught", "remark", "graph"]); + // ── what the emitter takes ── class Scope { @@ -114,6 +293,10 @@ class Scope { declared = new Set(); /** Why the last statement looked at was not taken. */ why = ""; + /** A map of our own: why each statement it refuses is refused (`island`), and whether that is a part it never takes. */ + whyOf = new Map(); + /** A map of our own: the slot of the program's owner, the current player while it runs. */ + owner: number | undefined; readonly decls: Map; constructor(decls: Map) { this.decls = decls; @@ -407,6 +590,9 @@ class Scope { * places of the graph's after it: its own writes, those of the functions it calls. */ island(s: Stmt): boolean { + // (A map of our own refuses every island, so whether the Python side could lower this one where it is — the walk + // below — is not the question: what it is, in a script's words, is.) + const named = this.native ? unlowered(s, this.decls, this.arrays) : undefined; let ok = true; // (`loops`: the loops around, inside the statement; `breaks`: those and the switches, which a break also ends.) const walk = (x: unknown, loops: number, breaks = loops): void => { @@ -428,7 +614,13 @@ class Scope { const inner = o.kind === "while" || o.kind === "for" || o.kind === "do" || o.kind === "unitLoop" || o.kind === "textLoop" || o.kind === "unrolled"; for (const v of Object.values(o)) if (v && typeof v === "object") walk(v, loops + (inner ? 1 : 0), breaks + (inner || o.kind === "switch" ? 1 : 0)); }; - walk(s, 0); + if (!named) walk(s, 0); + if (this.native) { + // (What in it the emitter never takes; else what the walk found, or the checks' own reason since the island before.) + this.whyOf.set(s, { why: named ?? this.why, never: named !== undefined }); + this.why = ""; + ok = true; + } if (!ok) return false; this.islands.set(s, writes(s, this.functions)); // (What value ranges know of what it stores, for the copies after it.) @@ -526,7 +718,12 @@ class Scope { case "print": // (Display Text shows to the current player, the owner in a map of our own: a text for anyone else is refused // until a print goes to each player of its group in turn, as the Python side's does.) - if (this.native && s.to !== PlayerGroup.CurrentPlayer) return this.no("a text shown to other players than the program's owner") || this.island(s); + if (this.native && s.to !== PlayerGroup.CurrentPlayer && s.to !== this.owner) { + // (TrigScript's own warning — a pool of closures or instances full — is shown to every player: say which.) + const first = s.parts[0]?.kind === "text" ? s.parts[0].text.replace(/^[\x01-\x1f]+/, "") : ""; + const warning = first.startsWith("TrigScript: ") ? first.slice(12).split(";")[0] : undefined; + return this.no(warning ? `a warning TrigScript shows every player ("${warning}")` : "a text shown to other players than the program's owner") || this.island(s); + } if (this.native && s.position === "chat" && s.parts.every((p) => this.printed(p))) return true; return this.island(s); case "call": return (this.native && this.call(s.call)) || this.island(s); @@ -3034,7 +3231,7 @@ export const emitterCounts = { hoisted: 0, reciprocals: 0, jumps: 0 }; * call of a function of the Python side's, and nothing read that the body did not write first (there is no one to * copy it in; a copy within the body is made of triggers by the layout). Returns the graph, or why it cannot be. */ -export function emitBody(body: Stmt[], decls: Map, key: string, functions = new Map(), arrays = new Map(), stackDepth = STACK_DEPTH): Graph | string { +export function emitBody(body: Stmt[], decls: Map, key: string, functions = new Map(), arrays = new Map(), stackDepth = STACK_DEPTH, owner?: number): Graph | string { // (Every statement must be the emitter's here, so a rewriting it would not take is refused whichever way it goes: // no trial is needed.) const take = () => true; @@ -3061,6 +3258,7 @@ export function emitBody(body: Stmt[], decls: Map, key: string, }; const rules = (scope: Scope) => { scope.native = true; + scope.owner = owner; scope.functions = functions; scope.arrays = arrays; scope.bodyOf = bodyOf; @@ -3073,10 +3271,16 @@ export function emitBody(body: Stmt[], decls: Map, key: string, rules(scope); if (!scope.stmt(s, false)) return scope.why || "?"; if (scope.islands.size) { - const first = [...scope.islands.keys()][0]; - return `a ${first.kind} it does not lower (${scope.why || "no reason given"})${first.at ? `, line ${first.at.line}` : ""}`; + // (The first that holds a part the emitter never takes, before one refused for how it is put together — a print to + // all players the front end adds, say, after a closure it cannot keep.) + const refused = [...scope.islands.keys()]; + const first = refused.find((s) => scope.whyOf.get(s)?.never) ?? refused[0]; + return `${scope.whyOf.get(first)?.why || `a ${first.kind} it does not lower`}${first.at ? `, line ${first.at.line}` : ""}`; + } + if (scope.calls.size) { + const [s, call] = [...scope.calls][0]; + return `a ${call.fn.fn.startsWith("fdiv") ? "quotient" : "product"} of fixed numbers${s.at ? `, line ${s.at.line}` : ""}`; } - if (scope.calls.size) return "a fixed product or quotient (a function of the Python side's)"; const make = (forced: Set): { e: Emitter; g: Graph | string } => { const e = new Emitter(key, scope); e.forced = forced; diff --git a/compiler/backend/own.ts b/compiler/backend/own.ts new file mode 100644 index 0000000..b12dd29 --- /dev/null +++ b/compiler/backend/own.ts @@ -0,0 +1,33 @@ +/** + * A map of our own, as far as the emitter goes: a compiled script's one program lowered whole by `emitBody`, or why + * it is not. `scripts/build-own.mts` makes the map from what this gives; the ledger (`tests/ledger.test.ts`) asks it + * of every feature, so that the two refuse the same. + */ +import type { CompileResult } from "../compiler"; +import type { ArrayDecl, FuncDecl, Program, VarDecl } from "../ir"; +import { emitBody } from "./emit"; +import type { Graph } from "./graph"; + +export function emitOwn(r: Pick): { program: Program; graph: Graph } | string { + if (r.ir.length !== 1) return `a map of our own has one program; this script has ${r.ir.length}`; + const program = r.ir[0]; + if (program.perPlayer) return "a map of our own has no per-player program yet"; + // (Its start-up is in the owner's triggers, and the game runs those of P1 to P8 only.) + if (program.owner >= 8) return `a program owned by P${program.owner + 1}: a map of our own starts a program from its owner's triggers, which the game runs for P1 to P8 only`; + // (A map of our own is built without the map's triggers, so `trigger()` records would be lost without a word.) + if (r.triggers.length) return `a map of our own keeps no trigger() or hyperTriggers() records yet; this script has ${r.triggers.length}`; + const decls = new Map(); + const collect = (x: unknown): void => { + if (Array.isArray(x)) x.forEach(collect); + else if (x && typeof x === "object") { + const o = x as Record; + if (o.decl && typeof o.decl === "object" && "id" in (o.decl as object)) decls.set((o.decl as VarDecl).id, o.decl as VarDecl); + for (const v of Object.values(o)) if (v && typeof v === "object") collect(v); + } + }; + collect(program); + const functions = new Map((program.functions ?? []).map((f) => [f.id, f])); + const arrays = new Map((program.arrays ?? []).map((a) => [a.id, a])); + const graph = emitBody(program.body, decls, "own", functions, arrays, undefined, program.owner); + return typeof graph === "string" ? graph : { program, graph }; +} diff --git a/scripts/build-own.mts b/scripts/build-own.mts index e593320..3ecc3ea 100644 --- a/scripts/build-own.mts +++ b/scripts/build-own.mts @@ -13,9 +13,8 @@ import { existsSync, readFileSync, writeFileSync } from "node:fs"; import { join, resolve } from "node:path"; import ts from "typescript"; import { compileScript } from "../compiler/compiler"; -import { emitBody } from "../compiler/backend/emit"; +import { emitOwn } from "../compiler/backend/own"; import { STRINGS, layout } from "../compiler/backend/layout"; -import type { ArrayDecl, FuncDecl, VarDecl } from "../compiler/ir"; import type { Graph } from "../compiler/backend/graph"; import { defaultScriptNames } from "../compiler/names"; import { defaultLib } from "../bundle/lib.mjs"; @@ -30,27 +29,10 @@ if (!existsSync(map)) { console.error(`no map at ${map}`); process.exit(2); } const r = compileScript(ts, scriptFiles(scriptPath), defaultScriptNames(), { lib: defaultLib(), emitter: false }); if (r.diagnostics.length) { for (const d of r.diagnostics) console.error(`${d.file}:${d.line}:${d.column} ${d.message}`); process.exit(1); } -if (r.ir.length !== 1) { console.error(`a map of our own has one program; this script has ${r.ir.length}`); process.exit(1); } -const program = r.ir[0]; -if (program.perPlayer) { console.error("a map of our own has no per-player program yet"); process.exit(1); } -// (The map's triggers are cleared below, so `trigger()` records would be lost without a word.) -if (r.triggers.length) { console.error(`a map of our own keeps no trigger() records yet; this script has ${r.triggers.length}`); process.exit(1); } const compiled = performance.now(); - -const decls = new Map(); -const collect = (x: unknown): void => { - if (Array.isArray(x)) x.forEach(collect); - else if (x && typeof x === "object") { - const o = x as Record; - if (o.decl && typeof o.decl === "object" && "id" in (o.decl as object)) decls.set((o.decl as VarDecl).id, o.decl as VarDecl); - for (const v of Object.values(o)) if (v && typeof v === "object") collect(v); - } -}; -collect(program); -const functions = new Map((program.functions ?? []).map((f) => [f.id, f])); -const arrays = new Map((program.arrays ?? []).map((a) => [a.id, a])); -const graph = emitBody(program.body, decls, "own", functions, arrays); -if (typeof graph === "string") { console.error(`the emitter cannot lower this program: ${graph}`); process.exit(1); } +const lowered = emitOwn(r); +if (typeof lowered === "string") { console.error(`the emitter cannot lower this program: ${lowered}`); process.exit(1); } +const { program, graph } = lowered; const emitted = performance.now(); const EDITOR = resolve(process.env.SCMJS_DIR ?? join(root, "..", "scm-js")); From d87284f66817a13da56152e98eeddf2a842586ee Mon Sep 17 00:00:00 2001 From: Jeany Date: Mon, 19 Oct 2026 23:23:59 -0600 Subject: [PATCH 2/6] A field read straight off a pick is the game's, not undefined when the script is built MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `print(`${first({ owner: P1 })?.hp}`)` printed "undefined", `if (first(…)?.cloaked)` compiled to nothing, and `n = first(…)?.hp ?? 5` to `n = 5`: the evaluator worked the field out of the pick object itself. A field of a pick is read while the game runs now, as one of `local.selected` already was. Both builds had it. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01T1CM9pLuTuAn3mgTEkjgVg --- compiler/structured.ts | 5 +++-- tests/units.test.ts | 9 +++++++++ 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/compiler/structured.ts b/compiler/structured.ts index 1aeed4d..b223ecb 100644 --- a/compiler/structured.ts +++ b/compiler/structured.ts @@ -806,8 +806,9 @@ export class Structured { } const obj = sub(e.expression); if (!obj || obj.value === null || obj.value === undefined) return undefined; - // A field of the unit selected (\`local.selected.hp\`) is read when the map is played, as any unit's is. - if (isSelected(obj.value) || isSelectionCount(obj.value)) return undefined; + // A field of the unit selected (\`local.selected.hp\`) is read when the map is played, as any unit's is — and so is + // one of a unit picked (`first(…)?.hp`), which, worked out here, was the pick's own field: undefined. + if (isSelected(obj.value) || isSelectionCount(obj.value) || isUnitPick(obj.value)) return undefined; return { value: readField(obj.value, e.name.text) }; } if (ts.isElementAccessExpression(e)) { diff --git a/tests/units.test.ts b/tests/units.test.ts index 42854a3..4556886 100644 --- a/tests/units.test.ts +++ b/tests/units.test.ts @@ -46,6 +46,15 @@ describe("units: the front end", () => { expect(test).toMatchObject({ kind: "if", cond: { kind: "unitAlive" }, then: [{ kind: "unitDo", verb: { do: "order", order: "move", target: 1 } }] }); expect(r.variables.map((v) => `${v.name}:${v.kind}`)).toEqual(["u:unit", "t:unit"]); }); + it("a field or a flag read straight off a pick is the game's, not worked out when the script is built", () => { + // (It was: `first(…)?.hp` printed "undefined", and `if (first(…)?.cloaked)` left nothing — the pick's own field.) + const r = compile(`print(\`\${first({ owner: P1 })?.hp}\`); if (nearest(units.TerranMarine, ${at(2)})?.burrowed) print("b"); let n = first({ owner: P2 })!.hp + 1;`); + const [print, test, decl] = r.ir[0].body as Stmt[]; + expect(print).toMatchObject({ kind: "print", parts: [{ kind: "number", expr: { kind: "unitField", field: "hp", unit: { kind: "pick", by: "first" } } }] }); + expect(test).toMatchObject({ kind: "if", cond: { kind: "unitFlag", flag: "burrowed", unit: { kind: "pick", by: "nearest" } } }); + expect(decl).toMatchObject({ kind: "declare", init: { kind: "binary", op: "+", left: { kind: "unitField", field: "hp", unit: { kind: "pick" } } } }); + expect(run(`let n = 0; n = first({ owner: P1 })!.hp;`).value("n")).toBe(40); + }); it("Anywhere and Any Unit are no filter at all", () => { const r = compile(`for (const u of unitsAt(locations.Anywhere, { type: units.AnyUnit })) u.kill();`); expect(r.ir[0].body[0]).toMatchObject({ kind: "unitLoop", filter: {} }); From 9b9c3da460880637ac16b56865e62f2e72ae51cf Mon Sep 17 00:00:00 2001 From: Jeany Date: Mon, 19 Oct 2026 23:24:00 -0600 Subject: [PATCH 3/6] The action order() compiles inside a program: it is not the event on.order MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit GAME_CALLS took every on.* event's name, so the library's own action `order(…)` was never worked out when the script was built, and a program using it failed with "A call's arguments must be known". An event's name is the game's only as `on.order(…)` or a unit's `u.order(…)`. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01T1CM9pLuTuAn3mgTEkjgVg --- compiler/hoist.ts | 20 +++++++++++++++++--- tests/events.test.ts | 8 ++++++++ 2 files changed, 25 insertions(+), 3 deletions(-) diff --git a/compiler/hoist.ts b/compiler/hoist.ts index dd24184..f0c1928 100644 --- a/compiler/hoist.ts +++ b/compiler/hoist.ts @@ -151,6 +151,20 @@ export function clockMessage(call: string): string { /** The library calls the game answers: never hoisted, the structured compiler lowers them. */ const GAME_CALLS = new Set(["random", "sleep", "nextFrame", "until", "sync", "select", "start", "rose", "once", "shared", "lines", "centerLocation", ...ON_EVENTS]); +const EVENT_NAMES: ReadonlySet = new Set(ON_EVENTS); +/** + * Whether a library name is one of `GAME_CALLS`. An event's name is the game's as `on.order(…)` or a unit's + * `u.order(…)`, not as the action of the same name, `order(…)`, a function of the library's own whose arguments the + * script works out as any action's. + */ +function answeredByGame(ts: typeof TS, checker: TS.TypeChecker, id: TS.Identifier, lib: string): boolean { + if (!GAME_CALLS.has(lib)) return false; + if (!EVENT_NAMES.has(lib)) return true; + let sym = checker.getSymbolAtLocation(id); + if (sym && sym.flags & ts.SymbolFlags.Alias) sym = checker.getAliasedSymbol(sym); + const decl = sym?.valueDeclaration ?? sym?.declarations?.[0]; + return !(decl && ts.isFunctionDeclaration(decl)); +} /** * Calls that read the game — `minerals(P1)`, and a comparing condition without its comparison * (`deaths(P1, unit)`) — or show text a program fills in (`print`). The call is never a @@ -311,7 +325,7 @@ export function usesGame(ts: typeof TS, checker: TS.TypeChecker, fn: TS.Node, se if (clockCall(ts, checker, n)) { found = true; return; } if (ts.isIdentifier(n)) { const lib = libraryName(ts, checker, n); - if (lib && (GAME_CALLS.has(lib) || READ_CALLS.has(lib) || READ_ARITY.has(lib) || GAME_CELL_ROOTS.has(lib))) { found = true; return; } + if (lib && (answeredByGame(ts, checker, n, lib) || READ_CALLS.has(lib) || READ_ARITY.has(lib) || GAME_CELL_ROOTS.has(lib))) { found = true; return; } const decl = declarationOf(ts, checker, n); const other = decl && decl !== fn && (isScriptFunction(ts, decl) ? decl : scriptArrow(ts, decl) ?? (classMember(ts, decl) ? decl : undefined)); if (other && usesGame(ts, checker, other, seen)) { found = true; return; } @@ -577,7 +591,7 @@ function planWithin(ts: typeof TS, checker: TS.TypeChecker, plan: ProgramPlan, a const property = ts.isPropertyAccessExpression(p) && p.name === n; if ((ts.isPropertyAssignment(p) && p.name === n) || (ts.isMethodDeclaration(p) && p.name === n) || ts.isQualifiedName(p)) return; const lib = libraryName(ts, checker, n); - if (lib && GAME_CALLS.has(lib)) { ok = false; return; } + if (lib && answeredByGame(ts, checker, n, lib)) { ok = false; return; } if (lib && FORBIDDEN_INSIDE.has(lib) && ts.isCallExpression(p.parent) && p.parent.expression === p && property) { ok = false; return; } if (lib && FORBIDDEN_INSIDE.has(lib) && ts.isCallExpression(p) && p.expression === n) { ok = false; return; } if (property) return; @@ -679,7 +693,7 @@ function planWithin(ts: typeof TS, checker: TS.TypeChecker, plan: ProgramPlan, a let callee: TS.Expression = e.expression; while (ts.isParenthesizedExpression(callee)) callee = callee.expression; if (isTaskFunction(ts, checker, callee)) callback(callee, items); - else if (!lib || !GAME_CALLS.has(lib)) value(ts.isCallExpression(e) && ts.isPropertyAccessExpression(e.expression) ? e.expression.expression : e.expression, items); + else if (!lib || !answeredByGame(ts, checker, ts.isPropertyAccessExpression(callee) ? callee.name as TS.Identifier : callee as TS.Identifier, lib)) value(ts.isCallExpression(e) && ts.isPropertyAccessExpression(e.expression) ? e.expression.expression : e.expression, items); for (const a of e.arguments ?? []) { let fn: TS.Expression = a; while (ts.isParenthesizedExpression(fn)) fn = fn.expression; diff --git a/tests/events.test.ts b/tests/events.test.ts index 9165757..4435439 100644 --- a/tests/events.test.ts +++ b/tests/events.test.ts @@ -128,6 +128,14 @@ describe("events, second part: killed, order, morphed, at the top of a script", expect(printed(s)).toEqual(["1:died"]); }); + it("the action order() is not the event on.order: its arguments are worked out when the script is built", () => { + // (It was refused inside a program — "A call's arguments must be known" — the event's name taken for the action's.) + const r = compile(`order(P1, units.TerranMarine, HOME, HOME, "attack"); const u = first({ owner: P1 }); if (u) u.order("move", HOME);`); + const [action, , test] = r.ir[0].body as Stmt[]; + expect(action).toMatchObject({ kind: "action", record: { type: ActionType.Order, modifier: 2, location: 1, target: 1 } }); + expect(test).toMatchObject({ kind: "if", then: [{ kind: "unitDo", verb: { do: "order", order: "move" } }] }); + }); + it("on.order hands the unit, the order it took up and the one it had; a filter on the order", () => { const r = compile(` on.order((u, order, was) => { print(\`\${u.type} \${was} -> \${order}\`); }); From de894951e3ba10b84faff952d9b6ef9720c51ce2 Mon Sep 17 00:00:00 2001 From: Jeany Date: Mon, 19 Oct 2026 23:24:00 -0600 Subject: [PATCH 4/6] music.loop(song) compiles: the wait at the end of each turn is the loop's own `while (true) await play(s)` showed the loop no sleep of its own, so `start(music.loop(song))`, the module's own example, never compiled. The loop plays the song itself and waits at least a frame at the end of each turn. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01T1CM9pLuTuAn3mgTEkjgVg --- compiler/generated/stdlib.ts | 2 +- stdlib/music.ts | 19 ++++++++++++++++++- tests/music.test.ts | 16 ++++++++++++++++ 3 files changed, 35 insertions(+), 2 deletions(-) diff --git a/compiler/generated/stdlib.ts b/compiler/generated/stdlib.ts index bd2c35c..a0a8229 100644 --- a/compiler/generated/stdlib.ts +++ b/compiler/generated/stdlib.ts @@ -3,7 +3,7 @@ export const STDLIB_SOURCES: Readonly> = { "cutscene": "/**\n * Scenes acted out in the game: the standard library's \"trigscript/cutscene\" (TrigScript 4).\n *\n * import { Camera, walk, line, play, SECOND } from \"trigscript/cutscene\";\n *\n * async function intro(raider: Unit) {\n * const cam = new Camera(raider.x, raider.y);\n * await line(units.JimRaynorMarine, \"Something's moving out there.\", 3 * SECOND);\n * const there = walk(raider, 1200, 640);\n * await cam.follow(raider, 2 * SECOND);\n * await there;\n * await cam.pan(1600, 400, 2 * SECOND);\n * await cam.shake(12);\n * }\n *\n * program(async () => {\n * const raider = first(unitsAt(locations.Base, { owner: P1, type: units.TerranMarine }));\n * if (!raider) return;\n * const skipped = await play(P1, start(intro(raider))); // Escape skips the rest of it\n * if (skipped) print(\"Skipped.\");\n * });\n *\n * Everything here is what a trigger can do, done a frame at a time: the camera is Center View on a location the library\n * keeps for itself, moved to where the camera should look; a unit walks by a Move order to another; a line is the\n * speaker's portrait talking and the words in the middle of the screen. Places are pixels (32 a tile), (0, 0) the map's\n * top left; time is frames of the game, 24 a second on Fastest (`SECOND`). The camera is the one of the player the\n * program runs as, as Center View's is.\n */\nimport { centerLocation, centerView, reserveLocation, talkingPortrait, print, sleep, frames, nextFrame, keyPressed, type Player, type Task, type Unit, type UnitType } from \"trigscript\";\nimport { easeInOutQuad, easeInQuad, easeOutQuad, easeOutBack } from \"trigscript/math\";\n\n/** Frames a second of game time on Fastest, the speed a map is played at. */\nexport const SECOND = 24;\n\nconst VIEW = reserveLocation(\"cutscene: where the camera looks\");\nconst GOAL = reserveLocation(\"cutscene: where a unit is sent\");\n\n/** How a glide speeds up and slows down: evenly, starting slow, ending slow, both, or ending past the mark and back. */\nexport type Ease = \"linear\" | \"in\" | \"out\" | \"inOut\" | \"back\";\n\n/** How far along a glide is at `t` (0 at its start, 1 at its end), as `ease` has it. */\nfunction eased(t: fixed, ease: Ease): fixed {\n if (ease === \"in\") return easeInQuad(t);\n if (ease === \"out\") return easeOutQuad(t);\n if (ease === \"inOut\") return easeInOutQuad(t);\n if (ease === \"back\") return easeOutBack(t);\n return t;\n}\n\n/**\n * The camera of the player the program runs as: where it looks, and how it gets somewhere else. It looks at its first\n * place as soon as it is made.\n */\nexport class Camera {\n /** The point the camera is centred on, in pixels. */\n x: number;\n y: number;\n\n constructor(x: number, y: number) {\n this.x = x;\n this.y = y;\n centerLocation(VIEW, x, y);\n centerView(VIEW);\n }\n\n /** Look at a point, now. */\n look(x: number, y: number) {\n this.x = x;\n this.y = y;\n centerLocation(VIEW, x, y);\n centerView(VIEW);\n }\n\n /**\n * Glide to a point over `time` frames, looking at each place on the way a frame at a time.\n * @param ease How it speeds up and slows down; \"inOut\" unless it says.\n */\n async pan(x: number, y: number, time: number, ease: Ease = \"inOut\") {\n const fromX = this.x;\n const fromY = this.y;\n let i = 1;\n while (i <= time) {\n const k = eased(fixed(i) / time, ease);\n this.look(fromX + Math.round((x - fromX) * k), fromY + Math.round((y - fromY) * k));\n await nextFrame();\n i++;\n }\n this.look(x, y);\n }\n\n /** Keep a unit in the middle of the screen for `time` frames, or until it is gone. */\n async follow(u: Unit, time: number) {\n let i = 0;\n while (i < time && u) {\n this.look(u.x, u.y);\n await nextFrame();\n i++;\n }\n }\n\n /** Shake for `time` frames, `size` pixels each way (8 unless it says), and settle where it was. */\n async shake(time: number, size: number = 8) {\n const x = this.x;\n const y = this.y;\n let i = 0;\n while (i < time) {\n const left = (i % 4) < 2;\n const up = (i % 2) === 0;\n centerLocation(VIEW, left ? x - size : x + size, up ? y - size : y + size);\n centerView(VIEW);\n await nextFrame();\n i++;\n }\n this.look(x, y);\n }\n}\n\n/**\n * Send a unit to a point by a Move order, and wait until it is there — within `near` pixels (48 unless it says) — or\n * `limit` frames have passed (30 seconds unless it says), or it is gone.\n * @returns Whether it got there.\n */\nexport async function walk(u: Unit, x: number, y: number, near: number = 48, limit: number = 30 * SECOND): Promise {\n return go(u, x, y, \"move\", near, limit);\n}\n\n/** The same with an Attack order: the unit fights what it meets on the way. */\nexport async function charge(u: Unit, x: number, y: number, near: number = 48, limit: number = 30 * SECOND): Promise {\n return go(u, x, y, \"attack\", near, limit);\n}\n\nasync function go(u: Unit, x: number, y: number, how: \"move\" | \"attack\", near: number, limit: number): Promise {\n centerLocation(GOAL, x, y);\n u.order(how, GOAL);\n let waited = 0;\n while (u && Math.abs(u.x - x) + Math.abs(u.y - y) > near && waited < limit) {\n await nextFrame();\n waited++;\n }\n return !!u && Math.abs(u.x - x) + Math.abs(u.y - y) <= near;\n}\n\n/**\n * Send every unit of a group to a point together, and wait until each is there, gone, or `limit` frames have passed.\n * @returns How many got there.\n */\nexport async function walkAll(group: Unit[], x: number, y: number, near: number = 64, limit: number = 30 * SECOND): Promise {\n centerLocation(GOAL, x, y);\n for (const u of group) if (u) u.order(\"move\", GOAL);\n let waited = 0;\n let there = 0;\n while (waited < limit) {\n there = 0;\n let moving = 0;\n for (const u of group) {\n if (!u) continue;\n if (Math.abs(u.x - x) + Math.abs(u.y - y) <= near) there++;\n else moving++;\n }\n if (moving === 0) break;\n await nextFrame();\n waited++;\n }\n return there;\n}\n\n/**\n * A line of the scene: the speaker's portrait talks and the words show in the middle of the screen, for `time` frames\n * (3 seconds unless it says), and then the scene goes on.\n */\nexport async function line(speaker: UnitType, text: string, time: number = 3 * SECOND) {\n talkingPortrait(speaker, Math.round((time * 1000) / SECOND));\n print(text, { position: \"center\" });\n await sleep(frames(time));\n}\n\n/**\n * Play a scene, started as a task (`start(intro())`), for a player who may skip it: when they press `key` (Escape unless\n * it says) the scene stops where it is. Put things where the scene would have left them when it gives true.\n * @returns Whether it was skipped.\n */\nexport async function play(player: Player, scene: Task, key: \"Escape\" | \"Space\" | \"Enter\" = \"Escape\"): Promise {\n while (scene.running) {\n if (keyPressed(player, key)) {\n scene.cancel();\n return true;\n }\n await nextFrame();\n }\n return false;\n}\n", "fog": "/**\n * Fog of war as a picture: the standard library's \"trigscript/fog\" (TrigScript 4).\n *\n * import { fog } from \"trigscript/fog\";\n *\n * const heart = fog.picture([\".##.##.\", \"#######\", \".#####.\", \"..###..\"]);\n *\n * program(async () => {\n * fog.reveal(P1); // the whole map lit once: from now on dark is fog, not black\n * while (true) {\n * fog.light(P1, 30, 20, 5); // a disc of size 5 around tile (30, 20), this frame only\n * fog.light(P1, 60, 40, heart); // a picture, its top left on tile (60, 40)\n * await nextFrame();\n * }\n * });\n *\n * The fog is a canvas that starts every frame dark: what a program lights this frame is what shows this frame, and\n * nothing else needs doing. There is no dark brush — the game's sight only ever reveals — so dark is whatever is not\n * lit; to draw dark shapes, light around them (a picture with `{ ink: \"dark\" }`). Dark is also the sharper ink: a lit\n * part is at least a brush wide (3 tiles across, 4 down), while a dark line one tile wide stays exactly that. Nothing\n * else needs doing — the library writes the game's vision countdown after every frame (memory.vision.countdown), so\n * the next frame starts from a wiped board. Watch it on the minimap. With Classic graphics (Options > Graphics) a tile\n * of the map is a pixel there, sharp; Remastered graphics blurs it a few tiles wide.\n *\n * What the board is made of decides how far apart lit and fog look. Remastered draws a tile on the minimap as the\n * average of its art and fog as 0.64 of lit, whatever the tile, so the brightest art is the best board there: plain\n * Ice snow (lit 130 against fog 82, measured 2026-10-19). Classic draws a tile as one colour of its own; Badlands\n * tile 23778 is the widest apart there, and dim and textured in Remastered. Snow played the better of the two.\n *\n * How it draws, played in StarCraft: Remastered (2026-10-17): a brush is a unit made on a tile and taken away in the\n * same trigger pass; the tiles its sight revealed stay lit through the frame. A brush's size is the unit's sight\n * range, so each size is a unit type of its own — the flying heroes below, which no ordinary map places — whose sight\n * is set every frame (Remastered takes a sight write a frame late, so one type cannot change size within a frame).\n * Coordinates are tiles, (0, 0) the map's top left. About 800 brushes a frame is the most the game makes: it keeps a\n * removed unit's place until the next frame.\n */\nimport { game, program, sleep, frames, stats, units, createUnit, removeUnit, centerLocation, reserveLocation, memory, cover, nextFrame, type Player, type UnitType, type Video } from \"trigscript\";\n\n/**\n * Each brush's shape, sizes 1 to 11: the tiles a flying unit of that sight range reveals, read out of the game.\n * \"@\" is the tile the brush is centred on. The game's air sight leaves the centre's row one short below, so every\n * shape reaches a row further up than down.\n */\nexport const SHAPES: readonly (readonly string[])[] = [\n // 1\n [\"###\", \"###\", \"#@#\", \"###\"],\n // 2\n [\".###.\", \"#####\", \"#####\", \"##@##\", \"#####\", \"#####\"],\n // 3\n [\"..###..\", \"#######\", \"#######\", \"#######\", \"###@###\", \"#######\", \"#######\", \"#######\"],\n // 4\n [\"..#####...\", \".#######..\", \"#########.\", \"##########\", \"##########\", \"####@#####\", \"##########\", \"##########\", \"#########.\", \".#######..\", \"......#...\"],\n // 5\n [\"...#####....\", \"..#######...\", \".#########..\", \"###########.\", \"############\", \"############\", \"#####@######\", \"############\", \"############\", \"###########.\", \".#########..\", \"..#######...\", \".......#....\"],\n // 6\n [\"....#####.....\", \"..#########...\", \".###########..\", \"#############.\", \"#############.\", \"##############\", \"##############\", \"######@#######\", \"##############\", \"##############\", \"#############.\", \"#############.\", \".###########..\", \"..#########...\", \"........#.....\"],\n // 7\n [\".....#####......\", \"...#########....\", \"..###########...\", \".#############..\", \"###############.\", \"###############.\", \"################\", \"################\", \"#######@########\", \"################\", \"################\", \"###############.\", \"###############.\", \".#############..\", \"..###########...\", \"...#########....\", \".........#......\"],\n // 8\n [\"......#####.......\", \"....#########.....\", \"..#############...\", \".###############..\", \".###############..\", \"#################.\", \"#################.\", \"##################\", \"##################\", \"########@#########\", \"##################\", \"##################\", \"#################.\", \"#################.\", \".###############..\", \".###############..\", \"..#############...\", \"....#########.....\", \"..........#.......\"],\n // 9\n [\"......#######.......\", \"....###########.....\", \"...#############....\", \"..###############...\", \".#################..\", \"###################.\", \"###################.\", \"####################\", \"####################\", \"####################\", \"#########@##########\", \"####################\", \"####################\", \"####################\", \"###################.\", \"###################.\", \".#################..\", \"..###############...\", \"...#############....\", \"....###########.....\", \"........#####.......\"],\n // 10\n [\".......#######........\", \".....###########......\", \"....#############.....\", \"..#################...\", \"..#################...\", \".###################..\", \"#####################.\", \"#####################.\", \"######################\", \"######################\", \"######################\", \"##########@###########\", \"######################\", \"######################\", \"######################\", \"#####################.\", \"#####################.\", \".###################..\", \"..#################...\", \"..#################...\", \"....#############.....\", \".....###########......\", \".........#####........\"],\n // 11\n [\"........#######.........\", \"......###########.......\", \"....###############.....\", \"...#################....\", \"..###################...\", \".#####################..\", \".#####################..\", \"#######################.\", \"#######################.\", \"########################\", \"########################\", \"########################\", \"###########@############\", \"########################\", \"########################\", \"########################\", \"#######################.\", \"#######################.\", \".#####################..\", \".#####################..\", \"..###################...\", \"...#################....\", \"....###############.....\", \"......###########.......\", \"..........#####.........\"],\n];\n\n/** Each size's unit, size 1 first: flying heroes without a sight upgrade. A map that places one gets odd sight from it. */\nconst BRUSHES: readonly UnitType[] = [\n units.KukulzaMutalisk, units.KukulzaGuardian, units.YggdrasillOverlord, units.MagellanScienceVessel,\n units.GantrithorCarrier, units.HyperionBattlecruiser, units.NoradIIBattlecruiser, units.TomKazanskyWraith,\n units.DanimothArbiter, units.MojoScout, units.ArtanisScout,\n];\n\n/** Where each brush is made: a location the map does not use, taken when the script is built. */\nconst SPOT = reserveLocation(\"fog: where each brush is made\");\n\n/**\n * One disc: the tiles a brush of `size` covers (SHAPES), centred on tile (x, y). What light() does with a size. A brush\n * is a unit made where SPOT is and taken away again at once; each branch makes its own size's, so that the unit type\n * of the action is known when the script is built (one given by the program costs writing it into the action).\n */\nconst disc = game((player: Player, x: number, y: number, size: number) => {\n discPx(player, x * 32 + 16, y * 32 + 16, size);\n});\n\n/**\n * disc() in pixels, (px, py) the centre of the tile: what a caller with many brushes uses, its products by 32 worked\n * out once and the brushes' offsets added (a product by 32 of a number the game holds is a test a bit, about 0.4 µs).\n */\nconst discPx = game((player: Player, px: number, py: number, size: number) => {\n centerLocation(SPOT, px, py, 32, 32);\n switch (size) {\n case 0: case 1: createUnit(player, BRUSHES[0], 1, SPOT); removeUnit(player, BRUSHES[0]); break;\n case 2: createUnit(player, BRUSHES[1], 1, SPOT); removeUnit(player, BRUSHES[1]); break;\n case 3: createUnit(player, BRUSHES[2], 1, SPOT); removeUnit(player, BRUSHES[2]); break;\n case 4: createUnit(player, BRUSHES[3], 1, SPOT); removeUnit(player, BRUSHES[3]); break;\n case 5: createUnit(player, BRUSHES[4], 1, SPOT); removeUnit(player, BRUSHES[4]); break;\n case 6: createUnit(player, BRUSHES[5], 1, SPOT); removeUnit(player, BRUSHES[5]); break;\n case 7: createUnit(player, BRUSHES[6], 1, SPOT); removeUnit(player, BRUSHES[6]); break;\n case 8: createUnit(player, BRUSHES[7], 1, SPOT); removeUnit(player, BRUSHES[7]); break;\n case 9: createUnit(player, BRUSHES[8], 1, SPOT); removeUnit(player, BRUSHES[8]); break;\n case 10: createUnit(player, BRUSHES[9], 1, SPOT); removeUnit(player, BRUSHES[9]); break;\n default: createUnit(player, BRUSHES[10], 1, SPOT); removeUnit(player, BRUSHES[10]);\n }\n});\n\n/**\n * Light the whole map once, so that from now on a tile left dark shows as fog (grey) rather than as never seen\n * (black) — every picture then has the same two tones. Takes one frame.\n * @param player Whose fog: P1 … P8, known when the script is built.\n */\nexport const reveal = game((player: Player) => {\n const width = memory.map.width;\n const height = memory.map.height;\n // Size 11 is 23 tiles wide: a brush every 14 tiles leaves no gap, and one more past the end, kept inside, reaches the edge.\n for (let y = 2; y < height + 12; y += 14) {\n for (let x = 2; x < width + 12; x += 14) disc(player, Math.min(x, width - 3), Math.min(y, height - 3), 11);\n }\n});\n\n/**\n * Light a rectangle of tiles for this frame, worked out while the game runs: its top left on tile (x, y), `width`\n * tiles across and `height` down. Every one of the five may change from frame to frame — the walls of a raycaster, a\n * bar of a chart. It is exact from 3 × 4 up (the smallest brush); a thinner one is lit a brush wide, centred.\n *\n * fog.rect(P1, 10, 20, 3, wall); // a column 3 tiles wide, `wall` tall\n *\n * @param player Whose fog: P1 … P8, known when the script is built.\n * @param x The column of its left edge.\n * @param y The row of its top edge.\n * @param width How many tiles across.\n * @param height How many tiles down.\n */\nexport const rect = game((player: Player, x: number, y: number, width: number, height: number) => {\n if (width < 1 || height < 1) return;\n // Size 1 is 3 tiles across and 4 down, its centre the middle column and the third row: a grid of them, the last\n // column and row moved back to end on the edge (overlapping the one before), covers the rectangle exactly.\n const right = Math.max(x + width - 2, x + 1);\n const bottom = Math.max(y + height - 2, y + 2);\n // (The brush made here, not through disc(): its unit type is then known when the script is built, which makes the\n // action a constant — a size from the program costs a branch and a written field a brush.)\n let cy = y + 2;\n while (true) {\n let cx = x + 1;\n while (true) {\n centerLocation(SPOT, cx * 32 + 16, cy * 32 + 16, 32, 32);\n createUnit(player, BRUSHES[0], 1, SPOT);\n removeUnit(player, BRUSHES[0]);\n if (cx >= right) break;\n cx = Math.min(cx + 3, right);\n }\n if (cy >= bottom) break;\n cy = Math.min(cy + 4, bottom);\n }\n});\n\n/** A picture for the fog, made when the script is built: its size in tiles and the brushes that draw it. */\nexport interface Picture {\n readonly width: number;\n readonly height: number;\n readonly brushes: readonly { readonly x: number; readonly y: number; readonly size: number }[];\n}\n\n/**\n * A picture from rows of text, worked out when the script is built: `#` is the ink, anything else is not.\n *\n * fog.picture([\"..##..\", \".####.\", \"######\"]) // light ink: the # tiles are lit\n * fog.picture([\"..##..\", \".####.\", \"######\"], { ink: \"dark\" }) // dark ink: the # tiles stay dark, the rest is lit\n *\n * With light ink a lit part thinner than a brush (3 tiles across, 4 down) is drawn a tile or two thicker. An image's\n * halftone (`fog.picture(photo.halftone(6))`) is drawn with the brushes it was made of, one a cell. With dark\n * ink every tile of the picture's rectangle that is not `#` is lit and the `#` tiles keep their shape exactly,\n * however thin — the ink for line art and silhouettes.\n */\nexport function picture(rows: readonly string[], options: { readonly ink?: \"light\" | \"dark\" } = {}): Picture {\n const height = rows.length;\n const width = Math.max(0, ...rows.map((r) => r.length));\n // An image's halftone (image.halftone(6)) comes with the brushes that drew it: a brush a cell, as they are.\n const drawn = (rows as { readonly brushes?: readonly { readonly x: number; readonly y: number; readonly shape: number }[] }).brushes;\n if (drawn && options.ink !== \"dark\") return Object.freeze({ width, height, brushes: Object.freeze(drawn.map((b) => ({ x: b.x, y: b.y, size: b.shape + 1 }))) });\n // Dark ink: the picture's rectangle lit but for the # tiles, so what is covered is the rest.\n const ink = options.ink === \"dark\" ? Array.from({ length: height }, (_, y) => Array.from({ length: width }, (_, x) => (rows[y][x] === \"#\" ? \".\" : \"#\")).join(\"\")) : rows;\n const brushes = cover(ink, SHAPES).map((b) => ({ x: b.x, y: b.y, size: b.shape + 1 }));\n return Object.freeze({ width, height, brushes: Object.freeze(brushes) });\n}\n\n/**\n * A video's frames as brushes, worked out when the script is built: a word a brush (x | y << 8 | size << 16), the\n * frames one after another, and where each one starts — the tables the game reads a frame from. Once a build.\n */\ninterface Film { readonly words: readonly number[]; readonly starts: readonly number[] }\nconst films = new Map();\nfunction film(video: Video): Film {\n const had = films.get(video);\n if (had) return had;\n const words: number[] = [], starts: number[] = [0];\n for (const brushes of cover(video, SHAPES)) {\n for (const b of brushes) words.push(b.x | (b.y << 8) | ((b.shape + 1) << 16));\n starts.push(words.length);\n }\n const made = { words, starts };\n films.set(video, made);\n return made;\n}\n\n/**\n * Light the fog for this frame: a disc, a picture or a frame of a video. Tiles count from the map's top left, (0, 0).\n *\n * fog.light(P1, 30, 20, 5); // a disc of size 5 centred on tile (30, 20)\n * fog.light(P1, 60, 40, heart); // a picture (fog.picture), its top left on tile (60, 40)\n * fog.light(P1, 0, 0, badApple, f); // frame f of a video the script imports, its top left on tile (0, 0)\n *\n * @param player Whose fog: P1 … P8, known when the script is built.\n * @param x The tile's column: a disc's centre, a picture's left edge.\n * @param y The tile's row: a disc's centre, a picture's top edge.\n * @param shape A size, 1 (3 tiles wide) to 11 (23 wide) — it may change while the game runs — a picture, or a video.\n * @param frame Of a video, which frame, from 0: it may change while the game runs. Past the end, nothing is lit.\n */\nexport const light = game((player: Player, x: number, y: number, shape: number | Picture | Video, frame: number = 0) => {\n if (typeof shape === \"number\") disc(player, x, y, shape);\n else {\n // The picture's corner in pixels once; each brush is its offset from there (a constant of a picture).\n const px = x * 32 + 16, py = y * 32 + 16;\n if (\"brushes\" in shape) for (const b of shape.brushes) discPx(player, px + b.x * 32, py + b.y * 32, b.size);\n else {\n const { words, starts } = film(shape);\n const end = starts[frame + 1];\n for (let i = starts[frame]; i < end; i++) {\n const w = words[i];\n // (A row's offset in pixels straight from its byte: the same as ((w >> 8) & 255) * 32, and the shift only has\n // the byte's eight bits to look at.)\n discPx(player, px + (w & 255) * 32, py + ((w & 0xff00) >> 3), w >> 16);\n }\n }\n }\n});\n\n/**\n * Play a video in the fog, a frame of it every frame of the game, from its first to its last; resolves when it ends.\n * Inside an async program: `await fog.play(P1, badApple)`. On Fastest a game frame is 42 ms, which is what Import\n * Video makes a video's frames by default; the game runs a little slower when a frame is heavy, and so does the video.\n *\n * @param player Whose fog: P1 … P8, known when the script is built.\n * @param video A video the script imports.\n * @param x The column of its left edge, 0 by default.\n * @param y The row of its top edge, 0 by default.\n */\nexport const play = game(async (player: Player, video: Video, x: number = 0, y: number = 0) => {\n // (A while: a for over a length known when the script is built would be unrolled, a copy of the body a frame.)\n let f = 0;\n while (f < video.length) {\n light(player, x, y, video, f);\n await nextFrame();\n f++;\n }\n});\n\n/**\n * How fog.columns() lays its columns out, known when the script is built: `count` columns side by side, each `width`\n * tiles across (3, a brush's width, by default) and up to `height` tiles tall, centred on the middle row. `ink`:\n * \"light\" lights each column (the walls of a raycaster bright, on fog), \"dark\" lights all but the columns (dark walls\n * between a lit ceiling and floor); dark is cheaper when columns fill most of the board, light when they are short.\n */\nexport interface ColumnLayout {\n readonly count: number;\n readonly height: number;\n readonly width?: number;\n readonly ink?: \"light\" | \"dark\";\n /**\n * How the walls are told apart, with fog.columns()'s `faces`. \"outline\", the default: every wall drawn as its edges —\n * its top and bottom, a brush deep, and a column lit whole wherever one face meets another (on the nearer of the\n * two) — so that blocks read as boxes, the look of the raycasters of monochrome screens; with dark ink, the edges are\n * lit lines through dark walls. A few brushes a column. Chosen over the rest when played (2026-10-17).\n *\n * The others shade the faces whose number is odd (a raycaster's other side) and leave the rest whole: \"hollow\" —\n * dark, lit only along its top and bottom edges; \"ribbed\" — hollow and lit across the middle of each half; \"panelled\"\n * — hollow and lit across its quarters; \"line\" / \"lines\" — dark lines a tile thick across it at its middle or its\n * quarters (the finest, and two dozen brushes a column near the eye: for a view with room to spare).\n */\n readonly shading?: \"outline\" | \"lines\" | \"line\" | \"hollow\" | \"ribbed\" | \"panelled\";\n}\n\n/**\n * What fog.columns() reads, worked out once a layout when the script is built: for each level of the drawing that is\n * wider than a column (all the columns, a group of them, the last group) and each depth from 0 to half the height, the\n * brushes that cover a rectangle that wide and that deep exactly — the encoder's covers (cover()), a few large brushes\n * in the middle and small ones at the edges. A brush is a word, dx | dy << 8 | size << 16 from the rectangle's top\n * left; `starts` says where each (level, depth) begins.\n */\ninterface ColumnPlan { readonly words: readonly number[]; readonly starts: readonly number[]; readonly group: number; readonly half: number }\nconst columnPlans = new Map();\nconst COLUMN_GROUP = 6;\nfunction columnPlan(layout: ColumnLayout): ColumnPlan {\n const width = layout.width ?? 3, half = layout.height >> 1, group = Math.min(COLUMN_GROUP, layout.count);\n const key = `${layout.count}:${width}:${half}`;\n const had = columnPlans.get(key);\n if (had) return had;\n const words: number[] = [], starts: number[] = [];\n for (const columns of [layout.count, group, layout.count % group || group]) {\n for (let depth = 0; depth <= half; depth++) {\n starts.push(words.length);\n if (depth === 0) continue;\n // A rectangle shallower than a brush is drawn a brush deep, the rest of it on what the level before lit.\n const rows = Array.from({ length: Math.max(depth, 4) }, () => \"#\".repeat(columns * width));\n for (const b of cover(rows, SHAPES)) words.push(b.x | (b.y << 8) | ((b.shape + 1) << 16));\n }\n }\n starts.push(words.length);\n const made = { words, starts, group, half };\n columnPlans.set(key, made);\n return made;\n}\n\n/**\n * One rectangle of fog.columns() wider than a column: the cover at `at` (a level and a depth) drawn twice, the half\n * above the middle and the half below, its top rows `top` and `bottom`, from column `x`.\n */\nconst columnRect = game((player: Player, words: readonly number[], starts: readonly number[], at: number, x: number, top: number, bottom: number) => {\n let i = starts[at];\n const end = starts[at + 1];\n // In pixels once; each brush is its offset (a few bits, a few tests) from there.\n const px = x * 32 + 16, pt = top * 32 + 16, pb = bottom * 32 + 16;\n while (i < end) {\n const w = words[i];\n const dx = (w & 255) * 32, dy = (w & 0xff00) >> 3, size = w >> 16;\n discPx(player, px + dx, pt + dy, size);\n discPx(player, px + dx, pb + dy, size);\n i++;\n }\n});\n\n/**\n * One column's part of fog.columns(), in pixels: brushes (3 × 4) a column of 4 rows apart and the last moved back to\n * end on the edge, twice — from `pt` and from `pb`, the centres of the first brush of each — down a column `k32`\n * pixels deep (32 a row; under 4 rows, a brush deep), centred on `px`. What the smallest brush covers exactly, worked out\n * here rather than read: it is the most common rectangle by far. In pixels because the caller has them, stepped: a\n * product by 32 of a number the game holds is a test a bit, about 0.4 µs, where a step is an add.\n */\nconst columnBarPx = game((player: Player, px: number, pt: number, pb: number, k32: number) => {\n // How far down the last brush is from the first.\n const end = Math.max(k32, 128) - 128;\n let r = 0;\n while (true) {\n centerLocation(SPOT, px, pt, 32, 32);\n createUnit(player, BRUSHES[0], 1, SPOT);\n removeUnit(player, BRUSHES[0]);\n centerLocation(SPOT, px, pb, 32, 32);\n createUnit(player, BRUSHES[0], 1, SPOT);\n removeUnit(player, BRUSHES[0]);\n if (r >= end) break;\n const step = Math.min(end - r, 128);\n r += step;\n pt += step;\n pb += step;\n }\n});\n\n/** columnBarPx() in tiles: `k` rows from row `top` down and from row `bottom` down, centred on column `x`. */\nconst columnBar = game((player: Player, x: number, top: number, bottom: number, k: number) => {\n columnBarPx(player, x * 32 + 16, top * 32 + 80, bottom * 32 + 80, k * 32);\n});\n\n/**\n * A shaded column of fog.columns() (ColumnLayout's `shading`): `a` is half its height, `lines` how many lines across\n * each half — 0 hollow, 1 at its middle, 3 at its quarters. A line is a dark row through a lit column (light ink), or\n * a lit band a brush deep through a dark one; a column too short for its lines is drawn plain.\n */\nconst columnShaded = game((player: Player, cx: number, mid: number, a: number, dark: boolean, lines: number) => {\n if (lines <= 0) {\n if (dark) { if (a >= 6) columnBar(player, cx, mid - 4, mid, 4); }\n else if (a < 8) columnBar(player, cx, mid - a, mid, a);\n else {\n // Hollow: the edges. Ribbed (-1) and panelled (-3): bands across each half too, centred on its middle or\n // quarters, where there is room for them between the edges.\n columnBar(player, cx, mid - a, mid + a - 4, 4);\n const ribs = -lines;\n if (ribs > 0 && a >= 6 * (ribs + 1)) {\n let k = 1;\n while (k <= ribs) {\n const at = (a * k) / (ribs + 1);\n columnBar(player, cx, mid - at - 2, mid + at - 2, 4);\n k++;\n }\n }\n }\n } else if (a < 5 * (lines + 1)) {\n if (!dark) columnBar(player, cx, mid - a, mid, a);\n } else {\n // From the middle out: lit up to each line, the line's row dark (light ink); or a lit band on each line (dark ink).\n let from = 0, k = 1;\n while (k <= lines + 1) {\n const to = k > lines ? a : (a * k) / (lines + 1);\n if (dark) { if (k <= lines) columnBar(player, cx, mid - to - 2, mid + to - 2, 4); }\n else columnBar(player, cx, mid - to, mid + from, to - from);\n from = to + 1;\n k++;\n }\n }\n});\n\n/**\n * Light columns for this frame — the walls of a raycaster, a bar chart — each as tall as the program says, centred on\n * the layout's middle row. The covers are worked out when the script is built (see ColumnLayout), so a frame costs a\n * brush for each part of the picture that differs from its neighbours, not one for every few tiles: a wall that fills\n * the board is a few large brushes.\n *\n * const VIEW = { count: 42, height: 96, ink: \"dark\" } as const;\n * fog.columns(P1, 1, 0, VIEW, heights); // heights: one a column, in tiles, the program's\n * fog.columns(P1, 1, 0, VIEW, heights, faces); // faces: which face of which wall each column shows\n *\n * @param player Whose fog: P1 … P8, known when the script is built.\n * @param x The column of the layout's left edge.\n * @param y The row of its top edge.\n * @param layout How the columns are laid out: known when the script is built.\n * @param heights Each column's height in tiles (0 to the layout's height), an array of the program's.\n * @param faces Which face of which wall each column shows, a number the same for every column of one face — in a\n * raycaster, \\`(mapY * width + mapX) * 2 + side\\` — an array of the program's as long as `heights`: what the\n * layout's `shading` tells walls apart by. Optional: without it, every column is lit whole (a bar chart).\n */\nexport const columns = game((player: Player, x: number, y: number, layout: ColumnLayout, heights: readonly number[], faces?: readonly number[]) => {\n const { words, starts, half, group } = columnPlan(layout);\n const count = layout.count, width = layout.width ?? 3, dark = layout.ink === \"dark\";\n // (Dark lines through a lit column count up; lit bands through a dark one, ribbed and panelled, down.)\n // (Without faces there is nothing to tell apart, and every column is whole.)\n const outline = faces !== undefined && (layout.shading === undefined || layout.shading === \"outline\");\n const lines = layout.shading === \"lines\" ? 3 : layout.shading === \"line\" ? 1 : layout.shading === \"ribbed\" ? -1 : layout.shading === \"hollow\" ? 0 : -3;\n const span = half + 1;\n // Each column's depth from where its lit part starts: the middle row for light ink, the top and bottom for dark.\n // A shaded column of light ink is drawn on its own, after the rest: to the rest it is as if it were not there — and\n // so is every column of a light outline, which is all edges.\n const depth: number[] = new Array(count).fill(0);\n // Half of each column's height, at most half the layout's: worked out once. (Clamped before the shift, the same\n // number: the shift then has the few bits the clamp leaves, where a height from the program could have any.)\n const halves: number[] = new Array(count).fill(0);\n // And in pixels (32 a row), for the bars, which place their brushes in pixels: a small number times 32 is a few tests.\n const halves32: number[] = new Array(count).fill(0);\n let least = half, most = 0;\n // (A light outline is all edges: every depth is 0, nothing to work out.)\n const flat = !dark && outline;\n if (flat) least = 0;\n let i = 0;\n while (i < count) {\n const a = Math.min(Math.max(heights[i], 0), 2 * half + 1) >> 1;\n halves[i] = a;\n halves32[i] = a * 32;\n if (!flat) {\n let d = dark ? half - a : a;\n if (faces !== undefined && !dark && !outline && (faces[i] & 1) != 0) d = 0;\n depth[i] = d;\n if (d < least) least = d;\n if (d > most) most = d;\n }\n i++;\n }\n // A rectangle from depth p, k deep, starts on the rows above and below the middle (light) or below the top and above\n // the bottom (dark); one shallower than 4 is drawn 4 deep, reaching back over what is lit already.\n const mid = y + half;\n // Every column, from depth 0 to the least.\n if (least > 0) {\n const k = Math.max(least, 4);\n if (dark) columnRect(player, words, starts, least, x, y + least - k, y + 2 * half - least);\n else columnRect(player, words, starts, least, x, mid - least, mid + least - k);\n }\n // Each group of columns, from the least to the group's least; then each column, from there to its own. (Nothing,\n // when every column's depth is the least: a light outline's always.)\n let g = most > least ? 0 : count;\n while (g < count) {\n const last = Math.min(g + group, count);\n let gl = half;\n let j = g;\n while (j < last) { if (depth[j] < gl) gl = depth[j]; j++; }\n const level = last - g === group ? 1 : 2;\n if (gl > least) {\n const k = gl - least, kk = Math.max(k, 4);\n if (dark) columnRect(player, words, starts, level * span + k, x + g * width, y + gl - kk, y + 2 * half - gl);\n else columnRect(player, words, starts, level * span + k, x + g * width, mid - gl, mid + gl - kk);\n }\n j = g;\n while (j < last) {\n const k = depth[j] - gl;\n if (k > 0) {\n const kk = Math.max(k, 4), cx = x + j * width + 1;\n if (dark) columnBar(player, cx, y + depth[j] - kk, y + 2 * half - depth[j], k);\n else columnBar(player, cx, mid - depth[j], mid + depth[j] - kk, k);\n }\n j++;\n }\n g = last;\n }\n if (outline) {\n // Where one face meets another, the nearer (taller) of the two columns is an edge.\n const edge: number[] = new Array(count).fill(0);\n let face = faces[0], tall = heights[0];\n i = 1;\n while (i < count) {\n const f = faces[i], h = heights[i];\n if (f != face) { if (h >= tall) edge[i] = 1; else edge[i - 1] = 1; }\n face = f;\n tall = h;\n i++;\n }\n // The bars in pixels: a column's centre stepped across, the middle row's worked out once.\n const mid80 = mid * 32 + 80;\n let px = (x + 1) * 32 + 16;\n i = 0;\n while (i < count) {\n const a = halves[i], a32 = halves32[i];\n if (dark) { if (edge[i] != 0 && a >= 2) columnBarPx(player, px, mid80 - a32, mid80, a32); }\n else if (a > 0) {\n if (edge[i] != 0 || a < 8) columnBarPx(player, px, mid80 - a32, mid80, a32);\n else columnBarPx(player, px, mid80 - a32, mid80 + a32 - 128, 128);\n }\n px += width * 32;\n i++;\n }\n } else if (faces !== undefined) {\n i = 0;\n while (i < count) {\n if ((faces[i] & 1) != 0) columnShaded(player, x + i * width + 1, mid, halves[i], dark, lines);\n i++;\n }\n }\n});\n\n/**\n * Keep what was lit this frame on the board for `n` frames, and wait them out: \\`await fog.show(2)\\` in place of\n * \\`await nextFrame()\\` draws every other frame at half the cost. StarCraft: Remastered redraws the fog on the minimap\n * every other frame (measured in two played recordings, 2026-10-17), so a picture drawn every other frame looks the\n * same there as one drawn every frame. The board is wiped at the start of the frame after them, ready for the next.\n *\n * @param n How many frames, 1 to 24, known when the script is built.\n */\nexport const show = game(async (n: number) => {\n memory.vision.countdown = n;\n await sleep(frames(n));\n});\n\n/** Everything above as one name: import { fog } from \"trigscript/fog\"; fog.light(P1, x, y, 3). */\nexport const fog = { light, rect, columns, show, play, reveal, picture, SHAPES };\n\n/** The longest fog.show() keeps a picture: the countdown the game sets itself after a wipe is longer than this. */\nconst SHOW_MOST = 24;\n\n// The canvas: every brush's sight set, every frame, and the board wiped on the next — for as long as the game runs —\n// unless a program asked for the picture to stay (fog.show), when the countdown is that program's to run down.\nprogram(() => {\n while (true) {\n for (let s = 1; s <= 11; s++) stats(BRUSHES[s - 1]).sight = s;\n const left = memory.vision.countdown;\n if (left == 0 || left > SHOW_MOST) memory.vision.countdown = 1;\n sleep(frames(1));\n }\n});\n", "math": "/**\n * Numbers for motion and play: the standard library's \"trigscript/math\" (TrigScript 4).\n *\n * import { Vec, lerp, easeOutQuad, distance, angleTo, approach, wrap } from \"trigscript/math\";\n *\n * const t = fixed(frame) / 48; // 0 to 1 over two seconds\n * const x = lerp(fromX, toX, easeOutQuad(t)); // eased from one place to another\n * const v = Vec.fromAngle(angleTo(x, y, tx, ty), fixed(4));\n * pos = pos.add(v);\n *\n * Fractions are the language's fixed numbers (16.16: four decimal places or so, from about −32 768 to 32 767): a\n * number becomes one when a fraction is stored into it, or written `fixed(…)`. Angles are radians, as JavaScript's are.\n * Whole numbers stay whole where they can — `distanceSq`, `manhattan`, `wrap`, `sign` — and cost less.\n */\n\n/** π, and a whole turn. */\nexport const PI = Math.PI;\nexport const TAU = Math.PI * 2;\n\n/** From a to b by t: a at 0, b at 1, between them in between (t outside 0 … 1 goes past). */\nexport function lerp(a: fixed, b: fixed, t: fixed): fixed {\n return a + (b - a) * t;\n}\n\n/** Where v is from a to b, as lerp's t: 0 at a, 1 at b. */\nexport function inverseLerp(a: fixed, b: fixed, v: fixed): fixed {\n return b === a ? fixed(0) : (v - a) / (b - a);\n}\n\n/** v from the range a1 … b1 into a2 … b2. */\nexport function remap(v: fixed, a1: fixed, b1: fixed, a2: fixed, b2: fixed): fixed {\n return lerp(a2, b2, inverseLerp(a1, b1, v));\n}\n\n/** t kept inside 0 … 1. */\nexport function clamp01(t: fixed): fixed {\n return t < 0 ? fixed(0) : t > 1 ? fixed(1) : t;\n}\n\n/** From `current` towards `target` by at most `step`, never past it: a value that follows another at a speed. */\nexport function approach(current: number, target: number, step: number): number {\n if (current < target) return current + step > target ? target : current + step;\n return current - step < target ? target : current - step;\n}\n\n/** n into 0 … size − 1, going round: wrap(-1, 10) is 9, wrap(12, 10) is 2. */\nexport function wrap(n: number, size: number): number {\n return ((n % size) + size) % size;\n}\n\n/** −1, 0 or 1, by whether n is below, at or above zero. */\nexport function sign(n: number): number {\n return n > 0 ? 1 : n < 0 ? -1 : 0;\n}\n\n/** The distance from one point to another, as the crow flies. */\nexport function distance(x1: number, y1: number, x2: number, y2: number): fixed {\n return Math.hypot(x2 - x1, y2 - y1);\n}\n\n/** The distance squared: whole, and cheaper — compare it with a range squared. */\nexport function distanceSq(x1: number, y1: number, x2: number, y2: number): number {\n return (x2 - x1) * (x2 - x1) + (y2 - y1) * (y2 - y1);\n}\n\n/** The distance along the axes, |dx| + |dy|: what the game's own \"nearest\" goes by. */\nexport function manhattan(x1: number, y1: number, x2: number, y2: number): number {\n return Math.abs(x2 - x1) + Math.abs(y2 - y1);\n}\n\n/** The angle from one point to another, in radians from −π to π: 0 is to the right (east), π / 2 down (south, y grows down). */\nexport function angleTo(x1: number, y1: number, x2: number, y2: number): fixed {\n return Math.atan2(y2 - y1, x2 - x1);\n}\n\n/** Degrees as radians. */\nexport function radians(degrees: fixed): fixed {\n return (degrees * PI) / 180;\n}\n\n/** Radians as degrees. */\nexport function degrees(radians: fixed): fixed {\n return (radians * 180) / PI;\n}\n\n/* Easing: t from 0 to 1, how far along a motion is at that point of its time — slow then fast, fast then slow, … */\n\n/** Slow at the start. */\nexport function easeInQuad(t: fixed): fixed {\n return t * t;\n}\n\n/** Slow at the end. */\nexport function easeOutQuad(t: fixed): fixed {\n return t * (2 - t);\n}\n\n/** Slow at both ends. */\nexport function easeInOutQuad(t: fixed): fixed {\n return t < 0.5 ? 2 * t * t : -1 + (4 - 2 * t) * t;\n}\n\n/** Slower at the start than easeInQuad. */\nexport function easeInCubic(t: fixed): fixed {\n return t * t * t;\n}\n\n/** Slower at the end than easeOutQuad. */\nexport function easeOutCubic(t: fixed): fixed {\n const u = t - 1;\n return u * u * u + 1;\n}\n\n/** Goes a little past the end and comes back. */\nexport function easeOutBack(t: fixed): fixed {\n const u = t - 1;\n return 1 + 2.70158 * u * u * u + 1.70158 * u * u;\n}\n\n/** Slow at both ends, smoothly (Hermite: 3t² − 2t³). */\nexport function smoothstep(t: fixed): fixed {\n return t * t * (3 - 2 * t);\n}\n\n/**\n * A point, or a direction and a length: two fixed numbers. Each operation gives a new Vec.\n *\n * let pos = new Vec(fixed(x), fixed(y));\n * const toward = new Vec(fixed(tx), fixed(ty)).sub(pos).normalized().scale(fixed(3));\n * pos = pos.add(toward);\n */\nexport class Vec {\n x: fixed;\n y: fixed;\n\n constructor(x: fixed, y: fixed) {\n this.x = x;\n this.y = y;\n }\n\n /** A direction `length` long at `angle` radians (0 to the right, π / 2 down). */\n static fromAngle(angle: fixed, length: fixed): Vec {\n return new Vec(Math.cos(angle) * length, Math.sin(angle) * length);\n }\n\n add(o: Vec): Vec {\n return new Vec(this.x + o.x, this.y + o.y);\n }\n\n sub(o: Vec): Vec {\n return new Vec(this.x - o.x, this.y - o.y);\n }\n\n scale(k: fixed): Vec {\n return new Vec(this.x * k, this.y * k);\n }\n\n dot(o: Vec): fixed {\n return this.x * o.x + this.y * o.y;\n }\n\n /** How long it is. */\n length(): fixed {\n return Math.sqrt(this.x * this.x + this.y * this.y);\n }\n\n /** The same direction, 1 long (or none, for none). */\n normalized(): Vec {\n const l = this.length();\n const k: fixed = l === 0 ? fixed(0) : 1 / l;\n return new Vec(this.x * k, this.y * k);\n }\n\n /** Its angle, in radians from −π to π. */\n angle(): fixed {\n return Math.atan2(this.y, this.x);\n }\n\n /** Turned by `angle` radians (clockwise on the map, where y grows down). */\n rotate(angle: fixed): Vec {\n const c = Math.cos(angle);\n const s = Math.sin(angle);\n return new Vec(this.x * c - this.y * s, this.x * s + this.y * c);\n }\n\n /** How far it is from another point. */\n distanceTo(o: Vec): fixed {\n return this.sub(o).length();\n }\n\n /** From this towards another by t (0 here, 1 there). */\n lerp(o: Vec, t: fixed): Vec {\n return new Vec(this.x + (o.x - this.x) * t, this.y + (o.y - this.y) * t);\n }\n}\n", - "music": "/**\n * Music in a program: the standard library's \"trigscript/music\" (TrigScript 4).\n *\n * import { music } from \"trigscript/music\";\n * import pluck from \"./pluck.sound\";\n *\n * const lead = music.synth({ wave: \"square\", volume: 0.2 });\n * const bass = music.sampler(pluck, \"A3\");\n * const drums = music.kit({ k: kick, s: snare, h: hat });\n *\n * const song = music.song({ bpm: 120 },\n * music.track(lead, \"C5 - E5 - G5 - - . | A5 - G5 - E5 - - .\"),\n * music.track(bass, \"C3 - - - - - - - | F3 - - - - - - -\"),\n * music.track(drums, \"kh h sh h kh h sh h | kh h sh h kh kh sh h\"),\n * );\n *\n * program(async () => {\n * start(music.loop(song)); // the song again and again, beside the program\n * playWav(lead.note(\"C6\", 0.1)); // an instrument's note on its own, for a sound effect\n * });\n *\n * A pattern is steps, a word each, a quarter of a beat each by default: a note's name starts it (\"C4\", \"F#5\", \"Bb2\"),\n * several with + sound together (\"C4+E4+G4\"), \"-\" holds what sounds a step longer, \".\" is a rest, \"|\" is only for the\n * eye; \"@\" after a step's notes says how loud (\"C4@0.5\"). A kit's step is its sounds' letters (\"kh\": kick and hat).\n * Everything is worked out when the script is built: each note an instrument plays becomes a clip once (`trigscript`'s\n * Clip, put in the map once however often it plays), and the song a table of which clip starts on which frame, which\n * `play` reads while the game runs.\n *\n * A longer song is written in sections — parts named once, each section saying what they play in it, a pattern played\n * again until the section is full — and chords by name, their notes drawn by a template:\n *\n * const parts = { keys: music.part(music.presets.keys()), bass: music.part(music.presets.bass()), drums: music.part(music.drums()) };\n * const verse = music.section(4, {\n * keys: music.chords(\"Am F C G\", \"1+2+3 - . 1+2+3 . . 1+2+3 -\", { octave: 4, lead: true }),\n * bass: music.chords(\"Am F C G\", \"1 - . 1 . 1 3 -\", { octave: 2 }),\n * drums: music.beat({ k: [0, 3, 4], s: [2, 6], h: [0, 1, 2, 3, 4, 5, 6, 7] }, 8),\n * });\n * const song = music.arrange({ bpm: 120, steps: 2 }, parts, intro, verse, verse.with({ keys: null }), outro);\n *\n * Music that follows the game is a song of sections cut into bars and layers, a clip each, which the program steers\n * bar by bar — which section, which layers, a stinger on the next beat:\n *\n * const cut = music.stems(song, { layers: { drums: [\"drums\"], rest: [\"keys\", \"bass\"] } });\n * program(async () => {\n * const band = new Band(); // import { music, Band } from \"trigscript/music\"\n * start(music.conduct(cut, band));\n * if (danger) band.go(cut.section(\"chorus\")); // at the next bar line\n * if (cleared) band.sting(cut.stinger(\"crash\")); // on the next beat\n * });\n *\n * What the game does with it (the sound probe, 2026-10-19): a bar is a whole number of frames (42 ms on Fastest), so\n * the tempo is the nearest that fits — `song.bpm` says which; a sound starts within 21 ms of its frame; 32 sound at\n * once, and a song that would need more is refused; notes that overlap add, with no limiter, so keep the volumes down.\n * `render` mixes a song into one clip when the script is built instead: every note exactly in time, one sound playing,\n * nothing to change while it plays. A program plays to its own player; a program for each player plays to each.\n */\nimport { game, sleep, frames, playWav, tone, noise, fm, pluck, mixdown, together, memory, type Clip, type Notes } from \"trigscript\";\n\n/** Seconds a frame lasts on Fastest, the speed a map is played at. */\nconst FRAME = 0.042;\n/** Sounds the game plays at once. */\nconst VOICES = 32;\n\n/** What plays a pattern's notes: each note a clip, made when the script is built. */\nexport interface Instrument {\n readonly kind: \"notes\";\n /**\n * Note `name` (\"C4\", \"F#5\") lasting `seconds`, release and all: a clip to play on its own, too. `rate`: samples a\n * second, for an instrument that makes its notes (render asks for 44 100).\n */\n note(name: string, seconds: number, rate?: number): Clip;\n}\n\n/** What plays a pattern's letters: a clip a letter. */\nexport interface Kit {\n readonly kind: \"kit\";\n readonly hits: Readonly>;\n}\n\nconst SEMITONE: Readonly> = { c: 0, d: 2, e: 4, f: 5, g: 7, a: 9, b: 11 };\n\n/** A note's number, 69 for A4: \"C4\" is 60, \"C#4\" 61, \"Db4\" 61. */\nfunction noteNumber(name: string): number {\n const m = /^([A-Ga-g])([#b]?)(-?\\d)$/.exec(name);\n if (!m) throw new Error(`A note is a name with its octave — \"C4\", \"F#5\", \"Bb2\" — not ${JSON.stringify(name)}.`);\n return 12 * (Number(m[3]) + 1) + SEMITONE[m[1].toLowerCase()] + (m[2] === \"#\" ? 1 : m[2] === \"b\" ? -1 : 0);\n}\n\n/** Notes kept as they are made, so a note played many times is the same clip (and the same file in the map). */\nfunction remembering(make: (name: string, seconds: number, rate?: number) => Clip): (name: string, seconds: number, rate?: number) => Clip {\n const made = new Map();\n return (name, seconds, rate) => {\n const key = `${name} ${seconds.toFixed(4)} ${rate ?? \"\"}`;\n let c = made.get(key);\n if (!c) { c = make(name, seconds, rate); made.set(key, c); }\n return c;\n };\n}\n\n/**\n * An instrument from a recording of one note, `root` (\"A3\"): every other note is it played faster or slower, as a\n * sampler does, cut to the note's length with a short release. Where the recording is shorter than a note, the note\n * is as long as the recording.\n */\nexport function sampler(clip: Clip, root: string = \"C4\", options: { readonly release?: number; readonly volume?: number } = {}): Instrument {\n const base = noteNumber(root), release = options.release ?? 0.05, volume = options.volume ?? 1;\n return Object.freeze({\n kind: \"notes\" as const,\n note: remembering((name, seconds) => {\n const pitched = clip.pitch(noteNumber(name) - base).gain(volume);\n return pitched.trim(0, Math.min(pitched.seconds, seconds)).fade(0, Math.min(release, seconds / 2));\n }),\n });\n}\n\n/** An attack, a decay to the sustain level, and a release after the note's length, in seconds (sustain 0 … 1). */\nexport interface Shape { readonly attack?: number; readonly decay?: number; readonly sustain?: number; readonly release?: number }\n\n/** A shape's points for a note held `held` seconds, as levels 0 … 1: the attack from 0, the release to 0. */\nfunction shapePoints(shape: Required, held: number): [number, number][] {\n const { attack, decay, sustain, release } = shape;\n const h = Math.max(attack + decay, held);\n return [[0, 0], [attack, 1], [attack + decay, sustain], [h, sustain], [h + release, 0]];\n}\nconst full = (s: Shape | undefined, d: Required): Required => ({ attack: s?.attack ?? d.attack, decay: s?.decay ?? d.decay, sustain: s?.sustain ?? d.sustain, release: s?.release ?? d.release });\n\n/** A synth's filter: its type, the cutoff it rests at, its resonance, and how many octaves above it a note's envelope opens it. */\nexport interface SynthFilter {\n readonly type?: \"lowpass\" | \"highpass\" | \"bandpass\";\n readonly cutoff: number;\n readonly resonance?: number;\n /** Octaves above the cutoff at the envelope's top (0: the filter does not move). */\n readonly amount?: number;\n /** The filter's own attack, decay, sustain and release, as a note's loudness has. */\n readonly envelope?: Shape;\n}\n\nexport interface SynthOptions extends Shape {\n readonly wave?: \"sine\" | \"square\" | \"triangle\" | \"saw\";\n readonly volume?: number;\n /** Copies of each note (1 to 16), `detune` cents apart from lowest to highest, `width` 0 … 1 across left and right. */\n readonly voices?: number;\n readonly detune?: number;\n readonly width?: number;\n readonly filter?: SynthFilter;\n /** Saturation after the filter, dB of push (`Clip.drive`). */\n readonly drive?: number;\n /** Semitones each note slides up from (a negative number: from below) over `glideTime` seconds. */\n readonly glide?: number;\n readonly glideTime?: number;\n}\n\n/**\n * An instrument made of tones: `wave` (\"sine\", \"square\", \"triangle\", \"saw\") shaped by an attack, a decay to the\n * sustain level, and a release after the note's length — a stack of detuned copies with `voices`, through a filter\n * that opens and closes with each note with `filter`, saturated with `drive`.\n */\nexport function synth(options: SynthOptions = {}): Instrument {\n const wave = options.wave ?? \"square\", volume = options.volume ?? 0.2;\n const amp = full(options, { attack: 0.005, decay: 0.08, sustain: 0.7, release: 0.06 });\n const f = options.filter, fshape = full(f?.envelope, { attack: 0.005, decay: 0.25, sustain: 0.2, release: 0.2 });\n const voices = options.voices ?? 1, detune = options.detune ?? (voices > 1 ? 20 : 0), width = options.width ?? 0;\n return Object.freeze({\n kind: \"notes\" as const,\n note: remembering((name, seconds, rate) => {\n const held = Math.max(amp.attack + amp.decay, seconds - amp.release), length = held + amp.release;\n const r = rate === undefined ? {} : { rate };\n const pitch = options.glide ? [[0, 440 * 2 ** ((noteNumber(name) + options.glide - 69) / 12)], [options.glideTime ?? 0.06, name]] as [number, number | string][] : name;\n let c = tone(pitch, length, { wave, volume, voices, detune, width, ...r });\n if (f) {\n const amount = f.amount ?? 0, base = f.cutoff;\n const cutoff = amount === 0 ? base : shapePoints(fshape, held).map(([t, level]) => [t, base * 2 ** (amount * level)] as [number, number]);\n c = c.filter(f.type ?? \"lowpass\", cutoff, { resonance: f.resonance ?? 0.1 });\n }\n if (options.drive) c = c.drive(options.drive);\n return c.envelope(...shapePoints(amp, held));\n }),\n });\n}\n\n/**\n * An FM instrument (`fm()`): `ratio` the modulator's pitch to the note's, `index` how bright a note starts, falling to\n * `index × brightness` over `bright` seconds — an electric piano (ratio 1), a bell (3.5), a bass (0.5).\n */\nexport function fmSynth(options: Shape & { readonly ratio?: number; readonly index?: number; readonly brightness?: number; readonly bright?: number; readonly feedback?: number; readonly volume?: number } = {}): Instrument {\n const amp = full(options, { attack: 0.002, decay: 1.2, sustain: 0.35, release: 0.3 });\n const ratio = options.ratio ?? 1, index = options.index ?? 3, after = index * (options.brightness ?? 0.2), bright = options.bright ?? 0.8, volume = options.volume ?? 0.25;\n return Object.freeze({\n kind: \"notes\" as const,\n note: remembering((name, seconds, rate) => {\n const held = Math.max(amp.attack + amp.decay, seconds - amp.release), length = held + amp.release;\n return fm(name, length, { ratio, index: [[0, index], [bright, after]], feedback: options.feedback ?? 0, volume, ...(rate === undefined ? {} : { rate }) }).envelope(...shapePoints(amp, held));\n }),\n });\n}\n\n/** A plucked-string instrument (`pluck()`): `brightness` 0 … 1, `decay` seconds; a note rings out its decay unless `cut`. */\nexport function strings(options: { readonly brightness?: number; readonly decay?: number; readonly volume?: number; readonly cut?: boolean } = {}): Instrument {\n const brightness = options.brightness ?? 0.55, decay = options.decay ?? 1.5, volume = options.volume ?? 0.3;\n return Object.freeze({\n kind: \"notes\" as const,\n note: remembering((name, seconds, rate) => {\n const length = options.cut ? seconds + 0.05 : Math.max(seconds + 0.05, Math.min(decay, 4));\n return pluck(name, length, { brightness, decay, volume, ...(rate === undefined ? {} : { rate }) }).fade(0, 0.05);\n }),\n });\n}\n\n/**\n * Instruments to start from, each a function that takes the same options as the instrument it is made with, over its\n * own: `music.presets.supersaw({ volume: 0.1 })`.\n */\nexport const presets = {\n /** Seven detuned saws through a filter that opens on each note: a trance lead, big chords. */\n supersaw: (o: SynthOptions = {}) => synth({ wave: \"saw\", voices: 7, detune: 28, width: 0.7, volume: 0.16, attack: 0.005, decay: 0.25, sustain: 0.8, release: 0.18, filter: { cutoff: 1800, amount: 2, resonance: 0.15, envelope: { decay: 0.35, sustain: 0.35 } }, ...o }),\n /** A slow, wide pad: five saws, the filter low and slowly opening. Give its track a chorus and the reverb. */\n pad: (o: SynthOptions = {}) => synth({ wave: \"saw\", voices: 5, detune: 18, width: 1, volume: 0.1, attack: 0.35, decay: 0.6, sustain: 0.85, release: 0.8, filter: { cutoff: 700, amount: 1.3, resonance: 0.1, envelope: { attack: 0.6, decay: 1.2, sustain: 0.5, release: 0.8 } }, ...o }),\n /** A round, punchy bass: two saws, the filter snapping shut after each note, a little drive. */\n bass: (o: SynthOptions = {}) => synth({ wave: \"saw\", voices: 2, detune: 8, volume: 0.32, attack: 0.002, decay: 0.12, sustain: 0.9, release: 0.04, filter: { cutoff: 180, amount: 2.6, resonance: 0.25, envelope: { decay: 0.18, sustain: 0.15, release: 0.05 } }, drive: 6, ...o }),\n /** A squelchy, resonant line (a 303's): one saw, the filter wide open on each note and shutting fast. */\n acid: (o: SynthOptions = {}) => synth({ wave: \"saw\", volume: 0.22, attack: 0.002, decay: 0.1, sustain: 0.8, release: 0.05, filter: { cutoff: 260, amount: 3.6, resonance: 0.82, envelope: { decay: 0.16, sustain: 0 } }, drive: 10, ...o }),\n /** A warm lead: two square waves a little apart, softened by a filter. */\n lead: (o: SynthOptions = {}) => synth({ wave: \"square\", voices: 2, detune: 12, width: 0.3, volume: 0.15, attack: 0.004, decay: 0.15, sustain: 0.7, release: 0.12, filter: { cutoff: 2200, amount: 1.2, resonance: 0.2, envelope: { decay: 0.25, sustain: 0.4 } }, ...o }),\n /** The old consoles' lead: a bare square wave. */\n chip: (o: SynthOptions = {}) => synth({ wave: \"square\", volume: 0.14, attack: 0.002, decay: 0.05, sustain: 0.8, release: 0.03, ...o }),\n /** An electric piano: FM at ratio 1, bright on the strike and mellowing. */\n keys: (o: Parameters[0] = {}) => fmSynth({ ratio: 1, index: 2.6, brightness: 0.25, bright: 0.6, volume: 0.22, decay: 1.4, sustain: 0.3, release: 0.35, ...o }),\n /** A bell: FM at ratio 3.5, long and shimmering. */\n bell: (o: Parameters[0] = {}) => fmSynth({ ratio: 3.5, index: 5, brightness: 0.08, bright: 1.6, volume: 0.16, decay: 2.5, sustain: 0, release: 1.2, ...o }),\n /** A plucked string, a little bright. */\n pluck: (o: Parameters[0] = {}) => strings({ brightness: 0.6, decay: 1.4, ...o }),\n};\n\n/**\n * A drum kit made from nothing, a letter a drum: k kick, s snare, c clap, h closed hat, o open hat, t tom, r rim, x crash. At\n * 44 100 Hz (`rate`), so the hats keep their top; `volume` 0 … 1 over the whole kit (1).\n */\nexport function drums(options: { readonly rate?: number; readonly volume?: number } = {}): Kit {\n const rate = options.rate ?? 44100, v = options.volume ?? 1;\n const metal = (seconds: number) => [263, 400, 421, 474, 587, 845].map((hz) => tone(hz * 2.6, seconds, { wave: \"square\", volume: 0.16, rate })).reduce((a, b) => a.mix(b));\n const kick = tone([[0, 170], [0.025, 85], [0.3, 46]], 0.42, { volume: 0.95, rate }).envelope([0, 1], [0.08, 0.85], [0.42, 0])\n .mix(noise(0.006, { volume: 0.35, rate }).filter(\"highpass\", 3000)).drive(5);\n const snare = tone([[0, 260], [0.04, 185]], 0.18, { wave: \"triangle\", volume: 0.6, rate }).envelope([0, 1], [0.15, 0])\n .mix(noise(0.28, { volume: 0.6, rate }).filter(\"highpass\", 1400).filter(\"peak\", 5000, { gain: 4 }).envelope([0, 1], [0.03, 0.55], [0.26, 0])).drive(4);\n const burst = (at: number) => ({ clip: noise(0.012, { volume: 0.6, rate }).envelope([0, 1], [0.012, 0.2]), at });\n const clap = mixdown([burst(0), burst(0.011), burst(0.023), { clip: noise(0.22, { volume: 0.5, rate }).envelope([0, 1], [0.22, 0]), at: 0.03 }], undefined, rate)\n .filter(\"bandpass\", 1300, { resonance: 0.35 }).gain(1.6);\n const hat = (seconds: number) => metal(seconds).mix(noise(seconds, { volume: 0.4, rate })).filter(\"highpass\", 7200).filter(\"highshelf\", 10000, { gain: 3 }).envelope([0, 1], [seconds * 0.25, 0.4], [seconds, 0]);\n const tom = tone([[0, 190], [0.2, 120]], 0.4, { volume: 0.8, rate }).envelope([0, 1], [0.4, 0]).mix(noise(0.01, { volume: 0.2, rate }).filter(\"bandpass\", 2500));\n const rim = tone(1700, 0.025, { wave: \"triangle\", volume: 0.5, rate }).mix(noise(0.012, { volume: 0.3, rate }).filter(\"highpass\", 4000)).envelope([0, 1], [0.025, 0]);\n const crash = metal(2).mix(noise(2, { volume: 0.5, rate })).filter(\"highpass\", 4500).filter(\"peak\", 7500, { gain: 3 }).envelope([0, 1], [0.08, 0.5], [2, 0]);\n return kit({ k: kick.gain(v), s: snare.gain(v), c: clap.gain(v), h: hat(0.07).gain(0.55 * v), o: hat(0.4).gain(0.5 * v), t: tom.gain(v), r: rim.gain(v), x: crash.gain(0.45 * v) });\n}\n\n/** A kit: a clip a letter, played by a pattern's letters (\"kh\" plays k and h together). */\nexport function kit(hits: Readonly>): Kit {\n for (const k of Object.keys(hits)) if (k.length !== 1 || k === \".\" || k === \"-\" || k === \"|\") throw new Error(`A kit's sounds are named by one letter each, not ${JSON.stringify(k)}.`);\n return Object.freeze({ kind: \"kit\" as const, hits: Object.freeze({ ...hits }) });\n}\n\n/* ── Patterns: what is written, and what writes it ── */\n\n/** A pattern: steps a word each, as one text or a text a bar. */\nexport type Pattern = string | readonly string[];\n\n/** A pattern's steps, the \"|\" left out. */\nconst wordsOf = (p: Pattern): string[] => (typeof p === \"string\" ? p : p.join(\" \")).split(/\\s+/).filter((w) => w !== \"\" && w !== \"|\");\n\n/** A step's sounds and how loud: \"C4+E4@0.6\" is C4 and E4 at 0.6; \"kh\" k and h at 1. */\nfunction velocityOf(word: string, step: number): { body: string; velocity: number } {\n const at = word.indexOf(\"@\");\n if (at < 0) return { body: word, velocity: 1 };\n const body = word.slice(0, at), v = Number(word.slice(at + 1));\n if (body === \"\" || body === \"-\" || body === \".\" || !(v >= 0 && v <= 4)) throw new Error(`Step ${step + 1}: \"@\" follows a note, a chord or a kit's letters, with how loud they play, 0 to 4 (1 as written): \"C4@0.6\", \"kh@1.3\" — not ${JSON.stringify(word)}.`);\n return { body, velocity: v };\n}\n\nconst NAMES = [\"C\", \"C#\", \"D\", \"D#\", \"E\", \"F\", \"F#\", \"G\", \"G#\", \"A\", \"A#\", \"B\"];\n/** A note's name from its number: 60 is \"C4\". Sharps, not flats. */\nfunction nameOf(n: number): string {\n const octave = Math.floor(n / 12) - 1;\n if (octave < -1 || octave > 9) throw new Error(`A note goes from C-1 to B9: this one would be ${NAMES[((n % 12) + 12) % 12]}${octave}.`);\n return NAMES[((n % 12) + 12) % 12] + octave;\n}\n\n/** Each note of a step moved by a function of its number; holds, rests, kit letters and \"@\" kept. */\nconst eachNote = (p: Pattern, move: (n: number) => number): string[] => wordsOf(p).map((w) => w.replace(/[A-Ga-g][#b]?-?\\d/g, (name) => nameOf(move(noteNumber(name)))));\n\n/** A pattern `semitones` up (or down: a negative number): `transpose(tune, 12)` is the tune an octave up. A text a bar stays a text a bar. */\nexport function transpose(p: Pattern, semitones: number): Pattern {\n if (!Number.isInteger(semitones)) throw new Error(`transpose() moves by whole semitones, not ${semitones}.`);\n if (semitones === 0) return p;\n return typeof p === \"string\" ? eachNote(p, (n) => n + semitones).join(\" \") : p.map((bar) => eachNote(bar, (n) => n + semitones).join(\" \"));\n}\n\n/** A pattern with each step `by` steps long — eighths written as sixteenths with `stretch(p, 2)`. A text a bar stays a text a bar. */\nexport function stretch(p: Pattern, by: number): Pattern {\n if (!Number.isInteger(by) || by < 1) throw new Error(`stretch() makes each step a whole number of steps long, 1 or more — not ${by}.`);\n const one = (text: string) => wordsOf(text).map((w) => [w, ...Array.from({ length: by - 1 }, () => (w.startsWith(\".\") ? \".\" : \"-\"))].join(\" \")).join(\" \");\n return typeof p === \"string\" ? one(p) : p.map(one);\n}\n\n/** A pattern `n` times over, as one text. */\nexport function repeat(p: Pattern, n: number): string {\n if (!Number.isInteger(n) || n < 1) throw new Error(`repeat() plays a pattern a whole number of times, 1 or more — not ${n}.`);\n return Array.from({ length: n }, () => wordsOf(p).join(\" \")).join(\" \");\n}\n\n/** A kit's bar: the steps (from 0) each letter plays on, `steps` steps long — `beat({ k: [0, 8], s: [4, 12] })`. */\nexport function beat(hits: Readonly>, steps: number = 16): string {\n for (const [k, at] of Object.entries(hits)) for (const s of at) if (!Number.isInteger(s) || s < 0 || s >= steps) throw new Error(`beat(): ${k} on step ${s}, of steps 0 to ${steps - 1}.`);\n return Array.from({ length: steps }, (_, i) => Object.keys(hits).filter((k) => hits[k].includes(i)).join(\"\") || \".\").join(\" \");\n}\n\n/** A chord's notes above its root, by what follows the root's name. */\nconst QUALITIES: Readonly> = {\n \"\": [0, 4, 7], m: [0, 3, 7], dim: [0, 3, 6], aug: [0, 4, 8], \"+\": [0, 4, 8], sus2: [0, 2, 7], sus4: [0, 5, 7], sus: [0, 5, 7], \"5\": [0, 7],\n \"6\": [0, 4, 7, 9], m6: [0, 3, 7, 9], \"7\": [0, 4, 7, 10], maj7: [0, 4, 7, 11], M7: [0, 4, 7, 11], m7: [0, 3, 7, 10], dim7: [0, 3, 6, 9],\n m7b5: [0, 3, 6, 10], \"7sus4\": [0, 5, 7, 10], \"9\": [0, 4, 7, 10, 14], maj9: [0, 4, 7, 11, 14], m9: [0, 3, 7, 10, 14], add9: [0, 4, 7, 14], madd9: [0, 3, 7, 14],\n};\n\n/** A chord's notes as numbers, lowest first. */\nfunction chordNumbers(symbol: string, octave: number, inversion: number): number[] {\n const m = /^([A-G][#b]?)([^/]*)(?:\\/([A-G][#b]?))?$/.exec(symbol);\n const steps = m ? QUALITIES[m[2]] : undefined;\n if (!m || !steps) throw new Error(`A chord is a root and what kind — \"Am\", \"C\", \"G7\", \"Fmaj7\", \"Dm7\", \"Esus4\", \"Bdim\", \"C/E\" — not ${JSON.stringify(symbol)}. The kinds: ${Object.keys(QUALITIES).map((q) => `\"${q}\"`).join(\", \")}.`);\n if (!Number.isInteger(inversion) || inversion < 0 || inversion >= steps.length) throw new Error(`${symbol} has ${steps.length} notes: its inversion is 0 to ${steps.length - 1}, not ${inversion}.`);\n const root = noteNumber(m[1] + octave);\n const notes = steps.map((s) => root + s);\n for (let k = 0; k < inversion; k++) notes.push(notes.shift()! + 12);\n if (m[3]) {\n // Over another bass: that note, below the rest.\n let bass = noteNumber(m[3] + octave);\n while (bass >= notes[0]) bass -= 12;\n notes.unshift(bass);\n }\n return notes;\n}\n\n/**\n * A chord's notes, as a step plays them: `chord(\"Am\")` is \"A4+C5+E5\", its root in octave 4 (`octave`); `inversion`\n * moves its lowest notes up an octave, one each — `chord(\"E\", 3, 1)` is \"G#3+B3+E4\". \"C/E\" is C over an E below it.\n */\nexport function chord(symbol: string, octave: number = 4, inversion: number = 0): string {\n return chordNumbers(symbol, octave, inversion).map(nameOf).join(\"+\");\n}\n\n/** Each chord's notes as numbers, lowest first: the ones written, or the symbol's at `octave`. */\nconst notesOf = (c: string, octave: number): number[] => (/\\d/.test(c) ? c.split(\"+\").map(noteNumber).sort((a, b) => a - b) : chordNumbers(c, octave, 0));\n\n/**\n * A chord played as `template` says: its steps name the chord's notes by number, 1 the lowest — \"1 3 2 3\", \"1+2+3 - - -\"\n * — past the last, round again an octave up (in a three-note chord, 4 is 1 an octave up); each ^ after a number takes it\n * an octave higher, each _ an octave lower (\"1__\" the root two octaves down, for a bass). \"-\", \".\" and \"@\" as in any\n * pattern. `notes` is a chord's notes (\"A3+C4+E4\") or its name (\"Am\", with its root in octave `octave`).\n */\nexport function over(notes: string, template: Pattern, octave: number = 4): string {\n const n = notesOf(notes, octave);\n return wordsOf(template).map((w, s) => {\n const { body, velocity } = velocityOf(w, s);\n if (body === \"-\" || body === \".\") return w;\n const named = body.split(\"+\").map((d) => {\n const m = /^(\\d+)([\\^_]*)$/.exec(d);\n if (!m || Number(m[1]) < 1) throw new Error(`Step ${s + 1} of a chord's template: its notes are numbers from 1, the lowest, with ^ or _ for an octave up or down — \"1\", \"3^\", \"1__\" — not ${JSON.stringify(d)}.`);\n const k = Number(m[1]) - 1, up = Math.floor(k / n.length) + (m[2].match(/\\^/g)?.length ?? 0) - (m[2].match(/_/g)?.length ?? 0);\n return nameOf(n[k % n.length] + 12 * up);\n });\n return named.join(\"+\") + (velocity === 1 ? \"\" : `@${velocity}`);\n }).join(\" \");\n}\n\n/**\n * A progression played as `template` says, a template a chord: `chords(\"Am Am E Am Dm C E Am\", \"1_ 1 2 3\")` — as\n * `over` plays one chord. Each chord's root in octave `octave` (4); `lead: true` turns each chord after the first to\n * the inversion nearest the one before (its notes move least), as a player's hands do. A text a chord.\n */\nexport function chords(progression: string | readonly string[], template: Pattern, options: { readonly octave?: number; readonly lead?: boolean } = {}): string[] {\n const octave = options.octave ?? 4, list = wordsOf(progression);\n let before: number[] | null = null;\n return list.map((c) => {\n let n = notesOf(c, octave);\n if (options.lead && before && !/\\d/.test(c)) {\n // Each inversion, an octave either side: the one whose notes are nearest those before.\n const prev: number[] = before, mean = prev.reduce((a, b) => a + b, 0) / prev.length;\n let best = n, cost = Infinity;\n for (let inv = 0; inv < n.length; inv++) for (const shift of [-12, 0, 12]) {\n const cand = chordNumbers(c, octave, inv).map((x) => x + shift);\n const d = cand.length === prev.length ? cand.reduce((s, x, i) => s + Math.abs(x - prev[i]), 0) : Math.abs(cand.reduce((a, b) => a + b, 0) / cand.length - mean) * cand.length;\n if (d < cost) { cost = d; best = cand; }\n }\n n = best;\n }\n before = n;\n return over(n.map(nameOf).join(\"+\"), template);\n });\n}\n\n/** Seconds `n` bars last at `bpm` as a song plays it (its bar a whole number of frames): how long to make a riser. */\nexport function bars(bpm: number, n: number = 1, beats: number = 4): number {\n return n * Math.max(1, Math.round((60 / bpm) * beats / FRAME)) * FRAME;\n}\n\n/**\n * What makes a part breathe with the beat, as a mix does under a kick: down to `1 − depth` on each beat (`beat`\n * seconds apart, from the clip's start) and back over `release` of a beat — a part's fx, or a section's:\n * `{ fx: music.pump(music.bars(143) / 4) }`.\n */\nexport function pump(beat: number, depth: number = 0.45, release: number = 0.55): (clip: Clip) => Clip {\n if (!(beat > 0) || !(depth >= 0 && depth <= 1) || !(release > 0 && release <= 1)) throw new Error(\"pump() takes seconds a beat, how deep (0 … 1) and how long it takes to come back, as a part of a beat (0 … 1).\");\n return (c) => {\n const points: [number, number][] = [];\n for (let t = 0; t < c.seconds; t += beat) points.push([t, 1 - depth], [Math.min(c.seconds, t + release * beat), 1], [Math.min(c.seconds, t + beat - 0.004), 1]);\n points.push([c.seconds, 1]);\n return c.envelope(...points);\n };\n}\n\n/* ── Sounds between sections ── */\n\n/** Noise rising for `seconds` through a filter opening, loudest at the end: what leads into a drop. */\nexport function riser(seconds: number, options: { readonly volume?: number; readonly rate?: number } = {}): Clip {\n const rate = options.rate ?? 44100, v = options.volume ?? 0.5, s = seconds;\n const swept = noise(s, { volume: v, rate }).filter(\"bandpass\", [[0, 350], [s * 0.6, 1800], [s, 9000]], { resonance: 0.55 });\n const lift = tone([[0, 220], [s, 880]], s, { wave: \"saw\", voices: 3, detune: 25, width: 0.8, volume: v * 0.25, rate }).filter(\"lowpass\", [[0, 500], [s, 6000]]);\n return swept.mix(lift).envelope([0, 0], [s * 0.5, 0.25], [s * 0.9, 0.85], [s, 1]).fade(0, 0.01);\n}\n\n/** A boom to land on: a falling sine under a burst of noise, ringing `seconds` (1.5). */\nexport function impact(options: { readonly seconds?: number; readonly volume?: number; readonly rate?: number } = {}): Clip {\n const rate = options.rate ?? 44100, v = options.volume ?? 0.8, s = options.seconds ?? 1.5;\n return tone([[0, 90], [0.4, 38], [s, 30]], s, { volume: v, rate }).envelope([0, 1], [0.15, 0.7], [s, 0])\n .mix(noise(s, { volume: v * 0.5, rate }).filter(\"lowpass\", [[0, 6000], [s, 300]]).envelope([0, 1], [0.05, 0.4], [s, 0])).drive(3);\n}\n\n/** How a track or a part is heard. */\nexport interface PartOptions {\n /** Where it is heard, −1 left to 1 right; 0, the middle, when left out. */\n readonly pan?: number;\n /** How loud, 1 as the instrument plays. */\n readonly volume?: number;\n /** What it goes through: its whole part in `render`, each note on its own in `play`. */\n readonly fx?: (clip: Clip) => Clip;\n /** How much of it goes to the song's reverb in `render`, 0 … 1 (0). */\n readonly reverb?: number;\n}\n\n/** What an instrument plays, step by step. */\nexport interface Track {\n readonly instrument: Instrument | Kit;\n readonly steps: readonly string[];\n /** Where it is heard, −1 left to 1 right; 0, the middle, when left out. */\n readonly pan: number;\n /** How loud, 1 as the instrument plays. */\n readonly volume: number;\n /** What the track goes through: its whole part in `render`, each note on its own in `play`. */\n readonly fx?: (clip: Clip) => Clip;\n /** How much of it goes to the song's reverb in `render`, 0 … 1 (0). */\n readonly reverb: number;\n}\n\n/** A part's options checked, and its defaults filled in. */\nfunction heard(options: PartOptions, what: string): { pan: number; volume: number; reverb: number; fx?: (clip: Clip) => Clip } {\n const pan = options.pan ?? 0, volume = options.volume ?? 1, reverb = options.reverb ?? 0;\n if (!(pan >= -1 && pan <= 1)) throw new Error(`A ${what}'s pan is −1 (left) to 1 (right), not ${pan}.`);\n if (!(volume >= 0)) throw new Error(`A ${what}'s volume is 0 or more, not ${volume}.`);\n if (!(reverb >= 0 && reverb <= 1)) throw new Error(`A ${what}'s reverb is 0 to 1, not ${reverb}.`);\n return { pan, volume, reverb, ...(options.fx ? { fx: options.fx } : {}) };\n}\n\n/**\n * A pattern for an instrument: steps a word each — a note or notes joined by +, \"-\" to hold, \".\" to rest, \"|\" for the\n * eye; \"@\" after a step's notes says how loud, \"C4@0.6\". `pan`: where it is heard, −1 left to 1 right (a panned track's\n * notes have two channels); `volume`: 1 as the instrument plays; `fx`: what the track goes through —\n * `(c) => c.delay(0.21).chorus()` — its whole part when the song is rendered (as a mixer's insert), each note on its own\n * when it is played note by note; `reverb`: how much goes to the song's reverb when it is rendered, 0 … 1.\n */\nexport function track(instrument: Instrument | Kit, pattern: Pattern, options: PartOptions = {}): Track {\n return Object.freeze({ instrument, steps: Object.freeze(wordsOf(pattern)), ...heard(options, \"track\") });\n}\n\n/** An instrument and how it is heard, which a song's sections give its notes: `arrange`. */\nexport interface Part extends Omit { readonly kind: \"part\" }\n\n/** An instrument as a part of a song made of sections (`arrange`), heard as `track` says. */\nexport function part(instrument: Instrument | Kit, options: PartOptions = {}): Part {\n return Object.freeze({ kind: \"part\" as const, instrument, ...heard(options, \"part\") });\n}\n\n/** How long a section lasts, as its effects need it for their points: seconds, its bars, and seconds a bar. */\nexport interface SectionTime { readonly seconds: number; readonly bars: number; readonly bar: number }\n\n/** What a section is and does, besides its notes. */\nexport interface SectionOptions {\n /** What `song.sections` calls it. */\n readonly name?: string;\n /**\n * What a part goes through in this section only, given how long the section is — a filter opening over a build:\n * `{ pad: (c, t) => c.filter(\"lowpass\", [[0, 400], [t.seconds, 8000]]) }`. Its notes from this section, tails and\n * all, before the part's own `fx`. Mixed by `render` only: a song with one is not played note by note.\n */\n readonly fx?: Readonly Clip>>;\n /** How loud a part is in this section only, 1 as it is. */\n readonly volume?: Readonly>;\n}\n\n/** Bars of a song: what each part plays in them. */\nexport interface Section {\n readonly kind: \"section\";\n readonly name: string;\n readonly bars: number;\n readonly parts: Readonly>;\n readonly fx: Readonly Clip>>;\n readonly volume: Readonly>;\n /** This section with some parts written again — null: that part rests — and these options over its own. */\n with(parts: Readonly>, options?: SectionOptions): Section;\n}\n\n/**\n * `bars` bars of a song (`arrange`), and what each part plays in them, by the part's name: `{ lead: tune, drums: beat }`.\n * A pattern shorter than the section plays again until it is filled, so a bar of drums fills eight; a part not named\n * rests. `options`: its name, a part's effects or volume for this section only.\n */\nexport function section(bars: number, parts: Readonly>, options: SectionOptions = {}): Section {\n if (!Number.isInteger(bars) || bars < 1) throw new Error(`A section is a whole number of bars, 1 or more — not ${bars}.`);\n for (const [k, v] of Object.entries(options.volume ?? {})) if (!(v >= 0)) throw new Error(`A section's volume for ${k} is 0 or more, not ${v}.`);\n const made: Section = Object.freeze({\n kind: \"section\" as const, name: options.name ?? \"\", bars,\n parts: Object.freeze({ ...parts }), fx: Object.freeze({ ...(options.fx ?? {}) }), volume: Object.freeze({ ...(options.volume ?? {}) }),\n with(changes: Readonly>, more: SectionOptions = {}): Section {\n const next: Record = { ...parts };\n for (const [k, p] of Object.entries(changes)) { if (p === null) delete next[k]; else next[k] = p; }\n return section(bars, next, { name: more.name ?? made.name, fx: { ...made.fx, ...(more.fx ?? {}) }, volume: { ...made.volume, ...(more.volume ?? {}) } });\n },\n });\n return made;\n}\n\n/** A song's tempo and grid. */\nexport interface SongOptions {\n readonly bpm: number;\n /** Steps a beat (4: sixteenths). */\n readonly steps?: number;\n /** Beats a bar (4). */\n readonly beats?: number;\n /** How late every second step starts, as a part of a step: 0 straight (as it is when left out), 1/3 a triplet's lilt. */\n readonly swing?: number;\n}\n\n/** Where a song's section is: from bar `bar` (0 the first) for `bars`, from frame `frame`. */\nexport interface SongSection { readonly name: string; readonly bar: number; readonly bars: number; readonly frame: number }\n\n/** Steps of a track that belong to one section, and what that section does to them. */\ninterface Piece { readonly from: number; readonly to: number; readonly volume: number; readonly fx?: (clip: Clip, time: SectionTime) => Clip; readonly time: SectionTime }\nconst piecesOf = new WeakMap();\n/** A song of sections' parts, by name, and the track each became: what `stems` cuts. */\nconst partsOf = new WeakMap();\n\n/**\n * A song made of sections, one after another: `parts` names each instrument and how it is heard; each section says\n * what the parts play in it. A section can be written once and played again — `arrange(o, parts, intro, verse, verse,\n * chorus)` — or played again changed: `verse.with({ lead: transpose(tune, 12) })`.\n */\nexport function arrange(options: SongOptions, parts: Readonly>, ...sections: readonly Section[]): Song {\n const { perBar, barFrames } = grid(options);\n if (sections.length === 0) throw new Error(\"arrange() takes the song's sections, one at least.\");\n const names = Object.keys(parts);\n for (const s of sections) for (const k of [...Object.keys(s.parts), ...Object.keys(s.fx), ...Object.keys(s.volume)]) {\n if (!(k in parts)) throw new Error(`A section${s.name ? ` (${s.name})` : \"\"} names ${JSON.stringify(k)}, which is no part of the song: its parts are ${names.join(\", \")}.`);\n }\n const tracks = names.map((k) => {\n const steps: string[] = [], pieces: Piece[] = [];\n for (const s of sections) {\n const length = s.bars * perBar, from = steps.length, p = s.parts[k];\n if (p === undefined) for (let i = 0; i < length; i++) steps.push(\".\");\n else {\n const words = wordsOf(p);\n if (words.length === 0 || length % words.length !== 0) throw new Error(`${k}${s.name ? ` in ${s.name}` : \"\"}: a pattern of ${words.length} steps does not fill ${s.bars} bar${s.bars === 1 ? \"\" : \"s\"} of ${perBar} steps evenly.`);\n for (let i = 0; i < length; i++) steps.push(words[i % words.length]);\n }\n const fx = k in s.fx ? s.fx[k] : undefined, volume = s.volume[k] ?? 1;\n if (fx || volume !== 1) pieces.push({ from, to: from + length, volume, ...(fx ? { fx } : {}), time: { seconds: s.bars * barFrames * FRAME, bars: s.bars, bar: barFrames * FRAME } });\n }\n const t = track(parts[k].instrument, steps, parts[k]);\n if (pieces.length) piecesOf.set(t, pieces);\n return t;\n });\n let bar = 0;\n const placedSections = sections.map((s) => { const at = { name: s.name, bar, bars: s.bars, frame: bar * barFrames }; bar += s.bars; return Object.freeze(at); });\n const made = compose(options, tracks, placedSections);\n partsOf.set(made, names.map((k, i) => ({ name: k, track: tracks[i] })));\n return made;\n}\n\n/** A clip as a track plays it note by note: through its fx, at its volume and its pan, made once a clip a track. */\nfunction placed(t: Track, made: Map, c: Clip): Clip {\n if (t.pan === 0 && t.volume === 1 && !t.fx) return c;\n let p = made.get(c);\n if (!p) {\n p = t.fx ? t.fx(c) : c;\n if (t.volume !== 1) p = p.gain(t.volume);\n if (t.pan !== 0) p = p.pan(t.pan);\n made.set(c, p);\n }\n return p;\n}\n\n/** What a song's notes are, kept for render, which makes them again at its own rate; `piece`, the section whose effects it goes through. */\ninterface Score { readonly at: number; readonly track: Track; readonly made: Map; readonly velocity: number; readonly piece?: Piece; readonly name?: string; readonly seconds?: number; readonly hit?: Clip }\nconst scores = new WeakMap();\n\n/** A song, when the script is built: the tables `play` reads, and what it takes. */\nexport interface Song {\n /**\n * Frames a step lasts: a bar is a whole number of frames, a step need not be (2.5 for sixteenths at 143 beats a\n * minute). Played note by note, a note starts on the frame nearest its time — within the 21 ms the game's own\n * mixer starts a sound in anyway; rendered, exactly on it.\n */\n readonly stepFrames: number;\n /** Frames a bar lasts, a whole number. */\n readonly barFrames: number;\n /** Beats a bar. */\n readonly beats: number;\n /** The tempo it plays at: the nearest to the one asked for whose bar is a whole number of frames. */\n readonly bpm: number;\n /** Steps, the longest track's. */\n readonly steps: number;\n /** Its sections, made by `arrange`, in the order they play (none for a song of tracks). */\n readonly sections: readonly SongSection[];\n /** Frames it lasts, its last note's release included. */\n readonly frames: number;\n readonly seconds: number;\n /** Each clip the song plays, once. */\n readonly clips: readonly Clip[];\n /** Each note in the order they start: the frame it starts on, from 0, and which of `clips` it is. */\n readonly at: readonly number[];\n readonly which: readonly number[];\n /** The most notes sounding at once: `play` refuses a song of more than 32 (`render` has no limit). */\n readonly voices: number;\n}\n\n/** A song's grid: steps a bar, and frames a bar and a step. */\nfunction grid(options: SongOptions): { perBar: number; barFrames: number; stepFrames: number } {\n const perBeat = options.steps ?? 4, beats = options.beats ?? 4, swing = options.swing ?? 0;\n if (!(options.bpm > 0) || !(perBeat > 0) || !(beats > 0)) throw new Error(\"A song takes a tempo, { bpm: 120 }, steps a beat (4 by default) and beats a bar (4).\");\n if (!(swing >= 0 && swing < 1)) throw new Error(`A song's swing is 0 (straight) to under 1 (a step late), not ${swing}.`);\n const barFrames = Math.max(1, Math.round((60 / options.bpm) * beats / FRAME));\n return { perBar: perBeat * beats, barFrames, stepFrames: barFrames / (beats * perBeat) };\n}\n\n/**\n * Tracks played together from their first step: `bpm` beats a minute, `steps` steps a beat (4 by default), `swing`\n * every second step late. Played note by note (`play`), a song is refused when more than 32 notes would sound at once;\n * rendered, it has no limit. A song of sections: `arrange`.\n */\nexport function song(options: SongOptions, ...tracks: readonly Track[]): Song {\n return compose(options, tracks, []);\n}\n\nfunction compose(options: SongOptions, tracks: readonly Track[], sections: readonly SongSection[]): Song {\n const { barFrames, stepFrames } = grid(options), beats = options.beats ?? 4, swing = options.swing ?? 0;\n const stepSeconds = stepFrames * FRAME;\n // Where step s starts, in frames: every second step `swing` of a step late.\n const time = (s: number) => (s + (s % 2 === 1 ? swing : 0)) * stepFrames;\n const score: Score[] = [];\n let steps = 0, effects = false;\n for (const t of tracks) {\n const ws = t.steps, made = new Map(), pieces = piecesOf.get(t) ?? [];\n if (pieces.some((p) => p.fx)) effects = true;\n steps = Math.max(steps, ws.length);\n let k = 0;\n ws.forEach((w, s) => {\n while (k < pieces.length && pieces[k].to <= s) k++;\n const piece = k < pieces.length && pieces[k].from <= s ? pieces[k] : undefined;\n const { body, velocity: v } = velocityOf(w, s);\n if (body === \"-\" || body === \".\") return;\n const velocity = v * (piece?.volume ?? 1), into = piece ? { piece } : {};\n if (t.instrument.kind === \"kit\") {\n for (const letter of body) {\n const hit = t.instrument.hits[letter];\n if (!hit) throw new Error(`Step ${s + 1}: the kit has no ${JSON.stringify(letter)} (it has ${Object.keys(t.instrument.hits).join(\", \")}).`);\n score.push({ at: time(s), track: t, made, velocity, ...into, hit });\n }\n return;\n }\n // A note lasts its step and every \"-\" after it.\n let length = 1;\n while (s + length < ws.length && ws[s + length] === \"-\") length++;\n const seconds = swing === 0 ? length * stepSeconds : (time(s + length) - time(s)) * FRAME;\n for (const name of body.split(\"+\")) {\n noteNumber(name);\n score.push({ at: time(s), track: t, made, velocity, ...into, name, seconds });\n }\n });\n }\n score.sort((a, b) => a.at - b.at);\n const base = (n: Score) => n.hit ?? (n.track.instrument as Instrument).note(n.name!, n.seconds!);\n // The tables `play` reads, made when they are first read: a song only rendered never makes its notes for the game.\n let table: { clips: readonly Clip[]; at: readonly number[]; which: readonly number[]; voices: number; peakStep: number; frames: number } | null = null;\n const tables = () => {\n if (table) return table;\n const notes = score.map((n) => ({ at: Math.round(n.at), clip: placed(n.track, n.made, n.velocity === 1 ? base(n) : base(n).gain(n.velocity)) }));\n // How many sound at once: at each note's start, those started before it and not yet over.\n let voices = 0, peakStep = 0;\n notes.forEach((n, k) => {\n let sounding = 0;\n for (let j = 0; j <= k; j++) if (notes[j].at + notes[j].clip.seconds / FRAME > n.at) sounding++;\n if (sounding > voices) { voices = sounding; peakStep = Math.round(n.at / stepFrames) + 1; }\n });\n const clips: Clip[] = [];\n const which = notes.map((n) => { let k = clips.indexOf(n.clip); if (k < 0) { clips.push(n.clip); k = clips.length - 1; } return k; });\n const frames = Math.max(Math.round(steps * stepFrames), ...notes.map((n) => n.at + Math.ceil(n.clip.seconds / FRAME)));\n table = { clips: Object.freeze(clips), at: Object.freeze(notes.map((n) => n.at)), which: Object.freeze(which), voices, peakStep, frames };\n return table;\n };\n // What the game plays note by note: refused, when it is read, if more than 32 would sound at once (render has no limit).\n const playable = () => {\n if (effects) throw new Error(\"A section of this song puts a part through effects of its own, which only music.render mixes: render the song, or give the part those effects for the whole song.\");\n const t = tables();\n if (t.voices > VOICES) throw new Error(`At step ${t.peakStep}, ${t.voices} notes would sound at once; the game plays ${VOICES} note by note. Shorten notes or thin the chords — or music.render the song, one sound however many it mixes.`);\n return t;\n };\n const made: Song = Object.freeze({\n stepFrames, barFrames, beats, bpm: (60 * beats) / (barFrames * FRAME), steps, sections: Object.freeze([...sections]),\n get frames() { return tables().frames; },\n get seconds() { return tables().frames * FRAME; },\n get clips() { return playable().clips; },\n get at() { return playable().at; },\n get which() { return playable().which; },\n get voices() { return tables().voices; },\n });\n scores.set(made, score);\n return made;\n}\n\n/** The General MIDI drums, by number, as `drums()` has them: kicks k, snares s, the clap c, hats h and o, toms t, the rim r, crashes x. */\nconst GM_DRUMS: Readonly> = {\n 35: \"k\", 36: \"k\", 37: \"r\", 38: \"s\", 40: \"s\", 39: \"c\", 42: \"h\", 44: \"h\", 51: \"h\", 53: \"h\", 59: \"h\", 46: \"o\",\n 41: \"t\", 43: \"t\", 45: \"t\", 47: \"t\", 48: \"t\", 50: \"t\", 49: \"x\", 52: \"x\", 55: \"x\", 57: \"x\",\n};\n\n/** How imported notes are made a song: its grid, and which kit letter a drum's number plays. */\nexport interface MidiOptions extends Omit {\n /** Beats a minute: the file's own when left out. */\n readonly bpm?: number;\n /** A drum's MIDI number to a kit's letter, over General MIDI's (36 \"k\", 38 \"s\", 42 \"h\", 46 \"o\", 49 \"x\", …). */\n readonly drums?: Readonly>;\n}\n\n/**\n * A song of imported notes (`import tune from \"./tune.notes\"`): each track `parts` names — by its name, or its place\n * in `tune.tracks` from 0 — played by an instrument, a kit (the drums by their General MIDI numbers) or a `part`;\n * the tracks not named are left out. Each note starts on the step nearest its time and lasts the steps nearest its\n * length (`steps` a beat, 4: sixteenths); how hard it was struck is how loud it plays, 100 as written. A track whose\n * notes overlap becomes several, so the song can hold them. At the file's first tempo unless `bpm` says.\n */\nexport function midi(notes: Notes, parts: Readonly>, options: MidiOptions = {}): Song {\n const perBeat = options.steps ?? 4, map = { ...GM_DRUMS, ...(options.drums ?? {}) };\n const names = notes.tracks.map((t) => t.name);\n const tracks: Track[] = [];\n for (const [key, given] of Object.entries(parts)) {\n const index = names.indexOf(key) >= 0 ? names.indexOf(key) : /^\\d+$/.test(key) ? Number(key) : -1;\n const source = notes.tracks[index];\n if (!source) throw new Error(`The notes have no track ${JSON.stringify(key)}: their tracks are ${names.map((n) => JSON.stringify(n)).join(\", \")} (or 0 to ${names.length - 1}).`);\n const instrument = given.kind === \"part\" ? given.instrument : given, how = given.kind === \"part\" ? given : {};\n const kit = instrument.kind === \"kit\" ? instrument : null;\n // Voices: steps that each hold one chord at a time; a note joins a chord struck with it, or the first voice free.\n type Word = { sounds: string[]; velocity: number; length: number };\n const voices: { words: Map; end: number; last: number }[] = [];\n const placed = source.notes.map((n) => {\n const from = Math.round(n.at * perBeat);\n return { from, length: kit ? 1 : Math.max(1, Math.round((n.at + n.beats) * perBeat) - from), sound: kit ? map[n.pitch] : nameOf(n.pitch), velocity: Math.max(0.05, Math.round((n.velocity / 100) * 20) / 20) };\n }).filter((n) => n.sound !== undefined && (!kit || n.sound in kit.hits)).sort((a, b) => a.from - b.from || b.length - a.length);\n for (const n of placed) {\n let voice = voices.find((v) => { const w = v.words.get(v.last); return v.last === n.from && w !== undefined && w.length === n.length && w.velocity === n.velocity; });\n if (voice) { const w = voice.words.get(n.from)!; if (!w.sounds.includes(n.sound)) w.sounds.push(n.sound); continue; }\n voice = voices.find((v) => v.end <= n.from);\n if (!voice) { voice = { words: new Map(), end: 0, last: -1 }; voices.push(voice); }\n voice.words.set(n.from, { sounds: [n.sound], velocity: n.velocity, length: n.length });\n voice.end = n.from + n.length;\n voice.last = n.from;\n }\n for (const v of voices) {\n const steps: string[] = Array.from({ length: v.end }, () => \".\");\n for (const [at, w] of v.words) {\n steps[at] = (kit ? w.sounds.join(\"\") : w.sounds.join(\"+\")) + (w.velocity === 1 ? \"\" : `@${w.velocity}`);\n if (!kit) for (let k = 1; k < w.length; k++) steps[at + k] = \"-\";\n }\n tracks.push(track(instrument, steps, how));\n }\n }\n return compose({ ...options, bpm: options.bpm ?? notes.bpm, beats: options.beats ?? notes.beats }, tracks, []);\n}\n\n/** The song's reverb in `render`, as `Clip.reverb` takes it, the dry part left out. */\nexport interface ReverbOptions { readonly decay?: number; readonly damping?: number; readonly predelay?: number; readonly width?: number }\n\n/**\n * A song mixed into one clip when the script is built: every note exactly in time, one sound playing. Mixed as a\n * mixing desk does: each track's notes made again at `rate` (44 100 when left out: what the ear hears up to 20 kHz; a\n * note played in the game note by note is made at 22 050, to keep the map small) into its own part, through a\n * section's effects where it has them, then its `fx`, at its volume, to its side; what each track sends (its `reverb`)\n * into one reverb shared by them all (`reverb`, its room); the whole through `master` —\n * `(mix) => mix.compress({ threshold: -14, ratio: 2 })` — and last a limiter (`Clip.limit`) so it never goes over\n * −1.5 dB however the parts add, between the samples too: `limit: false` leaves it as mixed, `{ ceiling, release }` sets it. A long clip a\n * program plays goes in the map as Ogg Vorbis, two channels when it has two.\n */\nexport function render(s: Song, options: { readonly rate?: number; readonly limit?: boolean | { readonly ceiling?: number; readonly release?: number }; readonly reverb?: ReverbOptions; readonly master?: (mix: Clip) => Clip } = {}): Clip {\n const rate = options.rate ?? 44100, score = scores.get(s), least = s.steps * s.stepFrames * FRAME;\n let mixed: Clip;\n if (!score) {\n // (A song from elsewhere, without its score: its own clips, at their rate.)\n mixed = mixdown(s.at.map((at, k) => ({ clip: s.clips[s.which[k]], at: at * FRAME })), s.seconds, rate);\n } else {\n const parts: { clip: Clip; at: number }[] = [], sends: { clip: Clip; at: number }[] = [];\n const byTrack = new Map();\n for (const n of score) { const list = byTrack.get(n.track); if (list) list.push(n); else byTrack.set(n.track, [n]); }\n for (const [t, notes] of byTrack) {\n const clipOf = (n: Score) => { const c = n.hit ?? (t.instrument as Instrument).note(n.name!, n.seconds!, rate); return n.velocity === 1 ? c : c.gain(n.velocity); };\n // The notes as they are, and each section's that has effects of its own mixed apart and through them.\n const entries: { clip: Clip; at: number }[] = [], apart = new Map();\n for (const n of notes) {\n if (n.piece?.fx) { const list = apart.get(n.piece); if (list) list.push(n); else apart.set(n.piece, [n]); }\n else entries.push({ clip: clipOf(n), at: n.at * FRAME });\n }\n for (const [p, list] of apart) {\n const from = p.from * s.stepFrames;\n const sub = mixdown(list.map((n) => ({ clip: clipOf(n), at: (n.at - from) * FRAME })), p.time.seconds, rate);\n entries.push({ clip: p.fx!(sub, p.time), at: from * FRAME });\n }\n let part = mixdown(entries, least, rate);\n if (t.fx) part = t.fx(part);\n if (t.volume !== 1) part = part.gain(t.volume);\n if (t.pan !== 0) part = part.pan(t.pan);\n parts.push({ clip: part, at: 0 });\n if (t.reverb > 0) sends.push({ clip: part.gain(t.reverb), at: 0 });\n }\n if (sends.length) {\n const r = options.reverb ?? {};\n parts.push({ clip: mixdown(sends, least, rate).reverb({ decay: r.decay ?? 0.7, damping: r.damping ?? 0.45, predelay: r.predelay ?? 0.02, width: r.width ?? 1, mix: 1, dry: 0 }), at: 0 });\n }\n mixed = mixdown(parts, least, rate);\n }\n if (options.master) mixed = options.master(mixed);\n const limit = options.limit ?? true;\n return limit === false ? mixed : mixed.limit(limit === true ? {} : limit);\n}\n\n/* ── Music that follows the game: a song cut into pieces, played piece by piece ── */\n\n/** How a song of sections is cut for the game to play piece by piece (`stems`). */\nexport interface StemsOptions {\n /**\n * Which parts each layer holds, by name — `{ drums: [\"kit\", \"hats\"], bass: [\"bass\"], melody: [\"lead\", \"saw\"] }` — a\n * layer the game can turn on and off where a piece begins. A part in no layer is left out.\n */\n readonly layers: Readonly>;\n /**\n * Beats a piece: where the game can go to another section or turn a layer on or off (a bar's beats when left out).\n * Two in a bar of four halves the wait for a change, for about half as much map again and more sounds at once.\n */\n readonly every?: number;\n /** Samples a second (44 100). */\n readonly rate?: number;\n /** The reverb's room, as `render` takes it. */\n readonly reverb?: ReverbOptions;\n /** What each piece's clip goes through last — an EQ, not a compressor: each is on its own, the game adds them up. */\n readonly master?: (clip: Clip) => Clip;\n /** The loudest the pieces may add up to as the song plays through, every layer on (0.84, −1.5 dB). */\n readonly peak?: number;\n /**\n * Seconds a piece's clip rings on after its notes end (2), its echoes and reverb faded out over the last of them: the\n * pieces after it cover the rest, and a shorter clip is a smaller map. A layer's last piece keeps all of its tail.\n */\n readonly tail?: number;\n /** The Ogg Vorbis quality of the pieces' clips (4: a little smaller than a song's 5, as many play at once). */\n readonly quality?: number;\n /**\n * Sounds to play on the next beat when the game asks (`band.sting`): a crash for a cleared line, a hit for a boss.\n * Turned down with the pieces, which leave room for one on any beat.\n */\n readonly stingers?: Readonly>;\n}\n\n/** A song cut into pieces for the game to play piece by piece, with `conduct` and a `Band`: made when the script is built. */\nexport interface Stems {\n readonly kind: \"stems\";\n /** Frames a bar lasts. */\n readonly barFrames: number;\n readonly bpm: number;\n /** Beats a bar, and beats a piece. */\n readonly beats: number;\n readonly every: number;\n /** Frames each beat of a piece lasts, whole frames (beats of a bar not a whole number of frames take turns). */\n readonly waits: readonly number[];\n /** The layers' names, in the order `band.on` has them. */\n readonly layers: readonly string[];\n /** The sections, in the order the song plays them: their names and how many bars. */\n readonly sections: readonly SongSection[];\n /**\n * Section `s`'s piece `p` in layer `l`: its notes, their tails and echoes and reverb with them, or null where it plays\n * none. A section of `bars` bars has bars × beats / every pieces.\n */\n readonly pieces: readonly (readonly (readonly (Clip | null)[])[])[];\n readonly stingers: readonly Clip[];\n /** A section's place in `sections`, by its name: `band.go(stems.section(\"drop\"))`. */\n section(name: string): number;\n /** A layer's place in `layers`, by its name: `band.mute(stems.layer(\"drums\"))`. */\n layer(name: string): number;\n /** A stinger's place, by its name: `band.sting(stems.stinger(\"crash\"))`. */\n stinger(name: string): number;\n /** The most clips sounding at once as the song plays through, every layer on: of the game's 32, what is left for the rest. */\n readonly voices: number;\n /** The pieces added up as the game plays the song through, every layer on, each on its frame: to hear it, or compare. */\n readonly playthrough: Clip;\n}\n\n/**\n * A song of sections (`arrange`) cut into pieces — a bar each, or `every` beats — a clip each piece of each layer, for\n * music that follows the game: where each piece begins the program says which section plays and which layers (`Band`,\n * `conduct`), and a stinger plays on the next beat. A piece's clip holds the notes that start in it, with their tails,\n * echoes and reverb, so it overlaps the next and nothing is cut off, whichever comes after it; a piece the same as\n * another is the same clip, one file in the map. Each layer is mixed as `render` mixes the song, but with no master\n * and no limiter between the parts — the game adds the clips up — so all of them are turned down together until the\n * song played through stays under `peak`.\n */\nexport function stems(s: Song, options: StemsOptions): Stems {\n const parts = partsOf.get(s), score = scores.get(s);\n if (!parts || !score) throw new Error(\"stems() cuts a song made of sections, by music.arrange.\");\n const rate = options.rate ?? 44100, layers = Object.keys(options.layers), r = options.reverb ?? {}, tail = options.tail ?? 2;\n if (!(tail > 0)) throw new Error(`stems()' tail is seconds after a piece, more than 0, not ${tail}.`);\n if (layers.length === 0 || layers.length > 8) throw new Error(`stems() takes one to eight layers, not ${layers.length}.`);\n const beats = s.beats, every = options.every ?? beats, piece = (s.barFrames * every) / beats;\n if (!Number.isInteger(every) || every < 1 || every > beats || beats % every !== 0) throw new Error(`stems() cuts every few beats of a bar's ${beats}, a number that divides it — not every ${every}.`);\n if (!Number.isInteger(piece)) throw new Error(`A piece of ${every} beats would be ${piece.toFixed(2)} frames: the game plays a sound on a frame. Make it a bar (every: ${beats}), or a tempo whose bar divides into whole frames.`);\n const perBar = beats / every;\n const layerOf = new Map();\n layers.forEach((name, l) => {\n for (const p of options.layers[name]) {\n const found = parts.find((x) => x.name === p);\n if (!found) throw new Error(`The layer ${name} names ${JSON.stringify(p)}, which is no part of the song: its parts are ${parts.map((x) => x.name).join(\", \")}.`);\n if (layerOf.has(found.track)) throw new Error(`${p} is in two layers: a part is in one at most.`);\n layerOf.set(found.track, l);\n }\n });\n const pieceSeconds = piece * FRAME, total = s.sections.reduce((n, x) => n + x.bars, 0) * perBar;\n // The notes of each piece of each layer, by part.\n const cells: Map[][] = Array.from({ length: total }, () => layers.map(() => new Map()));\n for (const n of score) {\n const l = layerOf.get(n.track);\n if (l === undefined) continue;\n const g = Math.min(total - 1, Math.floor((n.at + 1e-6) / piece));\n const m = cells[g][l], list = m.get(n.track);\n if (list) list.push(n); else m.set(n.track, [n]);\n }\n const clipOf = (t: Track, n: Score) => { const c = n.hit ?? (t.instrument as Instrument).note(n.name!, n.seconds!, rate); return n.velocity === 1 ? c : c.gain(n.velocity); };\n // Each layer's last piece with notes: it rings out whole, as the song ends.\n const last = layers.map((_, l) => { let g = total - 1; while (g > 0 && cells[g][l].size === 0) g--; return g; });\n // A piece of a layer, mixed: each part's notes from where it begins, through what the part goes through, and the reverb.\n const raw: (Clip | null)[][] = cells.map((byLayer, g) => byLayer.map((m, l) => {\n const out: { clip: Clip; at: number }[] = [], sends: { clip: Clip; at: number }[] = [];\n // Where the piece's last note ends, its release and all: a riser four bars long is kept whole.\n let end = pieceSeconds;\n for (const [t, notes] of m) for (const n of notes) end = Math.max(end, (n.at - g * piece) * FRAME + clipOf(t, n).seconds);\n for (const [t, notes] of m) {\n const plain = notes.filter((n) => !n.piece?.fx), apart = notes.filter((n) => n.piece?.fx);\n const entries: { clip: Clip; at: number }[] = plain.map((n) => ({ clip: clipOf(t, n), at: (n.at - g * piece) * FRAME }));\n if (apart.length) {\n // A section's effects take the whole section: this piece's notes where they fall in it, then the piece cut out.\n const p = apart[0].piece!, from = p.from * s.stepFrames, into = (g * piece - from) * FRAME;\n const whole = p.fx!(mixdown(apart.map((n) => ({ clip: clipOf(t, n), at: (n.at - from) * FRAME })), undefined, rate), p.time);\n entries.push({ clip: whole.trim(into, whole.seconds), at: 0 });\n }\n let part = mixdown(entries, undefined, rate);\n if (t.fx) part = t.fx(part);\n if (t.volume !== 1) part = part.gain(t.volume);\n if (t.pan !== 0) part = part.pan(t.pan);\n out.push({ clip: part, at: 0 });\n if (t.reverb > 0) sends.push({ clip: part.gain(t.reverb), at: 0 });\n }\n if (out.length === 0) return null;\n if (sends.length) out.push({ clip: mixdown(sends, undefined, rate).reverb({ decay: r.decay ?? 0.7, damping: r.damping ?? 0.45, predelay: r.predelay ?? 0.02, width: r.width ?? 1, mix: 1, dry: 0 }), at: 0 });\n let mixed = mixdown(out, undefined, rate);\n if (options.master) mixed = options.master(mixed);\n // What rings on after the piece's notes, kept for `tail` seconds and faded over the last of them — the pieces\n // after cover the rest — but all of it after the layer's last piece.\n const keep = end + tail;\n return g < last[l] && mixed.seconds > keep ? mixed.trim(0, keep).fade(0, Math.min(1, tail / 2)) : mixed;\n }));\n // All of them turned down together, as the song plays through.\n const placed: { clip: Clip; at: number; g: number; l: number }[] = [];\n raw.forEach((byLayer, g) => byLayer.forEach((c, l) => { if (c) placed.push({ clip: c, at: g * pieceSeconds, g, l }); }));\n if (placed.length === 0) throw new Error(\"stems(): the layers play no notes.\");\n // The stingers may play on any beat: turned down with the pieces, and room left for them over the loudest.\n const waits = Array.from({ length: every }, (_, j) => Math.round(((j + 1) * piece) / every) - Math.round((j * piece) / every));\n const stingerNames = Object.keys(options.stingers ?? {});\n const lines: number[] = [];\n for (let g = 0; g < total; g++) { let at = g * piece; for (const w of waits) { lines.push(at * FRAME); at += w; } }\n const fitted = together(placed, { peak: options.peak ?? 0.84, over: stingerNames.map((k) => ({ clip: options.stingers![k], at: lines })) });\n const grid: (Clip | null)[][] = raw.map((byLayer) => byLayer.map(() => null));\n const quality = options.quality ?? 4;\n placed.forEach((p, k) => { grid[p.g][p.l] = fitted[k].ogg(quality); });\n const stingers = stingerNames.map((_, k) => fitted[placed.length + k]);\n let start = 0;\n const pieces = s.sections.map((sec) => { const n = sec.bars * perBar, rows = grid.slice(start, start + n).map((row) => Object.freeze(row)); start += n; return Object.freeze(rows); });\n const lookup = (what: string, list: readonly string[], name: string) => {\n const k = list.indexOf(name);\n if (k < 0) throw new Error(`There is no ${what} ${JSON.stringify(name)}: there are ${list.map((x) => JSON.stringify(x)).join(\", \") || \"none\"}.`);\n return k;\n };\n let voices = -1;\n return Object.freeze({\n kind: \"stems\" as const, barFrames: s.barFrames, bpm: s.bpm, beats, every, waits: Object.freeze(waits),\n layers: Object.freeze(layers), sections: s.sections, pieces: Object.freeze(pieces), stingers: Object.freeze(stingers),\n section: (name: string) => lookup(\"section\", s.sections.map((x) => x.name), name),\n layer: (name: string) => lookup(\"layer\", layers, name),\n stinger: (name: string) => lookup(\"stinger\", stingerNames, name),\n get playthrough() {\n return mixdown(placed.map((p) => ({ clip: grid[p.g][p.l]!, at: p.at })));\n },\n get voices() {\n if (voices < 0) {\n voices = 0;\n for (const p of placed) { let n = 0; for (const q of placed) if (q.at <= p.at && q.at + q.clip.seconds > p.at) n++; voices = Math.max(voices, n); }\n }\n return voices;\n },\n });\n}\n\n/**\n * The music as the game steers it: which section plays, which piece of it, which layers. Make one inside the program\n * that steers the music and hand it to `conduct`, which plays a piece where each begins as it says:\n *\n * import { music, Band } from \"trigscript/music\";\n * const band = new Band();\n * start(music.conduct(stems, band));\n * band.go(stems.section(\"drop\")); // where the next piece begins\n * band.mute(stems.layer(\"drums\"));\n * band.sting(stems.stinger(\"crash\")); // on the next beat\n *\n * With nothing said, the song plays through its sections in order and starts again.\n */\nexport class Band {\n /** The section playing, its place in `stems.sections`; the piece of it (a bar, unless `stems.every` says), from 0; the beat of the piece. */\n section = 0;\n piece = 0;\n beat = 0;\n /** Pieces played since it started. */\n count = 0;\n /** The game's frame the beat playing began on: how far into the beat it is, for what moves with the music. */\n at = 0;\n /** The section to go to, and whether where the next piece begins (`go`) or when this section ends (`queue`); −1 for none. */\n next = -1;\n now = false;\n /** Whether a section plays again when it ends, instead of going on to the next. */\n loop = false;\n /** Which layers play, by their place in `stems.layers`. */\n on = [true, true, true, true, true, true, true, true];\n /** A stinger to play on the next beat, by its place; −1 for none. */\n stung = -1;\n /** Whether it plays: false stops it where the next piece would begin (what sounds rings out). */\n playing = true;\n\n /** Go to a section where the next piece begins, from its first piece. */\n go(section: number) {\n this.next = section;\n this.now = true;\n }\n /** Go to a section when the one playing ends. */\n queue(section: number) {\n this.next = section;\n this.now = false;\n }\n /** Turn a layer off, or on, from where the next piece begins. */\n mute(layer: number) {\n this.on[layer] = false;\n }\n unmute(layer: number) {\n this.on[layer] = true;\n }\n /** Play a stinger on the next beat. */\n sting(stinger: number) {\n this.stung = stinger;\n }\n /** Stop where the next piece would begin. */\n stop() {\n this.playing = false;\n }\n}\n\n/** Play `stems` as `band` says, beat by beat, until it stops: `start(music.conduct(stems, band))` beside the program. */\nexport const conduct = game(async (s: Stems, band: Band) => {\n band.playing = true;\n band.beat = 0;\n while (band.playing || band.beat > 0) {\n band.at = memory.game.frame;\n if (band.beat === 0) {\n // Where a piece begins: where to be, then the piece's clip of each layer that is on. (Which clip, by test: a\n // clip is no value a program holds.)\n if (band.next >= 0 && (band.now || band.piece === 0)) {\n band.section = band.next;\n band.piece = 0;\n band.next = -1;\n }\n for (let k = 0; k < s.sections.length; k++) if (band.section === k) {\n for (let p = 0; p < s.pieces[k].length; p++) if (band.piece === p) {\n for (let l = 0; l < s.layers.length; l++) if (s.pieces[k][p][l]) if (band.on[l]) playWav(s.pieces[k][p][l]!, 0);\n }\n }\n }\n for (let j = 0; j < s.stingers.length; j++) if (band.stung === j) playWav(s.stingers[j], 0);\n band.stung = -1;\n for (let j = 0; j < s.every; j++) if (band.beat === j) await sleep(frames(s.waits[j]));\n band.beat++;\n if (band.beat >= s.every) {\n band.beat = 0;\n band.count++;\n band.piece++;\n // A section ended: again, or on to the next (after the last, the first).\n for (let k = 0; k < s.sections.length; k++) if (band.section === k) if (band.piece >= s.pieces[k].length) {\n band.piece = 0;\n if (!band.loop) band.section = k + 1 < s.sections.length ? k + 1 : 0;\n }\n if (!band.playing) band.beat = 0;\n }\n }\n});\n\n/**\n * Play a song, note by note, inside an async program: \\`await music.play(song)\\` resolves at its end, or\n * \\`start(music.play(song))\\` plays it beside the program.\n */\nexport const play = game(async (s: Song) => {\n const at = s.at;\n const which = s.which;\n let i = 0;\n let now = 0;\n while (i < at.length) {\n if (at[i] > now) {\n await sleep(frames(at[i] - now));\n now = at[i];\n }\n const k = which[i];\n // Which clip, by its number: a test a clip (a loop over a length known when the script is built is unrolled, and\n // the clips stay the song's, known when it is built — a clip is no value a program holds).\n for (let j = 0; j < s.clips.length; j++) if (k === j) playWav(s.clips[j], 0);\n i++;\n }\n if (s.frames > now) await sleep(frames(s.frames - now));\n});\n\n/** Play a song again and again; start it beside a program: \\`start(music.loop(song))\\`. */\nexport const loop = game(async (s: Song) => {\n while (true) await play(s);\n});\n\n/** Everything above as one name: import { music } from \"trigscript/music\"; music.play(song). */\nexport const music = {\n sampler, synth, fmSynth, strings, presets, drums, kit, track, song, render, play, loop,\n part, section, arrange, transpose, stretch, repeat, beat, chord, over, chords, bars, pump, riser, impact, midi, stems, conduct,\n};\n", + "music": "/**\n * Music in a program: the standard library's \"trigscript/music\" (TrigScript 4).\n *\n * import { music } from \"trigscript/music\";\n * import pluck from \"./pluck.sound\";\n *\n * const lead = music.synth({ wave: \"square\", volume: 0.2 });\n * const bass = music.sampler(pluck, \"A3\");\n * const drums = music.kit({ k: kick, s: snare, h: hat });\n *\n * const song = music.song({ bpm: 120 },\n * music.track(lead, \"C5 - E5 - G5 - - . | A5 - G5 - E5 - - .\"),\n * music.track(bass, \"C3 - - - - - - - | F3 - - - - - - -\"),\n * music.track(drums, \"kh h sh h kh h sh h | kh h sh h kh kh sh h\"),\n * );\n *\n * program(async () => {\n * start(music.loop(song)); // the song again and again, beside the program\n * playWav(lead.note(\"C6\", 0.1)); // an instrument's note on its own, for a sound effect\n * });\n *\n * A pattern is steps, a word each, a quarter of a beat each by default: a note's name starts it (\"C4\", \"F#5\", \"Bb2\"),\n * several with + sound together (\"C4+E4+G4\"), \"-\" holds what sounds a step longer, \".\" is a rest, \"|\" is only for the\n * eye; \"@\" after a step's notes says how loud (\"C4@0.5\"). A kit's step is its sounds' letters (\"kh\": kick and hat).\n * Everything is worked out when the script is built: each note an instrument plays becomes a clip once (`trigscript`'s\n * Clip, put in the map once however often it plays), and the song a table of which clip starts on which frame, which\n * `play` reads while the game runs.\n *\n * A longer song is written in sections — parts named once, each section saying what they play in it, a pattern played\n * again until the section is full — and chords by name, their notes drawn by a template:\n *\n * const parts = { keys: music.part(music.presets.keys()), bass: music.part(music.presets.bass()), drums: music.part(music.drums()) };\n * const verse = music.section(4, {\n * keys: music.chords(\"Am F C G\", \"1+2+3 - . 1+2+3 . . 1+2+3 -\", { octave: 4, lead: true }),\n * bass: music.chords(\"Am F C G\", \"1 - . 1 . 1 3 -\", { octave: 2 }),\n * drums: music.beat({ k: [0, 3, 4], s: [2, 6], h: [0, 1, 2, 3, 4, 5, 6, 7] }, 8),\n * });\n * const song = music.arrange({ bpm: 120, steps: 2 }, parts, intro, verse, verse.with({ keys: null }), outro);\n *\n * Music that follows the game is a song of sections cut into bars and layers, a clip each, which the program steers\n * bar by bar — which section, which layers, a stinger on the next beat:\n *\n * const cut = music.stems(song, { layers: { drums: [\"drums\"], rest: [\"keys\", \"bass\"] } });\n * program(async () => {\n * const band = new Band(); // import { music, Band } from \"trigscript/music\"\n * start(music.conduct(cut, band));\n * if (danger) band.go(cut.section(\"chorus\")); // at the next bar line\n * if (cleared) band.sting(cut.stinger(\"crash\")); // on the next beat\n * });\n *\n * What the game does with it (the sound probe, 2026-10-19): a bar is a whole number of frames (42 ms on Fastest), so\n * the tempo is the nearest that fits — `song.bpm` says which; a sound starts within 21 ms of its frame; 32 sound at\n * once, and a song that would need more is refused; notes that overlap add, with no limiter, so keep the volumes down.\n * `render` mixes a song into one clip when the script is built instead: every note exactly in time, one sound playing,\n * nothing to change while it plays. A program plays to its own player; a program for each player plays to each.\n */\nimport { game, sleep, frames, playWav, tone, noise, fm, pluck, mixdown, together, memory, type Clip, type Notes } from \"trigscript\";\n\n/** Seconds a frame lasts on Fastest, the speed a map is played at. */\nconst FRAME = 0.042;\n/** Sounds the game plays at once. */\nconst VOICES = 32;\n\n/** What plays a pattern's notes: each note a clip, made when the script is built. */\nexport interface Instrument {\n readonly kind: \"notes\";\n /**\n * Note `name` (\"C4\", \"F#5\") lasting `seconds`, release and all: a clip to play on its own, too. `rate`: samples a\n * second, for an instrument that makes its notes (render asks for 44 100).\n */\n note(name: string, seconds: number, rate?: number): Clip;\n}\n\n/** What plays a pattern's letters: a clip a letter. */\nexport interface Kit {\n readonly kind: \"kit\";\n readonly hits: Readonly>;\n}\n\nconst SEMITONE: Readonly> = { c: 0, d: 2, e: 4, f: 5, g: 7, a: 9, b: 11 };\n\n/** A note's number, 69 for A4: \"C4\" is 60, \"C#4\" 61, \"Db4\" 61. */\nfunction noteNumber(name: string): number {\n const m = /^([A-Ga-g])([#b]?)(-?\\d)$/.exec(name);\n if (!m) throw new Error(`A note is a name with its octave — \"C4\", \"F#5\", \"Bb2\" — not ${JSON.stringify(name)}.`);\n return 12 * (Number(m[3]) + 1) + SEMITONE[m[1].toLowerCase()] + (m[2] === \"#\" ? 1 : m[2] === \"b\" ? -1 : 0);\n}\n\n/** Notes kept as they are made, so a note played many times is the same clip (and the same file in the map). */\nfunction remembering(make: (name: string, seconds: number, rate?: number) => Clip): (name: string, seconds: number, rate?: number) => Clip {\n const made = new Map();\n return (name, seconds, rate) => {\n const key = `${name} ${seconds.toFixed(4)} ${rate ?? \"\"}`;\n let c = made.get(key);\n if (!c) { c = make(name, seconds, rate); made.set(key, c); }\n return c;\n };\n}\n\n/**\n * An instrument from a recording of one note, `root` (\"A3\"): every other note is it played faster or slower, as a\n * sampler does, cut to the note's length with a short release. Where the recording is shorter than a note, the note\n * is as long as the recording.\n */\nexport function sampler(clip: Clip, root: string = \"C4\", options: { readonly release?: number; readonly volume?: number } = {}): Instrument {\n const base = noteNumber(root), release = options.release ?? 0.05, volume = options.volume ?? 1;\n return Object.freeze({\n kind: \"notes\" as const,\n note: remembering((name, seconds) => {\n const pitched = clip.pitch(noteNumber(name) - base).gain(volume);\n return pitched.trim(0, Math.min(pitched.seconds, seconds)).fade(0, Math.min(release, seconds / 2));\n }),\n });\n}\n\n/** An attack, a decay to the sustain level, and a release after the note's length, in seconds (sustain 0 … 1). */\nexport interface Shape { readonly attack?: number; readonly decay?: number; readonly sustain?: number; readonly release?: number }\n\n/** A shape's points for a note held `held` seconds, as levels 0 … 1: the attack from 0, the release to 0. */\nfunction shapePoints(shape: Required, held: number): [number, number][] {\n const { attack, decay, sustain, release } = shape;\n const h = Math.max(attack + decay, held);\n return [[0, 0], [attack, 1], [attack + decay, sustain], [h, sustain], [h + release, 0]];\n}\nconst full = (s: Shape | undefined, d: Required): Required => ({ attack: s?.attack ?? d.attack, decay: s?.decay ?? d.decay, sustain: s?.sustain ?? d.sustain, release: s?.release ?? d.release });\n\n/** A synth's filter: its type, the cutoff it rests at, its resonance, and how many octaves above it a note's envelope opens it. */\nexport interface SynthFilter {\n readonly type?: \"lowpass\" | \"highpass\" | \"bandpass\";\n readonly cutoff: number;\n readonly resonance?: number;\n /** Octaves above the cutoff at the envelope's top (0: the filter does not move). */\n readonly amount?: number;\n /** The filter's own attack, decay, sustain and release, as a note's loudness has. */\n readonly envelope?: Shape;\n}\n\nexport interface SynthOptions extends Shape {\n readonly wave?: \"sine\" | \"square\" | \"triangle\" | \"saw\";\n readonly volume?: number;\n /** Copies of each note (1 to 16), `detune` cents apart from lowest to highest, `width` 0 … 1 across left and right. */\n readonly voices?: number;\n readonly detune?: number;\n readonly width?: number;\n readonly filter?: SynthFilter;\n /** Saturation after the filter, dB of push (`Clip.drive`). */\n readonly drive?: number;\n /** Semitones each note slides up from (a negative number: from below) over `glideTime` seconds. */\n readonly glide?: number;\n readonly glideTime?: number;\n}\n\n/**\n * An instrument made of tones: `wave` (\"sine\", \"square\", \"triangle\", \"saw\") shaped by an attack, a decay to the\n * sustain level, and a release after the note's length — a stack of detuned copies with `voices`, through a filter\n * that opens and closes with each note with `filter`, saturated with `drive`.\n */\nexport function synth(options: SynthOptions = {}): Instrument {\n const wave = options.wave ?? \"square\", volume = options.volume ?? 0.2;\n const amp = full(options, { attack: 0.005, decay: 0.08, sustain: 0.7, release: 0.06 });\n const f = options.filter, fshape = full(f?.envelope, { attack: 0.005, decay: 0.25, sustain: 0.2, release: 0.2 });\n const voices = options.voices ?? 1, detune = options.detune ?? (voices > 1 ? 20 : 0), width = options.width ?? 0;\n return Object.freeze({\n kind: \"notes\" as const,\n note: remembering((name, seconds, rate) => {\n const held = Math.max(amp.attack + amp.decay, seconds - amp.release), length = held + amp.release;\n const r = rate === undefined ? {} : { rate };\n const pitch = options.glide ? [[0, 440 * 2 ** ((noteNumber(name) + options.glide - 69) / 12)], [options.glideTime ?? 0.06, name]] as [number, number | string][] : name;\n let c = tone(pitch, length, { wave, volume, voices, detune, width, ...r });\n if (f) {\n const amount = f.amount ?? 0, base = f.cutoff;\n const cutoff = amount === 0 ? base : shapePoints(fshape, held).map(([t, level]) => [t, base * 2 ** (amount * level)] as [number, number]);\n c = c.filter(f.type ?? \"lowpass\", cutoff, { resonance: f.resonance ?? 0.1 });\n }\n if (options.drive) c = c.drive(options.drive);\n return c.envelope(...shapePoints(amp, held));\n }),\n });\n}\n\n/**\n * An FM instrument (`fm()`): `ratio` the modulator's pitch to the note's, `index` how bright a note starts, falling to\n * `index × brightness` over `bright` seconds — an electric piano (ratio 1), a bell (3.5), a bass (0.5).\n */\nexport function fmSynth(options: Shape & { readonly ratio?: number; readonly index?: number; readonly brightness?: number; readonly bright?: number; readonly feedback?: number; readonly volume?: number } = {}): Instrument {\n const amp = full(options, { attack: 0.002, decay: 1.2, sustain: 0.35, release: 0.3 });\n const ratio = options.ratio ?? 1, index = options.index ?? 3, after = index * (options.brightness ?? 0.2), bright = options.bright ?? 0.8, volume = options.volume ?? 0.25;\n return Object.freeze({\n kind: \"notes\" as const,\n note: remembering((name, seconds, rate) => {\n const held = Math.max(amp.attack + amp.decay, seconds - amp.release), length = held + amp.release;\n return fm(name, length, { ratio, index: [[0, index], [bright, after]], feedback: options.feedback ?? 0, volume, ...(rate === undefined ? {} : { rate }) }).envelope(...shapePoints(amp, held));\n }),\n });\n}\n\n/** A plucked-string instrument (`pluck()`): `brightness` 0 … 1, `decay` seconds; a note rings out its decay unless `cut`. */\nexport function strings(options: { readonly brightness?: number; readonly decay?: number; readonly volume?: number; readonly cut?: boolean } = {}): Instrument {\n const brightness = options.brightness ?? 0.55, decay = options.decay ?? 1.5, volume = options.volume ?? 0.3;\n return Object.freeze({\n kind: \"notes\" as const,\n note: remembering((name, seconds, rate) => {\n const length = options.cut ? seconds + 0.05 : Math.max(seconds + 0.05, Math.min(decay, 4));\n return pluck(name, length, { brightness, decay, volume, ...(rate === undefined ? {} : { rate }) }).fade(0, 0.05);\n }),\n });\n}\n\n/**\n * Instruments to start from, each a function that takes the same options as the instrument it is made with, over its\n * own: `music.presets.supersaw({ volume: 0.1 })`.\n */\nexport const presets = {\n /** Seven detuned saws through a filter that opens on each note: a trance lead, big chords. */\n supersaw: (o: SynthOptions = {}) => synth({ wave: \"saw\", voices: 7, detune: 28, width: 0.7, volume: 0.16, attack: 0.005, decay: 0.25, sustain: 0.8, release: 0.18, filter: { cutoff: 1800, amount: 2, resonance: 0.15, envelope: { decay: 0.35, sustain: 0.35 } }, ...o }),\n /** A slow, wide pad: five saws, the filter low and slowly opening. Give its track a chorus and the reverb. */\n pad: (o: SynthOptions = {}) => synth({ wave: \"saw\", voices: 5, detune: 18, width: 1, volume: 0.1, attack: 0.35, decay: 0.6, sustain: 0.85, release: 0.8, filter: { cutoff: 700, amount: 1.3, resonance: 0.1, envelope: { attack: 0.6, decay: 1.2, sustain: 0.5, release: 0.8 } }, ...o }),\n /** A round, punchy bass: two saws, the filter snapping shut after each note, a little drive. */\n bass: (o: SynthOptions = {}) => synth({ wave: \"saw\", voices: 2, detune: 8, volume: 0.32, attack: 0.002, decay: 0.12, sustain: 0.9, release: 0.04, filter: { cutoff: 180, amount: 2.6, resonance: 0.25, envelope: { decay: 0.18, sustain: 0.15, release: 0.05 } }, drive: 6, ...o }),\n /** A squelchy, resonant line (a 303's): one saw, the filter wide open on each note and shutting fast. */\n acid: (o: SynthOptions = {}) => synth({ wave: \"saw\", volume: 0.22, attack: 0.002, decay: 0.1, sustain: 0.8, release: 0.05, filter: { cutoff: 260, amount: 3.6, resonance: 0.82, envelope: { decay: 0.16, sustain: 0 } }, drive: 10, ...o }),\n /** A warm lead: two square waves a little apart, softened by a filter. */\n lead: (o: SynthOptions = {}) => synth({ wave: \"square\", voices: 2, detune: 12, width: 0.3, volume: 0.15, attack: 0.004, decay: 0.15, sustain: 0.7, release: 0.12, filter: { cutoff: 2200, amount: 1.2, resonance: 0.2, envelope: { decay: 0.25, sustain: 0.4 } }, ...o }),\n /** The old consoles' lead: a bare square wave. */\n chip: (o: SynthOptions = {}) => synth({ wave: \"square\", volume: 0.14, attack: 0.002, decay: 0.05, sustain: 0.8, release: 0.03, ...o }),\n /** An electric piano: FM at ratio 1, bright on the strike and mellowing. */\n keys: (o: Parameters[0] = {}) => fmSynth({ ratio: 1, index: 2.6, brightness: 0.25, bright: 0.6, volume: 0.22, decay: 1.4, sustain: 0.3, release: 0.35, ...o }),\n /** A bell: FM at ratio 3.5, long and shimmering. */\n bell: (o: Parameters[0] = {}) => fmSynth({ ratio: 3.5, index: 5, brightness: 0.08, bright: 1.6, volume: 0.16, decay: 2.5, sustain: 0, release: 1.2, ...o }),\n /** A plucked string, a little bright. */\n pluck: (o: Parameters[0] = {}) => strings({ brightness: 0.6, decay: 1.4, ...o }),\n};\n\n/**\n * A drum kit made from nothing, a letter a drum: k kick, s snare, c clap, h closed hat, o open hat, t tom, r rim, x crash. At\n * 44 100 Hz (`rate`), so the hats keep their top; `volume` 0 … 1 over the whole kit (1).\n */\nexport function drums(options: { readonly rate?: number; readonly volume?: number } = {}): Kit {\n const rate = options.rate ?? 44100, v = options.volume ?? 1;\n const metal = (seconds: number) => [263, 400, 421, 474, 587, 845].map((hz) => tone(hz * 2.6, seconds, { wave: \"square\", volume: 0.16, rate })).reduce((a, b) => a.mix(b));\n const kick = tone([[0, 170], [0.025, 85], [0.3, 46]], 0.42, { volume: 0.95, rate }).envelope([0, 1], [0.08, 0.85], [0.42, 0])\n .mix(noise(0.006, { volume: 0.35, rate }).filter(\"highpass\", 3000)).drive(5);\n const snare = tone([[0, 260], [0.04, 185]], 0.18, { wave: \"triangle\", volume: 0.6, rate }).envelope([0, 1], [0.15, 0])\n .mix(noise(0.28, { volume: 0.6, rate }).filter(\"highpass\", 1400).filter(\"peak\", 5000, { gain: 4 }).envelope([0, 1], [0.03, 0.55], [0.26, 0])).drive(4);\n const burst = (at: number) => ({ clip: noise(0.012, { volume: 0.6, rate }).envelope([0, 1], [0.012, 0.2]), at });\n const clap = mixdown([burst(0), burst(0.011), burst(0.023), { clip: noise(0.22, { volume: 0.5, rate }).envelope([0, 1], [0.22, 0]), at: 0.03 }], undefined, rate)\n .filter(\"bandpass\", 1300, { resonance: 0.35 }).gain(1.6);\n const hat = (seconds: number) => metal(seconds).mix(noise(seconds, { volume: 0.4, rate })).filter(\"highpass\", 7200).filter(\"highshelf\", 10000, { gain: 3 }).envelope([0, 1], [seconds * 0.25, 0.4], [seconds, 0]);\n const tom = tone([[0, 190], [0.2, 120]], 0.4, { volume: 0.8, rate }).envelope([0, 1], [0.4, 0]).mix(noise(0.01, { volume: 0.2, rate }).filter(\"bandpass\", 2500));\n const rim = tone(1700, 0.025, { wave: \"triangle\", volume: 0.5, rate }).mix(noise(0.012, { volume: 0.3, rate }).filter(\"highpass\", 4000)).envelope([0, 1], [0.025, 0]);\n const crash = metal(2).mix(noise(2, { volume: 0.5, rate })).filter(\"highpass\", 4500).filter(\"peak\", 7500, { gain: 3 }).envelope([0, 1], [0.08, 0.5], [2, 0]);\n return kit({ k: kick.gain(v), s: snare.gain(v), c: clap.gain(v), h: hat(0.07).gain(0.55 * v), o: hat(0.4).gain(0.5 * v), t: tom.gain(v), r: rim.gain(v), x: crash.gain(0.45 * v) });\n}\n\n/** A kit: a clip a letter, played by a pattern's letters (\"kh\" plays k and h together). */\nexport function kit(hits: Readonly>): Kit {\n for (const k of Object.keys(hits)) if (k.length !== 1 || k === \".\" || k === \"-\" || k === \"|\") throw new Error(`A kit's sounds are named by one letter each, not ${JSON.stringify(k)}.`);\n return Object.freeze({ kind: \"kit\" as const, hits: Object.freeze({ ...hits }) });\n}\n\n/* ── Patterns: what is written, and what writes it ── */\n\n/** A pattern: steps a word each, as one text or a text a bar. */\nexport type Pattern = string | readonly string[];\n\n/** A pattern's steps, the \"|\" left out. */\nconst wordsOf = (p: Pattern): string[] => (typeof p === \"string\" ? p : p.join(\" \")).split(/\\s+/).filter((w) => w !== \"\" && w !== \"|\");\n\n/** A step's sounds and how loud: \"C4+E4@0.6\" is C4 and E4 at 0.6; \"kh\" k and h at 1. */\nfunction velocityOf(word: string, step: number): { body: string; velocity: number } {\n const at = word.indexOf(\"@\");\n if (at < 0) return { body: word, velocity: 1 };\n const body = word.slice(0, at), v = Number(word.slice(at + 1));\n if (body === \"\" || body === \"-\" || body === \".\" || !(v >= 0 && v <= 4)) throw new Error(`Step ${step + 1}: \"@\" follows a note, a chord or a kit's letters, with how loud they play, 0 to 4 (1 as written): \"C4@0.6\", \"kh@1.3\" — not ${JSON.stringify(word)}.`);\n return { body, velocity: v };\n}\n\nconst NAMES = [\"C\", \"C#\", \"D\", \"D#\", \"E\", \"F\", \"F#\", \"G\", \"G#\", \"A\", \"A#\", \"B\"];\n/** A note's name from its number: 60 is \"C4\". Sharps, not flats. */\nfunction nameOf(n: number): string {\n const octave = Math.floor(n / 12) - 1;\n if (octave < -1 || octave > 9) throw new Error(`A note goes from C-1 to B9: this one would be ${NAMES[((n % 12) + 12) % 12]}${octave}.`);\n return NAMES[((n % 12) + 12) % 12] + octave;\n}\n\n/** Each note of a step moved by a function of its number; holds, rests, kit letters and \"@\" kept. */\nconst eachNote = (p: Pattern, move: (n: number) => number): string[] => wordsOf(p).map((w) => w.replace(/[A-Ga-g][#b]?-?\\d/g, (name) => nameOf(move(noteNumber(name)))));\n\n/** A pattern `semitones` up (or down: a negative number): `transpose(tune, 12)` is the tune an octave up. A text a bar stays a text a bar. */\nexport function transpose(p: Pattern, semitones: number): Pattern {\n if (!Number.isInteger(semitones)) throw new Error(`transpose() moves by whole semitones, not ${semitones}.`);\n if (semitones === 0) return p;\n return typeof p === \"string\" ? eachNote(p, (n) => n + semitones).join(\" \") : p.map((bar) => eachNote(bar, (n) => n + semitones).join(\" \"));\n}\n\n/** A pattern with each step `by` steps long — eighths written as sixteenths with `stretch(p, 2)`. A text a bar stays a text a bar. */\nexport function stretch(p: Pattern, by: number): Pattern {\n if (!Number.isInteger(by) || by < 1) throw new Error(`stretch() makes each step a whole number of steps long, 1 or more — not ${by}.`);\n const one = (text: string) => wordsOf(text).map((w) => [w, ...Array.from({ length: by - 1 }, () => (w.startsWith(\".\") ? \".\" : \"-\"))].join(\" \")).join(\" \");\n return typeof p === \"string\" ? one(p) : p.map(one);\n}\n\n/** A pattern `n` times over, as one text. */\nexport function repeat(p: Pattern, n: number): string {\n if (!Number.isInteger(n) || n < 1) throw new Error(`repeat() plays a pattern a whole number of times, 1 or more — not ${n}.`);\n return Array.from({ length: n }, () => wordsOf(p).join(\" \")).join(\" \");\n}\n\n/** A kit's bar: the steps (from 0) each letter plays on, `steps` steps long — `beat({ k: [0, 8], s: [4, 12] })`. */\nexport function beat(hits: Readonly>, steps: number = 16): string {\n for (const [k, at] of Object.entries(hits)) for (const s of at) if (!Number.isInteger(s) || s < 0 || s >= steps) throw new Error(`beat(): ${k} on step ${s}, of steps 0 to ${steps - 1}.`);\n return Array.from({ length: steps }, (_, i) => Object.keys(hits).filter((k) => hits[k].includes(i)).join(\"\") || \".\").join(\" \");\n}\n\n/** A chord's notes above its root, by what follows the root's name. */\nconst QUALITIES: Readonly> = {\n \"\": [0, 4, 7], m: [0, 3, 7], dim: [0, 3, 6], aug: [0, 4, 8], \"+\": [0, 4, 8], sus2: [0, 2, 7], sus4: [0, 5, 7], sus: [0, 5, 7], \"5\": [0, 7],\n \"6\": [0, 4, 7, 9], m6: [0, 3, 7, 9], \"7\": [0, 4, 7, 10], maj7: [0, 4, 7, 11], M7: [0, 4, 7, 11], m7: [0, 3, 7, 10], dim7: [0, 3, 6, 9],\n m7b5: [0, 3, 6, 10], \"7sus4\": [0, 5, 7, 10], \"9\": [0, 4, 7, 10, 14], maj9: [0, 4, 7, 11, 14], m9: [0, 3, 7, 10, 14], add9: [0, 4, 7, 14], madd9: [0, 3, 7, 14],\n};\n\n/** A chord's notes as numbers, lowest first. */\nfunction chordNumbers(symbol: string, octave: number, inversion: number): number[] {\n const m = /^([A-G][#b]?)([^/]*)(?:\\/([A-G][#b]?))?$/.exec(symbol);\n const steps = m ? QUALITIES[m[2]] : undefined;\n if (!m || !steps) throw new Error(`A chord is a root and what kind — \"Am\", \"C\", \"G7\", \"Fmaj7\", \"Dm7\", \"Esus4\", \"Bdim\", \"C/E\" — not ${JSON.stringify(symbol)}. The kinds: ${Object.keys(QUALITIES).map((q) => `\"${q}\"`).join(\", \")}.`);\n if (!Number.isInteger(inversion) || inversion < 0 || inversion >= steps.length) throw new Error(`${symbol} has ${steps.length} notes: its inversion is 0 to ${steps.length - 1}, not ${inversion}.`);\n const root = noteNumber(m[1] + octave);\n const notes = steps.map((s) => root + s);\n for (let k = 0; k < inversion; k++) notes.push(notes.shift()! + 12);\n if (m[3]) {\n // Over another bass: that note, below the rest.\n let bass = noteNumber(m[3] + octave);\n while (bass >= notes[0]) bass -= 12;\n notes.unshift(bass);\n }\n return notes;\n}\n\n/**\n * A chord's notes, as a step plays them: `chord(\"Am\")` is \"A4+C5+E5\", its root in octave 4 (`octave`); `inversion`\n * moves its lowest notes up an octave, one each — `chord(\"E\", 3, 1)` is \"G#3+B3+E4\". \"C/E\" is C over an E below it.\n */\nexport function chord(symbol: string, octave: number = 4, inversion: number = 0): string {\n return chordNumbers(symbol, octave, inversion).map(nameOf).join(\"+\");\n}\n\n/** Each chord's notes as numbers, lowest first: the ones written, or the symbol's at `octave`. */\nconst notesOf = (c: string, octave: number): number[] => (/\\d/.test(c) ? c.split(\"+\").map(noteNumber).sort((a, b) => a - b) : chordNumbers(c, octave, 0));\n\n/**\n * A chord played as `template` says: its steps name the chord's notes by number, 1 the lowest — \"1 3 2 3\", \"1+2+3 - - -\"\n * — past the last, round again an octave up (in a three-note chord, 4 is 1 an octave up); each ^ after a number takes it\n * an octave higher, each _ an octave lower (\"1__\" the root two octaves down, for a bass). \"-\", \".\" and \"@\" as in any\n * pattern. `notes` is a chord's notes (\"A3+C4+E4\") or its name (\"Am\", with its root in octave `octave`).\n */\nexport function over(notes: string, template: Pattern, octave: number = 4): string {\n const n = notesOf(notes, octave);\n return wordsOf(template).map((w, s) => {\n const { body, velocity } = velocityOf(w, s);\n if (body === \"-\" || body === \".\") return w;\n const named = body.split(\"+\").map((d) => {\n const m = /^(\\d+)([\\^_]*)$/.exec(d);\n if (!m || Number(m[1]) < 1) throw new Error(`Step ${s + 1} of a chord's template: its notes are numbers from 1, the lowest, with ^ or _ for an octave up or down — \"1\", \"3^\", \"1__\" — not ${JSON.stringify(d)}.`);\n const k = Number(m[1]) - 1, up = Math.floor(k / n.length) + (m[2].match(/\\^/g)?.length ?? 0) - (m[2].match(/_/g)?.length ?? 0);\n return nameOf(n[k % n.length] + 12 * up);\n });\n return named.join(\"+\") + (velocity === 1 ? \"\" : `@${velocity}`);\n }).join(\" \");\n}\n\n/**\n * A progression played as `template` says, a template a chord: `chords(\"Am Am E Am Dm C E Am\", \"1_ 1 2 3\")` — as\n * `over` plays one chord. Each chord's root in octave `octave` (4); `lead: true` turns each chord after the first to\n * the inversion nearest the one before (its notes move least), as a player's hands do. A text a chord.\n */\nexport function chords(progression: string | readonly string[], template: Pattern, options: { readonly octave?: number; readonly lead?: boolean } = {}): string[] {\n const octave = options.octave ?? 4, list = wordsOf(progression);\n let before: number[] | null = null;\n return list.map((c) => {\n let n = notesOf(c, octave);\n if (options.lead && before && !/\\d/.test(c)) {\n // Each inversion, an octave either side: the one whose notes are nearest those before.\n const prev: number[] = before, mean = prev.reduce((a, b) => a + b, 0) / prev.length;\n let best = n, cost = Infinity;\n for (let inv = 0; inv < n.length; inv++) for (const shift of [-12, 0, 12]) {\n const cand = chordNumbers(c, octave, inv).map((x) => x + shift);\n const d = cand.length === prev.length ? cand.reduce((s, x, i) => s + Math.abs(x - prev[i]), 0) : Math.abs(cand.reduce((a, b) => a + b, 0) / cand.length - mean) * cand.length;\n if (d < cost) { cost = d; best = cand; }\n }\n n = best;\n }\n before = n;\n return over(n.map(nameOf).join(\"+\"), template);\n });\n}\n\n/** Seconds `n` bars last at `bpm` as a song plays it (its bar a whole number of frames): how long to make a riser. */\nexport function bars(bpm: number, n: number = 1, beats: number = 4): number {\n return n * Math.max(1, Math.round((60 / bpm) * beats / FRAME)) * FRAME;\n}\n\n/**\n * What makes a part breathe with the beat, as a mix does under a kick: down to `1 − depth` on each beat (`beat`\n * seconds apart, from the clip's start) and back over `release` of a beat — a part's fx, or a section's:\n * `{ fx: music.pump(music.bars(143) / 4) }`.\n */\nexport function pump(beat: number, depth: number = 0.45, release: number = 0.55): (clip: Clip) => Clip {\n if (!(beat > 0) || !(depth >= 0 && depth <= 1) || !(release > 0 && release <= 1)) throw new Error(\"pump() takes seconds a beat, how deep (0 … 1) and how long it takes to come back, as a part of a beat (0 … 1).\");\n return (c) => {\n const points: [number, number][] = [];\n for (let t = 0; t < c.seconds; t += beat) points.push([t, 1 - depth], [Math.min(c.seconds, t + release * beat), 1], [Math.min(c.seconds, t + beat - 0.004), 1]);\n points.push([c.seconds, 1]);\n return c.envelope(...points);\n };\n}\n\n/* ── Sounds between sections ── */\n\n/** Noise rising for `seconds` through a filter opening, loudest at the end: what leads into a drop. */\nexport function riser(seconds: number, options: { readonly volume?: number; readonly rate?: number } = {}): Clip {\n const rate = options.rate ?? 44100, v = options.volume ?? 0.5, s = seconds;\n const swept = noise(s, { volume: v, rate }).filter(\"bandpass\", [[0, 350], [s * 0.6, 1800], [s, 9000]], { resonance: 0.55 });\n const lift = tone([[0, 220], [s, 880]], s, { wave: \"saw\", voices: 3, detune: 25, width: 0.8, volume: v * 0.25, rate }).filter(\"lowpass\", [[0, 500], [s, 6000]]);\n return swept.mix(lift).envelope([0, 0], [s * 0.5, 0.25], [s * 0.9, 0.85], [s, 1]).fade(0, 0.01);\n}\n\n/** A boom to land on: a falling sine under a burst of noise, ringing `seconds` (1.5). */\nexport function impact(options: { readonly seconds?: number; readonly volume?: number; readonly rate?: number } = {}): Clip {\n const rate = options.rate ?? 44100, v = options.volume ?? 0.8, s = options.seconds ?? 1.5;\n return tone([[0, 90], [0.4, 38], [s, 30]], s, { volume: v, rate }).envelope([0, 1], [0.15, 0.7], [s, 0])\n .mix(noise(s, { volume: v * 0.5, rate }).filter(\"lowpass\", [[0, 6000], [s, 300]]).envelope([0, 1], [0.05, 0.4], [s, 0])).drive(3);\n}\n\n/** How a track or a part is heard. */\nexport interface PartOptions {\n /** Where it is heard, −1 left to 1 right; 0, the middle, when left out. */\n readonly pan?: number;\n /** How loud, 1 as the instrument plays. */\n readonly volume?: number;\n /** What it goes through: its whole part in `render`, each note on its own in `play`. */\n readonly fx?: (clip: Clip) => Clip;\n /** How much of it goes to the song's reverb in `render`, 0 … 1 (0). */\n readonly reverb?: number;\n}\n\n/** What an instrument plays, step by step. */\nexport interface Track {\n readonly instrument: Instrument | Kit;\n readonly steps: readonly string[];\n /** Where it is heard, −1 left to 1 right; 0, the middle, when left out. */\n readonly pan: number;\n /** How loud, 1 as the instrument plays. */\n readonly volume: number;\n /** What the track goes through: its whole part in `render`, each note on its own in `play`. */\n readonly fx?: (clip: Clip) => Clip;\n /** How much of it goes to the song's reverb in `render`, 0 … 1 (0). */\n readonly reverb: number;\n}\n\n/** A part's options checked, and its defaults filled in. */\nfunction heard(options: PartOptions, what: string): { pan: number; volume: number; reverb: number; fx?: (clip: Clip) => Clip } {\n const pan = options.pan ?? 0, volume = options.volume ?? 1, reverb = options.reverb ?? 0;\n if (!(pan >= -1 && pan <= 1)) throw new Error(`A ${what}'s pan is −1 (left) to 1 (right), not ${pan}.`);\n if (!(volume >= 0)) throw new Error(`A ${what}'s volume is 0 or more, not ${volume}.`);\n if (!(reverb >= 0 && reverb <= 1)) throw new Error(`A ${what}'s reverb is 0 to 1, not ${reverb}.`);\n return { pan, volume, reverb, ...(options.fx ? { fx: options.fx } : {}) };\n}\n\n/**\n * A pattern for an instrument: steps a word each — a note or notes joined by +, \"-\" to hold, \".\" to rest, \"|\" for the\n * eye; \"@\" after a step's notes says how loud, \"C4@0.6\". `pan`: where it is heard, −1 left to 1 right (a panned track's\n * notes have two channels); `volume`: 1 as the instrument plays; `fx`: what the track goes through —\n * `(c) => c.delay(0.21).chorus()` — its whole part when the song is rendered (as a mixer's insert), each note on its own\n * when it is played note by note; `reverb`: how much goes to the song's reverb when it is rendered, 0 … 1.\n */\nexport function track(instrument: Instrument | Kit, pattern: Pattern, options: PartOptions = {}): Track {\n return Object.freeze({ instrument, steps: Object.freeze(wordsOf(pattern)), ...heard(options, \"track\") });\n}\n\n/** An instrument and how it is heard, which a song's sections give its notes: `arrange`. */\nexport interface Part extends Omit { readonly kind: \"part\" }\n\n/** An instrument as a part of a song made of sections (`arrange`), heard as `track` says. */\nexport function part(instrument: Instrument | Kit, options: PartOptions = {}): Part {\n return Object.freeze({ kind: \"part\" as const, instrument, ...heard(options, \"part\") });\n}\n\n/** How long a section lasts, as its effects need it for their points: seconds, its bars, and seconds a bar. */\nexport interface SectionTime { readonly seconds: number; readonly bars: number; readonly bar: number }\n\n/** What a section is and does, besides its notes. */\nexport interface SectionOptions {\n /** What `song.sections` calls it. */\n readonly name?: string;\n /**\n * What a part goes through in this section only, given how long the section is — a filter opening over a build:\n * `{ pad: (c, t) => c.filter(\"lowpass\", [[0, 400], [t.seconds, 8000]]) }`. Its notes from this section, tails and\n * all, before the part's own `fx`. Mixed by `render` only: a song with one is not played note by note.\n */\n readonly fx?: Readonly Clip>>;\n /** How loud a part is in this section only, 1 as it is. */\n readonly volume?: Readonly>;\n}\n\n/** Bars of a song: what each part plays in them. */\nexport interface Section {\n readonly kind: \"section\";\n readonly name: string;\n readonly bars: number;\n readonly parts: Readonly>;\n readonly fx: Readonly Clip>>;\n readonly volume: Readonly>;\n /** This section with some parts written again — null: that part rests — and these options over its own. */\n with(parts: Readonly>, options?: SectionOptions): Section;\n}\n\n/**\n * `bars` bars of a song (`arrange`), and what each part plays in them, by the part's name: `{ lead: tune, drums: beat }`.\n * A pattern shorter than the section plays again until it is filled, so a bar of drums fills eight; a part not named\n * rests. `options`: its name, a part's effects or volume for this section only.\n */\nexport function section(bars: number, parts: Readonly>, options: SectionOptions = {}): Section {\n if (!Number.isInteger(bars) || bars < 1) throw new Error(`A section is a whole number of bars, 1 or more — not ${bars}.`);\n for (const [k, v] of Object.entries(options.volume ?? {})) if (!(v >= 0)) throw new Error(`A section's volume for ${k} is 0 or more, not ${v}.`);\n const made: Section = Object.freeze({\n kind: \"section\" as const, name: options.name ?? \"\", bars,\n parts: Object.freeze({ ...parts }), fx: Object.freeze({ ...(options.fx ?? {}) }), volume: Object.freeze({ ...(options.volume ?? {}) }),\n with(changes: Readonly>, more: SectionOptions = {}): Section {\n const next: Record = { ...parts };\n for (const [k, p] of Object.entries(changes)) { if (p === null) delete next[k]; else next[k] = p; }\n return section(bars, next, { name: more.name ?? made.name, fx: { ...made.fx, ...(more.fx ?? {}) }, volume: { ...made.volume, ...(more.volume ?? {}) } });\n },\n });\n return made;\n}\n\n/** A song's tempo and grid. */\nexport interface SongOptions {\n readonly bpm: number;\n /** Steps a beat (4: sixteenths). */\n readonly steps?: number;\n /** Beats a bar (4). */\n readonly beats?: number;\n /** How late every second step starts, as a part of a step: 0 straight (as it is when left out), 1/3 a triplet's lilt. */\n readonly swing?: number;\n}\n\n/** Where a song's section is: from bar `bar` (0 the first) for `bars`, from frame `frame`. */\nexport interface SongSection { readonly name: string; readonly bar: number; readonly bars: number; readonly frame: number }\n\n/** Steps of a track that belong to one section, and what that section does to them. */\ninterface Piece { readonly from: number; readonly to: number; readonly volume: number; readonly fx?: (clip: Clip, time: SectionTime) => Clip; readonly time: SectionTime }\nconst piecesOf = new WeakMap();\n/** A song of sections' parts, by name, and the track each became: what `stems` cuts. */\nconst partsOf = new WeakMap();\n\n/**\n * A song made of sections, one after another: `parts` names each instrument and how it is heard; each section says\n * what the parts play in it. A section can be written once and played again — `arrange(o, parts, intro, verse, verse,\n * chorus)` — or played again changed: `verse.with({ lead: transpose(tune, 12) })`.\n */\nexport function arrange(options: SongOptions, parts: Readonly>, ...sections: readonly Section[]): Song {\n const { perBar, barFrames } = grid(options);\n if (sections.length === 0) throw new Error(\"arrange() takes the song's sections, one at least.\");\n const names = Object.keys(parts);\n for (const s of sections) for (const k of [...Object.keys(s.parts), ...Object.keys(s.fx), ...Object.keys(s.volume)]) {\n if (!(k in parts)) throw new Error(`A section${s.name ? ` (${s.name})` : \"\"} names ${JSON.stringify(k)}, which is no part of the song: its parts are ${names.join(\", \")}.`);\n }\n const tracks = names.map((k) => {\n const steps: string[] = [], pieces: Piece[] = [];\n for (const s of sections) {\n const length = s.bars * perBar, from = steps.length, p = s.parts[k];\n if (p === undefined) for (let i = 0; i < length; i++) steps.push(\".\");\n else {\n const words = wordsOf(p);\n if (words.length === 0 || length % words.length !== 0) throw new Error(`${k}${s.name ? ` in ${s.name}` : \"\"}: a pattern of ${words.length} steps does not fill ${s.bars} bar${s.bars === 1 ? \"\" : \"s\"} of ${perBar} steps evenly.`);\n for (let i = 0; i < length; i++) steps.push(words[i % words.length]);\n }\n const fx = k in s.fx ? s.fx[k] : undefined, volume = s.volume[k] ?? 1;\n if (fx || volume !== 1) pieces.push({ from, to: from + length, volume, ...(fx ? { fx } : {}), time: { seconds: s.bars * barFrames * FRAME, bars: s.bars, bar: barFrames * FRAME } });\n }\n const t = track(parts[k].instrument, steps, parts[k]);\n if (pieces.length) piecesOf.set(t, pieces);\n return t;\n });\n let bar = 0;\n const placedSections = sections.map((s) => { const at = { name: s.name, bar, bars: s.bars, frame: bar * barFrames }; bar += s.bars; return Object.freeze(at); });\n const made = compose(options, tracks, placedSections);\n partsOf.set(made, names.map((k, i) => ({ name: k, track: tracks[i] })));\n return made;\n}\n\n/** A clip as a track plays it note by note: through its fx, at its volume and its pan, made once a clip a track. */\nfunction placed(t: Track, made: Map, c: Clip): Clip {\n if (t.pan === 0 && t.volume === 1 && !t.fx) return c;\n let p = made.get(c);\n if (!p) {\n p = t.fx ? t.fx(c) : c;\n if (t.volume !== 1) p = p.gain(t.volume);\n if (t.pan !== 0) p = p.pan(t.pan);\n made.set(c, p);\n }\n return p;\n}\n\n/** What a song's notes are, kept for render, which makes them again at its own rate; `piece`, the section whose effects it goes through. */\ninterface Score { readonly at: number; readonly track: Track; readonly made: Map; readonly velocity: number; readonly piece?: Piece; readonly name?: string; readonly seconds?: number; readonly hit?: Clip }\nconst scores = new WeakMap();\n\n/** A song, when the script is built: the tables `play` reads, and what it takes. */\nexport interface Song {\n /**\n * Frames a step lasts: a bar is a whole number of frames, a step need not be (2.5 for sixteenths at 143 beats a\n * minute). Played note by note, a note starts on the frame nearest its time — within the 21 ms the game's own\n * mixer starts a sound in anyway; rendered, exactly on it.\n */\n readonly stepFrames: number;\n /** Frames a bar lasts, a whole number. */\n readonly barFrames: number;\n /** Beats a bar. */\n readonly beats: number;\n /** The tempo it plays at: the nearest to the one asked for whose bar is a whole number of frames. */\n readonly bpm: number;\n /** Steps, the longest track's. */\n readonly steps: number;\n /** Its sections, made by `arrange`, in the order they play (none for a song of tracks). */\n readonly sections: readonly SongSection[];\n /** Frames it lasts, its last note's release included. */\n readonly frames: number;\n readonly seconds: number;\n /** Each clip the song plays, once. */\n readonly clips: readonly Clip[];\n /** Each note in the order they start: the frame it starts on, from 0, and which of `clips` it is. */\n readonly at: readonly number[];\n readonly which: readonly number[];\n /** The most notes sounding at once: `play` refuses a song of more than 32 (`render` has no limit). */\n readonly voices: number;\n}\n\n/** A song's grid: steps a bar, and frames a bar and a step. */\nfunction grid(options: SongOptions): { perBar: number; barFrames: number; stepFrames: number } {\n const perBeat = options.steps ?? 4, beats = options.beats ?? 4, swing = options.swing ?? 0;\n if (!(options.bpm > 0) || !(perBeat > 0) || !(beats > 0)) throw new Error(\"A song takes a tempo, { bpm: 120 }, steps a beat (4 by default) and beats a bar (4).\");\n if (!(swing >= 0 && swing < 1)) throw new Error(`A song's swing is 0 (straight) to under 1 (a step late), not ${swing}.`);\n const barFrames = Math.max(1, Math.round((60 / options.bpm) * beats / FRAME));\n return { perBar: perBeat * beats, barFrames, stepFrames: barFrames / (beats * perBeat) };\n}\n\n/**\n * Tracks played together from their first step: `bpm` beats a minute, `steps` steps a beat (4 by default), `swing`\n * every second step late. Played note by note (`play`), a song is refused when more than 32 notes would sound at once;\n * rendered, it has no limit. A song of sections: `arrange`.\n */\nexport function song(options: SongOptions, ...tracks: readonly Track[]): Song {\n return compose(options, tracks, []);\n}\n\nfunction compose(options: SongOptions, tracks: readonly Track[], sections: readonly SongSection[]): Song {\n const { barFrames, stepFrames } = grid(options), beats = options.beats ?? 4, swing = options.swing ?? 0;\n const stepSeconds = stepFrames * FRAME;\n // Where step s starts, in frames: every second step `swing` of a step late.\n const time = (s: number) => (s + (s % 2 === 1 ? swing : 0)) * stepFrames;\n const score: Score[] = [];\n let steps = 0, effects = false;\n for (const t of tracks) {\n const ws = t.steps, made = new Map(), pieces = piecesOf.get(t) ?? [];\n if (pieces.some((p) => p.fx)) effects = true;\n steps = Math.max(steps, ws.length);\n let k = 0;\n ws.forEach((w, s) => {\n while (k < pieces.length && pieces[k].to <= s) k++;\n const piece = k < pieces.length && pieces[k].from <= s ? pieces[k] : undefined;\n const { body, velocity: v } = velocityOf(w, s);\n if (body === \"-\" || body === \".\") return;\n const velocity = v * (piece?.volume ?? 1), into = piece ? { piece } : {};\n if (t.instrument.kind === \"kit\") {\n for (const letter of body) {\n const hit = t.instrument.hits[letter];\n if (!hit) throw new Error(`Step ${s + 1}: the kit has no ${JSON.stringify(letter)} (it has ${Object.keys(t.instrument.hits).join(\", \")}).`);\n score.push({ at: time(s), track: t, made, velocity, ...into, hit });\n }\n return;\n }\n // A note lasts its step and every \"-\" after it.\n let length = 1;\n while (s + length < ws.length && ws[s + length] === \"-\") length++;\n const seconds = swing === 0 ? length * stepSeconds : (time(s + length) - time(s)) * FRAME;\n for (const name of body.split(\"+\")) {\n noteNumber(name);\n score.push({ at: time(s), track: t, made, velocity, ...into, name, seconds });\n }\n });\n }\n score.sort((a, b) => a.at - b.at);\n const base = (n: Score) => n.hit ?? (n.track.instrument as Instrument).note(n.name!, n.seconds!);\n // The tables `play` reads, made when they are first read: a song only rendered never makes its notes for the game.\n let table: { clips: readonly Clip[]; at: readonly number[]; which: readonly number[]; voices: number; peakStep: number; frames: number } | null = null;\n const tables = () => {\n if (table) return table;\n const notes = score.map((n) => ({ at: Math.round(n.at), clip: placed(n.track, n.made, n.velocity === 1 ? base(n) : base(n).gain(n.velocity)) }));\n // How many sound at once: at each note's start, those started before it and not yet over.\n let voices = 0, peakStep = 0;\n notes.forEach((n, k) => {\n let sounding = 0;\n for (let j = 0; j <= k; j++) if (notes[j].at + notes[j].clip.seconds / FRAME > n.at) sounding++;\n if (sounding > voices) { voices = sounding; peakStep = Math.round(n.at / stepFrames) + 1; }\n });\n const clips: Clip[] = [];\n const which = notes.map((n) => { let k = clips.indexOf(n.clip); if (k < 0) { clips.push(n.clip); k = clips.length - 1; } return k; });\n const frames = Math.max(Math.round(steps * stepFrames), ...notes.map((n) => n.at + Math.ceil(n.clip.seconds / FRAME)));\n table = { clips: Object.freeze(clips), at: Object.freeze(notes.map((n) => n.at)), which: Object.freeze(which), voices, peakStep, frames };\n return table;\n };\n // What the game plays note by note: refused, when it is read, if more than 32 would sound at once (render has no limit).\n const playable = () => {\n if (effects) throw new Error(\"A section of this song puts a part through effects of its own, which only music.render mixes: render the song, or give the part those effects for the whole song.\");\n const t = tables();\n if (t.voices > VOICES) throw new Error(`At step ${t.peakStep}, ${t.voices} notes would sound at once; the game plays ${VOICES} note by note. Shorten notes or thin the chords — or music.render the song, one sound however many it mixes.`);\n return t;\n };\n const made: Song = Object.freeze({\n stepFrames, barFrames, beats, bpm: (60 * beats) / (barFrames * FRAME), steps, sections: Object.freeze([...sections]),\n get frames() { return tables().frames; },\n get seconds() { return tables().frames * FRAME; },\n get clips() { return playable().clips; },\n get at() { return playable().at; },\n get which() { return playable().which; },\n get voices() { return tables().voices; },\n });\n scores.set(made, score);\n return made;\n}\n\n/** The General MIDI drums, by number, as `drums()` has them: kicks k, snares s, the clap c, hats h and o, toms t, the rim r, crashes x. */\nconst GM_DRUMS: Readonly> = {\n 35: \"k\", 36: \"k\", 37: \"r\", 38: \"s\", 40: \"s\", 39: \"c\", 42: \"h\", 44: \"h\", 51: \"h\", 53: \"h\", 59: \"h\", 46: \"o\",\n 41: \"t\", 43: \"t\", 45: \"t\", 47: \"t\", 48: \"t\", 50: \"t\", 49: \"x\", 52: \"x\", 55: \"x\", 57: \"x\",\n};\n\n/** How imported notes are made a song: its grid, and which kit letter a drum's number plays. */\nexport interface MidiOptions extends Omit {\n /** Beats a minute: the file's own when left out. */\n readonly bpm?: number;\n /** A drum's MIDI number to a kit's letter, over General MIDI's (36 \"k\", 38 \"s\", 42 \"h\", 46 \"o\", 49 \"x\", …). */\n readonly drums?: Readonly>;\n}\n\n/**\n * A song of imported notes (`import tune from \"./tune.notes\"`): each track `parts` names — by its name, or its place\n * in `tune.tracks` from 0 — played by an instrument, a kit (the drums by their General MIDI numbers) or a `part`;\n * the tracks not named are left out. Each note starts on the step nearest its time and lasts the steps nearest its\n * length (`steps` a beat, 4: sixteenths); how hard it was struck is how loud it plays, 100 as written. A track whose\n * notes overlap becomes several, so the song can hold them. At the file's first tempo unless `bpm` says.\n */\nexport function midi(notes: Notes, parts: Readonly>, options: MidiOptions = {}): Song {\n const perBeat = options.steps ?? 4, map = { ...GM_DRUMS, ...(options.drums ?? {}) };\n const names = notes.tracks.map((t) => t.name);\n const tracks: Track[] = [];\n for (const [key, given] of Object.entries(parts)) {\n const index = names.indexOf(key) >= 0 ? names.indexOf(key) : /^\\d+$/.test(key) ? Number(key) : -1;\n const source = notes.tracks[index];\n if (!source) throw new Error(`The notes have no track ${JSON.stringify(key)}: their tracks are ${names.map((n) => JSON.stringify(n)).join(\", \")} (or 0 to ${names.length - 1}).`);\n const instrument = given.kind === \"part\" ? given.instrument : given, how = given.kind === \"part\" ? given : {};\n const kit = instrument.kind === \"kit\" ? instrument : null;\n // Voices: steps that each hold one chord at a time; a note joins a chord struck with it, or the first voice free.\n type Word = { sounds: string[]; velocity: number; length: number };\n const voices: { words: Map; end: number; last: number }[] = [];\n const placed = source.notes.map((n) => {\n const from = Math.round(n.at * perBeat);\n return { from, length: kit ? 1 : Math.max(1, Math.round((n.at + n.beats) * perBeat) - from), sound: kit ? map[n.pitch] : nameOf(n.pitch), velocity: Math.max(0.05, Math.round((n.velocity / 100) * 20) / 20) };\n }).filter((n) => n.sound !== undefined && (!kit || n.sound in kit.hits)).sort((a, b) => a.from - b.from || b.length - a.length);\n for (const n of placed) {\n let voice = voices.find((v) => { const w = v.words.get(v.last); return v.last === n.from && w !== undefined && w.length === n.length && w.velocity === n.velocity; });\n if (voice) { const w = voice.words.get(n.from)!; if (!w.sounds.includes(n.sound)) w.sounds.push(n.sound); continue; }\n voice = voices.find((v) => v.end <= n.from);\n if (!voice) { voice = { words: new Map(), end: 0, last: -1 }; voices.push(voice); }\n voice.words.set(n.from, { sounds: [n.sound], velocity: n.velocity, length: n.length });\n voice.end = n.from + n.length;\n voice.last = n.from;\n }\n for (const v of voices) {\n const steps: string[] = Array.from({ length: v.end }, () => \".\");\n for (const [at, w] of v.words) {\n steps[at] = (kit ? w.sounds.join(\"\") : w.sounds.join(\"+\")) + (w.velocity === 1 ? \"\" : `@${w.velocity}`);\n if (!kit) for (let k = 1; k < w.length; k++) steps[at + k] = \"-\";\n }\n tracks.push(track(instrument, steps, how));\n }\n }\n return compose({ ...options, bpm: options.bpm ?? notes.bpm, beats: options.beats ?? notes.beats }, tracks, []);\n}\n\n/** The song's reverb in `render`, as `Clip.reverb` takes it, the dry part left out. */\nexport interface ReverbOptions { readonly decay?: number; readonly damping?: number; readonly predelay?: number; readonly width?: number }\n\n/**\n * A song mixed into one clip when the script is built: every note exactly in time, one sound playing. Mixed as a\n * mixing desk does: each track's notes made again at `rate` (44 100 when left out: what the ear hears up to 20 kHz; a\n * note played in the game note by note is made at 22 050, to keep the map small) into its own part, through a\n * section's effects where it has them, then its `fx`, at its volume, to its side; what each track sends (its `reverb`)\n * into one reverb shared by them all (`reverb`, its room); the whole through `master` —\n * `(mix) => mix.compress({ threshold: -14, ratio: 2 })` — and last a limiter (`Clip.limit`) so it never goes over\n * −1.5 dB however the parts add, between the samples too: `limit: false` leaves it as mixed, `{ ceiling, release }` sets it. A long clip a\n * program plays goes in the map as Ogg Vorbis, two channels when it has two.\n */\nexport function render(s: Song, options: { readonly rate?: number; readonly limit?: boolean | { readonly ceiling?: number; readonly release?: number }; readonly reverb?: ReverbOptions; readonly master?: (mix: Clip) => Clip } = {}): Clip {\n const rate = options.rate ?? 44100, score = scores.get(s), least = s.steps * s.stepFrames * FRAME;\n let mixed: Clip;\n if (!score) {\n // (A song from elsewhere, without its score: its own clips, at their rate.)\n mixed = mixdown(s.at.map((at, k) => ({ clip: s.clips[s.which[k]], at: at * FRAME })), s.seconds, rate);\n } else {\n const parts: { clip: Clip; at: number }[] = [], sends: { clip: Clip; at: number }[] = [];\n const byTrack = new Map();\n for (const n of score) { const list = byTrack.get(n.track); if (list) list.push(n); else byTrack.set(n.track, [n]); }\n for (const [t, notes] of byTrack) {\n const clipOf = (n: Score) => { const c = n.hit ?? (t.instrument as Instrument).note(n.name!, n.seconds!, rate); return n.velocity === 1 ? c : c.gain(n.velocity); };\n // The notes as they are, and each section's that has effects of its own mixed apart and through them.\n const entries: { clip: Clip; at: number }[] = [], apart = new Map();\n for (const n of notes) {\n if (n.piece?.fx) { const list = apart.get(n.piece); if (list) list.push(n); else apart.set(n.piece, [n]); }\n else entries.push({ clip: clipOf(n), at: n.at * FRAME });\n }\n for (const [p, list] of apart) {\n const from = p.from * s.stepFrames;\n const sub = mixdown(list.map((n) => ({ clip: clipOf(n), at: (n.at - from) * FRAME })), p.time.seconds, rate);\n entries.push({ clip: p.fx!(sub, p.time), at: from * FRAME });\n }\n let part = mixdown(entries, least, rate);\n if (t.fx) part = t.fx(part);\n if (t.volume !== 1) part = part.gain(t.volume);\n if (t.pan !== 0) part = part.pan(t.pan);\n parts.push({ clip: part, at: 0 });\n if (t.reverb > 0) sends.push({ clip: part.gain(t.reverb), at: 0 });\n }\n if (sends.length) {\n const r = options.reverb ?? {};\n parts.push({ clip: mixdown(sends, least, rate).reverb({ decay: r.decay ?? 0.7, damping: r.damping ?? 0.45, predelay: r.predelay ?? 0.02, width: r.width ?? 1, mix: 1, dry: 0 }), at: 0 });\n }\n mixed = mixdown(parts, least, rate);\n }\n if (options.master) mixed = options.master(mixed);\n const limit = options.limit ?? true;\n return limit === false ? mixed : mixed.limit(limit === true ? {} : limit);\n}\n\n/* ── Music that follows the game: a song cut into pieces, played piece by piece ── */\n\n/** How a song of sections is cut for the game to play piece by piece (`stems`). */\nexport interface StemsOptions {\n /**\n * Which parts each layer holds, by name — `{ drums: [\"kit\", \"hats\"], bass: [\"bass\"], melody: [\"lead\", \"saw\"] }` — a\n * layer the game can turn on and off where a piece begins. A part in no layer is left out.\n */\n readonly layers: Readonly>;\n /**\n * Beats a piece: where the game can go to another section or turn a layer on or off (a bar's beats when left out).\n * Two in a bar of four halves the wait for a change, for about half as much map again and more sounds at once.\n */\n readonly every?: number;\n /** Samples a second (44 100). */\n readonly rate?: number;\n /** The reverb's room, as `render` takes it. */\n readonly reverb?: ReverbOptions;\n /** What each piece's clip goes through last — an EQ, not a compressor: each is on its own, the game adds them up. */\n readonly master?: (clip: Clip) => Clip;\n /** The loudest the pieces may add up to as the song plays through, every layer on (0.84, −1.5 dB). */\n readonly peak?: number;\n /**\n * Seconds a piece's clip rings on after its notes end (2), its echoes and reverb faded out over the last of them: the\n * pieces after it cover the rest, and a shorter clip is a smaller map. A layer's last piece keeps all of its tail.\n */\n readonly tail?: number;\n /** The Ogg Vorbis quality of the pieces' clips (4: a little smaller than a song's 5, as many play at once). */\n readonly quality?: number;\n /**\n * Sounds to play on the next beat when the game asks (`band.sting`): a crash for a cleared line, a hit for a boss.\n * Turned down with the pieces, which leave room for one on any beat.\n */\n readonly stingers?: Readonly>;\n}\n\n/** A song cut into pieces for the game to play piece by piece, with `conduct` and a `Band`: made when the script is built. */\nexport interface Stems {\n readonly kind: \"stems\";\n /** Frames a bar lasts. */\n readonly barFrames: number;\n readonly bpm: number;\n /** Beats a bar, and beats a piece. */\n readonly beats: number;\n readonly every: number;\n /** Frames each beat of a piece lasts, whole frames (beats of a bar not a whole number of frames take turns). */\n readonly waits: readonly number[];\n /** The layers' names, in the order `band.on` has them. */\n readonly layers: readonly string[];\n /** The sections, in the order the song plays them: their names and how many bars. */\n readonly sections: readonly SongSection[];\n /**\n * Section `s`'s piece `p` in layer `l`: its notes, their tails and echoes and reverb with them, or null where it plays\n * none. A section of `bars` bars has bars × beats / every pieces.\n */\n readonly pieces: readonly (readonly (readonly (Clip | null)[])[])[];\n readonly stingers: readonly Clip[];\n /** A section's place in `sections`, by its name: `band.go(stems.section(\"drop\"))`. */\n section(name: string): number;\n /** A layer's place in `layers`, by its name: `band.mute(stems.layer(\"drums\"))`. */\n layer(name: string): number;\n /** A stinger's place, by its name: `band.sting(stems.stinger(\"crash\"))`. */\n stinger(name: string): number;\n /** The most clips sounding at once as the song plays through, every layer on: of the game's 32, what is left for the rest. */\n readonly voices: number;\n /** The pieces added up as the game plays the song through, every layer on, each on its frame: to hear it, or compare. */\n readonly playthrough: Clip;\n}\n\n/**\n * A song of sections (`arrange`) cut into pieces — a bar each, or `every` beats — a clip each piece of each layer, for\n * music that follows the game: where each piece begins the program says which section plays and which layers (`Band`,\n * `conduct`), and a stinger plays on the next beat. A piece's clip holds the notes that start in it, with their tails,\n * echoes and reverb, so it overlaps the next and nothing is cut off, whichever comes after it; a piece the same as\n * another is the same clip, one file in the map. Each layer is mixed as `render` mixes the song, but with no master\n * and no limiter between the parts — the game adds the clips up — so all of them are turned down together until the\n * song played through stays under `peak`.\n */\nexport function stems(s: Song, options: StemsOptions): Stems {\n const parts = partsOf.get(s), score = scores.get(s);\n if (!parts || !score) throw new Error(\"stems() cuts a song made of sections, by music.arrange.\");\n const rate = options.rate ?? 44100, layers = Object.keys(options.layers), r = options.reverb ?? {}, tail = options.tail ?? 2;\n if (!(tail > 0)) throw new Error(`stems()' tail is seconds after a piece, more than 0, not ${tail}.`);\n if (layers.length === 0 || layers.length > 8) throw new Error(`stems() takes one to eight layers, not ${layers.length}.`);\n const beats = s.beats, every = options.every ?? beats, piece = (s.barFrames * every) / beats;\n if (!Number.isInteger(every) || every < 1 || every > beats || beats % every !== 0) throw new Error(`stems() cuts every few beats of a bar's ${beats}, a number that divides it — not every ${every}.`);\n if (!Number.isInteger(piece)) throw new Error(`A piece of ${every} beats would be ${piece.toFixed(2)} frames: the game plays a sound on a frame. Make it a bar (every: ${beats}), or a tempo whose bar divides into whole frames.`);\n const perBar = beats / every;\n const layerOf = new Map();\n layers.forEach((name, l) => {\n for (const p of options.layers[name]) {\n const found = parts.find((x) => x.name === p);\n if (!found) throw new Error(`The layer ${name} names ${JSON.stringify(p)}, which is no part of the song: its parts are ${parts.map((x) => x.name).join(\", \")}.`);\n if (layerOf.has(found.track)) throw new Error(`${p} is in two layers: a part is in one at most.`);\n layerOf.set(found.track, l);\n }\n });\n const pieceSeconds = piece * FRAME, total = s.sections.reduce((n, x) => n + x.bars, 0) * perBar;\n // The notes of each piece of each layer, by part.\n const cells: Map[][] = Array.from({ length: total }, () => layers.map(() => new Map()));\n for (const n of score) {\n const l = layerOf.get(n.track);\n if (l === undefined) continue;\n const g = Math.min(total - 1, Math.floor((n.at + 1e-6) / piece));\n const m = cells[g][l], list = m.get(n.track);\n if (list) list.push(n); else m.set(n.track, [n]);\n }\n const clipOf = (t: Track, n: Score) => { const c = n.hit ?? (t.instrument as Instrument).note(n.name!, n.seconds!, rate); return n.velocity === 1 ? c : c.gain(n.velocity); };\n // Each layer's last piece with notes: it rings out whole, as the song ends.\n const last = layers.map((_, l) => { let g = total - 1; while (g > 0 && cells[g][l].size === 0) g--; return g; });\n // A piece of a layer, mixed: each part's notes from where it begins, through what the part goes through, and the reverb.\n const raw: (Clip | null)[][] = cells.map((byLayer, g) => byLayer.map((m, l) => {\n const out: { clip: Clip; at: number }[] = [], sends: { clip: Clip; at: number }[] = [];\n // Where the piece's last note ends, its release and all: a riser four bars long is kept whole.\n let end = pieceSeconds;\n for (const [t, notes] of m) for (const n of notes) end = Math.max(end, (n.at - g * piece) * FRAME + clipOf(t, n).seconds);\n for (const [t, notes] of m) {\n const plain = notes.filter((n) => !n.piece?.fx), apart = notes.filter((n) => n.piece?.fx);\n const entries: { clip: Clip; at: number }[] = plain.map((n) => ({ clip: clipOf(t, n), at: (n.at - g * piece) * FRAME }));\n if (apart.length) {\n // A section's effects take the whole section: this piece's notes where they fall in it, then the piece cut out.\n const p = apart[0].piece!, from = p.from * s.stepFrames, into = (g * piece - from) * FRAME;\n const whole = p.fx!(mixdown(apart.map((n) => ({ clip: clipOf(t, n), at: (n.at - from) * FRAME })), undefined, rate), p.time);\n entries.push({ clip: whole.trim(into, whole.seconds), at: 0 });\n }\n let part = mixdown(entries, undefined, rate);\n if (t.fx) part = t.fx(part);\n if (t.volume !== 1) part = part.gain(t.volume);\n if (t.pan !== 0) part = part.pan(t.pan);\n out.push({ clip: part, at: 0 });\n if (t.reverb > 0) sends.push({ clip: part.gain(t.reverb), at: 0 });\n }\n if (out.length === 0) return null;\n if (sends.length) out.push({ clip: mixdown(sends, undefined, rate).reverb({ decay: r.decay ?? 0.7, damping: r.damping ?? 0.45, predelay: r.predelay ?? 0.02, width: r.width ?? 1, mix: 1, dry: 0 }), at: 0 });\n let mixed = mixdown(out, undefined, rate);\n if (options.master) mixed = options.master(mixed);\n // What rings on after the piece's notes, kept for `tail` seconds and faded over the last of them — the pieces\n // after cover the rest — but all of it after the layer's last piece.\n const keep = end + tail;\n return g < last[l] && mixed.seconds > keep ? mixed.trim(0, keep).fade(0, Math.min(1, tail / 2)) : mixed;\n }));\n // All of them turned down together, as the song plays through.\n const placed: { clip: Clip; at: number; g: number; l: number }[] = [];\n raw.forEach((byLayer, g) => byLayer.forEach((c, l) => { if (c) placed.push({ clip: c, at: g * pieceSeconds, g, l }); }));\n if (placed.length === 0) throw new Error(\"stems(): the layers play no notes.\");\n // The stingers may play on any beat: turned down with the pieces, and room left for them over the loudest.\n const waits = Array.from({ length: every }, (_, j) => Math.round(((j + 1) * piece) / every) - Math.round((j * piece) / every));\n const stingerNames = Object.keys(options.stingers ?? {});\n const lines: number[] = [];\n for (let g = 0; g < total; g++) { let at = g * piece; for (const w of waits) { lines.push(at * FRAME); at += w; } }\n const fitted = together(placed, { peak: options.peak ?? 0.84, over: stingerNames.map((k) => ({ clip: options.stingers![k], at: lines })) });\n const grid: (Clip | null)[][] = raw.map((byLayer) => byLayer.map(() => null));\n const quality = options.quality ?? 4;\n placed.forEach((p, k) => { grid[p.g][p.l] = fitted[k].ogg(quality); });\n const stingers = stingerNames.map((_, k) => fitted[placed.length + k]);\n let start = 0;\n const pieces = s.sections.map((sec) => { const n = sec.bars * perBar, rows = grid.slice(start, start + n).map((row) => Object.freeze(row)); start += n; return Object.freeze(rows); });\n const lookup = (what: string, list: readonly string[], name: string) => {\n const k = list.indexOf(name);\n if (k < 0) throw new Error(`There is no ${what} ${JSON.stringify(name)}: there are ${list.map((x) => JSON.stringify(x)).join(\", \") || \"none\"}.`);\n return k;\n };\n let voices = -1;\n return Object.freeze({\n kind: \"stems\" as const, barFrames: s.barFrames, bpm: s.bpm, beats, every, waits: Object.freeze(waits),\n layers: Object.freeze(layers), sections: s.sections, pieces: Object.freeze(pieces), stingers: Object.freeze(stingers),\n section: (name: string) => lookup(\"section\", s.sections.map((x) => x.name), name),\n layer: (name: string) => lookup(\"layer\", layers, name),\n stinger: (name: string) => lookup(\"stinger\", stingerNames, name),\n get playthrough() {\n return mixdown(placed.map((p) => ({ clip: grid[p.g][p.l]!, at: p.at })));\n },\n get voices() {\n if (voices < 0) {\n voices = 0;\n for (const p of placed) { let n = 0; for (const q of placed) if (q.at <= p.at && q.at + q.clip.seconds > p.at) n++; voices = Math.max(voices, n); }\n }\n return voices;\n },\n });\n}\n\n/**\n * The music as the game steers it: which section plays, which piece of it, which layers. Make one inside the program\n * that steers the music and hand it to `conduct`, which plays a piece where each begins as it says:\n *\n * import { music, Band } from \"trigscript/music\";\n * const band = new Band();\n * start(music.conduct(stems, band));\n * band.go(stems.section(\"drop\")); // where the next piece begins\n * band.mute(stems.layer(\"drums\"));\n * band.sting(stems.stinger(\"crash\")); // on the next beat\n *\n * With nothing said, the song plays through its sections in order and starts again.\n */\nexport class Band {\n /** The section playing, its place in `stems.sections`; the piece of it (a bar, unless `stems.every` says), from 0; the beat of the piece. */\n section = 0;\n piece = 0;\n beat = 0;\n /** Pieces played since it started. */\n count = 0;\n /** The game's frame the beat playing began on: how far into the beat it is, for what moves with the music. */\n at = 0;\n /** The section to go to, and whether where the next piece begins (`go`) or when this section ends (`queue`); −1 for none. */\n next = -1;\n now = false;\n /** Whether a section plays again when it ends, instead of going on to the next. */\n loop = false;\n /** Which layers play, by their place in `stems.layers`. */\n on = [true, true, true, true, true, true, true, true];\n /** A stinger to play on the next beat, by its place; −1 for none. */\n stung = -1;\n /** Whether it plays: false stops it where the next piece would begin (what sounds rings out). */\n playing = true;\n\n /** Go to a section where the next piece begins, from its first piece. */\n go(section: number) {\n this.next = section;\n this.now = true;\n }\n /** Go to a section when the one playing ends. */\n queue(section: number) {\n this.next = section;\n this.now = false;\n }\n /** Turn a layer off, or on, from where the next piece begins. */\n mute(layer: number) {\n this.on[layer] = false;\n }\n unmute(layer: number) {\n this.on[layer] = true;\n }\n /** Play a stinger on the next beat. */\n sting(stinger: number) {\n this.stung = stinger;\n }\n /** Stop where the next piece would begin. */\n stop() {\n this.playing = false;\n }\n}\n\n/** Play `stems` as `band` says, beat by beat, until it stops: `start(music.conduct(stems, band))` beside the program. */\nexport const conduct = game(async (s: Stems, band: Band) => {\n band.playing = true;\n band.beat = 0;\n while (band.playing || band.beat > 0) {\n band.at = memory.game.frame;\n if (band.beat === 0) {\n // Where a piece begins: where to be, then the piece's clip of each layer that is on. (Which clip, by test: a\n // clip is no value a program holds.)\n if (band.next >= 0 && (band.now || band.piece === 0)) {\n band.section = band.next;\n band.piece = 0;\n band.next = -1;\n }\n for (let k = 0; k < s.sections.length; k++) if (band.section === k) {\n for (let p = 0; p < s.pieces[k].length; p++) if (band.piece === p) {\n for (let l = 0; l < s.layers.length; l++) if (s.pieces[k][p][l]) if (band.on[l]) playWav(s.pieces[k][p][l]!, 0);\n }\n }\n }\n for (let j = 0; j < s.stingers.length; j++) if (band.stung === j) playWav(s.stingers[j], 0);\n band.stung = -1;\n for (let j = 0; j < s.every; j++) if (band.beat === j) await sleep(frames(s.waits[j]));\n band.beat++;\n if (band.beat >= s.every) {\n band.beat = 0;\n band.count++;\n band.piece++;\n // A section ended: again, or on to the next (after the last, the first).\n for (let k = 0; k < s.sections.length; k++) if (band.section === k) if (band.piece >= s.pieces[k].length) {\n band.piece = 0;\n if (!band.loop) band.section = k + 1 < s.sections.length ? k + 1 : 0;\n }\n if (!band.playing) band.beat = 0;\n }\n }\n});\n\n/**\n * Play a song, note by note, inside an async program: \\`await music.play(song)\\` resolves at its end, or\n * \\`start(music.play(song))\\` plays it beside the program.\n */\nexport const play = game(async (s: Song) => {\n const at = s.at;\n const which = s.which;\n let i = 0;\n let now = 0;\n while (i < at.length) {\n if (at[i] > now) {\n await sleep(frames(at[i] - now));\n now = at[i];\n }\n const k = which[i];\n // Which clip, by its number: a test a clip (a loop over a length known when the script is built is unrolled, and\n // the clips stay the song's, known when it is built — a clip is no value a program holds).\n for (let j = 0; j < s.clips.length; j++) if (k === j) playWav(s.clips[j], 0);\n i++;\n }\n if (s.frames > now) await sleep(frames(s.frames - now));\n});\n\n/** Play a song again and again; start it beside a program: \\`start(music.loop(song))\\`. */\nexport const loop = game(async (s: Song) => {\n // (\\`play\\` written out again, so that the wait at the end of each turn is where the loop shows it — at least a frame,\n // so that a song with nothing left at its end does not turn round within one frame for ever.)\n const at = s.at;\n const which = s.which;\n while (true) {\n let i = 0;\n let now = 0;\n while (i < at.length) {\n if (at[i] > now) {\n await sleep(frames(at[i] - now));\n now = at[i];\n }\n const k = which[i];\n for (let j = 0; j < s.clips.length; j++) if (k === j) playWav(s.clips[j], 0);\n i++;\n }\n await sleep(frames(Math.max(s.frames - now, 1)));\n }\n});\n\n/** Everything above as one name: import { music } from \"trigscript/music\"; music.play(song). */\nexport const music = {\n sampler, synth, fmSynth, strings, presets, drums, kit, track, song, render, play, loop,\n part, section, arrange, transpose, stretch, repeat, beat, chord, over, chords, bars, pump, riser, impact, midi, stems, conduct,\n};\n", "random": "/**\n * Chance: the standard library's \"trigscript/random\" (TrigScript 4).\n *\n * import { chance, range, pick, shuffle, weighted, Random } from \"trigscript/random\";\n *\n * if (chance(25)) dropLoot(); // one time in four\n * const lane = range(1, 3); // 1, 2 or 3\n * const boss = pick(BOSSES); // any one of a list\n * shuffle(order); // an array of the program, in place\n * const tier = weighted([70, 25, 5]); // 0 most of the time, 2 rarely\n *\n * const rng = new Random(seed); // the same numbers every time for the same seed\n * const x = rng.range(0, 63);\n *\n * The functions use the game's own random numbers, which every player's game draws alike, so nothing goes out of step\n * in a game of several players. `Random` is a generator of your own (xorshift), seeded: the same seed gives the same\n * numbers, game after game — a level made from a seed, a save code that holds one.\n */\nimport { random } from \"trigscript\";\n\n/** True `percent` times in a hundred (25: one time in four). */\nexport function chance(percent: number): boolean {\n return random(100) < percent;\n}\n\n/** A whole number from `min` to `max`, both included. */\nexport function range(min: number, max: number): number {\n return min + random(max - min + 1);\n}\n\n/** The total of `count` dice of `sides` sides each (3, 6: from 3 to 18). */\nexport function dice(count: number, sides: number): number {\n let total = 0;\n let i = 0;\n while (i < count) { total += 1 + random(sides); i++; }\n return total;\n}\n\n/** One of a list, any one alike; a list the script has, or an array of the program (the first's 0 when it is empty). */\nexport function pick(items: readonly T[]): T {\n return items[random(items.length)];\n}\n\n/** The place of one of the weights, each as likely as its share of their total: [70, 25, 5] gives 0 most of the time. */\nexport function weighted(weights: readonly number[]): number {\n let total = 0;\n for (const w of weights) total += w;\n let roll = random(total);\n let i = 0;\n while (i < weights.length) {\n if (roll < weights[i]) return i;\n roll -= weights[i];\n i++;\n }\n return weights.length - 1;\n}\n\n/** An array of the program in a random order, in place (Fisher–Yates: every order alike). */\nexport function shuffle(items: T[]) {\n let i = items.length - 1;\n while (i > 0) {\n const j = random(i + 1);\n const held = items[i];\n items[i] = items[j];\n items[j] = held;\n i--;\n }\n}\n\n/**\n * A generator of random numbers of your own, seeded: the same seed gives the same numbers, in the game and when the\n * script is tested. Make it in the program (`const rng = new Random(1234)`).\n */\nexport class Random {\n state: number;\n\n /** Seeded with any whole number (0 is taken as 1). */\n constructor(seed: number) {\n this.state = seed === 0 ? 1 : seed;\n }\n\n /** The next number, from 0 to 2 147 483 647. */\n next(): number {\n let x = this.state;\n x ^= x << 13;\n x ^= x >>> 17;\n x ^= x << 5;\n this.state = x;\n return x & 0x7fffffff;\n }\n\n /** A whole number from 0 to n − 1. */\n int(n: number): number {\n return this.next() % n;\n }\n\n /** A whole number from `min` to `max`, both included. */\n range(min: number, max: number): number {\n return min + this.int(max - min + 1);\n }\n\n /** True `percent` times in a hundred. */\n chance(percent: number): boolean {\n return this.int(100) < percent;\n }\n\n /** One of a list, any one alike. */\n pick(items: readonly T[]): T {\n return items[this.int(items.length)];\n }\n\n /** An array of the program in a random order, in place. */\n shuffle(items: T[]) {\n let i = items.length - 1;\n while (i > 0) {\n const j = this.int(i + 1);\n const held = items[i];\n items[i] = items[j];\n items[j] = held;\n i--;\n }\n }\n}\n", "screen": "/**\n * Pixel screens: the standard library's \"trigscript/screen\" (TrigScript 4).\n *\n * import { Screen, ScreenView, sprite } from \"trigscript/screen\";\n *\n * const SHIP = sprite([\".1.\", \"111\", \"1.1\"]);\n *\n * program(async () => {\n * const screen = new Screen(4, 4, 24, 16); // 24 × 16 pixels, its corner on tile (4, 4)\n * let x = 10;\n * while (true) {\n * screen.clear();\n * screen.rect(0, 15, 24, 1, 5); // the ground, in colour 5\n * screen.blit(SHIP, x, 12);\n * screen.number(score, 1, 1, 2); // digits 3 × 5 pixels, in colour 2\n * screen.present(); // on the map, and so on the minimap (screen.reveal(P1) once first)\n * await nextFrame();\n * }\n * });\n *\n * A screen is a grid of pixels, each 0 (empty) or a colour from 1 to 7; drawing changes the grid, and `present()`\n * shows it: a pixel is a unit on the map, owned by the player whose colour it is, so the minimap shows the picture —\n * neighbouring pixels of one colour join into a solid block there. Only the pixels that changed since the last\n * `present()` are made or taken away, and only the pixels drawn since are looked at, so a still picture costs nothing to\n * show. Draw through the methods (`set`, `clear`, `rect`, …): a pixel written into `pixels` directly is not seen. The same grid can be shown in a\n * player's text lines too, as squares (`ScreenView` in a `ui.lines` view): one game, two screens, as the Tetris probe has it.\n *\n * A player sees the pixels only where they have vision: `screen.reveal(P1)` once gives them Map Revealers over it.\n *\n * Two looks (the constructor's last argument): \"dots\", Overlords, which fly — the minimap draws each as a dot with a gap\n * round it, the screen may be over Space; and \"solid\", Pylons, which the minimap draws as their whole two-tile\n * footprint, so pixels side by side are one block — on buildable ground (probes/lib/screen-map.mts --ground).\n *\n * What the map needs, found playing the unit-pixel probe in StarCraft: Remastered (2026-10-16): the owners of the\n * pixels (P3, P4, P5, P1, P6, P7, P8 below; their colours are theirs on the map, so give them the colours of TEXT_COLORS) must not\n * be computers — a computer moves its own units — and Remastered will not start a game without a computer, so one slot\n * (P2 here) is a computer that owns no pixel. A pixel is an Overlord, which sits on any ground and on nothing else; keep\n * the screen's tiles clear of buildings. A Pylon needs buildable ground, and none of the map's own units in the way. About 80 pixels made and taken away in a frame did not slow the game.\n */\nimport { centerLocation, createUnit, removeUnitAt, reserveLocation, textColors, units, P1, P3, P4, P5, P6, P7, P8, type Player } from \"trigscript\";\n\n/**\n * Whose units show colours 1 to 7 on the map, in the order of TEXT_COLORS: give each player that colour in the map\n * (cyan, yellow, purple, green, red, blue, orange). Green is P1's — your own units are green on the minimap anyway.\n * Not computers.\n */\nexport const OWNERS: readonly Player[] = [P3, P4, P5, P1, P6, P7, P8];\n/** The same colours in the text lines (`ScreenView`), 0 an empty square: cyan, yellow, purple, green, red, blue, orange. */\nexport const TEXT_COLORS: readonly number[] = [0, textColors.cyan, textColors.yellow, textColors.purple, textColors.green, textColors.red, textColors.blue, textColors.orange];\n\n/** Where each pixel's unit is made: a location the map does not use, taken when the script is built. */\nconst SPOT = reserveLocation(\"screen: where each pixel is made\");\n\n/** A picture known when the script is built, to `blit` onto a screen: its size, and its pixels a row at a time (0 is see-through). */\nexport interface Sprite { readonly width: number; readonly height: number; readonly cells: readonly number[] }\n\n/**\n * A sprite from rows of text, when the script is built: \"1\" … \"7\" a pixel of that colour, anything else see-through.\n *\n * const SHIP = sprite([\".1.\", \"111\", \"1.1\"]);\n */\nexport function sprite(rows: readonly string[]): Sprite {\n const width = Math.max(0, ...rows.map((r) => r.length));\n const cells: number[] = [];\n for (const row of rows) for (let x = 0; x < width; x++) { const c = Number(row[x]); cells.push(c >= 1 && c <= 7 ? c : 0); }\n return { width, height: rows.length, cells };\n}\n\n/** The digits 0 to 9, three pixels wide and five high, for `number()`: each fifteen pixels, a row at a time. */\nconst DIGITS: readonly number[] = [\n \"111101101101111\", \"010110010010111\", \"111001111100111\", \"111001111001111\", \"101101111001001\",\n \"111100111001111\", \"111100111101111\", \"111001001010010\", \"111101111101111\", \"111101111001111\",\n].flatMap((d) => d.split(\"\").map(Number));\n\n/** The most pixels a screen has: 32 × 32 at two tiles a pixel covers a 64 × 64 map, 64 × 32 half of a 128 × 128 one. */\nexport const MAX_PIXELS = 2048;\n\nexport class Screen {\n /** The picture being drawn: a colour a pixel, a row at a time (the first width × height of them). */\n pixels: number[] = new Array(MAX_PIXELS).fill(0);\n /** What the map shows now: the picture as it was at the last `present()`. */\n shown: number[] = new Array(MAX_PIXELS).fill(0);\n /** The pixels drawn a colour they were not since the last `present()`, in order: each one's place and its x and y. */\n drawnAt: number[] = new Array(MAX_PIXELS).fill(0);\n drawnX: number[] = new Array(MAX_PIXELS).fill(0);\n drawnY: number[] = new Array(MAX_PIXELS).fill(0);\n drawn = 0;\n /** More drawn than the list holds: `present()` looks at every pixel instead. */\n overflow = false;\n x: number;\n y: number;\n width: number;\n height: number;\n cell: number;\n /** The unit a pixel is: an Overlord (\"dots\"), or a Pylon (\"solid\"). */\n unit = 0;\n\n /**\n * A screen `width` × `height` pixels (MAX_PIXELS at most), its top left corner on tile (x, y) of the map, a pixel two\n * tiles across.\n * @param look \"dots\" (the default): a pixel is an Overlord, which the minimap draws as a dot, a gap round it — it flies,\n * so the screen may be over anything, Space too. \"solid\": a pixel is a Pylon, which the minimap draws as its whole\n * footprint, two tiles square, so pixels side by side are one block — it needs buildable ground under the screen,\n * and gives its owner Protoss supply. (Overlords a tile apart do not fit: the game will not place most of them.)\n */\n constructor(x: number, y: number, width: number, height: number, look: \"dots\" | \"solid\" = \"dots\") {\n this.x = x;\n this.y = y;\n this.width = width;\n this.height = height;\n this.cell = 64;\n this.unit = look === \"solid\" ? units.ProtossPylon : units.ZergOverlord;\n }\n\n /** Every pixel the colour (0, empty, unless it says). */\n clear(color: number = 0) {\n let i = 0;\n let y = 0;\n while (y < this.height) {\n let x = 0;\n while (x < this.width) { this.put(i, x, y, color); i++; x++; }\n y++;\n }\n }\n\n /** One pixel; nothing happens outside the screen. */\n set(x: number, y: number, color: number) {\n if (x >= 0 && y >= 0 && x < this.width && y < this.height) this.put(y * this.width + x, x, y, color);\n }\n\n /** A pixel written, and listed for `present()` when its colour changed. */\n private put(i: number, x: number, y: number, color: number) {\n if (this.pixels[i] === color) return;\n this.pixels[i] = color;\n if (this.drawn < MAX_PIXELS) {\n this.drawnAt[this.drawn] = i;\n this.drawnX[this.drawn] = x;\n this.drawnY[this.drawn] = y;\n this.drawn++;\n } else this.overflow = true;\n }\n\n /** The colour of a pixel; 0 outside the screen. */\n get(x: number, y: number): number {\n if (x >= 0 && y >= 0 && x < this.width && y < this.height) return this.pixels[y * this.width + x];\n return 0;\n }\n\n /** A filled rectangle, its top left at (x, y); what is outside the screen is left out. */\n rect(x: number, y: number, width: number, height: number, color: number) {\n let j = 0;\n while (j < height) {\n let i = 0;\n while (i < width) { this.set(x + i, y + j, color); i++; }\n j++;\n }\n }\n\n /** A line from (x0, y0) to (x1, y1), both ends drawn. */\n line(x0: number, y0: number, x1: number, y1: number, color: number) {\n const dx = Math.abs(x1 - x0);\n const dy = -Math.abs(y1 - y0);\n const sx = x0 < x1 ? 1 : -1;\n const sy = y0 < y1 ? 1 : -1;\n let err = dx + dy;\n let x = x0;\n let y = y0;\n while (true) {\n this.set(x, y, color);\n if (x === x1 && y === y1) break;\n const e2 = 2 * err;\n if (e2 >= dy) { err += dy; x += sx; }\n if (e2 <= dx) { err += dx; y += sy; }\n }\n }\n\n /** A sprite with its top left at (x, y): its see-through pixels leave what is under them. */\n blit(s: Sprite, x: number, y: number) {\n let k = 0;\n let j = 0;\n while (j < s.height) {\n let i = 0;\n while (i < s.width) {\n const c = s.cells[k];\n if (c !== 0) this.set(x + i, y + j, c);\n k++;\n i++;\n }\n j++;\n }\n }\n\n /** A whole number's digits, 3 × 5 pixels each with a pixel between, its top left at (x, y): a score on the minimap. */\n number(n: number, x: number, y: number, color: number) {\n let rest = n < 0 ? -n : n;\n let count = 1;\n let probe = rest;\n while (probe >= 10) { probe = Math.floor(probe / 10); count++; }\n let place = x + (count - 1) * 4;\n while (true) {\n const d = rest % 10;\n let k = 0;\n while (k < 15) {\n if (DIGITS[d * 15 + k] === 1) this.set(place + (k % 3), y + Math.floor(k / 3), color);\n k++;\n }\n rest = Math.floor(rest / 10);\n place -= 4;\n if (rest === 0) break;\n }\n }\n\n /**\n * Let a player see the screen: Map Revealers of theirs over it, for the rest of the game. Without them its pixels,\n * owned by other players, are hidden by the fog of war — on the map and on the minimap. Once, before the first\n * `present()`.\n * @param player Who sees it: P1 … P8, known when the script is built.\n */\n reveal(player: Player) {\n // A Map Revealer sees ten tiles round; one every six tiles leaves no gap.\n const right = this.x * 32 + this.width * this.cell;\n const bottom = this.y * 32 + this.height * this.cell;\n let py = this.y * 32 + 96;\n while (py < bottom + 96) {\n let px = this.x * 32 + 96;\n while (px < right + 96) {\n centerLocation(SPOT, px, py, 32, 32);\n createUnit(player, units.MapRevealer, 1, SPOT);\n px += 192;\n }\n py += 192;\n }\n }\n\n /**\n * The picture on the map: each pixel that changed since the last time taken away and made again in its colour. Only\n * the pixels drawn since are looked at (one drawn and drawn back is passed over), unless more were drawn than the list\n * holds.\n */\n present() {\n if (this.overflow) {\n let i = 0;\n let y = 0;\n while (y < this.height) {\n let x = 0;\n while (x < this.width) { this.show(i, x, y); i++; x++; }\n y++;\n }\n this.overflow = false;\n } else {\n for (let k = 0; k < this.drawn; k++) this.show(this.drawnAt[k], this.drawnX[k], this.drawnY[k]);\n }\n this.drawn = 0;\n }\n\n /** One pixel on the map as it is in the picture, when it is not already. */\n private show(i: number, x: number, y: number) {\n const now = this.pixels[i];\n const was = this.shown[i];\n if (now === was) return;\n const half = this.cell >> 1;\n centerLocation(SPOT, this.x * 32 + x * this.cell + half, this.y * 32 + y * this.cell + half, 32, 32);\n OWNERS.forEach((owner, k) => { if (was === k + 1) removeUnitAt(owner, this.unit, 1, SPOT); });\n OWNERS.forEach((owner, k) => { if (now === k + 1) createUnit(owner, this.unit, 1, SPOT); });\n this.shown[i] = now;\n }\n}\n\n/**\n * A screen in the text lines, a square a pixel — filled in its colour, or empty — a line a row. For a `ui.lines` view:\n * ``, the screen's size known when the script is built. Eleven lines\n * show at once, sixty squares across.\n */\nexport function ScreenView({ screen, width, height }: { screen: Screen; width: number; height: number }) {\n return <>{UP_TO_60.map((y) => y < height && {UP_TO_60.map((x) => x < width && )})};\n}\n\n/** 0 … 59: the places a view's rows and columns are counted over, the ones past its size left out when the script is built. */\nconst UP_TO_60: readonly number[] = Array.from({ length: 60 }, (_, i) => i);\n", "signals": "/**\n * Events of your own: the standard library's \"trigscript/signals\" (TrigScript 4).\n *\n * import { Signal } from \"trigscript/signals\";\n *\n * const waveCleared = new Signal();\n *\n * program(async () => {\n * waveCleared.on((wave) => setResources(P1, \"add\", 100 * wave, \"ore\"));\n * waveCleared.once((wave) => print(`First wave down!`));\n * const id = waveCleared.on((wave) => print(`Wave ${wave} cleared`));\n * …\n * waveCleared.emit(3); // every handler, in the order they were added\n * waveCleared.off(id);\n * });\n *\n * A signal lets one part of a map say that something happened without knowing who listens: the shop, the score and the\n * music each add a handler, and the wave code only emits. What it carries is one value of the type it is made with —\n * a number, a boolean, a text or a unit (`new Signal()`). Its handlers are functions kept as values: they may use\n * what the block they were written in has, and they run to their end, one after another, within the frame.\n *\n * Make a signal inside the program that uses it (`const s = new Signal()` at its top): a signal is variables\n * of a program, as anything a program writes is.\n */\n\nexport class Signal {\n /** The handlers, by the number `on` gave; one taken off is undefined, and its place is given to the next one added. */\n handlers: (((value: T) => void) | undefined)[] = [];\n /** Which handlers go once they have run once. */\n onceOnly: boolean[] = [];\n\n /**\n * Add a handler: it runs at every `emit`, handed the value.\n * @returns Its number, for `off`.\n */\n on(handler: (value: T) => void): number {\n return this.add(handler, false);\n }\n\n /** Add a handler that runs at the next `emit` only. @returns Its number, for `off` before then. */\n once(handler: (value: T) => void): number {\n return this.add(handler, true);\n }\n\n /** Take a handler off, by the number `on` or `once` gave. */\n off(id: number) {\n if (id >= 0 && id < this.handlers.length) this.handlers[id] = undefined;\n }\n\n /** Run every handler, in the order they were added, each handed `value`. */\n emit(value: T) {\n let i = 0;\n while (i < this.handlers.length) {\n const h = this.handlers[i];\n if (h) {\n if (this.onceOnly[i]) this.handlers[i] = undefined;\n h(value);\n }\n i++;\n }\n }\n\n /** How many handlers it has. */\n count(): number {\n let n = 0;\n for (const h of this.handlers) if (h) n++;\n return n;\n }\n\n /** Take every handler off. */\n clear() {\n let i = 0;\n while (i < this.handlers.length) { this.handlers[i] = undefined; i++; }\n }\n\n private add(handler: (value: T) => void, once: boolean): number {\n // A place one was taken off from, or a new one at the end.\n let i = 0;\n while (i < this.handlers.length) {\n if (!this.handlers[i]) {\n this.handlers[i] = handler;\n this.onceOnly[i] = once;\n return i;\n }\n i++;\n }\n this.handlers.push(handler);\n this.onceOnly.push(once);\n return this.handlers.length - 1;\n }\n}\n", diff --git a/stdlib/music.ts b/stdlib/music.ts index 69f69e5..1c55aff 100644 --- a/stdlib/music.ts +++ b/stdlib/music.ts @@ -1151,7 +1151,24 @@ export const play = game(async (s: Song) => { /** Play a song again and again; start it beside a program: \`start(music.loop(song))\`. */ export const loop = game(async (s: Song) => { - while (true) await play(s); + // (\`play\` written out again, so that the wait at the end of each turn is where the loop shows it — at least a frame, + // so that a song with nothing left at its end does not turn round within one frame for ever.) + const at = s.at; + const which = s.which; + while (true) { + let i = 0; + let now = 0; + while (i < at.length) { + if (at[i] > now) { + await sleep(frames(at[i] - now)); + now = at[i]; + } + const k = which[i]; + for (let j = 0; j < s.clips.length; j++) if (k === j) playWav(s.clips[j], 0); + i++; + } + await sleep(frames(Math.max(s.frames - now, 1))); + } }); /** Everything above as one name: import { music } from "trigscript/music"; music.play(song). */ diff --git a/tests/music.test.ts b/tests/music.test.ts index f7c4fa5..818ddb2 100644 --- a/tests/music.test.ts +++ b/tests/music.test.ts @@ -90,6 +90,22 @@ program(async () => { await music.play(song); }); for (const p of played) expect(r.sounds.map((s) => s.path)).toContain(p.path); }); + it("plays it again and again beside the program: start(music.loop(song))", () => { + // (It never compiled: `while (true) await play(s)` showed the loop no wait of its own.) + const r = compile(`import { music } from "trigscript/music"; +const lead = music.synth(); +export const song = music.song({ bpm: 120 }, music.track(lead, "C5 . E5 .")); +program(async () => { start(music.loop(song)); }); +`); + expect(errors(r)).toEqual([]); + const sim = new ProgramSimulation(r.ir, { strings: r.strings }); + sim.run(60); + const frames = sim.events.filter((e) => e.action.type === ActionType.PlayWav).map((e) => e.frame); + // Two notes a turn, six frames apart; a turn of the song every twelve frames. + const start = frames[0]; + expect(frames.slice(0, 6).map((f) => f - start)).toEqual([0, 6, 12, 18, 24, 30]); + }); + it("renders into one clip, every note at its time", () => { const r = check( `const lead = music.synth({ volume: 0.3 });\nconst s = music.song({ bpm: 120 }, music.track(lead, "A4 . . . A4 . . ."));\nconst one = music.render(s);`, From 6d3bf90e5aa93e43f4b14be8fbc8cd9d3fca5ae3 Mon Sep 17 00:00:00 2001 From: Jeany Date: Mon, 19 Oct 2026 23:24:11 -0600 Subject: [PATCH 5/6] The ledger: a script for each feature, its step off eudplib, and what a map of our own makes of it 831 scripts in ledger//, each a feature of the language or the library, its first line the step of the way off eudplib that makes it a map of our own's and whether one takes it today, in the words of its refusal. 250 are taken: steps 1-3 whole, the start but for a ?: of booleans, fixed arrays but for a cell read at a shifted index. tests/ledger.test.ts holds every script to its first line, and holds the ledger to the language: every kind of the IR (but the six the front end takes out), every name a script has from the library, the members of on, ui, memory, local, a unit, a set of units and a task, and every export of the standard library's modules is in some script. scripts/ledger.mts prints the table by step, writes the first lines anew after a step (--write), and checks a few or lists what nothing reaches (--check, --missing). Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01T1CM9pLuTuAn3mgTEkjgVg --- ledger/arrays/array-compound-store.ts | 12 + ledger/arrays/array-destructured.ts | 12 + ledger/arrays/array-fill.ts | 9 + ledger/arrays/array-for-of.ts | 9 + ledger/arrays/array-from.ts | 10 + ledger/arrays/array-given-to-a-function.ts | 14 + ledger/arrays/array-in-a-record.ts | 9 + ledger/arrays/array-includes.ts | 10 + ledger/arrays/array-index-of.ts | 9 + .../arrays/array-length-of-a-fixed-array.ts | 9 + ledger/arrays/array-new-filled.ts | 9 + ledger/arrays/array-of-booleans.ts | 14 + ledger/arrays/array-of-instances.ts | 15 + ledger/arrays/array-of-records-that-grows.ts | 11 + ledger/arrays/array-of-records.ts | 13 + ledger/arrays/array-read-at-a-value.ts | 9 + ledger/arrays/array-read-past-the-end.ts | 9 + .../arrays/array-spread-into-a-fixed-array.ts | 10 + ledger/arrays/array-store-at-a-value.ts | 11 + ledger/arrays/array-store-past-the-end.ts | 9 + ledger/arrays/array-swap.ts | 10 + ledger/arrays/at.ts | 9 + ledger/arrays/concat.ts | 11 + ledger/arrays/every.ts | 10 + ledger/arrays/filter.ts | 10 + ledger/arrays/find-index.ts | 9 + ledger/arrays/find-last.ts | 9 + ledger/arrays/find.ts | 9 + ledger/arrays/for-each.ts | 13 + ledger/arrays/grid-made-when-built.ts | 10 + ledger/arrays/grid-read-and-store.ts | 16 + ledger/arrays/grid-row.ts | 15 + ledger/arrays/join.ts | 9 + ledger/arrays/last-index-of.ts | 9 + ledger/arrays/length-of-a-growing-array.ts | 11 + ledger/arrays/length-set.ts | 10 + .../arrays/list-of-records-read-at-a-value.ts | 12 + ledger/arrays/list-read-at-a-constant.ts | 9 + ledger/arrays/list-read-at-a-value.ts | 8 + ledger/arrays/map-for-of.ts | 13 + ledger/arrays/map-keyed-by-game-ids.ts | 15 + ledger/arrays/map-over-any-number.ts | 11 + ledger/arrays/map.ts | 10 + ledger/arrays/pop.ts | 10 + ledger/arrays/push.ts | 11 + ledger/arrays/read-at-a-shifted-index.ts | 10 + ledger/arrays/record-keyed-by-game-ids.ts | 14 + ledger/arrays/reduce.ts | 10 + ledger/arrays/rest-of-an-array.ts | 10 + ledger/arrays/reverse.ts | 10 + ledger/arrays/rows-that-grow.ts | 12 + ledger/arrays/set-keyed-by-game-ids.ts | 14 + ledger/arrays/set.ts | 12 + ledger/arrays/shift.ts | 10 + ledger/arrays/signal.ts | 16 + ledger/arrays/slice.ts | 10 + ledger/arrays/some.ts | 10 + ledger/arrays/sort.ts | 10 + ledger/arrays/splice.ts | 9 + ledger/arrays/spread-into-a-call.ts | 12 + ledger/arrays/spread-of-a-growing-array.ts | 12 + ledger/arrays/store-at-the-end.ts | 9 + ledger/arrays/to-reversed.ts | 10 + ledger/arrays/to-sorted.ts | 10 + ledger/arrays/u16-array.ts | 9 + ledger/arrays/u32-array.ts | 10 + ledger/arrays/u8-array.ts | 9 + ledger/arrays/unshift.ts | 9 + ledger/game/action-raw-record.ts | 6 + ledger/game/center-location-with-size.ts | 8 + ledger/game/center-location.ts | 9 + ledger/game/center-view.ts | 5 + ledger/game/condition-accumulate.ts | 5 + ledger/game/condition-always.ts | 5 + ledger/game/condition-bring.ts | 5 + ledger/game/condition-command-the-least-at.ts | 5 + ledger/game/condition-command-the-least.ts | 5 + ledger/game/condition-command-the-most-at.ts | 5 + ledger/game/condition-command-the-most.ts | 5 + ledger/game/condition-command.ts | 5 + ledger/game/condition-countdown-timer.ts | 5 + ledger/game/condition-deaths.ts | 5 + ledger/game/condition-elapsed-time.ts | 5 + ledger/game/condition-highest-score.ts | 5 + ledger/game/condition-in-a-while.ts | 6 + ledger/game/condition-kill.ts | 5 + ledger/game/condition-least-kills.ts | 5 + ledger/game/condition-least-resources.ts | 5 + ledger/game/condition-lowest-score.ts | 5 + ledger/game/condition-most-kills.ts | 5 + ledger/game/condition-most-resources.ts | 5 + ledger/game/condition-negated.ts | 5 + ledger/game/condition-never.ts | 5 + ledger/game/condition-opponents.ts | 5 + ledger/game/condition-raw-record.ts | 6 + ledger/game/condition-score.ts | 5 + ledger/game/condition-switch.ts | 5 + ledger/game/cover.ts | 14 + ledger/game/create-unit-count-from-a-value.ts | 7 + ledger/game/create-unit-type-from-a-value.ts | 8 + ...unit-with-properties-count-from-a-value.ts | 7 + ...-unit-with-properties-type-from-a-value.ts | 8 + ledger/game/create-unit-with-properties.ts | 6 + ledger/game/create-unit.ts | 5 + ledger/game/defeat.ts | 5 + ledger/game/display-text-with-a-number.ts | 8 + ledger/game/display-text.ts | 5 + ledger/game/draw.ts | 5 + ledger/game/give-units-count-from-a-value.ts | 7 + ledger/game/give-units-type-from-a-value.ts | 8 + ledger/game/give-units.ts | 5 + ledger/game/has-left.ts | 5 + ledger/game/is-human.ts | 5 + .../game/kill-unit-at-count-from-a-value.ts | 7 + ledger/game/kill-unit-at-type-from-a-value.ts | 8 + ledger/game/kill-unit-at.ts | 5 + ledger/game/kill-unit-type-from-a-value.ts | 8 + ledger/game/kill-unit.ts | 5 + ledger/game/leaderboard-computer-players.ts | 5 + ...eaderboard-control-at-type-from-a-value.ts | 8 + ledger/game/leaderboard-control-at.ts | 5 + .../leaderboard-control-type-from-a-value.ts | 8 + ledger/game/leaderboard-control.ts | 5 + ...board-goal-control-at-type-from-a-value.ts | 8 + ledger/game/leaderboard-goal-control-at.ts | 5 + ...derboard-goal-control-type-from-a-value.ts | 8 + ledger/game/leaderboard-goal-control.ts | 5 + ...eaderboard-goal-kills-type-from-a-value.ts | 8 + ledger/game/leaderboard-goal-kills.ts | 5 + ledger/game/leaderboard-goal-points.ts | 5 + ledger/game/leaderboard-goal-resources.ts | 5 + ledger/game/leaderboard-greed.ts | 5 + .../leaderboard-kills-type-from-a-value.ts | 8 + ledger/game/leaderboard-kills.ts | 5 + .../leaderboard-label-from-the-program.ts | 7 + ledger/game/leaderboard-points.ts | 5 + ledger/game/leaderboard-resources.ts | 5 + ledger/game/minimap-ping.ts | 5 + .../game/modify-energy-type-from-a-value.ts | 8 + ledger/game/modify-energy.ts | 5 + .../modify-hangar-count-type-from-a-value.ts | 8 + ledger/game/modify-hangar-count.ts | 5 + .../modify-hit-points-type-from-a-value.ts | 8 + ledger/game/modify-hit-points.ts | 5 + ledger/game/modify-resource-amount.ts | 5 + .../game/modify-shields-type-from-a-value.ts | 8 + ledger/game/modify-shields.ts | 5 + .../game/move-location-type-from-a-value.ts | 8 + ledger/game/move-location.ts | 5 + ledger/game/move-unit-type-from-a-value.ts | 8 + ledger/game/move-unit.ts | 5 + ledger/game/mute-unit-speech.ts | 5 + ledger/game/not-of-a-condition.ts | 6 + ledger/game/once.ts | 11 + ledger/game/order.ts | 7 + ledger/game/pause-game.ts | 5 + ledger/game/pause-timer.ts | 5 + ledger/game/read-accumulate.ts | 6 + ledger/game/read-bring.ts | 6 + ledger/game/read-command.ts | 6 + ledger/game/read-count-units-at.ts | 6 + ledger/game/read-count-units.ts | 6 + ledger/game/read-countdown-timer.ts | 6 + ledger/game/read-countdown.ts | 6 + ledger/game/read-deaths.ts | 6 + ledger/game/read-elapsed-time.ts | 6 + ledger/game/read-elapsed.ts | 6 + ledger/game/read-gas.ts | 6 + ledger/game/read-in-arithmetic.ts | 6 + ledger/game/read-kill.ts | 6 + ledger/game/read-kills.ts | 6 + ledger/game/read-minerals.ts | 6 + ledger/game/read-opponents.ts | 6 + ledger/game/read-race.ts | 5 + ledger/game/read-resources.ts | 6 + ledger/game/read-score.ts | 6 + ledger/game/read-slot.ts | 5 + ledger/game/read-supply-max.ts | 6 + ledger/game/read-supply-provided-of-a-race.ts | 6 + ledger/game/read-supply-used.ts | 6 + .../game/remove-unit-at-count-from-a-value.ts | 7 + .../game/remove-unit-at-type-from-a-value.ts | 8 + ledger/game/remove-unit-at.ts | 5 + ledger/game/remove-unit-type-from-a-value.ts | 8 + ledger/game/remove-unit.ts | 5 + ledger/game/reserve-location.ts | 8 + ledger/game/rose.ts | 11 + ledger/game/run-ai-script-at.ts | 5 + ledger/game/run-ai-script.ts | 5 + ledger/game/screen-max-pixels.ts | 7 + ledger/game/screen-owners.ts | 9 + ledger/game/screen-sprite.ts | 11 + ledger/game/screen-text-colors.tsx | 8 + ledger/game/screen-view.tsx | 10 + ledger/game/screen.ts | 10 + ledger/game/set-alliance-status.ts | 5 + .../game/set-countdown-timer-from-a-value.ts | 7 + ledger/game/set-countdown-timer.ts | 5 + ledger/game/set-deaths-amount-from-a-value.ts | 7 + ledger/game/set-deaths-unit-from-a-value.ts | 8 + ledger/game/set-deaths.ts | 5 + .../set-doodad-state-type-from-a-value.ts | 8 + ledger/game/set-doodad-state.ts | 5 + .../set-invincibility-type-from-a-value.ts | 8 + ledger/game/set-invincibility.ts | 5 + ...ission-objectives-text-from-the-program.ts | 7 + ledger/game/set-mission-objectives.ts | 5 + ledger/game/set-next-scenario.ts | 5 + .../game/set-resources-amount-from-a-read.ts | 5 + .../game/set-resources-amount-from-a-value.ts | 7 + ledger/game/set-resources.ts | 5 + ledger/game/set-score-amount-from-a-value.ts | 7 + ledger/game/set-score.ts | 5 + ledger/game/set-switch.ts | 5 + .../talking-portrait-type-from-a-value.ts | 8 + ledger/game/talking-portrait.ts | 5 + .../transmission-text-from-the-program.ts | 7 + ledger/game/transmission-type-from-a-value.ts | 8 + ledger/game/transmission.ts | 5 + ledger/game/unmute-unit-speech.ts | 5 + ledger/game/unpause-game.ts | 5 + ledger/game/unpause-timer.ts | 5 + ledger/game/victory.ts | 5 + ledger/game/wait.ts | 5 + ledger/input/chatted-number.ts | 9 + ledger/input/chatted-unit-type.ts | 9 + ledger/input/chatted-word.ts | 9 + ledger/input/chatted.ts | 8 + ledger/input/clicked-middle.ts | 8 + ledger/input/clicked-right.ts | 8 + ledger/input/clicked.ts | 8 + ledger/input/key-held.ts | 10 + ledger/input/key-pressed-current-player.ts | 8 + ledger/input/key-pressed.ts | 8 + ledger/input/local-camera.ts | 5 + ledger/input/local-mouse.ts | 5 + ledger/input/local-player.ts | 5 + ledger/input/local-selected-selected-again.ts | 5 + ledger/input/local-selected-unit.ts | 6 + ledger/input/local-selected.ts | 6 + ledger/input/local-selection-count.ts | 5 + ledger/input/local-shown-only.ts | 5 + ledger/input/mouse-places-a-location.ts | 10 + ledger/input/mouse.ts | 9 + ledger/input/on-chat.ts | 7 + ledger/input/on-key-from.ts | 9 + ledger/input/on-key.ts | 7 + ledger/input/sync-several.ts | 6 + ledger/input/sync.ts | 6 + ledger/input/under-mouse-filter.ts | 8 + ledger/input/under-mouse.ts | 8 + ledger/language/and.ts | 11 + ledger/language/array-destructuring-rest.ts | 8 + ledger/language/array-destructuring.ts | 8 + ledger/language/array-of-instances.ts | 19 ++ ledger/language/array-spread.ts | 10 + ledger/language/arrow-function.ts | 8 + ledger/language/arrow-in-program.ts | 15 + ledger/language/assignment-in-condition.ts | 8 + ledger/language/async-arrow.ts | 11 + ledger/language/async-function.ts | 12 + ledger/language/block-scope.ts | 13 + ledger/language/boolean-equality.ts | 16 + ledger/language/boolean-variable.ts | 11 + ledger/language/break.ts | 10 + ledger/language/callback-argument.ts | 14 + ledger/language/clamp.ts | 11 + ledger/language/class-abstract.ts | 22 ++ ledger/language/class-array-field.ts | 18 ++ ledger/language/class-by-reference.ts | 12 + ledger/language/class-extends-super.ts | 18 ++ ledger/language/class-field-from-field.ts | 11 + ledger/language/class-fields-constructor.ts | 15 + ledger/language/class-generator-method.ts | 19 ++ ledger/language/class-getter-setter.ts | 21 ++ ledger/language/class-implements.ts | 17 ++ ledger/language/class-instance-alias.ts | 15 + ledger/language/class-instance-in-function.ts | 19 ++ ledger/language/class-method.ts | 18 ++ .../language/class-override-super-method.ts | 20 ++ ledger/language/class-parameter-properties.ts | 15 + ledger/language/class-private-field.ts | 22 ++ ledger/language/class-protected.ts | 17 ++ ledger/language/class-readonly.ts | 16 + ledger/language/class-recursive-method.ts | 16 + ledger/language/class-static.ts | 23 ++ ledger/language/closure-made-in-loop.ts | 14 + ledger/language/closure.ts | 13 + ledger/language/comma-operator.ts | 10 + ledger/language/comparisons.ts | 16 + ledger/language/compound-assignment.ts | 13 + ledger/language/const-enum.ts | 11 + ledger/language/const-from-build-time-code.ts | 9 + ledger/language/const-in-program.ts | 8 + ledger/language/const-known-when-built.ts | 9 + ledger/language/continue.ts | 12 + ledger/language/default-parameter.ts | 15 + ledger/language/destructuring-parameter.ts | 15 + ledger/language/do-while-break-continue.ts | 13 + ledger/language/do-while-loop.ts | 9 + ledger/language/double-not.ts | 13 + ledger/language/else-if-chain.ts | 15 + ledger/language/enum.ts | 16 + ledger/language/error-class.ts | 14 + ledger/language/for-break-continue.ts | 13 + ledger/language/for-loop-unrolled.ts | 10 + ledger/language/for-loop.ts | 9 + ledger/language/for-of-array.ts | 13 + ledger/language/for-of-known-list.ts | 10 + ledger/language/function-boolean-parameter.ts | 18 ++ ledger/language/function-boolean-result.ts | 15 + ledger/language/function-called.ts | 13 + ledger/language/function-early-return.ts | 17 ++ ledger/language/function-in-program.ts | 10 + ledger/language/function-inlined.ts | 11 + ledger/language/function-overloads.ts | 17 ++ ledger/language/function-that-sleeps.ts | 12 + ledger/language/function-value-array.ts | 21 ++ ledger/language/function-value.ts | 19 ++ ledger/language/function-void.ts | 15 + ledger/language/game-function.ts | 13 + ledger/language/game-loop.ts | 10 + ledger/language/generator-early-return.ts | 17 ++ ledger/language/generator-for-of.ts | 16 + ledger/language/generator-yield-star.ts | 20 ++ ledger/language/generic-function.ts | 15 + ledger/language/i32-cast.ts | 11 + ledger/language/if-else.ts | 10 + ledger/language/iife.ts | 8 + ledger/language/in-at-build.ts | 10 + ledger/language/increment-as-value.ts | 11 + ledger/language/increment-decrement.ts | 11 + ledger/language/instanceof.ts | 17 ++ ledger/language/labeled-break.ts | 16 + ledger/language/labeled-continue.ts | 16 + ledger/language/let-variable.ts | 7 + ledger/language/math-min-max-abs.ts | 11 + ledger/language/method-returning-boolean.ts | 18 ++ ledger/language/mutual-recursion.ts | 16 + ledger/language/namespace.ts | 11 + ledger/language/negation.ts | 11 + ledger/language/nested-destructuring.ts | 8 + ledger/language/nested-function.ts | 18 ++ ledger/language/nested-loops.ts | 15 + ledger/language/nested-ternary.ts | 11 + ledger/language/non-null-assertion.ts | 11 + ledger/language/not.ts | 11 + ledger/language/nullish-coalescing.ts | 13 + ledger/language/number-as-test.ts | 11 + ledger/language/number-literals.ts | 7 + .../object-destructuring-rename-default.ts | 8 + ledger/language/object-destructuring.ts | 8 + ledger/language/object-rest.ts | 8 + ledger/language/optional-chaining.ts | 16 + ledger/language/optional-parameter.ts | 16 + ledger/language/or.ts | 11 + ledger/language/player-variable.ts | 11 + .../language/players-compared-when-built.ts | 14 + ledger/language/print-to-current-player.ts | 7 + ledger/language/program-for-all-players.ts | 10 + .../language/program-for-several-players.ts | 10 + ledger/language/program-owner.ts | 10 + ledger/language/record-assigned-whole.ts | 11 + ledger/language/record-from-function.ts | 11 + ledger/language/record-index-known.ts | 10 + ledger/language/record-nested.ts | 10 + ledger/language/record-shorthand.ts | 9 + ledger/language/record-spread.ts | 9 + ledger/language/record-type-annotation.ts | 12 + ledger/language/record.ts | 10 + ledger/language/recursion.ts | 11 + ledger/language/rest-parameter.ts | 18 ++ ledger/language/rethrow.ts | 16 + ledger/language/satisfies.ts | 8 + ledger/language/several-declarations.ts | 12 + ledger/language/short-circuit.ts | 17 ++ ledger/language/sleep-cycles.ts | 10 + ledger/language/sleep-in-async-program.ts | 10 + ledger/language/sleep-minutes.ts | 10 + ledger/language/sleep-seconds.ts | 10 + ledger/language/spread-arguments.ts | 15 + ledger/language/strict-equality.ts | 12 + ledger/language/swap-by-destructuring.ts | 13 + ledger/language/switch-case-block.ts | 21 ++ ledger/language/switch-fall-through.ts | 19 ++ ledger/language/switch-number.ts | 15 + ledger/language/switch-return.ts | 19 ++ ledger/language/switch-text.ts | 12 + ledger/language/ternary-boolean.ts | 13 + ledger/language/ternary-number.ts | 11 + ledger/language/throw-from-function.ts | 19 ++ ledger/language/throw-uncaught.ts | 9 + ledger/language/try-catch.ts | 12 + ledger/language/try-finally.ts | 15 + ledger/language/tuple-result.ts | 11 + ledger/language/two-programs.ts | 18 ++ ledger/language/type-alias-union.ts | 12 + ledger/language/type-assertion.ts | 8 + ledger/language/typeof-at-build.ts | 10 + ledger/language/u32-cast.ts | 11 + ledger/language/undefined-check.ts | 13 + ledger/language/var-declaration.ts | 7 + ledger/language/while-loop.ts | 7 + ledger/memory/fog-columns.ts | 12 + ledger/memory/fog-light.ts | 13 + ledger/memory/fog-picture.ts | 9 + ledger/memory/fog-play.ts | 11 + ledger/memory/fog-rect.ts | 13 + ledger/memory/fog-reveal.ts | 7 + ledger/memory/fog-shapes.ts | 16 + ledger/memory/fog-show.ts | 8 + ledger/memory/game-frame-read.ts | 6 + ledger/memory/map-size-read.ts | 7 + ledger/memory/memory-condition.ts | 5 + ledger/memory/player-color-by-name.ts | 5 + ledger/memory/player-color-written.ts | 5 + ledger/memory/player-researched-read.ts | 5 + ledger/memory/player-researched-written.ts | 5 + ledger/memory/player-upgrade-level-read.ts | 6 + ledger/memory/player-upgrade-level-written.ts | 5 + ledger/memory/set-memory.ts | 5 + ledger/memory/tech-cost-written.ts | 8 + ledger/memory/tech-energy-read.ts | 6 + ledger/memory/unit-air-weapon-written.ts | 5 + ledger/memory/unit-armor-added-to.ts | 5 + ledger/memory/unit-build-time-fraction.ts | 5 + ledger/memory/unit-cost-read.ts | 6 + ledger/memory/unit-cost-written.ts | 5 + ledger/memory/unit-flag-read.ts | 5 + ledger/memory/unit-flag-written.ts | 5 + ledger/memory/unit-flags-of-a-kind.ts | 12 + ledger/memory/unit-gas-written.ts | 5 + ledger/memory/unit-ground-weapon-written.ts | 5 + ledger/memory/unit-max-hp-from-a-value.ts | 7 + ledger/memory/unit-max-shields-written.ts | 5 + ledger/memory/unit-name-written.ts | 5 + ledger/memory/unit-sight-written.ts | 5 + ledger/memory/unit-size-read.ts | 6 + ledger/memory/unit-speed-written.ts | 5 + ledger/memory/unit-supply-provided-written.ts | 5 + ledger/memory/unit-supply-used-half.ts | 5 + ledger/memory/upgrade-cost-written.ts | 7 + ledger/memory/upgrade-max-level-read.ts | 6 + ledger/memory/vision-countdown-read.ts | 6 + ledger/memory/vision-countdown-written.ts | 5 + ledger/memory/weapon-damage-added-to.ts | 5 + ledger/memory/weapon-fields-written.ts | 8 + ledger/memory/weapon-range-read.ts | 6 + ledger/numbers/abs-of-a-value.ts | 7 + ledger/numbers/and-of-two-values.ts | 11 + .../numbers/and-with-a-constant-as-a-value.ts | 7 + ledger/numbers/and-with-a-constant-tested.ts | 8 + ledger/numbers/arctangent-of-two-values.ts | 13 + ledger/numbers/arctangent.ts | 9 + ledger/numbers/ceil-of-fixed.ts | 7 + ledger/numbers/clamp.ts | 7 + ledger/numbers/coin-toss.ts | 6 + .../numbers/constant-worked-out-when-built.ts | 8 + ledger/numbers/cosine.ts | 9 + ledger/numbers/difference-of-two-values.ts | 11 + ledger/numbers/fixed-compared.ts | 8 + ledger/numbers/fixed-from-a-whole-number.ts | 9 + ledger/numbers/fixed-product-by-a-constant.ts | 9 + ledger/numbers/fixed-product.ts | 13 + .../numbers/fixed-quotient-by-a-constant.ts | 9 + ledger/numbers/fixed-quotient.ts | 13 + ledger/numbers/fixed-reciprocal.ts | 9 + ledger/numbers/fixed-sum.ts | 12 + ledger/numbers/floor-of-fixed.ts | 7 + ledger/numbers/hypotenuse.ts | 13 + ledger/numbers/i32-of-a-u32.ts | 12 + ledger/numbers/increment-and-decrement.ts | 14 + ledger/numbers/math-angle-to.ts | 14 + ledger/numbers/math-approach.ts | 12 + ledger/numbers/math-clamp01.ts | 9 + ledger/numbers/math-degrees.ts | 10 + ledger/numbers/math-distance-squared.ts | 12 + ledger/numbers/math-distance.ts | 14 + ledger/numbers/math-easing.ts | 17 ++ ledger/numbers/math-inverse-lerp.ts | 16 + ledger/numbers/math-lerp.ts | 16 + ledger/numbers/math-manhattan.ts | 12 + ledger/numbers/math-pi.ts | 12 + ledger/numbers/math-radians.ts | 10 + ledger/numbers/math-remap.ts | 16 + ledger/numbers/math-sign.ts | 8 + ledger/numbers/math-tau.ts | 14 + ledger/numbers/math-vec-length.ts | 9 + ledger/numbers/math-vec.ts | 10 + ledger/numbers/math-wrap.ts | 8 + ledger/numbers/max-of-two-values.ts | 11 + ledger/numbers/min-of-two-values.ts | 11 + ledger/numbers/negative-of-a-value.ts | 8 + .../numbers/number-chosen-by-a-condition.ts | 12 + ledger/numbers/number-wraps-past-the-top.ts | 11 + ledger/numbers/or-of-two-values.ts | 11 + ledger/numbers/or-with-a-constant.ts | 7 + ledger/numbers/product-by-a-constant.ts | 11 + ledger/numbers/product-of-two-values.ts | 11 + ledger/numbers/quotient-by-a-constant.ts | 7 + ledger/numbers/quotient-by-a-power-of-two.ts | 8 + ledger/numbers/quotient-of-two-values.ts | 11 + ledger/numbers/random-chance.ts | 7 + ledger/numbers/random-dice.ts | 8 + ledger/numbers/random-number-below-a-value.ts | 9 + ledger/numbers/random-number.ts | 7 + ledger/numbers/random-pick.ts | 9 + ledger/numbers/random-range.ts | 8 + ledger/numbers/random-seeded-generator.ts | 10 + ledger/numbers/random-shuffle.ts | 9 + ledger/numbers/random-weighted.ts | 8 + ledger/numbers/remainder-by-a-constant.ts | 7 + ledger/numbers/remainder-by-a-power-of-two.ts | 8 + ledger/numbers/remainder-of-two-values.ts | 11 + ledger/numbers/round-of-fixed.ts | 7 + ledger/numbers/shift-left-by-a-constant.ts | 11 + ledger/numbers/shift-left-by-a-value.ts | 7 + ledger/numbers/shift-right-by-a-constant.ts | 7 + ledger/numbers/shift-right-by-a-value.ts | 11 + ledger/numbers/sine.ts | 9 + .../numbers/square-root-of-a-whole-number.ts | 9 + ledger/numbers/square-root-of-fixed.ts | 9 + ledger/numbers/sum-of-two-values.ts | 11 + ledger/numbers/tangent.ts | 9 + ledger/numbers/trunc-of-fixed.ts | 7 + ledger/numbers/trunc-with-or-zero.ts | 7 + ledger/numbers/u16-variable.ts | 11 + ledger/numbers/u32-against-a-signed-number.ts | 13 + ledger/numbers/u32-compared.ts | 8 + ledger/numbers/u32-quotient.ts | 9 + ledger/numbers/u32-with-shift-zero.ts | 8 + ledger/numbers/u8-variable.ts | 11 + .../unsigned-shift-right-by-a-constant.ts | 7 + .../unsigned-shift-right-by-a-value.ts | 11 + ledger/numbers/whole-divided-by-fixed.ts | 13 + ledger/numbers/xor-of-two-values.ts | 11 + ledger/numbers/xor-with-a-constant.ts | 7 + ledger/programs/current-player.ts | 5 + ledger/programs/game-function-two-programs.ts | 9 + ledger/programs/game-function.ts | 8 + ledger/programs/listener-at-the-top.ts | 8 + ledger/programs/owner-all-players-keys.ts | 8 + ledger/programs/owner-all-players-tasks.ts | 9 + ledger/programs/owner-all-players.ts | 7 + ledger/programs/owner-another-player.ts | 5 + ledger/programs/owner-force.ts | 5 + ledger/programs/owner-list.ts | 5 + ledger/programs/owner-one-of-a-list.ts | 5 + .../programs/owner-players-four-to-seven.ts | 5 + ledger/programs/print-to-all-players.ts | 5 + ledger/programs/print-to-another-player.ts | 5 + ledger/programs/print-to-current-player.ts | 5 + ledger/programs/print-to-the-owner-by-name.ts | 5 + ledger/programs/program-name.ts | 5 + ledger/programs/shared-array.ts | 7 + ledger/programs/shared-boolean.ts | 7 + ledger/programs/shared-in-one-program.ts | 7 + ledger/programs/shared-number.ts | 7 + ledger/programs/two-programs.ts | 8 + ledger/sounds/music-arrange.ts | 10 + ledger/sounds/music-bars.ts | 7 + ledger/sounds/music-beat.ts | 8 + ledger/sounds/music-chord.ts | 8 + ledger/sounds/music-chords.ts | 8 + ledger/sounds/music-conduct.ts | 13 + ledger/sounds/music-drums.ts | 8 + ledger/sounds/music-fm-synth.ts | 8 + ledger/sounds/music-impact.ts | 7 + ledger/sounds/music-kit.ts | 8 + ledger/sounds/music-loop.ts | 8 + ledger/sounds/music-midi.ts | 11 + ledger/sounds/music-over.ts | 8 + ledger/sounds/music-play.ts | 9 + ledger/sounds/music-presets.ts | 8 + ledger/sounds/music-pump.ts | 8 + ledger/sounds/music-render.ts | 9 + ledger/sounds/music-repeat.ts | 8 + ledger/sounds/music-riser.ts | 7 + ledger/sounds/music-sampler.ts | 8 + ledger/sounds/music-stretch.ts | 8 + ledger/sounds/music-strings.ts | 8 + ledger/sounds/music-transpose.ts | 8 + ledger/sounds/play-wav-chosen-by-a-value.ts | 9 + ledger/sounds/play-wav-clip-effects.ts | 6 + ledger/sounds/play-wav-fm.ts | 6 + ledger/sounds/play-wav-game-file.ts | 5 + ledger/sounds/play-wav-map-sound.ts | 7 + ledger/sounds/play-wav-mixdown.ts | 8 + ledger/sounds/play-wav-noise.ts | 6 + ledger/sounds/play-wav-pluck.ts | 6 + ledger/sounds/play-wav-together.ts | 9 + ledger/sounds/play-wav-tone.ts | 6 + ledger/sounds/play-wav-with-silence.ts | 7 + ledger/sounds/tone-made-when-built.ts | 8 + ledger/sounds/transmission-with-a-clip.ts | 6 + ledger/tasks/async-arrow-awaited.ts | 10 + ledger/tasks/async-function-awaited.ts | 9 + ledger/tasks/async-function-result.ts | 10 + ledger/tasks/async-program.ts | 7 + ledger/tasks/cutscene-camera-follow.ts | 10 + ledger/tasks/cutscene-camera-pan-ease.ts | 7 + ledger/tasks/cutscene-camera-pan.ts | 7 + ledger/tasks/cutscene-camera-shake.ts | 7 + ledger/tasks/cutscene-camera.ts | 8 + ledger/tasks/cutscene-charge.ts | 9 + ledger/tasks/cutscene-line.ts | 6 + ledger/tasks/cutscene-play.ts | 11 + ledger/tasks/cutscene-second.ts | 7 + ledger/tasks/cutscene-walk-all.ts | 8 + ledger/tasks/cutscene-walk.ts | 10 + ledger/tasks/generator-as-a-task.ts | 16 + ledger/tasks/generator-for-await.ts | 13 + ledger/tasks/generator-for-of.ts | 14 + ledger/tasks/next-frame.ts | 10 + ledger/tasks/promise-all-values.ts | 10 + ledger/tasks/promise-all.ts | 12 + ledger/tasks/promise-any.ts | 7 + ledger/tasks/promise-race.ts | 10 + ledger/tasks/sleep-in-a-loop.ts | 10 + ledger/tasks/sleep-not-awaited.ts | 7 + ledger/tasks/sleep-of-a-value.ts | 8 + ledger/tasks/sleep-seconds.ts | 6 + ledger/tasks/start-arrow.ts | 9 + ledger/tasks/start-call.ts | 14 + ledger/tasks/start-max.ts | 13 + ledger/tasks/start-void.ts | 10 + ledger/tasks/task-awaited.ts | 11 + ledger/tasks/task-cancel.ts | 10 + ledger/tasks/task-error-awaited.ts | 15 + ledger/tasks/task-error-not-awaited.ts | 11 + ledger/tasks/task-outlives-program.ts | 9 + ledger/tasks/task-running.ts | 14 + ledger/tasks/task-starts-a-task.ts | 13 + ledger/tasks/then.ts | 11 + ledger/tasks/timers-after.ts | 8 + ledger/tasks/timers-cooldown-ready.ts | 11 + ledger/tasks/timers-cooldown.ts | 14 + ledger/tasks/timers-countdown-changed.ts | 10 + ledger/tasks/timers-countdown-until.ts | 8 + ledger/tasks/timers-countdown.ts | 11 + ledger/tasks/timers-every-cancelled.ts | 10 + ledger/tasks/timers-every-times.ts | 8 + ledger/tasks/timers-every.ts | 8 + ledger/tasks/timers-now.ts | 6 + ledger/tasks/timers-over.ts | 14 + ledger/tasks/timers-second.ts | 7 + ledger/tasks/timers-spread.ts | 10 + ledger/tasks/timers-stopwatch.ts | 9 + ledger/tasks/until-condition.ts | 6 + ledger/tasks/until-function.ts | 10 + ledger/texts/boolean-as-a-text.ts | 8 + ledger/texts/character-code.ts | 7 + ledger/texts/jsx-bar.tsx | 11 + ledger/texts/jsx-in-a-program.tsx | 6 + ledger/texts/jsx-known-when-built.tsx | 11 + ledger/texts/jsx-with-a-number.tsx | 7 + ledger/texts/number-as-a-text.ts | 10 + ledger/texts/parse-int.ts | 8 + ledger/texts/player-colour-set.ts | 7 + ledger/texts/print-a-colour-from-a-value.tsx | 13 + ledger/texts/print-a-fixed-number.ts | 7 + ledger/texts/print-a-number-below-zero.ts | 7 + ledger/texts/print-a-player-colour.ts | 5 + ledger/texts/print-a-player-name.ts | 5 + ledger/texts/print-a-u32.ts | 8 + ledger/texts/print-a-whole-number.ts | 7 + ledger/texts/print-at-the-centre.ts | 7 + ledger/texts/print-known-text.ts | 5 + ledger/texts/print-several-numbers.ts | 11 + .../print-the-largest-and-smallest-numbers.ts | 11 + ledger/texts/print-to-all-players.ts | 7 + ledger/texts/print-to-another-player.ts | 7 + ledger/texts/template-as-a-value.ts | 8 + ledger/texts/text-array-join.ts | 9 + ledger/texts/text-array-that-grows.ts | 11 + ledger/texts/text-array.ts | 9 + ledger/texts/text-character-at.ts | 8 + ledger/texts/text-chosen-by-a-condition.ts | 7 + ledger/texts/text-for-of.ts | 9 + ledger/texts/text-function.ts | 10 + ledger/texts/text-in-a-printed-template.ts | 7 + ledger/texts/text-in-a-record.ts | 12 + ledger/texts/text-includes.ts | 8 + ledger/texts/text-index-of.ts | 8 + ledger/texts/text-is-empty.ts | 8 + ledger/texts/text-length.ts | 7 + ledger/texts/text-list-read-at-a-value.ts | 8 + ledger/texts/text-pad.ts | 9 + ledger/texts/text-repeat.ts | 7 + ledger/texts/text-replace.ts | 8 + ledger/texts/text-slice.ts | 9 + ledger/texts/text-split.ts | 8 + ledger/texts/text-starts-and-ends-with.ts | 8 + ledger/texts/text-switch.ts | 16 + ledger/texts/text-trim.ts | 9 + ledger/texts/text-upper-and-lower-case.ts | 8 + ledger/texts/text-variable.ts | 9 + ledger/texts/texts-compared.ts | 8 + ledger/texts/ui-clock.ts | 8 + ledger/texts/ui-dialog.tsx | 9 + ledger/texts/ui-lines.tsx | 10 + ledger/texts/ui-log.tsx | 13 + ledger/texts/ui-menu.tsx | 11 + ledger/texts/ui-meter.tsx | 9 + ledger/texts/ui-panel.tsx | 12 + ledger/texts/ui-thousands.ts | 8 + ledger/texts/ui-typewriter.ts | 8 + ledger/triggers/comment.ts | 6 + ledger/triggers/disable-debug-mode.ts | 5 + ledger/triggers/disabled-action.ts | 6 + ledger/triggers/disabled-condition.ts | 6 + ledger/triggers/enable-debug-mode.ts | 5 + ledger/triggers/hyper-triggers.ts | 7 + ledger/triggers/not-in-a-trigger.ts | 7 + ledger/triggers/preserve-trigger.ts | 6 + ledger/triggers/preserve.ts | 6 + ledger/triggers/trigger-disabled-option.ts | 6 + ledger/triggers/trigger-execution-flags.ts | 7 + .../triggers/trigger-for-several-players.ts | 6 + ledger/triggers/trigger-nested-lists.ts | 8 + ledger/triggers/trigger-preserved-option.ts | 6 + ledger/triggers/trigger.ts | 6 + ledger/units/all-units-for-of.ts | 7 + ledger/units/on-created-at-the-top.ts | 5 + ledger/units/on-created-filter.ts | 7 + ledger/units/on-created-type.ts | 7 + ledger/units/on-created.ts | 7 + ledger/units/on-died-filter.ts | 7 + ledger/units/on-died.ts | 9 + ledger/units/on-event-async.ts | 8 + ledger/units/on-event-stopped.ts | 9 + ledger/units/on-killed-by.ts | 7 + ledger/units/on-killed.ts | 7 + ledger/units/on-morphed-type.ts | 7 + ledger/units/on-morphed.ts | 7 + ledger/units/on-order-of-one.ts | 7 + ledger/units/on-order.ts | 7 + ledger/units/pick-first-filter.ts | 5 + ledger/units/pick-first.ts | 5 + ledger/units/pick-nearest-filter.ts | 5 + ledger/units/pick-nearest.ts | 5 + ledger/units/pick-random-unit-filter.ts | 5 + ledger/units/pick-random-unit.ts | 5 + ledger/units/select-one.ts | 5 + ledger/units/select-several.ts | 5 + ledger/units/unit-array.ts | 6 + ledger/units/unit-assigned.ts | 7 + ledger/units/unit-burrowed-read.ts | 5 + ledger/units/unit-cloaked-read.ts | 5 + ledger/units/unit-cooldown-read.ts | 5 + ledger/units/unit-cooldown-written.ts | 5 + ledger/units/unit-damage-by-a-value.ts | 7 + ledger/units/unit-damage-percent.ts | 5 + ledger/units/unit-damage.ts | 5 + ledger/units/unit-energy-read.ts | 5 + ledger/units/unit-energy-written.ts | 5 + ledger/units/unit-ensnare-read.ts | 5 + ledger/units/unit-ensnare-written.ts | 5 + ledger/units/unit-give.ts | 5 + ledger/units/unit-hallucinated-read.ts | 5 + ledger/units/unit-heal-percent.ts | 5 + ledger/units/unit-heal.ts | 5 + ledger/units/unit-hp-added-to.ts | 5 + ledger/units/unit-hp-from-a-value.ts | 7 + ledger/units/unit-hp-read.ts | 5 + ledger/units/unit-hp-written.ts | 5 + ledger/units/unit-in-a-text.ts | 6 + ledger/units/unit-invincible-read.ts | 5 + ledger/units/unit-invincible-written.ts | 5 + ledger/units/unit-irradiate-read.ts | 5 + ledger/units/unit-irradiate-written.ts | 5 + ledger/units/unit-kept-across-a-sleep.ts | 8 + ledger/units/unit-kept-in-a-variable.ts | 6 + ledger/units/unit-kill.ts | 5 + ledger/units/unit-kills-read.ts | 5 + ledger/units/unit-kills-written.ts | 5 + ledger/units/unit-locate.ts | 6 + ledger/units/unit-lockdown-read.ts | 5 + ledger/units/unit-lockdown-written.ts | 5 + ledger/units/unit-maelstrom-read.ts | 5 + ledger/units/unit-maelstrom-written.ts | 5 + ledger/units/unit-max-hp-read.ts | 5 + ledger/units/unit-max-shields-read.ts | 5 + ledger/units/unit-none.ts | 5 + ledger/units/unit-null.ts | 8 + ledger/units/unit-order-attack.ts | 6 + ledger/units/unit-order-id-read.ts | 5 + ledger/units/unit-order-move.ts | 6 + ledger/units/unit-order-patrol.ts | 6 + ledger/units/unit-owner-read.ts | 5 + ledger/units/unit-parameter.ts | 9 + ledger/units/unit-plague-read.ts | 5 + ledger/units/unit-plague-written.ts | 5 + ledger/units/unit-remove.ts | 5 + ledger/units/unit-resources-read.ts | 5 + ledger/units/unit-resources-written.ts | 5 + ledger/units/unit-result.ts | 8 + ledger/units/unit-same.ts | 5 + ledger/units/unit-set-every.ts | 5 + ledger/units/unit-set-filter.ts | 6 + ledger/units/unit-set-find.ts | 6 + ledger/units/unit-set-for-each.ts | 5 + ledger/units/unit-set-map.ts | 8 + ledger/units/unit-set-reduce.ts | 6 + ledger/units/unit-set-some.ts | 5 + ledger/units/unit-shields-read.ts | 5 + ledger/units/unit-shields-written.ts | 5 + ledger/units/unit-stasis-read.ts | 5 + ledger/units/unit-stasis-written.ts | 5 + ledger/units/unit-stim-read.ts | 5 + ledger/units/unit-stim-written.ts | 5 + ledger/units/unit-there.ts | 5 + ledger/units/unit-type-read.ts | 5 + ledger/units/unit-under-attack-read.ts | 5 + ledger/units/unit-verb-if-there.ts | 6 + ledger/units/unit-x-read.ts | 5 + ledger/units/unit-y-read.ts | 5 + ledger/units/units-at-for-of.ts | 7 + ledger/units/units-filter-all-three.ts | 8 + ledger/units/units-filter-at.ts | 8 + ledger/units/units-filter-buildings.ts | 7 + ledger/units/units-filter-factories.ts | 7 + ledger/units/units-filter-men.ts | 7 + ledger/units/units-filter-owner.ts | 7 + ledger/units/units-filter-type.ts | 7 + ledger/units/units-loop-break.ts | 12 + ledger/units/units-loop-continue.ts | 10 + ledger/units/units-loop-writes.ts | 5 + ledger/units/units-of-current-player.ts | 7 + ledger/units/units-of-for-of.ts | 7 + ledger/units/units-of-neutral.ts | 7 + scripts/ledger.mts | 66 +++++ tests/ledger.test.ts | 53 ++++ tests/ledger.ts | 273 ++++++++++++++++++ 834 files changed, 7639 insertions(+) create mode 100644 ledger/arrays/array-compound-store.ts create mode 100644 ledger/arrays/array-destructured.ts create mode 100644 ledger/arrays/array-fill.ts create mode 100644 ledger/arrays/array-for-of.ts create mode 100644 ledger/arrays/array-from.ts create mode 100644 ledger/arrays/array-given-to-a-function.ts create mode 100644 ledger/arrays/array-in-a-record.ts create mode 100644 ledger/arrays/array-includes.ts create mode 100644 ledger/arrays/array-index-of.ts create mode 100644 ledger/arrays/array-length-of-a-fixed-array.ts create mode 100644 ledger/arrays/array-new-filled.ts create mode 100644 ledger/arrays/array-of-booleans.ts create mode 100644 ledger/arrays/array-of-instances.ts create mode 100644 ledger/arrays/array-of-records-that-grows.ts create mode 100644 ledger/arrays/array-of-records.ts create mode 100644 ledger/arrays/array-read-at-a-value.ts create mode 100644 ledger/arrays/array-read-past-the-end.ts create mode 100644 ledger/arrays/array-spread-into-a-fixed-array.ts create mode 100644 ledger/arrays/array-store-at-a-value.ts create mode 100644 ledger/arrays/array-store-past-the-end.ts create mode 100644 ledger/arrays/array-swap.ts create mode 100644 ledger/arrays/at.ts create mode 100644 ledger/arrays/concat.ts create mode 100644 ledger/arrays/every.ts create mode 100644 ledger/arrays/filter.ts create mode 100644 ledger/arrays/find-index.ts create mode 100644 ledger/arrays/find-last.ts create mode 100644 ledger/arrays/find.ts create mode 100644 ledger/arrays/for-each.ts create mode 100644 ledger/arrays/grid-made-when-built.ts create mode 100644 ledger/arrays/grid-read-and-store.ts create mode 100644 ledger/arrays/grid-row.ts create mode 100644 ledger/arrays/join.ts create mode 100644 ledger/arrays/last-index-of.ts create mode 100644 ledger/arrays/length-of-a-growing-array.ts create mode 100644 ledger/arrays/length-set.ts create mode 100644 ledger/arrays/list-of-records-read-at-a-value.ts create mode 100644 ledger/arrays/list-read-at-a-constant.ts create mode 100644 ledger/arrays/list-read-at-a-value.ts create mode 100644 ledger/arrays/map-for-of.ts create mode 100644 ledger/arrays/map-keyed-by-game-ids.ts create mode 100644 ledger/arrays/map-over-any-number.ts create mode 100644 ledger/arrays/map.ts create mode 100644 ledger/arrays/pop.ts create mode 100644 ledger/arrays/push.ts create mode 100644 ledger/arrays/read-at-a-shifted-index.ts create mode 100644 ledger/arrays/record-keyed-by-game-ids.ts create mode 100644 ledger/arrays/reduce.ts create mode 100644 ledger/arrays/rest-of-an-array.ts create mode 100644 ledger/arrays/reverse.ts create mode 100644 ledger/arrays/rows-that-grow.ts create mode 100644 ledger/arrays/set-keyed-by-game-ids.ts create mode 100644 ledger/arrays/set.ts create mode 100644 ledger/arrays/shift.ts create mode 100644 ledger/arrays/signal.ts create mode 100644 ledger/arrays/slice.ts create mode 100644 ledger/arrays/some.ts create mode 100644 ledger/arrays/sort.ts create mode 100644 ledger/arrays/splice.ts create mode 100644 ledger/arrays/spread-into-a-call.ts create mode 100644 ledger/arrays/spread-of-a-growing-array.ts create mode 100644 ledger/arrays/store-at-the-end.ts create mode 100644 ledger/arrays/to-reversed.ts create mode 100644 ledger/arrays/to-sorted.ts create mode 100644 ledger/arrays/u16-array.ts create mode 100644 ledger/arrays/u32-array.ts create mode 100644 ledger/arrays/u8-array.ts create mode 100644 ledger/arrays/unshift.ts create mode 100644 ledger/game/action-raw-record.ts create mode 100644 ledger/game/center-location-with-size.ts create mode 100644 ledger/game/center-location.ts create mode 100644 ledger/game/center-view.ts create mode 100644 ledger/game/condition-accumulate.ts create mode 100644 ledger/game/condition-always.ts create mode 100644 ledger/game/condition-bring.ts create mode 100644 ledger/game/condition-command-the-least-at.ts create mode 100644 ledger/game/condition-command-the-least.ts create mode 100644 ledger/game/condition-command-the-most-at.ts create mode 100644 ledger/game/condition-command-the-most.ts create mode 100644 ledger/game/condition-command.ts create mode 100644 ledger/game/condition-countdown-timer.ts create mode 100644 ledger/game/condition-deaths.ts create mode 100644 ledger/game/condition-elapsed-time.ts create mode 100644 ledger/game/condition-highest-score.ts create mode 100644 ledger/game/condition-in-a-while.ts create mode 100644 ledger/game/condition-kill.ts create mode 100644 ledger/game/condition-least-kills.ts create mode 100644 ledger/game/condition-least-resources.ts create mode 100644 ledger/game/condition-lowest-score.ts create mode 100644 ledger/game/condition-most-kills.ts create mode 100644 ledger/game/condition-most-resources.ts create mode 100644 ledger/game/condition-negated.ts create mode 100644 ledger/game/condition-never.ts create mode 100644 ledger/game/condition-opponents.ts create mode 100644 ledger/game/condition-raw-record.ts create mode 100644 ledger/game/condition-score.ts create mode 100644 ledger/game/condition-switch.ts create mode 100644 ledger/game/cover.ts create mode 100644 ledger/game/create-unit-count-from-a-value.ts create mode 100644 ledger/game/create-unit-type-from-a-value.ts create mode 100644 ledger/game/create-unit-with-properties-count-from-a-value.ts create mode 100644 ledger/game/create-unit-with-properties-type-from-a-value.ts create mode 100644 ledger/game/create-unit-with-properties.ts create mode 100644 ledger/game/create-unit.ts create mode 100644 ledger/game/defeat.ts create mode 100644 ledger/game/display-text-with-a-number.ts create mode 100644 ledger/game/display-text.ts create mode 100644 ledger/game/draw.ts create mode 100644 ledger/game/give-units-count-from-a-value.ts create mode 100644 ledger/game/give-units-type-from-a-value.ts create mode 100644 ledger/game/give-units.ts create mode 100644 ledger/game/has-left.ts create mode 100644 ledger/game/is-human.ts create mode 100644 ledger/game/kill-unit-at-count-from-a-value.ts create mode 100644 ledger/game/kill-unit-at-type-from-a-value.ts create mode 100644 ledger/game/kill-unit-at.ts create mode 100644 ledger/game/kill-unit-type-from-a-value.ts create mode 100644 ledger/game/kill-unit.ts create mode 100644 ledger/game/leaderboard-computer-players.ts create mode 100644 ledger/game/leaderboard-control-at-type-from-a-value.ts create mode 100644 ledger/game/leaderboard-control-at.ts create mode 100644 ledger/game/leaderboard-control-type-from-a-value.ts create mode 100644 ledger/game/leaderboard-control.ts create mode 100644 ledger/game/leaderboard-goal-control-at-type-from-a-value.ts create mode 100644 ledger/game/leaderboard-goal-control-at.ts create mode 100644 ledger/game/leaderboard-goal-control-type-from-a-value.ts create mode 100644 ledger/game/leaderboard-goal-control.ts create mode 100644 ledger/game/leaderboard-goal-kills-type-from-a-value.ts create mode 100644 ledger/game/leaderboard-goal-kills.ts create mode 100644 ledger/game/leaderboard-goal-points.ts create mode 100644 ledger/game/leaderboard-goal-resources.ts create mode 100644 ledger/game/leaderboard-greed.ts create mode 100644 ledger/game/leaderboard-kills-type-from-a-value.ts create mode 100644 ledger/game/leaderboard-kills.ts create mode 100644 ledger/game/leaderboard-label-from-the-program.ts create mode 100644 ledger/game/leaderboard-points.ts create mode 100644 ledger/game/leaderboard-resources.ts create mode 100644 ledger/game/minimap-ping.ts create mode 100644 ledger/game/modify-energy-type-from-a-value.ts create mode 100644 ledger/game/modify-energy.ts create mode 100644 ledger/game/modify-hangar-count-type-from-a-value.ts create mode 100644 ledger/game/modify-hangar-count.ts create mode 100644 ledger/game/modify-hit-points-type-from-a-value.ts create mode 100644 ledger/game/modify-hit-points.ts create mode 100644 ledger/game/modify-resource-amount.ts create mode 100644 ledger/game/modify-shields-type-from-a-value.ts create mode 100644 ledger/game/modify-shields.ts create mode 100644 ledger/game/move-location-type-from-a-value.ts create mode 100644 ledger/game/move-location.ts create mode 100644 ledger/game/move-unit-type-from-a-value.ts create mode 100644 ledger/game/move-unit.ts create mode 100644 ledger/game/mute-unit-speech.ts create mode 100644 ledger/game/not-of-a-condition.ts create mode 100644 ledger/game/once.ts create mode 100644 ledger/game/order.ts create mode 100644 ledger/game/pause-game.ts create mode 100644 ledger/game/pause-timer.ts create mode 100644 ledger/game/read-accumulate.ts create mode 100644 ledger/game/read-bring.ts create mode 100644 ledger/game/read-command.ts create mode 100644 ledger/game/read-count-units-at.ts create mode 100644 ledger/game/read-count-units.ts create mode 100644 ledger/game/read-countdown-timer.ts create mode 100644 ledger/game/read-countdown.ts create mode 100644 ledger/game/read-deaths.ts create mode 100644 ledger/game/read-elapsed-time.ts create mode 100644 ledger/game/read-elapsed.ts create mode 100644 ledger/game/read-gas.ts create mode 100644 ledger/game/read-in-arithmetic.ts create mode 100644 ledger/game/read-kill.ts create mode 100644 ledger/game/read-kills.ts create mode 100644 ledger/game/read-minerals.ts create mode 100644 ledger/game/read-opponents.ts create mode 100644 ledger/game/read-race.ts create mode 100644 ledger/game/read-resources.ts create mode 100644 ledger/game/read-score.ts create mode 100644 ledger/game/read-slot.ts create mode 100644 ledger/game/read-supply-max.ts create mode 100644 ledger/game/read-supply-provided-of-a-race.ts create mode 100644 ledger/game/read-supply-used.ts create mode 100644 ledger/game/remove-unit-at-count-from-a-value.ts create mode 100644 ledger/game/remove-unit-at-type-from-a-value.ts create mode 100644 ledger/game/remove-unit-at.ts create mode 100644 ledger/game/remove-unit-type-from-a-value.ts create mode 100644 ledger/game/remove-unit.ts create mode 100644 ledger/game/reserve-location.ts create mode 100644 ledger/game/rose.ts create mode 100644 ledger/game/run-ai-script-at.ts create mode 100644 ledger/game/run-ai-script.ts create mode 100644 ledger/game/screen-max-pixels.ts create mode 100644 ledger/game/screen-owners.ts create mode 100644 ledger/game/screen-sprite.ts create mode 100644 ledger/game/screen-text-colors.tsx create mode 100644 ledger/game/screen-view.tsx create mode 100644 ledger/game/screen.ts create mode 100644 ledger/game/set-alliance-status.ts create mode 100644 ledger/game/set-countdown-timer-from-a-value.ts create mode 100644 ledger/game/set-countdown-timer.ts create mode 100644 ledger/game/set-deaths-amount-from-a-value.ts create mode 100644 ledger/game/set-deaths-unit-from-a-value.ts create mode 100644 ledger/game/set-deaths.ts create mode 100644 ledger/game/set-doodad-state-type-from-a-value.ts create mode 100644 ledger/game/set-doodad-state.ts create mode 100644 ledger/game/set-invincibility-type-from-a-value.ts create mode 100644 ledger/game/set-invincibility.ts create mode 100644 ledger/game/set-mission-objectives-text-from-the-program.ts create mode 100644 ledger/game/set-mission-objectives.ts create mode 100644 ledger/game/set-next-scenario.ts create mode 100644 ledger/game/set-resources-amount-from-a-read.ts create mode 100644 ledger/game/set-resources-amount-from-a-value.ts create mode 100644 ledger/game/set-resources.ts create mode 100644 ledger/game/set-score-amount-from-a-value.ts create mode 100644 ledger/game/set-score.ts create mode 100644 ledger/game/set-switch.ts create mode 100644 ledger/game/talking-portrait-type-from-a-value.ts create mode 100644 ledger/game/talking-portrait.ts create mode 100644 ledger/game/transmission-text-from-the-program.ts create mode 100644 ledger/game/transmission-type-from-a-value.ts create mode 100644 ledger/game/transmission.ts create mode 100644 ledger/game/unmute-unit-speech.ts create mode 100644 ledger/game/unpause-game.ts create mode 100644 ledger/game/unpause-timer.ts create mode 100644 ledger/game/victory.ts create mode 100644 ledger/game/wait.ts create mode 100644 ledger/input/chatted-number.ts create mode 100644 ledger/input/chatted-unit-type.ts create mode 100644 ledger/input/chatted-word.ts create mode 100644 ledger/input/chatted.ts create mode 100644 ledger/input/clicked-middle.ts create mode 100644 ledger/input/clicked-right.ts create mode 100644 ledger/input/clicked.ts create mode 100644 ledger/input/key-held.ts create mode 100644 ledger/input/key-pressed-current-player.ts create mode 100644 ledger/input/key-pressed.ts create mode 100644 ledger/input/local-camera.ts create mode 100644 ledger/input/local-mouse.ts create mode 100644 ledger/input/local-player.ts create mode 100644 ledger/input/local-selected-selected-again.ts create mode 100644 ledger/input/local-selected-unit.ts create mode 100644 ledger/input/local-selected.ts create mode 100644 ledger/input/local-selection-count.ts create mode 100644 ledger/input/local-shown-only.ts create mode 100644 ledger/input/mouse-places-a-location.ts create mode 100644 ledger/input/mouse.ts create mode 100644 ledger/input/on-chat.ts create mode 100644 ledger/input/on-key-from.ts create mode 100644 ledger/input/on-key.ts create mode 100644 ledger/input/sync-several.ts create mode 100644 ledger/input/sync.ts create mode 100644 ledger/input/under-mouse-filter.ts create mode 100644 ledger/input/under-mouse.ts create mode 100644 ledger/language/and.ts create mode 100644 ledger/language/array-destructuring-rest.ts create mode 100644 ledger/language/array-destructuring.ts create mode 100644 ledger/language/array-of-instances.ts create mode 100644 ledger/language/array-spread.ts create mode 100644 ledger/language/arrow-function.ts create mode 100644 ledger/language/arrow-in-program.ts create mode 100644 ledger/language/assignment-in-condition.ts create mode 100644 ledger/language/async-arrow.ts create mode 100644 ledger/language/async-function.ts create mode 100644 ledger/language/block-scope.ts create mode 100644 ledger/language/boolean-equality.ts create mode 100644 ledger/language/boolean-variable.ts create mode 100644 ledger/language/break.ts create mode 100644 ledger/language/callback-argument.ts create mode 100644 ledger/language/clamp.ts create mode 100644 ledger/language/class-abstract.ts create mode 100644 ledger/language/class-array-field.ts create mode 100644 ledger/language/class-by-reference.ts create mode 100644 ledger/language/class-extends-super.ts create mode 100644 ledger/language/class-field-from-field.ts create mode 100644 ledger/language/class-fields-constructor.ts create mode 100644 ledger/language/class-generator-method.ts create mode 100644 ledger/language/class-getter-setter.ts create mode 100644 ledger/language/class-implements.ts create mode 100644 ledger/language/class-instance-alias.ts create mode 100644 ledger/language/class-instance-in-function.ts create mode 100644 ledger/language/class-method.ts create mode 100644 ledger/language/class-override-super-method.ts create mode 100644 ledger/language/class-parameter-properties.ts create mode 100644 ledger/language/class-private-field.ts create mode 100644 ledger/language/class-protected.ts create mode 100644 ledger/language/class-readonly.ts create mode 100644 ledger/language/class-recursive-method.ts create mode 100644 ledger/language/class-static.ts create mode 100644 ledger/language/closure-made-in-loop.ts create mode 100644 ledger/language/closure.ts create mode 100644 ledger/language/comma-operator.ts create mode 100644 ledger/language/comparisons.ts create mode 100644 ledger/language/compound-assignment.ts create mode 100644 ledger/language/const-enum.ts create mode 100644 ledger/language/const-from-build-time-code.ts create mode 100644 ledger/language/const-in-program.ts create mode 100644 ledger/language/const-known-when-built.ts create mode 100644 ledger/language/continue.ts create mode 100644 ledger/language/default-parameter.ts create mode 100644 ledger/language/destructuring-parameter.ts create mode 100644 ledger/language/do-while-break-continue.ts create mode 100644 ledger/language/do-while-loop.ts create mode 100644 ledger/language/double-not.ts create mode 100644 ledger/language/else-if-chain.ts create mode 100644 ledger/language/enum.ts create mode 100644 ledger/language/error-class.ts create mode 100644 ledger/language/for-break-continue.ts create mode 100644 ledger/language/for-loop-unrolled.ts create mode 100644 ledger/language/for-loop.ts create mode 100644 ledger/language/for-of-array.ts create mode 100644 ledger/language/for-of-known-list.ts create mode 100644 ledger/language/function-boolean-parameter.ts create mode 100644 ledger/language/function-boolean-result.ts create mode 100644 ledger/language/function-called.ts create mode 100644 ledger/language/function-early-return.ts create mode 100644 ledger/language/function-in-program.ts create mode 100644 ledger/language/function-inlined.ts create mode 100644 ledger/language/function-overloads.ts create mode 100644 ledger/language/function-that-sleeps.ts create mode 100644 ledger/language/function-value-array.ts create mode 100644 ledger/language/function-value.ts create mode 100644 ledger/language/function-void.ts create mode 100644 ledger/language/game-function.ts create mode 100644 ledger/language/game-loop.ts create mode 100644 ledger/language/generator-early-return.ts create mode 100644 ledger/language/generator-for-of.ts create mode 100644 ledger/language/generator-yield-star.ts create mode 100644 ledger/language/generic-function.ts create mode 100644 ledger/language/i32-cast.ts create mode 100644 ledger/language/if-else.ts create mode 100644 ledger/language/iife.ts create mode 100644 ledger/language/in-at-build.ts create mode 100644 ledger/language/increment-as-value.ts create mode 100644 ledger/language/increment-decrement.ts create mode 100644 ledger/language/instanceof.ts create mode 100644 ledger/language/labeled-break.ts create mode 100644 ledger/language/labeled-continue.ts create mode 100644 ledger/language/let-variable.ts create mode 100644 ledger/language/math-min-max-abs.ts create mode 100644 ledger/language/method-returning-boolean.ts create mode 100644 ledger/language/mutual-recursion.ts create mode 100644 ledger/language/namespace.ts create mode 100644 ledger/language/negation.ts create mode 100644 ledger/language/nested-destructuring.ts create mode 100644 ledger/language/nested-function.ts create mode 100644 ledger/language/nested-loops.ts create mode 100644 ledger/language/nested-ternary.ts create mode 100644 ledger/language/non-null-assertion.ts create mode 100644 ledger/language/not.ts create mode 100644 ledger/language/nullish-coalescing.ts create mode 100644 ledger/language/number-as-test.ts create mode 100644 ledger/language/number-literals.ts create mode 100644 ledger/language/object-destructuring-rename-default.ts create mode 100644 ledger/language/object-destructuring.ts create mode 100644 ledger/language/object-rest.ts create mode 100644 ledger/language/optional-chaining.ts create mode 100644 ledger/language/optional-parameter.ts create mode 100644 ledger/language/or.ts create mode 100644 ledger/language/player-variable.ts create mode 100644 ledger/language/players-compared-when-built.ts create mode 100644 ledger/language/print-to-current-player.ts create mode 100644 ledger/language/program-for-all-players.ts create mode 100644 ledger/language/program-for-several-players.ts create mode 100644 ledger/language/program-owner.ts create mode 100644 ledger/language/record-assigned-whole.ts create mode 100644 ledger/language/record-from-function.ts create mode 100644 ledger/language/record-index-known.ts create mode 100644 ledger/language/record-nested.ts create mode 100644 ledger/language/record-shorthand.ts create mode 100644 ledger/language/record-spread.ts create mode 100644 ledger/language/record-type-annotation.ts create mode 100644 ledger/language/record.ts create mode 100644 ledger/language/recursion.ts create mode 100644 ledger/language/rest-parameter.ts create mode 100644 ledger/language/rethrow.ts create mode 100644 ledger/language/satisfies.ts create mode 100644 ledger/language/several-declarations.ts create mode 100644 ledger/language/short-circuit.ts create mode 100644 ledger/language/sleep-cycles.ts create mode 100644 ledger/language/sleep-in-async-program.ts create mode 100644 ledger/language/sleep-minutes.ts create mode 100644 ledger/language/sleep-seconds.ts create mode 100644 ledger/language/spread-arguments.ts create mode 100644 ledger/language/strict-equality.ts create mode 100644 ledger/language/swap-by-destructuring.ts create mode 100644 ledger/language/switch-case-block.ts create mode 100644 ledger/language/switch-fall-through.ts create mode 100644 ledger/language/switch-number.ts create mode 100644 ledger/language/switch-return.ts create mode 100644 ledger/language/switch-text.ts create mode 100644 ledger/language/ternary-boolean.ts create mode 100644 ledger/language/ternary-number.ts create mode 100644 ledger/language/throw-from-function.ts create mode 100644 ledger/language/throw-uncaught.ts create mode 100644 ledger/language/try-catch.ts create mode 100644 ledger/language/try-finally.ts create mode 100644 ledger/language/tuple-result.ts create mode 100644 ledger/language/two-programs.ts create mode 100644 ledger/language/type-alias-union.ts create mode 100644 ledger/language/type-assertion.ts create mode 100644 ledger/language/typeof-at-build.ts create mode 100644 ledger/language/u32-cast.ts create mode 100644 ledger/language/undefined-check.ts create mode 100644 ledger/language/var-declaration.ts create mode 100644 ledger/language/while-loop.ts create mode 100644 ledger/memory/fog-columns.ts create mode 100644 ledger/memory/fog-light.ts create mode 100644 ledger/memory/fog-picture.ts create mode 100644 ledger/memory/fog-play.ts create mode 100644 ledger/memory/fog-rect.ts create mode 100644 ledger/memory/fog-reveal.ts create mode 100644 ledger/memory/fog-shapes.ts create mode 100644 ledger/memory/fog-show.ts create mode 100644 ledger/memory/game-frame-read.ts create mode 100644 ledger/memory/map-size-read.ts create mode 100644 ledger/memory/memory-condition.ts create mode 100644 ledger/memory/player-color-by-name.ts create mode 100644 ledger/memory/player-color-written.ts create mode 100644 ledger/memory/player-researched-read.ts create mode 100644 ledger/memory/player-researched-written.ts create mode 100644 ledger/memory/player-upgrade-level-read.ts create mode 100644 ledger/memory/player-upgrade-level-written.ts create mode 100644 ledger/memory/set-memory.ts create mode 100644 ledger/memory/tech-cost-written.ts create mode 100644 ledger/memory/tech-energy-read.ts create mode 100644 ledger/memory/unit-air-weapon-written.ts create mode 100644 ledger/memory/unit-armor-added-to.ts create mode 100644 ledger/memory/unit-build-time-fraction.ts create mode 100644 ledger/memory/unit-cost-read.ts create mode 100644 ledger/memory/unit-cost-written.ts create mode 100644 ledger/memory/unit-flag-read.ts create mode 100644 ledger/memory/unit-flag-written.ts create mode 100644 ledger/memory/unit-flags-of-a-kind.ts create mode 100644 ledger/memory/unit-gas-written.ts create mode 100644 ledger/memory/unit-ground-weapon-written.ts create mode 100644 ledger/memory/unit-max-hp-from-a-value.ts create mode 100644 ledger/memory/unit-max-shields-written.ts create mode 100644 ledger/memory/unit-name-written.ts create mode 100644 ledger/memory/unit-sight-written.ts create mode 100644 ledger/memory/unit-size-read.ts create mode 100644 ledger/memory/unit-speed-written.ts create mode 100644 ledger/memory/unit-supply-provided-written.ts create mode 100644 ledger/memory/unit-supply-used-half.ts create mode 100644 ledger/memory/upgrade-cost-written.ts create mode 100644 ledger/memory/upgrade-max-level-read.ts create mode 100644 ledger/memory/vision-countdown-read.ts create mode 100644 ledger/memory/vision-countdown-written.ts create mode 100644 ledger/memory/weapon-damage-added-to.ts create mode 100644 ledger/memory/weapon-fields-written.ts create mode 100644 ledger/memory/weapon-range-read.ts create mode 100644 ledger/numbers/abs-of-a-value.ts create mode 100644 ledger/numbers/and-of-two-values.ts create mode 100644 ledger/numbers/and-with-a-constant-as-a-value.ts create mode 100644 ledger/numbers/and-with-a-constant-tested.ts create mode 100644 ledger/numbers/arctangent-of-two-values.ts create mode 100644 ledger/numbers/arctangent.ts create mode 100644 ledger/numbers/ceil-of-fixed.ts create mode 100644 ledger/numbers/clamp.ts create mode 100644 ledger/numbers/coin-toss.ts create mode 100644 ledger/numbers/constant-worked-out-when-built.ts create mode 100644 ledger/numbers/cosine.ts create mode 100644 ledger/numbers/difference-of-two-values.ts create mode 100644 ledger/numbers/fixed-compared.ts create mode 100644 ledger/numbers/fixed-from-a-whole-number.ts create mode 100644 ledger/numbers/fixed-product-by-a-constant.ts create mode 100644 ledger/numbers/fixed-product.ts create mode 100644 ledger/numbers/fixed-quotient-by-a-constant.ts create mode 100644 ledger/numbers/fixed-quotient.ts create mode 100644 ledger/numbers/fixed-reciprocal.ts create mode 100644 ledger/numbers/fixed-sum.ts create mode 100644 ledger/numbers/floor-of-fixed.ts create mode 100644 ledger/numbers/hypotenuse.ts create mode 100644 ledger/numbers/i32-of-a-u32.ts create mode 100644 ledger/numbers/increment-and-decrement.ts create mode 100644 ledger/numbers/math-angle-to.ts create mode 100644 ledger/numbers/math-approach.ts create mode 100644 ledger/numbers/math-clamp01.ts create mode 100644 ledger/numbers/math-degrees.ts create mode 100644 ledger/numbers/math-distance-squared.ts create mode 100644 ledger/numbers/math-distance.ts create mode 100644 ledger/numbers/math-easing.ts create mode 100644 ledger/numbers/math-inverse-lerp.ts create mode 100644 ledger/numbers/math-lerp.ts create mode 100644 ledger/numbers/math-manhattan.ts create mode 100644 ledger/numbers/math-pi.ts create mode 100644 ledger/numbers/math-radians.ts create mode 100644 ledger/numbers/math-remap.ts create mode 100644 ledger/numbers/math-sign.ts create mode 100644 ledger/numbers/math-tau.ts create mode 100644 ledger/numbers/math-vec-length.ts create mode 100644 ledger/numbers/math-vec.ts create mode 100644 ledger/numbers/math-wrap.ts create mode 100644 ledger/numbers/max-of-two-values.ts create mode 100644 ledger/numbers/min-of-two-values.ts create mode 100644 ledger/numbers/negative-of-a-value.ts create mode 100644 ledger/numbers/number-chosen-by-a-condition.ts create mode 100644 ledger/numbers/number-wraps-past-the-top.ts create mode 100644 ledger/numbers/or-of-two-values.ts create mode 100644 ledger/numbers/or-with-a-constant.ts create mode 100644 ledger/numbers/product-by-a-constant.ts create mode 100644 ledger/numbers/product-of-two-values.ts create mode 100644 ledger/numbers/quotient-by-a-constant.ts create mode 100644 ledger/numbers/quotient-by-a-power-of-two.ts create mode 100644 ledger/numbers/quotient-of-two-values.ts create mode 100644 ledger/numbers/random-chance.ts create mode 100644 ledger/numbers/random-dice.ts create mode 100644 ledger/numbers/random-number-below-a-value.ts create mode 100644 ledger/numbers/random-number.ts create mode 100644 ledger/numbers/random-pick.ts create mode 100644 ledger/numbers/random-range.ts create mode 100644 ledger/numbers/random-seeded-generator.ts create mode 100644 ledger/numbers/random-shuffle.ts create mode 100644 ledger/numbers/random-weighted.ts create mode 100644 ledger/numbers/remainder-by-a-constant.ts create mode 100644 ledger/numbers/remainder-by-a-power-of-two.ts create mode 100644 ledger/numbers/remainder-of-two-values.ts create mode 100644 ledger/numbers/round-of-fixed.ts create mode 100644 ledger/numbers/shift-left-by-a-constant.ts create mode 100644 ledger/numbers/shift-left-by-a-value.ts create mode 100644 ledger/numbers/shift-right-by-a-constant.ts create mode 100644 ledger/numbers/shift-right-by-a-value.ts create mode 100644 ledger/numbers/sine.ts create mode 100644 ledger/numbers/square-root-of-a-whole-number.ts create mode 100644 ledger/numbers/square-root-of-fixed.ts create mode 100644 ledger/numbers/sum-of-two-values.ts create mode 100644 ledger/numbers/tangent.ts create mode 100644 ledger/numbers/trunc-of-fixed.ts create mode 100644 ledger/numbers/trunc-with-or-zero.ts create mode 100644 ledger/numbers/u16-variable.ts create mode 100644 ledger/numbers/u32-against-a-signed-number.ts create mode 100644 ledger/numbers/u32-compared.ts create mode 100644 ledger/numbers/u32-quotient.ts create mode 100644 ledger/numbers/u32-with-shift-zero.ts create mode 100644 ledger/numbers/u8-variable.ts create mode 100644 ledger/numbers/unsigned-shift-right-by-a-constant.ts create mode 100644 ledger/numbers/unsigned-shift-right-by-a-value.ts create mode 100644 ledger/numbers/whole-divided-by-fixed.ts create mode 100644 ledger/numbers/xor-of-two-values.ts create mode 100644 ledger/numbers/xor-with-a-constant.ts create mode 100644 ledger/programs/current-player.ts create mode 100644 ledger/programs/game-function-two-programs.ts create mode 100644 ledger/programs/game-function.ts create mode 100644 ledger/programs/listener-at-the-top.ts create mode 100644 ledger/programs/owner-all-players-keys.ts create mode 100644 ledger/programs/owner-all-players-tasks.ts create mode 100644 ledger/programs/owner-all-players.ts create mode 100644 ledger/programs/owner-another-player.ts create mode 100644 ledger/programs/owner-force.ts create mode 100644 ledger/programs/owner-list.ts create mode 100644 ledger/programs/owner-one-of-a-list.ts create mode 100644 ledger/programs/owner-players-four-to-seven.ts create mode 100644 ledger/programs/print-to-all-players.ts create mode 100644 ledger/programs/print-to-another-player.ts create mode 100644 ledger/programs/print-to-current-player.ts create mode 100644 ledger/programs/print-to-the-owner-by-name.ts create mode 100644 ledger/programs/program-name.ts create mode 100644 ledger/programs/shared-array.ts create mode 100644 ledger/programs/shared-boolean.ts create mode 100644 ledger/programs/shared-in-one-program.ts create mode 100644 ledger/programs/shared-number.ts create mode 100644 ledger/programs/two-programs.ts create mode 100644 ledger/sounds/music-arrange.ts create mode 100644 ledger/sounds/music-bars.ts create mode 100644 ledger/sounds/music-beat.ts create mode 100644 ledger/sounds/music-chord.ts create mode 100644 ledger/sounds/music-chords.ts create mode 100644 ledger/sounds/music-conduct.ts create mode 100644 ledger/sounds/music-drums.ts create mode 100644 ledger/sounds/music-fm-synth.ts create mode 100644 ledger/sounds/music-impact.ts create mode 100644 ledger/sounds/music-kit.ts create mode 100644 ledger/sounds/music-loop.ts create mode 100644 ledger/sounds/music-midi.ts create mode 100644 ledger/sounds/music-over.ts create mode 100644 ledger/sounds/music-play.ts create mode 100644 ledger/sounds/music-presets.ts create mode 100644 ledger/sounds/music-pump.ts create mode 100644 ledger/sounds/music-render.ts create mode 100644 ledger/sounds/music-repeat.ts create mode 100644 ledger/sounds/music-riser.ts create mode 100644 ledger/sounds/music-sampler.ts create mode 100644 ledger/sounds/music-stretch.ts create mode 100644 ledger/sounds/music-strings.ts create mode 100644 ledger/sounds/music-transpose.ts create mode 100644 ledger/sounds/play-wav-chosen-by-a-value.ts create mode 100644 ledger/sounds/play-wav-clip-effects.ts create mode 100644 ledger/sounds/play-wav-fm.ts create mode 100644 ledger/sounds/play-wav-game-file.ts create mode 100644 ledger/sounds/play-wav-map-sound.ts create mode 100644 ledger/sounds/play-wav-mixdown.ts create mode 100644 ledger/sounds/play-wav-noise.ts create mode 100644 ledger/sounds/play-wav-pluck.ts create mode 100644 ledger/sounds/play-wav-together.ts create mode 100644 ledger/sounds/play-wav-tone.ts create mode 100644 ledger/sounds/play-wav-with-silence.ts create mode 100644 ledger/sounds/tone-made-when-built.ts create mode 100644 ledger/sounds/transmission-with-a-clip.ts create mode 100644 ledger/tasks/async-arrow-awaited.ts create mode 100644 ledger/tasks/async-function-awaited.ts create mode 100644 ledger/tasks/async-function-result.ts create mode 100644 ledger/tasks/async-program.ts create mode 100644 ledger/tasks/cutscene-camera-follow.ts create mode 100644 ledger/tasks/cutscene-camera-pan-ease.ts create mode 100644 ledger/tasks/cutscene-camera-pan.ts create mode 100644 ledger/tasks/cutscene-camera-shake.ts create mode 100644 ledger/tasks/cutscene-camera.ts create mode 100644 ledger/tasks/cutscene-charge.ts create mode 100644 ledger/tasks/cutscene-line.ts create mode 100644 ledger/tasks/cutscene-play.ts create mode 100644 ledger/tasks/cutscene-second.ts create mode 100644 ledger/tasks/cutscene-walk-all.ts create mode 100644 ledger/tasks/cutscene-walk.ts create mode 100644 ledger/tasks/generator-as-a-task.ts create mode 100644 ledger/tasks/generator-for-await.ts create mode 100644 ledger/tasks/generator-for-of.ts create mode 100644 ledger/tasks/next-frame.ts create mode 100644 ledger/tasks/promise-all-values.ts create mode 100644 ledger/tasks/promise-all.ts create mode 100644 ledger/tasks/promise-any.ts create mode 100644 ledger/tasks/promise-race.ts create mode 100644 ledger/tasks/sleep-in-a-loop.ts create mode 100644 ledger/tasks/sleep-not-awaited.ts create mode 100644 ledger/tasks/sleep-of-a-value.ts create mode 100644 ledger/tasks/sleep-seconds.ts create mode 100644 ledger/tasks/start-arrow.ts create mode 100644 ledger/tasks/start-call.ts create mode 100644 ledger/tasks/start-max.ts create mode 100644 ledger/tasks/start-void.ts create mode 100644 ledger/tasks/task-awaited.ts create mode 100644 ledger/tasks/task-cancel.ts create mode 100644 ledger/tasks/task-error-awaited.ts create mode 100644 ledger/tasks/task-error-not-awaited.ts create mode 100644 ledger/tasks/task-outlives-program.ts create mode 100644 ledger/tasks/task-running.ts create mode 100644 ledger/tasks/task-starts-a-task.ts create mode 100644 ledger/tasks/then.ts create mode 100644 ledger/tasks/timers-after.ts create mode 100644 ledger/tasks/timers-cooldown-ready.ts create mode 100644 ledger/tasks/timers-cooldown.ts create mode 100644 ledger/tasks/timers-countdown-changed.ts create mode 100644 ledger/tasks/timers-countdown-until.ts create mode 100644 ledger/tasks/timers-countdown.ts create mode 100644 ledger/tasks/timers-every-cancelled.ts create mode 100644 ledger/tasks/timers-every-times.ts create mode 100644 ledger/tasks/timers-every.ts create mode 100644 ledger/tasks/timers-now.ts create mode 100644 ledger/tasks/timers-over.ts create mode 100644 ledger/tasks/timers-second.ts create mode 100644 ledger/tasks/timers-spread.ts create mode 100644 ledger/tasks/timers-stopwatch.ts create mode 100644 ledger/tasks/until-condition.ts create mode 100644 ledger/tasks/until-function.ts create mode 100644 ledger/texts/boolean-as-a-text.ts create mode 100644 ledger/texts/character-code.ts create mode 100644 ledger/texts/jsx-bar.tsx create mode 100644 ledger/texts/jsx-in-a-program.tsx create mode 100644 ledger/texts/jsx-known-when-built.tsx create mode 100644 ledger/texts/jsx-with-a-number.tsx create mode 100644 ledger/texts/number-as-a-text.ts create mode 100644 ledger/texts/parse-int.ts create mode 100644 ledger/texts/player-colour-set.ts create mode 100644 ledger/texts/print-a-colour-from-a-value.tsx create mode 100644 ledger/texts/print-a-fixed-number.ts create mode 100644 ledger/texts/print-a-number-below-zero.ts create mode 100644 ledger/texts/print-a-player-colour.ts create mode 100644 ledger/texts/print-a-player-name.ts create mode 100644 ledger/texts/print-a-u32.ts create mode 100644 ledger/texts/print-a-whole-number.ts create mode 100644 ledger/texts/print-at-the-centre.ts create mode 100644 ledger/texts/print-known-text.ts create mode 100644 ledger/texts/print-several-numbers.ts create mode 100644 ledger/texts/print-the-largest-and-smallest-numbers.ts create mode 100644 ledger/texts/print-to-all-players.ts create mode 100644 ledger/texts/print-to-another-player.ts create mode 100644 ledger/texts/template-as-a-value.ts create mode 100644 ledger/texts/text-array-join.ts create mode 100644 ledger/texts/text-array-that-grows.ts create mode 100644 ledger/texts/text-array.ts create mode 100644 ledger/texts/text-character-at.ts create mode 100644 ledger/texts/text-chosen-by-a-condition.ts create mode 100644 ledger/texts/text-for-of.ts create mode 100644 ledger/texts/text-function.ts create mode 100644 ledger/texts/text-in-a-printed-template.ts create mode 100644 ledger/texts/text-in-a-record.ts create mode 100644 ledger/texts/text-includes.ts create mode 100644 ledger/texts/text-index-of.ts create mode 100644 ledger/texts/text-is-empty.ts create mode 100644 ledger/texts/text-length.ts create mode 100644 ledger/texts/text-list-read-at-a-value.ts create mode 100644 ledger/texts/text-pad.ts create mode 100644 ledger/texts/text-repeat.ts create mode 100644 ledger/texts/text-replace.ts create mode 100644 ledger/texts/text-slice.ts create mode 100644 ledger/texts/text-split.ts create mode 100644 ledger/texts/text-starts-and-ends-with.ts create mode 100644 ledger/texts/text-switch.ts create mode 100644 ledger/texts/text-trim.ts create mode 100644 ledger/texts/text-upper-and-lower-case.ts create mode 100644 ledger/texts/text-variable.ts create mode 100644 ledger/texts/texts-compared.ts create mode 100644 ledger/texts/ui-clock.ts create mode 100644 ledger/texts/ui-dialog.tsx create mode 100644 ledger/texts/ui-lines.tsx create mode 100644 ledger/texts/ui-log.tsx create mode 100644 ledger/texts/ui-menu.tsx create mode 100644 ledger/texts/ui-meter.tsx create mode 100644 ledger/texts/ui-panel.tsx create mode 100644 ledger/texts/ui-thousands.ts create mode 100644 ledger/texts/ui-typewriter.ts create mode 100644 ledger/triggers/comment.ts create mode 100644 ledger/triggers/disable-debug-mode.ts create mode 100644 ledger/triggers/disabled-action.ts create mode 100644 ledger/triggers/disabled-condition.ts create mode 100644 ledger/triggers/enable-debug-mode.ts create mode 100644 ledger/triggers/hyper-triggers.ts create mode 100644 ledger/triggers/not-in-a-trigger.ts create mode 100644 ledger/triggers/preserve-trigger.ts create mode 100644 ledger/triggers/preserve.ts create mode 100644 ledger/triggers/trigger-disabled-option.ts create mode 100644 ledger/triggers/trigger-execution-flags.ts create mode 100644 ledger/triggers/trigger-for-several-players.ts create mode 100644 ledger/triggers/trigger-nested-lists.ts create mode 100644 ledger/triggers/trigger-preserved-option.ts create mode 100644 ledger/triggers/trigger.ts create mode 100644 ledger/units/all-units-for-of.ts create mode 100644 ledger/units/on-created-at-the-top.ts create mode 100644 ledger/units/on-created-filter.ts create mode 100644 ledger/units/on-created-type.ts create mode 100644 ledger/units/on-created.ts create mode 100644 ledger/units/on-died-filter.ts create mode 100644 ledger/units/on-died.ts create mode 100644 ledger/units/on-event-async.ts create mode 100644 ledger/units/on-event-stopped.ts create mode 100644 ledger/units/on-killed-by.ts create mode 100644 ledger/units/on-killed.ts create mode 100644 ledger/units/on-morphed-type.ts create mode 100644 ledger/units/on-morphed.ts create mode 100644 ledger/units/on-order-of-one.ts create mode 100644 ledger/units/on-order.ts create mode 100644 ledger/units/pick-first-filter.ts create mode 100644 ledger/units/pick-first.ts create mode 100644 ledger/units/pick-nearest-filter.ts create mode 100644 ledger/units/pick-nearest.ts create mode 100644 ledger/units/pick-random-unit-filter.ts create mode 100644 ledger/units/pick-random-unit.ts create mode 100644 ledger/units/select-one.ts create mode 100644 ledger/units/select-several.ts create mode 100644 ledger/units/unit-array.ts create mode 100644 ledger/units/unit-assigned.ts create mode 100644 ledger/units/unit-burrowed-read.ts create mode 100644 ledger/units/unit-cloaked-read.ts create mode 100644 ledger/units/unit-cooldown-read.ts create mode 100644 ledger/units/unit-cooldown-written.ts create mode 100644 ledger/units/unit-damage-by-a-value.ts create mode 100644 ledger/units/unit-damage-percent.ts create mode 100644 ledger/units/unit-damage.ts create mode 100644 ledger/units/unit-energy-read.ts create mode 100644 ledger/units/unit-energy-written.ts create mode 100644 ledger/units/unit-ensnare-read.ts create mode 100644 ledger/units/unit-ensnare-written.ts create mode 100644 ledger/units/unit-give.ts create mode 100644 ledger/units/unit-hallucinated-read.ts create mode 100644 ledger/units/unit-heal-percent.ts create mode 100644 ledger/units/unit-heal.ts create mode 100644 ledger/units/unit-hp-added-to.ts create mode 100644 ledger/units/unit-hp-from-a-value.ts create mode 100644 ledger/units/unit-hp-read.ts create mode 100644 ledger/units/unit-hp-written.ts create mode 100644 ledger/units/unit-in-a-text.ts create mode 100644 ledger/units/unit-invincible-read.ts create mode 100644 ledger/units/unit-invincible-written.ts create mode 100644 ledger/units/unit-irradiate-read.ts create mode 100644 ledger/units/unit-irradiate-written.ts create mode 100644 ledger/units/unit-kept-across-a-sleep.ts create mode 100644 ledger/units/unit-kept-in-a-variable.ts create mode 100644 ledger/units/unit-kill.ts create mode 100644 ledger/units/unit-kills-read.ts create mode 100644 ledger/units/unit-kills-written.ts create mode 100644 ledger/units/unit-locate.ts create mode 100644 ledger/units/unit-lockdown-read.ts create mode 100644 ledger/units/unit-lockdown-written.ts create mode 100644 ledger/units/unit-maelstrom-read.ts create mode 100644 ledger/units/unit-maelstrom-written.ts create mode 100644 ledger/units/unit-max-hp-read.ts create mode 100644 ledger/units/unit-max-shields-read.ts create mode 100644 ledger/units/unit-none.ts create mode 100644 ledger/units/unit-null.ts create mode 100644 ledger/units/unit-order-attack.ts create mode 100644 ledger/units/unit-order-id-read.ts create mode 100644 ledger/units/unit-order-move.ts create mode 100644 ledger/units/unit-order-patrol.ts create mode 100644 ledger/units/unit-owner-read.ts create mode 100644 ledger/units/unit-parameter.ts create mode 100644 ledger/units/unit-plague-read.ts create mode 100644 ledger/units/unit-plague-written.ts create mode 100644 ledger/units/unit-remove.ts create mode 100644 ledger/units/unit-resources-read.ts create mode 100644 ledger/units/unit-resources-written.ts create mode 100644 ledger/units/unit-result.ts create mode 100644 ledger/units/unit-same.ts create mode 100644 ledger/units/unit-set-every.ts create mode 100644 ledger/units/unit-set-filter.ts create mode 100644 ledger/units/unit-set-find.ts create mode 100644 ledger/units/unit-set-for-each.ts create mode 100644 ledger/units/unit-set-map.ts create mode 100644 ledger/units/unit-set-reduce.ts create mode 100644 ledger/units/unit-set-some.ts create mode 100644 ledger/units/unit-shields-read.ts create mode 100644 ledger/units/unit-shields-written.ts create mode 100644 ledger/units/unit-stasis-read.ts create mode 100644 ledger/units/unit-stasis-written.ts create mode 100644 ledger/units/unit-stim-read.ts create mode 100644 ledger/units/unit-stim-written.ts create mode 100644 ledger/units/unit-there.ts create mode 100644 ledger/units/unit-type-read.ts create mode 100644 ledger/units/unit-under-attack-read.ts create mode 100644 ledger/units/unit-verb-if-there.ts create mode 100644 ledger/units/unit-x-read.ts create mode 100644 ledger/units/unit-y-read.ts create mode 100644 ledger/units/units-at-for-of.ts create mode 100644 ledger/units/units-filter-all-three.ts create mode 100644 ledger/units/units-filter-at.ts create mode 100644 ledger/units/units-filter-buildings.ts create mode 100644 ledger/units/units-filter-factories.ts create mode 100644 ledger/units/units-filter-men.ts create mode 100644 ledger/units/units-filter-owner.ts create mode 100644 ledger/units/units-filter-type.ts create mode 100644 ledger/units/units-loop-break.ts create mode 100644 ledger/units/units-loop-continue.ts create mode 100644 ledger/units/units-loop-writes.ts create mode 100644 ledger/units/units-of-current-player.ts create mode 100644 ledger/units/units-of-for-of.ts create mode 100644 ledger/units/units-of-neutral.ts create mode 100644 scripts/ledger.mts create mode 100644 tests/ledger.test.ts create mode 100644 tests/ledger.ts diff --git a/ledger/arrays/array-compound-store.ts b/ledger/arrays/array-compound-store.ts new file mode 100644 index 0000000..cdf504d --- /dev/null +++ b/ledger/arrays/array-compound-store.ts @@ -0,0 +1,12 @@ +// ledger: step 3, takes +// A cell of an array changed where it is: `hp[i] += 5`, `hp[i]++`. +program(() => { + const hp = [10, 20, 30]; + let i = 0; + while (i < 3) { + hp[i] += 5; + hp[i]++; + i += 1; + } + print(`${hp[1]}`); +}); diff --git a/ledger/arrays/array-destructured.ts b/ledger/arrays/array-destructured.ts new file mode 100644 index 0000000..f8a698a --- /dev/null +++ b/ledger/arrays/array-destructured.ts @@ -0,0 +1,12 @@ +// ledger: step 3, takes +// The items of an array taken out into variables: `const [a, b] = xs`. +program(() => { + const xs = [3, 4, 5]; + let i = 0; + while (i < 3) { + xs[i] += i; + i += 1; + } + const [a, b] = xs; + print(`${a + b}`); +}); diff --git a/ledger/arrays/array-fill.ts b/ledger/arrays/array-fill.ts new file mode 100644 index 0000000..5bf0f82 --- /dev/null +++ b/ledger/arrays/array-fill.ts @@ -0,0 +1,9 @@ +// ledger: step 3, takes +// Every cell of an array of the program set to a value that changes while the game runs: `board.fill(v)`. +program(() => { + const board = [1, 2, 3, 4]; + let v = 0; + while (v < 5) v += 2; + board.fill(v); + print(`${board[3]}`); +}); diff --git a/ledger/arrays/array-for-of.ts b/ledger/arrays/array-for-of.ts new file mode 100644 index 0000000..5a50f18 --- /dev/null +++ b/ledger/arrays/array-for-of.ts @@ -0,0 +1,9 @@ +// ledger: step 3, takes +// A loop over the cells of an array of the program, within one frame: `for (const x of xs)`. +program(() => { + const xs = [4, 8, 15, 16]; + xs[0] = 5; + let total = 0; + for (const x of xs) total += x; + print(`${total}`); +}); diff --git a/ledger/arrays/array-from.ts b/ledger/arrays/array-from.ts new file mode 100644 index 0000000..36b6742 --- /dev/null +++ b/ledger/arrays/array-from.ts @@ -0,0 +1,10 @@ +// ledger: step 3, takes +// An array made from a length written in the script and a function of each place: +// `Array.from({ length: 4 }, (_, i) => i * 2)`. +program(() => { + const xs = Array.from({ length: 4 }, (_, i) => i * 2); + let i = 0; + while (i < 3) i += 1; + xs[0] = i; + print(`${xs[i] + xs[0]}`); +}); diff --git a/ledger/arrays/array-given-to-a-function.ts b/ledger/arrays/array-given-to-a-function.ts new file mode 100644 index 0000000..637042d --- /dev/null +++ b/ledger/arrays/array-given-to-a-function.ts @@ -0,0 +1,14 @@ +// ledger: step 3, takes +// An array handed to a function of the script, which reads and stores into the array itself. +function bump(xs: number[], i: number) { + xs[i] += 10; +} +program(() => { + const xs = [1, 2, 3]; + let i = 0; + while (i < 2) { + bump(xs, i); + i += 1; + } + print(`${xs[0] + xs[1]}`); +}); diff --git a/ledger/arrays/array-in-a-record.ts b/ledger/arrays/array-in-a-record.ts new file mode 100644 index 0000000..2c36ca7 --- /dev/null +++ b/ledger/arrays/array-in-a-record.ts @@ -0,0 +1,9 @@ +// ledger: step 3, takes +// An array of a fixed length that is a field of a record: `shop.stock[i]`. +program(() => { + const shop = { gold: 10, stock: [3, 3, 3] }; + let i = 0; + while (i < 2) i += 1; + shop.stock[i] -= 1; + print(`${shop.stock[i] + shop.gold}`); +}); diff --git a/ledger/arrays/array-includes.ts b/ledger/arrays/array-includes.ts new file mode 100644 index 0000000..059a00f --- /dev/null +++ b/ledger/arrays/array-includes.ts @@ -0,0 +1,10 @@ +// ledger: step 3, takes +// Whether an array of the program has a value, a loop the game runs: `xs.includes(v)`. +program(() => { + const xs = [4, 8, 15, 16]; + xs[0] = 5; + let v = 0; + while (v < 15) v += 5; + if (xs.includes(v)) print("found"); + else print("not found"); +}); diff --git a/ledger/arrays/array-index-of.ts b/ledger/arrays/array-index-of.ts new file mode 100644 index 0000000..7eb3064 --- /dev/null +++ b/ledger/arrays/array-index-of.ts @@ -0,0 +1,9 @@ +// ledger: step 3, takes +// Where a value is in an array of the program, −1 when it is not: `xs.indexOf(v)`. +program(() => { + const xs = [4, 8, 15, 16]; + xs[0] = 5; + let v = 0; + while (v < 15) v += 5; + print(`${xs.indexOf(v)}`); +}); diff --git a/ledger/arrays/array-length-of-a-fixed-array.ts b/ledger/arrays/array-length-of-a-fixed-array.ts new file mode 100644 index 0000000..79e0002 --- /dev/null +++ b/ledger/arrays/array-length-of-a-fixed-array.ts @@ -0,0 +1,9 @@ +// ledger: step 0, takes +// The length of an array of a fixed length, known when the map is built: `xs.length`. +program(() => { + const xs = [4, 8, 15, 16]; + xs[0] = 5; + let i = 0; + while (i < xs.length) i += 3; + print(`${i}`); +}); diff --git a/ledger/arrays/array-new-filled.ts b/ledger/arrays/array-new-filled.ts new file mode 100644 index 0000000..042d5d4 --- /dev/null +++ b/ledger/arrays/array-new-filled.ts @@ -0,0 +1,9 @@ +// ledger: step 3, takes +// An array of a length written in the script, every cell one value: `new Array(8).fill(3)`. +program(() => { + const board = new Array(8).fill(3); + let i = 0; + while (i < 5) i += 2; + board[i] = 7; + print(`${board[i] + board[0]}`); +}); diff --git a/ledger/arrays/array-of-booleans.ts b/ledger/arrays/array-of-booleans.ts new file mode 100644 index 0000000..437f9f9 --- /dev/null +++ b/ledger/arrays/array-of-booleans.ts @@ -0,0 +1,14 @@ +// ledger: step 3, takes +// An array of booleans, stored and tested at a place that changes while the game runs: `seen[i] = true`. +program(() => { + const seen = [false, false, false, false]; + let i = 0; + while (i < 4) { + seen[i] = i > 1; + i += 1; + } + let j = 0; + while (j < 3) j += 1; + if (seen[j]) print("seen"); + else print("not seen"); +}); diff --git a/ledger/arrays/array-of-instances.ts b/ledger/arrays/array-of-instances.ts new file mode 100644 index 0000000..8e1d09c --- /dev/null +++ b/ledger/arrays/array-of-instances.ts @@ -0,0 +1,15 @@ +// ledger: step 3, takes +// An array of instances of a class of the script, a method called on one: `towers[i].upgrade()`. +class Tower { + level = 1; + upgrade() { + this.level += 1; + } +} +program(() => { + const towers = [new Tower(), new Tower()]; + let i = 0; + while (i < 1) i += 1; + towers[i].upgrade(); + print(`${towers[0].level} ${towers[1].level}`); +}); diff --git a/ledger/arrays/array-of-records-that-grows.ts b/ledger/arrays/array-of-records-that-grows.ts new file mode 100644 index 0000000..0522aa8 --- /dev/null +++ b/ledger/arrays/array-of-records-that-grows.ts @@ -0,0 +1,11 @@ +// ledger: step 9, refused: an array that grows +// An array of records that grows a record at a time: `enemies.push({ hp, x })`. +program(() => { + const enemies: { hp: number; x: number }[] = []; + let x = 0; + while (x < 10) { + enemies.push({ hp: 20, x }); + x += 4; + } + print(`${enemies.length} ${enemies[1].x}`); +}); diff --git a/ledger/arrays/array-of-records.ts b/ledger/arrays/array-of-records.ts new file mode 100644 index 0000000..64e4188 --- /dev/null +++ b/ledger/arrays/array-of-records.ts @@ -0,0 +1,13 @@ +// ledger: step 3, takes +// An array of records, each with its fields, read and written at a place that changes while the game runs: +// `waves[i].count`. +program(() => { + const waves = [ + { count: 5, gold: 10 }, + { count: 8, gold: 15 }, + ]; + let i = 0; + while (i < 1) i += 1; + waves[i].count += 2; + print(`${waves[i].count + waves[i].gold}`); +}); diff --git a/ledger/arrays/array-read-at-a-value.ts b/ledger/arrays/array-read-at-a-value.ts new file mode 100644 index 0000000..347219c --- /dev/null +++ b/ledger/arrays/array-read-at-a-value.ts @@ -0,0 +1,9 @@ +// ledger: step 3, takes +// A cell of an array of the program read at a value that changes while the game runs: `hp[i]`. +program(() => { + const hp = [10, 20, 30, 40]; + hp[0] = 15; + let i = 0; + while (i < 2) i += 1; + print(`${hp[i]}`); +}); diff --git a/ledger/arrays/array-read-past-the-end.ts b/ledger/arrays/array-read-past-the-end.ts new file mode 100644 index 0000000..5387f17 --- /dev/null +++ b/ledger/arrays/array-read-past-the-end.ts @@ -0,0 +1,9 @@ +// ledger: step 3, takes +// A cell read at a place past an array's end, which reads 0 (and is said when the script is tested). +program(() => { + const hp = [10, 20, 30]; + hp[0] = 5; + let i = 0; + while (i < 5) i += 2; + print(`${hp[i]}`); +}); diff --git a/ledger/arrays/array-spread-into-a-fixed-array.ts b/ledger/arrays/array-spread-into-a-fixed-array.ts new file mode 100644 index 0000000..9c743de --- /dev/null +++ b/ledger/arrays/array-spread-into-a-fixed-array.ts @@ -0,0 +1,10 @@ +// ledger: step 3, takes +// An array of a fixed length made from another and more: `[...xs, 9]`. +program(() => { + const xs = [3, 4, 5]; + let i = 0; + while (i < 2) i += 1; + xs[i] = 8; + const ys = [...xs, 9]; + print(`${ys[2] + ys[3]}`); +}); diff --git a/ledger/arrays/array-store-at-a-value.ts b/ledger/arrays/array-store-at-a-value.ts new file mode 100644 index 0000000..97e1611 --- /dev/null +++ b/ledger/arrays/array-store-at-a-value.ts @@ -0,0 +1,11 @@ +// ledger: step 3, takes +// A value stored into a cell of an array of the program at a place that changes while the game runs: `hp[i] = v`. +program(() => { + const hp = [0, 0, 0, 0]; + let i = 0; + while (i < 4) { + hp[i] = i * 10 + 5; + i += 1; + } + print(`${hp[2]}`); +}); diff --git a/ledger/arrays/array-store-past-the-end.ts b/ledger/arrays/array-store-past-the-end.ts new file mode 100644 index 0000000..ac99af0 --- /dev/null +++ b/ledger/arrays/array-store-past-the-end.ts @@ -0,0 +1,9 @@ +// ledger: step 3, takes +// A store at a place past an array's end, which stores nothing (and is said when the script is tested). +program(() => { + const hp = [10, 20, 30]; + let i = 0; + while (i < 5) i += 2; + hp[i] = 99; + print(`${hp[0] + hp[1] + hp[2]}`); +}); diff --git a/ledger/arrays/array-swap.ts b/ledger/arrays/array-swap.ts new file mode 100644 index 0000000..d694697 --- /dev/null +++ b/ledger/arrays/array-swap.ts @@ -0,0 +1,10 @@ +// ledger: step 3, takes +// Two cells swapped by a pattern: `[xs[i], xs[j]] = [xs[j], xs[i]]`. +program(() => { + const xs = [3, 4, 5]; + let i = 0; + let j = 0; + while (j < 2) j += 1; + [xs[i], xs[j]] = [xs[j], xs[i]]; + print(`${xs[0]}`); +}); diff --git a/ledger/arrays/at.ts b/ledger/arrays/at.ts new file mode 100644 index 0000000..46007f3 --- /dev/null +++ b/ledger/arrays/at.ts @@ -0,0 +1,9 @@ +// ledger: step 3, takes +// A value at a place counted from the end when below zero: `xs.at(-1)`. +program(() => { + const xs = [1, 2, 3, 4]; + let v = 0; + while (v < 10) v += 3; + xs[3] = v; + print(`${xs.at(-1) ?? 0}`); +}); diff --git a/ledger/arrays/concat.ts b/ledger/arrays/concat.ts new file mode 100644 index 0000000..e296ca0 --- /dev/null +++ b/ledger/arrays/concat.ts @@ -0,0 +1,11 @@ +// ledger: step 9, refused: an array that grows +// A new array of two arrays one after the other: `xs.concat(ys)`. +program(() => { + const xs = [5, 2]; + const ys = [8, 1]; + let v = 0; + while (v < 10) v += 3; + ys[0] = v; + const zs = xs.concat(ys); + print(`${zs[2]}`); +}); diff --git a/ledger/arrays/every.ts b/ledger/arrays/every.ts new file mode 100644 index 0000000..aefa47c --- /dev/null +++ b/ledger/arrays/every.ts @@ -0,0 +1,10 @@ +// ledger: step 3, takes +// Whether a function says yes to every value of an array: `xs.every((x) => x > 1)`. +program(() => { + const xs = [1, 2, 3, 4]; + let v = 0; + while (v < 10) v += 3; + xs[0] = v; + if (xs.every((x) => x > 1)) print("all are"); + else print("not all are"); +}); diff --git a/ledger/arrays/filter.ts b/ledger/arrays/filter.ts new file mode 100644 index 0000000..76476c0 --- /dev/null +++ b/ledger/arrays/filter.ts @@ -0,0 +1,10 @@ +// ledger: step 9, refused: an array that grows +// A new array of the values of another a function says yes to: `xs.filter((x) => x > 2)`. +program(() => { + const xs = [1, 2, 3, 4]; + let v = 0; + while (v < 10) v += 3; + xs[0] = v; + const ys = xs.filter((x) => x > 2); + print(`${ys[0]}`); +}); diff --git a/ledger/arrays/find-index.ts b/ledger/arrays/find-index.ts new file mode 100644 index 0000000..9dade02 --- /dev/null +++ b/ledger/arrays/find-index.ts @@ -0,0 +1,9 @@ +// ledger: step 3, takes +// The place of the first value of an array a function says yes to, −1 for none: `xs.findIndex((x) => x > 2)`. +program(() => { + const xs = [1, 2, 3, 4]; + let v = 0; + while (v < 10) v += 3; + xs[1] = v; + print(`${xs.findIndex((x) => x > 2)}`); +}); diff --git a/ledger/arrays/find-last.ts b/ledger/arrays/find-last.ts new file mode 100644 index 0000000..5e37aaa --- /dev/null +++ b/ledger/arrays/find-last.ts @@ -0,0 +1,9 @@ +// ledger: step 3, takes +// The last value of an array a function says yes to, and its place: `xs.findLast(…)`, `xs.findLastIndex(…)`. +program(() => { + const xs = [1, 2, 3, 4]; + let v = 0; + while (v < 10) v += 3; + xs[0] = v; + print(`${xs.findLast((x) => x < 4) ?? -1} ${xs.findLastIndex((x) => x > 9)}`); +}); diff --git a/ledger/arrays/find.ts b/ledger/arrays/find.ts new file mode 100644 index 0000000..5f84f9a --- /dev/null +++ b/ledger/arrays/find.ts @@ -0,0 +1,9 @@ +// ledger: step 3, takes +// The first value of an array a function says yes to, or none: `xs.find((x) => x > 2) ?? -1`. +program(() => { + const xs = [1, 2, 3, 4]; + let v = 0; + while (v < 10) v += 3; + xs[0] = v; + print(`${xs.find((x) => x > 2) ?? -1}`); +}); diff --git a/ledger/arrays/for-each.ts b/ledger/arrays/for-each.ts new file mode 100644 index 0000000..21e8535 --- /dev/null +++ b/ledger/arrays/for-each.ts @@ -0,0 +1,13 @@ +// ledger: step 3, takes +// A function run for each value of an array: `xs.forEach((x) => { total += x; })`. +program(() => { + const xs = [1, 2, 3, 4]; + let v = 0; + while (v < 10) v += 3; + xs[0] = v; + let total = 0; + xs.forEach((x) => { + total += x; + }); + print(`${total}`); +}); diff --git a/ledger/arrays/grid-made-when-built.ts b/ledger/arrays/grid-made-when-built.ts new file mode 100644 index 0000000..4f6c79b --- /dev/null +++ b/ledger/arrays/grid-made-when-built.ts @@ -0,0 +1,10 @@ +// ledger: step 3, takes +// A grid made the ways people make one, worked out when the map is built: +// `Array.from({ length: 3 }, () => new Array(4).fill(0))`. +program(() => { + const grid = Array.from({ length: 3 }, () => new Array(4).fill(0)); + let y = 0; + while (y < 2) y += 1; + grid[y][3] = 5; + print(`${grid[2][3]}`); +}); diff --git a/ledger/arrays/grid-read-and-store.ts b/ledger/arrays/grid-read-and-store.ts new file mode 100644 index 0000000..5e0ad29 --- /dev/null +++ b/ledger/arrays/grid-read-and-store.ts @@ -0,0 +1,16 @@ +// ledger: step 3, takes +// A grid, an array of rows, read and stored at two places that change while the game runs: `grid[y][x]`. +program(() => { + const grid = [ + [0, 0, 0], + [0, 0, 0], + ]; + let x = 0; + let y = 0; + while (x < 2) { + x += 1; + y = 1; + } + grid[y][x] = 7; + print(`${grid[1][2]}`); +}); diff --git a/ledger/arrays/grid-row.ts b/ledger/arrays/grid-row.ts new file mode 100644 index 0000000..3676bca --- /dev/null +++ b/ledger/arrays/grid-row.ts @@ -0,0 +1,15 @@ +// ledger: step 3, takes +// A row of a grid kept in a variable and looped over: `const row = grid[y]`, which is the grid's own cells. +program(() => { + const grid = [ + [1, 2, 3], + [4, 5, 6], + ]; + grid[0][0] = 9; + let y = 0; + while (y < 1) y += 1; + const row = grid[y]; + let total = 0; + for (const c of row) total += c; + print(`${total}`); +}); diff --git a/ledger/arrays/join.ts b/ledger/arrays/join.ts new file mode 100644 index 0000000..489a3df --- /dev/null +++ b/ledger/arrays/join.ts @@ -0,0 +1,9 @@ +// ledger: step 8, refused: xs.join() +// The values of an array of numbers as one text, with a separator between them: `xs.join(", ")`. +program(() => { + const xs = [5, 2, 8]; + let v = 0; + while (v < 10) v += 3; + xs[0] = v; + print(xs.join(", ")); +}); diff --git a/ledger/arrays/last-index-of.ts b/ledger/arrays/last-index-of.ts new file mode 100644 index 0000000..9c26974 --- /dev/null +++ b/ledger/arrays/last-index-of.ts @@ -0,0 +1,9 @@ +// ledger: step 3, takes +// Where a value is last in an array, −1 when it is not: `xs.lastIndexOf(v)`. +program(() => { + const xs = [4, 8, 4, 8]; + let v = 0; + while (v < 8) v += 4; + xs[0] = 5; + print(`${xs.lastIndexOf(v)}`); +}); diff --git a/ledger/arrays/length-of-a-growing-array.ts b/ledger/arrays/length-of-a-growing-array.ts new file mode 100644 index 0000000..1165a09 --- /dev/null +++ b/ledger/arrays/length-of-a-growing-array.ts @@ -0,0 +1,11 @@ +// ledger: step 9, refused: an array that grows +// How many values an array that grows holds: `xs.length`. +program(() => { + const xs: number[] = []; + let v = 0; + while (v < 10) { + xs.push(v); + v += 3; + } + print(`${xs.length}`); +}); diff --git a/ledger/arrays/length-set.ts b/ledger/arrays/length-set.ts new file mode 100644 index 0000000..44d9e6c --- /dev/null +++ b/ledger/arrays/length-set.ts @@ -0,0 +1,10 @@ +// ledger: step 9, refused: an array that grows +// An array that grows cut short, or emptied, by setting its length: `xs.length = 1`. +program(() => { + const xs = [1, 2, 3]; + let v = 0; + while (v < 10) v += 3; + xs.push(v); + xs.length = 1; + print(`${xs[0]}`); +}); diff --git a/ledger/arrays/list-of-records-read-at-a-value.ts b/ledger/arrays/list-of-records-read-at-a-value.ts new file mode 100644 index 0000000..5a446f2 --- /dev/null +++ b/ledger/arrays/list-of-records-read-at-a-value.ts @@ -0,0 +1,12 @@ +// ledger: step 3, takes +// A list of records written in the script, looked up at a value that changes while the game runs: `WAVES[i].count`. +const WAVES = [ + { count: 5, gold: 10 }, + { count: 8, gold: 15 }, + { count: 13, gold: 25 }, +]; +program(() => { + let i = 0; + while (i < 2) i += 1; + print(`${WAVES[i].count + WAVES[i].gold}`); +}); diff --git a/ledger/arrays/list-read-at-a-constant.ts b/ledger/arrays/list-read-at-a-constant.ts new file mode 100644 index 0000000..98026fd --- /dev/null +++ b/ledger/arrays/list-read-at-a-constant.ts @@ -0,0 +1,9 @@ +// ledger: step 0, takes +// A list written in the script, read at a place written in the script, which is its value when the map is built: +// `PRICES[2]`. +const PRICES = [50, 75, 100, 150]; +program(() => { + let n = 0; + while (n < PRICES[2]) n += 30; + print(`${n}`); +}); diff --git a/ledger/arrays/list-read-at-a-value.ts b/ledger/arrays/list-read-at-a-value.ts new file mode 100644 index 0000000..404a8c7 --- /dev/null +++ b/ledger/arrays/list-read-at-a-value.ts @@ -0,0 +1,8 @@ +// ledger: step 3, takes +// A list written in the script, looked up at a value that changes while the game runs: `PRICES[i]`. +const PRICES = [50, 75, 100, 150]; +program(() => { + let i = 0; + while (i < 2) i += 1; + print(`${PRICES[i]}`); +}); diff --git a/ledger/arrays/map-for-of.ts b/ledger/arrays/map-for-of.ts new file mode 100644 index 0000000..6e38fa2 --- /dev/null +++ b/ledger/arrays/map-for-of.ts @@ -0,0 +1,13 @@ +// ledger: step 9, refused: an array that grows +// A loop over a `Map`'s keys and values, in the order the keys went in: `for (const [key, value] of map)`. +program(() => { + const scores = new Map(); + let key = 1; + while (key < 20) { + scores.set(key, key + 1); + key += 6; + } + let total = 0; + for (const [k, v] of scores) total += k + v; + print(`${total}`); +}); diff --git a/ledger/arrays/map-keyed-by-game-ids.ts b/ledger/arrays/map-keyed-by-game-ids.ts new file mode 100644 index 0000000..cd05066 --- /dev/null +++ b/ledger/arrays/map-keyed-by-game-ids.ts @@ -0,0 +1,15 @@ +// ledger: step 3, takes +// A `Map` keyed by ids of the game, at a key that changes while the game runs: `get`, `set`, `has`, `delete` and +// `size`. +program(() => { + const bounty = new Map(); + let type: UnitType = units.ZergZergling; + let n = 0; + while (n < 5) { + bounty.set(type, n); + type = units.ZergHydralisk; + n += 2; + } + bounty.delete(units.ZergZergling); + if (bounty.has(type)) print(`${bounty.get(type) ?? 0} ${bounty.size}`); +}); diff --git a/ledger/arrays/map-over-any-number.ts b/ledger/arrays/map-over-any-number.ts new file mode 100644 index 0000000..233351a --- /dev/null +++ b/ledger/arrays/map-over-any-number.ts @@ -0,0 +1,11 @@ +// ledger: step 9, refused: an array that grows +// A `Map` keyed by any number, below zero and large too: `scores.set(key, v)`, `scores.get(key) ?? 0`. +program(() => { + const scores = new Map(); + let key = 1000; + while (key < 100000) { + scores.set(key, key / 1000); + key *= 7; + } + print(`${scores.get(7000) ?? 0} ${scores.size}`); +}); diff --git a/ledger/arrays/map.ts b/ledger/arrays/map.ts new file mode 100644 index 0000000..12d05dc --- /dev/null +++ b/ledger/arrays/map.ts @@ -0,0 +1,10 @@ +// ledger: step 3, takes +// A new array of each value of another put through a function: `xs.map((x) => x * 2)`. +program(() => { + const xs = [1, 2, 3]; + let v = 0; + while (v < 10) v += 3; + xs[0] = v; + const ys = xs.map((x) => x * 2); + print(`${ys[0]}`); +}); diff --git a/ledger/arrays/pop.ts b/ledger/arrays/pop.ts new file mode 100644 index 0000000..2ddeea4 --- /dev/null +++ b/ledger/arrays/pop.ts @@ -0,0 +1,10 @@ +// ledger: step 9, refused: an array that grows +// The last value of an array taken off it: `xs.pop()`. +program(() => { + const xs = [1, 2, 3]; + let v = 0; + while (v < 10) v += 3; + xs.push(v); + const last = xs.pop() ?? 0; + print(`${last}`); +}); diff --git a/ledger/arrays/push.ts b/ledger/arrays/push.ts new file mode 100644 index 0000000..fd4f7b8 --- /dev/null +++ b/ledger/arrays/push.ts @@ -0,0 +1,11 @@ +// ledger: step 9, refused: an array that grows +// A value added at the end of an array, which grows: `xs.push(v)`. +program(() => { + const xs: number[] = []; + let v = 0; + while (v < 10) { + xs.push(v); + v += 3; + } + print(`${xs[2]}`); +}); diff --git a/ledger/arrays/read-at-a-shifted-index.ts b/ledger/arrays/read-at-a-shifted-index.ts new file mode 100644 index 0000000..59353f7 --- /dev/null +++ b/ledger/arrays/read-at-a-shifted-index.ts @@ -0,0 +1,10 @@ +// ledger: step 3, refused: a shift by a constant (>>) as a value +// A cell read at a place worked out by a shift, as the halves of a list are: `xs[a >> 1]` (stored there, or with the +// place kept in a variable first, it is taken). +program(() => { + const xs = [1, 2, 3, 4, 5, 6, 7]; + xs[0] = 9; + let a = 0; + while (a < 10) a += 3; + print(`${xs[a >> 1]}`); +}); diff --git a/ledger/arrays/record-keyed-by-game-ids.ts b/ledger/arrays/record-keyed-by-game-ids.ts new file mode 100644 index 0000000..67b9dbb --- /dev/null +++ b/ledger/arrays/record-keyed-by-game-ids.ts @@ -0,0 +1,14 @@ +// ledger: step 3, takes +// A record keyed by an id of the game, a cell for every id, read and written at a key that changes while the game +// runs: `const score: Record = { [P1]: 0 }`, then `score[p] += 7`. +program(() => { + const score: Record = { [P1]: 0 }; + let p: Player = P1; + let n = 0; + while (n < 3) { + score[p] += 7; + p = P2; + n += 1; + } + print(`${score[P1]} ${score[p]}`); +}); diff --git a/ledger/arrays/reduce.ts b/ledger/arrays/reduce.ts new file mode 100644 index 0000000..683f598 --- /dev/null +++ b/ledger/arrays/reduce.ts @@ -0,0 +1,10 @@ +// ledger: step 3, takes +// The values of an array put together into one, a function at a time: `xs.reduce((so, x) => so + x, 0)`. +program(() => { + const xs = [1, 2, 3, 4]; + let v = 0; + while (v < 10) v += 3; + xs[0] = v; + const total = xs.reduce((so, x) => so + x, 0); + print(`${total}`); +}); diff --git a/ledger/arrays/rest-of-an-array.ts b/ledger/arrays/rest-of-an-array.ts new file mode 100644 index 0000000..6258466 --- /dev/null +++ b/ledger/arrays/rest-of-an-array.ts @@ -0,0 +1,10 @@ +// ledger: step 3, takes +// The rest of an array taken out by a pattern, an array of its own: `const [first, ...rest] = xs`. +program(() => { + const xs = [5, 2, 8, 1]; + let v = 0; + while (v < 10) v += 3; + xs[0] = v; + const [first, ...rest] = xs; + print(`${first} ${rest[0]}`); +}); diff --git a/ledger/arrays/reverse.ts b/ledger/arrays/reverse.ts new file mode 100644 index 0000000..3bf7414 --- /dev/null +++ b/ledger/arrays/reverse.ts @@ -0,0 +1,10 @@ +// ledger: step 3, takes +// An array turned round in place: `xs.reverse()`. +program(() => { + const xs = [5, 2, 8, 1]; + let v = 0; + while (v < 10) v += 3; + xs[0] = v; + xs.reverse(); + print(`${xs[0]} ${xs[3]}`); +}); diff --git a/ledger/arrays/rows-that-grow.ts b/ledger/arrays/rows-that-grow.ts new file mode 100644 index 0000000..6b7e2a6 --- /dev/null +++ b/ledger/arrays/rows-that-grow.ts @@ -0,0 +1,12 @@ +// ledger: step 9, refused: an array that grows +// An array of arrays each of its own length, a row pushed to: `rows[i].push(v)`. +program(() => { + const rows: number[][] = [[], [], []]; + let i = 0; + while (i < 2) { + rows[i].push(i + 5); + i += 1; + } + rows[1].push(9); + print(`${rows[1].length} ${rows[1][1]}`); +}); diff --git a/ledger/arrays/set-keyed-by-game-ids.ts b/ledger/arrays/set-keyed-by-game-ids.ts new file mode 100644 index 0000000..e0a0f94 --- /dev/null +++ b/ledger/arrays/set-keyed-by-game-ids.ts @@ -0,0 +1,14 @@ +// ledger: step 3, takes +// A `Set` of ids of the game, at a key that changes while the game runs: `add`, `has`, `delete` and `size`. +program(() => { + const seen = new Set(); + let type: UnitType = units.ZergZergling; + let n = 0; + while (n < 5) { + seen.add(type); + type = units.ZergHydralisk; + n += 2; + } + seen.delete(units.ZergZergling); + if (seen.has(type)) print(`${seen.size}`); +}); diff --git a/ledger/arrays/set.ts b/ledger/arrays/set.ts new file mode 100644 index 0000000..6075b6f --- /dev/null +++ b/ledger/arrays/set.ts @@ -0,0 +1,12 @@ +// ledger: step 9, refused: an array that grows +// A `Set` of numbers: `add`, `has`, `delete` and `size`. +program(() => { + const seen = new Set(); + let v = 0; + while (v < 20) { + seen.add(v); + v += 6; + } + seen.delete(6); + if (seen.has(12)) print(`${seen.size}`); +}); diff --git a/ledger/arrays/shift.ts b/ledger/arrays/shift.ts new file mode 100644 index 0000000..b3c526b --- /dev/null +++ b/ledger/arrays/shift.ts @@ -0,0 +1,10 @@ +// ledger: step 9, refused: an array that grows +// The first value of an array taken off its front: `xs.shift()`. +program(() => { + const xs = [1, 2, 3]; + let v = 0; + while (v < 10) v += 3; + xs[0] = v; + const first = xs.shift() ?? 0; + print(`${first} ${xs[0]}`); +}); diff --git a/ledger/arrays/signal.ts b/ledger/arrays/signal.ts new file mode 100644 index 0000000..44fb7b9 --- /dev/null +++ b/ledger/arrays/signal.ts @@ -0,0 +1,16 @@ +// ledger: step 9, refused: an array that grows +// An event of the script's own from `trigscript/signals`: handlers added with `on` and run with `emit`. +import { Signal } from "trigscript/signals"; +program(() => { + const cleared = new Signal(); + let total = 0; + cleared.on((wave) => { + total += wave; + }); + let wave = 0; + while (wave < 3) { + wave += 1; + cleared.emit(wave); + } + print(`${total}`); +}); diff --git a/ledger/arrays/slice.ts b/ledger/arrays/slice.ts new file mode 100644 index 0000000..1cb674f --- /dev/null +++ b/ledger/arrays/slice.ts @@ -0,0 +1,10 @@ +// ledger: step 9, refused: an array that grows +// A copy of part of an array: `xs.slice(1, 3)`. +program(() => { + const xs = [5, 2, 8, 1]; + let v = 0; + while (v < 10) v += 3; + xs[1] = v; + const ys = xs.slice(1, 3); + print(`${ys[0]}`); +}); diff --git a/ledger/arrays/some.ts b/ledger/arrays/some.ts new file mode 100644 index 0000000..fcb62c0 --- /dev/null +++ b/ledger/arrays/some.ts @@ -0,0 +1,10 @@ +// ledger: step 3, takes +// Whether a function says yes to any value of an array: `xs.some((x) => x > 10)`. +program(() => { + const xs = [1, 2, 3, 4]; + let v = 0; + while (v < 10) v += 3; + xs[0] = v; + if (xs.some((x) => x > 10)) print("one is"); + else print("none is"); +}); diff --git a/ledger/arrays/sort.ts b/ledger/arrays/sort.ts new file mode 100644 index 0000000..86274a9 --- /dev/null +++ b/ledger/arrays/sort.ts @@ -0,0 +1,10 @@ +// ledger: step 3, takes +// An array sorted in place by a function that compares two values: `xs.sort((a, b) => a - b)`. +program(() => { + const xs = [5, 2, 8, 1]; + let v = 0; + while (v < 10) v += 3; + xs[0] = v; + xs.sort((a, b) => a - b); + print(`${xs[0]} ${xs[3]}`); +}); diff --git a/ledger/arrays/splice.ts b/ledger/arrays/splice.ts new file mode 100644 index 0000000..22bc6a1 --- /dev/null +++ b/ledger/arrays/splice.ts @@ -0,0 +1,9 @@ +// ledger: step 9, refused: an array that grows +// Values taken out of an array and others put in their place: `xs.splice(i, 1, v)`. +program(() => { + const xs = [1, 2, 3, 4]; + let i = 0; + while (i < 2) i += 1; + xs.splice(i, 1, 9, 9); + print(`${xs[2]} ${xs[3]}`); +}); diff --git a/ledger/arrays/spread-into-a-call.ts b/ledger/arrays/spread-into-a-call.ts new file mode 100644 index 0000000..34602c1 --- /dev/null +++ b/ledger/arrays/spread-into-a-call.ts @@ -0,0 +1,12 @@ +// ledger: step 3, takes +// An array of a fixed length spread into a call's arguments: `f(...xs)`. +function sum3(a: number, b: number, c: number): number { + return a + b + c; +} +program(() => { + const xs: [number, number, number] = [1, 2, 3]; + let v = 0; + while (v < 10) v += 3; + xs[1] = v; + print(`${sum3(...xs)}`); +}); diff --git a/ledger/arrays/spread-of-a-growing-array.ts b/ledger/arrays/spread-of-a-growing-array.ts new file mode 100644 index 0000000..b0d1b82 --- /dev/null +++ b/ledger/arrays/spread-of-a-growing-array.ts @@ -0,0 +1,12 @@ +// ledger: step 9, refused: an array that grows +// An array that grows made from another that grows and more: `[...xs, 9]`. +program(() => { + const xs: number[] = []; + let v = 0; + while (v < 10) { + xs.push(v); + v += 3; + } + const ys = [...xs, 9]; + print(`${ys.length}`); +}); diff --git a/ledger/arrays/store-at-the-end.ts b/ledger/arrays/store-at-the-end.ts new file mode 100644 index 0000000..0c510b4 --- /dev/null +++ b/ledger/arrays/store-at-the-end.ts @@ -0,0 +1,9 @@ +// ledger: step 9, refused: an array that grows +// A value stored one past the end of an array, which grows it as `push` does: `xs[xs.length] = v`. +program(() => { + const xs = [1, 2]; + let v = 0; + while (v < 10) v += 3; + xs[xs.length] = v; + print(`${xs[2]}`); +}); diff --git a/ledger/arrays/to-reversed.ts b/ledger/arrays/to-reversed.ts new file mode 100644 index 0000000..8c2794c --- /dev/null +++ b/ledger/arrays/to-reversed.ts @@ -0,0 +1,10 @@ +// ledger: step 9, refused: an array that grows +// A copy of an array turned round, the array left as it was: `xs.toReversed()`. +program(() => { + const xs = [5, 2, 8, 1]; + let v = 0; + while (v < 10) v += 3; + xs[0] = v; + const ys = xs.toReversed(); + print(`${ys[3]} ${xs[0]}`); +}); diff --git a/ledger/arrays/to-sorted.ts b/ledger/arrays/to-sorted.ts new file mode 100644 index 0000000..413cdca --- /dev/null +++ b/ledger/arrays/to-sorted.ts @@ -0,0 +1,10 @@ +// ledger: step 9, refused: an array that grows +// A sorted copy of an array, the array left as it was: `xs.toSorted((a, b) => a - b)`. +program(() => { + const xs = [5, 2, 8, 1]; + let v = 0; + while (v < 10) v += 3; + xs[0] = v; + const ys = xs.toSorted((a, b) => a - b); + print(`${ys[0]} ${xs[0]}`); +}); diff --git a/ledger/arrays/u16-array.ts b/ledger/arrays/u16-array.ts new file mode 100644 index 0000000..47bf5e0 --- /dev/null +++ b/ledger/arrays/u16-array.ts @@ -0,0 +1,9 @@ +// ledger: step 4, refused: a u16 array +// An array of `u16` cells, each within 0 … 65 535: a store below zero is 0. +program(() => { + const xs: u16[] = [5, 5, 5]; + let i = 0; + while (i < 2) i += 1; + xs[i] -= 10; + print(`${xs[i]}`); +}); diff --git a/ledger/arrays/u32-array.ts b/ledger/arrays/u32-array.ts new file mode 100644 index 0000000..18f37f0 --- /dev/null +++ b/ledger/arrays/u32-array.ts @@ -0,0 +1,10 @@ +// ledger: step 3, takes +// An array of `u32` cells, read from 0 up: a cell set to −1 is larger than 100. +program(() => { + const xs: u32[] = [u32(0), u32(0), u32(0)]; + let i = 0; + while (i < 2) i += 1; + xs[i] = u32(-1); + if (xs[i] > 100) print("large"); + else print("small"); +}); diff --git a/ledger/arrays/u8-array.ts b/ledger/arrays/u8-array.ts new file mode 100644 index 0000000..d0a81db --- /dev/null +++ b/ledger/arrays/u8-array.ts @@ -0,0 +1,9 @@ +// ledger: step 4, refused: a u8 array +// An array of `u8` cells, each within 0 … 255: a store below zero is 0, above 255 is 255. +program(() => { + const xs: u8[] = [250, 250, 250]; + let i = 0; + while (i < 2) i += 1; + xs[i] += 10; + print(`${xs[i]}`); +}); diff --git a/ledger/arrays/unshift.ts b/ledger/arrays/unshift.ts new file mode 100644 index 0000000..9ad3268 --- /dev/null +++ b/ledger/arrays/unshift.ts @@ -0,0 +1,9 @@ +// ledger: step 9, refused: an array that grows +// A value put at the front of an array: `xs.unshift(v)`. +program(() => { + const xs = [1, 2, 3]; + let v = 0; + while (v < 10) v += 3; + xs.unshift(v); + print(`${xs[0]} ${xs[1]}`); +}); diff --git a/ledger/game/action-raw-record.ts b/ledger/game/action-raw-record.ts new file mode 100644 index 0000000..4efdbfa --- /dev/null +++ b/ledger/game/action-raw-record.ts @@ -0,0 +1,6 @@ +// ledger: step 5, refused: the action Center View +// An action by its raw type number and fields, `action(type, …)`: what StarEdit stores, for a type the +// editor does not know (here 10, Center View). +program(() => { + action(10, 64, 0, 0, 0, 0, 0, 0, 0); +}); diff --git a/ledger/game/center-location-with-size.ts b/ledger/game/center-location-with-size.ts new file mode 100644 index 0000000..1ee3f10 --- /dev/null +++ b/ledger/game/center-location-with-size.ts @@ -0,0 +1,8 @@ +// ledger: step 5, refused: a location moved (centerLocation) +// A location centred on a point and given a size, all in pixels: `centerLocation(location, x, y, width, height)`. +const spot = reserveLocation("ledger: where the box is"); +program(() => { + let x = 64; + x += 32; + centerLocation(spot, x, 96, 32, 32); +}); diff --git a/ledger/game/center-location.ts b/ledger/game/center-location.ts new file mode 100644 index 0000000..3e2a551 --- /dev/null +++ b/ledger/game/center-location.ts @@ -0,0 +1,9 @@ +// ledger: step 5, refused: a location moved (centerLocation) +// A location centred on a point of the map, in pixels from values of the program, its size kept: +// `centerLocation(location, x, y)`. +const spot = reserveLocation("ledger: where the point is"); +program(() => { + let x = 64; + x += 32; + centerLocation(spot, x, 96); +}); diff --git a/ledger/game/center-view.ts b/ledger/game/center-view.ts new file mode 100644 index 0000000..b0ea166 --- /dev/null +++ b/ledger/game/center-view.ts @@ -0,0 +1,5 @@ +// ledger: step 5, refused: the action Center View +// The action Center View: the player's screen moved to a location. +program(() => { + centerView(locations.Anywhere); +}); diff --git a/ledger/game/condition-accumulate.ts b/ledger/game/condition-accumulate.ts new file mode 100644 index 0000000..226b38e --- /dev/null +++ b/ledger/game/condition-accumulate.ts @@ -0,0 +1,5 @@ +// ledger: step 5, refused: the condition Accumulate +// The condition Accumulate as a test in an `if`: a player has at least so many minerals. +program(() => { + if (accumulate(P1, ">=", 500, "ore")) print("yes"); +}); diff --git a/ledger/game/condition-always.ts b/ledger/game/condition-always.ts new file mode 100644 index 0000000..8114848 --- /dev/null +++ b/ledger/game/condition-always.ts @@ -0,0 +1,5 @@ +// ledger: step 5, refused: the condition Always +// The condition Always as a test in an `if`. +program(() => { + if (always()) print("yes"); +}); diff --git a/ledger/game/condition-bring.ts b/ledger/game/condition-bring.ts new file mode 100644 index 0000000..99d5ee8 --- /dev/null +++ b/ledger/game/condition-bring.ts @@ -0,0 +1,5 @@ +// ledger: step 5, refused: the condition Bring +// The condition Bring as a test in an `if`: a player has units of a type at a location. +program(() => { + if (bring(P1, units.TerranMarine, locations.Anywhere, ">=", 5)) print("yes"); +}); diff --git a/ledger/game/condition-command-the-least-at.ts b/ledger/game/condition-command-the-least-at.ts new file mode 100644 index 0000000..9fa929e --- /dev/null +++ b/ledger/game/condition-command-the-least-at.ts @@ -0,0 +1,5 @@ +// ledger: step 5, refused: the condition Command the Least At +// The condition Command the Least At as a test in an `if`: the fewest units of a type at a location. +program(() => { + if (commandTheLeastAt(units.TerranMarine, locations.Anywhere)) print("yes"); +}); diff --git a/ledger/game/condition-command-the-least.ts b/ledger/game/condition-command-the-least.ts new file mode 100644 index 0000000..523e29f --- /dev/null +++ b/ledger/game/condition-command-the-least.ts @@ -0,0 +1,5 @@ +// ledger: step 5, refused: the condition Command the Least +// The condition Command the Least as a test in an `if`: the current player has the fewest units of a type. +program(() => { + if (commandTheLeast(units.TerranMarine)) print("yes"); +}); diff --git a/ledger/game/condition-command-the-most-at.ts b/ledger/game/condition-command-the-most-at.ts new file mode 100644 index 0000000..e516e21 --- /dev/null +++ b/ledger/game/condition-command-the-most-at.ts @@ -0,0 +1,5 @@ +// ledger: step 5, refused: the condition Command the Most At +// The condition Command the Most At as a test in an `if`: the most units of a type at a location. +program(() => { + if (commandTheMostAt(units.TerranMarine, locations.Anywhere)) print("yes"); +}); diff --git a/ledger/game/condition-command-the-most.ts b/ledger/game/condition-command-the-most.ts new file mode 100644 index 0000000..11933b6 --- /dev/null +++ b/ledger/game/condition-command-the-most.ts @@ -0,0 +1,5 @@ +// ledger: step 5, refused: the condition Command the Most +// The condition Command the Most as a test in an `if`: the current player has the most units of a type. +program(() => { + if (commandTheMost(units.TerranMarine)) print("yes"); +}); diff --git a/ledger/game/condition-command.ts b/ledger/game/condition-command.ts new file mode 100644 index 0000000..0df012e --- /dev/null +++ b/ledger/game/condition-command.ts @@ -0,0 +1,5 @@ +// ledger: step 5, refused: the condition Command +// The condition Command as a test in an `if`: a player has so many units of a type anywhere. +program(() => { + if (command(P1, units.TerranMarine, ">=", 5)) print("yes"); +}); diff --git a/ledger/game/condition-countdown-timer.ts b/ledger/game/condition-countdown-timer.ts new file mode 100644 index 0000000..208e951 --- /dev/null +++ b/ledger/game/condition-countdown-timer.ts @@ -0,0 +1,5 @@ +// ledger: step 5, refused: the condition Countdown Timer +// The condition Countdown Timer as a test in an `if`: the timer is at most so many seconds. +program(() => { + if (countdownTimer("<=", 10)) print("yes"); +}); diff --git a/ledger/game/condition-deaths.ts b/ledger/game/condition-deaths.ts new file mode 100644 index 0000000..7536ad8 --- /dev/null +++ b/ledger/game/condition-deaths.ts @@ -0,0 +1,5 @@ +// ledger: step 5, refused: the condition Deaths +// The condition Deaths as a test in an `if`: the counter a map keeps in the death table. +program(() => { + if (deaths(P1, units.TerranMarine, ">=", 3)) print("yes"); +}); diff --git a/ledger/game/condition-elapsed-time.ts b/ledger/game/condition-elapsed-time.ts new file mode 100644 index 0000000..ed84806 --- /dev/null +++ b/ledger/game/condition-elapsed-time.ts @@ -0,0 +1,5 @@ +// ledger: step 5, refused: the condition Elapsed Time +// The condition Elapsed Time as a test in an `if`: so many game seconds have gone by. +program(() => { + if (elapsedTime(">=", 60)) print("yes"); +}); diff --git a/ledger/game/condition-highest-score.ts b/ledger/game/condition-highest-score.ts new file mode 100644 index 0000000..a226cc5 --- /dev/null +++ b/ledger/game/condition-highest-score.ts @@ -0,0 +1,5 @@ +// ledger: step 5, refused: the condition Highest Score +// The condition Highest Score as a test in an `if`: the current player has the highest score of a kind. +program(() => { + if (highestScore("kills")) print("yes"); +}); diff --git a/ledger/game/condition-in-a-while.ts b/ledger/game/condition-in-a-while.ts new file mode 100644 index 0000000..e7b49d0 --- /dev/null +++ b/ledger/game/condition-in-a-while.ts @@ -0,0 +1,6 @@ +// ledger: step 5, refused: the condition Switch +// A condition as the test of a `while`: the loop goes on, a frame at a time, until the switch is set. +program(() => { + while (!switchIs(switches.Switch1, "set")) sleep(frames(1)); + print("open"); +}); diff --git a/ledger/game/condition-kill.ts b/ledger/game/condition-kill.ts new file mode 100644 index 0000000..9813ab4 --- /dev/null +++ b/ledger/game/condition-kill.ts @@ -0,0 +1,5 @@ +// ledger: step 5, refused: the condition Kill +// The condition Kill as a test in an `if`: a player has killed so many units of a type. +program(() => { + if (kill(P1, units.ZergZergling, ">=", 10)) print("yes"); +}); diff --git a/ledger/game/condition-least-kills.ts b/ledger/game/condition-least-kills.ts new file mode 100644 index 0000000..c3c0fd3 --- /dev/null +++ b/ledger/game/condition-least-kills.ts @@ -0,0 +1,5 @@ +// ledger: step 5, refused: the condition Least Kills +// The condition Least Kills as a test in an `if`. +program(() => { + if (leastKills(units.ZergZergling)) print("yes"); +}); diff --git a/ledger/game/condition-least-resources.ts b/ledger/game/condition-least-resources.ts new file mode 100644 index 0000000..73d26b2 --- /dev/null +++ b/ledger/game/condition-least-resources.ts @@ -0,0 +1,5 @@ +// ledger: step 5, refused: the condition Least Resources +// The condition Least Resources as a test in an `if`. +program(() => { + if (leastResources("ore")) print("yes"); +}); diff --git a/ledger/game/condition-lowest-score.ts b/ledger/game/condition-lowest-score.ts new file mode 100644 index 0000000..be20015 --- /dev/null +++ b/ledger/game/condition-lowest-score.ts @@ -0,0 +1,5 @@ +// ledger: step 5, refused: the condition Lowest Score +// The condition Lowest Score as a test in an `if`. +program(() => { + if (lowestScore("total")) print("yes"); +}); diff --git a/ledger/game/condition-most-kills.ts b/ledger/game/condition-most-kills.ts new file mode 100644 index 0000000..8e833a8 --- /dev/null +++ b/ledger/game/condition-most-kills.ts @@ -0,0 +1,5 @@ +// ledger: step 5, refused: the condition Most Kills +// The condition Most Kills as a test in an `if`. +program(() => { + if (mostKills(units.ZergZergling)) print("yes"); +}); diff --git a/ledger/game/condition-most-resources.ts b/ledger/game/condition-most-resources.ts new file mode 100644 index 0000000..83c5f60 --- /dev/null +++ b/ledger/game/condition-most-resources.ts @@ -0,0 +1,5 @@ +// ledger: step 5, refused: the condition Most Resources +// The condition Most Resources as a test in an `if`. +program(() => { + if (mostResources("oreAndGas")) print("yes"); +}); diff --git a/ledger/game/condition-negated.ts b/ledger/game/condition-negated.ts new file mode 100644 index 0000000..94b735d --- /dev/null +++ b/ledger/game/condition-negated.ts @@ -0,0 +1,5 @@ +// ledger: step 5, refused: the condition Bring +// A condition turned around with `!` in an `if`: `if (!bring(…))`, which works for every condition. +program(() => { + if (!bring(P1, units.AnyUnit, locations.Anywhere, ">=", 1)) print("yes"); +}); diff --git a/ledger/game/condition-never.ts b/ledger/game/condition-never.ts new file mode 100644 index 0000000..117f4af --- /dev/null +++ b/ledger/game/condition-never.ts @@ -0,0 +1,5 @@ +// ledger: step 5, refused: the condition Never +// The condition Never as a test in an `if`. +program(() => { + if (never()) print("yes"); +}); diff --git a/ledger/game/condition-opponents.ts b/ledger/game/condition-opponents.ts new file mode 100644 index 0000000..1208abb --- /dev/null +++ b/ledger/game/condition-opponents.ts @@ -0,0 +1,5 @@ +// ledger: step 5, refused: the condition Opponents +// The condition Opponents as a test in an `if`: how many opponents a player has left. +program(() => { + if (opponents(P1, "==", 0)) print("yes"); +}); diff --git a/ledger/game/condition-raw-record.ts b/ledger/game/condition-raw-record.ts new file mode 100644 index 0000000..d9ea21b --- /dev/null +++ b/ledger/game/condition-raw-record.ts @@ -0,0 +1,6 @@ +// ledger: step 5, refused: the condition Deaths +// A condition by its raw type number and fields, `condition(type, …)`: what StarEdit stores, for a type the +// editor does not know (here 15, Deaths). +program(() => { + if (condition(15, 0, 0, 3, 0, 0, 0)) print("yes"); +}); diff --git a/ledger/game/condition-score.ts b/ledger/game/condition-score.ts new file mode 100644 index 0000000..d2f76b8 --- /dev/null +++ b/ledger/game/condition-score.ts @@ -0,0 +1,5 @@ +// ledger: step 5, refused: the condition Score +// The condition Score as a test in an `if`: a player has so many points of a kind. +program(() => { + if (score(P1, "kills", ">=", 1000)) print("yes"); +}); diff --git a/ledger/game/condition-switch.ts b/ledger/game/condition-switch.ts new file mode 100644 index 0000000..2c44be8 --- /dev/null +++ b/ledger/game/condition-switch.ts @@ -0,0 +1,5 @@ +// ledger: step 5, refused: the condition Switch +// The condition Switch as a test in an `if`: one of the map's switches (`switches.*`) is set. +program(() => { + if (switchIs(switches.Switch1, "set")) print("yes"); +}); diff --git a/ledger/game/cover.ts b/ledger/game/cover.ts new file mode 100644 index 0000000..1e28bd3 --- /dev/null +++ b/ledger/game/cover.ts @@ -0,0 +1,14 @@ +// ledger: step 3, takes +// `cover(picture, shapes)`: the brushes that draw a picture, worked out when the map is built; the +// program reads them from an array at a place it works out. +const brushes = cover([".##.", "####", "####"], [["#@#"]]); +const xs = brushes.map((b) => b.x); +program(() => { + let i = 0; + let sum = 0; + while (i < xs.length) { + sum += xs[i]; + i += 1; + } + print(`${sum}`); +}); diff --git a/ledger/game/create-unit-count-from-a-value.ts b/ledger/game/create-unit-count-from-a-value.ts new file mode 100644 index 0000000..6d39c64 --- /dev/null +++ b/ledger/game/create-unit-count-from-a-value.ts @@ -0,0 +1,7 @@ +// ledger: step 5, refused: the action Create Unit, its fields from values +// The action Create Unit, how many from a value of the program. +program(() => { + let n = 2; + n += 1; + createUnit(P1, units.TerranMarine, n, locations.Anywhere); +}); diff --git a/ledger/game/create-unit-type-from-a-value.ts b/ledger/game/create-unit-type-from-a-value.ts new file mode 100644 index 0000000..be7f763 --- /dev/null +++ b/ledger/game/create-unit-type-from-a-value.ts @@ -0,0 +1,8 @@ +// ledger: step 5, refused: the action Create Unit, its fields from values +// The action Create Unit, the unit type from a value of the program. +program(() => { + let kind: UnitType = units.TerranMarine; + let wave = 3; + if (wave > 2) kind = units.TerranFirebat; + createUnit(P1, kind, 1, locations.Anywhere); +}); diff --git a/ledger/game/create-unit-with-properties-count-from-a-value.ts b/ledger/game/create-unit-with-properties-count-from-a-value.ts new file mode 100644 index 0000000..488e691 --- /dev/null +++ b/ledger/game/create-unit-with-properties-count-from-a-value.ts @@ -0,0 +1,7 @@ +// ledger: step 5, refused: the action Create Unit with Properties, its fields from values +// The action Create Unit with Properties, how many from a value of the program. +program(() => { + let n = 1; + n += 2; + createUnitWithProperties(P1, units.TerranMarine, n, locations.Anywhere, 1); +}); diff --git a/ledger/game/create-unit-with-properties-type-from-a-value.ts b/ledger/game/create-unit-with-properties-type-from-a-value.ts new file mode 100644 index 0000000..0b6a031 --- /dev/null +++ b/ledger/game/create-unit-with-properties-type-from-a-value.ts @@ -0,0 +1,8 @@ +// ledger: step 5, refused: the action Create Unit with Properties, its fields from values +// The action Create Unit with Properties, the unit type from a value of the program. +program(() => { + let kind: UnitType = units.TerranMarine; + let wave = 3; + if (wave > 2) kind = units.TerranFirebat; + createUnitWithProperties(P1, kind, 1, locations.Anywhere, 1); +}); diff --git a/ledger/game/create-unit-with-properties.ts b/ledger/game/create-unit-with-properties.ts new file mode 100644 index 0000000..2346523 --- /dev/null +++ b/ledger/game/create-unit-with-properties.ts @@ -0,0 +1,6 @@ +// ledger: step 5, refused: the action Create Unit with Properties +// The action Create Unit with Properties, all its fields known when the map is built: units made with +// one of the map's unit property sets (hit points, energy, …). +program(() => { + createUnitWithProperties(P1, units.TerranMarine, 1, locations.Anywhere, 1); +}); diff --git a/ledger/game/create-unit.ts b/ledger/game/create-unit.ts new file mode 100644 index 0000000..8f9fd83 --- /dev/null +++ b/ledger/game/create-unit.ts @@ -0,0 +1,5 @@ +// ledger: step 5, refused: the action Create Unit +// The action Create Unit, all its fields known when the map is built. +program(() => { + createUnit(P1, units.TerranMarine, 1, locations.Anywhere); +}); diff --git a/ledger/game/defeat.ts b/ledger/game/defeat.ts new file mode 100644 index 0000000..7071811 --- /dev/null +++ b/ledger/game/defeat.ts @@ -0,0 +1,5 @@ +// ledger: step 5, refused: the action Defeat +// The action Defeat: the player loses the game. +program(() => { + defeat(); +}); diff --git a/ledger/game/display-text-with-a-number.ts b/ledger/game/display-text-with-a-number.ts new file mode 100644 index 0000000..bd90167 --- /dev/null +++ b/ledger/game/display-text-with-a-number.ts @@ -0,0 +1,8 @@ +// ledger: step 2, takes +// Display Text Message with a number of the program in it: `displayText(`${n} left`)` shows it to the +// current player, as `print` does. +program(() => { + let n = 3; + n += 1; + displayText(`${n} waves left`); +}); diff --git a/ledger/game/display-text.ts b/ledger/game/display-text.ts new file mode 100644 index 0000000..3f43e97 --- /dev/null +++ b/ledger/game/display-text.ts @@ -0,0 +1,5 @@ +// ledger: step 5, refused: the action Display Text Message +// The action Display Text Message, its text written in the script: `displayText("…")`. +program(() => { + displayText("The gates are open."); +}); diff --git a/ledger/game/draw.ts b/ledger/game/draw.ts new file mode 100644 index 0000000..5a0e973 --- /dev/null +++ b/ledger/game/draw.ts @@ -0,0 +1,5 @@ +// ledger: step 5, refused: the action Draw +// The action Draw: the game ends in a draw. +program(() => { + draw(); +}); diff --git a/ledger/game/give-units-count-from-a-value.ts b/ledger/game/give-units-count-from-a-value.ts new file mode 100644 index 0000000..9cccc20 --- /dev/null +++ b/ledger/game/give-units-count-from-a-value.ts @@ -0,0 +1,7 @@ +// ledger: step 5, refused: the action Give Units to Player, its fields from values +// The action Give Units to Player, how many from a value of the program. +program(() => { + let n = 1; + n += 1; + giveUnits(P1, P2, units.TerranMarine, n, locations.Anywhere); +}); diff --git a/ledger/game/give-units-type-from-a-value.ts b/ledger/game/give-units-type-from-a-value.ts new file mode 100644 index 0000000..b143ed5 --- /dev/null +++ b/ledger/game/give-units-type-from-a-value.ts @@ -0,0 +1,8 @@ +// ledger: step 5, refused: the action Give Units to Player, its fields from values +// The action Give Units to Player, the unit type from a value of the program. +program(() => { + let kind: UnitType = units.TerranMarine; + let wave = 3; + if (wave > 2) kind = units.TerranFirebat; + giveUnits(P1, P2, kind, "All", locations.Anywhere); +}); diff --git a/ledger/game/give-units.ts b/ledger/game/give-units.ts new file mode 100644 index 0000000..155cd33 --- /dev/null +++ b/ledger/game/give-units.ts @@ -0,0 +1,5 @@ +// ledger: step 5, refused: the action Give Units to Player +// The action Give Units to Player, all its fields known when the map is built. +program(() => { + giveUnits(P1, P2, units.TerranMarine, 1, locations.Anywhere); +}); diff --git a/ledger/game/has-left.ts b/ledger/game/has-left.ts new file mode 100644 index 0000000..8294724 --- /dev/null +++ b/ledger/game/has-left.ts @@ -0,0 +1,5 @@ +// ledger: step 5, refused: whether a player has left +// Whether a player has left the game: `hasLeft(P2)`. +program(() => { + if (hasLeft(P2)) print("P2 left"); +}); diff --git a/ledger/game/is-human.ts b/ledger/game/is-human.ts new file mode 100644 index 0000000..76345de --- /dev/null +++ b/ledger/game/is-human.ts @@ -0,0 +1,5 @@ +// ledger: step 5, refused: a player's slot +// Whether a person plays a slot: `isHuman(P2)`. +program(() => { + if (isHuman(P2)) print("two players"); +}); diff --git a/ledger/game/kill-unit-at-count-from-a-value.ts b/ledger/game/kill-unit-at-count-from-a-value.ts new file mode 100644 index 0000000..6f66fbe --- /dev/null +++ b/ledger/game/kill-unit-at-count-from-a-value.ts @@ -0,0 +1,7 @@ +// ledger: step 5, refused: the action Kill Unit At Location, its fields from values +// The action Kill Unit at Location, how many from a value of the program. +program(() => { + let n = 1; + n += 1; + killUnitAt(P2, units.ZergZergling, n, locations.Anywhere); +}); diff --git a/ledger/game/kill-unit-at-type-from-a-value.ts b/ledger/game/kill-unit-at-type-from-a-value.ts new file mode 100644 index 0000000..441e7be --- /dev/null +++ b/ledger/game/kill-unit-at-type-from-a-value.ts @@ -0,0 +1,8 @@ +// ledger: step 5, refused: the action Kill Unit At Location, its fields from values +// The action Kill Unit at Location, the unit type from a value of the program. +program(() => { + let kind: UnitType = units.ZergZergling; + let wave = 3; + if (wave > 2) kind = units.ZergHydralisk; + killUnitAt(P2, kind, "All", locations.Anywhere); +}); diff --git a/ledger/game/kill-unit-at.ts b/ledger/game/kill-unit-at.ts new file mode 100644 index 0000000..4384372 --- /dev/null +++ b/ledger/game/kill-unit-at.ts @@ -0,0 +1,5 @@ +// ledger: step 5, refused: the action Kill Unit At Location +// The action Kill Unit at Location, all its fields known when the map is built. +program(() => { + killUnitAt(P2, units.ZergZergling, "All", locations.Anywhere); +}); diff --git a/ledger/game/kill-unit-type-from-a-value.ts b/ledger/game/kill-unit-type-from-a-value.ts new file mode 100644 index 0000000..30af66f --- /dev/null +++ b/ledger/game/kill-unit-type-from-a-value.ts @@ -0,0 +1,8 @@ +// ledger: step 5, refused: the action Kill Unit, its fields from values +// The action Kill Unit, the unit type from a value of the program. +program(() => { + let kind: UnitType = units.ZergZergling; + let wave = 3; + if (wave > 2) kind = units.ZergHydralisk; + killUnit(P2, kind); +}); diff --git a/ledger/game/kill-unit.ts b/ledger/game/kill-unit.ts new file mode 100644 index 0000000..d170aa7 --- /dev/null +++ b/ledger/game/kill-unit.ts @@ -0,0 +1,5 @@ +// ledger: step 5, refused: the action Kill Unit +// The action Kill Unit: every unit of a type a player has dies. +program(() => { + killUnit(P2, units.ZergZergling); +}); diff --git a/ledger/game/leaderboard-computer-players.ts b/ledger/game/leaderboard-computer-players.ts new file mode 100644 index 0000000..bcd8ca4 --- /dev/null +++ b/ledger/game/leaderboard-computer-players.ts @@ -0,0 +1,5 @@ +// ledger: step 5, refused: the action Leaderboard Computer Players +// The action Leaderboard Computer Players: whether the computers show on the leader board. +program(() => { + leaderboardComputerPlayers("disable"); +}); diff --git a/ledger/game/leaderboard-control-at-type-from-a-value.ts b/ledger/game/leaderboard-control-at-type-from-a-value.ts new file mode 100644 index 0000000..175aa89 --- /dev/null +++ b/ledger/game/leaderboard-control-at-type-from-a-value.ts @@ -0,0 +1,8 @@ +// ledger: step 5, refused: the action Leader Board Control At Location, its fields from values +// The action Leader Board (Control at Location), the unit type from a value of the program. +program(() => { + let kind: UnitType = units.TerranMarine; + let wave = 3; + if (wave > 2) kind = units.TerranFirebat; + leaderboardControlAt("Men on the hill", kind, locations.Anywhere); +}); diff --git a/ledger/game/leaderboard-control-at.ts b/ledger/game/leaderboard-control-at.ts new file mode 100644 index 0000000..5417c0d --- /dev/null +++ b/ledger/game/leaderboard-control-at.ts @@ -0,0 +1,5 @@ +// ledger: step 5, refused: the action Leader Board Control At Location +// The action Leader Board (Control at Location): how many units of a type each player has at a location. +program(() => { + leaderboardControlAt("Marines on the hill", units.TerranMarine, locations.Anywhere); +}); diff --git a/ledger/game/leaderboard-control-type-from-a-value.ts b/ledger/game/leaderboard-control-type-from-a-value.ts new file mode 100644 index 0000000..ed0b0bf --- /dev/null +++ b/ledger/game/leaderboard-control-type-from-a-value.ts @@ -0,0 +1,8 @@ +// ledger: step 5, refused: the action Leader Board Control, its fields from values +// The action Leader Board (Control), the unit type from a value of the program. +program(() => { + let kind: UnitType = units.TerranMarine; + let wave = 3; + if (wave > 2) kind = units.TerranFirebat; + leaderboardControl("Men", kind); +}); diff --git a/ledger/game/leaderboard-control.ts b/ledger/game/leaderboard-control.ts new file mode 100644 index 0000000..4401730 --- /dev/null +++ b/ledger/game/leaderboard-control.ts @@ -0,0 +1,5 @@ +// ledger: step 5, refused: the action Leader Board Control +// The action Leader Board (Control): how many units of a type each player has, under a label. +program(() => { + leaderboardControl("Marines", units.TerranMarine); +}); diff --git a/ledger/game/leaderboard-goal-control-at-type-from-a-value.ts b/ledger/game/leaderboard-goal-control-at-type-from-a-value.ts new file mode 100644 index 0000000..3bade63 --- /dev/null +++ b/ledger/game/leaderboard-goal-control-at-type-from-a-value.ts @@ -0,0 +1,8 @@ +// ledger: step 5, refused: the action Leaderboard Goal Control At Location, its fields from values +// The action Leader Board Goal (Control at Location), the unit type from a value of the program. +program(() => { + let kind: UnitType = units.TerranMarine; + let wave = 3; + if (wave > 2) kind = units.TerranFirebat; + leaderboardGoalControlAt("Men on the hill", kind, 10, locations.Anywhere); +}); diff --git a/ledger/game/leaderboard-goal-control-at.ts b/ledger/game/leaderboard-goal-control-at.ts new file mode 100644 index 0000000..be893b6 --- /dev/null +++ b/ledger/game/leaderboard-goal-control-at.ts @@ -0,0 +1,5 @@ +// ledger: step 5, refused: the action Leaderboard Goal Control At Location +// The action Leader Board Goal (Control at Location): units of a type at a location, against a goal. +program(() => { + leaderboardGoalControlAt("Marines on the hill", units.TerranMarine, 10, locations.Anywhere); +}); diff --git a/ledger/game/leaderboard-goal-control-type-from-a-value.ts b/ledger/game/leaderboard-goal-control-type-from-a-value.ts new file mode 100644 index 0000000..4157661 --- /dev/null +++ b/ledger/game/leaderboard-goal-control-type-from-a-value.ts @@ -0,0 +1,8 @@ +// ledger: step 5, refused: the action Leaderboard Goal Control, its fields from values +// The action Leader Board Goal (Control), the unit type from a value of the program. +program(() => { + let kind: UnitType = units.TerranMarine; + let wave = 3; + if (wave > 2) kind = units.TerranFirebat; + leaderboardGoalControl("Men to 20", kind, 20); +}); diff --git a/ledger/game/leaderboard-goal-control.ts b/ledger/game/leaderboard-goal-control.ts new file mode 100644 index 0000000..f1b98a0 --- /dev/null +++ b/ledger/game/leaderboard-goal-control.ts @@ -0,0 +1,5 @@ +// ledger: step 5, refused: the action Leaderboard Goal Control +// The action Leader Board Goal (Control): units of a type each player has, against a goal. +program(() => { + leaderboardGoalControl("Marines to 20", units.TerranMarine, 20); +}); diff --git a/ledger/game/leaderboard-goal-kills-type-from-a-value.ts b/ledger/game/leaderboard-goal-kills-type-from-a-value.ts new file mode 100644 index 0000000..75fb080 --- /dev/null +++ b/ledger/game/leaderboard-goal-kills-type-from-a-value.ts @@ -0,0 +1,8 @@ +// ledger: step 5, refused: the action Leaderboard Goal Kills, its fields from values +// The action Leader Board Goal (Kills), the unit type from a value of the program. +program(() => { + let kind: UnitType = units.ZergZergling; + let wave = 3; + if (wave > 2) kind = units.ZergHydralisk; + leaderboardGoalKills("Kill 50", kind, 50); +}); diff --git a/ledger/game/leaderboard-goal-kills.ts b/ledger/game/leaderboard-goal-kills.ts new file mode 100644 index 0000000..409dfe9 --- /dev/null +++ b/ledger/game/leaderboard-goal-kills.ts @@ -0,0 +1,5 @@ +// ledger: step 5, refused: the action Leaderboard Goal Kills +// The action Leader Board Goal (Kills): units of a type each player has killed, against a goal. +program(() => { + leaderboardGoalKills("Kill 50 Zerglings", units.ZergZergling, 50); +}); diff --git a/ledger/game/leaderboard-goal-points.ts b/ledger/game/leaderboard-goal-points.ts new file mode 100644 index 0000000..230a361 --- /dev/null +++ b/ledger/game/leaderboard-goal-points.ts @@ -0,0 +1,5 @@ +// ledger: step 5, refused: the action Leaderboard Goal Points +// The action Leader Board Goal (Points): a score of each player, against a goal. +program(() => { + leaderboardGoalPoints("Kills to 1000", "kills", 1000); +}); diff --git a/ledger/game/leaderboard-goal-resources.ts b/ledger/game/leaderboard-goal-resources.ts new file mode 100644 index 0000000..19d7c1e --- /dev/null +++ b/ledger/game/leaderboard-goal-resources.ts @@ -0,0 +1,5 @@ +// ledger: step 5, refused: the action Leaderboard Goal Resources +// The action Leader Board Goal (Resources): each player's resources, against a goal. +program(() => { + leaderboardGoalResources("Gas to 800", 800, "gas"); +}); diff --git a/ledger/game/leaderboard-greed.ts b/ledger/game/leaderboard-greed.ts new file mode 100644 index 0000000..f329807 --- /dev/null +++ b/ledger/game/leaderboard-greed.ts @@ -0,0 +1,5 @@ +// ledger: step 5, refused: the action Leader Board Greed +// The action Leader Board Greed: who first gathers so many minerals and gas. +program(() => { + leaderboardGreed(5000); +}); diff --git a/ledger/game/leaderboard-kills-type-from-a-value.ts b/ledger/game/leaderboard-kills-type-from-a-value.ts new file mode 100644 index 0000000..dff43bd --- /dev/null +++ b/ledger/game/leaderboard-kills-type-from-a-value.ts @@ -0,0 +1,8 @@ +// ledger: step 5, refused: the action Leader Board Kills, its fields from values +// The action Leader Board (Kills), the unit type from a value of the program. +program(() => { + let kind: UnitType = units.ZergZergling; + let wave = 3; + if (wave > 2) kind = units.ZergHydralisk; + leaderboardKills("Kills", kind); +}); diff --git a/ledger/game/leaderboard-kills.ts b/ledger/game/leaderboard-kills.ts new file mode 100644 index 0000000..7f5da8e --- /dev/null +++ b/ledger/game/leaderboard-kills.ts @@ -0,0 +1,5 @@ +// ledger: step 5, refused: the action Leader Board Kills +// The action Leader Board (Kills): how many units of a type each player has killed. +program(() => { + leaderboardKills("Zerglings killed", units.ZergZergling); +}); diff --git a/ledger/game/leaderboard-label-from-the-program.ts b/ledger/game/leaderboard-label-from-the-program.ts new file mode 100644 index 0000000..963cf3e --- /dev/null +++ b/ledger/game/leaderboard-label-from-the-program.ts @@ -0,0 +1,7 @@ +// ledger: step 5, refused: the action Leader Board Control, its text the program's +// A leader board's label made while the game runs: a number of the program in it. +program(() => { + let goal = 10; + goal += 5; + leaderboardControl(`Marines (goal ${goal})`, units.TerranMarine); +}); diff --git a/ledger/game/leaderboard-points.ts b/ledger/game/leaderboard-points.ts new file mode 100644 index 0000000..cbe13d5 --- /dev/null +++ b/ledger/game/leaderboard-points.ts @@ -0,0 +1,5 @@ +// ledger: step 5, refused: the action Leader Board Points +// The action Leader Board (Points): a score of each player. +program(() => { + leaderboardPoints("Score", "total"); +}); diff --git a/ledger/game/leaderboard-resources.ts b/ledger/game/leaderboard-resources.ts new file mode 100644 index 0000000..98198eb --- /dev/null +++ b/ledger/game/leaderboard-resources.ts @@ -0,0 +1,5 @@ +// ledger: step 5, refused: the action Leader Board Resources +// The action Leader Board (Resources): each player's minerals, gas or both. +program(() => { + leaderboardResources("Minerals", "ore"); +}); diff --git a/ledger/game/minimap-ping.ts b/ledger/game/minimap-ping.ts new file mode 100644 index 0000000..78ee02e --- /dev/null +++ b/ledger/game/minimap-ping.ts @@ -0,0 +1,5 @@ +// ledger: step 5, refused: the action Minimap Ping +// The action Minimap Ping: a flash on the minimap at a location. +program(() => { + minimapPing(locations.Anywhere); +}); diff --git a/ledger/game/modify-energy-type-from-a-value.ts b/ledger/game/modify-energy-type-from-a-value.ts new file mode 100644 index 0000000..065f201 --- /dev/null +++ b/ledger/game/modify-energy-type-from-a-value.ts @@ -0,0 +1,8 @@ +// ledger: step 5, refused: the action Modify Unit Energy, its fields from values +// The action Modify Unit Energy, the unit type from a value of the program. +program(() => { + let kind: UnitType = units.TerranGhost; + let wave = 3; + if (wave > 2) kind = units.TerranMedic; + modifyEnergy(P1, kind, 100, "All", locations.Anywhere); +}); diff --git a/ledger/game/modify-energy.ts b/ledger/game/modify-energy.ts new file mode 100644 index 0000000..81c13e5 --- /dev/null +++ b/ledger/game/modify-energy.ts @@ -0,0 +1,5 @@ +// ledger: step 5, refused: the action Modify Unit Energy +// The action Modify Unit Energy: units at a location get a percentage of their energy. +program(() => { + modifyEnergy(P1, units.TerranGhost, 100, "All", locations.Anywhere); +}); diff --git a/ledger/game/modify-hangar-count-type-from-a-value.ts b/ledger/game/modify-hangar-count-type-from-a-value.ts new file mode 100644 index 0000000..c3596b8 --- /dev/null +++ b/ledger/game/modify-hangar-count-type-from-a-value.ts @@ -0,0 +1,8 @@ +// ledger: step 5, refused: the action Modify Unit Hanger Count, its fields from values +// The action Modify Unit Hanger Count, the unit type from a value of the program. +program(() => { + let kind: UnitType = units.ProtossCarrier; + let wave = 3; + if (wave > 2) kind = units.ProtossReaver; + modifyHangarCount(P1, kind, 4, "All", locations.Anywhere); +}); diff --git a/ledger/game/modify-hangar-count.ts b/ledger/game/modify-hangar-count.ts new file mode 100644 index 0000000..3f68335 --- /dev/null +++ b/ledger/game/modify-hangar-count.ts @@ -0,0 +1,5 @@ +// ledger: step 5, refused: the action Modify Unit Hanger Count +// The action Modify Unit Hanger Count: Interceptors or Scarabs added to the units at a location. +program(() => { + modifyHangarCount(P1, units.ProtossCarrier, 4, "All", locations.Anywhere); +}); diff --git a/ledger/game/modify-hit-points-type-from-a-value.ts b/ledger/game/modify-hit-points-type-from-a-value.ts new file mode 100644 index 0000000..64ba22c --- /dev/null +++ b/ledger/game/modify-hit-points-type-from-a-value.ts @@ -0,0 +1,8 @@ +// ledger: step 5, refused: the action Modify Unit Hit Points, its fields from values +// The action Modify Unit Hit Points, the unit type from a value of the program. +program(() => { + let kind: UnitType = units.TerranMarine; + let wave = 3; + if (wave > 2) kind = units.TerranFirebat; + modifyHitPoints(P1, kind, 100, "All", locations.Anywhere); +}); diff --git a/ledger/game/modify-hit-points.ts b/ledger/game/modify-hit-points.ts new file mode 100644 index 0000000..085e767 --- /dev/null +++ b/ledger/game/modify-hit-points.ts @@ -0,0 +1,5 @@ +// ledger: step 5, refused: the action Modify Unit Hit Points +// The action Modify Unit Hit Points: units at a location get a percentage of their hit points. +program(() => { + modifyHitPoints(P2, units.ZergZergling, 50, "All", locations.Anywhere); +}); diff --git a/ledger/game/modify-resource-amount.ts b/ledger/game/modify-resource-amount.ts new file mode 100644 index 0000000..a0989ae --- /dev/null +++ b/ledger/game/modify-resource-amount.ts @@ -0,0 +1,5 @@ +// ledger: step 5, refused: the action Modify Unit Resource Amount +// The action Modify Unit Resource Amount: what the mineral fields and geysers at a location hold. +program(() => { + modifyResourceAmount(players.Neutral, 500, "All", locations.Anywhere); +}); diff --git a/ledger/game/modify-shields-type-from-a-value.ts b/ledger/game/modify-shields-type-from-a-value.ts new file mode 100644 index 0000000..d9da344 --- /dev/null +++ b/ledger/game/modify-shields-type-from-a-value.ts @@ -0,0 +1,8 @@ +// ledger: step 5, refused: the action Modify Unit Shield Points, its fields from values +// The action Modify Unit Shield Points, the unit type from a value of the program. +program(() => { + let kind: UnitType = units.ProtossZealot; + let wave = 3; + if (wave > 2) kind = units.ProtossDragoon; + modifyShields(P1, kind, 100, "All", locations.Anywhere); +}); diff --git a/ledger/game/modify-shields.ts b/ledger/game/modify-shields.ts new file mode 100644 index 0000000..84ab4a0 --- /dev/null +++ b/ledger/game/modify-shields.ts @@ -0,0 +1,5 @@ +// ledger: step 5, refused: the action Modify Unit Shield Points +// The action Modify Unit Shield Points: units at a location get a percentage of their shields. +program(() => { + modifyShields(P1, units.ProtossZealot, 100, "All", locations.Anywhere); +}); diff --git a/ledger/game/move-location-type-from-a-value.ts b/ledger/game/move-location-type-from-a-value.ts new file mode 100644 index 0000000..0f2188b --- /dev/null +++ b/ledger/game/move-location-type-from-a-value.ts @@ -0,0 +1,8 @@ +// ledger: step 5, refused: the action Move Location, its fields from values +// The action Move Location, the unit type from a value of the program. +program(() => { + let kind: UnitType = units.TerranMarine; + let wave = 3; + if (wave > 2) kind = units.TerranFirebat; + moveLocation(P1, kind, locations.Anywhere, locations.Anywhere); +}); diff --git a/ledger/game/move-location.ts b/ledger/game/move-location.ts new file mode 100644 index 0000000..bce13ac --- /dev/null +++ b/ledger/game/move-location.ts @@ -0,0 +1,5 @@ +// ledger: step 5, refused: the action Move Location +// The action Move Location: a location centred on a unit of a type at another location. +program(() => { + moveLocation(P1, units.TerranMarine, locations.Anywhere, locations.Anywhere); +}); diff --git a/ledger/game/move-unit-type-from-a-value.ts b/ledger/game/move-unit-type-from-a-value.ts new file mode 100644 index 0000000..ce36558 --- /dev/null +++ b/ledger/game/move-unit-type-from-a-value.ts @@ -0,0 +1,8 @@ +// ledger: step 5, refused: the action Move Unit, its fields from values +// The action Move Unit, the unit type from a value of the program. +program(() => { + let kind: UnitType = units.TerranMarine; + let wave = 3; + if (wave > 2) kind = units.TerranFirebat; + moveUnit(P1, kind, "All", locations.Anywhere, locations.Anywhere); +}); diff --git a/ledger/game/move-unit.ts b/ledger/game/move-unit.ts new file mode 100644 index 0000000..2620546 --- /dev/null +++ b/ledger/game/move-unit.ts @@ -0,0 +1,5 @@ +// ledger: step 5, refused: the action Move Unit +// The action Move Unit: units taken from one location to another at once. +program(() => { + moveUnit(P1, units.TerranMarine, 4, locations.Anywhere, locations.Anywhere); +}); diff --git a/ledger/game/mute-unit-speech.ts b/ledger/game/mute-unit-speech.ts new file mode 100644 index 0000000..48e10c4 --- /dev/null +++ b/ledger/game/mute-unit-speech.ts @@ -0,0 +1,5 @@ +// ledger: step 5, refused: the action Mute Unit Speech +// The action Mute Unit Speech: the units' voices quiet, for a cinematic. +program(() => { + muteUnitSpeech(); +}); diff --git a/ledger/game/not-of-a-condition.ts b/ledger/game/not-of-a-condition.ts new file mode 100644 index 0000000..fb89937 --- /dev/null +++ b/ledger/game/not-of-a-condition.ts @@ -0,0 +1,6 @@ +// ledger: step 5, refused: the condition Bring +// A condition turned around by `not(…)`, worked out when the map is built: a program tests the +// flipped condition ("at least 1" becomes "at most 0"). +program(() => { + if (not(bring(P1, units.AnyUnit, locations.Anywhere, ">=", 1))) print("yes"); +}); diff --git a/ledger/game/once.ts b/ledger/game/once.ts new file mode 100644 index 0000000..cc9665c --- /dev/null +++ b/ledger/game/once.ts @@ -0,0 +1,11 @@ +// ledger: step 5, refused: once() +// `once(…)`: true the first time its test holds, never again. +program(() => { + let n = 0; + let high = false; + while (n < 6) { + high = n > 2; + if (once(high)) print(`first at ${n}`); + n += 1; + } +}); diff --git a/ledger/game/order.ts b/ledger/game/order.ts new file mode 100644 index 0000000..0af5271 --- /dev/null +++ b/ledger/game/order.ts @@ -0,0 +1,7 @@ +// ledger: step 5, refused: the action Order +// The action Order: units at a location sent to another, to move, patrol or attack. (Built outside the +// program and stood as a statement inside it: written inside, `order(…)` does not compile.) +const charge = order(P1, units.TerranMarine, locations.Anywhere, locations.Anywhere, "attack"); +program(() => { + charge; +}); diff --git a/ledger/game/pause-game.ts b/ledger/game/pause-game.ts new file mode 100644 index 0000000..0639bfa --- /dev/null +++ b/ledger/game/pause-game.ts @@ -0,0 +1,5 @@ +// ledger: step 5, refused: the action Pause Game +// The action Pause Game (single player only). +program(() => { + pauseGame(); +}); diff --git a/ledger/game/pause-timer.ts b/ledger/game/pause-timer.ts new file mode 100644 index 0000000..a312727 --- /dev/null +++ b/ledger/game/pause-timer.ts @@ -0,0 +1,5 @@ +// ledger: step 5, refused: the action Pause Timer +// The action Pause Timer: the countdown timer stops. +program(() => { + pauseTimer(); +}); diff --git a/ledger/game/read-accumulate.ts b/ledger/game/read-accumulate.ts new file mode 100644 index 0000000..8099e02 --- /dev/null +++ b/ledger/game/read-accumulate.ts @@ -0,0 +1,6 @@ +// ledger: step 5, refused: a read of the game (Accumulate) +// A condition that compares a quantity, read as a value: `accumulate(P1, "gas")`. +program(() => { + const n = accumulate(P1, "gas"); + print(`${n}`); +}); diff --git a/ledger/game/read-bring.ts b/ledger/game/read-bring.ts new file mode 100644 index 0000000..d1ebc8c --- /dev/null +++ b/ledger/game/read-bring.ts @@ -0,0 +1,6 @@ +// ledger: step 5, refused: a read of the game (Bring) +// A condition that compares a quantity, read as a value: `bring(P1, unit, location)`. +program(() => { + const n = bring(P1, units.AnyUnit, locations.Anywhere); + print(`${n}`); +}); diff --git a/ledger/game/read-command.ts b/ledger/game/read-command.ts new file mode 100644 index 0000000..1a61f2e --- /dev/null +++ b/ledger/game/read-command.ts @@ -0,0 +1,6 @@ +// ledger: step 5, refused: a read of the game (Command) +// A condition that compares a quantity, read as a value: `command(P1, unit)`. +program(() => { + const n = command(P1, units.TerranMarine); + print(`${n}`); +}); diff --git a/ledger/game/read-count-units-at.ts b/ledger/game/read-count-units-at.ts new file mode 100644 index 0000000..9cf0641 --- /dev/null +++ b/ledger/game/read-count-units-at.ts @@ -0,0 +1,6 @@ +// ledger: step 5, refused: a read of the game (Bring) +// How many units of a type a player has at a location, read when the line runs: `countUnits(P1, …, location)`. +program(() => { + const n = countUnits(P1, units.TerranMarine, locations.Anywhere); + print(`${n}`); +}); diff --git a/ledger/game/read-count-units.ts b/ledger/game/read-count-units.ts new file mode 100644 index 0000000..60c2b5d --- /dev/null +++ b/ledger/game/read-count-units.ts @@ -0,0 +1,6 @@ +// ledger: step 5, refused: a read of the game (Command) +// How many units of a type a player has anywhere, read when the line runs: `countUnits(P1, …)`. +program(() => { + const n = countUnits(P1, units.TerranMarine); + print(`${n}`); +}); diff --git a/ledger/game/read-countdown-timer.ts b/ledger/game/read-countdown-timer.ts new file mode 100644 index 0000000..e56bafe --- /dev/null +++ b/ledger/game/read-countdown-timer.ts @@ -0,0 +1,6 @@ +// ledger: step 5, refused: a read of the game (Countdown Timer) +// A condition that compares a quantity, read as a value: `countdownTimer()`. +program(() => { + const n = countdownTimer(); + print(`${n}`); +}); diff --git a/ledger/game/read-countdown.ts b/ledger/game/read-countdown.ts new file mode 100644 index 0000000..1d5aa7b --- /dev/null +++ b/ledger/game/read-countdown.ts @@ -0,0 +1,6 @@ +// ledger: step 5, refused: a read of the game (Countdown Timer) +// The countdown timer in game seconds, read when the line runs: `countdown()`. +program(() => { + const n = countdown(); + print(`${n}`); +}); diff --git a/ledger/game/read-deaths.ts b/ledger/game/read-deaths.ts new file mode 100644 index 0000000..493c410 --- /dev/null +++ b/ledger/game/read-deaths.ts @@ -0,0 +1,6 @@ +// ledger: step 5, refused: a read of the game (Deaths) +// The death counter of a player and a unit type, read as a value: `deaths(P1, unit)`. +program(() => { + const n = deaths(P1, units.TerranMarine); + print(`${n}`); +}); diff --git a/ledger/game/read-elapsed-time.ts b/ledger/game/read-elapsed-time.ts new file mode 100644 index 0000000..dd535da --- /dev/null +++ b/ledger/game/read-elapsed-time.ts @@ -0,0 +1,6 @@ +// ledger: step 5, refused: a read of the game (Elapsed Time) +// A condition that compares a quantity, read as a value: `elapsedTime()`. +program(() => { + const n = elapsedTime(); + print(`${n}`); +}); diff --git a/ledger/game/read-elapsed.ts b/ledger/game/read-elapsed.ts new file mode 100644 index 0000000..236938e --- /dev/null +++ b/ledger/game/read-elapsed.ts @@ -0,0 +1,6 @@ +// ledger: step 5, refused: a read of the game (Elapsed Time) +// Game seconds since the start, read when the line runs: `elapsed()`. +program(() => { + const n = elapsed(); + print(`${n}`); +}); diff --git a/ledger/game/read-gas.ts b/ledger/game/read-gas.ts new file mode 100644 index 0000000..4ca1c47 --- /dev/null +++ b/ledger/game/read-gas.ts @@ -0,0 +1,6 @@ +// ledger: step 5, refused: a read of the game (Accumulate) +// A player's gas read when the line runs: `gas(P1)`. +program(() => { + const n = gas(P1); + print(`${n}`); +}); diff --git a/ledger/game/read-in-arithmetic.ts b/ledger/game/read-in-arithmetic.ts new file mode 100644 index 0000000..c6e09c3 --- /dev/null +++ b/ledger/game/read-in-arithmetic.ts @@ -0,0 +1,6 @@ +// ledger: step 5, refused: a read of the game (Accumulate) +// A read of the game in the program's arithmetic: `minerals(P1) - gas(P1)`. +program(() => { + const n = minerals(P1) - gas(P1); + print(`${n}`); +}); diff --git a/ledger/game/read-kill.ts b/ledger/game/read-kill.ts new file mode 100644 index 0000000..861b4c7 --- /dev/null +++ b/ledger/game/read-kill.ts @@ -0,0 +1,6 @@ +// ledger: step 5, refused: a read of the game (Kill) +// A condition that compares a quantity, read as a value: `kill(P1, unit)`. +program(() => { + const n = kill(P1, units.ZergZergling); + print(`${n}`); +}); diff --git a/ledger/game/read-kills.ts b/ledger/game/read-kills.ts new file mode 100644 index 0000000..dd6728f --- /dev/null +++ b/ledger/game/read-kills.ts @@ -0,0 +1,6 @@ +// ledger: step 5, refused: a read of the game (Kill) +// How many units of a type a player has killed, read when the line runs: `kills(P1, …)`. +program(() => { + const n = kills(P1, units.ZergZergling); + print(`${n}`); +}); diff --git a/ledger/game/read-minerals.ts b/ledger/game/read-minerals.ts new file mode 100644 index 0000000..c42ae49 --- /dev/null +++ b/ledger/game/read-minerals.ts @@ -0,0 +1,6 @@ +// ledger: step 5, refused: a read of the game (Accumulate) +// A player's minerals read when the line runs: `minerals(P1)`. +program(() => { + const n = minerals(P1); + print(`${n}`); +}); diff --git a/ledger/game/read-opponents.ts b/ledger/game/read-opponents.ts new file mode 100644 index 0000000..b557098 --- /dev/null +++ b/ledger/game/read-opponents.ts @@ -0,0 +1,6 @@ +// ledger: step 5, refused: a read of the game (Opponents) +// How many opponents a player has left, read as a value: `opponents(P1)`. +program(() => { + const n = opponents(P1); + print(`${n}`); +}); diff --git a/ledger/game/read-race.ts b/ledger/game/read-race.ts new file mode 100644 index 0000000..ed1c40d --- /dev/null +++ b/ledger/game/read-race.ts @@ -0,0 +1,5 @@ +// ledger: step 5, refused: a player's race +// The race a player plays, as one of `races.*`: `race(P1) == races.Zerg`. +program(() => { + if (race(P1) == races.Zerg) print("Zerg"); +}); diff --git a/ledger/game/read-resources.ts b/ledger/game/read-resources.ts new file mode 100644 index 0000000..d5f787e --- /dev/null +++ b/ledger/game/read-resources.ts @@ -0,0 +1,6 @@ +// ledger: step 5, refused: a read of the game (Accumulate) +// A player's minerals and gas added up, read when the line runs: `resources(P1, "oreAndGas")`. +program(() => { + const n = resources(P1, "oreAndGas"); + print(`${n}`); +}); diff --git a/ledger/game/read-score.ts b/ledger/game/read-score.ts new file mode 100644 index 0000000..d492c34 --- /dev/null +++ b/ledger/game/read-score.ts @@ -0,0 +1,6 @@ +// ledger: step 5, refused: a read of the game (Score) +// A player's score of a kind, read as a value: `score(P1, "kills")`. +program(() => { + const n = score(P1, "kills"); + print(`${n}`); +}); diff --git a/ledger/game/read-slot.ts b/ledger/game/read-slot.ts new file mode 100644 index 0000000..3222cc7 --- /dev/null +++ b/ledger/game/read-slot.ts @@ -0,0 +1,5 @@ +// ledger: step 5, refused: a player's slot +// What holds a player's slot, as one of `slots.*`: `slot(P3) == slots.Computer`. +program(() => { + if (slot(P3) == slots.Computer) print("a computer"); +}); diff --git a/ledger/game/read-supply-max.ts b/ledger/game/read-supply-max.ts new file mode 100644 index 0000000..1ece890 --- /dev/null +++ b/ledger/game/read-supply-max.ts @@ -0,0 +1,6 @@ +// ledger: step 5, refused: a player's supply (max) +// A player's supply cap, read when the line runs: `supply(P1, "max")`. +program(() => { + const n = supply(P1, "max"); + print(`${n}`); +}); diff --git a/ledger/game/read-supply-provided-of-a-race.ts b/ledger/game/read-supply-provided-of-a-race.ts new file mode 100644 index 0000000..df17a1f --- /dev/null +++ b/ledger/game/read-supply-provided-of-a-race.ts @@ -0,0 +1,6 @@ +// ledger: step 5, refused: a player's supply (provided) +// The supply a player's depots, overlords or pylons provide, of one race: `supply(P1, "provided", races.Zerg)`. +program(() => { + const n = supply(P1, "provided", races.Zerg); + print(`${n}`); +}); diff --git a/ledger/game/read-supply-used.ts b/ledger/game/read-supply-used.ts new file mode 100644 index 0000000..dd58463 --- /dev/null +++ b/ledger/game/read-supply-used.ts @@ -0,0 +1,6 @@ +// ledger: step 5, refused: a player's supply (used) +// A player's supply used, of the race they play, read when the line runs: `supply(P1)`. +program(() => { + const n = supply(P1); + print(`${n}`); +}); diff --git a/ledger/game/remove-unit-at-count-from-a-value.ts b/ledger/game/remove-unit-at-count-from-a-value.ts new file mode 100644 index 0000000..54a102b --- /dev/null +++ b/ledger/game/remove-unit-at-count-from-a-value.ts @@ -0,0 +1,7 @@ +// ledger: step 5, refused: the action Remove Unit At Location, its fields from values +// The action Remove Unit at Location, how many from a value of the program. +program(() => { + let n = 1; + n += 1; + removeUnitAt(P2, units.ZergZergling, n, locations.Anywhere); +}); diff --git a/ledger/game/remove-unit-at-type-from-a-value.ts b/ledger/game/remove-unit-at-type-from-a-value.ts new file mode 100644 index 0000000..1c0bc52 --- /dev/null +++ b/ledger/game/remove-unit-at-type-from-a-value.ts @@ -0,0 +1,8 @@ +// ledger: step 5, refused: the action Remove Unit At Location, its fields from values +// The action Remove Unit at Location, the unit type from a value of the program. +program(() => { + let kind: UnitType = units.ZergZergling; + let wave = 3; + if (wave > 2) kind = units.ZergHydralisk; + removeUnitAt(P2, kind, "All", locations.Anywhere); +}); diff --git a/ledger/game/remove-unit-at.ts b/ledger/game/remove-unit-at.ts new file mode 100644 index 0000000..2f558db --- /dev/null +++ b/ledger/game/remove-unit-at.ts @@ -0,0 +1,5 @@ +// ledger: step 5, refused: the action Remove Unit At Location +// The action Remove Unit at Location, all its fields known when the map is built. +program(() => { + removeUnitAt(P2, units.ZergZergling, 2, locations.Anywhere); +}); diff --git a/ledger/game/remove-unit-type-from-a-value.ts b/ledger/game/remove-unit-type-from-a-value.ts new file mode 100644 index 0000000..48f8364 --- /dev/null +++ b/ledger/game/remove-unit-type-from-a-value.ts @@ -0,0 +1,8 @@ +// ledger: step 5, refused: the action Remove Unit, its fields from values +// The action Remove Unit, the unit type from a value of the program. +program(() => { + let kind: UnitType = units.ZergZergling; + let wave = 3; + if (wave > 2) kind = units.ZergHydralisk; + removeUnit(P2, kind); +}); diff --git a/ledger/game/remove-unit.ts b/ledger/game/remove-unit.ts new file mode 100644 index 0000000..332dc31 --- /dev/null +++ b/ledger/game/remove-unit.ts @@ -0,0 +1,5 @@ +// ledger: step 5, refused: the action Remove Unit +// The action Remove Unit: every unit of a type a player has taken off the map, without a death. +program(() => { + removeUnit(P2, units.ZergZergling); +}); diff --git a/ledger/game/reserve-location.ts b/ledger/game/reserve-location.ts new file mode 100644 index 0000000..f68a255 --- /dev/null +++ b/ledger/game/reserve-location.ts @@ -0,0 +1,8 @@ +// ledger: step 0, takes +// A location the map does not use, taken when the map is built: `reserveLocation(why)`. Worked out when +// the map is built: the program has its number. +const spot = reserveLocation("ledger: a spot of the library's own"); +program(() => { + let n = spot; + print(`location ${n}`); +}); diff --git a/ledger/game/rose.ts b/ledger/game/rose.ts new file mode 100644 index 0000000..602438f --- /dev/null +++ b/ledger/game/rose.ts @@ -0,0 +1,11 @@ +// ledger: step 5, refused: rose() +// `rose(…)`: true on the frame its test becomes true, false until it becomes false and true again. +program(() => { + let n = 0; + let high = false; + while (n < 6) { + high = n > 2; + if (rose(high)) print(`up at ${n}`); + n += 1; + } +}); diff --git a/ledger/game/run-ai-script-at.ts b/ledger/game/run-ai-script-at.ts new file mode 100644 index 0000000..ef211aa --- /dev/null +++ b/ledger/game/run-ai-script-at.ts @@ -0,0 +1,5 @@ +// ledger: step 5, refused: the action Run AI Script At Location +// The action Run AI Script at Location. +program(() => { + runAiScriptAt(aiScripts.ZergCustomLevel, locations.Anywhere); +}); diff --git a/ledger/game/run-ai-script.ts b/ledger/game/run-ai-script.ts new file mode 100644 index 0000000..d908645 --- /dev/null +++ b/ledger/game/run-ai-script.ts @@ -0,0 +1,5 @@ +// ledger: step 5, refused: the action Run AI Script +// The action Run AI Script: one of the game's AI scripts, by name (`aiScripts.*`). +program(() => { + runAiScript(aiScripts.TerranCustomLevel); +}); diff --git a/ledger/game/screen-max-pixels.ts b/ledger/game/screen-max-pixels.ts new file mode 100644 index 0000000..f34c9bc --- /dev/null +++ b/ledger/game/screen-max-pixels.ts @@ -0,0 +1,7 @@ +// ledger: step 0, takes +// The screen library's most pixels a screen has, `MAX_PIXELS`: a number known when the map is built. +import { MAX_PIXELS } from "trigscript/screen"; +program(() => { + let rows = MAX_PIXELS >> 5; + print(`${rows} rows of 32`); +}); diff --git a/ledger/game/screen-owners.ts b/ledger/game/screen-owners.ts new file mode 100644 index 0000000..7386258 --- /dev/null +++ b/ledger/game/screen-owners.ts @@ -0,0 +1,9 @@ +// ledger: step 6, refused: a write of player.color +// The screen library's pixel owners, `OWNERS`, a player a colour: each given the colour of its pixels. +import { OWNERS } from "trigscript/screen"; +const LOOK = ["teal", "yellow", "purple", "green", "red", "blue", "orange"] as const; +program(() => { + OWNERS.forEach((owner, k) => { + stats(owner).color = LOOK[k]; + }); +}); diff --git a/ledger/game/screen-sprite.ts b/ledger/game/screen-sprite.ts new file mode 100644 index 0000000..2e1aaf7 --- /dev/null +++ b/ledger/game/screen-sprite.ts @@ -0,0 +1,11 @@ +// ledger: step 6, refused: a product of two values +// The screen library: a picture made of rows of text when the map is built (`sprite`), drawn on a screen +// where the program says (`blit`). +import { Screen, sprite } from "trigscript/screen"; +const SHIP = sprite([".1.", "111", "1.1"]); +program(async () => { + const screen = new Screen(4, 4, 8, 8); + let x = 2; + screen.blit(SHIP, x, 4); + screen.present(); +}); diff --git a/ledger/game/screen-text-colors.tsx b/ledger/game/screen-text-colors.tsx new file mode 100644 index 0000000..de1974f --- /dev/null +++ b/ledger/game/screen-text-colors.tsx @@ -0,0 +1,8 @@ +// ledger: step 8, refused: JSX made while the game runs +// The screen library's colours in the text lines, `TEXT_COLORS`: a pixel's colour, 1 to 7, as a square of text. +import { TEXT_COLORS } from "trigscript/screen"; +program(async () => { + let c = 2; + c += 1; + print(); +}); diff --git a/ledger/game/screen-view.tsx b/ledger/game/screen-view.tsx new file mode 100644 index 0000000..918d717 --- /dev/null +++ b/ledger/game/screen-view.tsx @@ -0,0 +1,10 @@ +// ledger: step 8, refused: JSX made while the game runs +// The screen library: a screen's pixels shown in the text lines, a coloured square each: +// ``. +import { Screen, ScreenView } from "trigscript/screen"; +program(async () => { + const screen = new Screen(0, 0, 4, 3); + let x = 1; + screen.set(x, 1, 3); + print(); +}); diff --git a/ledger/game/screen.ts b/ledger/game/screen.ts new file mode 100644 index 0000000..cc9171b --- /dev/null +++ b/ledger/game/screen.ts @@ -0,0 +1,10 @@ +// ledger: step 6, refused: a location moved (centerLocation) +// The screen library: a grid of pixels drawn by the program and shown on the map as units, a pixel +// each: `new Screen(x, y, width, height)`, `set`, `present()`. +import { Screen } from "trigscript/screen"; +program(async () => { + const screen = new Screen(4, 4, 8, 8); + let x = 2; + screen.set(x, 3, 2); + screen.present(); +}); diff --git a/ledger/game/set-alliance-status.ts b/ledger/game/set-alliance-status.ts new file mode 100644 index 0000000..ba56a4b --- /dev/null +++ b/ledger/game/set-alliance-status.ts @@ -0,0 +1,5 @@ +// ledger: step 5, refused: the action Set Alliance Status +// The action Set Alliance Status: the current player's stance toward another, ally or enemy. +program(() => { + setAllianceStatus(P2, "ally"); +}); diff --git a/ledger/game/set-countdown-timer-from-a-value.ts b/ledger/game/set-countdown-timer-from-a-value.ts new file mode 100644 index 0000000..5959fb7 --- /dev/null +++ b/ledger/game/set-countdown-timer-from-a-value.ts @@ -0,0 +1,7 @@ +// ledger: step 5, refused: the action Set Countdown Timer, its fields from values +// The action Set Countdown Timer, the seconds from a value of the program. +program(() => { + let seconds = 60; + seconds += 30; + setCountdownTimer("set", seconds); +}); diff --git a/ledger/game/set-countdown-timer.ts b/ledger/game/set-countdown-timer.ts new file mode 100644 index 0000000..614b059 --- /dev/null +++ b/ledger/game/set-countdown-timer.ts @@ -0,0 +1,5 @@ +// ledger: step 5, refused: the action Set Countdown Timer +// The action Set Countdown Timer, its seconds known when the map is built. +program(() => { + setCountdownTimer("set", 90); +}); diff --git a/ledger/game/set-deaths-amount-from-a-value.ts b/ledger/game/set-deaths-amount-from-a-value.ts new file mode 100644 index 0000000..389ea60 --- /dev/null +++ b/ledger/game/set-deaths-amount-from-a-value.ts @@ -0,0 +1,7 @@ +// ledger: step 5, refused: the action Set Deaths, its fields from values +// The action Set Deaths, the amount from a value of the program. +program(() => { + let n = 4; + n += 1; + setDeaths(P1, units.TerranMarine, "set", n); +}); diff --git a/ledger/game/set-deaths-unit-from-a-value.ts b/ledger/game/set-deaths-unit-from-a-value.ts new file mode 100644 index 0000000..2405a94 --- /dev/null +++ b/ledger/game/set-deaths-unit-from-a-value.ts @@ -0,0 +1,8 @@ +// ledger: step 5, refused: the action Set Deaths, its fields from values +// The action Set Deaths, the unit type (which counter) from a value of the program. +program(() => { + let kind: UnitType = units.TerranMarine; + let wave = 3; + if (wave > 2) kind = units.TerranFirebat; + setDeaths(P1, kind, "add", 1); +}); diff --git a/ledger/game/set-deaths.ts b/ledger/game/set-deaths.ts new file mode 100644 index 0000000..c4c40fa --- /dev/null +++ b/ledger/game/set-deaths.ts @@ -0,0 +1,5 @@ +// ledger: step 5, refused: the action Set Deaths +// The action Set Deaths, all its fields known when the map is built: the counter a map keeps in the death table. +program(() => { + setDeaths(P1, units.TerranMarine, "add", 1); +}); diff --git a/ledger/game/set-doodad-state-type-from-a-value.ts b/ledger/game/set-doodad-state-type-from-a-value.ts new file mode 100644 index 0000000..2d7e642 --- /dev/null +++ b/ledger/game/set-doodad-state-type-from-a-value.ts @@ -0,0 +1,8 @@ +// ledger: step 5, refused: the action Set Doodad State, its fields from values +// The action Set Doodad State, the unit type from a value of the program. +program(() => { + let kind: UnitType = units.FloorGunTrap; + let wave = 3; + if (wave > 2) kind = units.LeftWallMissileTrap; + setDoodadState(players.Neutral, kind, locations.Anywhere, "disable"); +}); diff --git a/ledger/game/set-doodad-state.ts b/ledger/game/set-doodad-state.ts new file mode 100644 index 0000000..9eacdb9 --- /dev/null +++ b/ledger/game/set-doodad-state.ts @@ -0,0 +1,5 @@ +// ledger: step 5, refused: the action Set Doodad State +// The action Set Doodad State: a trap or a door at a location turned on or off. +program(() => { + setDoodadState(players.Neutral, units.FloorGunTrap, locations.Anywhere, "disable"); +}); diff --git a/ledger/game/set-invincibility-type-from-a-value.ts b/ledger/game/set-invincibility-type-from-a-value.ts new file mode 100644 index 0000000..a3bf66c --- /dev/null +++ b/ledger/game/set-invincibility-type-from-a-value.ts @@ -0,0 +1,8 @@ +// ledger: step 5, refused: the action Set Invincibility, its fields from values +// The action Set Invincibility, the unit type from a value of the program. +program(() => { + let kind: UnitType = units.TerranMarine; + let wave = 3; + if (wave > 2) kind = units.TerranFirebat; + setInvincibility(P1, kind, locations.Anywhere, "enable"); +}); diff --git a/ledger/game/set-invincibility.ts b/ledger/game/set-invincibility.ts new file mode 100644 index 0000000..e72aee9 --- /dev/null +++ b/ledger/game/set-invincibility.ts @@ -0,0 +1,5 @@ +// ledger: step 5, refused: the action Set Invincibility +// The action Set Invincibility: units at a location cannot be hurt, or can again. +program(() => { + setInvincibility(P1, units.TerranMarine, locations.Anywhere, "enable"); +}); diff --git a/ledger/game/set-mission-objectives-text-from-the-program.ts b/ledger/game/set-mission-objectives-text-from-the-program.ts new file mode 100644 index 0000000..956e4fd --- /dev/null +++ b/ledger/game/set-mission-objectives-text-from-the-program.ts @@ -0,0 +1,7 @@ +// ledger: step 5, refused: the action Set Mission Objectives, its text the program's +// The action Set Mission Objectives, its text made while the game runs: a number of the program in it. +program(() => { + let wave = 2; + wave += 1; + setMissionObjectives(`Survive wave ${wave}`); +}); diff --git a/ledger/game/set-mission-objectives.ts b/ledger/game/set-mission-objectives.ts new file mode 100644 index 0000000..266402a --- /dev/null +++ b/ledger/game/set-mission-objectives.ts @@ -0,0 +1,5 @@ +// ledger: step 5, refused: the action Set Mission Objectives +// The action Set Mission Objectives, its text written in the script. +program(() => { + setMissionObjectives("Hold the bridge."); +}); diff --git a/ledger/game/set-next-scenario.ts b/ledger/game/set-next-scenario.ts new file mode 100644 index 0000000..79c2170 --- /dev/null +++ b/ledger/game/set-next-scenario.ts @@ -0,0 +1,5 @@ +// ledger: step 5, refused: the action Set Next Scenario +// The action Set Next Scenario: the map a campaign goes on to. +program(() => { + setNextScenario("Chapter 2"); +}); diff --git a/ledger/game/set-resources-amount-from-a-read.ts b/ledger/game/set-resources-amount-from-a-read.ts new file mode 100644 index 0000000..622bc34 --- /dev/null +++ b/ledger/game/set-resources-amount-from-a-read.ts @@ -0,0 +1,5 @@ +// ledger: step 5, refused: the action Set Resources, its fields from values +// The action Set Resources, the amount read from the game when the line runs: `setResources(P2, "set", minerals(P1), "ore")`. +program(() => { + setResources(P2, "set", minerals(P1), "ore"); +}); diff --git a/ledger/game/set-resources-amount-from-a-value.ts b/ledger/game/set-resources-amount-from-a-value.ts new file mode 100644 index 0000000..1aac56f --- /dev/null +++ b/ledger/game/set-resources-amount-from-a-value.ts @@ -0,0 +1,7 @@ +// ledger: step 5, refused: the action Set Resources, its fields from values +// The action Set Resources, the amount from a value of the program. +program(() => { + let bounty = 50; + bounty += 25; + setResources(P1, "add", bounty, "ore"); +}); diff --git a/ledger/game/set-resources.ts b/ledger/game/set-resources.ts new file mode 100644 index 0000000..9d77969 --- /dev/null +++ b/ledger/game/set-resources.ts @@ -0,0 +1,5 @@ +// ledger: step 5, refused: the action Set Resources +// The action Set Resources, all its fields known when the map is built. +program(() => { + setResources(P1, "add", 500, "ore"); +}); diff --git a/ledger/game/set-score-amount-from-a-value.ts b/ledger/game/set-score-amount-from-a-value.ts new file mode 100644 index 0000000..da7a8f6 --- /dev/null +++ b/ledger/game/set-score-amount-from-a-value.ts @@ -0,0 +1,7 @@ +// ledger: step 5, refused: the action Set Score, its fields from values +// The action Set Score, the amount from a value of the program. +program(() => { + let points = 10; + points += 5; + setScore(P1, "add", points, "custom"); +}); diff --git a/ledger/game/set-score.ts b/ledger/game/set-score.ts new file mode 100644 index 0000000..1684123 --- /dev/null +++ b/ledger/game/set-score.ts @@ -0,0 +1,5 @@ +// ledger: step 5, refused: the action Set Score +// The action Set Score, all its fields known when the map is built. +program(() => { + setScore(P1, "add", 10, "custom"); +}); diff --git a/ledger/game/set-switch.ts b/ledger/game/set-switch.ts new file mode 100644 index 0000000..ee5088b --- /dev/null +++ b/ledger/game/set-switch.ts @@ -0,0 +1,5 @@ +// ledger: step 5, refused: the action Set Switch +// The action Set Switch: one of the map's switches (`switches.*`) set, cleared, toggled or randomized. +program(() => { + setSwitch(switches.Switch1, "set"); +}); diff --git a/ledger/game/talking-portrait-type-from-a-value.ts b/ledger/game/talking-portrait-type-from-a-value.ts new file mode 100644 index 0000000..b4f56fa --- /dev/null +++ b/ledger/game/talking-portrait-type-from-a-value.ts @@ -0,0 +1,8 @@ +// ledger: step 5, refused: the action Talking Portrait, its fields from values +// The action Talking Portrait, the unit type from a value of the program. +program(() => { + let kind: UnitType = units.TerranMarine; + let wave = 3; + if (wave > 2) kind = units.TerranFirebat; + talkingPortrait(kind, 2000); +}); diff --git a/ledger/game/talking-portrait.ts b/ledger/game/talking-portrait.ts new file mode 100644 index 0000000..8a36888 --- /dev/null +++ b/ledger/game/talking-portrait.ts @@ -0,0 +1,5 @@ +// ledger: step 5, refused: the action Talking Portrait +// The action Talking Portrait: a unit's portrait talks for so many milliseconds. +program(() => { + talkingPortrait(units.TerranMarine, 2000); +}); diff --git a/ledger/game/transmission-text-from-the-program.ts b/ledger/game/transmission-text-from-the-program.ts new file mode 100644 index 0000000..40ec024 --- /dev/null +++ b/ledger/game/transmission-text-from-the-program.ts @@ -0,0 +1,7 @@ +// ledger: step 5, refused: the action Transmission, its text the program's +// The action Transmission, its text made while the game runs: a number of the program in it. +program(() => { + let left = 4; + left -= 1; + transmission(`${left} bunkers left`, units.TerranMarine, locations.Anywhere, "add", 0, "sound\\glue\\mousedown2.wav", 500); +}); diff --git a/ledger/game/transmission-type-from-a-value.ts b/ledger/game/transmission-type-from-a-value.ts new file mode 100644 index 0000000..d287d16 --- /dev/null +++ b/ledger/game/transmission-type-from-a-value.ts @@ -0,0 +1,8 @@ +// ledger: step 5, refused: the action Transmission, its fields from values +// The action Transmission, the unit type from a value of the program. +program(() => { + let kind: UnitType = units.TerranMarine; + let wave = 3; + if (wave > 2) kind = units.TerranFirebat; + transmission("Report in.", kind, locations.Anywhere, "add", 0, "sound\\glue\\mousedown2.wav", 500); +}); diff --git a/ledger/game/transmission.ts b/ledger/game/transmission.ts new file mode 100644 index 0000000..8d9e5c0 --- /dev/null +++ b/ledger/game/transmission.ts @@ -0,0 +1,5 @@ +// ledger: step 5, refused: the action Transmission +// The action Transmission: a text, a sound and a talking portrait, the unit pinged on the minimap. +program(() => { + transmission("Report in.", units.TerranMarine, locations.Anywhere, "add", 0, "sound\\glue\\mousedown2.wav", 500); +}); diff --git a/ledger/game/unmute-unit-speech.ts b/ledger/game/unmute-unit-speech.ts new file mode 100644 index 0000000..24b547f --- /dev/null +++ b/ledger/game/unmute-unit-speech.ts @@ -0,0 +1,5 @@ +// ledger: step 5, refused: the action Unmute Unit Speech +// The action Unmute Unit Speech: the units' voices back. +program(() => { + unmuteUnitSpeech(); +}); diff --git a/ledger/game/unpause-game.ts b/ledger/game/unpause-game.ts new file mode 100644 index 0000000..7336f61 --- /dev/null +++ b/ledger/game/unpause-game.ts @@ -0,0 +1,5 @@ +// ledger: step 5, refused: the action Unpause Game +// The action Unpause Game. +program(() => { + unpauseGame(); +}); diff --git a/ledger/game/unpause-timer.ts b/ledger/game/unpause-timer.ts new file mode 100644 index 0000000..f7a1331 --- /dev/null +++ b/ledger/game/unpause-timer.ts @@ -0,0 +1,5 @@ +// ledger: step 5, refused: the action Unpause Timer +// The action Unpause Timer: the countdown timer runs on. +program(() => { + unpauseTimer(); +}); diff --git a/ledger/game/victory.ts b/ledger/game/victory.ts new file mode 100644 index 0000000..71d2665 --- /dev/null +++ b/ledger/game/victory.ts @@ -0,0 +1,5 @@ +// ledger: step 5, refused: the action Victory +// The action Victory: the player wins the game. +program(() => { + victory(); +}); diff --git a/ledger/game/wait.ts b/ledger/game/wait.ts new file mode 100644 index 0000000..ab23c50 --- /dev/null +++ b/ledger/game/wait.ts @@ -0,0 +1,5 @@ +// ledger: step 5, refused: the action Wait +// The action Wait, inside a program: the trigger waits so many milliseconds. +program(() => { + wait(500); +}); diff --git a/ledger/input/chatted-number.ts b/ledger/input/chatted-number.ts new file mode 100644 index 0000000..2f41171 --- /dev/null +++ b/ledger/input/chatted-number.ts @@ -0,0 +1,9 @@ +// ledger: step 12, refused: the players' input (chat) +// A number read out of a typed line: `const m = chatted(P1, "-give {n}"); if (m) … m.n …`. +program(async () => { + while (true) { + const m = chatted(P1, "-give {n}"); + if (m) print(`${m.n}`); + await nextFrame(); + } +}); diff --git a/ledger/input/chatted-unit-type.ts b/ledger/input/chatted-unit-type.ts new file mode 100644 index 0000000..53f7433 --- /dev/null +++ b/ledger/input/chatted-unit-type.ts @@ -0,0 +1,9 @@ +// ledger: step 12, refused: the players' input (chat) +// A unit type read out of a typed line by its name: `chatted(P1, "-spawn {what:unit}")`. +program(async () => { + while (true) { + const m = chatted(P1, "-spawn {what:unit}"); + if (m && m.what == units.TerranMarine) print("a marine"); + await nextFrame(); + } +}); diff --git a/ledger/input/chatted-word.ts b/ledger/input/chatted-word.ts new file mode 100644 index 0000000..28a5e23 --- /dev/null +++ b/ledger/input/chatted-word.ts @@ -0,0 +1,9 @@ +// ledger: step 12, refused: the players' input (chat) +// One of a list of words read out of a typed line, as its place in the list: `chatted(P1, "-take {kind:ore|gas}")`. +program(async () => { + while (true) { + const m = chatted(P1, "-take {kind:ore|gas}"); + if (m) print(`${m.kind}`); + await nextFrame(); + } +}); diff --git a/ledger/input/chatted.ts b/ledger/input/chatted.ts new file mode 100644 index 0000000..e5a03f7 --- /dev/null +++ b/ledger/input/chatted.ts @@ -0,0 +1,8 @@ +// ledger: step 12, refused: the players' input (chat) +// A line a player typed, matched whole: `if (chatted(P1, "-help"))`. +program(async () => { + while (true) { + if (chatted(P1, "-help")) print("help"); + await nextFrame(); + } +}); diff --git a/ledger/input/clicked-middle.ts b/ledger/input/clicked-middle.ts new file mode 100644 index 0000000..0842f0f --- /dev/null +++ b/ledger/input/clicked-middle.ts @@ -0,0 +1,8 @@ +// ledger: step 12, refused: the players' input (click) +// A click of the middle mouse button: `clicked(P1, "middle")`. +program(async () => { + while (true) { + if (clicked(P1, "middle")) print("middle"); + await nextFrame(); + } +}); diff --git a/ledger/input/clicked-right.ts b/ledger/input/clicked-right.ts new file mode 100644 index 0000000..28956ec --- /dev/null +++ b/ledger/input/clicked-right.ts @@ -0,0 +1,8 @@ +// ledger: step 12, refused: the players' input (click) +// A click of the right mouse button: `clicked(P1, "right")`. +program(async () => { + while (true) { + if (clicked(P1, "right")) print("right"); + await nextFrame(); + } +}); diff --git a/ledger/input/clicked.ts b/ledger/input/clicked.ts new file mode 100644 index 0000000..c336af4 --- /dev/null +++ b/ledger/input/clicked.ts @@ -0,0 +1,8 @@ +// ledger: step 12, refused: the players' input (click) +// A click of the left mouse button, true on the frame it arrives: `clicked(P1)`. +program(async () => { + while (true) { + if (clicked(P1)) print("click"); + await nextFrame(); + } +}); diff --git a/ledger/input/key-held.ts b/ledger/input/key-held.ts new file mode 100644 index 0000000..3e9557a --- /dev/null +++ b/ledger/input/key-held.ts @@ -0,0 +1,10 @@ +// ledger: step 12, refused: the players' input (key) +// A key held down, true on every frame it is down: `keyHeld(P1, "D")`. +program(async () => { + let x = 0; + while (true) { + if (keyHeld(P1, "D")) x += 1; + print(`${x}`); + await nextFrame(); + } +}); diff --git a/ledger/input/key-pressed-current-player.ts b/ledger/input/key-pressed-current-player.ts new file mode 100644 index 0000000..c9fc495 --- /dev/null +++ b/ledger/input/key-pressed-current-player.ts @@ -0,0 +1,8 @@ +// ledger: step 12, refused: the players' input (key) +// A key pressed by the player the program runs as: `keyPressed(CurrentPlayer, "Space")`. +program(async () => { + while (true) { + if (keyPressed(CurrentPlayer, "Space")) print("jump"); + await nextFrame(); + } +}, { owner: P2 }); diff --git a/ledger/input/key-pressed.ts b/ledger/input/key-pressed.ts new file mode 100644 index 0000000..8d77c8d --- /dev/null +++ b/ledger/input/key-pressed.ts @@ -0,0 +1,8 @@ +// ledger: step 12, refused: the players' input (key) +// A key a player pressed, true on the frame the press arrives: `keyPressed(P1, "F7")`, looked for every frame. +program(async () => { + while (true) { + if (keyPressed(P1, "F7")) print("help"); + await nextFrame(); + } +}); diff --git a/ledger/input/local-camera.ts b/ledger/input/local-camera.ts new file mode 100644 index 0000000..1655046 --- /dev/null +++ b/ledger/input/local-camera.ts @@ -0,0 +1,5 @@ +// ledger: step 12, refused: a read of local.camera.x +// Where this computer's screen is on the map: `local.camera.x`, `local.camera.y`. +program(() => { + print(`${local.camera.x}, ${local.camera.y}`); +}); diff --git a/ledger/input/local-mouse.ts b/ledger/input/local-mouse.ts new file mode 100644 index 0000000..332aecc --- /dev/null +++ b/ledger/input/local-mouse.ts @@ -0,0 +1,5 @@ +// ledger: step 12, refused: a read of local.mouse.x +// Where this computer's mouse is on the screen: `local.mouse.x`, `local.mouse.y`. +program(() => { + print(`${local.mouse.x}, ${local.mouse.y}`); +}); diff --git a/ledger/input/local-player.ts b/ledger/input/local-player.ts new file mode 100644 index 0000000..7f320ba --- /dev/null +++ b/ledger/input/local-player.ts @@ -0,0 +1,5 @@ +// ledger: step 12, refused: a read of local.player +// The player sitting at this computer, which every computer knows differently: `local.player`. +program(() => { + print(`${local.player}`); +}); diff --git a/ledger/input/local-selected-selected-again.ts b/ledger/input/local-selected-selected-again.ts new file mode 100644 index 0000000..a4c2299 --- /dev/null +++ b/ledger/input/local-selected-selected-again.ts @@ -0,0 +1,5 @@ +// ledger: step 12, refused: select() +// The unit this computer has selected, made the player's selection on every computer: `select(P1, local.selected)`. +program(() => { + select(P1, local.selected!); +}); diff --git a/ledger/input/local-selected-unit.ts b/ledger/input/local-selected-unit.ts new file mode 100644 index 0000000..87d9bb7 --- /dev/null +++ b/ledger/input/local-selected-unit.ts @@ -0,0 +1,6 @@ +// ledger: step 12, refused: a unit kept in a variable +// The unit this computer has selected, kept and its hit points shown: `const u = local.selected; if (u) … u.hp`. +program(() => { + const u = local.selected; + if (u) print(`${u.hp}`); +}); diff --git a/ledger/input/local-selected.ts b/ledger/input/local-selected.ts new file mode 100644 index 0000000..458efc9 --- /dev/null +++ b/ledger/input/local-selected.ts @@ -0,0 +1,6 @@ +// ledger: step 12, refused: a unit +// Whether this computer has a unit selected: `local.selected`, shown but not acted on. +program(() => { + if (local.selected) print("selected"); + else print("none"); +}); diff --git a/ledger/input/local-selection-count.ts b/ledger/input/local-selection-count.ts new file mode 100644 index 0000000..6d03ea2 --- /dev/null +++ b/ledger/input/local-selection-count.ts @@ -0,0 +1,5 @@ +// ledger: step 12, refused: a read of local.selection[0] +// How many units this computer has selected: `local.selectionCount`. +program(() => { + print(`${local.selectionCount}`); +}); diff --git a/ledger/input/local-shown-only.ts b/ledger/input/local-shown-only.ts new file mode 100644 index 0000000..a52e48d --- /dev/null +++ b/ledger/input/local-shown-only.ts @@ -0,0 +1,5 @@ +// ledger: step 12, refused: a read of local.camera.x +// A value only this computer knows, tested to choose what is shown here: `if (local.camera.x > 300) print(…)`. +program(() => { + if (local.camera.x > 300) print("right side"); +}); diff --git a/ledger/input/mouse-places-a-location.ts b/ledger/input/mouse-places-a-location.ts new file mode 100644 index 0000000..f0d96ca --- /dev/null +++ b/ledger/input/mouse-places-a-location.ts @@ -0,0 +1,10 @@ +// ledger: step 12, refused: the players' input (mouse) +// A location moved to where a player's mouse is: `centerLocation(spot, at.x, at.y)` after `const at = mouse(P1)`. +const spot = reserveLocation("where the mouse is"); +program(async () => { + while (true) { + const at = mouse(P1); + centerLocation(spot, at.x, at.y); + await nextFrame(); + } +}); diff --git a/ledger/input/mouse.ts b/ledger/input/mouse.ts new file mode 100644 index 0000000..b372153 --- /dev/null +++ b/ledger/input/mouse.ts @@ -0,0 +1,9 @@ +// ledger: step 12, refused: the players' input (mouse) +// Where a player's mouse is on the map, the same on every computer: `mouse(P1).x`, `mouse(P1).y`. +program(async () => { + while (true) { + const at = mouse(P1); + print(`${at.x}, ${at.y}`); + await nextFrame(); + } +}); diff --git a/ledger/input/on-chat.ts b/ledger/input/on-chat.ts new file mode 100644 index 0000000..ca8b257 --- /dev/null +++ b/ledger/input/on-chat.ts @@ -0,0 +1,7 @@ +// ledger: step 12, refused: an event handled (on.chat) +// A function run each time a player types a line that fits a pattern: `on.chat("-give {n}", (p, { n }) => …)`. +program(async () => { + on.chat("-give {n}", (p, { n }) => { + setResources(p, "add", n, "ore"); + }); +}); diff --git a/ledger/input/on-key-from.ts b/ledger/input/on-key-from.ts new file mode 100644 index 0000000..7eeab33 --- /dev/null +++ b/ledger/input/on-key-from.ts @@ -0,0 +1,9 @@ +// ledger: step 12, refused: an event handled (on.key) +// A function run each time one player presses a key: `on.key("Space", (p) => …, { from: P1 })`. +program(async () => { + let n = 0; + on.key("Space", (p) => { + n += 1; + print(`${n}`); + }, { from: P1 }); +}); diff --git a/ledger/input/on-key.ts b/ledger/input/on-key.ts new file mode 100644 index 0000000..12e1943 --- /dev/null +++ b/ledger/input/on-key.ts @@ -0,0 +1,7 @@ +// ledger: step 12, refused: an event handled (on.key) +// A function run each time a player presses a key: `on.key("F7", (p) => …)`. +program(async () => { + on.key("F7", (p) => { + print("help"); + }); +}); diff --git a/ledger/input/sync-several.ts b/ledger/input/sync-several.ts new file mode 100644 index 0000000..7e04ebe --- /dev/null +++ b/ledger/input/sync-several.ts @@ -0,0 +1,6 @@ +// ledger: step 12, refused: sync() +// Several of one player's values sent together, read on the same frame: `const [x, y] = await sync(P1, a, b)`. +program(async () => { + const [x, y] = await sync(P1, local.camera.x + local.mouse.x, local.camera.y + local.mouse.y); + print(`${x}, ${y}`); +}); diff --git a/ledger/input/sync.ts b/ledger/input/sync.ts new file mode 100644 index 0000000..3014956 --- /dev/null +++ b/ledger/input/sync.ts @@ -0,0 +1,6 @@ +// ledger: step 12, refused: sync() +// One player's value only their computer knows, sent to every computer: `const x = await sync(P1, local.camera.x)`. +program(async () => { + const x = await sync(P1, local.camera.x); + print(`${x}`); +}); diff --git a/ledger/input/under-mouse-filter.ts b/ledger/input/under-mouse-filter.ts new file mode 100644 index 0000000..6ca5a28 --- /dev/null +++ b/ledger/input/under-mouse-filter.ts @@ -0,0 +1,8 @@ +// ledger: step 12, refused: a unit +// The nearest unit to a player's mouse that a filter matches, within so many pixels: `underMouse(P1, { owner: P2, within: 32 })`. +program(async () => { + while (true) { + if (underMouse(P1, { owner: P2, within: 32 })) print("over an enemy"); + await nextFrame(); + } +}); diff --git a/ledger/input/under-mouse.ts b/ledger/input/under-mouse.ts new file mode 100644 index 0000000..31c11db --- /dev/null +++ b/ledger/input/under-mouse.ts @@ -0,0 +1,8 @@ +// ledger: step 12, refused: a unit +// The unit nearest a player's mouse, or none: `underMouse(P1)`. +program(async () => { + while (true) { + if (underMouse(P1)) print("over a unit"); + await nextFrame(); + } +}); diff --git a/ledger/language/and.ts b/ledger/language/and.ts new file mode 100644 index 0000000..e5e71c8 --- /dev/null +++ b/ledger/language/and.ts @@ -0,0 +1,11 @@ +// ledger: step 0, takes +// `&&` of two tests: true when both hold. +program(() => { + let n = 0; + let count = 0; + while (n < 10) { + if (n > 2 && n < 7) count += 1; + n += 1; + } + print(`${count}`); +}); diff --git a/ledger/language/array-destructuring-rest.ts b/ledger/language/array-destructuring-rest.ts new file mode 100644 index 0000000..ea48d1a --- /dev/null +++ b/ledger/language/array-destructuring-rest.ts @@ -0,0 +1,8 @@ +// ledger: step 3, takes +// Destructuring the first cell of an array and the rest into another: `const [first, ...rest] = xs`. +program(() => { + let xs = [1, 2, 3]; + xs[2] += 5; + const [first, ...rest] = xs; + print(`${first} ${rest[1]}`); +}); diff --git a/ledger/language/array-destructuring.ts b/ledger/language/array-destructuring.ts new file mode 100644 index 0000000..bcf3582 --- /dev/null +++ b/ledger/language/array-destructuring.ts @@ -0,0 +1,8 @@ +// ledger: step 3, takes +// Destructuring an array of numbers into variables: `const [a, b] = xs`. +program(() => { + let xs = [1, 2, 3]; + xs[1] += 5; + const [a, b] = xs; + print(`${a} ${b}`); +}); diff --git a/ledger/language/array-of-instances.ts b/ledger/language/array-of-instances.ts new file mode 100644 index 0000000..124632c --- /dev/null +++ b/ledger/language/array-of-instances.ts @@ -0,0 +1,19 @@ +// ledger: step 3, takes +// A fixed-length array of instances of a class, a row picked by an index that changes while the game runs. +class Wave { + constructor(public left: number) {} + spawn() { + if (this.left > 0) this.left -= 1; + } +} +program(() => { + const waves = [new Wave(2), new Wave(5)]; + let i = 0; + let which = 0; + while (i < 4) { + waves[which].spawn(); + which = 1 - which; + i += 1; + } + print(`${waves[0].left} ${waves[1].left}`); +}); diff --git a/ledger/language/array-spread.ts b/ledger/language/array-spread.ts new file mode 100644 index 0000000..f7d2bef --- /dev/null +++ b/ledger/language/array-spread.ts @@ -0,0 +1,10 @@ +// ledger: step 3, takes +// An array made from another with `...`: `[...xs, 4]`, of a fixed length. +program(() => { + let xs = [1, 2, 3]; + xs[0] += 5; + const ys = [...xs, 4]; + let total = 0; + for (const y of ys) total += y; + print(`${total}`); +}); diff --git a/ledger/language/arrow-function.ts b/ledger/language/arrow-function.ts new file mode 100644 index 0000000..5200c07 --- /dev/null +++ b/ledger/language/arrow-function.ts @@ -0,0 +1,8 @@ +// ledger: step 1, takes +// A function written as an arrow kept in a `const`: `const twice = (x: number) => x + x`. +const twice = (x: number) => x + x; +program(() => { + let n = 1; + while (n < 50) n = twice(n) + 1; + print(`${twice(n)}`); +}); diff --git a/ledger/language/arrow-in-program.ts b/ledger/language/arrow-in-program.ts new file mode 100644 index 0000000..f394a3d --- /dev/null +++ b/ledger/language/arrow-in-program.ts @@ -0,0 +1,15 @@ +// ledger: step 1, takes +// An arrow function declared in the program that reads and writes the program's variables. +program(() => { + let total = 0; + const add = (n: number) => { + total += n; + }; + let n = 0; + while (n < 4) { + add(n); + add(2); + n += 1; + } + print(`${total}`); +}); diff --git a/ledger/language/assignment-in-condition.ts b/ledger/language/assignment-in-condition.ts new file mode 100644 index 0000000..a77d742 --- /dev/null +++ b/ledger/language/assignment-in-condition.ts @@ -0,0 +1,8 @@ +// ledger: step 0, takes +// An assignment used as a value inside a test: `while ((n = n + 2) < 9)`. +program(() => { + let n = 0; + let turns = 0; + while ((n = n + 2) < 9) turns += 1; + print(`${n} ${turns}`); +}); diff --git a/ledger/language/async-arrow.ts b/ledger/language/async-arrow.ts new file mode 100644 index 0000000..c5ffba5 --- /dev/null +++ b/ledger/language/async-arrow.ts @@ -0,0 +1,11 @@ +// ledger: step 0, takes +// An `async` arrow function kept in a `const` and awaited. +const pause = async (n: number) => { + await sleep(frames(2)); + return n + 1; +}; +program(async () => { + let n = 0; + while (n < 3) n = await pause(n); + print(`${n}`); +}); diff --git a/ledger/language/async-function.ts b/ledger/language/async-function.ts new file mode 100644 index 0000000..f43fbb7 --- /dev/null +++ b/ledger/language/async-function.ts @@ -0,0 +1,12 @@ +// ledger: step 0, takes +// An `async` function that waits, awaited by the program: the program goes on when it returns, with its result. +async function slowDouble(n: number) { + await sleep(frames(3)); + return n + n; +} +program(async () => { + let n = 0; + while (n < 3) n += 1; + n = await slowDouble(n); + print(`${n}`); +}); diff --git a/ledger/language/block-scope.ts b/ledger/language/block-scope.ts new file mode 100644 index 0000000..97e4d56 --- /dev/null +++ b/ledger/language/block-scope.ts @@ -0,0 +1,13 @@ +// ledger: step 0, takes +// A variable declared in a block is that block's own, and a name inside may shadow one outside. +program(() => { + let n = 1; + let total = 0; + while (total < 10) { + let n = 3; + n += total; + total += n; + } + n += total; + print(`${n} ${total}`); +}); diff --git a/ledger/language/boolean-equality.ts b/ledger/language/boolean-equality.ts new file mode 100644 index 0000000..a2466af --- /dev/null +++ b/ledger/language/boolean-equality.ts @@ -0,0 +1,16 @@ +// ledger: step 0, takes +// Two booleans compared, `a == b`, `a != b`. +program(() => { + let n = 0; + let same = 0; + let a = false; + let b = true; + while (n < 6) { + a = n > 2; + b = n < 4; + if (a == b) same += 1; + if (a != b) same += 10; + n += 1; + } + print(`${same}`); +}); diff --git a/ledger/language/boolean-variable.ts b/ledger/language/boolean-variable.ts new file mode 100644 index 0000000..74992f2 --- /dev/null +++ b/ledger/language/boolean-variable.ts @@ -0,0 +1,11 @@ +// ledger: step 0, takes +// A boolean variable, set from a test while the game runs and tested later. +program(() => { + let n = 0; + let seen = false; + while (n < 5) { + seen = n == 3; + if (seen) print("three"); + n += 1; + } +}); diff --git a/ledger/language/break.ts b/ledger/language/break.ts new file mode 100644 index 0000000..5b241b3 --- /dev/null +++ b/ledger/language/break.ts @@ -0,0 +1,10 @@ +// ledger: step 0, takes +// `break` leaves the loop it is in. +program(() => { + let n = 0; + while (true) { + n += 3; + if (n > 10) break; + } + print(`${n}`); +}); diff --git a/ledger/language/callback-argument.ts b/ledger/language/callback-argument.ts new file mode 100644 index 0000000..20b0f77 --- /dev/null +++ b/ledger/language/callback-argument.ts @@ -0,0 +1,14 @@ +// ledger: step 1, takes +// An arrow function handed to a function as an argument and called inside it: worked out where it is called. +function twice(f: (n: number) => number, n: number) { + return f(f(n)); +} +program(() => { + let n = 1; + let turns = 0; + while (turns < 3) { + n = twice((x) => x + x + 1, n); + turns += 1; + } + print(`${n}`); +}); diff --git a/ledger/language/clamp.ts b/ledger/language/clamp.ts new file mode 100644 index 0000000..8fe991d --- /dev/null +++ b/ledger/language/clamp.ts @@ -0,0 +1,11 @@ +// ledger: step 0, takes +// `clamp(value, low, high)`: the value kept within low … high. +program(() => { + let n = -2; + let total = 0; + while (n < 8) { + total += clamp(n, 0, 5); + n += 1; + } + print(`${total}`); +}); diff --git a/ledger/language/class-abstract.ts b/ledger/language/class-abstract.ts new file mode 100644 index 0000000..9680dc1 --- /dev/null +++ b/ledger/language/class-abstract.ts @@ -0,0 +1,22 @@ +// ledger: step 1, takes +// An `abstract` class with an abstract method, and a class that extends it and gives the method a body. +abstract class Shape { + abstract area(): number; + doubled() { + return this.area() + this.area(); + } +} +class Square extends Shape { + constructor(public side: number) { + super(); + } + area() { + return this.side + this.side + this.side + this.side; + } +} +program(() => { + let k = 0; + while (k < 3) k += 1; + const s = new Square(k); + print(`${s.doubled()}`); +}); diff --git a/ledger/language/class-array-field.ts b/ledger/language/class-array-field.ts new file mode 100644 index 0000000..862e553 --- /dev/null +++ b/ledger/language/class-array-field.ts @@ -0,0 +1,18 @@ +// ledger: step 3, takes +// A class with a field that is a fixed-length array of numbers. +class Squad { + hps = [40, 40, 40]; + hurt(i: number, n: number) { + this.hps[i] -= n; + } +} +program(() => { + const s = new Squad(); + let i = 0; + while (i < 3) { + s.hurt(i, i + 5); + s.hurt(i, 1); + i += 1; + } + print(`${s.hps[0]} ${s.hps[2]}`); +}); diff --git a/ledger/language/class-by-reference.ts b/ledger/language/class-by-reference.ts new file mode 100644 index 0000000..c07e248 --- /dev/null +++ b/ledger/language/class-by-reference.ts @@ -0,0 +1,12 @@ +// ledger: step 9, refused: a warning TrigScript shows every player ("more than 1023 closures and their variables are kept at once") +// An instance kept by reference: a variable that may hold one instance or another, or none, chosen while the game runs. +class Node { + constructor(public v: number) {} +} +program(() => { + let a: Node | null = null; + let k = 0; + while (k < 3) k += 1; + a = new Node(k); + print(`${a.v}`); +}); diff --git a/ledger/language/class-extends-super.ts b/ledger/language/class-extends-super.ts new file mode 100644 index 0000000..82d30a2 --- /dev/null +++ b/ledger/language/class-extends-super.ts @@ -0,0 +1,18 @@ +// ledger: step 0, takes +// A class that `extends` another: `super(…)` runs the constructor of what it extends first. +class Animal { + legs = 4; + constructor(public size: number) {} +} +class Bird extends Animal { + constructor(size: number, public wings: number) { + super(size + size); + this.legs = 2; + } +} +program(() => { + let k = 0; + while (k < 4) k += 1; + const b = new Bird(k, 7); + print(`${b.legs} ${b.size} ${b.wings}`); +}); diff --git a/ledger/language/class-field-from-field.ts b/ledger/language/class-field-from-field.ts new file mode 100644 index 0000000..6be222e --- /dev/null +++ b/ledger/language/class-field-from-field.ts @@ -0,0 +1,11 @@ +// ledger: step 0, takes +// A field's initial value worked out from another field: `max = 40; hp = this.max`. +class Marine { + max = 40; + hp = this.max; +} +program(() => { + const m = new Marine(); + while (m.hp > 10) m.hp -= 7; + print(`${m.hp} ${m.max}`); +}); diff --git a/ledger/language/class-fields-constructor.ts b/ledger/language/class-fields-constructor.ts new file mode 100644 index 0000000..64f921c --- /dev/null +++ b/ledger/language/class-fields-constructor.ts @@ -0,0 +1,15 @@ +// ledger: step 0, takes +// A class with fields and a constructor: an instance is a record whose fields the constructor sets. +class Wave { + left = 0; + constructor(public count: number) { + this.left = count; + } +} +program(() => { + let n = 0; + while (n < 3) n += 1; + const w = new Wave(n); + while (w.left > 0) w.left -= 1; + print(`${w.count} ${w.left}`); +}); diff --git a/ledger/language/class-generator-method.ts b/ledger/language/class-generator-method.ts new file mode 100644 index 0000000..c130467 --- /dev/null +++ b/ledger/language/class-generator-method.ts @@ -0,0 +1,19 @@ +// ledger: step 0, takes +// A generator method, `*[Symbol.iterator]()`: an instance walked by `for…of`. +class Squad { + size = 3; + *[Symbol.iterator]() { + let i = 0; + while (i < this.size) { + yield i; + i += 1; + } + } +} +program(() => { + const s = new Squad(); + while (s.size < 5) s.size += 1; + let total = 0; + for (const x of s) total += x; + print(`${total}`); +}); diff --git a/ledger/language/class-getter-setter.ts b/ledger/language/class-getter-setter.ts new file mode 100644 index 0000000..d9b43c7 --- /dev/null +++ b/ledger/language/class-getter-setter.ts @@ -0,0 +1,21 @@ +// ledger: step 0, takes +// A getter and a setter: `get twice()` read like a field, `set twice(v)` written like one. +class Counter { + n = 0; + get twice() { + return this.n + this.n; + } + set twice(v: number) { + this.n = v >> 1; + } +} +program(() => { + const c = new Counter(); + let k = 0; + while (c.twice < 20) { + c.n += 3; + k += 1; + } + c.twice = 40; + print(`${c.n} ${k}`); +}); diff --git a/ledger/language/class-implements.ts b/ledger/language/class-implements.ts new file mode 100644 index 0000000..043e668 --- /dev/null +++ b/ledger/language/class-implements.ts @@ -0,0 +1,17 @@ +// ledger: step 0, takes +// A class that `implements` an interface: the interface is a type only, gone in the map. +interface Scored { + score(): number; +} +class Kill implements Scored { + constructor(public worth: number) {} + score() { + return this.worth + 1; + } +} +program(() => { + let k = 0; + while (k < 5) k += 1; + const s: Scored = new Kill(k); + print(`${s.score()}`); +}); diff --git a/ledger/language/class-instance-alias.ts b/ledger/language/class-instance-alias.ts new file mode 100644 index 0000000..e08feb8 --- /dev/null +++ b/ledger/language/class-instance-alias.ts @@ -0,0 +1,15 @@ +// ledger: step 0, takes +// An instance under a second name is the same instance: a change through one is seen through the other. +class Counter { + n = 0; +} +program(() => { + const a = new Counter(); + const same = a; + let k = 0; + while (k < 4) { + same.n += k; + k += 1; + } + print(`${a.n}`); +}); diff --git a/ledger/language/class-instance-in-function.ts b/ledger/language/class-instance-in-function.ts new file mode 100644 index 0000000..e719ec3 --- /dev/null +++ b/ledger/language/class-instance-in-function.ts @@ -0,0 +1,19 @@ +// ledger: step 1, takes +// An instance handed to a function, which changes it: the same instance, not a copy. +class Counter { + n = 0; +} +function twice(c: Counter, by: number) { + c.n += by; + c.n += by; +} +program(() => { + const c = new Counter(); + let k = 0; + while (k < 3) { + twice(c, k); + twice(c, 1); + k += 1; + } + print(`${c.n}`); +}); diff --git a/ledger/language/class-method.ts b/ledger/language/class-method.ts new file mode 100644 index 0000000..5a1daf2 --- /dev/null +++ b/ledger/language/class-method.ts @@ -0,0 +1,18 @@ +// ledger: step 1, takes +// A method that changes its instance, called from more than one place: a function with the instance first. +class Counter { + n = 0; + bump(by: number) { + this.n += by; + } +} +program(() => { + const c = new Counter(); + let k = 0; + while (k < 4) { + c.bump(k); + c.bump(1); + k += 1; + } + print(`${c.n}`); +}); diff --git a/ledger/language/class-override-super-method.ts b/ledger/language/class-override-super-method.ts new file mode 100644 index 0000000..86f2ed1 --- /dev/null +++ b/ledger/language/class-override-super-method.ts @@ -0,0 +1,20 @@ +// ledger: step 0, takes +// A method overridden in a class that extends another, `super.speak()` reaching the one it overrides. +class Animal { + constructor(public size: number) {} + speak() { + return this.size + 1; + } +} +class Bird extends Animal { + speak() { + return super.speak() + 100; + } +} +program(() => { + let k = 0; + while (k < 4) k += 1; + const a = new Animal(k); + const b = new Bird(k); + print(`${a.speak()} ${b.speak()}`); +}); diff --git a/ledger/language/class-parameter-properties.ts b/ledger/language/class-parameter-properties.ts new file mode 100644 index 0000000..c1b8e4d --- /dev/null +++ b/ledger/language/class-parameter-properties.ts @@ -0,0 +1,15 @@ +// ledger: step 0, takes +// Fields declared in the constructor's parameters: `constructor(public x: number, private y: number, readonly z = 9)`. +class P { + constructor(public x: number, private y: number, readonly z = 9) {} + sum() { + return this.x + this.y + this.z; + } +} +program(() => { + let k = 0; + while (k < 3) k += 1; + const p = new P(1, k); + while (p.x < 11) p.x += 1; + print(`${p.sum()}`); +}); diff --git a/ledger/language/class-private-field.ts b/ledger/language/class-private-field.ts new file mode 100644 index 0000000..fd96b0e --- /dev/null +++ b/ledger/language/class-private-field.ts @@ -0,0 +1,22 @@ +// ledger: step 0, takes +// A `#private` field, and one marked `private`: reached only from the class's own code. +class Bank { + #gold = 0; + private spent = 0; + pay(n: number) { + this.#gold += n; + this.spent += 1; + } + get gold() { + return this.#gold - this.spent; + } +} +program(() => { + const b = new Bank(); + let k = 0; + while (k < 4) { + b.pay(k + 2); + k += 1; + } + print(`${b.gold}`); +}); diff --git a/ledger/language/class-protected.ts b/ledger/language/class-protected.ts new file mode 100644 index 0000000..edf278b --- /dev/null +++ b/ledger/language/class-protected.ts @@ -0,0 +1,17 @@ +// ledger: step 0, takes +// A `protected` field: reached from the class and from classes that extend it. +class Soldier { + protected hp = 10; +} +class Hero extends Soldier { + hurt(n: number) { + this.hp -= n; + return this.hp; + } +} +program(() => { + const h = new Hero(); + let left = 10; + while (left > 0) left = h.hurt(3); + print(`${left}`); +}); diff --git a/ledger/language/class-readonly.ts b/ledger/language/class-readonly.ts new file mode 100644 index 0000000..9c09676 --- /dev/null +++ b/ledger/language/class-readonly.ts @@ -0,0 +1,16 @@ +// ledger: step 0, takes +// A `readonly` field: set once by the constructor, read after. +class Price { + readonly cost: number; + constructor(cost: number) { + this.cost = cost; + } +} +program(() => { + let n = 0; + while (n < 50) n += 1; + const p = new Price(n); + let ore = 400; + while (ore >= p.cost) ore -= p.cost; + print(`${ore}`); +}); diff --git a/ledger/language/class-recursive-method.ts b/ledger/language/class-recursive-method.ts new file mode 100644 index 0000000..cdb25ad --- /dev/null +++ b/ledger/language/class-recursive-method.ts @@ -0,0 +1,16 @@ +// ledger: step 1, takes +// A method that calls itself. +class M { + calls = 0; + fib(n: number): number { + this.calls++; + if (n < 2) return n; + return this.fib(n - 1) + this.fib(n - 2); + } +} +program(() => { + const m = new M(); + let n = 0; + while (n < 7) n += 1; + print(`${m.fib(n)} ${m.calls}`); +}); diff --git a/ledger/language/class-static.ts b/ledger/language/class-static.ts new file mode 100644 index 0000000..27c1948 --- /dev/null +++ b/ledger/language/class-static.ts @@ -0,0 +1,23 @@ +// ledger: step 0, takes +// A `static` field and a `static` method: one for the class, shared by every instance. +class Id { + static next = 1; + id = 0; + constructor() { + this.id = Id.next; + Id.next++; + } + static made() { + return Id.next - 1; + } +} +program(() => { + const a = new Id(); + const b = new Id(); + let k = 0; + while (k < 3) { + Id.next += 1; + k += 1; + } + print(`${a.id} ${b.id} ${Id.made()}`); +}); diff --git a/ledger/language/closure-made-in-loop.ts b/ledger/language/closure-made-in-loop.ts new file mode 100644 index 0000000..87eee15 --- /dev/null +++ b/ledger/language/closure-made-in-loop.ts @@ -0,0 +1,14 @@ +// ledger: step 9, refused: a variable a closure keeps +// A closure made at each turn of a loop over that turn's `const`, as JavaScript has it. +program(() => { + let total = 0; + let f: () => number = () => 0; + let n = 0; + while (n < 5) { + const k = n; + f = () => k + k; + total += f(); + n += 1; + } + print(`${total}`); +}); diff --git a/ledger/language/closure.ts b/ledger/language/closure.ts new file mode 100644 index 0000000..5c40a09 --- /dev/null +++ b/ledger/language/closure.ts @@ -0,0 +1,13 @@ +// ledger: step 9, refused: a variable a closure keeps +// A closure over a value worked out while the game runs: `adder(k)` gives a function that keeps its `k`. +function adder(k: number) { + return (x: number) => x + k; +} +program(() => { + let n = 0; + while (n < 5) n += 1; + const add = adder(n); + let total = 0; + while (total < 20) total = add(total); + print(`${total}`); +}); diff --git a/ledger/language/comma-operator.ts b/ledger/language/comma-operator.ts new file mode 100644 index 0000000..2b7a1e7 --- /dev/null +++ b/ledger/language/comma-operator.ts @@ -0,0 +1,10 @@ +// ledger: step 0, takes +// The comma operator, in a `for` loop's update: both sides run, in order. +program(() => { + let a = 0; + let b = 10; + let end = 0; + while (end < 5) end += 1; + for (let i = 0; i < end; i++, a += 2) b -= 1; + print(`${a} ${b}`); +}); diff --git a/ledger/language/comparisons.ts b/ledger/language/comparisons.ts new file mode 100644 index 0000000..14eb9eb --- /dev/null +++ b/ledger/language/comparisons.ts @@ -0,0 +1,16 @@ +// ledger: step 0, takes +// The comparisons `<`, `<=`, `>`, `>=`, `==` and `!=` of two values that change while the game runs. +program(() => { + let a = 0; + let b = 5; + let count = 0; + while (a < 10) { + if (a <= b) count += 1; + if (a > b) count += 10; + if (a >= b) count += 100; + if (a == b) count += 1000; + if (a != b) count += 10000; + a += 1; + } + print(`${count}`); +}); diff --git a/ledger/language/compound-assignment.ts b/ledger/language/compound-assignment.ts new file mode 100644 index 0000000..67278e2 --- /dev/null +++ b/ledger/language/compound-assignment.ts @@ -0,0 +1,13 @@ +// ledger: step 0, takes +// Compound assignment that takes a step: `+=`, `-=`, and `<<=` by a constant. +program(() => { + let n = 1; + let turns = 0; + while (n < 100) { + n <<= 2; + n += 1; + n -= 2; + turns += 1; + } + print(`${n} ${turns}`); +}); diff --git a/ledger/language/const-enum.ts b/ledger/language/const-enum.ts new file mode 100644 index 0000000..34973fd --- /dev/null +++ b/ledger/language/const-enum.ts @@ -0,0 +1,11 @@ +// ledger: step 0, takes +// A `const enum` with values given: each member the number it was given. +const enum Cost { + Marine = 50, + Medic = 75, +} +program(() => { + let ore = 400; + while (ore >= Cost.Medic) ore -= Cost.Marine; + print(`${ore}`); +}); diff --git a/ledger/language/const-from-build-time-code.ts b/ledger/language/const-from-build-time-code.ts new file mode 100644 index 0000000..0e8b282 --- /dev/null +++ b/ledger/language/const-from-build-time-code.ts @@ -0,0 +1,9 @@ +// ledger: step 0, takes +// A `const` worked out by code that runs when the map is built (an array's `reduce`, a loop), used in the program as a number. +const COSTS = [50, 75, 100, 150]; +const TOTAL = COSTS.reduce((a, b) => a + b, 0); +program(() => { + let spent = 0; + spent += TOTAL; + print(`${spent}`); +}); diff --git a/ledger/language/const-in-program.ts b/ledger/language/const-in-program.ts new file mode 100644 index 0000000..695e3f0 --- /dev/null +++ b/ledger/language/const-in-program.ts @@ -0,0 +1,8 @@ +// ledger: step 0, takes +// A `const` inside the program holding a value worked out while the game runs: it cannot be changed after. +program(() => { + let n = 0; + while (n < 7) n += 1; + const twice = n + n; + print(`${twice}`); +}); diff --git a/ledger/language/const-known-when-built.ts b/ledger/language/const-known-when-built.ts new file mode 100644 index 0000000..c1f31f0 --- /dev/null +++ b/ledger/language/const-known-when-built.ts @@ -0,0 +1,9 @@ +// ledger: step 0, takes +// A `const` whose value is known when the map is built: worked out then, and used as that number. +const WAVES = 4; +const PER_WAVE = WAVES * 5 + 2; +program(() => { + let left = PER_WAVE; + left -= 1; + print(`${left}`); +}); diff --git a/ledger/language/continue.ts b/ledger/language/continue.ts new file mode 100644 index 0000000..9889efb --- /dev/null +++ b/ledger/language/continue.ts @@ -0,0 +1,12 @@ +// ledger: step 0, takes +// `continue` goes on to the loop's next turn, skipping the rest of the body. +program(() => { + let n = 0; + let counted = 0; + while (n < 9) { + n += 1; + if (n == 3 || n == 6) continue; + counted += 1; + } + print(`${counted}`); +}); diff --git a/ledger/language/default-parameter.ts b/ledger/language/default-parameter.ts new file mode 100644 index 0000000..df74bcd --- /dev/null +++ b/ledger/language/default-parameter.ts @@ -0,0 +1,15 @@ +// ledger: step 1, takes +// A parameter with a default value, `step = 1`, used when the call leaves it out. +function advance(n: number, step = 1) { + return n + step; +} +program(() => { + let n = 0; + let turns = 0; + while (turns < 4) { + n = advance(n); + n = advance(n, 10); + turns += 1; + } + print(`${n}`); +}); diff --git a/ledger/language/destructuring-parameter.ts b/ledger/language/destructuring-parameter.ts new file mode 100644 index 0000000..1b06e4b --- /dev/null +++ b/ledger/language/destructuring-parameter.ts @@ -0,0 +1,15 @@ +// ledger: step 0, takes +// A function whose parameter is destructured: `function area({ w, h }: Size)`. +function area({ w, h }: { w: number; h: number }) { + return w + h + h; +} +program(() => { + let s = { w: 2, h: 3 }; + let total = 0; + while (s.w < 8) { + total += area(s); + total += area({ w: 1, h: s.w }); + s.w += 2; + } + print(`${total}`); +}); diff --git a/ledger/language/do-while-break-continue.ts b/ledger/language/do-while-break-continue.ts new file mode 100644 index 0000000..86b2e52 --- /dev/null +++ b/ledger/language/do-while-break-continue.ts @@ -0,0 +1,13 @@ +// ledger: step 0, takes +// `break` and `continue` inside a `do … while` loop. +program(() => { + let n = 0; + let total = 0; + do { + n += 1; + if (n == 2) continue; + if (n > 5) break; + total += n; + } while (n < 10); + print(`${total}`); +}); diff --git a/ledger/language/do-while-loop.ts b/ledger/language/do-while-loop.ts new file mode 100644 index 0000000..8797a8e --- /dev/null +++ b/ledger/language/do-while-loop.ts @@ -0,0 +1,9 @@ +// ledger: step 0, takes +// A `do … while` loop: the body runs once before the test is asked. +program(() => { + let n = 0; + do { + n += 4; + } while (n < 10); + print(`${n}`); +}); diff --git a/ledger/language/double-not.ts b/ledger/language/double-not.ts new file mode 100644 index 0000000..5485f5a --- /dev/null +++ b/ledger/language/double-not.ts @@ -0,0 +1,13 @@ +// ledger: step 0, takes +// `!!n`: a number as a boolean, true when it is not 0. +program(() => { + let n = 3; + let on = false; + let turns = 0; + while (turns < 5) { + on = !!n; + if (on) n -= 1; + turns += 1; + } + print(`${n}`); +}); diff --git a/ledger/language/else-if-chain.ts b/ledger/language/else-if-chain.ts new file mode 100644 index 0000000..d09f8a6 --- /dev/null +++ b/ledger/language/else-if-chain.ts @@ -0,0 +1,15 @@ +// ledger: step 0, takes +// A chain of `else if`: the first test that holds picks the branch. +program(() => { + let n = 0; + let small = 0; + let middle = 0; + let large = 0; + while (n < 12) { + if (n < 4) small += 1; + else if (n < 8) middle += 1; + else large += 1; + n += 1; + } + print(`${small} ${middle} ${large}`); +}); diff --git a/ledger/language/enum.ts b/ledger/language/enum.ts new file mode 100644 index 0000000..85c16de --- /dev/null +++ b/ledger/language/enum.ts @@ -0,0 +1,16 @@ +// ledger: step 0, takes +// An `enum` declared outside the program: its members are numbers known when the map is built. +enum Phase { + Build, + Fight, + Done, +} +program(() => { + let phase = Phase.Build; + let turns = 0; + while (phase != Phase.Done) { + phase = phase == Phase.Build ? Phase.Fight : Phase.Done; + turns += 1; + } + print(`${turns}`); +}); diff --git a/ledger/language/error-class.ts b/ledger/language/error-class.ts new file mode 100644 index 0000000..5c4265a --- /dev/null +++ b/ledger/language/error-class.ts @@ -0,0 +1,14 @@ +// ledger: step 11, refused: an error thrown or caught (try, throw) +// A class of error of the script's own, `class Late extends Error`, told apart in a `catch` with `instanceof`. +class Late extends Error {} +program(() => { + let k = 0; + while (k < 2) k += 1; + try { + if (k > 1) throw new Late("late"); + throw new RangeError("range"); + } catch (e) { + if (e instanceof Late) print("late"); + else print("other"); + } +}); diff --git a/ledger/language/for-break-continue.ts b/ledger/language/for-break-continue.ts new file mode 100644 index 0000000..c7a24e0 --- /dev/null +++ b/ledger/language/for-break-continue.ts @@ -0,0 +1,13 @@ +// ledger: step 0, takes +// `break` and `continue` inside a `for` loop that stays a loop in the map. +program(() => { + let end = 0; + while (end < 15) end += 1; + let total = 0; + for (let i = 0; i < end; i++) { + if (i == 2) continue; + if (i > 6) break; + total += i; + } + print(`${total}`); +}); diff --git a/ledger/language/for-loop-unrolled.ts b/ledger/language/for-loop-unrolled.ts new file mode 100644 index 0000000..3494588 --- /dev/null +++ b/ledger/language/for-loop-unrolled.ts @@ -0,0 +1,10 @@ +// ledger: step 0, takes +// A `for` loop over bounds known when the map is built: worked out then, its body written out once a turn. +program(() => { + let total = 0; + for (let i = 0; i < 4; i++) { + total += i; + sleep(frames(1)); + } + print(`${total}`); +}); diff --git a/ledger/language/for-loop.ts b/ledger/language/for-loop.ts new file mode 100644 index 0000000..6c87e3d --- /dev/null +++ b/ledger/language/for-loop.ts @@ -0,0 +1,9 @@ +// ledger: step 0, takes +// A `for` loop whose end is a value that changes while the game runs, so it stays a loop in the map. +program(() => { + let end = 0; + while (end < 5) end += 1; + let total = 0; + for (let i = 0; i < end; i++) total += i; + print(`${total}`); +}); diff --git a/ledger/language/for-of-array.ts b/ledger/language/for-of-array.ts new file mode 100644 index 0000000..a793860 --- /dev/null +++ b/ledger/language/for-of-array.ts @@ -0,0 +1,13 @@ +// ledger: step 3, takes +// `for (const x of xs)` over a fixed-length array of the program's, its cells changed while the game runs. +program(() => { + let xs = [3, 5, 8]; + let n = 0; + while (n < 3) { + xs[n] += n; + n += 1; + } + let total = 0; + for (const x of xs) total += x; + print(`${total}`); +}); diff --git a/ledger/language/for-of-known-list.ts b/ledger/language/for-of-known-list.ts new file mode 100644 index 0000000..b3856f4 --- /dev/null +++ b/ledger/language/for-of-known-list.ts @@ -0,0 +1,10 @@ +// ledger: step 0, takes +// `for (const x of [3, 5, 8])`: a loop over a list known when the map is built, written out once an item. +program(() => { + let total = 0; + for (const cost of [3, 5, 8]) { + total += cost; + sleep(frames(1)); + } + print(`${total}`); +}); diff --git a/ledger/language/function-boolean-parameter.ts b/ledger/language/function-boolean-parameter.ts new file mode 100644 index 0000000..efd8c9e --- /dev/null +++ b/ledger/language/function-boolean-parameter.ts @@ -0,0 +1,18 @@ +// ledger: step 1, takes +// A function that takes a boolean parameter, given a boolean variable. +function step(n: number, up: boolean) { + if (up) return n + 1; + return n - 1; +} +program(() => { + let n = 5; + let turns = 0; + let up = true; + while (turns < 6) { + up = turns < 2; + n = step(n, up); + n = step(n, false); + turns += 1; + } + print(`${n}`); +}); diff --git a/ledger/language/function-boolean-result.ts b/ledger/language/function-boolean-result.ts new file mode 100644 index 0000000..240d15b --- /dev/null +++ b/ledger/language/function-boolean-result.ts @@ -0,0 +1,15 @@ +// ledger: step 1, takes +// A function that returns a boolean, called from more than one place and tested. +function isBig(n: number): boolean { + return n > 5; +} +program(() => { + let n = 0; + let count = 0; + while (n < 9) { + if (isBig(n)) count += 1; + if (isBig(n + 3)) count += 10; + n += 1; + } + print(`${count}`); +}); diff --git a/ledger/language/function-called.ts b/ledger/language/function-called.ts new file mode 100644 index 0000000..c45bcf2 --- /dev/null +++ b/ledger/language/function-called.ts @@ -0,0 +1,13 @@ +// ledger: step 1, takes +// A function called from more than one place: one copy in the map, its number parameter set by each call and its number result read back. +function bonus(n: number) { + return n + n + 1; +} +program(() => { + let a = 0; + while (a < 3) a += 1; + let b = a + 4; + a = bonus(a); + b = bonus(b); + print(`${a} ${b}`); +}); diff --git a/ledger/language/function-early-return.ts b/ledger/language/function-early-return.ts new file mode 100644 index 0000000..5f246ee --- /dev/null +++ b/ledger/language/function-early-return.ts @@ -0,0 +1,17 @@ +// ledger: step 1, takes +// `return` from the middle of a function: what follows it in the function does not run. +function cost(n: number) { + if (n > 3) return 100; + if (n > 1) return 10; + return 1; +} +program(() => { + let n = 0; + let total = 0; + while (n < 6) { + total += cost(n); + n += 1; + } + total += cost(total); + print(`${total}`); +}); diff --git a/ledger/language/function-in-program.ts b/ledger/language/function-in-program.ts new file mode 100644 index 0000000..cf62948 --- /dev/null +++ b/ledger/language/function-in-program.ts @@ -0,0 +1,10 @@ +// ledger: step 1, takes +// A function declared inside the program, beside the variables it uses. +program(() => { + let n = 0; + function twice(x: number) { + return x + x; + } + while (n < 50) n = twice(n) + 1; + print(`${twice(n)}`); +}); diff --git a/ledger/language/function-inlined.ts b/ledger/language/function-inlined.ts new file mode 100644 index 0000000..7081e79 --- /dev/null +++ b/ledger/language/function-inlined.ts @@ -0,0 +1,11 @@ +// ledger: step 0, takes +// A function called once: its body put where it is called, its parameter the value it was given. +function bonus(n: number) { + return n + n + 1; +} +program(() => { + let gold = 0; + while (gold < 15) gold += 1; + gold = bonus(gold); + print(`${gold}`); +}); diff --git a/ledger/language/function-overloads.ts b/ledger/language/function-overloads.ts new file mode 100644 index 0000000..675d93b --- /dev/null +++ b/ledger/language/function-overloads.ts @@ -0,0 +1,17 @@ +// ledger: step 1, takes +// A function with overload signatures: the types are checked, the one body is what runs. +function add(a: number): number; +function add(a: number, b: number): number; +function add(a: number, b = 1) { + return a + b; +} +program(() => { + let n = 0; + let turns = 0; + while (turns < 3) { + n = add(n, 2); + n = add(n, turns); + turns += 1; + } + print(`${n}`); +}); diff --git a/ledger/language/function-that-sleeps.ts b/ledger/language/function-that-sleeps.ts new file mode 100644 index 0000000..e629749 --- /dev/null +++ b/ledger/language/function-that-sleeps.ts @@ -0,0 +1,12 @@ +// ledger: step 0, takes +// A function that sleeps: put where each call is, the program waiting inside it. +function pause(n: number) { + sleep(frames(2)); + return n + 1; +} +program(() => { + let n = 0; + n = pause(n); + n = pause(n); + print(`${n}`); +}); diff --git a/ledger/language/function-value-array.ts b/ledger/language/function-value-array.ts new file mode 100644 index 0000000..e395fc2 --- /dev/null +++ b/ledger/language/function-value-array.ts @@ -0,0 +1,21 @@ +// ledger: step 3, takes +// An array of functions, one picked by an index that changes while the game runs. +function inc(n: number) { + return n + 1; +} +function triple(n: number) { + return n + n + n; +} +program(() => { + const steps = [inc, triple]; + let n = 1; + let which = 0; + let turns = 0; + while (turns < 4) { + n = steps[which](n); + n = steps[1 - which](n); + which = 1 - which; + turns += 1; + } + print(`${n}`); +}); diff --git a/ledger/language/function-value.ts b/ledger/language/function-value.ts new file mode 100644 index 0000000..ae99df4 --- /dev/null +++ b/ledger/language/function-value.ts @@ -0,0 +1,19 @@ +// ledger: step 1, takes +// A function kept as a value and chosen while the game runs: `const f = turns < 3 ? inc : dec; f(n)`. +function inc(n: number) { + return n + 1; +} +function dec(n: number) { + return n - 1; +} +program(() => { + let n = 10; + let turns = 0; + while (turns < 5) { + const f = turns < 3 ? inc : dec; + n = f(n); + n = f(n + 1); + turns += 1; + } + print(`${n}`); +}); diff --git a/ledger/language/function-void.ts b/ledger/language/function-void.ts new file mode 100644 index 0000000..4e2ed1f --- /dev/null +++ b/ledger/language/function-void.ts @@ -0,0 +1,15 @@ +// ledger: step 1, takes +// A function with no result that changes the program's variables: called as a statement. +program(() => { + let total = 0; + function add(n: number) { + total += n; + } + let n = 0; + while (n < 4) { + add(n); + add(1); + n += 1; + } + print(`${total}`); +}); diff --git a/ledger/language/game-function.ts b/ledger/language/game-function.ts new file mode 100644 index 0000000..2e612db --- /dev/null +++ b/ledger/language/game-function.ts @@ -0,0 +1,13 @@ +// ledger: step 1, takes +// `game(…)`: a function for programs to call, which may be exported from another file; its body follows a program's rules. +const award = game((n: number) => n + n + 5); +program(() => { + let gold = 0; + let turns = 0; + while (turns < 3) { + gold = award(gold); + gold = award(turns); + turns += 1; + } + print(`${gold}`); +}); diff --git a/ledger/language/game-loop.ts b/ledger/language/game-loop.ts new file mode 100644 index 0000000..133e842 --- /dev/null +++ b/ledger/language/game-loop.ts @@ -0,0 +1,10 @@ +// ledger: step 0, takes +// A loop that goes on for ever, `while (true)`, giving the frame back with `sleep(frames(1))` at every turn. +program(() => { + let n = 0; + while (true) { + n += 1; + if (n == 48) print("two seconds"); + sleep(frames(1)); + } +}); diff --git a/ledger/language/generator-early-return.ts b/ledger/language/generator-early-return.ts new file mode 100644 index 0000000..71bbeb2 --- /dev/null +++ b/ledger/language/generator-early-return.ts @@ -0,0 +1,17 @@ +// ledger: step 0, takes +// A generator that `return`s before its end: the `for…of` walking it ends there. +function* untilBig(limit: number) { + let i = 0; + while (true) { + if (i > limit) return; + yield i; + i += 2; + } +} +program(() => { + let n = 0; + while (n < 7) n += 1; + let total = 0; + for (const x of untilBig(n)) total += x; + print(`${total}`); +}); diff --git a/ledger/language/generator-for-of.ts b/ledger/language/generator-for-of.ts new file mode 100644 index 0000000..a2988b2 --- /dev/null +++ b/ledger/language/generator-for-of.ts @@ -0,0 +1,16 @@ +// ledger: step 0, takes +// A generator, `function*`, walked by `for…of`: the loop's body put where each `yield` is. +function* upTo(n: number) { + let i = 0; + while (i < n) { + yield i; + i += 1; + } +} +program(() => { + let end = 0; + while (end < 5) end += 1; + let total = 0; + for (const x of upTo(end)) total += x; + print(`${total}`); +}); diff --git a/ledger/language/generator-yield-star.ts b/ledger/language/generator-yield-star.ts new file mode 100644 index 0000000..8d4ab47 --- /dev/null +++ b/ledger/language/generator-yield-star.ts @@ -0,0 +1,20 @@ +// ledger: step 0, takes +// `yield*` of another generator inside a generator; `break` leaves both. +function* pair(n: number) { + yield n; + yield n + 1; +} +function* all(n: number) { + yield* pair(n); + yield* pair(n + 10); +} +program(() => { + let k = 0; + while (k < 2) k += 1; + let total = 0; + for (const x of all(k)) { + if (x > 12) break; + total += x; + } + print(`${total}`); +}); diff --git a/ledger/language/generic-function.ts b/ledger/language/generic-function.ts new file mode 100644 index 0000000..7cd3379 --- /dev/null +++ b/ledger/language/generic-function.ts @@ -0,0 +1,15 @@ +// ledger: step 1, takes +// A generic function, `function larger(…)`: the type is gone in the map, the function the same for every call. +function larger(a: T, b: T): T { + return a > b ? a : b; +} +program(() => { + let n = 0; + let total = 0; + while (n < 4) { + total += larger(n, 2); + total += larger(5, n); + n += 1; + } + print(`${total}`); +}); diff --git a/ledger/language/i32-cast.ts b/ledger/language/i32-cast.ts new file mode 100644 index 0000000..7b7a427 --- /dev/null +++ b/ledger/language/i32-cast.ts @@ -0,0 +1,11 @@ +// ledger: step 0, takes +// `i32(x)`: the same 32 bits read as a signed number again. +program(() => { + let n = u32(5); + let count = 0; + while (n > 0) { + if (i32(n) < 3) count += 1; + n -= 1; + } + print(`${count}`); +}); diff --git a/ledger/language/if-else.ts b/ledger/language/if-else.ts new file mode 100644 index 0000000..ced5850 --- /dev/null +++ b/ledger/language/if-else.ts @@ -0,0 +1,10 @@ +// ledger: step 0, takes +// `if` with an `else`, on a number that changes while the game runs. +program(() => { + let n = 0; + while (n < 7) { + if (n < 3) n += 2; + else n += 1; + } + print(`${n}`); +}); diff --git a/ledger/language/iife.ts b/ledger/language/iife.ts new file mode 100644 index 0000000..ccb0c78 --- /dev/null +++ b/ledger/language/iife.ts @@ -0,0 +1,8 @@ +// ledger: step 0, takes +// A function called where it is written, `(() => { … })()`. +program(() => { + let n = 0; + while (n < 4) n += 1; + const doubled = ((x: number) => x + x)(n); + print(`${doubled}`); +}); diff --git a/ledger/language/in-at-build.ts b/ledger/language/in-at-build.ts new file mode 100644 index 0000000..036f05e --- /dev/null +++ b/ledger/language/in-at-build.ts @@ -0,0 +1,10 @@ +// ledger: step 0, takes +// `"field" in record` worked out when the map is built. +const BOSS: { hp: number } | { hp: number; shield: number } = { hp: 100, shield: 50 }; +program(() => { + let total = 0; + if ("shield" in BOSS) total += BOSS.shield; + total += BOSS.hp; + total -= 1; + print(`${total}`); +}); diff --git a/ledger/language/increment-as-value.ts b/ledger/language/increment-as-value.ts new file mode 100644 index 0000000..53c1bd9 --- /dev/null +++ b/ledger/language/increment-as-value.ts @@ -0,0 +1,11 @@ +// ledger: step 0, takes +// `n++` and `++n` as values: the number before the step, and the one after. +program(() => { + let n = 0; + let total = 0; + while (total < 20) { + total += n++; + total += ++n; + } + print(`${total} ${n}`); +}); diff --git a/ledger/language/increment-decrement.ts b/ledger/language/increment-decrement.ts new file mode 100644 index 0000000..6b0b428 --- /dev/null +++ b/ledger/language/increment-decrement.ts @@ -0,0 +1,11 @@ +// ledger: step 0, takes +// `++` and `--` on a variable, before and after. +program(() => { + let up = 0; + let down = 10; + while (up < 5) { + up++; + --down; + } + print(`${up} ${down}`); +}); diff --git a/ledger/language/instanceof.ts b/ledger/language/instanceof.ts new file mode 100644 index 0000000..9d9ac84 --- /dev/null +++ b/ledger/language/instanceof.ts @@ -0,0 +1,17 @@ +// ledger: step 0, takes +// `instanceof`, answered when the map is built: the class of each instance is known then. +class Animal { + constructor(public size: number) {} +} +class Bird extends Animal { + wings = 2; +} +function describe(a: Animal) { + if (a instanceof Bird) return a.wings + a.size; + return a.size; +} +program(() => { + let k = 0; + while (k < 4) k += 1; + print(`${describe(new Animal(k))} ${describe(new Bird(k))}`); +}); diff --git a/ledger/language/labeled-break.ts b/ledger/language/labeled-break.ts new file mode 100644 index 0000000..e33209c --- /dev/null +++ b/ledger/language/labeled-break.ts @@ -0,0 +1,16 @@ +// ledger: step 0, takes +// A label on a loop, and `break outer` from a loop inside it: both loops are left. +program(() => { + let i = 0; + let turns = 0; + outer: while (i < 5) { + let j = 0; + while (j < 5) { + if (i + j > 4) break outer; + turns += 1; + j += 1; + } + i += 1; + } + print(`${turns}`); +}); diff --git a/ledger/language/labeled-continue.ts b/ledger/language/labeled-continue.ts new file mode 100644 index 0000000..048f257 --- /dev/null +++ b/ledger/language/labeled-continue.ts @@ -0,0 +1,16 @@ +// ledger: step 0, takes +// `continue outer` from a loop inside another: the outer loop goes on to its next turn. +program(() => { + let i = 0; + let turns = 0; + outer: while (i < 4) { + i += 1; + let j = 0; + while (j < 4) { + j += 1; + if (j > i) continue outer; + turns += 1; + } + } + print(`${turns}`); +}); diff --git a/ledger/language/let-variable.ts b/ledger/language/let-variable.ts new file mode 100644 index 0000000..3394ab8 --- /dev/null +++ b/ledger/language/let-variable.ts @@ -0,0 +1,7 @@ +// ledger: step 0, takes +// A `let` variable: a number the program keeps, set and changed while the game runs. +program(() => { + let gold = 50; + while (gold < 100) gold += 25; + print(`${gold}`); +}); diff --git a/ledger/language/math-min-max-abs.ts b/ledger/language/math-min-max-abs.ts new file mode 100644 index 0000000..514b887 --- /dev/null +++ b/ledger/language/math-min-max-abs.ts @@ -0,0 +1,11 @@ +// ledger: step 0, takes +// `Math.min`, `Math.max` and `Math.abs` of values that change while the game runs. +program(() => { + let n = -3; + let total = 0; + while (n < 4) { + total += Math.min(n, 1) + Math.max(n, 0) + Math.abs(n); + n += 1; + } + print(`${total}`); +}); diff --git a/ledger/language/method-returning-boolean.ts b/ledger/language/method-returning-boolean.ts new file mode 100644 index 0000000..90ccfb2 --- /dev/null +++ b/ledger/language/method-returning-boolean.ts @@ -0,0 +1,18 @@ +// ledger: step 1, takes +// A method that returns a boolean, called from more than one place and tested in an `if`. +class Timer { + left = 5; + done(): boolean { + return this.left <= 0; + } +} +program(() => { + const t = new Timer(); + let ticks = 0; + while (ticks < 10) { + if (t.done()) break; + t.left -= 1; + ticks += 1; + } + if (t.done()) print(`${ticks}`); +}); diff --git a/ledger/language/mutual-recursion.ts b/ledger/language/mutual-recursion.ts new file mode 100644 index 0000000..9a41caf --- /dev/null +++ b/ledger/language/mutual-recursion.ts @@ -0,0 +1,16 @@ +// ledger: step 1, takes +// Two functions that call each other. +function isEven(n: number): boolean { + if (n == 0) return true; + return isOdd(n - 1); +} +function isOdd(n: number): boolean { + if (n == 0) return false; + return isEven(n - 1); +} +program(() => { + let n = 0; + while (n < 7) n += 1; + if (isEven(n)) print("even"); + else print("odd"); +}); diff --git a/ledger/language/namespace.ts b/ledger/language/namespace.ts new file mode 100644 index 0000000..30e8100 --- /dev/null +++ b/ledger/language/namespace.ts @@ -0,0 +1,11 @@ +// ledger: step 0, takes +// A `namespace` outside the program holding constants known when the map is built. +namespace Costs { + export const Marine = 50; + export const Medic = 75; +} +program(() => { + let ore = 400; + while (ore >= Costs.Medic) ore -= Costs.Marine; + print(`${ore}`); +}); diff --git a/ledger/language/negation.ts b/ledger/language/negation.ts new file mode 100644 index 0000000..a9f92d9 --- /dev/null +++ b/ledger/language/negation.ts @@ -0,0 +1,11 @@ +// ledger: step 0, takes +// A number turned negative, `-n`, while the game runs. +program(() => { + let n = 4; + let total = 0; + while (n > 0) { + total += -n; + n -= 1; + } + print(`${total}`); +}); diff --git a/ledger/language/nested-destructuring.ts b/ledger/language/nested-destructuring.ts new file mode 100644 index 0000000..e33debb --- /dev/null +++ b/ledger/language/nested-destructuring.ts @@ -0,0 +1,8 @@ +// ledger: step 0, takes +// Destructuring a record inside a record: `const { at: { x } } = p`. +program(() => { + let p = { hp: 5, at: { x: 1, y: 2 } }; + p.at.x += 6; + const { hp, at: { x } } = p; + print(`${hp} ${x}`); +}); diff --git a/ledger/language/nested-function.ts b/ledger/language/nested-function.ts new file mode 100644 index 0000000..b2b22ba --- /dev/null +++ b/ledger/language/nested-function.ts @@ -0,0 +1,18 @@ +// ledger: step 1, takes +// A function declared inside another function, using its parameter. +function total(n: number) { + function sq(k: number) { + return k + k; + } + return sq(n) + sq(n + 1); +} +program(() => { + let n = 0; + let sum = 0; + while (n < 3) { + sum += total(n); + sum += total(n + 5); + n += 1; + } + print(`${sum}`); +}); diff --git a/ledger/language/nested-loops.ts b/ledger/language/nested-loops.ts new file mode 100644 index 0000000..0eee254 --- /dev/null +++ b/ledger/language/nested-loops.ts @@ -0,0 +1,15 @@ +// ledger: step 0, takes +// A loop inside another, both running while the game runs. +program(() => { + let i = 0; + let total = 0; + while (i < 4) { + let j = 0; + while (j < i) { + total += 1; + j += 1; + } + i += 1; + } + print(`${total}`); +}); diff --git a/ledger/language/nested-ternary.ts b/ledger/language/nested-ternary.ts new file mode 100644 index 0000000..1080caf --- /dev/null +++ b/ledger/language/nested-ternary.ts @@ -0,0 +1,11 @@ +// ledger: step 0, takes +// `?:` inside another `?:`. +program(() => { + let n = 0; + let total = 0; + while (n < 9) { + total += n < 3 ? 1 : n < 6 ? 10 : 100; + n += 1; + } + print(`${total}`); +}); diff --git a/ledger/language/non-null-assertion.ts b/ledger/language/non-null-assertion.ts new file mode 100644 index 0000000..6670af4 --- /dev/null +++ b/ledger/language/non-null-assertion.ts @@ -0,0 +1,11 @@ +// ledger: step 0, takes +// The non-null assertion `x!`: it changes nothing in the map. +program(() => { + let boss: { hp: number } | null = { hp: 10 }; + let hits = 0; + while (boss!.hp > 0) { + boss!.hp -= 3; + hits += 1; + } + print(`${hits}`); +}); diff --git a/ledger/language/not.ts b/ledger/language/not.ts new file mode 100644 index 0000000..a6d362e --- /dev/null +++ b/ledger/language/not.ts @@ -0,0 +1,11 @@ +// ledger: step 0, takes +// `!` turns a test around. +program(() => { + let n = 0; + let count = 0; + while (n < 10) { + if (!(n < 4)) count += 1; + n += 1; + } + print(`${count}`); +}); diff --git a/ledger/language/nullish-coalescing.ts b/ledger/language/nullish-coalescing.ts new file mode 100644 index 0000000..e5bcd10 --- /dev/null +++ b/ledger/language/nullish-coalescing.ts @@ -0,0 +1,13 @@ +// ledger: step 0, takes +// `??` on a number that may be `undefined`: the other side when it is. +program(() => { + let k = 0; + let total = 0; + while (k < 6) { + let n: number | undefined = undefined; + if (k > 2) n = k; + total += n ?? 10; + k += 1; + } + print(`${total}`); +}); diff --git a/ledger/language/number-as-test.ts b/ledger/language/number-as-test.ts new file mode 100644 index 0000000..fa81e06 --- /dev/null +++ b/ledger/language/number-as-test.ts @@ -0,0 +1,11 @@ +// ledger: step 0, takes +// A number used as a test, `while (n)`: true when it is not 0. +program(() => { + let n = 3; + let turns = 0; + while (n) { + n -= 1; + turns += 1; + } + print(`${turns}`); +}); diff --git a/ledger/language/number-literals.ts b/ledger/language/number-literals.ts new file mode 100644 index 0000000..c7f7c8e --- /dev/null +++ b/ledger/language/number-literals.ts @@ -0,0 +1,7 @@ +// ledger: step 0, takes +// Numbers written in hexadecimal, binary and with separators: `0x10`, `0b101`, `1_000`. +program(() => { + let n = 0; + while (n < 1_000) n += 0x10 + 0b101; + print(`${n}`); +}); diff --git a/ledger/language/object-destructuring-rename-default.ts b/ledger/language/object-destructuring-rename-default.ts new file mode 100644 index 0000000..c7e50a8 --- /dev/null +++ b/ledger/language/object-destructuring-rename-default.ts @@ -0,0 +1,8 @@ +// ledger: step 0, takes +// Destructuring that renames a field and gives one that is missing a default: `const { x: left, z = 5 } = …`. +program(() => { + let pos: { x: number; z?: number } = { x: 1 }; + while (pos.x < 3) pos.x += 1; + const { x: left, z = 5 } = pos; + print(`${left} ${z}`); +}); diff --git a/ledger/language/object-destructuring.ts b/ledger/language/object-destructuring.ts new file mode 100644 index 0000000..9ca18df --- /dev/null +++ b/ledger/language/object-destructuring.ts @@ -0,0 +1,8 @@ +// ledger: step 0, takes +// Destructuring a record into variables: `const { x, y } = pos`. +program(() => { + let pos = { x: 1, y: 2 }; + while (pos.x < 5) pos.x += 1; + const { x, y } = pos; + print(`${x + y}`); +}); diff --git a/ledger/language/object-rest.ts b/ledger/language/object-rest.ts new file mode 100644 index 0000000..6b8ab18 --- /dev/null +++ b/ledger/language/object-rest.ts @@ -0,0 +1,8 @@ +// ledger: step 0, takes +// Destructuring one field and the rest into another record: `const { x, ...others } = p`. +program(() => { + let p = { x: 1, y: 2, z: 3 }; + while (p.z < 7) p.z += 1; + const { x, ...others } = p; + print(`${x} ${others.z}`); +}); diff --git a/ledger/language/optional-chaining.ts b/ledger/language/optional-chaining.ts new file mode 100644 index 0000000..094df9a --- /dev/null +++ b/ledger/language/optional-chaining.ts @@ -0,0 +1,16 @@ +// ledger: step 9, refused: a warning TrigScript shows every player ("more than 1023 closures and their variables are kept at once") +// `?.` reading a number through an instance that may be `null`: `a?.v ?? 0`. +class Node { + constructor(public v: number) {} +} +program(() => { + let a: Node | null = null; + let k = 0; + let total = 0; + while (k < 4) { + if (k == 2) a = new Node(k); + total += a?.v ?? 10; + k += 1; + } + print(`${total}`); +}); diff --git a/ledger/language/optional-parameter.ts b/ledger/language/optional-parameter.ts new file mode 100644 index 0000000..baf7e7c --- /dev/null +++ b/ledger/language/optional-parameter.ts @@ -0,0 +1,16 @@ +// ledger: step 0, takes +// An optional parameter, `extra?: number`, tested for `undefined` inside the function. +function advance(n: number, extra?: number) { + if (extra !== undefined) return n + extra; + return n + 1; +} +program(() => { + let n = 0; + let turns = 0; + while (turns < 4) { + n = advance(n); + n = advance(n, 10); + turns += 1; + } + print(`${n}`); +}); diff --git a/ledger/language/or.ts b/ledger/language/or.ts new file mode 100644 index 0000000..0ac772f --- /dev/null +++ b/ledger/language/or.ts @@ -0,0 +1,11 @@ +// ledger: step 0, takes +// `||` of two tests: true when either holds. +program(() => { + let n = 0; + let count = 0; + while (n < 10) { + if (n < 2 || n > 7) count += 1; + n += 1; + } + print(`${count}`); +}); diff --git a/ledger/language/player-variable.ts b/ledger/language/player-variable.ts new file mode 100644 index 0000000..586bc69 --- /dev/null +++ b/ledger/language/player-variable.ts @@ -0,0 +1,11 @@ +// ledger: step 0, takes +// A player kept in a variable, changed and compared while the game runs: `let p: Player = P10`. +program(() => { + let p: Player = P10; + let n = 0; + while (n < 3) { + n += 1; + if (n == 2) p = P11; + } + if (p == P11) print("eleven"); +}); diff --git a/ledger/language/players-compared-when-built.ts b/ledger/language/players-compared-when-built.ts new file mode 100644 index 0000000..910507c --- /dev/null +++ b/ledger/language/players-compared-when-built.ts @@ -0,0 +1,14 @@ +// ledger: step 0, takes +// Players compared in a function run when the map is built: `P1`, `P2`, `P9`, `players.P12`, each a slot known then. +function slotOf(p: Player) { + if (p == P1) return 1; + if (p == P2) return 2; + if (p == P9) return 9; + if (p == players.P12) return 12; + return 0; +} +program(() => { + let total = slotOf(P2) + slotOf(P9) + slotOf(P12); + total += 1; + print(`${total}`); +}); diff --git a/ledger/language/print-to-current-player.ts b/ledger/language/print-to-current-player.ts new file mode 100644 index 0000000..f1fa57f --- /dev/null +++ b/ledger/language/print-to-current-player.ts @@ -0,0 +1,7 @@ +// ledger: step 0, takes +// A print to `CurrentPlayer`, the player the program runs as. +program(() => { + let n = 0; + while (n < 3) n += 1; + print(`${n}`, { to: CurrentPlayer }); +}); diff --git a/ledger/language/program-for-all-players.ts b/ledger/language/program-for-all-players.ts new file mode 100644 index 0000000..df7d633 --- /dev/null +++ b/ledger/language/program-for-all-players.ts @@ -0,0 +1,10 @@ +// ledger: step 7, refused: a map of our own has no per-player program yet +// A program run for every player, `{ owner: AllPlayers }`: `CurrentPlayer` is the one it is running for. +program( + () => { + let n = 0; + while (n < 3) n += 1; + print(`${n}`, { to: CurrentPlayer }); + }, + { owner: AllPlayers }, +); diff --git a/ledger/language/program-for-several-players.ts b/ledger/language/program-for-several-players.ts new file mode 100644 index 0000000..3d38097 --- /dev/null +++ b/ledger/language/program-for-several-players.ts @@ -0,0 +1,10 @@ +// ledger: step 7, refused: a map of our own has no per-player program yet +// A program run once for each of several players at the same time, `{ owner: [P4, P5, P6] }`, each with variables of their own. +program( + () => { + let n = 0; + while (n < 3) n += 1; + print(`${n}`, { to: CurrentPlayer }); + }, + { owner: [P4, P5, P6] }, +); diff --git a/ledger/language/program-owner.ts b/ledger/language/program-owner.ts new file mode 100644 index 0000000..8817dd1 --- /dev/null +++ b/ledger/language/program-owner.ts @@ -0,0 +1,10 @@ +// ledger: step 7, takes +// A program run as another player than the first: `program(…, { owner: P3 })`. +program( + () => { + let n = 0; + while (n < 3) n += 1; + print(`${n}`); + }, + { owner: P3 }, +); diff --git a/ledger/language/record-assigned-whole.ts b/ledger/language/record-assigned-whole.ts new file mode 100644 index 0000000..21ca923 --- /dev/null +++ b/ledger/language/record-assigned-whole.ts @@ -0,0 +1,11 @@ +// ledger: step 0, takes +// A whole record given a new record: every field set at once. +program(() => { + let pos = { x: 1, y: 2 }; + let turns = 0; + while (turns < 3) { + pos = { x: pos.y, y: pos.x + 1 }; + turns += 1; + } + print(`${pos.x} ${pos.y}`); +}); diff --git a/ledger/language/record-from-function.ts b/ledger/language/record-from-function.ts new file mode 100644 index 0000000..642b325 --- /dev/null +++ b/ledger/language/record-from-function.ts @@ -0,0 +1,11 @@ +// ledger: step 0, takes +// A function that returns a record, destructured where it is called. +function split(n: number) { + return { big: n > 4, half: n >> 1 }; +} +program(() => { + let n = 0; + while (n < 7) n += 1; + const { big, half } = split(n); + if (big) print(`${half}`); +}); diff --git a/ledger/language/record-index-known.ts b/ledger/language/record-index-known.ts new file mode 100644 index 0000000..0ae31c7 --- /dev/null +++ b/ledger/language/record-index-known.ts @@ -0,0 +1,10 @@ +// ledger: step 0, takes +// A record's field named by a text known when the map is built: `p["hp"]`. +program(() => { + let p = { hp: 10, shield: 5 }; + while (p["hp"] > 0) { + p.hp -= 3; + p.shield += 1; + } + print(`${p["hp"]} ${p["shield"]}`); +}); diff --git a/ledger/language/record-nested.ts b/ledger/language/record-nested.ts new file mode 100644 index 0000000..3f15167 --- /dev/null +++ b/ledger/language/record-nested.ts @@ -0,0 +1,10 @@ +// ledger: step 0, takes +// A record that holds another record: `p.at.x`. +program(() => { + let p = { hp: 10, at: { x: 0, y: 0 } }; + while (p.hp > 0) { + p.at.x += 2; + p.hp -= 3; + } + print(`${p.at.x} ${p.hp}`); +}); diff --git a/ledger/language/record-shorthand.ts b/ledger/language/record-shorthand.ts new file mode 100644 index 0000000..3e3d05a --- /dev/null +++ b/ledger/language/record-shorthand.ts @@ -0,0 +1,9 @@ +// ledger: step 0, takes +// A record written with shorthand fields: `{ x, y }` takes each from the variable of that name. +program(() => { + let x = 0; + while (x < 3) x += 1; + const y = x + 1; + const p = { x, y }; + print(`${p.x} ${p.y}`); +}); diff --git a/ledger/language/record-spread.ts b/ledger/language/record-spread.ts new file mode 100644 index 0000000..0ad1baf --- /dev/null +++ b/ledger/language/record-spread.ts @@ -0,0 +1,9 @@ +// ledger: step 0, takes +// A record made from another with `...`, one field changed: `{ ...pos, x: 9 }`. +program(() => { + let pos = { x: 1, y: 2 }; + while (pos.y < 5) pos.y += 1; + let moved = { ...pos, x: 9 }; + moved.x += pos.x; + print(`${moved.x} ${moved.y}`); +}); diff --git a/ledger/language/record-type-annotation.ts b/ledger/language/record-type-annotation.ts new file mode 100644 index 0000000..4dfd6a8 --- /dev/null +++ b/ledger/language/record-type-annotation.ts @@ -0,0 +1,12 @@ +// ledger: step 0, takes +// A record with a declared type (an `interface`), and a field marked optional. +interface Wave { + count: number; + delay: number; + boss?: boolean; +} +program(() => { + let w: Wave = { count: 3, delay: 10 }; + while (w.count < 8) w.count += w.delay > 5 ? 2 : 1; + print(`${w.count}`); +}); diff --git a/ledger/language/record.ts b/ledger/language/record.ts new file mode 100644 index 0000000..b43c36f --- /dev/null +++ b/ledger/language/record.ts @@ -0,0 +1,10 @@ +// ledger: step 0, takes +// A record, `{ x: 1, y: 2 }`: each field a variable of its own, read and written while the game runs. +program(() => { + let pos = { x: 1, y: 2 }; + while (pos.x < 10) { + pos.x += 3; + pos.y += 1; + } + print(`${pos.x} ${pos.y}`); +}); diff --git a/ledger/language/recursion.ts b/ledger/language/recursion.ts new file mode 100644 index 0000000..3992ba7 --- /dev/null +++ b/ledger/language/recursion.ts @@ -0,0 +1,11 @@ +// ledger: step 1, takes +// A function that calls itself: each call has its own parameters and locals, kept on a stack. +function fib(n: number): number { + if (n < 2) return n; + return fib(n - 1) + fib(n - 2); +} +program(() => { + let n = 0; + while (n < 7) n += 1; + print(`${fib(n)}`); +}); diff --git a/ledger/language/rest-parameter.ts b/ledger/language/rest-parameter.ts new file mode 100644 index 0000000..2aa8931 --- /dev/null +++ b/ledger/language/rest-parameter.ts @@ -0,0 +1,18 @@ +// ledger: step 3, takes +// A rest parameter, `...xs: number[]`: the arguments a call gives, however many, as a fixed-length array. +function sum(...xs: number[]) { + let t = 0; + for (const x of xs) t += x; + return t; +} +program(() => { + let a = 1; + let b = 2; + let turns = 0; + while (turns < 3) { + a = sum(a, b, 1); + b = sum(b, a); + turns += 1; + } + print(`${a} ${b}`); +}); diff --git a/ledger/language/rethrow.ts b/ledger/language/rethrow.ts new file mode 100644 index 0000000..261de2b --- /dev/null +++ b/ledger/language/rethrow.ts @@ -0,0 +1,16 @@ +// ledger: step 11, refused: an error thrown or caught (try, throw) +// An error caught and thrown again, `catch (e) { …; throw e; }`, to a `try` further out. +program(() => { + let k = 0; + while (k < 2) k += 1; + try { + try { + if (k > 1) throw new Error("inner"); + } catch (e) { + print("seen inside"); + throw e; + } + } catch { + print("caught outside"); + } +}); diff --git a/ledger/language/satisfies.ts b/ledger/language/satisfies.ts new file mode 100644 index 0000000..bbc93b3 --- /dev/null +++ b/ledger/language/satisfies.ts @@ -0,0 +1,8 @@ +// ledger: step 0, takes +// `satisfies`, a check of a type: it changes nothing in the map. +const LIMITS = { low: 5, high: 20 } satisfies Record; +program(() => { + let n = 0; + while (n < LIMITS.high) n += LIMITS.low; + print(`${n}`); +}); diff --git a/ledger/language/several-declarations.ts b/ledger/language/several-declarations.ts new file mode 100644 index 0000000..cad459c --- /dev/null +++ b/ledger/language/several-declarations.ts @@ -0,0 +1,12 @@ +// ledger: step 0, takes +// Several variables declared in one statement: `let a = 1, b = 2;`. +program(() => { + let a = 1, + b = 2, + turns = 0; + while (turns < 4) { + a += b; + turns += 1; + } + print(`${a} ${b}`); +}); diff --git a/ledger/language/short-circuit.ts b/ledger/language/short-circuit.ts new file mode 100644 index 0000000..c519f3e --- /dev/null +++ b/ledger/language/short-circuit.ts @@ -0,0 +1,17 @@ +// ledger: step 1, takes +// `&&` that does not ask its right side when the left is false: a call on the right runs only then. +program(() => { + let calls = 0; + function big(n: number) { + calls += 1; + return n > 3; + } + let n = 0; + let count = 0; + while (n < 6) { + if (n > 1 && big(n)) count += 1; + if (n > 4 || big(n + 1)) count += 10; + n += 1; + } + print(`${count} ${calls}`); +}); diff --git a/ledger/language/sleep-cycles.ts b/ledger/language/sleep-cycles.ts new file mode 100644 index 0000000..4dde319 --- /dev/null +++ b/ledger/language/sleep-cycles.ts @@ -0,0 +1,10 @@ +// ledger: step 0, takes +// `sleep(cycles(n))`, the older word for `frames(n)`. +program(() => { + let n = 0; + while (n < 4) { + sleep(cycles(3)); + n += 1; + } + print(`${n}`); +}); diff --git a/ledger/language/sleep-in-async-program.ts b/ledger/language/sleep-in-async-program.ts new file mode 100644 index 0000000..db2c591 --- /dev/null +++ b/ledger/language/sleep-in-async-program.ts @@ -0,0 +1,10 @@ +// ledger: step 0, takes +// `await sleep(…)` in an `async` program: the same pause as `sleep(…)` on its own. +program(async () => { + let n = 0; + while (n < 3) { + await sleep(frames(5)); + n += 1; + } + print(`${n}`); +}); diff --git a/ledger/language/sleep-minutes.ts b/ledger/language/sleep-minutes.ts new file mode 100644 index 0000000..8a46944 --- /dev/null +++ b/ledger/language/sleep-minutes.ts @@ -0,0 +1,10 @@ +// ledger: step 0, takes +// `sleep(minutes(n))`: the program goes on that many minutes later. +program(() => { + let n = 0; + while (n < 2) { + sleep(minutes(1)); + n += 1; + } + print(`${n}`); +}); diff --git a/ledger/language/sleep-seconds.ts b/ledger/language/sleep-seconds.ts new file mode 100644 index 0000000..b2bf2ac --- /dev/null +++ b/ledger/language/sleep-seconds.ts @@ -0,0 +1,10 @@ +// ledger: step 0, takes +// `sleep(seconds(n))`: the program goes on that many seconds later, twenty-four frames a second. +program(() => { + let n = 0; + while (n < 3) { + sleep(seconds(2)); + n += 1; + } + print(`${n}`); +}); diff --git a/ledger/language/spread-arguments.ts b/ledger/language/spread-arguments.ts new file mode 100644 index 0000000..fc23915 --- /dev/null +++ b/ledger/language/spread-arguments.ts @@ -0,0 +1,15 @@ +// ledger: step 0, takes +// Arguments spread from a list known when the map is built: `mix(n, ...PAIR)`. +function mix(a: number, b: number, c: number) { + return a + b + b + c; +} +const PAIR = [3, 4] as const; +program(() => { + let n = 0; + let turns = 0; + while (turns < 3) { + n = mix(n, ...PAIR); + turns += 1; + } + print(`${n}`); +}); diff --git a/ledger/language/strict-equality.ts b/ledger/language/strict-equality.ts new file mode 100644 index 0000000..1c69ed5 --- /dev/null +++ b/ledger/language/strict-equality.ts @@ -0,0 +1,12 @@ +// ledger: step 0, takes +// `===` and `!==`, the same as `==` and `!=` for numbers. +program(() => { + let n = 0; + let count = 0; + while (n < 6) { + if (n === 2) count += 1; + if (n !== 4) count += 10; + n += 1; + } + print(`${count}`); +}); diff --git a/ledger/language/swap-by-destructuring.ts b/ledger/language/swap-by-destructuring.ts new file mode 100644 index 0000000..991a459 --- /dev/null +++ b/ledger/language/swap-by-destructuring.ts @@ -0,0 +1,13 @@ +// ledger: step 0, takes +// Two variables swapped with a destructuring assignment: `[a, b] = [b, a]`. +program(() => { + let a = 1; + let b = 7; + let turns = 0; + while (turns < 3) { + [a, b] = [b, a]; + a += 1; + turns += 1; + } + print(`${a} ${b}`); +}); diff --git a/ledger/language/switch-case-block.ts b/ledger/language/switch-case-block.ts new file mode 100644 index 0000000..28f246a --- /dev/null +++ b/ledger/language/switch-case-block.ts @@ -0,0 +1,21 @@ +// ledger: step 0, takes +// A `case` with a block of its own, `case 1: { const x = …; … }`, its variables only its own. +program(() => { + let n = 0; + let total = 0; + while (n < 3) { + switch (n) { + case 1: { + const bonus = n + 10; + total += bonus; + break; + } + default: { + const bonus = n + 1; + total += bonus; + } + } + n += 1; + } + print(`${total}`); +}); diff --git a/ledger/language/switch-fall-through.ts b/ledger/language/switch-fall-through.ts new file mode 100644 index 0000000..8caed17 --- /dev/null +++ b/ledger/language/switch-fall-through.ts @@ -0,0 +1,19 @@ +// ledger: step 0, takes +// A `case` with no `break` goes on into the next one, as JavaScript's does. +program(() => { + let n = 0; + let score = 0; + while (n < 3) { + switch (n) { + case 0: + score += 1; + case 1: + score += 10; + break; + case 2: + score += 100; + } + n += 1; + } + print(`${score}`); +}); diff --git a/ledger/language/switch-number.ts b/ledger/language/switch-number.ts new file mode 100644 index 0000000..8dda69b --- /dev/null +++ b/ledger/language/switch-number.ts @@ -0,0 +1,15 @@ +// ledger: step 0, takes +// `switch` on a number that changes while the game runs, each `case` ending in `break`, and a `default`. +program(() => { + let n = 0; + let score = 0; + while (n < 5) { + switch (n) { + case 0: score += 1; break; + case 2: score += 10; break; + default: score += 100; break; + } + n += 1; + } + print(`${score}`); +}); diff --git a/ledger/language/switch-return.ts b/ledger/language/switch-return.ts new file mode 100644 index 0000000..10cab53 --- /dev/null +++ b/ledger/language/switch-return.ts @@ -0,0 +1,19 @@ +// ledger: step 1, takes +// A `switch` inside a function whose cases `return` the function's result. +function price(kind: number) { + switch (kind) { + case 0: return 50; + case 1: return 75; + default: return 100; + } +} +program(() => { + let n = 0; + let total = 0; + while (n < 4) { + total += price(n); + total += price(n + 1); + n += 1; + } + print(`${total}`); +}); diff --git a/ledger/language/switch-text.ts b/ledger/language/switch-text.ts new file mode 100644 index 0000000..347ad6b --- /dev/null +++ b/ledger/language/switch-text.ts @@ -0,0 +1,12 @@ +// ledger: step 8, refused: a text kept in a variable +// `switch` on a text the program holds, its cases texts. +program(() => { + let n = 0; + let word = "red"; + while (n < 3) n += 1; + if (n > 2) word = "blue"; + switch (word) { + case "red": print("stop"); break; + case "blue": print("go"); break; + } +}); diff --git a/ledger/language/ternary-boolean.ts b/ledger/language/ternary-boolean.ts new file mode 100644 index 0000000..a3b112b --- /dev/null +++ b/ledger/language/ternary-boolean.ts @@ -0,0 +1,13 @@ +// ledger: step 0, refused: a ?: of booleans +// `?:` choosing between two booleans, its test asked while the game runs. +program(() => { + let n = 0; + let count = 0; + let pick = false; + while (n < 6) { + pick = n > 2 ? n < 4 : n > 0; + if (pick) count += 1; + n += 1; + } + print(`${count}`); +}); diff --git a/ledger/language/ternary-number.ts b/ledger/language/ternary-number.ts new file mode 100644 index 0000000..e567cff --- /dev/null +++ b/ledger/language/ternary-number.ts @@ -0,0 +1,11 @@ +// ledger: step 0, takes +// `?:` choosing between two numbers, its test asked while the game runs. +program(() => { + let n = 0; + let total = 0; + while (n < 6) { + total += n < 3 ? 1 : 10; + n += 1; + } + print(`${total}`); +}); diff --git a/ledger/language/throw-from-function.ts b/ledger/language/throw-from-function.ts new file mode 100644 index 0000000..77d95ba --- /dev/null +++ b/ledger/language/throw-from-function.ts @@ -0,0 +1,19 @@ +// ledger: step 11, refused: an error thrown or caught (try, throw) +// An error thrown in a called function, caught by the `try` around the call. +function check(n: number) { + if (n > 2) throw new Error("too big"); + return n + 1; +} +program(() => { + let n = 0; + let total = 0; + try { + while (n < 5) { + total += check(n); + n += 1; + } + } catch { + print(`stopped at ${n}`); + } + print(`${total}`); +}); diff --git a/ledger/language/throw-uncaught.ts b/ledger/language/throw-uncaught.ts new file mode 100644 index 0000000..577569c --- /dev/null +++ b/ledger/language/throw-uncaught.ts @@ -0,0 +1,9 @@ +// ledger: step 11, refused: an error thrown or caught (try, throw) +// A `throw` nothing catches: the game says the error, and the program stops there. +program(() => { + let k = 0; + while (k < 2) k += 1; + print("before"); + if (k > 1) throw new RangeError("k is too large"); + print("after"); +}); diff --git a/ledger/language/try-catch.ts b/ledger/language/try-catch.ts new file mode 100644 index 0000000..d6ef5b0 --- /dev/null +++ b/ledger/language/try-catch.ts @@ -0,0 +1,12 @@ +// ledger: step 11, refused: an error thrown or caught (try, throw) +// `try { … } catch { … }`: an error thrown inside the `try` goes to its `catch`. +program(() => { + let k = 0; + while (k < 2) k += 1; + try { + if (k > 1) throw new Error("too many"); + print("fine"); + } catch { + print("caught"); + } +}); diff --git a/ledger/language/try-finally.ts b/ledger/language/try-finally.ts new file mode 100644 index 0000000..f6615be --- /dev/null +++ b/ledger/language/try-finally.ts @@ -0,0 +1,15 @@ +// ledger: step 11, refused: an error thrown or caught (try, throw) +// `finally`: what runs however the `try` is left. +program(() => { + let k = 0; + let cleaned = 0; + while (k < 3) { + try { + if (k == 1) break; + } finally { + cleaned += 1; + } + k += 1; + } + print(`${cleaned}`); +}); diff --git a/ledger/language/tuple-result.ts b/ledger/language/tuple-result.ts new file mode 100644 index 0000000..a576827 --- /dev/null +++ b/ledger/language/tuple-result.ts @@ -0,0 +1,11 @@ +// ledger: step 3, takes +// A function that returns two numbers as a tuple, `return [a, b] as const`, destructured where it is called. +function split(n: number) { + return [n + 1, n + 2] as const; +} +program(() => { + let n = 0; + while (n < 4) n += 1; + const [a, b] = split(n); + print(`${a} ${b}`); +}); diff --git a/ledger/language/two-programs.ts b/ledger/language/two-programs.ts new file mode 100644 index 0000000..f3ef1b1 --- /dev/null +++ b/ledger/language/two-programs.ts @@ -0,0 +1,18 @@ +// ledger: step 7, refused: a map of our own has one program; this script has 2 +// Two programs in one map, each with an owner of its own, running side by side. +program( + () => { + let n = 0; + while (n < 3) n += 1; + print(`${n}`); + }, + { owner: P7 }, +); +program( + () => { + let n = 0; + while (n < 5) n += 1; + print(`${n}`); + }, + { owner: P8 }, +); diff --git a/ledger/language/type-alias-union.ts b/ledger/language/type-alias-union.ts new file mode 100644 index 0000000..47ef522 --- /dev/null +++ b/ledger/language/type-alias-union.ts @@ -0,0 +1,12 @@ +// ledger: step 0, takes +// A type alias of a union of numbers, `type Lane = 0 | 1 | 2`: a type only, the values numbers. +type Lane = 0 | 1 | 2; +program(() => { + let lane: Lane = 0; + let turns = 0; + while (turns < 5) { + lane = lane == 2 ? 0 : lane == 1 ? 2 : 1; + turns += 1; + } + print(`${lane}`); +}); diff --git a/ledger/language/type-assertion.ts b/ledger/language/type-assertion.ts new file mode 100644 index 0000000..a606df7 --- /dev/null +++ b/ledger/language/type-assertion.ts @@ -0,0 +1,8 @@ +// ledger: step 0, takes +// A type assertion, `value as number`: it changes nothing in the map. +program(() => { + let n = 4 as number; + let total = 0; + while (total < 20) total += n; + print(`${total}`); +}); diff --git a/ledger/language/typeof-at-build.ts b/ledger/language/typeof-at-build.ts new file mode 100644 index 0000000..5f63e69 --- /dev/null +++ b/ledger/language/typeof-at-build.ts @@ -0,0 +1,10 @@ +// ledger: step 0, takes +// `typeof` worked out when the map is built: only the branch it picks is left in the map. +const LIMIT: number | string = 30; +program(() => { + let n = 0; + if (typeof LIMIT === "number") { + while (n < LIMIT) n += 7; + } + print(`${n}`); +}); diff --git a/ledger/language/u32-cast.ts b/ledger/language/u32-cast.ts new file mode 100644 index 0000000..af20947 --- /dev/null +++ b/ledger/language/u32-cast.ts @@ -0,0 +1,11 @@ +// ledger: step 0, takes +// `u32(x)`: the same 32 bits read as a number from 0 up, so `-1` compares as the largest. +program(() => { + let n = 3; + let count = 0; + while (n > -3) { + if (u32(n) > 100) count += 1; + n -= 1; + } + print(`${count}`); +}); diff --git a/ledger/language/undefined-check.ts b/ledger/language/undefined-check.ts new file mode 100644 index 0000000..51cd1f9 --- /dev/null +++ b/ledger/language/undefined-check.ts @@ -0,0 +1,13 @@ +// ledger: step 0, takes +// A number that may be `undefined`, tested with `!== undefined` before it is used. +program(() => { + let k = 0; + let total = 0; + while (k < 6) { + let n: number | undefined = undefined; + if (k > 2) n = k; + if (n !== undefined) total += n; + k += 1; + } + print(`${total}`); +}); diff --git a/ledger/language/var-declaration.ts b/ledger/language/var-declaration.ts new file mode 100644 index 0000000..03d5aa1 --- /dev/null +++ b/ledger/language/var-declaration.ts @@ -0,0 +1,7 @@ +// ledger: step 0, takes +// A `var` declaration: the same as a `let` in a program. +program(() => { + var n = 0; + while (n < 7) n += 2; + print(`${n}`); +}); diff --git a/ledger/language/while-loop.ts b/ledger/language/while-loop.ts new file mode 100644 index 0000000..784f94f --- /dev/null +++ b/ledger/language/while-loop.ts @@ -0,0 +1,7 @@ +// ledger: step 0, takes +// A `while` loop over a number, within one frame. +program(() => { + let n = 0; + while (n < 10) n += 3; + print(`${n}`); +}); diff --git a/ledger/memory/fog-columns.ts b/ledger/memory/fog-columns.ts new file mode 100644 index 0000000..4a110e4 --- /dev/null +++ b/ledger/memory/fog-columns.ts @@ -0,0 +1,12 @@ +// ledger: step 7, refused: a map of our own has one program; this script has 2 +// The fog library: columns lit for this frame, each as tall as an array of the program says, laid out +// when the map is built: `fog.columns(P1, x, y, layout, heights)` (a bar chart, a raycaster's walls). +// The library adds a program of its own, the canvas that wipes the fog every frame. +import { fog } from "trigscript/fog"; +const VIEW = { count: 4, height: 16 } as const; +program(async () => { + const heights = [4, 8, 12, 16]; + heights[0] = 6; + fog.columns(P1, 1, 0, VIEW, heights); + await nextFrame(); +}); diff --git a/ledger/memory/fog-light.ts b/ledger/memory/fog-light.ts new file mode 100644 index 0000000..146702b --- /dev/null +++ b/ledger/memory/fog-light.ts @@ -0,0 +1,13 @@ +// ledger: step 7, refused: a map of our own has one program; this script has 2 +// The fog library: a disc of fog lit for this frame, its size a value of the program: +// `light(P1, x, y, size)`. +// The library adds a program of its own, the canvas that wipes the fog every frame. +import { light } from "trigscript/fog"; +program(async () => { + let size = 1; + while (size < 6) { + light(P1, 30, 20, size); + await nextFrame(); + size += 1; + } +}); diff --git a/ledger/memory/fog-picture.ts b/ledger/memory/fog-picture.ts new file mode 100644 index 0000000..7595de2 --- /dev/null +++ b/ledger/memory/fog-picture.ts @@ -0,0 +1,9 @@ +// ledger: step 7, refused: a map of our own has one program; this script has 2 +// The fog library: a picture made of rows of text when the map is built (`fog.picture`), lit for a frame. +// The library adds a program of its own, the canvas that wipes the fog every frame. +import { fog } from "trigscript/fog"; +const heart = fog.picture([".##.##.", "#######", ".#####.", "..###.."]); +program(async () => { + fog.light(P1, 60, 40, heart); + await nextFrame(); +}); diff --git a/ledger/memory/fog-play.ts b/ledger/memory/fog-play.ts new file mode 100644 index 0000000..7d9a718 --- /dev/null +++ b/ledger/memory/fog-play.ts @@ -0,0 +1,11 @@ +// ledger: step 7, refused: a map of our own has one program; this script has 2 +// The fog library: a video played in the fog, a frame of it every frame of the game: `await fog.play(P1, video)`. +// A video comes only from a .video file the script imports, which a script of the ledger, one file, cannot: +// this one has none, and the call is left out when the map is built. +// The library adds a program of its own, the canvas that wipes the fog every frame. +import { fog } from "trigscript/fog"; +const film: Video | null = null; +program(async () => { + if (film) await fog.play(P1, film); + fog.reveal(P1); +}); diff --git a/ledger/memory/fog-rect.ts b/ledger/memory/fog-rect.ts new file mode 100644 index 0000000..0c0d13b --- /dev/null +++ b/ledger/memory/fog-rect.ts @@ -0,0 +1,13 @@ +// ledger: step 7, refused: a map of our own has one program; this script has 2 +// The fog library: a rectangle of tiles lit for this frame, every side from values of the program: +// `fog.rect(P1, x, y, width, height)`. +// The library adds a program of its own, the canvas that wipes the fog every frame. +import { fog } from "trigscript/fog"; +program(async () => { + let tall = 4; + while (tall < 12) { + fog.rect(P1, 10, 20, 3, tall); + await nextFrame(); + tall += 2; + } +}); diff --git a/ledger/memory/fog-reveal.ts b/ledger/memory/fog-reveal.ts new file mode 100644 index 0000000..288a681 --- /dev/null +++ b/ledger/memory/fog-reveal.ts @@ -0,0 +1,7 @@ +// ledger: step 7, refused: a map of our own has one program; this script has 2 +// The fog library: the whole map lit once, so that from then on dark is fog, not black: `fog.reveal(P1)`. +// The library adds a program of its own, the canvas that wipes the fog every frame. +import { fog } from "trigscript/fog"; +program(async () => { + fog.reveal(P1); +}); diff --git a/ledger/memory/fog-shapes.ts b/ledger/memory/fog-shapes.ts new file mode 100644 index 0000000..e57e847 --- /dev/null +++ b/ledger/memory/fog-shapes.ts @@ -0,0 +1,16 @@ +// ledger: step 7, refused: a map of our own has one program; this script has 2 +// The fog library's brushes, `SHAPES`: the tiles each size of brush reveals, for a renderer of the +// script's own (`cover(picture, SHAPES)`), worked out when the map is built. +// The library adds a program of its own, the canvas that wipes the fog every frame. +import { SHAPES } from "trigscript/fog"; +const brushes = cover(["#####", "#####", "#####", "#####"], SHAPES); +const sizes = brushes.map((b) => b.shape + 1); +program(() => { + let i = 0; + let sum = 0; + while (i < sizes.length) { + sum += sizes[i]; + i += 1; + } + print(`${sum}`); +}); diff --git a/ledger/memory/fog-show.ts b/ledger/memory/fog-show.ts new file mode 100644 index 0000000..a011677 --- /dev/null +++ b/ledger/memory/fog-show.ts @@ -0,0 +1,8 @@ +// ledger: step 7, refused: a map of our own has one program; this script has 2 +// The fog library: what was lit this frame kept for so many frames, and waited out: `await fog.show(2)`. +// The library adds a program of its own, the canvas that wipes the fog every frame. +import { fog } from "trigscript/fog"; +program(async () => { + fog.light(P1, 30, 20, 4); + await fog.show(2); +}); diff --git a/ledger/memory/game-frame-read.ts b/ledger/memory/game-frame-read.ts new file mode 100644 index 0000000..bc248d4 --- /dev/null +++ b/ledger/memory/game-frame-read.ts @@ -0,0 +1,6 @@ +// ledger: step 6, refused: a read of memory.game.frame +// Game frames since the game began: `memory.game.frame`. +program(() => { + const frame = memory.game.frame; + print(`${frame}`); +}); diff --git a/ledger/memory/map-size-read.ts b/ledger/memory/map-size-read.ts new file mode 100644 index 0000000..808f04b --- /dev/null +++ b/ledger/memory/map-size-read.ts @@ -0,0 +1,7 @@ +// ledger: step 6, refused: a read of memory.map.width +// The map's width and height in tiles: `memory.map.width`, `memory.map.height`. +program(() => { + const w = memory.map.width; + const h = memory.map.height; + print(`${w} by ${h}`); +}); diff --git a/ledger/memory/memory-condition.ts b/ledger/memory/memory-condition.ts new file mode 100644 index 0000000..e397bc9 --- /dev/null +++ b/ledger/memory/memory-condition.ts @@ -0,0 +1,5 @@ +// ledger: step 6, refused: the condition Deaths +// A cell of the game's memory compared, by its address (EUD): `memory(address, ">=", value)`. +program(() => { + if (memory(0x58A364, ">=", 1)) print("set"); +}); diff --git a/ledger/memory/player-color-by-name.ts b/ledger/memory/player-color-by-name.ts new file mode 100644 index 0000000..889171b --- /dev/null +++ b/ledger/memory/player-color-by-name.ts @@ -0,0 +1,5 @@ +// ledger: step 6, refused: a write of player.color +// A player's colour by its name: `stats(P2).color = "teal"`. +program(() => { + stats(P2).color = "teal"; +}); diff --git a/ledger/memory/player-color-written.ts b/ledger/memory/player-color-written.ts new file mode 100644 index 0000000..0e38c6d --- /dev/null +++ b/ledger/memory/player-color-written.ts @@ -0,0 +1,5 @@ +// ledger: step 6, refused: a write of player.color +// A player's colour, one of `colors.*`, at once: `stats(P1).color = colors.green`. +program(() => { + stats(P1).color = colors.green; +}); diff --git a/ledger/memory/player-researched-read.ts b/ledger/memory/player-researched-read.ts new file mode 100644 index 0000000..94a80ae --- /dev/null +++ b/ledger/memory/player-researched-read.ts @@ -0,0 +1,5 @@ +// ledger: step 6, refused: a read of player.researched +// Whether a player has a technology, tested in an `if`. +program(() => { + if (stats(P1).researched[techs.StimPacks]) print("stim ready"); +}); diff --git a/ledger/memory/player-researched-written.ts b/ledger/memory/player-researched-written.ts new file mode 100644 index 0000000..b7094d7 --- /dev/null +++ b/ledger/memory/player-researched-written.ts @@ -0,0 +1,5 @@ +// ledger: step 6, refused: a write of player.researched +// Whether a player has a technology, given: `stats(P1).researched[techs.Lockdown] = true`. +program(() => { + stats(P1).researched[techs.Lockdown] = true; +}); diff --git a/ledger/memory/player-upgrade-level-read.ts b/ledger/memory/player-upgrade-level-read.ts new file mode 100644 index 0000000..97de047 --- /dev/null +++ b/ledger/memory/player-upgrade-level-read.ts @@ -0,0 +1,6 @@ +// ledger: step 6, refused: a read of player.upgrades +// A player's level of an upgrade, read: `stats(P1).upgrades[upgrades.*]`. +program(() => { + const level = stats(P1).upgrades[upgrades.TerranInfantryWeapons]; + print(`${level}`); +}); diff --git a/ledger/memory/player-upgrade-level-written.ts b/ledger/memory/player-upgrade-level-written.ts new file mode 100644 index 0000000..5f1eb55 --- /dev/null +++ b/ledger/memory/player-upgrade-level-written.ts @@ -0,0 +1,5 @@ +// ledger: step 6, refused: a write of player.upgrades +// A player's level of an upgrade: `stats(P1).upgrades[upgrades.*] = 3`. +program(() => { + stats(P1).upgrades[upgrades.TerranInfantryWeapons] = 3; +}); diff --git a/ledger/memory/set-memory.ts b/ledger/memory/set-memory.ts new file mode 100644 index 0000000..93340dc --- /dev/null +++ b/ledger/memory/set-memory.ts @@ -0,0 +1,5 @@ +// ledger: step 6, refused: the action Set Deaths +// A cell of the game's memory written, by its address (EUD): `setMemory(address, "add", value)`. +program(() => { + setMemory(0x58A364, "add", 1); +}); diff --git a/ledger/memory/tech-cost-written.ts b/ledger/memory/tech-cost-written.ts new file mode 100644 index 0000000..123ddd1 --- /dev/null +++ b/ledger/memory/tech-cost-written.ts @@ -0,0 +1,8 @@ +// ledger: step 6, refused: a write of tech.minerals +// A technology's research cost and time, and the energy a cast takes, written: `stats(techs.*)`. +program(() => { + stats(techs.StimPacks).minerals = 50; + stats(techs.StimPacks).gas = 50; + stats(techs.StimPacks).time = 40; + stats(techs.Lockdown).energy = 75; +}); diff --git a/ledger/memory/tech-energy-read.ts b/ledger/memory/tech-energy-read.ts new file mode 100644 index 0000000..db56833 --- /dev/null +++ b/ledger/memory/tech-energy-read.ts @@ -0,0 +1,6 @@ +// ledger: step 6, refused: a read of tech.energy +// The energy a spell costs, read: `stats(techs.Lockdown).energy`. +program(() => { + const energy = stats(techs.Lockdown).energy; + print(`${energy}`); +}); diff --git a/ledger/memory/unit-air-weapon-written.ts b/ledger/memory/unit-air-weapon-written.ts new file mode 100644 index 0000000..4f711a0 --- /dev/null +++ b/ledger/memory/unit-air-weapon-written.ts @@ -0,0 +1,5 @@ +// ledger: step 6, refused: a write of unit.airWeapon +// A unit type's air weapon: `weapons.None` for none. +program(() => { + stats(units.TerranMarine).airWeapon = weapons.None; +}); diff --git a/ledger/memory/unit-armor-added-to.ts b/ledger/memory/unit-armor-added-to.ts new file mode 100644 index 0000000..2880c0c --- /dev/null +++ b/ledger/memory/unit-armor-added-to.ts @@ -0,0 +1,5 @@ +// ledger: step 6, refused: a write of unit.armor +// A unit type's armour raised by one: `stats(…).armor += 1`. +program(() => { + stats(units.TerranMarine).armor += 1; +}); diff --git a/ledger/memory/unit-build-time-fraction.ts b/ledger/memory/unit-build-time-fraction.ts new file mode 100644 index 0000000..9ded6cc --- /dev/null +++ b/ledger/memory/unit-build-time-fraction.ts @@ -0,0 +1,5 @@ +// ledger: step 6, refused: a write of unit.buildTime +// A unit type's build time written as a fraction known when the map is built: `buildTime = 1.5`. +program(() => { + stats(units.TerranMarine).buildTime = 1.5; +}); diff --git a/ledger/memory/unit-cost-read.ts b/ledger/memory/unit-cost-read.ts new file mode 100644 index 0000000..0dedff1 --- /dev/null +++ b/ledger/memory/unit-cost-read.ts @@ -0,0 +1,6 @@ +// ledger: step 6, refused: a read of unit.minerals +// A unit type's mineral cost read from the game's tables: `stats(units.TerranMarine).minerals`. +program(() => { + const cost = stats(units.TerranMarine).minerals; + print(`${cost}`); +}); diff --git a/ledger/memory/unit-cost-written.ts b/ledger/memory/unit-cost-written.ts new file mode 100644 index 0000000..4717678 --- /dev/null +++ b/ledger/memory/unit-cost-written.ts @@ -0,0 +1,5 @@ +// ledger: step 6, refused: a write of unit.minerals +// A unit type's mineral cost written: `stats(units.TerranMarine).minerals = 25`. +program(() => { + stats(units.TerranMarine).minerals = 25; +}); diff --git a/ledger/memory/unit-flag-read.ts b/ledger/memory/unit-flag-read.ts new file mode 100644 index 0000000..89cf167 --- /dev/null +++ b/ledger/memory/unit-flag-read.ts @@ -0,0 +1,5 @@ +// ledger: step 6, refused: a read of unit.organic +// A unit type's flag tested in an `if`: `if (stats(units.TerranMarine).organic)`. +program(() => { + if (stats(units.TerranMarine).organic) print("a Medic heals it"); +}); diff --git a/ledger/memory/unit-flag-written.ts b/ledger/memory/unit-flag-written.ts new file mode 100644 index 0000000..28be317 --- /dev/null +++ b/ledger/memory/unit-flag-written.ts @@ -0,0 +1,5 @@ +// ledger: step 6, refused: a write of unit.detector +// A unit type's flag set: `stats(units.TerranMarine).detector = true`. +program(() => { + stats(units.TerranMarine).detector = true; +}); diff --git a/ledger/memory/unit-flags-of-a-kind.ts b/ledger/memory/unit-flags-of-a-kind.ts new file mode 100644 index 0000000..7a2ef46 --- /dev/null +++ b/ledger/memory/unit-flags-of-a-kind.ts @@ -0,0 +1,12 @@ +// ledger: step 6, refused: a write of unit.permanentCloak +// The rest of a unit type's flags: cloak, burrow, regeneration, invincible, hero, mechanical, robotic. +program(() => { + stats(units.TerranMarine).permanentCloak = true; + stats(units.TerranMarine).cloakable = true; + stats(units.TerranMarine).burrowable = true; + stats(units.TerranMarine).regenerates = true; + stats(units.TerranMarine).invincible = false; + stats(units.TerranMarine).hero = true; + stats(units.TerranMarine).mechanical = true; + stats(units.TerranMarine).robotic = false; +}); diff --git a/ledger/memory/unit-gas-written.ts b/ledger/memory/unit-gas-written.ts new file mode 100644 index 0000000..122d4a9 --- /dev/null +++ b/ledger/memory/unit-gas-written.ts @@ -0,0 +1,5 @@ +// ledger: step 6, refused: a write of unit.gas +// A unit type's gas cost written: `stats(units.TerranGhost).gas = 50`. +program(() => { + stats(units.TerranGhost).gas = 50; +}); diff --git a/ledger/memory/unit-ground-weapon-written.ts b/ledger/memory/unit-ground-weapon-written.ts new file mode 100644 index 0000000..ac94c93 --- /dev/null +++ b/ledger/memory/unit-ground-weapon-written.ts @@ -0,0 +1,5 @@ +// ledger: step 6, refused: a write of unit.groundWeapon +// A unit type's ground weapon switched to another of `weapons.*`. +program(() => { + stats(units.TerranMarine).groundWeapon = weapons.C10CanisterRifle; +}); diff --git a/ledger/memory/unit-max-hp-from-a-value.ts b/ledger/memory/unit-max-hp-from-a-value.ts new file mode 100644 index 0000000..63e229e --- /dev/null +++ b/ledger/memory/unit-max-hp-from-a-value.ts @@ -0,0 +1,7 @@ +// ledger: step 6, refused: a write of unit.maxHp +// A unit type's hit points written from a value of the program, for the units made after. +program(() => { + let hp = 40; + hp += 5; + stats(units.TerranMarine).maxHp = hp; +}); diff --git a/ledger/memory/unit-max-shields-written.ts b/ledger/memory/unit-max-shields-written.ts new file mode 100644 index 0000000..b6aa4aa --- /dev/null +++ b/ledger/memory/unit-max-shields-written.ts @@ -0,0 +1,5 @@ +// ledger: step 6, refused: a write of unit.maxShields +// A unit type's shield points written. +program(() => { + stats(units.ProtossZealot).maxShields = 80; +}); diff --git a/ledger/memory/unit-name-written.ts b/ledger/memory/unit-name-written.ts new file mode 100644 index 0000000..a2c3174 --- /dev/null +++ b/ledger/memory/unit-name-written.ts @@ -0,0 +1,5 @@ +// ledger: step 6, refused: a write of unit.name +// The name shown for a unit type, a text known when the map is built: `stats(…).name = "…"`. +program(() => { + stats(units.TerranMarine).name = "Space Marine"; +}); diff --git a/ledger/memory/unit-sight-written.ts b/ledger/memory/unit-sight-written.ts new file mode 100644 index 0000000..5bc8dbc --- /dev/null +++ b/ledger/memory/unit-sight-written.ts @@ -0,0 +1,5 @@ +// ledger: step 6, refused: a write of unit.sight +// A unit type's sight range in tiles. +program(() => { + stats(units.TerranMarine).sight = 9; +}); diff --git a/ledger/memory/unit-size-read.ts b/ledger/memory/unit-size-read.ts new file mode 100644 index 0000000..77d964e --- /dev/null +++ b/ledger/memory/unit-size-read.ts @@ -0,0 +1,6 @@ +// ledger: step 6, refused: a read of unit.size +// A unit type's size (small, medium, large), read: what concussive and explosive damage scale by. +program(() => { + const size = stats(units.TerranMarine).size; + print(`${size}`); +}); diff --git a/ledger/memory/unit-speed-written.ts b/ledger/memory/unit-speed-written.ts new file mode 100644 index 0000000..f264b9f --- /dev/null +++ b/ledger/memory/unit-speed-written.ts @@ -0,0 +1,5 @@ +// ledger: step 6, refused: a write of unit.speed +// A unit type's top speed in pixels a frame, for units made after: `stats(…).speed = 6`. +program(() => { + stats(units.TerranMarine).speed = 6; +}); diff --git a/ledger/memory/unit-supply-provided-written.ts b/ledger/memory/unit-supply-provided-written.ts new file mode 100644 index 0000000..77a4e9b --- /dev/null +++ b/ledger/memory/unit-supply-provided-written.ts @@ -0,0 +1,5 @@ +// ledger: step 6, refused: a write of unit.supplyProvided +// Supply a unit type provides, for the ones made after. +program(() => { + stats(units.TerranSupplyDepot).supplyProvided = 10; +}); diff --git a/ledger/memory/unit-supply-used-half.ts b/ledger/memory/unit-supply-used-half.ts new file mode 100644 index 0000000..92c9585 --- /dev/null +++ b/ledger/memory/unit-supply-used-half.ts @@ -0,0 +1,5 @@ +// ledger: step 6, refused: a write of unit.supplyUsed +// Supply a unit type takes, a half: `stats(units.ZergZergling).supplyUsed = 0.5`. +program(() => { + stats(units.ZergZergling).supplyUsed = 0.5; +}); diff --git a/ledger/memory/upgrade-cost-written.ts b/ledger/memory/upgrade-cost-written.ts new file mode 100644 index 0000000..422d2bb --- /dev/null +++ b/ledger/memory/upgrade-cost-written.ts @@ -0,0 +1,7 @@ +// ledger: step 6, refused: a write of upgrade.minerals +// An upgrade's cost and time written: `stats(upgrades.*)`. +program(() => { + stats(upgrades.TerranInfantryArmor).minerals = 50; + stats(upgrades.TerranInfantryArmor).gas = 50; + stats(upgrades.TerranInfantryArmor).time = 30; +}); diff --git a/ledger/memory/upgrade-max-level-read.ts b/ledger/memory/upgrade-max-level-read.ts new file mode 100644 index 0000000..f03c671 --- /dev/null +++ b/ledger/memory/upgrade-max-level-read.ts @@ -0,0 +1,6 @@ +// ledger: step 6, refused: a read of upgrade.maxLevel +// How many times an upgrade can be researched, read: `stats(upgrades.*).maxLevel`. +program(() => { + const most = stats(upgrades.TerranInfantryArmor).maxLevel; + print(`${most}`); +}); diff --git a/ledger/memory/vision-countdown-read.ts b/ledger/memory/vision-countdown-read.ts new file mode 100644 index 0000000..77c8838 --- /dev/null +++ b/ledger/memory/vision-countdown-read.ts @@ -0,0 +1,6 @@ +// ledger: step 6, refused: a read of memory.vision.countdown +// The game's vision countdown read: `memory.vision.countdown`. +program(() => { + const left = memory.vision.countdown; + print(`${left}`); +}); diff --git a/ledger/memory/vision-countdown-written.ts b/ledger/memory/vision-countdown-written.ts new file mode 100644 index 0000000..4a46f81 --- /dev/null +++ b/ledger/memory/vision-countdown-written.ts @@ -0,0 +1,5 @@ +// ledger: step 6, refused: a write of memory.vision.countdown +// The game's vision countdown written: `memory.vision.countdown = 1` wipes the fog on the next frame. +program(() => { + memory.vision.countdown = 1; +}); diff --git a/ledger/memory/weapon-damage-added-to.ts b/ledger/memory/weapon-damage-added-to.ts new file mode 100644 index 0000000..298ceef --- /dev/null +++ b/ledger/memory/weapon-damage-added-to.ts @@ -0,0 +1,5 @@ +// ledger: step 6, refused: a write of weapon.damage +// A weapon's damage raised: `stats(weapons.GaussRifle).damage += 2`. +program(() => { + stats(weapons.GaussRifle).damage += 2; +}); diff --git a/ledger/memory/weapon-fields-written.ts b/ledger/memory/weapon-fields-written.ts new file mode 100644 index 0000000..efb8315 --- /dev/null +++ b/ledger/memory/weapon-fields-written.ts @@ -0,0 +1,8 @@ +// ledger: step 6, refused: a write of weapon.bonus +// A weapon's other fields written: upgrade bonus, cooldown, hits per attack, least range. +program(() => { + stats(weapons.GaussRifle).bonus = 2; + stats(weapons.GaussRifle).cooldown = 10; + stats(weapons.GaussRifle).factor = 2; + stats(weapons.GaussRifle).minRange = 0; +}); diff --git a/ledger/memory/weapon-range-read.ts b/ledger/memory/weapon-range-read.ts new file mode 100644 index 0000000..f2a5b18 --- /dev/null +++ b/ledger/memory/weapon-range-read.ts @@ -0,0 +1,6 @@ +// ledger: step 6, refused: a read of weapon.range +// A weapon's range in pixels read from the game's tables: `stats(weapons.GaussRifle).range`. +program(() => { + const range = stats(weapons.GaussRifle).range; + print(`${range}`); +}); diff --git a/ledger/numbers/abs-of-a-value.ts b/ledger/numbers/abs-of-a-value.ts new file mode 100644 index 0000000..73f84ca --- /dev/null +++ b/ledger/numbers/abs-of-a-value.ts @@ -0,0 +1,7 @@ +// ledger: step 0, takes +// A value without its sign: `Math.abs(a)`. +program(() => { + let a = 5; + while (a > -10) a -= 4; + print(`${Math.abs(a)}`); +}); diff --git a/ledger/numbers/and-of-two-values.ts b/ledger/numbers/and-of-two-values.ts new file mode 100644 index 0000000..3b3274d --- /dev/null +++ b/ledger/numbers/and-of-two-values.ts @@ -0,0 +1,11 @@ +// ledger: step 4, refused: & of two values +// The bits two values that change while the game runs both have: `a & b`. +program(() => { + let a = 0; + let b = 1; + while (a < 50) { + a += 9; + b += 5; + } + print(`${a & b}`); +}); diff --git a/ledger/numbers/and-with-a-constant-as-a-value.ts b/ledger/numbers/and-with-a-constant-as-a-value.ts new file mode 100644 index 0000000..005951e --- /dev/null +++ b/ledger/numbers/and-with-a-constant-as-a-value.ts @@ -0,0 +1,7 @@ +// ledger: step 4, refused: & with a constant, as a value +// A value's low bits kept, as a value: `a & 7`. +program(() => { + let a = 0; + while (a < 50) a += 9; + print(`${a & 7}`); +}); diff --git a/ledger/numbers/and-with-a-constant-tested.ts b/ledger/numbers/and-with-a-constant-tested.ts new file mode 100644 index 0000000..bae8e3e --- /dev/null +++ b/ledger/numbers/and-with-a-constant-tested.ts @@ -0,0 +1,8 @@ +// ledger: step 0, takes +// Whether a bit of a value is set, in an `if`: `if ((a & 4) !== 0)`. +program(() => { + let a = 0; + while (a < 50) a += 9; + if ((a & 4) !== 0) print("set"); + else print("clear"); +}); diff --git a/ledger/numbers/arctangent-of-two-values.ts b/ledger/numbers/arctangent-of-two-values.ts new file mode 100644 index 0000000..9fa996b --- /dev/null +++ b/ledger/numbers/arctangent-of-two-values.ts @@ -0,0 +1,13 @@ +// ledger: step 4, refused: Math.atan2 +// The angle from the x axis to a point, in radians: `Math.atan2(y, x)`. +program(() => { + let x = 0; + let y = 0; + while (x < 10) { + x += 3; + y += 2; + } + const a = Math.atan2(y, x); + if (a > 0.5) print("more than 0.5"); + else print("0.5 or less"); +}); diff --git a/ledger/numbers/arctangent.ts b/ledger/numbers/arctangent.ts new file mode 100644 index 0000000..5bc5dce --- /dev/null +++ b/ledger/numbers/arctangent.ts @@ -0,0 +1,9 @@ +// ledger: step 4, refused: Math.atan2 +// The angle whose tangent is a value, in radians: `Math.atan(t)`. +program(() => { + let t = 0; + while (t < 1) t += 0.25; + const a = Math.atan(t); + if (a > 0.7) print("more than 0.7"); + else print("0.7 or less"); +}); diff --git a/ledger/numbers/ceil-of-fixed.ts b/ledger/numbers/ceil-of-fixed.ts new file mode 100644 index 0000000..84abef6 --- /dev/null +++ b/ledger/numbers/ceil-of-fixed.ts @@ -0,0 +1,7 @@ +// ledger: step 0, takes +// A number with a fraction rounded up to a whole number: `Math.ceil(x)`. +program(() => { + let x = 0.5; + while (x < 3) x += 0.75; + print(`${Math.ceil(x)}`); +}); diff --git a/ledger/numbers/clamp.ts b/ledger/numbers/clamp.ts new file mode 100644 index 0000000..8782418 --- /dev/null +++ b/ledger/numbers/clamp.ts @@ -0,0 +1,7 @@ +// ledger: step 0, takes +// A value kept between a low and a high: `clamp(a, 0, 10)`. +program(() => { + let a = 0; + while (a < 20) a += 7; + print(`${clamp(a, 0, 10)}`); +}); diff --git a/ledger/numbers/coin-toss.ts b/ledger/numbers/coin-toss.ts new file mode 100644 index 0000000..231428a --- /dev/null +++ b/ledger/numbers/coin-toss.ts @@ -0,0 +1,6 @@ +// ledger: step 4, refused: a random boolean +// A coin toss, true or false alike: `random()`. +program(() => { + if (random()) print("heads"); + else print("tails"); +}); diff --git a/ledger/numbers/constant-worked-out-when-built.ts b/ledger/numbers/constant-worked-out-when-built.ts new file mode 100644 index 0000000..c027a06 --- /dev/null +++ b/ledger/numbers/constant-worked-out-when-built.ts @@ -0,0 +1,8 @@ +// ledger: step 0, takes +// A number the script works out from numbers written in it, when the map is built: `const SIZE = (64 * 4 + 2) / 3 % 50`. +const SIZE = ((64 * 4 + 2) / 3) % 50; +program(() => { + let n = 0; + while (n < SIZE) n += 4; + print(`${n}`); +}); diff --git a/ledger/numbers/cosine.ts b/ledger/numbers/cosine.ts new file mode 100644 index 0000000..cde0d96 --- /dev/null +++ b/ledger/numbers/cosine.ts @@ -0,0 +1,9 @@ +// ledger: step 4, refused: Math.cos +// The cosine of an angle in radians: `Math.cos(a)`. +program(() => { + let a = 0; + while (a < 1) a += 0.25; + const c = Math.cos(a); + if (c > 0.5) print("more than 0.5"); + else print("0.5 or less"); +}); diff --git a/ledger/numbers/difference-of-two-values.ts b/ledger/numbers/difference-of-two-values.ts new file mode 100644 index 0000000..6657d64 --- /dev/null +++ b/ledger/numbers/difference-of-two-values.ts @@ -0,0 +1,11 @@ +// ledger: step 0, takes +// A difference of two values that change while the game runs: `a - b`. +program(() => { + let a = 0; + let b = 0; + while (a < 20) { + a += 7; + b += 2; + } + print(`${a - b}`); +}); diff --git a/ledger/numbers/fixed-compared.ts b/ledger/numbers/fixed-compared.ts new file mode 100644 index 0000000..e5851b2 --- /dev/null +++ b/ledger/numbers/fixed-compared.ts @@ -0,0 +1,8 @@ +// ledger: step 0, takes +// A number with a fraction compared with a whole number and with another fraction, below zero too: `x < -1`. +program(() => { + let x = 0.75; + while (x > -2) x -= 0.5; + if (x < -1) print("below -1"); + if (x >= -2.25) print("not below -2.25"); +}); diff --git a/ledger/numbers/fixed-from-a-whole-number.ts b/ledger/numbers/fixed-from-a-whole-number.ts new file mode 100644 index 0000000..ef1fc4c --- /dev/null +++ b/ledger/numbers/fixed-from-a-whole-number.ts @@ -0,0 +1,9 @@ +// ledger: step 0, takes +// A whole number made a fixed one, so that what is worked out with it keeps a fraction: `fixed(n)`. +program(() => { + let n = 0; + while (n < 5) n += 2; + const x = fixed(n) + 0.5; + if (x > 6) print("more than six"); + else print("six or less"); +}); diff --git a/ledger/numbers/fixed-product-by-a-constant.ts b/ledger/numbers/fixed-product-by-a-constant.ts new file mode 100644 index 0000000..a0e9db8 --- /dev/null +++ b/ledger/numbers/fixed-product-by-a-constant.ts @@ -0,0 +1,9 @@ +// ledger: step 4, refused: a product of fixed numbers +// A number with a fraction times a fraction written in the script: `x * 1.5`. +program(() => { + let x = 0.5; + while (x < 3) x += 0.75; + const p = x * 1.5; + if (p > 4) print("more than 4"); + else print("4 or less"); +}); diff --git a/ledger/numbers/fixed-product.ts b/ledger/numbers/fixed-product.ts new file mode 100644 index 0000000..d662573 --- /dev/null +++ b/ledger/numbers/fixed-product.ts @@ -0,0 +1,13 @@ +// ledger: step 4, refused: a product of fixed numbers +// A product of two numbers with a fraction that change while the game runs: `x * y`. +program(() => { + let x = 0.5; + let y = 1.25; + while (x < 3) { + x += 0.75; + y += 0.5; + } + const p = x * y; + if (p > 8) print("more than 8"); + else print("8 or less"); +}); diff --git a/ledger/numbers/fixed-quotient-by-a-constant.ts b/ledger/numbers/fixed-quotient-by-a-constant.ts new file mode 100644 index 0000000..4483646 --- /dev/null +++ b/ledger/numbers/fixed-quotient-by-a-constant.ts @@ -0,0 +1,9 @@ +// ledger: step 4, refused: a quotient by a constant +// A number with a fraction divided by a number written in the script: `x / 3`. +program(() => { + let x = 0.5; + while (x < 3) x += 0.75; + const q = x / 3; + if (q > 1) print("more than 1"); + else print("1 or less"); +}); diff --git a/ledger/numbers/fixed-quotient.ts b/ledger/numbers/fixed-quotient.ts new file mode 100644 index 0000000..94c4656 --- /dev/null +++ b/ledger/numbers/fixed-quotient.ts @@ -0,0 +1,13 @@ +// ledger: step 4, refused: a quotient of fixed numbers +// A quotient of two numbers with a fraction that change while the game runs: `x / y`. +program(() => { + let x = 0.5; + let y = 0.25; + while (x < 3) { + x += 0.75; + y += 0.5; + } + const q = x / y; + if (q > 1.5) print("more than 1.5"); + else print("1.5 or less"); +}); diff --git a/ledger/numbers/fixed-reciprocal.ts b/ledger/numbers/fixed-reciprocal.ts new file mode 100644 index 0000000..38fe584 --- /dev/null +++ b/ledger/numbers/fixed-reciprocal.ts @@ -0,0 +1,9 @@ +// ledger: step 4, refused: a quotient of fixed numbers +// A number written in the script divided by a number with a fraction: `1 / x`. +program(() => { + let x = 0.5; + while (x < 3) x += 0.75; + const q = 1 / x; + if (q < 0.5) print("below 0.5"); + else print("0.5 or more"); +}); diff --git a/ledger/numbers/fixed-sum.ts b/ledger/numbers/fixed-sum.ts new file mode 100644 index 0000000..bf30d7a --- /dev/null +++ b/ledger/numbers/fixed-sum.ts @@ -0,0 +1,12 @@ +// ledger: step 0, takes +// Numbers with a fraction added and subtracted: `x += 0.25`, `x - 0.5`. +program(() => { + let x = 0.5; + let n = 0; + while (n < 5) { + x += 0.25; + n += 1; + } + if (x - 0.5 > 1.2) print("past 1.2"); + else print("not past 1.2"); +}); diff --git a/ledger/numbers/floor-of-fixed.ts b/ledger/numbers/floor-of-fixed.ts new file mode 100644 index 0000000..6820d80 --- /dev/null +++ b/ledger/numbers/floor-of-fixed.ts @@ -0,0 +1,7 @@ +// ledger: step 0, takes +// A number with a fraction rounded down to a whole number: `Math.floor(x)`. +program(() => { + let x = 0.5; + while (x < 3) x += 0.75; + print(`${Math.floor(x)}`); +}); diff --git a/ledger/numbers/hypotenuse.ts b/ledger/numbers/hypotenuse.ts new file mode 100644 index 0000000..814082f --- /dev/null +++ b/ledger/numbers/hypotenuse.ts @@ -0,0 +1,13 @@ +// ledger: step 4, refused: a product of two values +// The length of a line from its two sides: `Math.hypot(dx, dy)`. +program(() => { + let dx = 0; + let dy = 0; + while (dx < 10) { + dx += 3; + dy += 4; + } + const d = Math.hypot(dx, dy); + if (d > 19.5) print("more than 19.5"); + else print("19.5 or less"); +}); diff --git a/ledger/numbers/i32-of-a-u32.ts b/ledger/numbers/i32-of-a-u32.ts new file mode 100644 index 0000000..1b91e4a --- /dev/null +++ b/ledger/numbers/i32-of-a-u32.ts @@ -0,0 +1,12 @@ +// ledger: step 0, takes +// A `u32` read back as a signed number: `i32(x)`. +program(() => { + let x: u32 = u32(0); + let n = 0; + while (n < 3) { + x = u32(x - 1); + n += 1; + } + if (i32(x) < 0) print("below zero"); + else print("not below zero"); +}); diff --git a/ledger/numbers/increment-and-decrement.ts b/ledger/numbers/increment-and-decrement.ts new file mode 100644 index 0000000..a2424e8 --- /dev/null +++ b/ledger/numbers/increment-and-decrement.ts @@ -0,0 +1,14 @@ +// ledger: step 0, takes +// A variable counted up and down, and changed by an amount: `n++`, `n--`, `n += k`, `n -= k`. +program(() => { + let n = 0; + let k = 1; + while (k < 6) { + n++; + n += k; + n -= 2; + k++; + } + n--; + print(`${n}`); +}); diff --git a/ledger/numbers/math-angle-to.ts b/ledger/numbers/math-angle-to.ts new file mode 100644 index 0000000..5225fd9 --- /dev/null +++ b/ledger/numbers/math-angle-to.ts @@ -0,0 +1,14 @@ +// ledger: step 4, refused: Math.atan2 +// The angle from one point to another, in radians, from `trigscript/math`: `angleTo(x1, y1, x2, y2)`. +import { angleTo } from "trigscript/math"; +program(() => { + let x = 0; + let y = 0; + while (x < 10) { + x += 3; + y += 4; + } + const a = angleTo(0, 0, x, y); + if (a > 0.9) print("more than 0.9"); + else print("0.9 or less"); +}); diff --git a/ledger/numbers/math-approach.ts b/ledger/numbers/math-approach.ts new file mode 100644 index 0000000..c6ae549 --- /dev/null +++ b/ledger/numbers/math-approach.ts @@ -0,0 +1,12 @@ +// ledger: step 0, takes +// A number moved towards another by a step at most, never past it, from `trigscript/math`: `approach(x, target, step)`. +import { approach } from "trigscript/math"; +program(() => { + let x = 0; + let n = 0; + while (n < 5) { + x = approach(x, 12, 5); + n += 1; + } + print(`${x}`); +}); diff --git a/ledger/numbers/math-clamp01.ts b/ledger/numbers/math-clamp01.ts new file mode 100644 index 0000000..c5752bb --- /dev/null +++ b/ledger/numbers/math-clamp01.ts @@ -0,0 +1,9 @@ +// ledger: step 0, takes +// A fraction kept between 0 and 1, from `trigscript/math`: `clamp01(t)`. +import { clamp01 } from "trigscript/math"; +program(() => { + let t = 0; + while (t < 1.2) t += 0.5; + if (clamp01(t) === 1) print("one"); + else print("below one"); +}); diff --git a/ledger/numbers/math-degrees.ts b/ledger/numbers/math-degrees.ts new file mode 100644 index 0000000..f2f72d6 --- /dev/null +++ b/ledger/numbers/math-degrees.ts @@ -0,0 +1,10 @@ +// ledger: step 4, refused: a quotient of fixed numbers +// Radians as degrees, from `trigscript/math`: `degrees(r)`. +import { degrees } from "trigscript/math"; +program(() => { + let r = 0; + while (r < 1.5) r += 0.25; + const d = degrees(r); + if (d > 90) print("more than 90"); + else print("90 or less"); +}); diff --git a/ledger/numbers/math-distance-squared.ts b/ledger/numbers/math-distance-squared.ts new file mode 100644 index 0000000..c18d04d --- /dev/null +++ b/ledger/numbers/math-distance-squared.ts @@ -0,0 +1,12 @@ +// ledger: step 4, refused: a product of two values +// The distance between two points squared, a whole number, from `trigscript/math`: `distanceSq(x1, y1, x2, y2)`. +import { distanceSq } from "trigscript/math"; +program(() => { + let x = 0; + let y = 0; + while (x < 10) { + x += 3; + y += 4; + } + print(`${distanceSq(0, 0, x, y)}`); +}); diff --git a/ledger/numbers/math-distance.ts b/ledger/numbers/math-distance.ts new file mode 100644 index 0000000..455bff6 --- /dev/null +++ b/ledger/numbers/math-distance.ts @@ -0,0 +1,14 @@ +// ledger: step 4, refused: a product of two values +// The distance between two points as the crow flies, from `trigscript/math`: `distance(x1, y1, x2, y2)`. +import { distance } from "trigscript/math"; +program(() => { + let x = 0; + let y = 0; + while (x < 10) { + x += 3; + y += 4; + } + const d = distance(0, 0, x, y); + if (d > 19.5) print("more than 19.5"); + else print("19.5 or less"); +}); diff --git a/ledger/numbers/math-easing.ts b/ledger/numbers/math-easing.ts new file mode 100644 index 0000000..e2bddc7 --- /dev/null +++ b/ledger/numbers/math-easing.ts @@ -0,0 +1,17 @@ +// ledger: step 4, refused: a product of fixed numbers +// The easing curves of `trigscript/math`, how far along a motion is at a fraction of its time: `easeInQuad`, +// `easeOutQuad`, `easeInOutQuad`, `easeInCubic`, `easeOutCubic`, `easeOutBack` and `smoothstep`, each of `t`. +import { easeInCubic, easeInOutQuad, easeInQuad, easeOutBack, easeOutCubic, easeOutQuad, smoothstep } from "trigscript/math"; +program(() => { + let t = 0; + while (t < 0.5) t += 0.125; + let past = 0; + if (easeInQuad(t) > 0.5) past += 1; + if (easeOutQuad(t) > 0.5) past += 1; + if (easeInOutQuad(t) > 0.5) past += 1; + if (easeInCubic(t) > 0.5) past += 1; + if (easeOutCubic(t) > 0.5) past += 1; + if (easeOutBack(t) > 0.5) past += 1; + if (smoothstep(t) > 0.5) past += 1; + print(`${past} past half`); +}); diff --git a/ledger/numbers/math-inverse-lerp.ts b/ledger/numbers/math-inverse-lerp.ts new file mode 100644 index 0000000..c3b3c75 --- /dev/null +++ b/ledger/numbers/math-inverse-lerp.ts @@ -0,0 +1,16 @@ +// ledger: step 4, refused: a quotient of two values +// Where a number is between two others, as a fraction, from `trigscript/math`: `inverseLerp(a, b, v)`. +import { inverseLerp } from "trigscript/math"; +program(() => { + let a = 0; + let b = 10; + let v = 0; + while (v < 14) { + a += 1; + b += 3; + v += 3.5; + } + const t = inverseLerp(a, b, v); + if (t > 0.4) print("past 0.4"); + else print("not past 0.4"); +}); diff --git a/ledger/numbers/math-lerp.ts b/ledger/numbers/math-lerp.ts new file mode 100644 index 0000000..7cf1c52 --- /dev/null +++ b/ledger/numbers/math-lerp.ts @@ -0,0 +1,16 @@ +// ledger: step 4, refused: a product of two values +// From one number to another by a fraction, from `trigscript/math`: `lerp(a, b, t)`. +import { lerp } from "trigscript/math"; +program(() => { + let a = 0; + let b = 10; + let t = 0; + while (t < 0.5) { + a += 1; + b += 3; + t += 0.125; + } + const x = lerp(a, b, t); + if (x > 14) print("past 14"); + else print("not past 14"); +}); diff --git a/ledger/numbers/math-manhattan.ts b/ledger/numbers/math-manhattan.ts new file mode 100644 index 0000000..e64a10e --- /dev/null +++ b/ledger/numbers/math-manhattan.ts @@ -0,0 +1,12 @@ +// ledger: step 0, takes +// The distance between two points along the axes, |dx| + |dy|, from `trigscript/math`: `manhattan(x1, y1, x2, y2)`. +import { manhattan } from "trigscript/math"; +program(() => { + let x = 0; + let y = 0; + while (x < 10) { + x += 3; + y -= 4; + } + print(`${manhattan(0, 0, x, y)}`); +}); diff --git a/ledger/numbers/math-pi.ts b/ledger/numbers/math-pi.ts new file mode 100644 index 0000000..e9c7e8b --- /dev/null +++ b/ledger/numbers/math-pi.ts @@ -0,0 +1,12 @@ +// ledger: step 0, takes +// π from `trigscript/math`: `PI`, a number with a fraction known when the map is built. +import { PI } from "trigscript/math"; +program(() => { + let a = 0; + let steps = 0; + while (a < PI) { + a += 0.5; + steps += 1; + } + print(`${steps}`); +}); diff --git a/ledger/numbers/math-radians.ts b/ledger/numbers/math-radians.ts new file mode 100644 index 0000000..25eebc8 --- /dev/null +++ b/ledger/numbers/math-radians.ts @@ -0,0 +1,10 @@ +// ledger: step 4, refused: a quotient by a constant +// Degrees as radians, from `trigscript/math`: `radians(d)`. +import { radians } from "trigscript/math"; +program(() => { + let d = 0; + while (d < 90) d += 25; + const r = radians(d); + if (r > 1.6) print("more than 1.6"); + else print("1.6 or less"); +}); diff --git a/ledger/numbers/math-remap.ts b/ledger/numbers/math-remap.ts new file mode 100644 index 0000000..b980108 --- /dev/null +++ b/ledger/numbers/math-remap.ts @@ -0,0 +1,16 @@ +// ledger: step 4, refused: a quotient of two values +// A number moved from one range into another, from `trigscript/math`: `remap(v, a1, b1, a2, b2)`. +import { remap } from "trigscript/math"; +program(() => { + let low = 0; + let high = 10; + let v = 0; + while (v < 4) { + low += 1; + high += 2; + v += 1.5; + } + const x = remap(v, low, high, 100, 200); + if (x > 140) print("past 140"); + else print("not past 140"); +}); diff --git a/ledger/numbers/math-sign.ts b/ledger/numbers/math-sign.ts new file mode 100644 index 0000000..28ab277 --- /dev/null +++ b/ledger/numbers/math-sign.ts @@ -0,0 +1,8 @@ +// ledger: step 0, takes +// −1, 0 or 1 by whether a number is below, at or above zero, from `trigscript/math`: `sign(n)`. +import { sign } from "trigscript/math"; +program(() => { + let n = 5; + while (n > -10) n -= 4; + print(`${sign(n)}`); +}); diff --git a/ledger/numbers/math-tau.ts b/ledger/numbers/math-tau.ts new file mode 100644 index 0000000..e135035 --- /dev/null +++ b/ledger/numbers/math-tau.ts @@ -0,0 +1,14 @@ +// ledger: step 0, takes +// A whole turn from `trigscript/math`: `TAU`, 2π, to keep an angle within one turn. +import { TAU } from "trigscript/math"; +program(() => { + let a = 0; + let n = 0; + while (n < 10) { + a += 1.5; + if (a >= TAU) a -= TAU; + n += 1; + } + if (a < 3) print("first half"); + else print("second half"); +}); diff --git a/ledger/numbers/math-vec-length.ts b/ledger/numbers/math-vec-length.ts new file mode 100644 index 0000000..06d5b24 --- /dev/null +++ b/ledger/numbers/math-vec-length.ts @@ -0,0 +1,9 @@ +// ledger: step 4, refused: a product of fixed numbers +// How long a `Vec` from `trigscript/math` is: `v.length()`, the square root of its sides' squares. +import { Vec } from "trigscript/math"; +program(() => { + let v = new Vec(fixed(0), fixed(0)); + while (v.x < 3) v = v.add(new Vec(fixed(1), fixed(1))); + if (v.length() > 4) print("longer than 4"); + else print("4 or shorter"); +}); diff --git a/ledger/numbers/math-vec.ts b/ledger/numbers/math-vec.ts new file mode 100644 index 0000000..859d5b5 --- /dev/null +++ b/ledger/numbers/math-vec.ts @@ -0,0 +1,10 @@ +// ledger: step 0, takes +// A point of two numbers with a fraction, from `trigscript/math`: `new Vec(x, y)`, moved by another with `add`. +import { Vec } from "trigscript/math"; +program(() => { + let p = new Vec(fixed(0), fixed(0)); + const step = new Vec(fixed(1), 0.5); + while (p.x < 5) p = p.add(step); + if (p.y > 2) print("past 2"); + else print("not past 2"); +}); diff --git a/ledger/numbers/math-wrap.ts b/ledger/numbers/math-wrap.ts new file mode 100644 index 0000000..3b996b4 --- /dev/null +++ b/ledger/numbers/math-wrap.ts @@ -0,0 +1,8 @@ +// ledger: step 4, refused: a remainder by a constant +// A number brought round into 0 … size − 1, below zero too, from `trigscript/math`: `wrap(n, size)` (−1 is size − 1). +import { wrap } from "trigscript/math"; +program(() => { + let n = 0; + while (n > -10) n -= 3; + print(`${wrap(n, 10)}`); +}); diff --git a/ledger/numbers/max-of-two-values.ts b/ledger/numbers/max-of-two-values.ts new file mode 100644 index 0000000..34b3909 --- /dev/null +++ b/ledger/numbers/max-of-two-values.ts @@ -0,0 +1,11 @@ +// ledger: step 0, takes +// The larger of two values that change while the game runs: `Math.max(a, b)`. +program(() => { + let a = 0; + let b = 30; + while (a < 20) { + a += 3; + b -= 2; + } + print(`${Math.max(a, b)}`); +}); diff --git a/ledger/numbers/min-of-two-values.ts b/ledger/numbers/min-of-two-values.ts new file mode 100644 index 0000000..6e8aafb --- /dev/null +++ b/ledger/numbers/min-of-two-values.ts @@ -0,0 +1,11 @@ +// ledger: step 0, takes +// The smaller of two values that change while the game runs: `Math.min(a, b)`. +program(() => { + let a = 0; + let b = 30; + while (a < 20) { + a += 3; + b -= 2; + } + print(`${Math.min(a, b)}`); +}); diff --git a/ledger/numbers/negative-of-a-value.ts b/ledger/numbers/negative-of-a-value.ts new file mode 100644 index 0000000..4a34460 --- /dev/null +++ b/ledger/numbers/negative-of-a-value.ts @@ -0,0 +1,8 @@ +// ledger: step 0, takes +// A value's negative: `-a`. +program(() => { + let a = 0; + while (a < 10) a += 3; + const b = -a; + print(`${b}`); +}); diff --git a/ledger/numbers/number-chosen-by-a-condition.ts b/ledger/numbers/number-chosen-by-a-condition.ts new file mode 100644 index 0000000..5cefac8 --- /dev/null +++ b/ledger/numbers/number-chosen-by-a-condition.ts @@ -0,0 +1,12 @@ +// ledger: step 0, takes +// One number or another, by a test while the game runs: `a > b ? a : b + 1`. +program(() => { + let a = 0; + let b = 12; + while (a < 20) { + a += 3; + b += 1; + } + const c = a > b ? a : b + 1; + print(`${c}`); +}); diff --git a/ledger/numbers/number-wraps-past-the-top.ts b/ledger/numbers/number-wraps-past-the-top.ts new file mode 100644 index 0000000..9afe21e --- /dev/null +++ b/ledger/numbers/number-wraps-past-the-top.ts @@ -0,0 +1,11 @@ +// ledger: step 0, takes +// A number goes round past 2 147 483 647 to −2 147 483 648, as a 32-bit number does. +program(() => { + let a = 2147483640; + let n = 0; + while (n < 10) { + a += 1; + n += 1; + } + print(`${a}`); +}); diff --git a/ledger/numbers/or-of-two-values.ts b/ledger/numbers/or-of-two-values.ts new file mode 100644 index 0000000..c7f02f7 --- /dev/null +++ b/ledger/numbers/or-of-two-values.ts @@ -0,0 +1,11 @@ +// ledger: step 4, refused: | of two values +// The bits either of two values that change while the game runs has: `a | b`. +program(() => { + let a = 0; + let b = 1; + while (a < 50) { + a += 9; + b += 5; + } + print(`${a | b}`); +}); diff --git a/ledger/numbers/or-with-a-constant.ts b/ledger/numbers/or-with-a-constant.ts new file mode 100644 index 0000000..e05f272 --- /dev/null +++ b/ledger/numbers/or-with-a-constant.ts @@ -0,0 +1,7 @@ +// ledger: step 4, refused: | with a constant, as a value +// A value with bits set, as a value: `a | 1`. +program(() => { + let a = 0; + while (a < 50) a += 10; + print(`${a | 1}`); +}); diff --git a/ledger/numbers/product-by-a-constant.ts b/ledger/numbers/product-by-a-constant.ts new file mode 100644 index 0000000..7c73f4e --- /dev/null +++ b/ledger/numbers/product-by-a-constant.ts @@ -0,0 +1,11 @@ +// ledger: step 0, takes +// A value times a whole number written in the script: `a * 3`. +program(() => { + let a = 0; + let n = 0; + while (n < 5) { + a = n * 3; + n += 1; + } + print(`${a}`); +}); diff --git a/ledger/numbers/product-of-two-values.ts b/ledger/numbers/product-of-two-values.ts new file mode 100644 index 0000000..1f34607 --- /dev/null +++ b/ledger/numbers/product-of-two-values.ts @@ -0,0 +1,11 @@ +// ledger: step 4, refused: a product of two values +// A product of two values that change while the game runs: `a * b`. +program(() => { + let a = 1; + let i = 1; + while (i < 5) { + a = a * i; + i += 1; + } + print(`${a}`); +}); diff --git a/ledger/numbers/quotient-by-a-constant.ts b/ledger/numbers/quotient-by-a-constant.ts new file mode 100644 index 0000000..2515b4e --- /dev/null +++ b/ledger/numbers/quotient-by-a-constant.ts @@ -0,0 +1,7 @@ +// ledger: step 4, refused: a quotient by a constant +// A value divided by a whole number written in the script, rounded towards zero: `a / 7`. +program(() => { + let a = 0; + while (a < 50) a += 9; + print(`${a / 7}`); +}); diff --git a/ledger/numbers/quotient-by-a-power-of-two.ts b/ledger/numbers/quotient-by-a-power-of-two.ts new file mode 100644 index 0000000..bd3111b --- /dev/null +++ b/ledger/numbers/quotient-by-a-power-of-two.ts @@ -0,0 +1,8 @@ +// ledger: step 4, refused: a quotient by a constant +// A value divided by a power of two written in the script, rounded towards zero as JavaScript does: `a / 4` is −2 for +// −9, where `a >> 2` is −3. +program(() => { + let a = 0; + while (a > -8) a -= 3; + print(`${a / 4}`); +}); diff --git a/ledger/numbers/quotient-of-two-values.ts b/ledger/numbers/quotient-of-two-values.ts new file mode 100644 index 0000000..0789931 --- /dev/null +++ b/ledger/numbers/quotient-of-two-values.ts @@ -0,0 +1,11 @@ +// ledger: step 4, refused: a quotient of two values +// A quotient of two values that change while the game runs, rounded towards zero: `a / b`. +program(() => { + let a = 0; + let b = 1; + while (a < 20) { + a += 7; + b += 1; + } + print(`${a / b}`); +}); diff --git a/ledger/numbers/random-chance.ts b/ledger/numbers/random-chance.ts new file mode 100644 index 0000000..f454f1a --- /dev/null +++ b/ledger/numbers/random-chance.ts @@ -0,0 +1,7 @@ +// ledger: step 4, refused: a random number +// True a number of times in a hundred, from `trigscript/random`: `chance(25)`. +import { chance } from "trigscript/random"; +program(() => { + if (chance(25)) print("lucky"); + else print("not this time"); +}); diff --git a/ledger/numbers/random-dice.ts b/ledger/numbers/random-dice.ts new file mode 100644 index 0000000..06dae19 --- /dev/null +++ b/ledger/numbers/random-dice.ts @@ -0,0 +1,8 @@ +// ledger: step 4, refused: a random number +// The total of dice thrown, from `trigscript/random`: `dice(3, 6)`, from 3 to 18. +import { dice } from "trigscript/random"; +program(() => { + const total = dice(3, 6); + if (total > 10) print("high"); + else print("low"); +}); diff --git a/ledger/numbers/random-number-below-a-value.ts b/ledger/numbers/random-number-below-a-value.ts new file mode 100644 index 0000000..a70b669 --- /dev/null +++ b/ledger/numbers/random-number-below-a-value.ts @@ -0,0 +1,9 @@ +// ledger: step 4, refused: a random number +// A random whole number below a bound that changes while the game runs: `random(n)`. +program(() => { + let n = 1; + while (n < 10) n += 4; + const roll = random(n); + if (roll < 5) print("low"); + else print("high"); +}); diff --git a/ledger/numbers/random-number.ts b/ledger/numbers/random-number.ts new file mode 100644 index 0000000..f3e560a --- /dev/null +++ b/ledger/numbers/random-number.ts @@ -0,0 +1,7 @@ +// ledger: step 4, refused: a random number +// A random whole number from 0 to n − 1, the game's own random numbers: `random(6)`. +program(() => { + const roll = random(6); + if (roll < 3) print("low"); + else print("high"); +}); diff --git a/ledger/numbers/random-pick.ts b/ledger/numbers/random-pick.ts new file mode 100644 index 0000000..dde2870 --- /dev/null +++ b/ledger/numbers/random-pick.ts @@ -0,0 +1,9 @@ +// ledger: step 4, refused: a random number +// One of a list at random, from `trigscript/random`: `pick(list)`. +import { pick } from "trigscript/random"; +const WAVES = [5, 8, 13]; +program(() => { + const size = pick(WAVES); + if (size > 6) print("a big wave"); + else print("a small wave"); +}); diff --git a/ledger/numbers/random-range.ts b/ledger/numbers/random-range.ts new file mode 100644 index 0000000..14948dc --- /dev/null +++ b/ledger/numbers/random-range.ts @@ -0,0 +1,8 @@ +// ledger: step 4, refused: a random number +// A random whole number from a low to a high, both included, from `trigscript/random`: `range(1, 3)`. +import { range } from "trigscript/random"; +program(() => { + const lane = range(1, 3); + if (lane === 2) print("the middle lane"); + else print("a side lane"); +}); diff --git a/ledger/numbers/random-seeded-generator.ts b/ledger/numbers/random-seeded-generator.ts new file mode 100644 index 0000000..63af0d7 --- /dev/null +++ b/ledger/numbers/random-seeded-generator.ts @@ -0,0 +1,10 @@ +// ledger: step 4, refused: ^ of two values +// A random number generator of the script's own, seeded, the same numbers every game, from `trigscript/random`: +// `new Random(seed)`, then `rng.range(0, 63)`. +import { Random } from "trigscript/random"; +program(() => { + const rng = new Random(1234); + const x = rng.range(0, 63); + if (x < 32) print("left"); + else print("right"); +}); diff --git a/ledger/numbers/random-shuffle.ts b/ledger/numbers/random-shuffle.ts new file mode 100644 index 0000000..5aa1a37 --- /dev/null +++ b/ledger/numbers/random-shuffle.ts @@ -0,0 +1,9 @@ +// ledger: step 4, refused: a random number +// An array of the program put in a random order, in place, from `trigscript/random`: `shuffle(order)`. +import { shuffle } from "trigscript/random"; +program(() => { + const order = [1, 2, 3, 4]; + shuffle(order); + if (order[0] === 1) print("1 stayed first"); + else print("1 moved"); +}); diff --git a/ledger/numbers/random-weighted.ts b/ledger/numbers/random-weighted.ts new file mode 100644 index 0000000..adf2de7 --- /dev/null +++ b/ledger/numbers/random-weighted.ts @@ -0,0 +1,8 @@ +// ledger: step 4, refused: a random number +// The place of one of some weights, each as likely as its share, from `trigscript/random`: `weighted([70, 25, 5])`. +import { weighted } from "trigscript/random"; +program(() => { + const tier = weighted([70, 25, 5]); + if (tier === 0) print("common"); + else print("rare"); +}); diff --git a/ledger/numbers/remainder-by-a-constant.ts b/ledger/numbers/remainder-by-a-constant.ts new file mode 100644 index 0000000..7ff54fc --- /dev/null +++ b/ledger/numbers/remainder-by-a-constant.ts @@ -0,0 +1,7 @@ +// ledger: step 4, refused: a remainder by a constant +// A remainder by a whole number written in the script: `a % 7`. +program(() => { + let a = 0; + while (a < 50) a += 9; + print(`${a % 7}`); +}); diff --git a/ledger/numbers/remainder-by-a-power-of-two.ts b/ledger/numbers/remainder-by-a-power-of-two.ts new file mode 100644 index 0000000..baf4c6d --- /dev/null +++ b/ledger/numbers/remainder-by-a-power-of-two.ts @@ -0,0 +1,8 @@ +// ledger: step 4, refused: a remainder by a constant +// A remainder by a power of two written in the script, which keeps the sign of the value as JavaScript's does: `a % 8` +// is −1 for −9, where `a & 7` is 7. +program(() => { + let a = 0; + while (a > -8) a -= 3; + print(`${a % 8}`); +}); diff --git a/ledger/numbers/remainder-of-two-values.ts b/ledger/numbers/remainder-of-two-values.ts new file mode 100644 index 0000000..e93060e --- /dev/null +++ b/ledger/numbers/remainder-of-two-values.ts @@ -0,0 +1,11 @@ +// ledger: step 4, refused: a remainder of two values +// A remainder of two values that change while the game runs: `a % b`. +program(() => { + let a = 0; + let b = 1; + while (a < 20) { + a += 7; + b += 2; + } + print(`${a % b}`); +}); diff --git a/ledger/numbers/round-of-fixed.ts b/ledger/numbers/round-of-fixed.ts new file mode 100644 index 0000000..939cf34 --- /dev/null +++ b/ledger/numbers/round-of-fixed.ts @@ -0,0 +1,7 @@ +// ledger: step 0, takes +// A number with a fraction rounded to the nearest whole number: `Math.round(x)`. +program(() => { + let x = 0.5; + while (x < 3) x += 0.75; + print(`${Math.round(x)}`); +}); diff --git a/ledger/numbers/shift-left-by-a-constant.ts b/ledger/numbers/shift-left-by-a-constant.ts new file mode 100644 index 0000000..97eed39 --- /dev/null +++ b/ledger/numbers/shift-left-by-a-constant.ts @@ -0,0 +1,11 @@ +// ledger: step 0, takes +// A value shifted left by a number written in the script, as a value: `a << 2` (the value times 4). +program(() => { + let a = 0; + let n = 0; + while (n < 5) { + a = n << 2; + n += 1; + } + print(`${a}`); +}); diff --git a/ledger/numbers/shift-left-by-a-value.ts b/ledger/numbers/shift-left-by-a-value.ts new file mode 100644 index 0000000..3812b67 --- /dev/null +++ b/ledger/numbers/shift-left-by-a-value.ts @@ -0,0 +1,7 @@ +// ledger: step 4, refused: a shift by a value (<<) +// A value shifted left by a number that changes while the game runs: `1 << n`. +program(() => { + let n = 0; + while (n < 5) n += 2; + print(`${1 << n}`); +}); diff --git a/ledger/numbers/shift-right-by-a-constant.ts b/ledger/numbers/shift-right-by-a-constant.ts new file mode 100644 index 0000000..a446af2 --- /dev/null +++ b/ledger/numbers/shift-right-by-a-constant.ts @@ -0,0 +1,7 @@ +// ledger: step 0, takes +// A value shifted right by a number written in the script, keeping its sign, as a value: `a >> 2`. +program(() => { + let a = 0; + while (a > -50) a -= 9; + print(`${a >> 2}`); +}); diff --git a/ledger/numbers/shift-right-by-a-value.ts b/ledger/numbers/shift-right-by-a-value.ts new file mode 100644 index 0000000..eb21591 --- /dev/null +++ b/ledger/numbers/shift-right-by-a-value.ts @@ -0,0 +1,11 @@ +// ledger: step 4, refused: a shift by a value (>>) +// A value shifted right by a number that changes while the game runs: `a >> n`. +program(() => { + let a = 1000; + let n = 0; + while (n < 5) { + a += 9; + n += 2; + } + print(`${a >> n}`); +}); diff --git a/ledger/numbers/sine.ts b/ledger/numbers/sine.ts new file mode 100644 index 0000000..a0ff78b --- /dev/null +++ b/ledger/numbers/sine.ts @@ -0,0 +1,9 @@ +// ledger: step 4, refused: Math.sin +// The sine of an angle in radians: `Math.sin(a)`. +program(() => { + let a = 0; + while (a < 1) a += 0.25; + const s = Math.sin(a); + if (s > 0.8) print("more than 0.8"); + else print("0.8 or less"); +}); diff --git a/ledger/numbers/square-root-of-a-whole-number.ts b/ledger/numbers/square-root-of-a-whole-number.ts new file mode 100644 index 0000000..02a4430 --- /dev/null +++ b/ledger/numbers/square-root-of-a-whole-number.ts @@ -0,0 +1,9 @@ +// ledger: step 4, refused: Math.sqrt +// The square root of a whole number, as a number with a fraction: `Math.sqrt(n)`. +program(() => { + let n = 0; + while (n < 20) n += 7; + const r = Math.sqrt(n); + if (r > 4.5) print("more than 4.5"); + else print("4.5 or less"); +}); diff --git a/ledger/numbers/square-root-of-fixed.ts b/ledger/numbers/square-root-of-fixed.ts new file mode 100644 index 0000000..0212cea --- /dev/null +++ b/ledger/numbers/square-root-of-fixed.ts @@ -0,0 +1,9 @@ +// ledger: step 4, refused: Math.sqrt +// The square root of a number with a fraction: `Math.sqrt(x)`. +program(() => { + let x = 0.5; + while (x < 3) x += 0.75; + const r = Math.sqrt(x); + if (r > 1.5) print("more than 1.5"); + else print("1.5 or less"); +}); diff --git a/ledger/numbers/sum-of-two-values.ts b/ledger/numbers/sum-of-two-values.ts new file mode 100644 index 0000000..06f8841 --- /dev/null +++ b/ledger/numbers/sum-of-two-values.ts @@ -0,0 +1,11 @@ +// ledger: step 0, takes +// A sum of two values that change while the game runs: `a + b`. +program(() => { + let a = 0; + let b = 0; + while (a < 20) { + a += 3; + b += 5; + } + print(`${a + b}`); +}); diff --git a/ledger/numbers/tangent.ts b/ledger/numbers/tangent.ts new file mode 100644 index 0000000..235b7c6 --- /dev/null +++ b/ledger/numbers/tangent.ts @@ -0,0 +1,9 @@ +// ledger: step 4, refused: Math.sin +// The tangent of an angle in radians, its sine over its cosine: `Math.tan(a)`. +program(() => { + let a = 0; + while (a < 1) a += 0.25; + const t = Math.tan(a); + if (t > 1.5) print("more than 1.5"); + else print("1.5 or less"); +}); diff --git a/ledger/numbers/trunc-of-fixed.ts b/ledger/numbers/trunc-of-fixed.ts new file mode 100644 index 0000000..7b20cdd --- /dev/null +++ b/ledger/numbers/trunc-of-fixed.ts @@ -0,0 +1,7 @@ +// ledger: step 4, refused: a quotient by a constant +// A number with a fraction cut to its whole part, towards zero: `Math.trunc(x)` (−2.5 is −2). +program(() => { + let x = 0.5; + while (x > -2) x -= 0.75; + print(`${Math.trunc(x)}`); +}); diff --git a/ledger/numbers/trunc-with-or-zero.ts b/ledger/numbers/trunc-with-or-zero.ts new file mode 100644 index 0000000..d04c866 --- /dev/null +++ b/ledger/numbers/trunc-with-or-zero.ts @@ -0,0 +1,7 @@ +// ledger: step 4, refused: a quotient by a constant +// A number with a fraction cut to its whole part the JavaScript way: `x | 0`. +program(() => { + let x = 0.5; + while (x > -2) x -= 0.75; + print(`${x | 0}`); +}); diff --git a/ledger/numbers/u16-variable.ts b/ledger/numbers/u16-variable.ts new file mode 100644 index 0000000..e14bda6 --- /dev/null +++ b/ledger/numbers/u16-variable.ts @@ -0,0 +1,11 @@ +// ledger: step 4, refused: a u16 variable +// A `u16` variable, which stays within 0 … 65 535: stored below zero it is 0. +program(() => { + let a: u16 = 5; + let n = 0; + while (n < 3) { + a -= 4; + n += 1; + } + print(`${a}`); +}); diff --git a/ledger/numbers/u32-against-a-signed-number.ts b/ledger/numbers/u32-against-a-signed-number.ts new file mode 100644 index 0000000..f98b58b --- /dev/null +++ b/ledger/numbers/u32-against-a-signed-number.ts @@ -0,0 +1,13 @@ +// ledger: step 0, takes +// A `u32` compared with a plain number below zero, exactly: any `u32` is larger than −1, however its bits read signed. +program(() => { + let a = 0; + let n = 0; + while (a > -10) { + a -= 3; + n -= 1; + } + const x: u32 = u32(a); + if (x > n) print("larger"); + else print("not larger"); +}); diff --git a/ledger/numbers/u32-compared.ts b/ledger/numbers/u32-compared.ts new file mode 100644 index 0000000..d3b0753 --- /dev/null +++ b/ledger/numbers/u32-compared.ts @@ -0,0 +1,8 @@ +// ledger: step 0, takes +// A number read as a `u32`, 0 … 4 294 967 295, compared as one: `u32(a) > 100` is true for a number below zero. +program(() => { + let a = 0; + while (a > -10) a -= 3; + if (u32(a) > 100) print("large"); + else print("small"); +}); diff --git a/ledger/numbers/u32-quotient.ts b/ledger/numbers/u32-quotient.ts new file mode 100644 index 0000000..3a637e9 --- /dev/null +++ b/ledger/numbers/u32-quotient.ts @@ -0,0 +1,9 @@ +// ledger: step 4, refused: a quotient by a constant +// A `u32` divided, as an unsigned number: `u32(a) / 2` of −2 is 2 147 483 647. +program(() => { + let a = 0; + while (a > -2) a -= 1; + const q: u32 = u32(a) / 2; + if (q > 1000) print("large"); + else print("small"); +}); diff --git a/ledger/numbers/u32-with-shift-zero.ts b/ledger/numbers/u32-with-shift-zero.ts new file mode 100644 index 0000000..d25b2e7 --- /dev/null +++ b/ledger/numbers/u32-with-shift-zero.ts @@ -0,0 +1,8 @@ +// ledger: step 0, takes +// A number read as a `u32` the JavaScript way: `a >>> 0`, which costs nothing. +program(() => { + let a = 0; + while (a > -10) a -= 3; + if (a >>> 0 > 100) print("large"); + else print("small"); +}); diff --git a/ledger/numbers/u8-variable.ts b/ledger/numbers/u8-variable.ts new file mode 100644 index 0000000..49a1d21 --- /dev/null +++ b/ledger/numbers/u8-variable.ts @@ -0,0 +1,11 @@ +// ledger: step 4, refused: a u8 variable +// A `u8` variable, which stays within 0 … 255: stored below zero it is 0, above 255 it is 255. +program(() => { + let a: u8 = 250; + let n = 0; + while (n < 3) { + a += 4; + n += 1; + } + print(`${a}`); +}); diff --git a/ledger/numbers/unsigned-shift-right-by-a-constant.ts b/ledger/numbers/unsigned-shift-right-by-a-constant.ts new file mode 100644 index 0000000..05cf209 --- /dev/null +++ b/ledger/numbers/unsigned-shift-right-by-a-constant.ts @@ -0,0 +1,7 @@ +// ledger: step 0, takes +// A value shifted right by a number written in the script, its top bits zero: `a >>> 28` (a number below zero is large). +program(() => { + let a = 0; + while (a > -50) a -= 9; + print(`${a >>> 28}`); +}); diff --git a/ledger/numbers/unsigned-shift-right-by-a-value.ts b/ledger/numbers/unsigned-shift-right-by-a-value.ts new file mode 100644 index 0000000..11f91b7 --- /dev/null +++ b/ledger/numbers/unsigned-shift-right-by-a-value.ts @@ -0,0 +1,11 @@ +// ledger: step 4, refused: a shift by a value (>>>) +// A value shifted right by a number that changes while the game runs, its top bits zero: `a >>> n`. +program(() => { + let a = 0; + let n = 20; + while (a > -50) { + a -= 9; + n += 2; + } + print(`${a >>> n}`); +}); diff --git a/ledger/numbers/whole-divided-by-fixed.ts b/ledger/numbers/whole-divided-by-fixed.ts new file mode 100644 index 0000000..a3e14c2 --- /dev/null +++ b/ledger/numbers/whole-divided-by-fixed.ts @@ -0,0 +1,13 @@ +// ledger: step 4, refused: a quotient of fixed numbers +// A whole number divided by one made fixed, which keeps the fraction: `n / fixed(d)` (3 / fixed(2) is 1.5). +program(() => { + let n = 0; + let d = 1; + while (n < 5) { + n += 3; + d += 1; + } + const q = n / fixed(d); + if (q > 2) print("more than 2"); + else print("2 or less"); +}); diff --git a/ledger/numbers/xor-of-two-values.ts b/ledger/numbers/xor-of-two-values.ts new file mode 100644 index 0000000..a27bfb8 --- /dev/null +++ b/ledger/numbers/xor-of-two-values.ts @@ -0,0 +1,11 @@ +// ledger: step 4, refused: ^ of two values +// The bits one of two values that change while the game runs has and the other has not: `a ^ b`. +program(() => { + let a = 0; + let b = 1; + while (a < 50) { + a += 9; + b += 5; + } + print(`${a ^ b}`); +}); diff --git a/ledger/numbers/xor-with-a-constant.ts b/ledger/numbers/xor-with-a-constant.ts new file mode 100644 index 0000000..9c39d5e --- /dev/null +++ b/ledger/numbers/xor-with-a-constant.ts @@ -0,0 +1,7 @@ +// ledger: step 4, refused: ^ with a constant, as a value +// A value with bits flipped, as a value: `a ^ 5`. +program(() => { + let a = 0; + while (a < 50) a += 9; + print(`${a ^ 5}`); +}); diff --git a/ledger/programs/current-player.ts b/ledger/programs/current-player.ts new file mode 100644 index 0000000..e8d6d30 --- /dev/null +++ b/ledger/programs/current-player.ts @@ -0,0 +1,5 @@ +// ledger: step 7, refused: a map of our own has no per-player program yet +// The player a program runs as, named in it: `CurrentPlayer`, here a player's own minerals in a program for each. +program(() => { + setResources(CurrentPlayer, "add", 50, "ore"); +}, { owner: AllPlayers }); diff --git a/ledger/programs/game-function-two-programs.ts b/ledger/programs/game-function-two-programs.ts new file mode 100644 index 0000000..20c5b92 --- /dev/null +++ b/ledger/programs/game-function-two-programs.ts @@ -0,0 +1,9 @@ +// ledger: step 7, refused: a map of our own has one program; this script has 2 +// One `game(…)` function called from two programs. +const award = game((n: number): number => n + 5); +program(() => { + print(`${award(1)}`); +}); +program(() => { + print(`${award(2)}`); +}, { owner: P2 }); diff --git a/ledger/programs/game-function.ts b/ledger/programs/game-function.ts new file mode 100644 index 0000000..95eb8b7 --- /dev/null +++ b/ledger/programs/game-function.ts @@ -0,0 +1,8 @@ +// ledger: step 1, takes +// A function for programs to call, written with `game(…)`: `const award = game((n: number) => …)`. +const award = game((n: number): number => n + 5); +program(() => { + let k = 1; + while (k < 4) k += 1; + print(`${award(k)}`); +}); diff --git a/ledger/programs/listener-at-the-top.ts b/ledger/programs/listener-at-the-top.ts new file mode 100644 index 0000000..d06705b --- /dev/null +++ b/ledger/programs/listener-at-the-top.ts @@ -0,0 +1,8 @@ +// ledger: step 11, refused: a map of our own has one program; this script has 2 +// A listener at the top of a script, beside a program: a program of its own that listens from the start of the game. +on.created((u) => { + u.energy = 50; +}); +program(() => { + print("started"); +}); diff --git a/ledger/programs/owner-all-players-keys.ts b/ledger/programs/owner-all-players-keys.ts new file mode 100644 index 0000000..3c1676b --- /dev/null +++ b/ledger/programs/owner-all-players-keys.ts @@ -0,0 +1,8 @@ +// ledger: step 12, refused: a map of our own has no per-player program yet +// Each player's own keys in a program for every player: `keyPressed(CurrentPlayer, "Space")` with `{ owner: AllPlayers }`. +program(async () => { + while (true) { + if (keyPressed(CurrentPlayer, "Space")) print("jump"); + await nextFrame(); + } +}, { owner: AllPlayers }); diff --git a/ledger/programs/owner-all-players-tasks.ts b/ledger/programs/owner-all-players-tasks.ts new file mode 100644 index 0000000..edc913e --- /dev/null +++ b/ledger/programs/owner-all-players-tasks.ts @@ -0,0 +1,9 @@ +// ledger: step 11, refused: a map of our own has no per-player program yet +// Each player's own tasks in a program for every player: `start(…)` with `{ owner: AllPlayers }`. +async function hello(n: number) { + await sleep(frames(n)); + print(`hello ${n}`); +} +program(async () => { + start(hello(2)); +}, { owner: AllPlayers }); diff --git a/ledger/programs/owner-all-players.ts b/ledger/programs/owner-all-players.ts new file mode 100644 index 0000000..92eef02 --- /dev/null +++ b/ledger/programs/owner-all-players.ts @@ -0,0 +1,7 @@ +// ledger: step 7, refused: a map of our own has no per-player program yet +// A program that runs once for each player at the same time, each with their own variables: `{ owner: AllPlayers }`. +program(() => { + let n = 0; + while (n < 3) n += 1; + print(`${n}`); +}, { owner: AllPlayers }); diff --git a/ledger/programs/owner-another-player.ts b/ledger/programs/owner-another-player.ts new file mode 100644 index 0000000..d271279 --- /dev/null +++ b/ledger/programs/owner-another-player.ts @@ -0,0 +1,5 @@ +// ledger: step 0, takes +// A program that runs as another player than Player 1: `program(…, { owner: P2 })`. +program(() => { + print("for P2"); +}, { owner: P2 }); diff --git a/ledger/programs/owner-force.ts b/ledger/programs/owner-force.ts new file mode 100644 index 0000000..9f356ea --- /dev/null +++ b/ledger/programs/owner-force.ts @@ -0,0 +1,5 @@ +// ledger: step 7, refused: a map of our own has no per-player program yet +// A program that runs once for each player of a force: `{ owner: players.Force1 }`. +program(() => { + print("for my force"); +}, { owner: players.Force1 }); diff --git a/ledger/programs/owner-list.ts b/ledger/programs/owner-list.ts new file mode 100644 index 0000000..dabd416 --- /dev/null +++ b/ledger/programs/owner-list.ts @@ -0,0 +1,5 @@ +// ledger: step 7, refused: a map of our own has no per-player program yet +// A program that runs once for each player of a list: `{ owner: [P1, P2] }`. +program(() => { + print("for each of us"); +}, { owner: [P1, P2] }); diff --git a/ledger/programs/owner-one-of-a-list.ts b/ledger/programs/owner-one-of-a-list.ts new file mode 100644 index 0000000..da8ca19 --- /dev/null +++ b/ledger/programs/owner-one-of-a-list.ts @@ -0,0 +1,5 @@ +// ledger: step 0, takes +// A program whose list of owners has one player in it: `{ owner: [P3] }`. +program(() => { + print("for P3"); +}, { owner: [P3] }); diff --git a/ledger/programs/owner-players-four-to-seven.ts b/ledger/programs/owner-players-four-to-seven.ts new file mode 100644 index 0000000..4e5669d --- /dev/null +++ b/ledger/programs/owner-players-four-to-seven.ts @@ -0,0 +1,5 @@ +// ledger: step 7, refused: a map of our own has no per-player program yet +// A program for the players numbered four to seven, once for each of them: `{ owner: [P4, P5, P6, P7] }`. +program(() => { + print("for each of us"); +}, { owner: [P4, P5, P6, P7] }); diff --git a/ledger/programs/print-to-all-players.ts b/ledger/programs/print-to-all-players.ts new file mode 100644 index 0000000..0fc5fc9 --- /dev/null +++ b/ledger/programs/print-to-all-players.ts @@ -0,0 +1,5 @@ +// ledger: step 5, refused: a text shown to other players than the program's owner +// A text shown to every player: `print("…", { to: AllPlayers })`. +program(() => { + print("for everyone", { to: AllPlayers }); +}); diff --git a/ledger/programs/print-to-another-player.ts b/ledger/programs/print-to-another-player.ts new file mode 100644 index 0000000..8e2a457 --- /dev/null +++ b/ledger/programs/print-to-another-player.ts @@ -0,0 +1,5 @@ +// ledger: step 5, refused: a text shown to other players than the program's owner +// A text shown to another player than the one the program runs as: `print("…", { to: P2 })`. +program(() => { + print("for P2", { to: P2 }); +}); diff --git a/ledger/programs/print-to-current-player.ts b/ledger/programs/print-to-current-player.ts new file mode 100644 index 0000000..82ade99 --- /dev/null +++ b/ledger/programs/print-to-current-player.ts @@ -0,0 +1,5 @@ +// ledger: step 0, takes +// A text shown to the player the program runs as, named so: `print("…", { to: CurrentPlayer })`. +program(() => { + print("for me", { to: CurrentPlayer }); +}, { owner: P2 }); diff --git a/ledger/programs/print-to-the-owner-by-name.ts b/ledger/programs/print-to-the-owner-by-name.ts new file mode 100644 index 0000000..8f04edd --- /dev/null +++ b/ledger/programs/print-to-the-owner-by-name.ts @@ -0,0 +1,5 @@ +// ledger: step 0, takes +// A text shown to the program's own player, named: `print("…", { to: P2 })` in a program that runs as P2. +program(() => { + print("for me", { to: P2 }); +}, { owner: P2 }); diff --git a/ledger/programs/program-name.ts b/ledger/programs/program-name.ts new file mode 100644 index 0000000..227d533 --- /dev/null +++ b/ledger/programs/program-name.ts @@ -0,0 +1,5 @@ +// ledger: step 0, takes +// A program given a name, for the Explorer's list and the tests: `program(…, { name: "waves" })`. +program(() => { + print("named"); +}, { name: "waves" }); diff --git a/ledger/programs/shared-array.ts b/ledger/programs/shared-array.ts new file mode 100644 index 0000000..116b666 --- /dev/null +++ b/ledger/programs/shared-array.ts @@ -0,0 +1,7 @@ +// ledger: step 7, refused: a map of our own has no per-player program yet +// An array every player of a program for several players shares: `const seen = shared([0, 0, 0, 0])`. +program(() => { + const seen = shared([0, 0, 0, 0]); + seen[0] += 1; + print(`${seen[0]}`); +}, { owner: AllPlayers }); diff --git a/ledger/programs/shared-boolean.ts b/ledger/programs/shared-boolean.ts new file mode 100644 index 0000000..279cc1e --- /dev/null +++ b/ledger/programs/shared-boolean.ts @@ -0,0 +1,7 @@ +// ledger: step 7, refused: a map of our own has no per-player program yet +// A boolean every player of a program for several players shares: `let done = shared(false)`. +program(() => { + let done = shared(false); + if (!done) print("first here"); + done = true; +}, { owner: AllPlayers }); diff --git a/ledger/programs/shared-in-one-program.ts b/ledger/programs/shared-in-one-program.ts new file mode 100644 index 0000000..fa9f423 --- /dev/null +++ b/ledger/programs/shared-in-one-program.ts @@ -0,0 +1,7 @@ +// ledger: step 0, takes +// `shared()` in a program for one player, where there is only one copy to share: `let total = shared(0)`. +program(() => { + let total = shared(0); + while (total < 5) total += 2; + print(`${total}`); +}); diff --git a/ledger/programs/shared-number.ts b/ledger/programs/shared-number.ts new file mode 100644 index 0000000..8f3a3a8 --- /dev/null +++ b/ledger/programs/shared-number.ts @@ -0,0 +1,7 @@ +// ledger: step 7, refused: a map of our own has no per-player program yet +// A number every player of a program for several players shares, one cell for them all: `let total = shared(0)`. +program(() => { + let total = shared(0); + total += 1; + print(`${total}`); +}, { owner: AllPlayers }); diff --git a/ledger/programs/two-programs.ts b/ledger/programs/two-programs.ts new file mode 100644 index 0000000..47e4619 --- /dev/null +++ b/ledger/programs/two-programs.ts @@ -0,0 +1,8 @@ +// ledger: step 7, refused: a map of our own has one program; this script has 2 +// Two programs in one script, each running beside the other. +program(() => { + print("first"); +}); +program(() => { + print("second"); +}); diff --git a/ledger/sounds/music-arrange.ts b/ledger/sounds/music-arrange.ts new file mode 100644 index 0000000..ff9eff1 --- /dev/null +++ b/ledger/sounds/music-arrange.ts @@ -0,0 +1,10 @@ +// ledger: step 5, refused: the action Play WAV +// A song in sections: parts named once (`music.part`), each section saying what they play +// (`music.section`), put in order by `music.arrange`. +import { music } from "trigscript/music"; +const parts = { lead: music.part(music.synth({ release: 0.05 })) }; +const verse = music.section(1, { lead: "C5 E5 G5 E5" }); +const tune = music.arrange({ bpm: 120, steps: 1 }, parts, verse, verse.with({ lead: "G5 E5 C5 ." })); +program(async () => { + await music.play(tune); +}); diff --git a/ledger/sounds/music-bars.ts b/ledger/sounds/music-bars.ts new file mode 100644 index 0000000..c243253 --- /dev/null +++ b/ledger/sounds/music-bars.ts @@ -0,0 +1,7 @@ +// ledger: step 5, refused: the action Play WAV +// How long bars last at a tempo, as a song plays them: `music.bars(120, 1)`, the length of a riser. +import { music } from "trigscript/music"; +const rise = music.riser(music.bars(240, 1), { volume: 0.3 }); +program(() => { + playWav(rise, 0); +}); diff --git a/ledger/sounds/music-beat.ts b/ledger/sounds/music-beat.ts new file mode 100644 index 0000000..d69f9c0 --- /dev/null +++ b/ledger/sounds/music-beat.ts @@ -0,0 +1,8 @@ +// ledger: step 5, refused: the action Play WAV +// A drum pattern from the steps each drum hits, when the map is built: `music.beat({ k: [0, 4], h: […] }, 8)`. +import { music } from "trigscript/music"; +const drums = music.kit({ k: tone([[0, 150], [0.08, 45]], 0.2, { volume: 0.6 }), h: noise(0.03, { volume: 0.2 }) }); +const groove = music.song({ bpm: 120 }, music.track(drums, music.beat({ k: [0, 4], h: [0, 2, 4, 6] }, 8))); +program(async () => { + await music.play(groove); +}); diff --git a/ledger/sounds/music-chord.ts b/ledger/sounds/music-chord.ts new file mode 100644 index 0000000..26b4e1c --- /dev/null +++ b/ledger/sounds/music-chord.ts @@ -0,0 +1,8 @@ +// ledger: step 5, refused: the action Play WAV +// A chord by its name, its notes sounding together: `music.chord("Am")`. +import { music } from "trigscript/music"; +const keys = music.synth({ volume: 0.1, release: 0.05 }); +const tune = music.song({ bpm: 120 }, music.track(keys, `${music.chord("Am")} - - .`)); +program(async () => { + await music.play(tune); +}); diff --git a/ledger/sounds/music-chords.ts b/ledger/sounds/music-chords.ts new file mode 100644 index 0000000..710bd77 --- /dev/null +++ b/ledger/sounds/music-chords.ts @@ -0,0 +1,8 @@ +// ledger: step 5, refused: the action Play WAV +// A chord progression by name, each drawn by a template: `music.chords("Am F", "1+2+3 - . .")`. +import { music } from "trigscript/music"; +const keys = music.synth({ volume: 0.1, release: 0.05 }); +const tune = music.song({ bpm: 120 }, music.track(keys, music.chords("Am F", "1+2+3 - . ."))); +program(async () => { + await music.play(tune); +}); diff --git a/ledger/sounds/music-conduct.ts b/ledger/sounds/music-conduct.ts new file mode 100644 index 0000000..42c1033 --- /dev/null +++ b/ledger/sounds/music-conduct.ts @@ -0,0 +1,13 @@ +// ledger: step 6, refused: a read of memory.game.frame +// Music that follows the game: a song cut into bars and layers (`music.stems`), played by +// `music.conduct(stems, band)` as a `Band` says: which section, which layers, a stinger on the next beat. +import { music, Band } from "trigscript/music"; +const parts = { lead: music.part(music.synth({ release: 0.05 })) }; +const verse = music.section(1, { lead: "C5 E5 G5 E5" }, { name: "verse" }); +const tune = music.arrange({ bpm: 120, steps: 1 }, parts, verse); +const cut = music.stems(tune, { layers: { lead: ["lead"] }, rate: 22050, tail: 0.2 }); +program(async () => { + const band = new Band(); + band.go(cut.section("verse")); + await music.conduct(cut, band); +}); diff --git a/ledger/sounds/music-drums.ts b/ledger/sounds/music-drums.ts new file mode 100644 index 0000000..c355f73 --- /dev/null +++ b/ledger/sounds/music-drums.ts @@ -0,0 +1,8 @@ +// ledger: step 5, refused: the action Play WAV +// A drum kit made from nothing, a letter a drum: `music.drums()`. +import { music } from "trigscript/music"; +const kit = music.drums(); +const groove = music.song({ bpm: 120 }, music.track(kit, "kh h sh h")); +program(async () => { + await music.play(groove); +}); diff --git a/ledger/sounds/music-fm-synth.ts b/ledger/sounds/music-fm-synth.ts new file mode 100644 index 0000000..616c46b --- /dev/null +++ b/ledger/sounds/music-fm-synth.ts @@ -0,0 +1,8 @@ +// ledger: step 5, refused: the action Play WAV +// An FM instrument: `music.fmSynth({ ratio, index })`. +import { music } from "trigscript/music"; +const keys = music.fmSynth({ ratio: 1, index: 2, release: 0.1 }); +const tune = music.song({ bpm: 120 }, music.track(keys, "E4 - G4 -")); +program(async () => { + await music.play(tune); +}); diff --git a/ledger/sounds/music-impact.ts b/ledger/sounds/music-impact.ts new file mode 100644 index 0000000..39a9d3f --- /dev/null +++ b/ledger/sounds/music-impact.ts @@ -0,0 +1,7 @@ +// ledger: step 5, refused: the action Play WAV +// A boom to land on, made when the map is built: `music.impact()`. +import { music } from "trigscript/music"; +const boom = music.impact({ seconds: 0.5 }); +program(() => { + playWav(boom, 0); +}); diff --git a/ledger/sounds/music-kit.ts b/ledger/sounds/music-kit.ts new file mode 100644 index 0000000..468dfee --- /dev/null +++ b/ledger/sounds/music-kit.ts @@ -0,0 +1,8 @@ +// ledger: step 5, refused: the action Play WAV +// A drum kit of the script's own clips, a letter each: `music.kit({ k: kick, h: hat })`. +import { music } from "trigscript/music"; +const drums = music.kit({ k: tone([[0, 150], [0.08, 45]], 0.2, { volume: 0.6 }), h: noise(0.03, { volume: 0.2 }) }); +const groove = music.song({ bpm: 120 }, music.track(drums, "kh h kh h")); +program(async () => { + await music.play(groove); +}); diff --git a/ledger/sounds/music-loop.ts b/ledger/sounds/music-loop.ts new file mode 100644 index 0000000..2317992 --- /dev/null +++ b/ledger/sounds/music-loop.ts @@ -0,0 +1,8 @@ +// ledger: step 11, refused: a task started +// A song played again and again beside the program: `start(music.loop(song))`. +import { music } from "trigscript/music"; +const lead = music.synth({ release: 0.05 }); +const tune = music.song({ bpm: 120 }, music.track(lead, "C5 . E5 .")); +program(async () => { + start(music.loop(tune)); +}); diff --git a/ledger/sounds/music-midi.ts b/ledger/sounds/music-midi.ts new file mode 100644 index 0000000..fc1728a --- /dev/null +++ b/ledger/sounds/music-midi.ts @@ -0,0 +1,11 @@ +// ledger: step 5, refused: the action Play WAV +// A song of notes (as a MIDI file imports them), each track played by an instrument: `music.midi(notes, { Piano: keys })`. +import { music } from "trigscript/music"; +const notes: Notes = { + bpm: 120, beats: 4, tempoChanges: 0, + tracks: [{ name: "Piano", channel: 1, program: 1, drums: false, notes: [{ at: 0, beats: 1, pitch: 60, note: "C4", velocity: 100 }, { at: 1, beats: 1, pitch: 64, note: "E4", velocity: 100 }] }], +}; +const tune = music.midi(notes, { Piano: music.synth({ release: 0.05 }) }); +program(async () => { + await music.play(tune); +}); diff --git a/ledger/sounds/music-over.ts b/ledger/sounds/music-over.ts new file mode 100644 index 0000000..ef2b840 --- /dev/null +++ b/ledger/sounds/music-over.ts @@ -0,0 +1,8 @@ +// ledger: step 5, refused: the action Play WAV +// A chord's notes drawn by a template, when the map is built: `music.over("C4+E4+G4", "1 2 3 2")`. +import { music } from "trigscript/music"; +const lead = music.synth({ release: 0.05 }); +const tune = music.song({ bpm: 120 }, music.track(lead, music.over("C4+E4+G4", "1 2 3 2"))); +program(async () => { + await music.play(tune); +}); diff --git a/ledger/sounds/music-play.ts b/ledger/sounds/music-play.ts new file mode 100644 index 0000000..49bef7c --- /dev/null +++ b/ledger/sounds/music-play.ts @@ -0,0 +1,9 @@ +// ledger: step 5, refused: the action Play WAV +// A song played note by note from the music library: `music.synth`, `music.track`, `music.song`, and +// `await music.play(song)` in an async program. +import { music } from "trigscript/music"; +const lead = music.synth({ release: 0.05 }); +const tune = music.song({ bpm: 120 }, music.track(lead, "C5 - E5 . G5 - - .")); +program(async () => { + await music.play(tune); +}); diff --git a/ledger/sounds/music-presets.ts b/ledger/sounds/music-presets.ts new file mode 100644 index 0000000..75a22ca --- /dev/null +++ b/ledger/sounds/music-presets.ts @@ -0,0 +1,8 @@ +// ledger: step 5, refused: the action Play WAV +// An instrument to start from: `music.presets.chip()`. +import { music } from "trigscript/music"; +const chip = music.presets.chip(); +const tune = music.song({ bpm: 120 }, music.track(chip, "C5 E5 G5 E5")); +program(async () => { + await music.play(tune); +}); diff --git a/ledger/sounds/music-pump.ts b/ledger/sounds/music-pump.ts new file mode 100644 index 0000000..afc71ff --- /dev/null +++ b/ledger/sounds/music-pump.ts @@ -0,0 +1,8 @@ +// ledger: step 5, refused: the action Play WAV +// A part that breathes with the beat, each note ducked on it: `{ fx: music.pump(beat) }`. +import { music } from "trigscript/music"; +const pad = music.synth({ volume: 0.1, release: 0.05 }); +const tune = music.song({ bpm: 120 }, music.track(pad, "C4+E4 - - -", { fx: music.pump(0.125) })); +program(async () => { + await music.play(tune); +}); diff --git a/ledger/sounds/music-render.ts b/ledger/sounds/music-render.ts new file mode 100644 index 0000000..9801fa9 --- /dev/null +++ b/ledger/sounds/music-render.ts @@ -0,0 +1,9 @@ +// ledger: step 5, refused: the action Play WAV +// A song mixed into one clip when the map is built, every note in time: `music.render(song)`. +import { music } from "trigscript/music"; +const lead = music.synth({ volume: 0.2, release: 0.05 }); +const tune = music.song({ bpm: 120 }, music.track(lead, "C5 E5 G5 .")); +const mixed = music.render(tune, { rate: 22050 }); +program(() => { + playWav(mixed, 0); +}); diff --git a/ledger/sounds/music-repeat.ts b/ledger/sounds/music-repeat.ts new file mode 100644 index 0000000..5f5129a --- /dev/null +++ b/ledger/sounds/music-repeat.ts @@ -0,0 +1,8 @@ +// ledger: step 5, refused: the action Play WAV +// A pattern played several times over, when the map is built: `music.repeat(pattern, 2)`. +import { music } from "trigscript/music"; +const lead = music.synth({ release: 0.05 }); +const tune = music.song({ bpm: 120 }, music.track(lead, music.repeat("C5 G4", 2))); +program(async () => { + await music.play(tune); +}); diff --git a/ledger/sounds/music-riser.ts b/ledger/sounds/music-riser.ts new file mode 100644 index 0000000..ba458c7 --- /dev/null +++ b/ledger/sounds/music-riser.ts @@ -0,0 +1,7 @@ +// ledger: step 5, refused: the action Play WAV +// Noise rising through an opening filter, made when the map is built: `music.riser(seconds)`. +import { music } from "trigscript/music"; +const rise = music.riser(1, { volume: 0.3 }); +program(() => { + playWav(rise, 0); +}); diff --git a/ledger/sounds/music-sampler.ts b/ledger/sounds/music-sampler.ts new file mode 100644 index 0000000..34a2a33 --- /dev/null +++ b/ledger/sounds/music-sampler.ts @@ -0,0 +1,8 @@ +// ledger: step 5, refused: the action Play WAV +// An instrument from a clip, played at each note's pitch: `music.sampler(clip, root)`. +import { music } from "trigscript/music"; +const harp = music.sampler(pluck("C4", 0.5), "C4"); +const tune = music.song({ bpm: 120 }, music.track(harp, "C4 E4 G4 C5")); +program(async () => { + await music.play(tune); +}); diff --git a/ledger/sounds/music-stretch.ts b/ledger/sounds/music-stretch.ts new file mode 100644 index 0000000..09f5963 --- /dev/null +++ b/ledger/sounds/music-stretch.ts @@ -0,0 +1,8 @@ +// ledger: step 5, refused: the action Play WAV +// A pattern made slower, each step held longer, when the map is built: `music.stretch(pattern, 2)`. +import { music } from "trigscript/music"; +const lead = music.synth({ release: 0.05 }); +const tune = music.song({ bpm: 120 }, music.track(lead, music.stretch("C5 E5", 2))); +program(async () => { + await music.play(tune); +}); diff --git a/ledger/sounds/music-strings.ts b/ledger/sounds/music-strings.ts new file mode 100644 index 0000000..ccd55eb --- /dev/null +++ b/ledger/sounds/music-strings.ts @@ -0,0 +1,8 @@ +// ledger: step 5, refused: the action Play WAV +// A plucked-string instrument: `music.strings()`. +import { music } from "trigscript/music"; +const guitar = music.strings({ brightness: 0.5, decay: 0.6 }); +const tune = music.song({ bpm: 120 }, music.track(guitar, "A3 C4 E4 A4")); +program(async () => { + await music.play(tune); +}); diff --git a/ledger/sounds/music-transpose.ts b/ledger/sounds/music-transpose.ts new file mode 100644 index 0000000..3595383 --- /dev/null +++ b/ledger/sounds/music-transpose.ts @@ -0,0 +1,8 @@ +// ledger: step 5, refused: the action Play WAV +// A pattern moved up or down by semitones when the map is built: `music.transpose(pattern, 12)`. +import { music } from "trigscript/music"; +const lead = music.synth({ release: 0.05 }); +const tune = music.song({ bpm: 120 }, music.track(lead, music.transpose("C4 E4 G4 .", 12))); +program(async () => { + await music.play(tune); +}); diff --git a/ledger/sounds/play-wav-chosen-by-a-value.ts b/ledger/sounds/play-wav-chosen-by-a-value.ts new file mode 100644 index 0000000..aecc362 --- /dev/null +++ b/ledger/sounds/play-wav-chosen-by-a-value.ts @@ -0,0 +1,9 @@ +// ledger: step 5, refused: the action Play WAV +// The action Play WAV of one of two clips, chosen by a value of the program. +const low = tone("C4", 0.1, { volume: 0.3 }); +const high = tone("C5", 0.1, { volume: 0.3 }); +program(() => { + let n = 3; + if (n > 2) playWav(high, 0); + else playWav(low, 0); +}); diff --git a/ledger/sounds/play-wav-clip-effects.ts b/ledger/sounds/play-wav-clip-effects.ts new file mode 100644 index 0000000..335af64 --- /dev/null +++ b/ledger/sounds/play-wav-clip-effects.ts @@ -0,0 +1,6 @@ +// ledger: step 5, refused: the action Play WAV +// The action Play WAV of a clip put through effects when the map is built: a filter, an echo, a reverb, a limiter. +const zap = tone("A3", 0.4, { wave: "saw", volume: 0.4 }).filter("lowpass", [[0, 300], [0.4, 3000]]).delay(0.1).reverb({ decay: 0.3 }).limit(); +program(() => { + playWav(zap, 0); +}); diff --git a/ledger/sounds/play-wav-fm.ts b/ledger/sounds/play-wav-fm.ts new file mode 100644 index 0000000..fb613e2 --- /dev/null +++ b/ledger/sounds/play-wav-fm.ts @@ -0,0 +1,6 @@ +// ledger: step 5, refused: the action Play WAV +// The action Play WAV of a clip made by `fm()` when the map is built: a bell. +const bell = fm("E5", 1, { ratio: 3.5, index: [[0, 6], [1, 0.3]], volume: 0.3 }).fade(0, 0.5); +program(() => { + playWav(bell, 0); +}); diff --git a/ledger/sounds/play-wav-game-file.ts b/ledger/sounds/play-wav-game-file.ts new file mode 100644 index 0000000..8e77f74 --- /dev/null +++ b/ledger/sounds/play-wav-game-file.ts @@ -0,0 +1,5 @@ +// ledger: step 5, refused: the action Play WAV +// The action Play WAV of one of the game's own sound files, by its path. +program(() => { + playWav("sound\\glue\\mousedown2.wav", 0); +}); diff --git a/ledger/sounds/play-wav-map-sound.ts b/ledger/sounds/play-wav-map-sound.ts new file mode 100644 index 0000000..837fb46 --- /dev/null +++ b/ledger/sounds/play-wav-map-sound.ts @@ -0,0 +1,7 @@ +// ledger: step 5, refused: the action Play WAV +// The action Play WAV of one of the map's own sounds (the Sound Editor's list) by its name, `sounds.Name`. +// The ledger's map has none, so one of the game's own files, chosen when the map is built. +const own = Object.values(sounds) as Sound[]; +program(() => { + playWav(own.length > 0 ? own[0] : "sound\\glue\\mousedown2.wav", 0); +}); diff --git a/ledger/sounds/play-wav-mixdown.ts b/ledger/sounds/play-wav-mixdown.ts new file mode 100644 index 0000000..f6acdc6 --- /dev/null +++ b/ledger/sounds/play-wav-mixdown.ts @@ -0,0 +1,8 @@ +// ledger: step 5, refused: the action Play WAV +// The action Play WAV of clips laid over one another by `mixdown()` when the map is built: one clip. +const kick = tone([[0, 150], [0.08, 45]], 0.2, { volume: 0.6 }); +const hat = noise(0.03, { volume: 0.2 }); +const groove = mixdown([{ clip: kick, at: 0 }, { clip: hat, at: 0.25 }, { clip: kick, at: 0.5 }]); +program(() => { + playWav(groove, 0); +}); diff --git a/ledger/sounds/play-wav-noise.ts b/ledger/sounds/play-wav-noise.ts new file mode 100644 index 0000000..ec0203c --- /dev/null +++ b/ledger/sounds/play-wav-noise.ts @@ -0,0 +1,6 @@ +// ledger: step 5, refused: the action Play WAV +// The action Play WAV of a clip made by `noise()` when the map is built: a burst. +const burst = noise(0.15, { volume: 0.4 }).fade(0, 0.1); +program(() => { + playWav(burst, 0); +}); diff --git a/ledger/sounds/play-wav-pluck.ts b/ledger/sounds/play-wav-pluck.ts new file mode 100644 index 0000000..e2f113c --- /dev/null +++ b/ledger/sounds/play-wav-pluck.ts @@ -0,0 +1,6 @@ +// ledger: step 5, refused: the action Play WAV +// The action Play WAV of a clip made by `pluck()` when the map is built: a plucked string. +const harp = pluck("G3", 0.8, { brightness: 0.6 }); +program(() => { + playWav(harp, 0); +}); diff --git a/ledger/sounds/play-wav-together.ts b/ledger/sounds/play-wav-together.ts new file mode 100644 index 0000000..cb8d9ef --- /dev/null +++ b/ledger/sounds/play-wav-together.ts @@ -0,0 +1,9 @@ +// ledger: step 5, refused: the action Play WAV +// The action Play WAV of clips the game plays together, turned down by `together()` when the map is built +// so that they never add up past the peak. +const [low, high] = together([{ clip: tone("C4", 0.5, { volume: 0.8 }), at: 0 }, { clip: tone("G4", 0.5, { volume: 0.8 }), at: 0.25 }]); +program(() => { + playWav(low, 0); + sleep(frames(6)); + playWav(high, 0); +}); diff --git a/ledger/sounds/play-wav-tone.ts b/ledger/sounds/play-wav-tone.ts new file mode 100644 index 0000000..1987294 --- /dev/null +++ b/ledger/sounds/play-wav-tone.ts @@ -0,0 +1,6 @@ +// ledger: step 5, refused: the action Play WAV +// The action Play WAV of a clip made by `tone()` when the map is built: a beep. +const beep = tone("A5", 0.1, { wave: "square", volume: 0.3 }).fade(0, 0.05); +program(() => { + playWav(beep, 0); +}); diff --git a/ledger/sounds/play-wav-with-silence.ts b/ledger/sounds/play-wav-with-silence.ts new file mode 100644 index 0000000..51c8615 --- /dev/null +++ b/ledger/sounds/play-wav-with-silence.ts @@ -0,0 +1,7 @@ +// ledger: step 5, refused: the action Play WAV +// The action Play WAV of two beeps a gap apart, the gap made by `silence()` when the map is built. +const beep = tone("C5", 0.08, { volume: 0.3 }); +const twice = beep.then(silence(0.1), beep); +program(() => { + playWav(twice, 0); +}); diff --git a/ledger/sounds/tone-made-when-built.ts b/ledger/sounds/tone-made-when-built.ts new file mode 100644 index 0000000..7d29e33 --- /dev/null +++ b/ledger/sounds/tone-made-when-built.ts @@ -0,0 +1,8 @@ +// ledger: step 0, takes +// `tone()` makes a clip when the map is built: its length and loudness are numbers the program has as +// constants. Nothing of it is played. +const beep = tone("A4", 0.25, { volume: 0.5 }); +program(() => { + let ms = Math.round(beep.seconds * 1000); + print(`${ms} ms`); +}); diff --git a/ledger/sounds/transmission-with-a-clip.ts b/ledger/sounds/transmission-with-a-clip.ts new file mode 100644 index 0000000..6b323e9 --- /dev/null +++ b/ledger/sounds/transmission-with-a-clip.ts @@ -0,0 +1,6 @@ +// ledger: step 5, refused: the action Transmission +// The action Transmission with a clip made when the map is built as its sound. +const chirp = tone([[0, 800], [0.2, 1600]], 0.2, { volume: 0.3 }); +program(() => { + transmission("Incoming.", units.TerranMarine, locations.Anywhere, "add", 0, chirp, 200); +}); diff --git a/ledger/tasks/async-arrow-awaited.ts b/ledger/tasks/async-arrow-awaited.ts new file mode 100644 index 0000000..e9f650d --- /dev/null +++ b/ledger/tasks/async-arrow-awaited.ts @@ -0,0 +1,10 @@ +// ledger: step 1, takes +// An async arrow function called and awaited where it is written: `await (async () => { … })()`. +program(async () => { + let n = 0; + await (async () => { + await sleep(frames(2)); + n = 4; + })(); + print(`${n}`); +}); diff --git a/ledger/tasks/async-function-awaited.ts b/ledger/tasks/async-function-awaited.ts new file mode 100644 index 0000000..4299ef8 --- /dev/null +++ b/ledger/tasks/async-function-awaited.ts @@ -0,0 +1,9 @@ +// ledger: step 1, takes +// An async function, awaited: it waits where it sleeps, and the program goes on after it. +async function pause(n: number) { + await sleep(frames(n)); +} +program(async () => { + await pause(3); + print("after"); +}); diff --git a/ledger/tasks/async-function-result.ts b/ledger/tasks/async-function-result.ts new file mode 100644 index 0000000..07af9c6 --- /dev/null +++ b/ledger/tasks/async-function-result.ts @@ -0,0 +1,10 @@ +// ledger: step 1, takes +// An async function that gives back a number once it has waited: `const v = await slow(2)`. +async function slow(n: number): Promise { + await sleep(frames(n)); + return n + 1; +} +program(async () => { + const v = await slow(2); + print(`${v}`); +}); diff --git a/ledger/tasks/async-program.ts b/ledger/tasks/async-program.ts new file mode 100644 index 0000000..ca4bfad --- /dev/null +++ b/ledger/tasks/async-program.ts @@ -0,0 +1,7 @@ +// ledger: step 0, takes +// A program written as an async function, that waits with `await sleep(…)`. +program(async () => { + print("now"); + await sleep(frames(3)); + print("later"); +}); diff --git a/ledger/tasks/cutscene-camera-follow.ts b/ledger/tasks/cutscene-camera-follow.ts new file mode 100644 index 0000000..81cf065 --- /dev/null +++ b/ledger/tasks/cutscene-camera-follow.ts @@ -0,0 +1,10 @@ +// ledger: step 10, refused: a unit kept in a variable +// trigscript/cutscene: a camera that keeps a unit in the middle of the screen for so many frames: `await cam.follow(u, time)`. +import { Camera, SECOND } from "trigscript/cutscene"; +program(async () => { + const u = first({ owner: P1 }); + if (u) { + const cam = new Camera(u.x, u.y); + await cam.follow(u, 2 * SECOND); + } +}); diff --git a/ledger/tasks/cutscene-camera-pan-ease.ts b/ledger/tasks/cutscene-camera-pan-ease.ts new file mode 100644 index 0000000..6ae42ca --- /dev/null +++ b/ledger/tasks/cutscene-camera-pan-ease.ts @@ -0,0 +1,7 @@ +// ledger: step 5, refused: a location moved (centerLocation) +// trigscript/cutscene: a camera's glide that starts slow, ends slow or goes past and back: `cam.pan(x, y, time, "back")`. +import { Camera } from "trigscript/cutscene"; +program(async () => { + const cam = new Camera(640, 480); + await cam.pan(1600, 400, 48, "back"); +}); diff --git a/ledger/tasks/cutscene-camera-pan.ts b/ledger/tasks/cutscene-camera-pan.ts new file mode 100644 index 0000000..4b6a21f --- /dev/null +++ b/ledger/tasks/cutscene-camera-pan.ts @@ -0,0 +1,7 @@ +// ledger: step 5, refused: a location moved (centerLocation) +// trigscript/cutscene: a camera that glides to a point over so many frames: `await cam.pan(x, y, time)`. +import { Camera, SECOND } from "trigscript/cutscene"; +program(async () => { + const cam = new Camera(640, 480); + await cam.pan(1600, 400, 2 * SECOND); +}); diff --git a/ledger/tasks/cutscene-camera-shake.ts b/ledger/tasks/cutscene-camera-shake.ts new file mode 100644 index 0000000..26e3f45 --- /dev/null +++ b/ledger/tasks/cutscene-camera-shake.ts @@ -0,0 +1,7 @@ +// ledger: step 5, refused: a location moved (centerLocation) +// trigscript/cutscene: a camera that shakes for so many frames and settles where it was: `await cam.shake(12)`. +import { Camera } from "trigscript/cutscene"; +program(async () => { + const cam = new Camera(640, 480); + await cam.shake(12); +}); diff --git a/ledger/tasks/cutscene-camera.ts b/ledger/tasks/cutscene-camera.ts new file mode 100644 index 0000000..8276550 --- /dev/null +++ b/ledger/tasks/cutscene-camera.ts @@ -0,0 +1,8 @@ +// ledger: step 5, refused: a location moved (centerLocation) +// trigscript/cutscene: a `Camera`, which looks at a point of the map as soon as it is made, and at another by `look(x, y)`. +import { Camera } from "trigscript/cutscene"; +program(async () => { + const cam = new Camera(640, 480); + await sleep(frames(24)); + cam.look(1024, 512); +}); diff --git a/ledger/tasks/cutscene-charge.ts b/ledger/tasks/cutscene-charge.ts new file mode 100644 index 0000000..fd11b16 --- /dev/null +++ b/ledger/tasks/cutscene-charge.ts @@ -0,0 +1,9 @@ +// ledger: step 10, refused: a unit kept in a variable +// trigscript/cutscene: a unit sent to a point by an Attack order, fighting on the way: `await charge(u, x, y)`. +import { charge } from "trigscript/cutscene"; +program(async () => { + const u = first({ owner: P1 }); + if (u) { + await charge(u, 1200, 640); + } +}); diff --git a/ledger/tasks/cutscene-line.ts b/ledger/tasks/cutscene-line.ts new file mode 100644 index 0000000..8b8e62f --- /dev/null +++ b/ledger/tasks/cutscene-line.ts @@ -0,0 +1,6 @@ +// ledger: step 5, refused: the action Talking Portrait +// trigscript/cutscene: a line of a scene, the speaker's portrait talking and the words in the middle of the screen: `await line(speaker, text)`. +import { line, SECOND } from "trigscript/cutscene"; +program(async () => { + await line(units.JimRaynorMarine, "Something's moving out there.", 3 * SECOND); +}); diff --git a/ledger/tasks/cutscene-play.ts b/ledger/tasks/cutscene-play.ts new file mode 100644 index 0000000..571353d --- /dev/null +++ b/ledger/tasks/cutscene-play.ts @@ -0,0 +1,11 @@ +// ledger: step 12, refused: a task started +// trigscript/cutscene: a scene started as a task, which the player may skip with Escape: `await play(P1, start(scene()))`. +import { play } from "trigscript/cutscene"; +async function scene() { + await sleep(frames(48)); + print("the end"); +} +program(async () => { + const skipped = await play(P1, start(scene())); + if (skipped) print("skipped"); +}); diff --git a/ledger/tasks/cutscene-second.ts b/ledger/tasks/cutscene-second.ts new file mode 100644 index 0000000..897b731 --- /dev/null +++ b/ledger/tasks/cutscene-second.ts @@ -0,0 +1,7 @@ +// ledger: step 0, takes +// trigscript/cutscene: `SECOND`, the frames of a second of game time (24), for a scene's lengths. +import { SECOND } from "trigscript/cutscene"; +program(async () => { + await sleep(frames(3 * SECOND)); + print("three seconds"); +}); diff --git a/ledger/tasks/cutscene-walk-all.ts b/ledger/tasks/cutscene-walk-all.ts new file mode 100644 index 0000000..5a6798e --- /dev/null +++ b/ledger/tasks/cutscene-walk-all.ts @@ -0,0 +1,8 @@ +// ledger: step 10, refused: a unit kept in a variable +// trigscript/cutscene: a group of units sent to a point together, waited for until each is there: `await walkAll(group, x, y)`. +import { walkAll } from "trigscript/cutscene"; +program(async () => { + const group = [first({ owner: P1, type: units.TerranMarine })!, first({ owner: P1, type: units.TerranFirebat })!]; + const there = await walkAll(group, 1200, 640); + print(`${there}`); +}); diff --git a/ledger/tasks/cutscene-walk.ts b/ledger/tasks/cutscene-walk.ts new file mode 100644 index 0000000..b84febd --- /dev/null +++ b/ledger/tasks/cutscene-walk.ts @@ -0,0 +1,10 @@ +// ledger: step 10, refused: a unit kept in a variable +// trigscript/cutscene: a unit sent to a point by a Move order, waited for until it is there: `await walk(u, x, y)`. +import { walk } from "trigscript/cutscene"; +program(async () => { + const u = first({ owner: P1 }); + if (u) { + const there = await walk(u, 1200, 640); + if (there) print("arrived"); + } +}); diff --git a/ledger/tasks/generator-as-a-task.ts b/ledger/tasks/generator-as-a-task.ts new file mode 100644 index 0000000..0d6d250 --- /dev/null +++ b/ledger/tasks/generator-as-a-task.ts @@ -0,0 +1,16 @@ +// ledger: step 11, refused: a task started +// A generator walked inside a task started beside the program. +function* steps() { + yield 1; + yield 2; + yield 3; +} +async function walkSteps() { + for (const s of steps()) { + print(`step ${s}`); + await sleep(frames(1)); + } +} +program(async () => { + start(walkSteps()); +}); diff --git a/ledger/tasks/generator-for-await.ts b/ledger/tasks/generator-for-await.ts new file mode 100644 index 0000000..0198844 --- /dev/null +++ b/ledger/tasks/generator-for-await.ts @@ -0,0 +1,13 @@ +// ledger: step 1, takes +// An async generator walked by `for await`, waiting between its values. +async function* slowly(n: number) { + let i = 0; + while (i < n) { + await sleep(frames(2)); + yield i; + i += 1; + } +} +program(async () => { + for await (const k of slowly(3)) print(`${k}`); +}); diff --git a/ledger/tasks/generator-for-of.ts b/ledger/tasks/generator-for-of.ts new file mode 100644 index 0000000..a43a19f --- /dev/null +++ b/ledger/tasks/generator-for-of.ts @@ -0,0 +1,14 @@ +// ledger: step 1, takes +// A generator walked by `for…of`, the loop's body put where each `yield` is. +function* upTo(n: number) { + let i = 0; + while (i < n) { + yield i; + i += 1; + } +} +program(() => { + let s = 0; + for (const x of upTo(4)) s += x; + print(`${s}`); +}); diff --git a/ledger/tasks/next-frame.ts b/ledger/tasks/next-frame.ts new file mode 100644 index 0000000..55b54b6 --- /dev/null +++ b/ledger/tasks/next-frame.ts @@ -0,0 +1,10 @@ +// ledger: step 0, takes +// A wait for the next frame: `await nextFrame()`, the same as one frame's sleep. +program(async () => { + let n = 0; + while (n < 3) { + n += 1; + await nextFrame(); + } + print(`${n}`); +}); diff --git a/ledger/tasks/promise-all-values.ts b/ledger/tasks/promise-all-values.ts new file mode 100644 index 0000000..0d3993c --- /dev/null +++ b/ledger/tasks/promise-all-values.ts @@ -0,0 +1,10 @@ +// ledger: step 11, refused: Promise.all +// Each side's value from a `Promise.all`: `const [a, b] = await Promise.all([…])`. +async function slow(n: number): Promise { + await sleep(frames(n)); + return n * 2; +} +program(async () => { + const [a, b] = await Promise.all([slow(2), slow(3)]); + print(`${a} ${b}`); +}); diff --git a/ledger/tasks/promise-all.ts b/ledger/tasks/promise-all.ts new file mode 100644 index 0000000..1975fb4 --- /dev/null +++ b/ledger/tasks/promise-all.ts @@ -0,0 +1,12 @@ +// ledger: step 11, refused: Promise.all +// Two waits side by side, the program going on when both have ended: `await Promise.all([…])`. +program(async () => { + await Promise.all([ + (async () => { + await sleep(frames(3)); + print("a"); + })(), + sleep(frames(5)), + ]); + print("both"); +}); diff --git a/ledger/tasks/promise-any.ts b/ledger/tasks/promise-any.ts new file mode 100644 index 0000000..1c629ce --- /dev/null +++ b/ledger/tasks/promise-any.ts @@ -0,0 +1,7 @@ +// ledger: step 11, refused: Promise.race +// `Promise.any`, the same as a race (nothing in a program fails): `await Promise.any([…])`. +program(async () => { + let n = 0; + const ok = await Promise.any([until(() => n >= 9).then(() => false), sleep(frames(2)).then(() => true)]); + if (ok) print("held"); +}); diff --git a/ledger/tasks/promise-race.ts b/ledger/tasks/promise-race.ts new file mode 100644 index 0000000..a698755 --- /dev/null +++ b/ledger/tasks/promise-race.ts @@ -0,0 +1,10 @@ +// ledger: step 11, refused: Promise.race +// Two waits side by side, the program going on when the first ends: `await Promise.race([…])`. +async function wave(n: number): Promise { + await sleep(frames(n)); + return n; +} +program(async () => { + const v = await Promise.race([wave(5), sleep(frames(3)).then(() => 0)]); + print(`${v}`); +}); diff --git a/ledger/tasks/sleep-in-a-loop.ts b/ledger/tasks/sleep-in-a-loop.ts new file mode 100644 index 0000000..0e98899 --- /dev/null +++ b/ledger/tasks/sleep-in-a-loop.ts @@ -0,0 +1,10 @@ +// ledger: step 0, takes +// A game loop: a wait of one frame at the end of each turn, `while (…) { …; await sleep(frames(1)); }`. +program(async () => { + let n = 0; + while (n < 10) { + n += 1; + await sleep(frames(1)); + } + print(`${n}`); +}); diff --git a/ledger/tasks/sleep-not-awaited.ts b/ledger/tasks/sleep-not-awaited.ts new file mode 100644 index 0000000..a11fb8e --- /dev/null +++ b/ledger/tasks/sleep-not-awaited.ts @@ -0,0 +1,7 @@ +// ledger: step 0, takes +// A wait of so many frames on its own, in a program that is not async: `sleep(frames(3))`. +program(() => { + print("now"); + sleep(frames(3)); + print("later"); +}); diff --git a/ledger/tasks/sleep-of-a-value.ts b/ledger/tasks/sleep-of-a-value.ts new file mode 100644 index 0000000..12ef924 --- /dev/null +++ b/ledger/tasks/sleep-of-a-value.ts @@ -0,0 +1,8 @@ +// ledger: step 0, takes +// A wait whose length the program worked out while the game runs: `await sleep(frames(n))`. +program(async () => { + let n = 1; + while (n < 5) n += 2; + await sleep(frames(n)); + print("later"); +}); diff --git a/ledger/tasks/sleep-seconds.ts b/ledger/tasks/sleep-seconds.ts new file mode 100644 index 0000000..56626c1 --- /dev/null +++ b/ledger/tasks/sleep-seconds.ts @@ -0,0 +1,6 @@ +// ledger: step 0, takes +// A wait of so many seconds of game time, 24 frames each: `await sleep(seconds(2))`. +program(async () => { + await sleep(seconds(2)); + print("two seconds"); +}); diff --git a/ledger/tasks/start-arrow.ts b/ledger/tasks/start-arrow.ts new file mode 100644 index 0000000..186f56f --- /dev/null +++ b/ledger/tasks/start-arrow.ts @@ -0,0 +1,9 @@ +// ledger: step 11, refused: a task started +// A task written where it is started: `start(async () => { … })`. +program(async () => { + start(async () => { + await sleep(frames(5)); + print("later"); + }); + print("now"); +}); diff --git a/ledger/tasks/start-call.ts b/ledger/tasks/start-call.ts new file mode 100644 index 0000000..f8aeaa5 --- /dev/null +++ b/ledger/tasks/start-call.ts @@ -0,0 +1,14 @@ +// ledger: step 11, refused: a task started +// A task started beside the program, the program going on at once: `start(tick(3))`. +async function tick(n: number) { + let i = 0; + while (i < n) { + print(`tick ${i}`); + await sleep(frames(2)); + i += 1; + } +} +program(async () => { + start(tick(3)); + print("started"); +}); diff --git a/ledger/tasks/start-max.ts b/ledger/tasks/start-max.ts new file mode 100644 index 0000000..f5b3053 --- /dev/null +++ b/ledger/tasks/start-max.ts @@ -0,0 +1,13 @@ +// ledger: step 11, refused: a task started +// A start that lets so many of its tasks run at once: `start(wave(n), { max: 4 })`. +async function wave(n: number) { + await sleep(frames(n)); + print(`wave ${n}`); +} +program(async () => { + let n = 1; + while (n <= 3) { + start(wave(n), { max: 4 }); + n += 1; + } +}); diff --git a/ledger/tasks/start-void.ts b/ledger/tasks/start-void.ts new file mode 100644 index 0000000..18cb006 --- /dev/null +++ b/ledger/tasks/start-void.ts @@ -0,0 +1,10 @@ +// ledger: step 11, refused: a task started +// A task started by a call nothing waits for: `void later()`. +async function later() { + await sleep(frames(2)); + print("later"); +} +program(async () => { + void later(); + print("now"); +}); diff --git a/ledger/tasks/task-awaited.ts b/ledger/tasks/task-awaited.ts new file mode 100644 index 0000000..1561a9e --- /dev/null +++ b/ledger/tasks/task-awaited.ts @@ -0,0 +1,11 @@ +// ledger: step 11, refused: a task started +// A task waited for: `await t`, the program going on in the frame the task ends. +async function short() { + await sleep(frames(3)); +} +program(async () => { + const t = start(short()); + print("started"); + await t; + print("ended"); +}); diff --git a/ledger/tasks/task-cancel.ts b/ledger/tasks/task-cancel.ts new file mode 100644 index 0000000..4299404 --- /dev/null +++ b/ledger/tasks/task-cancel.ts @@ -0,0 +1,10 @@ +// ledger: step 11, refused: a task started +// A task stopped where it is: `t.cancel()`. +async function forever() { + while (true) await sleep(frames(1)); +} +program(async () => { + const t = start(forever()); + await sleep(frames(10)); + t.cancel(); +}); diff --git a/ledger/tasks/task-error-awaited.ts b/ledger/tasks/task-error-awaited.ts new file mode 100644 index 0000000..4313183 --- /dev/null +++ b/ledger/tasks/task-error-awaited.ts @@ -0,0 +1,15 @@ +// ledger: step 11, refused: an error thrown or caught (try, throw) +// An error a task ends on, thrown again where the task is awaited and caught there: `try { await t; } catch (e) { … }`. +async function wave(n: number) { + await sleep(frames(2)); + if (n == 2) throw new Error("lost"); +} +program(async () => { + const t = start(wave(2)); + try { + await t; + print("done"); + } catch (e) { + print("the wave was lost"); + } +}); diff --git a/ledger/tasks/task-error-not-awaited.ts b/ledger/tasks/task-error-not-awaited.ts new file mode 100644 index 0000000..d1fea7b --- /dev/null +++ b/ledger/tasks/task-error-not-awaited.ts @@ -0,0 +1,11 @@ +// ledger: step 11, refused: an error thrown or caught (try, throw) +// An error a task ends on that nothing awaits: the task ends and the game says so, and the program goes on. +async function worker() { + await sleep(frames(1)); + throw new Error("worker failed"); +} +program(async () => { + start(worker()); + await sleep(frames(3)); + print("still here"); +}); diff --git a/ledger/tasks/task-outlives-program.ts b/ledger/tasks/task-outlives-program.ts new file mode 100644 index 0000000..f56f90b --- /dev/null +++ b/ledger/tasks/task-outlives-program.ts @@ -0,0 +1,9 @@ +// ledger: step 11, refused: a task started +// A task that runs on after the program has ended. +program(async () => { + start(async () => { + await sleep(frames(5)); + print("later"); + }); + print("end"); +}); diff --git a/ledger/tasks/task-running.ts b/ledger/tasks/task-running.ts new file mode 100644 index 0000000..18731e4 --- /dev/null +++ b/ledger/tasks/task-running.ts @@ -0,0 +1,14 @@ +// ledger: step 11, refused: a task started +// Whether a task still runs: `t.running`, true until it ends or is cancelled. +async function short() { + await sleep(frames(3)); +} +program(async () => { + const t = start(short()); + let n = 0; + while (t.running) { + n += 1; + await nextFrame(); + } + print(`${n}`); +}); diff --git a/ledger/tasks/task-starts-a-task.ts b/ledger/tasks/task-starts-a-task.ts new file mode 100644 index 0000000..de97475 --- /dev/null +++ b/ledger/tasks/task-starts-a-task.ts @@ -0,0 +1,13 @@ +// ledger: step 11, refused: a task started +// A task that starts a task of its own. +async function inner(n: number) { + await sleep(frames(n)); + print(`inner ${n}`); +} +async function outer() { + await sleep(frames(1)); + start(inner(2)); +} +program(async () => { + start(outer()); +}); diff --git a/ledger/tasks/then.ts b/ledger/tasks/then.ts new file mode 100644 index 0000000..7e146e1 --- /dev/null +++ b/ledger/tasks/then.ts @@ -0,0 +1,11 @@ +// ledger: step 1, takes +// What a promise gives, handed to a function by `.then((k) => …)` where it is awaited. +async function slow(n: number): Promise { + await sleep(frames(n)); + return n + 1; +} +program(async () => { + await slow(2).then((k) => { + print(`then ${k}`); + }); +}); diff --git a/ledger/tasks/timers-after.ts b/ledger/tasks/timers-after.ts new file mode 100644 index 0000000..71ebfb2 --- /dev/null +++ b/ledger/tasks/timers-after.ts @@ -0,0 +1,8 @@ +// ledger: step 11, refused: a task started +// trigscript/timers: `after(delay, fn)`, a function run once, so many frames from now, in a task beside the program. +import { after, SECOND } from "trigscript/timers"; +program(async () => { + after(3 * SECOND, () => { + print("Go!"); + }); +}); diff --git a/ledger/tasks/timers-cooldown-ready.ts b/ledger/tasks/timers-cooldown-ready.ts new file mode 100644 index 0000000..dc73690 --- /dev/null +++ b/ledger/tasks/timers-cooldown-ready.ts @@ -0,0 +1,11 @@ +// ledger: step 6, refused: a read of memory.game.frame +// trigscript/timers: a `Cooldown` asked whether it is ready, how long until it is, and made ready again: `ready()`, `left()`, `reset()`. +import { Cooldown } from "trigscript/timers"; +program(async () => { + const blink = new Cooldown(24); + blink.use(); + await sleep(frames(5)); + if (!blink.ready()) print(`${blink.left()}`); + blink.reset(); + if (blink.ready()) print("ready"); +}); diff --git a/ledger/tasks/timers-cooldown.ts b/ledger/tasks/timers-cooldown.ts new file mode 100644 index 0000000..fa22e8e --- /dev/null +++ b/ledger/tasks/timers-cooldown.ts @@ -0,0 +1,14 @@ +// ledger: step 6, refused: a read of memory.game.frame +// trigscript/timers: a `Cooldown`, something that can be done once every so many frames: `shot.use()` is true when it was ready. +import { Cooldown } from "trigscript/timers"; +program(async () => { + const shot = new Cooldown(12); + let fired = 0; + let i = 0; + while (i < 48) { + if (shot.use()) fired += 1; + await nextFrame(); + i += 1; + } + print(`${fired}`); +}); diff --git a/ledger/tasks/timers-countdown-changed.ts b/ledger/tasks/timers-countdown-changed.ts new file mode 100644 index 0000000..e17f565 --- /dev/null +++ b/ledger/tasks/timers-countdown-changed.ts @@ -0,0 +1,10 @@ +// ledger: step 6, refused: a read of memory.game.frame +// trigscript/timers: a `Countdown` given more time, and started again: `add(frames)`, `restart(length)`. +import { Countdown } from "trigscript/timers"; +program(async () => { + const fuse = new Countdown(24); + fuse.add(12); + await sleep(frames(30)); + if (!fuse.done()) fuse.restart(24); + print(`${fuse.left()}`); +}); diff --git a/ledger/tasks/timers-countdown-until.ts b/ledger/tasks/timers-countdown-until.ts new file mode 100644 index 0000000..dcc4837 --- /dev/null +++ b/ledger/tasks/timers-countdown-until.ts @@ -0,0 +1,8 @@ +// ledger: step 6, refused: a read of memory.game.frame +// trigscript/timers: a wait until a `Countdown` has run out: `await until(() => round.done())`. +import { Countdown, SECOND } from "trigscript/timers"; +program(async () => { + const round = new Countdown(3 * SECOND); + await until(() => round.done()); + print("time"); +}); diff --git a/ledger/tasks/timers-countdown.ts b/ledger/tasks/timers-countdown.ts new file mode 100644 index 0000000..3ecda8c --- /dev/null +++ b/ledger/tasks/timers-countdown.ts @@ -0,0 +1,11 @@ +// ledger: step 6, refused: a read of memory.game.frame +// trigscript/timers: a `Countdown`, a length of time that runs down while the game runs: `left()` and `done()`. +import { Countdown } from "trigscript/timers"; +program(async () => { + const round = new Countdown(48); + while (!round.done()) { + print(`${round.left()}`); + await sleep(frames(12)); + } + print("time"); +}); diff --git a/ledger/tasks/timers-every-cancelled.ts b/ledger/tasks/timers-every-cancelled.ts new file mode 100644 index 0000000..f94cabc --- /dev/null +++ b/ledger/tasks/timers-every-cancelled.ts @@ -0,0 +1,10 @@ +// ledger: step 11, refused: a task started +// trigscript/timers: an `every` stopped by the task it gives back: `const t = every(…); t.cancel()`. +import { every, SECOND } from "trigscript/timers"; +program(async () => { + const t = every(SECOND, (n) => { + print(`${n}`); + }); + await sleep(frames(5 * SECOND)); + t.cancel(); +}); diff --git a/ledger/tasks/timers-every-times.ts b/ledger/tasks/timers-every-times.ts new file mode 100644 index 0000000..ad6aaf7 --- /dev/null +++ b/ledger/tasks/timers-every-times.ts @@ -0,0 +1,8 @@ +// ledger: step 11, refused: a task started +// trigscript/timers: `every(period, fn, times)`, a function run so many times, so many frames apart. +import { every } from "trigscript/timers"; +program(async () => { + every(12, (n) => { + print(`${n}`); + }, 3); +}); diff --git a/ledger/tasks/timers-every.ts b/ledger/tasks/timers-every.ts new file mode 100644 index 0000000..8e8d063 --- /dev/null +++ b/ledger/tasks/timers-every.ts @@ -0,0 +1,8 @@ +// ledger: step 11, refused: a task started +// trigscript/timers: `every(period, fn)`, a function run every so many frames in a task beside the program, handed how many times it has run. +import { every, SECOND } from "trigscript/timers"; +program(async () => { + every(SECOND, (n) => { + print(`${n}`); + }); +}); diff --git a/ledger/tasks/timers-now.ts b/ledger/tasks/timers-now.ts new file mode 100644 index 0000000..a2e85e7 --- /dev/null +++ b/ledger/tasks/timers-now.ts @@ -0,0 +1,6 @@ +// ledger: step 6, refused: a read of memory.game.frame +// trigscript/timers: `now()`, the frames of game time since the game began. +import { now } from "trigscript/timers"; +program(() => { + print(`${now()}`); +}); diff --git a/ledger/tasks/timers-over.ts b/ledger/tasks/timers-over.ts new file mode 100644 index 0000000..8f940c0 --- /dev/null +++ b/ledger/tasks/timers-over.ts @@ -0,0 +1,14 @@ +// ledger: step 3, takes +// trigscript/timers: `for await (const x of over(list, { per: 2 }))`, a loop over an array spread over frames, so many elements a frame. +import { over } from "trigscript/timers"; +program(async () => { + const xs = [1, 2, 3, 4, 5]; + let k = 1; + while (k < 4) { + xs[k] = k * 4; + k += 1; + } + let s = 0; + for await (const x of over(xs, { per: 2 })) s += x; + print(`${s}`); +}); diff --git a/ledger/tasks/timers-second.ts b/ledger/tasks/timers-second.ts new file mode 100644 index 0000000..164a940 --- /dev/null +++ b/ledger/tasks/timers-second.ts @@ -0,0 +1,7 @@ +// ledger: step 0, takes +// trigscript/timers: `SECOND`, the frames of a second of game time (24), for a wait counted in frames. +import { SECOND } from "trigscript/timers"; +program(async () => { + await sleep(frames(2 * SECOND)); + print("two seconds"); +}); diff --git a/ledger/tasks/timers-spread.ts b/ledger/tasks/timers-spread.ts new file mode 100644 index 0000000..950aa93 --- /dev/null +++ b/ledger/tasks/timers-spread.ts @@ -0,0 +1,10 @@ +// ledger: step 1, takes +// trigscript/timers: `for await (const i of spread(count, { per: 4 }))`, a count from 0 spread over frames, so many a frame. +import { spread } from "trigscript/timers"; +program(async () => { + let count = 2; + while (count < 10) count += 3; + let s = 0; + for await (const i of spread(count, { per: 4 })) s += i; + print(`${s}`); +}); diff --git a/ledger/tasks/timers-stopwatch.ts b/ledger/tasks/timers-stopwatch.ts new file mode 100644 index 0000000..bf47e02 --- /dev/null +++ b/ledger/tasks/timers-stopwatch.ts @@ -0,0 +1,9 @@ +// ledger: step 6, refused: a read of memory.game.frame +// trigscript/timers: a `Stopwatch`, the frames since it was made or reset: `elapsed()`, `reset()`. +import { Stopwatch } from "trigscript/timers"; +program(async () => { + const lap = new Stopwatch(); + await sleep(frames(10)); + print(`${lap.elapsed()}`); + lap.reset(); +}); diff --git a/ledger/tasks/until-condition.ts b/ledger/tasks/until-condition.ts new file mode 100644 index 0000000..966a66e --- /dev/null +++ b/ledger/tasks/until-condition.ts @@ -0,0 +1,6 @@ +// ledger: step 5, refused: the condition Elapsed Time +// A wait until a condition of the game holds, asked every frame: `await until(elapsedTime(">=", 5))`. +program(async () => { + await until(elapsedTime(">=", 5)); + print("five seconds"); +}); diff --git a/ledger/tasks/until-function.ts b/ledger/tasks/until-function.ts new file mode 100644 index 0000000..93d4b13 --- /dev/null +++ b/ledger/tasks/until-function.ts @@ -0,0 +1,10 @@ +// ledger: step 1, takes +// A wait until a function says true, asked once now and once every frame: `await until(() => n >= 3)`. +program(async () => { + let n = 0; + await until(() => { + n += 1; + return n >= 3; + }); + print(`${n}`); +}); diff --git a/ledger/texts/boolean-as-a-text.ts b/ledger/texts/boolean-as-a-text.ts new file mode 100644 index 0000000..5e9c4df --- /dev/null +++ b/ledger/texts/boolean-as-a-text.ts @@ -0,0 +1,8 @@ +// ledger: step 8, refused: a ?: of texts +// A boolean of the program as a text, true or false: `` `${done}` ``. +program(() => { + let n = 0; + while (n < 12) n += 5; + const done = n > 10; + print(`${done}`); +}); diff --git a/ledger/texts/character-code.ts b/ledger/texts/character-code.ts new file mode 100644 index 0000000..6ee54dd --- /dev/null +++ b/ledger/texts/character-code.ts @@ -0,0 +1,7 @@ +// ledger: step 8, refused: a character's code +// The code of a character of a text, at a place that changes while the game runs: `"Marine".charCodeAt(i)`. +program(() => { + let i = 0; + while (i < 3) i += 2; + print(`${"Marine".charCodeAt(i)}`); +}); diff --git a/ledger/texts/jsx-bar.tsx b/ledger/texts/jsx-bar.tsx new file mode 100644 index 0000000..ff929af --- /dev/null +++ b/ledger/texts/jsx-bar.tsx @@ -0,0 +1,11 @@ +// ledger: step 8, refused: JSX made while the game runs +// A bar of squares in a line of JSX, as long as a value of the program: ``. +program(() => { + let hp = 0; + while (hp < 60) hp += 25; + print( + + Base + , + ); +}); diff --git a/ledger/texts/jsx-in-a-program.tsx b/ledger/texts/jsx-in-a-program.tsx new file mode 100644 index 0000000..11cef4e --- /dev/null +++ b/ledger/texts/jsx-in-a-program.tsx @@ -0,0 +1,6 @@ +// ledger: step 8, refused: JSX made while the game runs +// A line of JSX written in the program, every part of it known when the map is built: +// `print(Wave cleared)`. +program(() => { + print(Wave cleared); +}); diff --git a/ledger/texts/jsx-known-when-built.tsx b/ledger/texts/jsx-known-when-built.tsx new file mode 100644 index 0000000..de37cba --- /dev/null +++ b/ledger/texts/jsx-known-when-built.tsx @@ -0,0 +1,11 @@ +// ledger: step 0, takes +// A line of JSX written outside the program, worked out when the map is built, with a colour by name and a cell in one +// of `textColors`: `const title = …`, then `print(title)`. +const title = ( + + Score + +); +program(() => { + print(title); +}); diff --git a/ledger/texts/jsx-with-a-number.tsx b/ledger/texts/jsx-with-a-number.tsx new file mode 100644 index 0000000..fbd5ea5 --- /dev/null +++ b/ledger/texts/jsx-with-a-number.tsx @@ -0,0 +1,7 @@ +// ledger: step 8, refused: JSX made while the game runs +// A line of JSX with a number of the program in it, put together while the game runs: `Wave {n}`. +program(() => { + let n = 0; + while (n < 12) n += 5; + print(Wave {n}); +}); diff --git a/ledger/texts/number-as-a-text.ts b/ledger/texts/number-as-a-text.ts new file mode 100644 index 0000000..2e459a7 --- /dev/null +++ b/ledger/texts/number-as-a-text.ts @@ -0,0 +1,10 @@ +// ledger: step 8, refused: a text kept in a variable +// A number of the program made a text: `String(n)` and `n.toString()`, kept as a text. +program(() => { + let n = 0; + while (n < 12) n += 5; + const a = String(n); + const b = n.toString(); + print(a); + print(b); +}); diff --git a/ledger/texts/parse-int.ts b/ledger/texts/parse-int.ts new file mode 100644 index 0000000..e2a5acd --- /dev/null +++ b/ledger/texts/parse-int.ts @@ -0,0 +1,8 @@ +// ledger: step 8, refused: a ?: of texts +// The whole number written at the start of a text: `parseInt(s)`, and `isNaN(parseInt(s))` for whether there is one. +program(() => { + let n = 0; + while (n < 12) n += 5; + if (isNaN(parseInt(n > 10 ? " 42 ore" : "none"))) print("no number"); + else print(`${parseInt(n > 10 ? " 42 ore" : "none")}`); +}); diff --git a/ledger/texts/player-colour-set.ts b/ledger/texts/player-colour-set.ts new file mode 100644 index 0000000..305e096 --- /dev/null +++ b/ledger/texts/player-colour-set.ts @@ -0,0 +1,7 @@ +// ledger: step 6, refused: a write of player.color +// A player's colour changed while the game runs, to one of `colors`: `stats(P2).color = colors.teal`. +program(() => { + let n = 0; + while (n < 12) n += 5; + if (n > 10) stats(P2).color = colors.teal; +}); diff --git a/ledger/texts/print-a-colour-from-a-value.tsx b/ledger/texts/print-a-colour-from-a-value.tsx new file mode 100644 index 0000000..c69ca57 --- /dev/null +++ b/ledger/texts/print-a-colour-from-a-value.tsx @@ -0,0 +1,13 @@ +// ledger: step 8, refused: JSX made while the game runs +// A colour of the game's texts chosen while the game runs, in a printed line: ``, `c` one of +// `textColors`. +program(() => { + let n = 0; + while (n < 12) n += 5; + const c = n > 10 ? textColors.green : textColors.red; + print( + + Health + , + ); +}); diff --git a/ledger/texts/print-a-fixed-number.ts b/ledger/texts/print-a-fixed-number.ts new file mode 100644 index 0000000..3dc002b --- /dev/null +++ b/ledger/texts/print-a-fixed-number.ts @@ -0,0 +1,7 @@ +// ledger: step 4, refused: a fixed number printed +// A number with a fraction in a printed text, as a decimal of up to four places: `` print(`${x}`) `` of 2.75. +program(() => { + let x = 0.5; + while (x < 2.5) x += 0.75; + print(`${x}`); +}); diff --git a/ledger/texts/print-a-number-below-zero.ts b/ledger/texts/print-a-number-below-zero.ts new file mode 100644 index 0000000..7f48f77 --- /dev/null +++ b/ledger/texts/print-a-number-below-zero.ts @@ -0,0 +1,7 @@ +// ledger: step 2, takes +// A number below zero in a printed text, with its minus sign: `` print(`${n}`) `` of −7. +program(() => { + let n = 5; + while (n > -5) n -= 4; + print(`n=${n}`); +}); diff --git a/ledger/texts/print-a-player-colour.ts b/ledger/texts/print-a-player-colour.ts new file mode 100644 index 0000000..3b9edf4 --- /dev/null +++ b/ledger/texts/print-a-player-colour.ts @@ -0,0 +1,5 @@ +// ledger: step 8, refused: a printed player's colour +// A player's colour in a printed text, so that what follows is in it: `` print(`${color(P2)}Blue team`) ``. +program(() => { + print(`${color(P2)}Blue team`); +}); diff --git a/ledger/texts/print-a-player-name.ts b/ledger/texts/print-a-player-name.ts new file mode 100644 index 0000000..63a5870 --- /dev/null +++ b/ledger/texts/print-a-player-name.ts @@ -0,0 +1,5 @@ +// ledger: step 8, refused: a printed player's name +// A player's name in a printed text, filled in by the game: `` print(`${name(P2)} joined`) ``. +program(() => { + print(`${name(P2)} joined`); +}); diff --git a/ledger/texts/print-a-u32.ts b/ledger/texts/print-a-u32.ts new file mode 100644 index 0000000..327a7d7 --- /dev/null +++ b/ledger/texts/print-a-u32.ts @@ -0,0 +1,8 @@ +// ledger: step 2, takes +// A `u32` in a printed text, read from 0 up: `u32(-1)` prints 4294967295. +program(() => { + let a = 0; + while (a > -1) a -= 1; + const x: u32 = u32(a); + print(`${x}`); +}); diff --git a/ledger/texts/print-a-whole-number.ts b/ledger/texts/print-a-whole-number.ts new file mode 100644 index 0000000..b28aa93 --- /dev/null +++ b/ledger/texts/print-a-whole-number.ts @@ -0,0 +1,7 @@ +// ledger: step 2, takes +// A text with a whole number of the program in it, written out in digits: `` print(`Wave ${wave}`) ``. +program(() => { + let wave = 0; + while (wave < 12) wave += 5; + print(`Wave ${wave} of 20`); +}); diff --git a/ledger/texts/print-at-the-centre.ts b/ledger/texts/print-at-the-centre.ts new file mode 100644 index 0000000..6644d39 --- /dev/null +++ b/ledger/texts/print-at-the-centre.ts @@ -0,0 +1,7 @@ +// ledger: step 5, refused: a text at the centre of the screen +// A text shown in the middle of the screen, where the game's own messages appear: `print(…, { position: "center" })`. +program(() => { + let n = 0; + while (n < 12) n += 5; + print(`Wave ${n}`, { position: "center" }); +}); diff --git a/ledger/texts/print-known-text.ts b/ledger/texts/print-known-text.ts new file mode 100644 index 0000000..343dc64 --- /dev/null +++ b/ledger/texts/print-known-text.ts @@ -0,0 +1,5 @@ +// ledger: step 0, takes +// A text written in the script shown to the program's owner: `print("Wave cleared")`. +program(() => { + print("Wave cleared"); +}); diff --git a/ledger/texts/print-several-numbers.ts b/ledger/texts/print-several-numbers.ts new file mode 100644 index 0000000..77f1138 --- /dev/null +++ b/ledger/texts/print-several-numbers.ts @@ -0,0 +1,11 @@ +// ledger: step 2, takes +// A text with several numbers of the program in it: `` print(`${a} + ${b} = ${a + b}`) ``. +program(() => { + let a = 0; + let b = 100; + while (a < 12) { + a += 5; + b -= 7; + } + print(`${a} + ${b} = ${a + b}`); +}); diff --git a/ledger/texts/print-the-largest-and-smallest-numbers.ts b/ledger/texts/print-the-largest-and-smallest-numbers.ts new file mode 100644 index 0000000..a43c2a6 --- /dev/null +++ b/ledger/texts/print-the-largest-and-smallest-numbers.ts @@ -0,0 +1,11 @@ +// ledger: step 2, takes +// The largest and the smallest numbers a variable holds, printed: 2 147 483 647 and −2 147 483 648. +program(() => { + let top = 2147483600; + let bottom = -2147483600; + while (top < 2147483647) { + top += 47; + bottom -= 48; + } + print(`${top} ${bottom}`); +}); diff --git a/ledger/texts/print-to-all-players.ts b/ledger/texts/print-to-all-players.ts new file mode 100644 index 0000000..1e0eb89 --- /dev/null +++ b/ledger/texts/print-to-all-players.ts @@ -0,0 +1,7 @@ +// ledger: step 5, refused: a text shown to other players than the program's owner +// A text shown to every player: `print(…, { to: AllPlayers })`. +program(() => { + let n = 0; + while (n < 12) n += 5; + print(`Wave ${n}`, { to: AllPlayers }); +}); diff --git a/ledger/texts/print-to-another-player.ts b/ledger/texts/print-to-another-player.ts new file mode 100644 index 0000000..8acae6d --- /dev/null +++ b/ledger/texts/print-to-another-player.ts @@ -0,0 +1,7 @@ +// ledger: step 5, refused: a text shown to other players than the program's owner +// A text shown to another player than the program's owner: `print(…, { to: P2 })`. +program(() => { + let n = 0; + while (n < 12) n += 5; + print(`${n} left`, { to: P2 }); +}); diff --git a/ledger/texts/template-as-a-value.ts b/ledger/texts/template-as-a-value.ts new file mode 100644 index 0000000..7447bac --- /dev/null +++ b/ledger/texts/template-as-a-value.ts @@ -0,0 +1,8 @@ +// ledger: step 8, refused: two texts compared +// A template put together while the game runs and used as a text, not printed at once: `` `${n}` === "15" ``. +program(() => { + let n = 0; + while (n < 12) n += 5; + if (`${n}` === "15") print("fifteen"); + else print("not fifteen"); +}); diff --git a/ledger/texts/text-array-join.ts b/ledger/texts/text-array-join.ts new file mode 100644 index 0000000..d259f2f --- /dev/null +++ b/ledger/texts/text-array-join.ts @@ -0,0 +1,9 @@ +// ledger: step 8, refused: a text of the program's +// The texts of an array of texts as one, with a separator between them: `names.join(", ")`. +program(() => { + const names = ["Raynor", "Kerrigan"]; + let n = 0; + while (n < 12) n += 5; + if (n > 10) names[1] = "Tassadar"; + print(names.join(", ")); +}); diff --git a/ledger/texts/text-array-that-grows.ts b/ledger/texts/text-array-that-grows.ts new file mode 100644 index 0000000..73a271d --- /dev/null +++ b/ledger/texts/text-array-that-grows.ts @@ -0,0 +1,11 @@ +// ledger: step 9, refused: the length of an array that grows +// An array of texts that grows: `log.push("wave")`. +program(() => { + const log: string[] = []; + let n = 0; + while (n < 12) { + log.push("wave"); + n += 5; + } + print(`${log.length}`); +}); diff --git a/ledger/texts/text-array.ts b/ledger/texts/text-array.ts new file mode 100644 index 0000000..9d916a9 --- /dev/null +++ b/ledger/texts/text-array.ts @@ -0,0 +1,9 @@ +// ledger: step 8, refused: a text of the program's +// An array of texts of the program, stored and read at a place that changes while the game runs: `names[i] = "Medic"`. +program(() => { + const names = ["Marine", "Marine", "Marine"]; + let i = 0; + while (i < 2) i += 1; + names[i] = "Medic"; + print(names[i]); +}); diff --git a/ledger/texts/text-character-at.ts b/ledger/texts/text-character-at.ts new file mode 100644 index 0000000..6da462f --- /dev/null +++ b/ledger/texts/text-character-at.ts @@ -0,0 +1,8 @@ +// ledger: step 8, refused: a ?: of texts +// One character of a text, at a place that changes while the game runs: `s[i]`, `s.charAt(i)`, `s.at(-1)`. +program(() => { + let i = 0; + while (i < 6) i += 4; + const s = "Terran Marine"; + print(`${s[i]}${s.charAt(i + 1)}${s.at(-1)}`); +}); diff --git a/ledger/texts/text-chosen-by-a-condition.ts b/ledger/texts/text-chosen-by-a-condition.ts new file mode 100644 index 0000000..548ad0b --- /dev/null +++ b/ledger/texts/text-chosen-by-a-condition.ts @@ -0,0 +1,7 @@ +// ledger: step 8, refused: a ?: of texts +// One text or another, by a test while the game runs: `print(n > 10 ? "Medic" : "Marine")`. +program(() => { + let n = 0; + while (n < 12) n += 5; + print(n > 10 ? "Medic" : "Marine"); +}); diff --git a/ledger/texts/text-for-of.ts b/ledger/texts/text-for-of.ts new file mode 100644 index 0000000..12e8411 --- /dev/null +++ b/ledger/texts/text-for-of.ts @@ -0,0 +1,9 @@ +// ledger: step 8, refused: a loop over a text +// A loop over the characters of a text: `for (const ch of s)`. +program(() => { + let n = 0; + while (n < 12) n += 5; + let vowels = 0; + for (const ch of n > 10 ? "Medic" : "Marine") if (ch === "a" || ch === "e" || ch === "i") vowels += 1; + print(`${vowels}`); +}); diff --git a/ledger/texts/text-function.ts b/ledger/texts/text-function.ts new file mode 100644 index 0000000..6f7a2e6 --- /dev/null +++ b/ledger/texts/text-function.ts @@ -0,0 +1,10 @@ +// ledger: step 8, refused: a function that gives back a text (label) +// A function of the script that takes a text and gives one back: `label(name, n)`. +function label(name: string, n: number): string { + return `${name} x${n}`; +} +program(() => { + let n = 0; + while (n < 12) n += 5; + print(label(n > 10 ? "Medic" : "Marine", n)); +}); diff --git a/ledger/texts/text-in-a-printed-template.ts b/ledger/texts/text-in-a-printed-template.ts new file mode 100644 index 0000000..0a83895 --- /dev/null +++ b/ledger/texts/text-in-a-printed-template.ts @@ -0,0 +1,7 @@ +// ledger: step 8, refused: a ?: of texts +// A text of the program in a printed text: `` print(`${n > 10 ? "Medic" : "Marine"} ready`) ``. +program(() => { + let n = 0; + while (n < 12) n += 5; + print(`${n > 10 ? "Medic" : "Marine"} ready`); +}); diff --git a/ledger/texts/text-in-a-record.ts b/ledger/texts/text-in-a-record.ts new file mode 100644 index 0000000..8e68fc3 --- /dev/null +++ b/ledger/texts/text-in-a-record.ts @@ -0,0 +1,12 @@ +// ledger: step 8, refused: a text of the program's +// A text that is a field of each row of an array of records, written while the game runs: `heroes[i].name = "…"`. +program(() => { + const heroes = [ + { name: "Raynor", hp: 5 }, + { name: "Kerrigan", hp: 7 }, + ]; + let i = 0; + while (i < 1) i += 1; + heroes[i].name = "Tassadar"; + print(heroes[i].name); +}); diff --git a/ledger/texts/text-includes.ts b/ledger/texts/text-includes.ts new file mode 100644 index 0000000..be43c69 --- /dev/null +++ b/ledger/texts/text-includes.ts @@ -0,0 +1,8 @@ +// ledger: step 8, refused: a text's includes +// Whether a text of the program has another in it: `s.includes("ed")`. +program(() => { + let n = 0; + while (n < 12) n += 5; + if ((n > 10 ? "Medic" : "Marine").includes("ed")) print("has it"); + else print("has it not"); +}); diff --git a/ledger/texts/text-index-of.ts b/ledger/texts/text-index-of.ts new file mode 100644 index 0000000..5683560 --- /dev/null +++ b/ledger/texts/text-index-of.ts @@ -0,0 +1,8 @@ +// ledger: step 8, refused: a search in a text +// Where a text is found in another, from a place that changes while the game runs, −1 when it is not: +// `"abcabc".indexOf("c", i)`, and `lastIndexOf`. +program(() => { + let i = 0; + while (i < 3) i += 2; + print(`${"abcabc".indexOf("c", i)} ${"abcabc".lastIndexOf("a")}`); +}); diff --git a/ledger/texts/text-is-empty.ts b/ledger/texts/text-is-empty.ts new file mode 100644 index 0000000..93582dc --- /dev/null +++ b/ledger/texts/text-is-empty.ts @@ -0,0 +1,8 @@ +// ledger: step 8, refused: two texts compared +// Whether a text is empty, by testing it: `if (s)`. +program(() => { + let n = 0; + while (n < 12) n += 5; + if (n > 10 ? "" : "x") print("not empty"); + else print("empty"); +}); diff --git a/ledger/texts/text-length.ts b/ledger/texts/text-length.ts new file mode 100644 index 0000000..8c775d0 --- /dev/null +++ b/ledger/texts/text-length.ts @@ -0,0 +1,7 @@ +// ledger: step 8, refused: a text's length +// How many characters a text of the program has: `.length`. +program(() => { + let n = 0; + while (n < 12) n += 5; + print(`${(n > 10 ? "Medic" : "Marine").length}`); +}); diff --git a/ledger/texts/text-list-read-at-a-value.ts b/ledger/texts/text-list-read-at-a-value.ts new file mode 100644 index 0000000..dc7c4e3 --- /dev/null +++ b/ledger/texts/text-list-read-at-a-value.ts @@ -0,0 +1,8 @@ +// ledger: step 8, refused: a text of the program's +// A list of texts written in the script, looked up at a value that changes while the game runs: `NAMES[i]`. +const NAMES = ["Raynor", "Kerrigan", "Tassadar"]; +program(() => { + let i = 0; + while (i < 2) i += 1; + print(NAMES[i]); +}); diff --git a/ledger/texts/text-pad.ts b/ledger/texts/text-pad.ts new file mode 100644 index 0000000..4946a05 --- /dev/null +++ b/ledger/texts/text-pad.ts @@ -0,0 +1,9 @@ +// ledger: step 8, refused: a text padded (padStart, padEnd) +// A text made as wide as a number of characters, filled at the start or the end: `String(n).padStart(4, "0")`, +// `padEnd`. +program(() => { + let n = 0; + while (n < 12) n += 5; + print(String(n).padStart(4, "0")); + print("Wave".padEnd(n - 7, ".")); +}); diff --git a/ledger/texts/text-repeat.ts b/ledger/texts/text-repeat.ts new file mode 100644 index 0000000..d69e0a3 --- /dev/null +++ b/ledger/texts/text-repeat.ts @@ -0,0 +1,7 @@ +// ledger: step 8, refused: a text repeated +// A text over again a number of times that changes while the game runs: `"■".repeat(n)`. +program(() => { + let n = 0; + while (n < 6) n += 2; + print("■".repeat(n)); +}); diff --git a/ledger/texts/text-replace.ts b/ledger/texts/text-replace.ts new file mode 100644 index 0000000..a6c8cf6 --- /dev/null +++ b/ledger/texts/text-replace.ts @@ -0,0 +1,8 @@ +// ledger: step 8, refused: a text's replace() +// A text with the first of another in it replaced, or every one: `replace("a", "o")`, `replaceAll("a", "o")`. +program(() => { + let n = 0; + while (n < 12) n += 5; + print((n > 10 ? "banana" : "papaya").replace("a", "o")); + print((n > 10 ? "banana" : "papaya").replaceAll("a", "o")); +}); diff --git a/ledger/texts/text-slice.ts b/ledger/texts/text-slice.ts new file mode 100644 index 0000000..1774976 --- /dev/null +++ b/ledger/texts/text-slice.ts @@ -0,0 +1,9 @@ +// ledger: step 8, refused: a part of a text (slice) +// Part of a text, between places that change while the game runs: `"Terran Marine".slice(i, i + 4)`, and +// `substring`. +program(() => { + let i = 0; + while (i < 6) i += 3; + print("Terran Marine".slice(i, i + 4)); + print("Terran Marine".substring(i + 1)); +}); diff --git a/ledger/texts/text-split.ts b/ledger/texts/text-split.ts new file mode 100644 index 0000000..761ce9f --- /dev/null +++ b/ledger/texts/text-split.ts @@ -0,0 +1,8 @@ +// ledger: step 9, refused: the length of an array that grows +// A text cut at a separator into an array of texts: `"a,b,c".split(",")`. +program(() => { + let n = 0; + while (n < 12) n += 5; + const parts = (n > 10 ? "1,2,3" : "4,5").split(","); + print(`${parts.length}`); +}); diff --git a/ledger/texts/text-starts-and-ends-with.ts b/ledger/texts/text-starts-and-ends-with.ts new file mode 100644 index 0000000..05936ee --- /dev/null +++ b/ledger/texts/text-starts-and-ends-with.ts @@ -0,0 +1,8 @@ +// ledger: step 8, refused: a text's startsWith +// Whether a text of the program begins or ends with another: `startsWith`, `endsWith`. +program(() => { + let n = 0; + while (n < 12) n += 5; + if ((n > 10 ? "Medic" : "Marine").startsWith("Me")) print("starts so"); + if ((n > 10 ? "Medic" : "Marine").endsWith("ine")) print("ends so"); +}); diff --git a/ledger/texts/text-switch.ts b/ledger/texts/text-switch.ts new file mode 100644 index 0000000..cdefe85 --- /dev/null +++ b/ledger/texts/text-switch.ts @@ -0,0 +1,16 @@ +// ledger: step 8, refused: a text made while the game runs +// A `switch` over a text, its cases texts: `switch (unit) { case "Marine": … }`. +program(() => { + let n = 0; + while (n < 12) n += 5; + switch (n > 10 ? "Medic" : "Marine") { + case "Marine": + print("50 ore"); + break; + case "Medic": + print("75 ore"); + break; + default: + print("?"); + } +}); diff --git a/ledger/texts/text-trim.ts b/ledger/texts/text-trim.ts new file mode 100644 index 0000000..ae2dbfd --- /dev/null +++ b/ledger/texts/text-trim.ts @@ -0,0 +1,9 @@ +// ledger: step 8, refused: a text's trim() +// A text without the spaces at its ends: `trim()`, `trimStart()`, `trimEnd()`. +program(() => { + let n = 0; + while (n < 12) n += 5; + print((n > 10 ? " go " : " stop ").trim()); + print((n > 10 ? " go" : " stop").trimStart()); + print((n > 10 ? "go " : "stop ").trimEnd()); +}); diff --git a/ledger/texts/text-upper-and-lower-case.ts b/ledger/texts/text-upper-and-lower-case.ts new file mode 100644 index 0000000..bde05aa --- /dev/null +++ b/ledger/texts/text-upper-and-lower-case.ts @@ -0,0 +1,8 @@ +// ledger: step 8, refused: a text's toUpperCase() +// A text in capitals or in small letters, A to Z only: `toUpperCase()`, `toLowerCase()`. +program(() => { + let n = 0; + while (n < 12) n += 5; + print((n > 10 ? "Medic" : "Marine").toUpperCase()); + print((n > 10 ? "Medic" : "Marine").toLowerCase()); +}); diff --git a/ledger/texts/text-variable.ts b/ledger/texts/text-variable.ts new file mode 100644 index 0000000..681517b --- /dev/null +++ b/ledger/texts/text-variable.ts @@ -0,0 +1,9 @@ +// ledger: step 8, refused: a text kept in a variable +// A text kept in a variable and changed while the game runs: `let unit = "Marine"`, then `unit = "Medic"`. +program(() => { + let unit = "Marine"; + let n = 0; + while (n < 12) n += 5; + if (n > 10) unit = "Medic"; + print(unit); +}); diff --git a/ledger/texts/texts-compared.ts b/ledger/texts/texts-compared.ts new file mode 100644 index 0000000..14cc964 --- /dev/null +++ b/ledger/texts/texts-compared.ts @@ -0,0 +1,8 @@ +// ledger: step 8, refused: two texts compared +// Two texts compared while the game runs: `===`, `!==`, and `<` in the order of their characters. +program(() => { + let n = 0; + while (n < 12) n += 5; + if ((n > 10 ? "Medic" : "Marine") < "Mf") print("before"); + else print("after"); +}); diff --git a/ledger/texts/ui-clock.ts b/ledger/texts/ui-clock.ts new file mode 100644 index 0000000..61183fd --- /dev/null +++ b/ledger/texts/ui-clock.ts @@ -0,0 +1,8 @@ +// ledger: step 8, refused: a function that gives back a text (clock) +// Frames of game time as minutes and seconds, from `trigscript/ui`: `clock(frames)` ("1:05"). +import { clock } from "trigscript/ui"; +program(() => { + let frames = 0; + while (frames < 1500) frames += 24; + print(`Time ${clock(frames)}`); +}); diff --git a/ledger/texts/ui-dialog.tsx b/ledger/texts/ui-dialog.tsx new file mode 100644 index 0000000..e57dd17 --- /dev/null +++ b/ledger/texts/ui-dialog.tsx @@ -0,0 +1,9 @@ +// ledger: step 12, refused: a text kept in a variable +// Someone speaking to a player, the words typed in, closed with Space, from `trigscript/ui`: +// `await say(P1, talk, "Raynor", "…")` and ``. +import { DialogBox, say } from "trigscript/ui"; +program(async () => { + const talk = { speaker: "", text: "", open: false }; + ui.lines(P1, () => ); + await say(P1, talk, "Raynor", "Hold the bridge until dawn."); +}); diff --git a/ledger/texts/ui-lines.tsx b/ledger/texts/ui-lines.tsx new file mode 100644 index 0000000..7078b7c --- /dev/null +++ b/ledger/texts/ui-lines.tsx @@ -0,0 +1,10 @@ +// ledger: step 11, refused: a task started +// A text kept in a player's text lines and shown again as the program's variables change: `ui.lines(P1, () => …)`. +program(async () => { + let score = 0; + ui.lines(P1, () => Score {score}); + while (score < 30) { + await sleep(frames(1)); + score += 10; + } +}); diff --git a/ledger/texts/ui-log.tsx b/ledger/texts/ui-log.tsx new file mode 100644 index 0000000..e9664c4 --- /dev/null +++ b/ledger/texts/ui-log.tsx @@ -0,0 +1,13 @@ +// ledger: step 9, refused: the length of an array that grows +// A log that keeps its last lines, shown a line each, from `trigscript/ui`: `log(lines, text)` and +// ``. +import { LogLines, log } from "trigscript/ui"; +program(async () => { + const lines: string[] = []; + let wave = 0; + while (wave < 8) { + wave += 1; + log(lines, wave > 4 ? "a wave" : "a quiet wave", 3); + } + print(); +}); diff --git a/ledger/texts/ui-menu.tsx b/ledger/texts/ui-menu.tsx new file mode 100644 index 0000000..c31c2fc --- /dev/null +++ b/ledger/texts/ui-menu.tsx @@ -0,0 +1,11 @@ +// ledger: step 12, refused: a task started +// A menu a player chooses from with W, S and Space, shown in the text lines, from `trigscript/ui`: +// `await choose(P1, ITEMS.length, shop)` and ``. +import { MenuList, choose } from "trigscript/ui"; +const ITEMS = ["Marine (50)", "Medic (75)", "Leave"]; +program(async () => { + const shop = { selected: 0, open: false }; + ui.lines(P1, () => <>{shop.open && }); + const picked = await choose(P1, ITEMS.length, shop); + print(`Picked ${picked}`); +}); diff --git a/ledger/texts/ui-meter.tsx b/ledger/texts/ui-meter.tsx new file mode 100644 index 0000000..ed54065 --- /dev/null +++ b/ledger/texts/ui-meter.tsx @@ -0,0 +1,9 @@ +// ledger: step 8, refused: JSX made while the game runs +// A labelled bar, green, yellow or red by how full it is, from `trigscript/ui`: +// ``. +import { Meter } from "trigscript/ui"; +program(async () => { + let hp = 0; + while (hp < 900) hp += 400; + print(); +}); diff --git a/ledger/texts/ui-panel.tsx b/ledger/texts/ui-panel.tsx new file mode 100644 index 0000000..d92370a --- /dev/null +++ b/ledger/texts/ui-panel.tsx @@ -0,0 +1,12 @@ +// ledger: step 8, refused: JSX made while the game runs +// A title over the lines inside it, from `trigscript/ui`: `…`. +import { Panel } from "trigscript/ui"; +program(async () => { + let wave = 0; + while (wave < 3) wave += 1; + print( + + Hold the bridge + , + ); +}); diff --git a/ledger/texts/ui-thousands.ts b/ledger/texts/ui-thousands.ts new file mode 100644 index 0000000..6d48bcb --- /dev/null +++ b/ledger/texts/ui-thousands.ts @@ -0,0 +1,8 @@ +// ledger: step 8, refused: a function that gives back a text (thousands) +// A whole number with its thousands marked, from `trigscript/ui`: `thousands(n)` ("12,345"). +import { thousands } from "trigscript/ui"; +program(() => { + let n = 0; + while (n < 12000) n += 4115; + print(`Ore ${thousands(n)}`); +}); diff --git a/ledger/texts/ui-typewriter.ts b/ledger/texts/ui-typewriter.ts new file mode 100644 index 0000000..b274f98 --- /dev/null +++ b/ledger/texts/ui-typewriter.ts @@ -0,0 +1,8 @@ +// ledger: step 8, refused: a text kept in a variable +// A text put into a record a character at a time, from `trigscript/ui`: `await typewriter(box, "…")`. +import { typewriter } from "trigscript/ui"; +program(async () => { + const box = { text: "" }; + await typewriter(box, "Incoming!"); + print(box.text); +}); diff --git a/ledger/triggers/comment.ts b/ledger/triggers/comment.ts new file mode 100644 index 0000000..d8f5a03 --- /dev/null +++ b/ledger/triggers/comment.ts @@ -0,0 +1,6 @@ +// ledger: step 13, refused: a map of our own keeps no trigger() or hyperTriggers() records yet; this script has 1 +// The action Comment: a trigger's name in StarEdit's list, which does nothing in the game. +trigger(P1, [always()], [comment("Starting minerals"), setResources(P1, "set", 50, "ore")]); +program(() => { + print("started"); +}); diff --git a/ledger/triggers/disable-debug-mode.ts b/ledger/triggers/disable-debug-mode.ts new file mode 100644 index 0000000..19499e5 --- /dev/null +++ b/ledger/triggers/disable-debug-mode.ts @@ -0,0 +1,5 @@ +// ledger: step 5, refused: the action Disable Debug Mode +// The action Disable Debug Mode, inside a program. +program(() => { + disableDebugMode(); +}); diff --git a/ledger/triggers/disabled-action.ts b/ledger/triggers/disabled-action.ts new file mode 100644 index 0000000..f5bd160 --- /dev/null +++ b/ledger/triggers/disabled-action.ts @@ -0,0 +1,6 @@ +// ledger: step 13, refused: a map of our own keeps no trigger() or hyperTriggers() records yet; this script has 1 +// An action kept in a trigger but switched off, as StarEdit can: `disabled(…)`. +trigger(P1, [always()], [displayText("Shown."), disabled(defeat())]); +program(() => { + print("started"); +}); diff --git a/ledger/triggers/disabled-condition.ts b/ledger/triggers/disabled-condition.ts new file mode 100644 index 0000000..b7d9c78 --- /dev/null +++ b/ledger/triggers/disabled-condition.ts @@ -0,0 +1,6 @@ +// ledger: step 13, refused: a map of our own keeps no trigger() or hyperTriggers() records yet; this script has 1 +// A condition kept in a trigger but switched off, as StarEdit can: `disabled(…)`. +trigger(P1, [always(), disabled(never())], [displayText("Fires anyway.")]); +program(() => { + print("started"); +}); diff --git a/ledger/triggers/enable-debug-mode.ts b/ledger/triggers/enable-debug-mode.ts new file mode 100644 index 0000000..95998ae --- /dev/null +++ b/ledger/triggers/enable-debug-mode.ts @@ -0,0 +1,5 @@ +// ledger: step 5, refused: the action Enable Debug Mode +// The action Enable Debug Mode, inside a program. +program(() => { + enableDebugMode(); +}); diff --git a/ledger/triggers/hyper-triggers.ts b/ledger/triggers/hyper-triggers.ts new file mode 100644 index 0000000..3103ea2 --- /dev/null +++ b/ledger/triggers/hyper-triggers.ts @@ -0,0 +1,7 @@ +// ledger: step 13, refused: a map of our own keeps no trigger() or hyperTriggers() records yet; this script has 3 +// `hyperTriggers()`: preserved triggers of Wait(0)s that make the game go through the trigger list every +// 2 frames instead of every 31. +hyperTriggers(P8); +program(() => { + print("started"); +}); diff --git a/ledger/triggers/not-in-a-trigger.ts b/ledger/triggers/not-in-a-trigger.ts new file mode 100644 index 0000000..9e48985 --- /dev/null +++ b/ledger/triggers/not-in-a-trigger.ts @@ -0,0 +1,7 @@ +// ledger: step 13, refused: a map of our own keeps no trigger() or hyperTriggers() records yet; this script has 1 +// A condition turned around by `not(…)` in a trigger's conditions: a comparison flips, a switch test flips, +// always becomes never. +trigger(P1, [not(bring(P1, units.AnyUnit, locations.Anywhere, ">=", 1)), not(switchIs(switches.Switch1, "set"))], [defeat()]); +program(() => { + print("started"); +}); diff --git a/ledger/triggers/preserve-trigger.ts b/ledger/triggers/preserve-trigger.ts new file mode 100644 index 0000000..18eefec --- /dev/null +++ b/ledger/triggers/preserve-trigger.ts @@ -0,0 +1,6 @@ +// ledger: step 13, refused: a map of our own keeps no trigger() or hyperTriggers() records yet; this script has 1 +// The action Preserve Trigger in a trigger's actions: `preserveTrigger()`. +trigger(P1, [always()], [setResources(P1, "add", 1, "ore"), preserveTrigger()]); +program(() => { + print("started"); +}); diff --git a/ledger/triggers/preserve.ts b/ledger/triggers/preserve.ts new file mode 100644 index 0000000..1c66497 --- /dev/null +++ b/ledger/triggers/preserve.ts @@ -0,0 +1,6 @@ +// ledger: step 13, refused: a map of our own keeps no trigger() or hyperTriggers() records yet; this script has 1 +// The action Preserve Trigger in a trigger's actions, by its short name: `preserve()`. +trigger(P1, [always()], [setResources(P1, "add", 1, "ore"), preserve()]); +program(() => { + print("started"); +}); diff --git a/ledger/triggers/trigger-disabled-option.ts b/ledger/triggers/trigger-disabled-option.ts new file mode 100644 index 0000000..a3e56cc --- /dev/null +++ b/ledger/triggers/trigger-disabled-option.ts @@ -0,0 +1,6 @@ +// ledger: step 13, refused: a map of our own keeps no trigger() or hyperTriggers() records yet; this script has 1 +// A trigger kept in the map but switched off: `{ disabled: true }`. +trigger(P1, [always()], [victory()], { disabled: true }); +program(() => { + print("started"); +}); diff --git a/ledger/triggers/trigger-execution-flags.ts b/ledger/triggers/trigger-execution-flags.ts new file mode 100644 index 0000000..753715c --- /dev/null +++ b/ledger/triggers/trigger-execution-flags.ts @@ -0,0 +1,7 @@ +// ledger: step 13, refused: a map of our own keeps no trigger() or hyperTriggers() records yet; this script has 1 +// A trigger's other execution flags: ignore the game's end, ignore Display Text, conditions met, +// paused, Wait skipping off, and raw flags ORed in. +trigger(P1, [always()], [displayText("Still here.")], { ignoreGameEnd: true, ignoreDisplay: true, conditionsMet: false, paused: false, waitSkipDisabled: true, flags: 0 }); +program(() => { + print("started"); +}); diff --git a/ledger/triggers/trigger-for-several-players.ts b/ledger/triggers/trigger-for-several-players.ts new file mode 100644 index 0000000..1e2d339 --- /dev/null +++ b/ledger/triggers/trigger-for-several-players.ts @@ -0,0 +1,6 @@ +// ledger: step 13, refused: a map of our own keeps no trigger() or hyperTriggers() records yet; this script has 1 +// A trigger for several players at once: `trigger([P1, P2], …)`. +trigger([P1, P2], [elapsedTime(">=", 10)], [displayText("Ten seconds in.")]); +program(() => { + print("started"); +}); diff --git a/ledger/triggers/trigger-nested-lists.ts b/ledger/triggers/trigger-nested-lists.ts new file mode 100644 index 0000000..b3f3dc0 --- /dev/null +++ b/ledger/triggers/trigger-nested-lists.ts @@ -0,0 +1,8 @@ +// ledger: step 13, refused: a map of our own keeps no trigger() or hyperTriggers() records yet; this script has 1 +// A trigger's conditions and actions as nested lists, `null` entries left out: what a helper puts together. +const hard: boolean = false; +const reward = [setResources(P1, "add", 100, "ore"), hard ? setResources(P1, "subtract", 50, "gas") : null]; +trigger(P1, [always(), [switchIs(switches.Switch1, "set")]], [reward, displayText("Bonus.")]); +program(() => { + print("started"); +}); diff --git a/ledger/triggers/trigger-preserved-option.ts b/ledger/triggers/trigger-preserved-option.ts new file mode 100644 index 0000000..ad08d39 --- /dev/null +++ b/ledger/triggers/trigger-preserved-option.ts @@ -0,0 +1,6 @@ +// ledger: step 13, refused: a map of our own keeps no trigger() or hyperTriggers() records yet; this script has 1 +// A trigger that runs again every time its conditions hold: `{ preserve: true }`. +trigger(P1, [always()], [setResources(P1, "add", 1, "ore")], { preserve: true }); +program(() => { + print("started"); +}); diff --git a/ledger/triggers/trigger.ts b/ledger/triggers/trigger.ts new file mode 100644 index 0000000..4debaf3 --- /dev/null +++ b/ledger/triggers/trigger.ts @@ -0,0 +1,6 @@ +// ledger: step 13, refused: a map of our own keeps no trigger() or hyperTriggers() records yet; this script has 1 +// A trigger of the map's own, beside a program: `trigger(players, conditions, actions)`. +trigger(P1, [bring(P1, units.TerranMarine, locations.Anywhere, ">=", 1)], [displayText("A Marine is here."), victory()]); +program(() => { + print("started"); +}); diff --git a/ledger/units/all-units-for-of.ts b/ledger/units/all-units-for-of.ts new file mode 100644 index 0000000..d2bbbe1 --- /dev/null +++ b/ledger/units/all-units-for-of.ts @@ -0,0 +1,7 @@ +// ledger: step 10, refused: a loop over units +// A loop over every unit on the map: `for (const u of allUnits())`. +program(() => { + let n = 0; + for (const u of allUnits()) n += 1; + print(`${n}`); +}); diff --git a/ledger/units/on-created-at-the-top.ts b/ledger/units/on-created-at-the-top.ts new file mode 100644 index 0000000..1a1d740 --- /dev/null +++ b/ledger/units/on-created-at-the-top.ts @@ -0,0 +1,5 @@ +// ledger: step 11, refused: an event handled (on.created) +// A listener written at the top of a script, not in a program: it listens from the start of the game. +on.created(units.ZergZergling, (u) => { + u.energy = 50; +}); diff --git a/ledger/units/on-created-filter.ts b/ledger/units/on-created-filter.ts new file mode 100644 index 0000000..a4c80fa --- /dev/null +++ b/ledger/units/on-created-filter.ts @@ -0,0 +1,7 @@ +// ledger: step 11, refused: an event handled (on.created) +// The units that come onto the map that a filter matches: `on.created({ owner: P2 }, (u) => …)`. +program(async () => { + on.created({ owner: P2 }, (u) => { + u.invincible = true; + }); +}); diff --git a/ledger/units/on-created-type.ts b/ledger/units/on-created-type.ts new file mode 100644 index 0000000..414cbc3 --- /dev/null +++ b/ledger/units/on-created-type.ts @@ -0,0 +1,7 @@ +// ledger: step 11, refused: an event handled (on.created) +// The units of one type that come onto the map: `on.created(units.ZergZergling, (u) => …)`. +program(async () => { + on.created(units.ZergZergling, (u) => { + u.hp = 10; + }); +}); diff --git a/ledger/units/on-created.ts b/ledger/units/on-created.ts new file mode 100644 index 0000000..f9c6914 --- /dev/null +++ b/ledger/units/on-created.ts @@ -0,0 +1,7 @@ +// ledger: step 11, refused: an event handled (on.created) +// Every unit that comes onto the map, handed to a function in the next frame: `on.created((u) => …)`. +program(async () => { + on.created((u) => { + u.energy = 50; + }); +}); diff --git a/ledger/units/on-died-filter.ts b/ledger/units/on-died-filter.ts new file mode 100644 index 0000000..45f7d29 --- /dev/null +++ b/ledger/units/on-died-filter.ts @@ -0,0 +1,7 @@ +// ledger: step 11, refused: an event handled (on.died) +// The units that die that a filter matches, as they died: `on.died({ owner: P8 }, (u) => …)`. +program(async () => { + on.died({ owner: P8 }, (u) => { + if (u.type == units.ZergZergling) print("a zergling"); + }); +}); diff --git a/ledger/units/on-died.ts b/ledger/units/on-died.ts new file mode 100644 index 0000000..ee01743 --- /dev/null +++ b/ledger/units/on-died.ts @@ -0,0 +1,9 @@ +// ledger: step 11, refused: an event handled (on.died) +// Every unit that dies or is removed, handed to a function as it was: `on.died((u) => …)`. +program(async () => { + let n = 0; + on.died((u) => { + n += 1; + print(`${n}`); + }); +}); diff --git a/ledger/units/on-event-async.ts b/ledger/units/on-event-async.ts new file mode 100644 index 0000000..200f59b --- /dev/null +++ b/ledger/units/on-event-async.ts @@ -0,0 +1,8 @@ +// ledger: step 11, refused: an event handled (on.died) +// An event's function that waits, started as a task each time: `on.died(…, async (u) => { await sleep(…); … })`. +program(async () => { + on.died({ owner: P8 }, async (u) => { + await sleep(seconds(2)); + print("back"); + }, { max: 4 }); +}); diff --git a/ledger/units/on-event-stopped.ts b/ledger/units/on-event-stopped.ts new file mode 100644 index 0000000..f2d145b --- /dev/null +++ b/ledger/units/on-event-stopped.ts @@ -0,0 +1,9 @@ +// ledger: step 11, refused: an event handled (on.created) +// An event's listening stopped by the task it gives back: `const t = on.created(…); t.cancel()`. +program(async () => { + const t = on.created((u) => { + u.kill(); + }); + await sleep(seconds(10)); + t.cancel(); +}); diff --git a/ledger/units/on-killed-by.ts b/ledger/units/on-killed-by.ts new file mode 100644 index 0000000..f1c1e9b --- /dev/null +++ b/ledger/units/on-killed-by.ts @@ -0,0 +1,7 @@ +// ledger: step 11, refused: an event handled (on.killed) +// The units one player kills of another's: `on.killed({ owner: P8, by: P1 }, (u, by) => …)`. +program(async () => { + on.killed({ owner: P8, by: P1 }, (u, by) => { + setResources(by, "add", 25, "ore"); + }); +}); diff --git a/ledger/units/on-killed.ts b/ledger/units/on-killed.ts new file mode 100644 index 0000000..79d445b --- /dev/null +++ b/ledger/units/on-killed.ts @@ -0,0 +1,7 @@ +// ledger: step 11, refused: an event handled (on.killed) +// Every unit killed in a fight, with the player whose attack killed it: `on.killed((u, by) => …)`. +program(async () => { + on.killed((u, by) => { + if (by == P1) print("P1 got one"); + }); +}); diff --git a/ledger/units/on-morphed-type.ts b/ledger/units/on-morphed-type.ts new file mode 100644 index 0000000..e21b9a5 --- /dev/null +++ b/ledger/units/on-morphed-type.ts @@ -0,0 +1,7 @@ +// ledger: step 11, refused: an event handled (on.morphed) +// The units that turn into one type: `on.morphed(units.TerranSiegeTankSiegeMode, (u) => …)`. +program(async () => { + on.morphed(units.TerranSiegeTankSiegeMode, (u) => { + print("sieged"); + }); +}); diff --git a/ledger/units/on-morphed.ts b/ledger/units/on-morphed.ts new file mode 100644 index 0000000..2edc4fa --- /dev/null +++ b/ledger/units/on-morphed.ts @@ -0,0 +1,7 @@ +// ledger: step 11, refused: an event handled (on.morphed) +// A unit that turns into another type and stays the same unit, with the type it was: `on.morphed((u, was) => …)`. +program(async () => { + on.morphed((u, was) => { + if (was == units.ZergLarva) print("hatched"); + }); +}); diff --git a/ledger/units/on-order-of-one.ts b/ledger/units/on-order-of-one.ts new file mode 100644 index 0000000..77ff87f --- /dev/null +++ b/ledger/units/on-order-of-one.ts @@ -0,0 +1,7 @@ +// ledger: step 11, refused: an event handled (on.order) +// The units of a type that take up one order: `on.order(units.TerranMarine, orders.Move, (u) => …)`. +program(async () => { + on.order(units.TerranMarine, orders.Move, (u) => { + u.stim = 50; + }); +}); diff --git a/ledger/units/on-order.ts b/ledger/units/on-order.ts new file mode 100644 index 0000000..a474f71 --- /dev/null +++ b/ledger/units/on-order.ts @@ -0,0 +1,7 @@ +// ledger: step 11, refused: an event handled (on.order) +// Every unit that takes up a new order, with the order and the one it had: `on.order((u, order, was) => …)`. +program(async () => { + on.order((u, order, was) => { + if (order == orders.Move) print("moving"); + }); +}); diff --git a/ledger/units/pick-first-filter.ts b/ledger/units/pick-first-filter.ts new file mode 100644 index 0000000..2426b70 --- /dev/null +++ b/ledger/units/pick-first-filter.ts @@ -0,0 +1,5 @@ +// ledger: step 10, refused: a unit's verb (kill) +// The first unit a filter matches: `first({ owner: P2, type: units.ZergZergling })`. +program(() => { + first({ owner: P2, type: units.ZergZergling })?.kill(); +}); diff --git a/ledger/units/pick-first.ts b/ledger/units/pick-first.ts new file mode 100644 index 0000000..dc89913 --- /dev/null +++ b/ledger/units/pick-first.ts @@ -0,0 +1,5 @@ +// ledger: step 10, refused: a unit's verb (kill) +// The first unit on the map, whatever it is: `first()`. +program(() => { + first()?.kill(); +}); diff --git a/ledger/units/pick-nearest-filter.ts b/ledger/units/pick-nearest-filter.ts new file mode 100644 index 0000000..a5e9811 --- /dev/null +++ b/ledger/units/pick-nearest-filter.ts @@ -0,0 +1,5 @@ +// ledger: step 10, refused: a unit's verb (kill) +// The nearest unit of any type that one player owns: `nearest(units.AnyUnit, there, { owner: P2 })`. +program(() => { + nearest(units.AnyUnit, locations.Anywhere, { owner: P2 })?.kill(); +}); diff --git a/ledger/units/pick-nearest.ts b/ledger/units/pick-nearest.ts new file mode 100644 index 0000000..6d8885c --- /dev/null +++ b/ledger/units/pick-nearest.ts @@ -0,0 +1,5 @@ +// ledger: step 10, refused: a unit's verb (kill) +// The unit of a type nearest the centre of a location: `nearest(units.TerranMarine, there)`. +program(() => { + nearest(units.TerranMarine, locations.Anywhere)?.kill(); +}); diff --git a/ledger/units/pick-random-unit-filter.ts b/ledger/units/pick-random-unit-filter.ts new file mode 100644 index 0000000..2905e05 --- /dev/null +++ b/ledger/units/pick-random-unit-filter.ts @@ -0,0 +1,5 @@ +// ledger: step 10, refused: a unit's verb (kill) +// A unit picked at random among those a filter matches: `randomUnit({ owner: P2 })`. +program(() => { + randomUnit({ owner: P2 })?.kill(); +}); diff --git a/ledger/units/pick-random-unit.ts b/ledger/units/pick-random-unit.ts new file mode 100644 index 0000000..43b202a --- /dev/null +++ b/ledger/units/pick-random-unit.ts @@ -0,0 +1,5 @@ +// ledger: step 10, refused: a unit's verb (kill) +// A unit picked at random by the game: `randomUnit()`. +program(() => { + randomUnit()?.kill(); +}); diff --git a/ledger/units/select-one.ts b/ledger/units/select-one.ts new file mode 100644 index 0000000..17407c5 --- /dev/null +++ b/ledger/units/select-one.ts @@ -0,0 +1,5 @@ +// ledger: step 10, refused: select() +// What a player has selected made one unit, as if they had clicked it: `select(P1, u)`. +program(() => { + select(P1, first({ owner: P1, type: units.TerranMarine })!); +}); diff --git a/ledger/units/select-several.ts b/ledger/units/select-several.ts new file mode 100644 index 0000000..b4a1756 --- /dev/null +++ b/ledger/units/select-several.ts @@ -0,0 +1,5 @@ +// ledger: step 10, refused: select() +// What a player has selected made several units at once: `select(P1, [a, b])`. +program(() => { + select(P1, [first({ owner: P1, type: units.TerranMarine })!, first({ owner: P1, type: units.TerranSCV })!]); +}); diff --git a/ledger/units/unit-array.ts b/ledger/units/unit-array.ts new file mode 100644 index 0000000..4f5ca37 --- /dev/null +++ b/ledger/units/unit-array.ts @@ -0,0 +1,6 @@ +// ledger: step 10, refused: a unit kept in a variable +// Units kept in a fixed-length array: `const pair: (Unit | null)[] = [a, b]`. +program(() => { + const pair: (Unit | null)[] = [first({ owner: P1 }), first({ owner: P2 })]; + for (const u of pair) if (u) u.heal(5); +}); diff --git a/ledger/units/unit-assigned.ts b/ledger/units/unit-assigned.ts new file mode 100644 index 0000000..e2d1386 --- /dev/null +++ b/ledger/units/unit-assigned.ts @@ -0,0 +1,7 @@ +// ledger: step 10, refused: a unit kept in a variable +// A unit variable given another unit: `u = first(…)`. +program(() => { + let u = first({ owner: P1 }); + if (!u) u = first({ owner: P2 }); + if (u) u.kill(); +}); diff --git a/ledger/units/unit-burrowed-read.ts b/ledger/units/unit-burrowed-read.ts new file mode 100644 index 0000000..b22a360 --- /dev/null +++ b/ledger/units/unit-burrowed-read.ts @@ -0,0 +1,5 @@ +// ledger: step 10, refused: a unit's flag burrowed +// Whether a unit is burrowed: `u.burrowed`, here straight off a pick (`first(…)`). +program(() => { + if (first({ owner: P1 })?.burrowed) print("burrowed"); +}); diff --git a/ledger/units/unit-cloaked-read.ts b/ledger/units/unit-cloaked-read.ts new file mode 100644 index 0000000..eb670e7 --- /dev/null +++ b/ledger/units/unit-cloaked-read.ts @@ -0,0 +1,5 @@ +// ledger: step 10, refused: a unit's flag cloaked +// Whether a unit is cloaked: `u.cloaked`, here straight off a pick (`first(…)`). +program(() => { + if (first({ owner: P1 })?.cloaked) print("cloaked"); +}); diff --git a/ledger/units/unit-cooldown-read.ts b/ledger/units/unit-cooldown-read.ts new file mode 100644 index 0000000..dc1909f --- /dev/null +++ b/ledger/units/unit-cooldown-read.ts @@ -0,0 +1,5 @@ +// ledger: step 10, refused: a unit's cooldown +// Frames until a unit can attack again, read: `u.cooldown`, here straight off a pick (`first(…)`). +program(() => { + print(`${first({ owner: P1 })!.cooldown}`); +}); diff --git a/ledger/units/unit-cooldown-written.ts b/ledger/units/unit-cooldown-written.ts new file mode 100644 index 0000000..6062faf --- /dev/null +++ b/ledger/units/unit-cooldown-written.ts @@ -0,0 +1,5 @@ +// ledger: step 10, refused: a unit's cooldown written +// A unit's weapon cooldown, written, holding its fire that many frames: `u.cooldown = 48`. +program(() => { + first({ owner: P1 })!.cooldown = 48; +}); diff --git a/ledger/units/unit-damage-by-a-value.ts b/ledger/units/unit-damage-by-a-value.ts new file mode 100644 index 0000000..1f9e68a --- /dev/null +++ b/ledger/units/unit-damage-by-a-value.ts @@ -0,0 +1,7 @@ +// ledger: step 10, refused: a unit's verb (damage) +// A unit damaged by an amount the program worked out: `u.damage(n)`. +program(() => { + let n = 3; + while (n < 20) n += 5; + first({ owner: P1 })?.damage(n); +}); diff --git a/ledger/units/unit-damage-percent.ts b/ledger/units/unit-damage-percent.ts new file mode 100644 index 0000000..5bc2dfe --- /dev/null +++ b/ledger/units/unit-damage-percent.ts @@ -0,0 +1,5 @@ +// ledger: step 10, refused: a unit's verb (damage) +// A unit's hit points taken down by a share of its type's most: `u.damage({ percent: 50 })`. +program(() => { + first({ owner: P1 })?.damage({ percent: 50 }); +}); diff --git a/ledger/units/unit-damage.ts b/ledger/units/unit-damage.ts new file mode 100644 index 0000000..abd4714 --- /dev/null +++ b/ledger/units/unit-damage.ts @@ -0,0 +1,5 @@ +// ledger: step 10, refused: a unit's verb (damage) +// A unit's hit points taken down by so many: `u.damage(20)`. +program(() => { + first({ owner: P1 })?.damage(20); +}); diff --git a/ledger/units/unit-energy-read.ts b/ledger/units/unit-energy-read.ts new file mode 100644 index 0000000..63344da --- /dev/null +++ b/ledger/units/unit-energy-read.ts @@ -0,0 +1,5 @@ +// ledger: step 10, refused: a unit's energy +// A unit's energy, read: `u.energy`, here straight off a pick (`first(…)`). +program(() => { + print(`${first({ owner: P1 })!.energy}`); +}); diff --git a/ledger/units/unit-energy-written.ts b/ledger/units/unit-energy-written.ts new file mode 100644 index 0000000..21cc496 --- /dev/null +++ b/ledger/units/unit-energy-written.ts @@ -0,0 +1,5 @@ +// ledger: step 10, refused: a unit's energy written +// A unit's energy, written: `u.energy = 200`. +program(() => { + first({ owner: P1 })!.energy = 200; +}); diff --git a/ledger/units/unit-ensnare-read.ts b/ledger/units/unit-ensnare-read.ts new file mode 100644 index 0000000..a977cce --- /dev/null +++ b/ledger/units/unit-ensnare-read.ts @@ -0,0 +1,5 @@ +// ledger: step 10, refused: a unit's ensnare +// Frames of Ensnare left on a unit, read: `u.ensnare`, here straight off a pick (`first(…)`). +program(() => { + print(`${first({ owner: P1 })!.ensnare}`); +}); diff --git a/ledger/units/unit-ensnare-written.ts b/ledger/units/unit-ensnare-written.ts new file mode 100644 index 0000000..7369983 --- /dev/null +++ b/ledger/units/unit-ensnare-written.ts @@ -0,0 +1,5 @@ +// ledger: step 10, refused: a unit's ensnare written +// Ensnare put on a unit by writing its frames: `u.ensnare = 100`. +program(() => { + first({ owner: P1 })!.ensnare = 100; +}); diff --git a/ledger/units/unit-give.ts b/ledger/units/unit-give.ts new file mode 100644 index 0000000..683903d --- /dev/null +++ b/ledger/units/unit-give.ts @@ -0,0 +1,5 @@ +// ledger: step 10, refused: a unit's verb (give) +// A unit handed to another player: `u.give(P2)`. +program(() => { + first({ owner: P1 })?.give(P2); +}); diff --git a/ledger/units/unit-hallucinated-read.ts b/ledger/units/unit-hallucinated-read.ts new file mode 100644 index 0000000..e7ff3be --- /dev/null +++ b/ledger/units/unit-hallucinated-read.ts @@ -0,0 +1,5 @@ +// ledger: step 10, refused: a unit's flag hallucinated +// Whether a unit is a hallucination: `u.hallucinated`, here straight off a pick (`first(…)`). +program(() => { + if (first({ owner: P1 })?.hallucinated) print("not real"); +}); diff --git a/ledger/units/unit-heal-percent.ts b/ledger/units/unit-heal-percent.ts new file mode 100644 index 0000000..b600325 --- /dev/null +++ b/ledger/units/unit-heal-percent.ts @@ -0,0 +1,5 @@ +// ledger: step 10, refused: a unit's verb (heal) +// A unit healed by a share of its type's most: `u.heal({ percent: 25 })`. +program(() => { + first({ owner: P1 })?.heal({ percent: 25 }); +}); diff --git a/ledger/units/unit-heal.ts b/ledger/units/unit-heal.ts new file mode 100644 index 0000000..9449987 --- /dev/null +++ b/ledger/units/unit-heal.ts @@ -0,0 +1,5 @@ +// ledger: step 10, refused: a unit's verb (heal) +// A unit's hit points given back, up to its type's most: `u.heal(20)`. +program(() => { + first({ owner: P1 })?.heal(20); +}); diff --git a/ledger/units/unit-hp-added-to.ts b/ledger/units/unit-hp-added-to.ts new file mode 100644 index 0000000..af1c06c --- /dev/null +++ b/ledger/units/unit-hp-added-to.ts @@ -0,0 +1,5 @@ +// ledger: step 10, refused: a unit's hp written +// A unit's hit points read and written in one: `u.hp += 10`. +program(() => { + first({ owner: P1 })!.hp += 10; +}); diff --git a/ledger/units/unit-hp-from-a-value.ts b/ledger/units/unit-hp-from-a-value.ts new file mode 100644 index 0000000..bcb09f3 --- /dev/null +++ b/ledger/units/unit-hp-from-a-value.ts @@ -0,0 +1,7 @@ +// ledger: step 10, refused: a unit's hp written +// A unit's hit points written from a value of the program: `u.hp = n`. +program(() => { + let n = 5; + while (n < 40) n += 7; + first({ owner: P1 })!.hp = n; +}); diff --git a/ledger/units/unit-hp-read.ts b/ledger/units/unit-hp-read.ts new file mode 100644 index 0000000..d4d6a54 --- /dev/null +++ b/ledger/units/unit-hp-read.ts @@ -0,0 +1,5 @@ +// ledger: step 10, refused: a unit's hp +// A unit's hit points, read: `u.hp`, here straight off a pick (`first(…)`). +program(() => { + print(`${first({ owner: P1 })!.hp}`); +}); diff --git a/ledger/units/unit-hp-written.ts b/ledger/units/unit-hp-written.ts new file mode 100644 index 0000000..5b47298 --- /dev/null +++ b/ledger/units/unit-hp-written.ts @@ -0,0 +1,5 @@ +// ledger: step 10, refused: a unit's hp written +// A unit's hit points, written: `u.hp = 10`. +program(() => { + first({ owner: P1 })!.hp = 10; +}); diff --git a/ledger/units/unit-in-a-text.ts b/ledger/units/unit-in-a-text.ts new file mode 100644 index 0000000..80604a6 --- /dev/null +++ b/ledger/units/unit-in-a-text.ts @@ -0,0 +1,6 @@ +// ledger: step 10, refused: a unit kept in a variable +// A unit's numbers in a printed text: `${u.hp} of ${u.maxHp}`. +program(() => { + const u = first({ owner: P1 }); + if (u) print(`${u.hp} of ${u.maxHp}`); +}); diff --git a/ledger/units/unit-invincible-read.ts b/ledger/units/unit-invincible-read.ts new file mode 100644 index 0000000..678c31b --- /dev/null +++ b/ledger/units/unit-invincible-read.ts @@ -0,0 +1,5 @@ +// ledger: step 10, refused: a unit's flag invincible +// Whether a unit cannot be hurt, read: `u.invincible`, here straight off a pick (`first(…)`). +program(() => { + if (first({ owner: P1 })?.invincible) print("invincible"); +}); diff --git a/ledger/units/unit-invincible-written.ts b/ledger/units/unit-invincible-written.ts new file mode 100644 index 0000000..1aac36f --- /dev/null +++ b/ledger/units/unit-invincible-written.ts @@ -0,0 +1,5 @@ +// ledger: step 10, refused: a unit's invincible written +// A unit made invincible: `u.invincible = true`. +program(() => { + first({ owner: P1 })!.invincible = true; +}); diff --git a/ledger/units/unit-irradiate-read.ts b/ledger/units/unit-irradiate-read.ts new file mode 100644 index 0000000..3b2da62 --- /dev/null +++ b/ledger/units/unit-irradiate-read.ts @@ -0,0 +1,5 @@ +// ledger: step 10, refused: a unit's irradiate +// Frames of Irradiate left on a unit, read: `u.irradiate`, here straight off a pick (`first(…)`). +program(() => { + print(`${first({ owner: P1 })!.irradiate}`); +}); diff --git a/ledger/units/unit-irradiate-written.ts b/ledger/units/unit-irradiate-written.ts new file mode 100644 index 0000000..262e0d6 --- /dev/null +++ b/ledger/units/unit-irradiate-written.ts @@ -0,0 +1,5 @@ +// ledger: step 10, refused: a unit's irradiate written +// Irradiate put on a unit by writing its frames: `u.irradiate = 100`. +program(() => { + first({ owner: P1 })!.irradiate = 100; +}); diff --git a/ledger/units/unit-kept-across-a-sleep.ts b/ledger/units/unit-kept-across-a-sleep.ts new file mode 100644 index 0000000..bc3d0b9 --- /dev/null +++ b/ledger/units/unit-kept-across-a-sleep.ts @@ -0,0 +1,8 @@ +// ledger: step 10, refused: a unit kept in a variable +// A unit kept across a sleep, checked again before it is used: gone, it reads as none. +program(() => { + const u = first({ owner: P1 }); + sleep(frames(24)); + if (u) u.heal(10); + else print("gone"); +}); diff --git a/ledger/units/unit-kept-in-a-variable.ts b/ledger/units/unit-kept-in-a-variable.ts new file mode 100644 index 0000000..49967fc --- /dev/null +++ b/ledger/units/unit-kept-in-a-variable.ts @@ -0,0 +1,6 @@ +// ledger: step 10, refused: a unit kept in a variable +// A unit kept in a variable, and told something there: `const u = first(…); if (u) u.kill()`. +program(() => { + const u = first({ owner: P1 }); + if (u) u.kill(); +}); diff --git a/ledger/units/unit-kill.ts b/ledger/units/unit-kill.ts new file mode 100644 index 0000000..0ebfd09 --- /dev/null +++ b/ledger/units/unit-kill.ts @@ -0,0 +1,5 @@ +// ledger: step 10, refused: a unit's verb (kill) +// A unit killed: `u.kill()`. +program(() => { + first({ owner: P1 })?.kill(); +}); diff --git a/ledger/units/unit-kills-read.ts b/ledger/units/unit-kills-read.ts new file mode 100644 index 0000000..45ff205 --- /dev/null +++ b/ledger/units/unit-kills-read.ts @@ -0,0 +1,5 @@ +// ledger: step 10, refused: a unit's kills +// How many units a unit has killed, read: `u.kills`, here straight off a pick (`first(…)`). +program(() => { + print(`${first({ owner: P1 })!.kills}`); +}); diff --git a/ledger/units/unit-kills-written.ts b/ledger/units/unit-kills-written.ts new file mode 100644 index 0000000..d5a9c70 --- /dev/null +++ b/ledger/units/unit-kills-written.ts @@ -0,0 +1,5 @@ +// ledger: step 10, refused: a unit's kills written +// A unit's kill count, written: `u.kills = 5`. +program(() => { + first({ owner: P1 })!.kills = 5; +}); diff --git a/ledger/units/unit-locate.ts b/ledger/units/unit-locate.ts new file mode 100644 index 0000000..efe2c0e --- /dev/null +++ b/ledger/units/unit-locate.ts @@ -0,0 +1,6 @@ +// ledger: step 10, refused: a unit's verb (locate) +// A location centred on a unit, so that an action can happen where the unit is: `u.locate(there)`. +const there = reserveLocation("where the unit is"); +program(() => { + first({ owner: P1 })?.locate(there); +}); diff --git a/ledger/units/unit-lockdown-read.ts b/ledger/units/unit-lockdown-read.ts new file mode 100644 index 0000000..66f127d --- /dev/null +++ b/ledger/units/unit-lockdown-read.ts @@ -0,0 +1,5 @@ +// ledger: step 10, refused: a unit's lockdown +// Frames of Lockdown left on a unit, read: `u.lockdown`, here straight off a pick (`first(…)`). +program(() => { + print(`${first({ owner: P1 })!.lockdown}`); +}); diff --git a/ledger/units/unit-lockdown-written.ts b/ledger/units/unit-lockdown-written.ts new file mode 100644 index 0000000..24ff0fc --- /dev/null +++ b/ledger/units/unit-lockdown-written.ts @@ -0,0 +1,5 @@ +// ledger: step 10, refused: a unit's lockdown written +// Lockdown put on a unit by writing its frames: `u.lockdown = 100`. +program(() => { + first({ owner: P1 })!.lockdown = 100; +}); diff --git a/ledger/units/unit-maelstrom-read.ts b/ledger/units/unit-maelstrom-read.ts new file mode 100644 index 0000000..3364833 --- /dev/null +++ b/ledger/units/unit-maelstrom-read.ts @@ -0,0 +1,5 @@ +// ledger: step 10, refused: a unit's maelstrom +// Frames of Maelstrom left on a unit, read: `u.maelstrom`, here straight off a pick (`first(…)`). +program(() => { + print(`${first({ owner: P1 })!.maelstrom}`); +}); diff --git a/ledger/units/unit-maelstrom-written.ts b/ledger/units/unit-maelstrom-written.ts new file mode 100644 index 0000000..d9a4471 --- /dev/null +++ b/ledger/units/unit-maelstrom-written.ts @@ -0,0 +1,5 @@ +// ledger: step 10, refused: a unit's maelstrom written +// Maelstrom put on a unit by writing its frames: `u.maelstrom = 100`. +program(() => { + first({ owner: P1 })!.maelstrom = 100; +}); diff --git a/ledger/units/unit-max-hp-read.ts b/ledger/units/unit-max-hp-read.ts new file mode 100644 index 0000000..1c7210b --- /dev/null +++ b/ledger/units/unit-max-hp-read.ts @@ -0,0 +1,5 @@ +// ledger: step 10, refused: a unit's maxHp +// A unit's type's most hit points, read: `u.maxHp`, here straight off a pick (`first(…)`). +program(() => { + print(`${first({ owner: P1 })!.maxHp}`); +}); diff --git a/ledger/units/unit-max-shields-read.ts b/ledger/units/unit-max-shields-read.ts new file mode 100644 index 0000000..95e7129 --- /dev/null +++ b/ledger/units/unit-max-shields-read.ts @@ -0,0 +1,5 @@ +// ledger: step 10, refused: a unit's maxShields +// A unit's type's most shield points, read: `u.maxShields`, here straight off a pick (`first(…)`). +program(() => { + print(`${first({ owner: P1 })!.maxShields}`); +}); diff --git a/ledger/units/unit-none.ts b/ledger/units/unit-none.ts new file mode 100644 index 0000000..6d7b5bf --- /dev/null +++ b/ledger/units/unit-none.ts @@ -0,0 +1,5 @@ +// ledger: step 10, refused: a unit +// A pick compared with null: `first(…) == null` when it found none. +program(() => { + if (first({ owner: P2 }) == null) print("none"); +}); diff --git a/ledger/units/unit-null.ts b/ledger/units/unit-null.ts new file mode 100644 index 0000000..4c56ab8 --- /dev/null +++ b/ledger/units/unit-null.ts @@ -0,0 +1,8 @@ +// ledger: step 10, refused: a unit kept in a variable +// A variable that holds a unit or none, none to begin with: `let u: Unit | null = null`. +program(() => { + let u: Unit | null = null; + if (u) print("there"); + u = first({ owner: P1 }); + if (u) print("found"); +}); diff --git a/ledger/units/unit-order-attack.ts b/ledger/units/unit-order-attack.ts new file mode 100644 index 0000000..a1b21d4 --- /dev/null +++ b/ledger/units/unit-order-attack.ts @@ -0,0 +1,6 @@ +// ledger: step 10, refused: a unit kept in a variable +// A unit sent to attack-move, that unit alone: `u.order("attack", there)`. +program(() => { + const u = first({ owner: P1 }); + if (u) u.order("attack", locations.Anywhere); +}); diff --git a/ledger/units/unit-order-id-read.ts b/ledger/units/unit-order-id-read.ts new file mode 100644 index 0000000..250f27b --- /dev/null +++ b/ledger/units/unit-order-id-read.ts @@ -0,0 +1,5 @@ +// ledger: step 10, refused: a unit's orderId +// What a unit is doing, compared with an order: `u.orderId == orders.Move`, here straight off a pick (`first(…)`). +program(() => { + if (first({ owner: P1 })?.orderId == orders.Move) print("moving"); +}); diff --git a/ledger/units/unit-order-move.ts b/ledger/units/unit-order-move.ts new file mode 100644 index 0000000..7d01b4c --- /dev/null +++ b/ledger/units/unit-order-move.ts @@ -0,0 +1,6 @@ +// ledger: step 10, refused: a unit kept in a variable +// A unit sent somewhere by a Move order, that unit alone: `u.order("move", there)`. +program(() => { + const u = first({ owner: P1 }); + if (u) u.order("move", locations.Anywhere); +}); diff --git a/ledger/units/unit-order-patrol.ts b/ledger/units/unit-order-patrol.ts new file mode 100644 index 0000000..8ee6d8f --- /dev/null +++ b/ledger/units/unit-order-patrol.ts @@ -0,0 +1,6 @@ +// ledger: step 10, refused: a unit kept in a variable +// A unit sent to patrol, that unit alone: `u.order("patrol", there)`. +program(() => { + const u = first({ owner: P1 }); + if (u) u.order("patrol", locations.Anywhere); +}); diff --git a/ledger/units/unit-owner-read.ts b/ledger/units/unit-owner-read.ts new file mode 100644 index 0000000..d7e6c1f --- /dev/null +++ b/ledger/units/unit-owner-read.ts @@ -0,0 +1,5 @@ +// ledger: step 10, refused: a unit's owner +// Who owns a unit, read and compared with a player: `u.owner == P2`, here straight off a pick (`first(…)`). +program(() => { + if (first({ owner: P1 })?.owner == P2) print("given"); +}); diff --git a/ledger/units/unit-parameter.ts b/ledger/units/unit-parameter.ts new file mode 100644 index 0000000..6cd8a04 --- /dev/null +++ b/ledger/units/unit-parameter.ts @@ -0,0 +1,9 @@ +// ledger: step 10, refused: a unit kept in a variable +// A function that takes a unit: `function hurt(u: Unit, n: number)`. +function hurt(u: Unit, n: number) { + u.damage(n); +} +program(() => { + const u = first({ owner: P1 }); + if (u) hurt(u, 10); +}); diff --git a/ledger/units/unit-plague-read.ts b/ledger/units/unit-plague-read.ts new file mode 100644 index 0000000..a074d1e --- /dev/null +++ b/ledger/units/unit-plague-read.ts @@ -0,0 +1,5 @@ +// ledger: step 10, refused: a unit's plague +// Frames of Plague left on a unit, read: `u.plague`, here straight off a pick (`first(…)`). +program(() => { + print(`${first({ owner: P1 })!.plague}`); +}); diff --git a/ledger/units/unit-plague-written.ts b/ledger/units/unit-plague-written.ts new file mode 100644 index 0000000..0fd3e4f --- /dev/null +++ b/ledger/units/unit-plague-written.ts @@ -0,0 +1,5 @@ +// ledger: step 10, refused: a unit's plague written +// Plague put on a unit by writing its frames: `u.plague = 100`. +program(() => { + first({ owner: P1 })!.plague = 100; +}); diff --git a/ledger/units/unit-remove.ts b/ledger/units/unit-remove.ts new file mode 100644 index 0000000..c0ec1bd --- /dev/null +++ b/ledger/units/unit-remove.ts @@ -0,0 +1,5 @@ +// ledger: step 10, refused: a unit's verb (remove) +// A unit taken off the map without a death: `u.remove()`. +program(() => { + first({ owner: P1 })?.remove(); +}); diff --git a/ledger/units/unit-resources-read.ts b/ledger/units/unit-resources-read.ts new file mode 100644 index 0000000..548ab5e --- /dev/null +++ b/ledger/units/unit-resources-read.ts @@ -0,0 +1,5 @@ +// ledger: step 10, refused: a unit's resources +// What a mineral field or a geyser still holds, read: `u.resources`, here straight off a pick (`first(…)`). +program(() => { + print(`${first({ owner: P1 })!.resources}`); +}); diff --git a/ledger/units/unit-resources-written.ts b/ledger/units/unit-resources-written.ts new file mode 100644 index 0000000..d060799 --- /dev/null +++ b/ledger/units/unit-resources-written.ts @@ -0,0 +1,5 @@ +// ledger: step 10, refused: a unit's resources written +// What a mineral field holds, written: `u.resources = 5000`. +program(() => { + first({ type: units.MineralFieldType1 })!.resources = 5000; +}); diff --git a/ledger/units/unit-result.ts b/ledger/units/unit-result.ts new file mode 100644 index 0000000..28df85c --- /dev/null +++ b/ledger/units/unit-result.ts @@ -0,0 +1,8 @@ +// ledger: step 10, refused: a unit's verb (kill) +// A function that gives back a unit or none: `function pick(): Unit | null`. +function pick(): Unit | null { + return first({ owner: P2, type: units.ZergZergling }); +} +program(() => { + pick()?.kill(); +}); diff --git a/ledger/units/unit-same.ts b/ledger/units/unit-same.ts new file mode 100644 index 0000000..9d93a54 --- /dev/null +++ b/ledger/units/unit-same.ts @@ -0,0 +1,5 @@ +// ledger: step 10, refused: two units compared +// Two units compared: `a == b` when both name the same unit of the game. +program(() => { + if (first({ owner: P1 }) == nearest(units.AnyUnit, locations.Anywhere, { owner: P1 })) print("the same"); +}); diff --git a/ledger/units/unit-set-every.ts b/ledger/units/unit-set-every.ts new file mode 100644 index 0000000..bdc3ff8 --- /dev/null +++ b/ledger/units/unit-set-every.ts @@ -0,0 +1,5 @@ +// ledger: step 10, refused: a loop over units +// Whether every unit of a set fits: `unitsOf(P1).every((u) => u.hp == u.maxHp)`. +program(() => { + if (unitsOf(P1).every((u) => u.hp == u.maxHp)) print("all well"); +}); diff --git a/ledger/units/unit-set-filter.ts b/ledger/units/unit-set-filter.ts new file mode 100644 index 0000000..1e3460e --- /dev/null +++ b/ledger/units/unit-set-filter.ts @@ -0,0 +1,6 @@ +// ledger: step 10, refused: an array that grows +// The units of a set that fit, kept in an array: `unitsOf(P1).filter((u) => u.hp < 20)`. +program(() => { + const weak = unitsOf(P1).filter((u) => u.hp < 20); + for (const u of weak) if (u) u.heal(10); +}); diff --git a/ledger/units/unit-set-find.ts b/ledger/units/unit-set-find.ts new file mode 100644 index 0000000..cda61df --- /dev/null +++ b/ledger/units/unit-set-find.ts @@ -0,0 +1,6 @@ +// ledger: step 10, refused: a unit kept in a variable +// The first unit of a set that fits, or none: `unitsOf(P1).find((u) => u.energy >= 75)`. +program(() => { + const caster = unitsOf(P1).find((u) => u.energy >= 75); + if (caster) caster.energy = 0; +}); diff --git a/ledger/units/unit-set-for-each.ts b/ledger/units/unit-set-for-each.ts new file mode 100644 index 0000000..f522e51 --- /dev/null +++ b/ledger/units/unit-set-for-each.ts @@ -0,0 +1,5 @@ +// ledger: step 10, refused: a loop over units +// Every unit of a set handed to a function: `unitsOf(P1).forEach((u) => u.heal(10))`. +program(() => { + unitsOf(P1).forEach((u) => u.heal(10)); +}); diff --git a/ledger/units/unit-set-map.ts b/ledger/units/unit-set-map.ts new file mode 100644 index 0000000..68f1c2f --- /dev/null +++ b/ledger/units/unit-set-map.ts @@ -0,0 +1,8 @@ +// ledger: step 10, refused: an array that grows +// A number from each unit of a set, in an array: `unitsOf(P1).map((u) => u.hp)`. +program(() => { + const hps = unitsOf(P1).map((u) => u.hp); + let total = 0; + for (const h of hps) total += h; + print(`${total}`); +}); diff --git a/ledger/units/unit-set-reduce.ts b/ledger/units/unit-set-reduce.ts new file mode 100644 index 0000000..24940c7 --- /dev/null +++ b/ledger/units/unit-set-reduce.ts @@ -0,0 +1,6 @@ +// ledger: step 10, refused: a loop over units +// A set of units folded into one number: `unitsOf(P1).reduce((sum, u) => sum + u.hp, 0)`. +program(() => { + const total = unitsOf(P1).reduce((sum, u) => sum + u.hp, 0); + print(`${total}`); +}); diff --git a/ledger/units/unit-set-some.ts b/ledger/units/unit-set-some.ts new file mode 100644 index 0000000..57740f6 --- /dev/null +++ b/ledger/units/unit-set-some.ts @@ -0,0 +1,5 @@ +// ledger: step 10, refused: a loop over units +// Whether any unit of a set fits: `unitsOf(P1).some((u) => u.hp < 20)`. +program(() => { + if (unitsOf(P1).some((u) => u.hp < 20)) print("one is hurt"); +}); diff --git a/ledger/units/unit-shields-read.ts b/ledger/units/unit-shields-read.ts new file mode 100644 index 0000000..ae19b0b --- /dev/null +++ b/ledger/units/unit-shields-read.ts @@ -0,0 +1,5 @@ +// ledger: step 10, refused: a unit's shields +// A unit's shield points, read: `u.shields`, here straight off a pick (`first(…)`). +program(() => { + print(`${first({ owner: P1 })!.shields}`); +}); diff --git a/ledger/units/unit-shields-written.ts b/ledger/units/unit-shields-written.ts new file mode 100644 index 0000000..f71fc03 --- /dev/null +++ b/ledger/units/unit-shields-written.ts @@ -0,0 +1,5 @@ +// ledger: step 10, refused: a unit's shields written +// A unit's shield points, written: `u.shields = 0`. +program(() => { + first({ owner: P1 })!.shields = 0; +}); diff --git a/ledger/units/unit-stasis-read.ts b/ledger/units/unit-stasis-read.ts new file mode 100644 index 0000000..510e044 --- /dev/null +++ b/ledger/units/unit-stasis-read.ts @@ -0,0 +1,5 @@ +// ledger: step 10, refused: a unit's stasis +// Frames of Stasis Field left on a unit, read: `u.stasis`, here straight off a pick (`first(…)`). +program(() => { + print(`${first({ owner: P1 })!.stasis}`); +}); diff --git a/ledger/units/unit-stasis-written.ts b/ledger/units/unit-stasis-written.ts new file mode 100644 index 0000000..dbce16a --- /dev/null +++ b/ledger/units/unit-stasis-written.ts @@ -0,0 +1,5 @@ +// ledger: step 10, refused: a unit's stasis written +// A unit held in Stasis Field by writing its frames: `u.stasis = 100`. +program(() => { + first({ owner: P1 })!.stasis = 100; +}); diff --git a/ledger/units/unit-stim-read.ts b/ledger/units/unit-stim-read.ts new file mode 100644 index 0000000..5d16767 --- /dev/null +++ b/ledger/units/unit-stim-read.ts @@ -0,0 +1,5 @@ +// ledger: step 10, refused: a unit's stim +// Frames of stim left on a unit, read: `u.stim`, here straight off a pick (`first(…)`). +program(() => { + print(`${first({ owner: P1 })!.stim}`); +}); diff --git a/ledger/units/unit-stim-written.ts b/ledger/units/unit-stim-written.ts new file mode 100644 index 0000000..06ef939 --- /dev/null +++ b/ledger/units/unit-stim-written.ts @@ -0,0 +1,5 @@ +// ledger: step 10, refused: a unit's stim written +// Stim started on a unit by writing its frames: `u.stim = 100`. +program(() => { + first({ owner: P1 })!.stim = 100; +}); diff --git a/ledger/units/unit-there.ts b/ledger/units/unit-there.ts new file mode 100644 index 0000000..73e62fa --- /dev/null +++ b/ledger/units/unit-there.ts @@ -0,0 +1,5 @@ +// ledger: step 10, refused: a unit +// Whether a pick found a unit that is still on the map: `if (first(…))`. +program(() => { + if (first({ owner: P1, type: units.TerranMarine })) print("a marine"); +}); diff --git a/ledger/units/unit-type-read.ts b/ledger/units/unit-type-read.ts new file mode 100644 index 0000000..0963dac --- /dev/null +++ b/ledger/units/unit-type-read.ts @@ -0,0 +1,5 @@ +// ledger: step 10, refused: a unit's type +// What a unit is, compared with a type: `u.type == units.TerranMarine`, here straight off a pick (`first(…)`). +program(() => { + if (first({ owner: P1 })?.type == units.TerranMarine) print("a marine"); +}); diff --git a/ledger/units/unit-under-attack-read.ts b/ledger/units/unit-under-attack-read.ts new file mode 100644 index 0000000..4e29f51 --- /dev/null +++ b/ledger/units/unit-under-attack-read.ts @@ -0,0 +1,5 @@ +// ledger: step 10, refused: a unit's flag underAttack +// Whether something hit a unit in the last second or so: `u.underAttack`, here straight off a pick (`first(…)`). +program(() => { + if (first({ owner: P1 })?.underAttack) print("hit"); +}); diff --git a/ledger/units/unit-verb-if-there.ts b/ledger/units/unit-verb-if-there.ts new file mode 100644 index 0000000..2261775 --- /dev/null +++ b/ledger/units/unit-verb-if-there.ts @@ -0,0 +1,6 @@ +// ledger: step 10, refused: a unit kept in a variable +// A verb on a unit kept in a variable that may be none: `u?.kill()`. +program(() => { + const u = first({ owner: P1 }); + u?.kill(); +}); diff --git a/ledger/units/unit-x-read.ts b/ledger/units/unit-x-read.ts new file mode 100644 index 0000000..3c75c6a --- /dev/null +++ b/ledger/units/unit-x-read.ts @@ -0,0 +1,5 @@ +// ledger: step 10, refused: a unit's x +// Where a unit is across the map, in pixels: `u.x`, here straight off a pick (`first(…)`). +program(() => { + print(`${first({ owner: P1 })!.x}`); +}); diff --git a/ledger/units/unit-y-read.ts b/ledger/units/unit-y-read.ts new file mode 100644 index 0000000..a66fb29 --- /dev/null +++ b/ledger/units/unit-y-read.ts @@ -0,0 +1,5 @@ +// ledger: step 10, refused: a unit's y +// Where a unit is down the map, in pixels: `u.y`, here straight off a pick (`first(…)`). +program(() => { + print(`${first({ owner: P1 })!.y}`); +}); diff --git a/ledger/units/units-at-for-of.ts b/ledger/units/units-at-for-of.ts new file mode 100644 index 0000000..82c08fe --- /dev/null +++ b/ledger/units/units-at-for-of.ts @@ -0,0 +1,7 @@ +// ledger: step 10, refused: a loop over units +// A loop over the units at a location: `for (const u of unitsAt(…))`. +program(() => { + let n = 0; + for (const u of unitsAt(locations.Anywhere)) if (u.hp > 0) n += 1; + print(`${n}`); +}); diff --git a/ledger/units/units-filter-all-three.ts b/ledger/units/units-filter-all-three.ts new file mode 100644 index 0000000..f5685fa --- /dev/null +++ b/ledger/units/units-filter-all-three.ts @@ -0,0 +1,8 @@ +// ledger: step 10, refused: a loop over units +// A loop over a player's units of one type inside a location: `unitsAt(there, { owner: P1, type: units.TerranMarine })`. +const there = reserveLocation("where to count"); +program(() => { + let n = 0; + for (const u of unitsAt(there, { owner: P1, type: units.TerranMarine })) n += 1; + print(`${n}`); +}); diff --git a/ledger/units/units-filter-at.ts b/ledger/units/units-filter-at.ts new file mode 100644 index 0000000..5001be3 --- /dev/null +++ b/ledger/units/units-filter-at.ts @@ -0,0 +1,8 @@ +// ledger: step 10, refused: a loop over units +// A loop over the units inside a location by the filter's `at`: `allUnits({ at: there })`. +const there = reserveLocation("where to count"); +program(() => { + let n = 0; + for (const u of allUnits({ at: there })) n += 1; + print(`${n}`); +}); diff --git a/ledger/units/units-filter-buildings.ts b/ledger/units/units-filter-buildings.ts new file mode 100644 index 0000000..de27087 --- /dev/null +++ b/ledger/units/units-filter-buildings.ts @@ -0,0 +1,7 @@ +// ledger: step 10, refused: a loop over units +// A loop over a player's buildings: `unitsOf(P1, { type: units.Buildings })`. +program(() => { + let n = 0; + for (const u of unitsOf(P1, { type: units.Buildings })) n += 1; + print(`${n}`); +}); diff --git a/ledger/units/units-filter-factories.ts b/ledger/units/units-filter-factories.ts new file mode 100644 index 0000000..a830252 --- /dev/null +++ b/ledger/units/units-filter-factories.ts @@ -0,0 +1,7 @@ +// ledger: step 10, refused: a loop over units +// A loop over a player's factories: `unitsOf(P1, { type: units.Factories })`. +program(() => { + let n = 0; + for (const u of unitsOf(P1, { type: units.Factories })) n += 1; + print(`${n}`); +}); diff --git a/ledger/units/units-filter-men.ts b/ledger/units/units-filter-men.ts new file mode 100644 index 0000000..09f158c --- /dev/null +++ b/ledger/units/units-filter-men.ts @@ -0,0 +1,7 @@ +// ledger: step 10, refused: a loop over units +// A loop over a group of types, the game's own: `unitsOf(P1, { type: units.Men })`. +program(() => { + let n = 0; + for (const u of unitsOf(P1, { type: units.Men })) n += 1; + print(`${n}`); +}); diff --git a/ledger/units/units-filter-owner.ts b/ledger/units/units-filter-owner.ts new file mode 100644 index 0000000..217a607 --- /dev/null +++ b/ledger/units/units-filter-owner.ts @@ -0,0 +1,7 @@ +// ledger: step 10, refused: a loop over units +// A loop over one player's units by the filter's owner: `allUnits({ owner: P2 })`. +program(() => { + let n = 0; + for (const u of allUnits({ owner: P2 })) n += 1; + print(`${n}`); +}); diff --git a/ledger/units/units-filter-type.ts b/ledger/units/units-filter-type.ts new file mode 100644 index 0000000..6ec70f4 --- /dev/null +++ b/ledger/units/units-filter-type.ts @@ -0,0 +1,7 @@ +// ledger: step 10, refused: a loop over units +// A loop over the units of one type: `allUnits({ type: units.TerranMarine })`. +program(() => { + let n = 0; + for (const u of allUnits({ type: units.TerranMarine })) n += 1; + print(`${n}`); +}); diff --git a/ledger/units/units-loop-break.ts b/ledger/units/units-loop-break.ts new file mode 100644 index 0000000..4fc8dd5 --- /dev/null +++ b/ledger/units/units-loop-break.ts @@ -0,0 +1,12 @@ +// ledger: step 10, refused: a loop over units +// A loop over units left early: `break` at the first unit that is hurt. +program(() => { + let found = false; + for (const u of unitsOf(P1)) { + if (u.hp < u.maxHp) { + found = true; + break; + } + } + if (found) print("hurt"); +}); diff --git a/ledger/units/units-loop-continue.ts b/ledger/units/units-loop-continue.ts new file mode 100644 index 0000000..d498c7d --- /dev/null +++ b/ledger/units/units-loop-continue.ts @@ -0,0 +1,10 @@ +// ledger: step 10, refused: a loop over units +// A loop over units that passes some by: `continue` for a burrowed unit. +program(() => { + let n = 0; + for (const u of allUnits()) { + if (u.burrowed) continue; + n += 1; + } + print(`${n}`); +}); diff --git a/ledger/units/units-loop-writes.ts b/ledger/units/units-loop-writes.ts new file mode 100644 index 0000000..982e9e5 --- /dev/null +++ b/ledger/units/units-loop-writes.ts @@ -0,0 +1,5 @@ +// ledger: step 10, refused: a loop over units +// A loop over units that writes to each: `for (const u of unitsOf(P2)) u.hp = u.maxHp / 2`. +program(() => { + for (const u of unitsOf(P2)) u.hp = u.maxHp / 2; +}); diff --git a/ledger/units/units-of-current-player.ts b/ledger/units/units-of-current-player.ts new file mode 100644 index 0000000..594f98a --- /dev/null +++ b/ledger/units/units-of-current-player.ts @@ -0,0 +1,7 @@ +// ledger: step 10, refused: a loop over units +// A loop over the units of the player the program runs as: `unitsOf(CurrentPlayer)`. +program(() => { + let n = 0; + for (const u of unitsOf(CurrentPlayer)) n += 1; + print(`${n}`); +}, { owner: P2 }); diff --git a/ledger/units/units-of-for-of.ts b/ledger/units/units-of-for-of.ts new file mode 100644 index 0000000..5d9fd0c --- /dev/null +++ b/ledger/units/units-of-for-of.ts @@ -0,0 +1,7 @@ +// ledger: step 10, refused: a loop over units +// A loop over a player's units: `for (const u of unitsOf(P1))`. +program(() => { + let n = 0; + for (const u of unitsOf(P1)) n += 1; + print(`${n}`); +}); diff --git a/ledger/units/units-of-neutral.ts b/ledger/units/units-of-neutral.ts new file mode 100644 index 0000000..e6d6bce --- /dev/null +++ b/ledger/units/units-of-neutral.ts @@ -0,0 +1,7 @@ +// ledger: step 10, refused: a loop over units +// A loop over the neutral player's units, the critters and the mineral fields: `unitsOf(P12)`. +program(() => { + let n = 0; + for (const u of unitsOf(P12)) n += 1; + print(`${n}`); +}); diff --git a/scripts/ledger.mts b/scripts/ledger.mts new file mode 100644 index 0000000..508012b --- /dev/null +++ b/scripts/ledger.mts @@ -0,0 +1,66 @@ +/** + * The ledger (`ledger/`, `tests/ledger.ts`) by step: how much of each step a map of our own takes, and what it still + * refuses, with the reason it gives. + * + * npx tsx scripts/ledger.mts the table, step by step + * npx tsx scripts/ledger.mts --write each script's first line made what a map of our own makes of it now + * npx tsx scripts/ledger.mts --check those scripts alone: compiled, and whether their first lines hold + * npx tsx scripts/ledger.mts --missing kinds of the IR and names of the library no script reaches yet + */ +import { readFileSync, writeFileSync } from "node:fs"; +import { join, relative, resolve } from "node:path"; +import { LEDGER_DIR, agrees, apiNames, headerFor, irKeys, irKinds, outcome, parseRow, readLedger, uses, type Row } from "../tests/ledger.ts"; + +const args = process.argv.slice(2); +const mode = args[0] ?? "--table"; + +if (mode === "--check") { + for (const path of args.slice(1)) { + const file = relative(LEDGER_DIR, resolve(path)).split("\\").join("/"); + const source = readFileSync(resolve(path), "utf8"); + const row = parseRow(file, source); + const o = outcome({ file, source }); + const now = o.error ? `does not compile: ${o.error}` : headerFor(typeof row === "string" ? 0 : row.step, o).replace(/^\/\/ ledger: step \d+, /, ""); + const verdict = typeof row === "string" ? row : agrees(row, o) ? "holds" : "DIFFERS"; + // (What the script compiled to, so that a feature the front end worked out when the map is built shows.) + const kinds = [...irKeys(o.ir)].filter((k) => !["block", "const", "var", "remark"].includes(k)).sort().join(" "); + console.log(`${file}\n now: ${now}\n first line: ${verdict}\n IR: ${kinds}`); + } + process.exit(0); +} + +const rows = readLedger(); + +if (mode === "--missing") { + const seen = new Set(); + for (const row of rows) irKeys(outcome(row).ir, seen); + console.log("kinds of the IR no script reaches:", irKinds().filter((k) => !seen.has(k)).join(", ") || "none"); + console.log("names no script uses:", apiNames().filter((n) => !rows.some((r) => uses(r, n))).map((n) => n.name).join(", ") || "none"); + process.exit(0); +} + +const results = rows.map((row) => ({ row, o: outcome(row) })); + +if (mode === "--write") { + let changed = 0; + for (const { row, o } of results) { + if (o.error) { console.error(`${row.file}: does not compile — ${o.error}`); continue; } + if (agrees(row, o)) continue; + const path = join(LEDGER_DIR, row.file); + const [, ...rest] = row.source.split("\n"); + writeFileSync(path, [headerFor(row.step, o), ...rest].join("\n")); + console.log(`${row.file}: ${headerFor(row.step, o).replace(/^\/\/ ledger: /, "")}`); + changed++; + } + console.log(`${changed} first lines written`); + process.exit(0); +} + +const byStep = new Map(); +for (const { row, o } of results) byStep.set(row.step, [...(byStep.get(row.step) ?? []), { row, takes: o.takes, reason: o.reason ?? o.error }]); +for (const step of [...byStep.keys()].sort((a, b) => a - b)) { + const list = byStep.get(step)!; + const taken = list.filter((x) => x.takes).length; + console.log(`\nStep ${step}: ${taken} of ${list.length} taken`); + for (const x of list.filter((y) => !y.takes)) console.log(` ${x.row.file} — ${x.row.what}\n ${x.reason}`); +} diff --git a/tests/ledger.test.ts b/tests/ledger.test.ts new file mode 100644 index 0000000..9b2712f --- /dev/null +++ b/tests/ledger.test.ts @@ -0,0 +1,53 @@ +/** + * The ledger (`ledger/`, `tests/ledger.ts`): every feature's script says what a map of our own makes of it, and the + * scripts together reach every kind of the IR and every name of the library. When a step of the way off eudplib lands, + * `npx tsx scripts/ledger.mts --write` brings the first lines up to date, and the diff is the step's account. + */ +import { beforeAll, describe, expect, it } from "vitest"; +import { agrees, apiNames, headerFor, irKeys, irKinds, outcome, readLedger, uses, type Outcome, type Row } from "./ledger"; + +const rows = readLedger(); +const outcomes = new Map(); + +/** Kinds of the IR no script compiles to, and why that is right. */ +const NOT_FROM_A_SCRIPT: Record = { + graph: "the emitter's own statement, made only when a map is built through eudplib with the emitter on", + cast: "`u32()`, `i32()` and `fixed()` settle a value's type; `compiler/numbers.ts` takes every cast out once types are known", + "intrinsic floor": "`compiler/numbers.ts` makes it the number itself, or a shift of a fixed one", + "intrinsic ceil": "`compiler/numbers.ts` makes it the number itself, or a shift of a fixed one", + "intrinsic round": "`compiler/numbers.ts` makes it the number itself, or a shift of a fixed one", + "intrinsic trunc": "`compiler/numbers.ts` makes it the number itself, or a quotient of a fixed one", +}; + +describe("the ledger", () => { + beforeAll(() => { + for (const row of rows) outcomes.set(row, outcome(row)); + }, 600_000); + + it("has a script for each feature", () => { + expect(rows.length).toBeGreaterThan(300); + }); + + it("says, for every script, what a map of our own makes of it", () => { + const wrong: string[] = []; + for (const row of rows) { + const o = outcomes.get(row)!; + if (o.error) wrong.push(`${row.file}: does not compile — ${o.error}`); + else if (!agrees(row, o)) wrong.push(`${row.file}: says "${row.takes ? "takes" : `refused: ${row.reason}`}", is "${headerFor(row.step, o).replace(/^\/\/ ledger: step \d+, /, "")}"`); + } + // (After a step lands: `npx tsx scripts/ledger.mts --write`, and read the diff.) + expect(wrong).toEqual([]); + }); + + it("reaches every kind of the IR", () => { + const seen = new Set(); + for (const o of outcomes.values()) irKeys(o.ir, seen); + const missing = irKinds().filter((k) => !seen.has(k) && !(k in NOT_FROM_A_SCRIPT)); + expect(missing).toEqual([]); + }); + + it("uses every name a script has from the library", () => { + const missing = apiNames().filter((n) => !rows.some((row) => uses(row, n))).map((n) => n.name); + expect(missing).toEqual([]); + }); +}); diff --git a/tests/ledger.ts b/tests/ledger.ts new file mode 100644 index 0000000..36c38cf --- /dev/null +++ b/tests/ledger.ts @@ -0,0 +1,273 @@ +/** + * The ledger (`ledger/`): a script for each feature of TrigScript, each with the step of the plan's way off eudplib + * that makes it a map of our own's, and whether a map of our own takes it today (docs/v4-plan.md, "The way off + * eudplib, audited"). Its first line says so: + * + * // ledger: step 4, refused: a product of two values + * // ledger: step 1, takes + * + * then a line or more of what the feature is. `tests/ledger.test.ts` holds every script to its line, and holds the + * ledger to the language: every kind of the IR, every name a script has from the library and every export of the + * standard library's modules is in some script, or the test says which is not. `scripts/ledger.mts` prints it by + * step, and writes the lines anew after a step changes what a map of our own takes. + */ +import { readdirSync, readFileSync, statSync } from "node:fs"; +import { join, relative, resolve } from "node:path"; +import ts from "typescript"; +import { compileScript } from "../compiler/compiler"; +import { generateDeclarations } from "../compiler/declarations"; +import { defaultScriptNames } from "../compiler/names"; +import { STDLIB } from "../compiler/stdlib"; +import { emitOwn } from "../compiler/backend/own"; +import { defaultLib } from "../bundle/lib.mjs"; + +export const ROOT = resolve(import.meta.dirname, ".."); +export const LEDGER_DIR = join(ROOT, "ledger"); + +export interface Row { + /** Its path under `ledger/`. */ + file: string; + step: number; + /** What its first line says: taken, or refused with these words in the reason. */ + takes: boolean; + reason?: string; + what: string; + source: string; +} + +const HEADER = /^\/\/ ledger: step (\d+), (?:(takes)|refused: (.+))$/; + +/** A script of the ledger, or what is wrong with its first lines. */ +export function parseRow(file: string, source: string): Row | string { + const lines = source.split("\n"); + const m = HEADER.exec(lines[0].trim()); + if (!m) return `${file}: the first line is not "// ledger: step N, takes" or "// ledger: step N, refused: "`; + const what: string[] = []; + for (const l of lines.slice(1)) { + if (!l.startsWith("//")) break; + what.push(l.replace(/^\/\/\s?/, "")); + } + if (!what.length) return `${file}: no line under the first says what the feature is`; + return { file, step: Number(m[1]), takes: m[2] === "takes", reason: m[3]?.trim(), what: what.join(" "), source }; +} + +export function readLedger(dir = LEDGER_DIR): Row[] { + const out: Row[] = []; + const errors: string[] = []; + const visit = (d: string) => { + for (const f of readdirSync(d).sort()) { + const p = join(d, f); + if (statSync(p).isDirectory()) visit(p); + else if (/\.tsx?$/.test(f)) { + const r = parseRow(relative(dir, p).split("\\").join("/"), readFileSync(p, "utf8")); + if (typeof r === "string") errors.push(r); + else out.push(r); + } + } + }; + visit(dir); + if (errors.length) throw new Error(errors.join("\n")); + return out; +} + +let lib: string | undefined; +let names: ReturnType | undefined; + +export interface Outcome { + /** The compiler's first error, when there is one: a script of the ledger must compile. */ + error?: string; + takes: boolean; + /** Why a map of our own refuses it (`emitOwn`). */ + reason?: string; + /** The IR the script compiled to: its programs. */ + ir: unknown[]; +} + +/** What a map of our own makes of a script: compiled as the editor compiles it, then `emitOwn`, as `build-own` does. */ +export function outcome(row: Pick): Outcome { + lib ??= defaultLib(); + names ??= defaultScriptNames(); + const r = compileScript(ts, { [row.file.endsWith(".tsx") ? "main.tsx" : "main.ts"]: row.source }, names, { lib, emitter: false }); + if (r.diagnostics.length) { + const d = r.diagnostics[0]; + return { error: `line ${d.line}: ${d.message}`, takes: false, ir: r.ir }; + } + const own = emitOwn(r); + return typeof own === "string" ? { takes: false, reason: own, ir: r.ir } : { takes: true, ir: r.ir }; +} + +/** Whether a row's first line says what the outcome is. */ +export function agrees(row: Row, o: Outcome): boolean { + if (o.error) return false; + return row.takes ? o.takes : !o.takes && !!o.reason && o.reason.includes(row.reason!); +} + +/** The first line a row should have, from its outcome (the refusal's words without its line number). */ +export function headerFor(step: number, o: Outcome): string { + return o.takes ? `// ledger: step ${step}, takes` : `// ledger: step ${step}, refused: ${(o.reason ?? "").replace(/, line \d+$/, "")}`; +} + +// ── What the ledger must hold ── + +/** + * Every kind of the IR, from `compiler/ir.ts` itself: the kinds of statements, values, tests, texts, units and the + * parts of a printed text; each operator; each function of `Math` the IR names; each source of a read. And each source + * of input, from `compiler/input.ts`. A key is how `irKeys` names what it finds in a program. + */ +export function irKinds(): string[] { + const ir = readFileSync(join(ROOT, "compiler", "ir.ts"), "utf8"); + const union = (name: string): string => { + const start = ir.indexOf(`export type ${name} =`); + if (start < 0) throw new Error(`no ${name} in compiler/ir.ts`); + return ir.slice(start, ir.indexOf(";\n", ir.indexOf("\n", start)) + 1); + }; + const keys = new Set(); + for (const u of ["Stmt", "NumExpr", "BoolExpr", "TextExpr", "UnitExpr", "TextPart"]) { + for (const m of union(u).matchAll(/kind: "(\w+)"/g)) keys.add(m[1]); + } + for (const m of union("ArithOp").matchAll(/"([^"]+)"/g)) keys.add(`binary ${m[1]}`); + const intrinsic = /kind: "intrinsic"; name: ([^;]+);/.exec(union("NumExpr")); + for (const m of intrinsic![1].matchAll(/"(\w+)"/g)) keys.add(`intrinsic ${m[1]}`); + for (const m of union("ReadSource").matchAll(/source: "(\w+)"/g)) keys.add(`read ${m[1]}`); + const input = readFileSync(join(ROOT, "compiler", "input.ts"), "utf8"); + const sources = input.slice(input.indexOf("export type InputSource ="), input.indexOf(";\n", input.indexOf("export type InputSource ="))); + for (const m of sources.matchAll(/source: "(\w+)"/g)) keys.add(`input ${m[1]}`); + return [...keys].sort(); +} + +/** What of `irKinds` a program has in it. */ +export function irKeys(x: unknown, out = new Set()): Set { + if (Array.isArray(x)) { x.forEach((y) => irKeys(y, out)); return out; } + if (!x || typeof x !== "object") return out; + const o = x as Record; + if (typeof o.kind === "string") { + out.add(o.kind); + if (o.kind === "binary") out.add(`binary ${o.op as string}`); + if (o.kind === "intrinsic") out.add(`intrinsic ${o.name as string}`); + if (o.kind === "read") out.add(`read ${(o.read as { source: string }).source}`); + if (o.kind === "input") out.add(`input ${(o.input as { source: string }).source}`); + } + for (const v of Object.values(o)) if (v && typeof v === "object") irKeys(v, out); + return out; +} + +/** The objects whose members are features of their own (`on.killed`, `ui.lines`, `memory.vision`, `local.mouse`). */ +const MEMBERS_OF = new Set(["on", "ui", "memory", "local"]); +/** The kinds of value a program holds whose members are features (`u.hp`, `set.forEach`, `task.cancel`). */ +const VALUE_TYPES = new Set(["Unit", "UnitSet", "Task"]); + +export interface ApiName { + /** As the ledger lists it: `createUnit`, `on.killed`, `Unit.hp`, `trigscript/fog.reveal`. */ + name: string; + /** A use of it in a script, its comments and strings taken out. */ + used: RegExp; + /** The module a script imports it from, for the standard library's, and its name there. */ + module?: string; + export?: string; +} + +const word = (s: string) => s.replace(/[$]/g, "\\$"); +/** A name used as itself: not the field of something else (`band.loop` is not `loop`). */ +const bare = (n: string) => new RegExp(`(?): string[] => { + const found = new Set(); + for (const m of members) { + if ((ts.isPropertySignature(m) || ts.isMethodSignature(m) || ts.isGetAccessor(m)) && m.name && ts.isIdentifier(m.name)) found.add(m.name.text); + if (ts.isVariableStatement(m)) for (const d of m.declarationList.declarations) if (ts.isIdentifier(d.name)) found.add(d.name.text); + if (ts.isFunctionDeclaration(m) && m.name) found.add(m.name.text); + } + return [...found]; + }; + const seen = new Set(); + const add = (n: ApiName) => { if (!seen.has(n.name)) { seen.add(n.name); out.push(n); } }; + for (const st of sf.statements) { + if (ts.isFunctionDeclaration(st) && st.name) add({ name: st.name.text, used: bare(st.name.text) }); + if (ts.isVariableStatement(st)) { + for (const d of st.declarationList.declarations) { + if (!ts.isIdentifier(d.name)) continue; + const n = d.name.text; + add({ name: n, used: bare(n) }); + if (MEMBERS_OF.has(n) && d.type && ts.isTypeLiteralNode(d.type)) { + for (const m of memberNames(d.type.members)) add({ name: `${n}.${m}`, used: new RegExp(`\\b${word(n)}\\s*\\.\\s*${word(m)}\\b`) }); + } + } + } + if (ts.isModuleDeclaration(st) && ts.isIdentifier(st.name)) { + const n = st.name.text; + if (n === "JSX") continue; + add({ name: n, used: bare(n) }); + if (MEMBERS_OF.has(n) && st.body && ts.isModuleBlock(st.body)) { + for (const m of memberNames(st.body.statements)) add({ name: `${n}.${m}`, used: new RegExp(`\\b${word(n)}\\s*\\.\\s*${word(m)}\\b`) }); + } + } + if (ts.isInterfaceDeclaration(st) && VALUE_TYPES.has(st.name.text)) { + for (const m of memberNames(st.members)) add({ name: `${st.name.text}.${m}`, used: new RegExp(`\\.\\s*${word(m)}\\b`) }); + } + } + for (const [mod, source] of Object.entries(STDLIB)) { + const msf = ts.createSourceFile(`${mod}.tsx`, source, ts.ScriptTarget.ES2022, true, ts.ScriptKind.TSX); + for (const st of msf.statements) { + const exported = ts.canHaveModifiers(st) && ts.getModifiers(st)?.some((m) => m.kind === ts.SyntaxKind.ExportKeyword); + if (!exported) continue; + const named: string[] = []; + if ((ts.isFunctionDeclaration(st) || ts.isClassDeclaration(st)) && st.name) named.push(st.name.text); + if (ts.isVariableStatement(st)) for (const d of st.declarationList.declarations) if (ts.isIdentifier(d.name)) named.push(d.name.text); + for (const n of named) add({ name: `trigscript/${mod}.${n}`, used: bare(n), module: `trigscript/${mod}`, export: n }); + } + } + return out.filter((n) => !n.name.split(".").some((part) => part.startsWith("__"))); +} + +/** A script with its comments and the insides of its strings taken out (a template's `${…}` kept), for `ApiName.used`. */ +export function code(source: string): string { + const sf = ts.createSourceFile("x.tsx", source, ts.ScriptTarget.ES2022, true, ts.ScriptKind.TSX); + const blank: [number, number][] = []; + const comments = (pos: number) => { + for (const c of [...(ts.getLeadingCommentRanges(source, pos) ?? []), ...(ts.getTrailingCommentRanges(source, pos) ?? [])]) blank.push([c.pos, c.end]); + }; + const visit = (n: ts.Node) => { + comments(n.getFullStart()); + comments(n.getEnd()); + if (ts.isStringLiteral(n) || ts.isNoSubstitutionTemplateLiteral(n)) blank.push([n.getStart() + 1, n.getEnd() - 1]); + // (A template's text between its `${…}`: from after the backtick or `}` to before the `${` or the closing backtick.) + else if (ts.isTemplateHead(n) || ts.isTemplateMiddle(n)) blank.push([n.getStart() + 1, n.getEnd() - 2]); + else if (ts.isTemplateTail(n)) blank.push([n.getStart() + 1, n.getEnd() - 1]); + else if (ts.isJsxText(n)) blank.push([n.getStart(), n.getEnd()]); + n.forEachChild(visit); + }; + visit(sf); + const chars = source.split(""); + for (const [a, b] of blank) for (let i = a; i < b; i++) if (chars[i] !== "\n") chars[i] = " "; + return chars.join(""); +} + +const codes = new Map(); + +/** Whether a script uses a name: in its code, and imported from the module when it is the standard library's. */ +export function uses(row: Pick, name: ApiName): boolean { + let c = codes.get(row.source); + if (c === undefined) codes.set(row.source, (c = code(row.source).replace(/^\s*import\b[^;]*;/gm, ""))); + if (!name.module) return name.used.test(c); + // (The standard library's: imported from its module — by its name, under another, or with the module or an object of + // it — and used so.) + for (const m of row.source.matchAll(/import\s*(?:\*\s*as\s+(\w+)|\{([^}]*)\})\s*from\s*["']([^"']+)["']/g)) { + if (m[3] !== name.module) continue; + if (m[1] && new RegExp(`\\b${m[1]}\\s*\\.\\s*${word(name.export!)}\\b`).test(c)) return true; + for (const spec of (m[2] ?? "").split(",").map((x) => x.trim()).filter(Boolean)) { + const [orig, local = orig] = spec.split(/\s+as\s+/).map((x) => x.trim()); + if (orig === name.export && bare(local).test(c)) return true; + if (new RegExp(`(? Date: Mon, 19 Oct 2026 23:24:11 -0600 Subject: [PATCH 6/6] Plan: the ledger as built, by step, and what writing it found Its table by step; what was fixed with it (a field read off a pick, order() in a program, music.loop, a print to the owner by slot, owners P9-P12); what is open (a function of the script that keeps a state, called in a program, worked out once; orders.* in order(); disabled() and preserve() in a program said nothing; false errors; JavaScript a program lacks); and the README's language reference, older than TrigScript 4, to be written again from the ledger at 4.0. Refusals now name what they refuse. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01T1CM9pLuTuAn3mgTEkjgVg --- docs/v4-plan.md | 87 ++++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 82 insertions(+), 5 deletions(-) diff --git a/docs/v4-plan.md b/docs/v4-plan.md index 1a4e424..7c7eff7 100644 --- a/docs/v4-plan.md +++ b/docs/v4-plan.md @@ -3849,7 +3849,10 @@ clears the map's triggers: refused now. **Why the arithmetic stayed hidden.** Most refusals read `a assign it does not lower (no reason given), line N`: `Scope.test()` drops the reasons `bool()` gives, `lowers()` and `withCells()` give none, and `why ||=` keeps an earlier reason that is not the one. `a * b`, `a / 4`, `random(6)` and `minerals(P1)` were all refused in the same -words. Step 14 asks for a refusal that names what it refuses; the ledger (below) asks for it to be right. +words. Step 14 asks for a refusal that names what it refuses; the ledger (below) asks for it to be right. Since the +ledger, a refusal names the first part of the statement that the emitter never takes (`unlowered` in `emit.ts`) and +its line — "a product of two values, line 51", "the action Set Resources, line 10", "a write of unit.sight, line 550", +"a unit's hp" — and falls back on the checks' own reason, now kept statement by statement. **Smaller things**, to fix inside the step that meets them: - A boolean `?:` is refused, with constants on both sides too (`Rewriter.cond` has no case for it). @@ -3872,10 +3875,84 @@ Bad Apple: 5, 6, 7, as the fog library. The bullets: 5 (a death count read), 9. - *Magenta.* It keeps plugin-eudplib alive. Until it builds on the own layout, a map with both could have TrigScript's program built into Magenta's eudplib build, or refuse the pair. -**The ledger, so the list stays whole.** A script for each feature — the audit's 230 are the start — each with its -step and whether a map of our own takes it, run by a test in `tests/`. It fails when a script's answer changes, and -when a kind of the IR or a name the library exports has no row. A step is done when its rows say so. Proposed as the -first thing before step 4. +**The ledger, so the list stays whole.** Built before step 4: see the next section. + +### The ledger (2026-10-19) + +`ledger/` holds a script for each feature of the language and the library — 831 of them, in folders by area — whose +first line says which step of the way off eudplib makes it a map of our own's, and whether a map of our own takes it +today, in the words of its refusal: + + // ledger: step 4, refused: a product of two values + // ledger: step 1, takes + +`tests/ledger.test.ts` compiles each one as the editor does and gives it to `emitOwn` (`compiler/backend/own.ts`, the +checks `build-own` makes, now shared); it fails when a first line no longer says what happens, and when a kind of the IR +(129, less six that never survive the front end), a name a script has from the library, a member of `on`, `ui`, +`memory`, `local`, a unit, a set of units or a task, or an export of a module of the standard library (340 names in +all) is in no script. When a step lands, `npx tsx scripts/ledger.mts --write` writes the first lines anew and the diff +is the step's account; `npx tsx scripts/ledger.mts` prints the table, `--check ` looks at a few, `--missing` +lists what no script reaches. A step is done when its scripts say so. + +| Step | Taken today | +| --- | ---: | +| 0 The start | 157 of 158 | +| 1 Calls and recursion | 33 of 33 | +| 2 Texts with numbers | 6 of 6 | +| 3 Arrays of a fixed length | 53 of 54 | +| 4 Numbers | 0 of 59 | +| 5 The game's conditions and actions | 0 of 193 | +| 6 The game's memory | 0 of 49 | +| 7 Programs, owners and players | 1 of 22 | +| 8 Texts as values | 0 of 43 | +| 9 Arrays that grow, the heap | 0 of 27 | +| 10 Units | 0 of 99 | +| 11 Tasks, `async` and errors | 0 of 44 | +| 12 Input | 0 of 31 | +| 13 The map's own triggers | 0 of 13 | + +Two steps called done are not quite: a `?:` of two booleans (step 0) and a cell read at a shifted index, `xs[a >> 1]` +(step 3 — the store there and `const i = a >> 1; xs[i]` are taken). Every script a map of our own takes was also run +from its laid-out bytes against the simulator — the numbers, arrays, texts and language scripts, and about forty edge +cases besides (signs of `>>`, `u32` printed and compared, wrap-around, reads past the ends, negative indexes): all +printed the same. The README's reference to the language predates TrigScript 4 (it says `try` and `async` are missing): +the ledger is the list it can be written again from at 4.0. + +**Found while writing it**, in the front end — both builds — unless said: + +*Fixed with the ledger:* +- A field or flag read straight off a pick was worked out when the script was built, as `undefined`: + `` print(`${first(…)?.hp}`) `` printed "undefined", `if (first(…)?.cloaked)` was nothing at all, and + `n = first(…)?.hp ?? 5` was `n = 5`. Now a read of the game's, as `u.hp` is (`tests/units.test.ts`); a `??` after + it is a compile error, as after `u?.hp` (below). +- The action `order(…)` did not compile inside a program: `on.order`'s name was taken for it, and its arguments were + never worked out (`tests/events.test.ts`). +- `music.loop(song)` never compiled — the loop showed no wait of its own (`tests/music.test.ts`). +- A map of our own refused a print to the owner named by its slot (`{ to: P1 }` in P1's program), and took a program + owned by P9 to P12, whose triggers the game does not run — refused now (to be confirmed in the game). + +*Open:* +- **Built wrong without a word:** a function written outside the program that keeps a state and is called inside it + (`` const next = counter(); while (…) print(`${next()}`) ``) is worked out once when the script is built: + 1, 1, 1 for 1, 2, 3. Kept out of the ledger until it is refused or right. +- **Built wrong without a word:** `order(…, orders.Move)` type-checks (`OrderKind | number`) and makes an Order action + of modifier 6; the action knows move, patrol and attack (0, 1, 2). +- `displayText(text, always)` made a print loses its flag — to check against the flags a print gives Display Text. +- `disabled(…)` inside a program is neither refused nor said: a disabled condition in an `if` holds, as in a trigger. + `preserve()` and `preserveTrigger()` there are dropped (every program is preserved), also without a word. +- False errors: `n++` used as a value in a loop's body ("This loop's condition never changes" — `checkSleeps` does not + see writes inside expressions or calls); a comparison passed as a boolean argument (`step(n, turns < 2)`); a `switch` + on a text known when built; `?:` between two closures; an arrow using `this` inside a method (a JavaScript exception + as the message); `first(…)?.order(…)` on a pick; `u?.hp ?? 0`; `await sleep(…).then(() => 7)`. +- `return` at the top of a program is "return outside a function", and `trigscript/cutscene`'s own example uses it (and + `first(unitsAt(…))`, which `first` does not take). +- Not in a program, against JavaScript: `~a`, `**`, `Math.pow`, `Math.sign`, `Math.log2`, `fill(v, start, end)`, + `reduce` with no first value. A `const HALF = 7 / 2` worked out when built is 3.5, a fixed number; the same `7 / 2` in + the game is 3. +- A map of our own: `displayText("known text")` is refused where `print("known text")` is taken (step 5's first); + JSX inside a program is "made while the game runs" even when it holds nothing of the program's (step 8); + `a >> 0` is refused where `a >> 1` to `a >> 31` are taken; `Math.tan` and `Math.hypot` are refused as what they + become (`Math.sin`, a product). ## Part C — the game, pushed