hascii 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/Makefile +19 -0
- package/README.md +185 -0
- package/carts/draw.yaml +69 -0
- package/carts/hello.yaml +22 -0
- package/carts/move.yaml +68 -0
- package/carts/pong.yaml +185 -0
- package/carts/rain.yaml +69 -0
- package/carts/snake.yaml +292 -0
- package/carts/tracker.yaml +1838 -0
- package/carts/wave.yaml +56 -0
- package/dist/cli.d.mts +1 -0
- package/dist/cli.mjs +6105 -0
- package/dist/index.d.mts +489 -0
- package/dist/index.mjs +3 -0
- package/dist/run-mrITeQUl.mjs +1750 -0
- package/hascii.schema.json +1023 -0
- package/package.json +39 -0
|
@@ -0,0 +1,1750 @@
|
|
|
1
|
+
import { LineCounter, isMap, isPair, isScalar, isSeq, parse, parseDocument } from "yaml";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import { spawn } from "node:child_process";
|
|
4
|
+
import * as fs from "node:fs";
|
|
5
|
+
import * as os from "node:os";
|
|
6
|
+
import * as path from "node:path";
|
|
7
|
+
|
|
8
|
+
//#region src/compiler/ansi.ts
|
|
9
|
+
const ANSI = Object.freeze({
|
|
10
|
+
RESET: "\x1B[0m",
|
|
11
|
+
BOLD: "\x1B[1m",
|
|
12
|
+
DIM: "\x1B[2m",
|
|
13
|
+
RED: "\x1B[31m",
|
|
14
|
+
GREEN: "\x1B[32m",
|
|
15
|
+
YELLOW: "\x1B[33m",
|
|
16
|
+
BLUE: "\x1B[34m",
|
|
17
|
+
MAGENTA: "\x1B[35m",
|
|
18
|
+
CYAN: "\x1B[36m",
|
|
19
|
+
BRIGHT_RED: "\x1B[91m",
|
|
20
|
+
BRIGHT_BLUE: "\x1B[94m"
|
|
21
|
+
});
|
|
22
|
+
const COLORS = Object.freeze({
|
|
23
|
+
error: `${ANSI.BOLD}${ANSI.BRIGHT_RED}`,
|
|
24
|
+
category: `${ANSI.BOLD}${ANSI.CYAN}`,
|
|
25
|
+
location: `${ANSI.BRIGHT_BLUE}`,
|
|
26
|
+
lineNumber: `${ANSI.BRIGHT_BLUE}`,
|
|
27
|
+
gutter: `${ANSI.BRIGHT_BLUE}`,
|
|
28
|
+
arrow: `${ANSI.BRIGHT_RED}`,
|
|
29
|
+
hint: `${ANSI.YELLOW}`,
|
|
30
|
+
help: `${ANSI.GREEN}`,
|
|
31
|
+
expected: `${ANSI.GREEN}`,
|
|
32
|
+
received: `${ANSI.RED}`,
|
|
33
|
+
note: `${ANSI.CYAN}`,
|
|
34
|
+
emphasis: `${ANSI.BOLD}${ANSI.MAGENTA}`
|
|
35
|
+
});
|
|
36
|
+
const BOX = Object.freeze({
|
|
37
|
+
TOP: "┌",
|
|
38
|
+
VERT: "│",
|
|
39
|
+
BOTTOM: "└",
|
|
40
|
+
HORIZ: "─",
|
|
41
|
+
CROSS: "┼"
|
|
42
|
+
});
|
|
43
|
+
function supportsColor() {
|
|
44
|
+
if (process.env.NO_COLOR !== void 0) return false;
|
|
45
|
+
if (process.env.FORCE_COLOR !== void 0) return true;
|
|
46
|
+
if (typeof process.stdout.isTTY === "boolean") return process.stdout.isTTY;
|
|
47
|
+
return false;
|
|
48
|
+
}
|
|
49
|
+
function stripAnsi(text) {
|
|
50
|
+
return text.replace(/\x1b\[[0-9;]*m/g, "");
|
|
51
|
+
}
|
|
52
|
+
function line(char, length) {
|
|
53
|
+
return char.repeat(length);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
//#endregion
|
|
57
|
+
//#region src/compiler/error-codes.ts
|
|
58
|
+
const E001 = Object.freeze({
|
|
59
|
+
code: "E001",
|
|
60
|
+
title: "invalid YAML syntax",
|
|
61
|
+
hint: "check indentation and colon placement"
|
|
62
|
+
});
|
|
63
|
+
const E101 = Object.freeze({
|
|
64
|
+
code: "E101",
|
|
65
|
+
title: "invalid type",
|
|
66
|
+
hint: "wrong type for this value"
|
|
67
|
+
});
|
|
68
|
+
const E102 = Object.freeze({
|
|
69
|
+
code: "E102",
|
|
70
|
+
title: "missing required field",
|
|
71
|
+
hint: "this field is required"
|
|
72
|
+
});
|
|
73
|
+
const E103 = Object.freeze({
|
|
74
|
+
code: "E103",
|
|
75
|
+
title: "unexpected field",
|
|
76
|
+
hint: "unknown field, maybe a typo?"
|
|
77
|
+
});
|
|
78
|
+
const E104 = Object.freeze({
|
|
79
|
+
code: "E104",
|
|
80
|
+
title: "invalid array length",
|
|
81
|
+
hint: "wrong number of arguments"
|
|
82
|
+
});
|
|
83
|
+
const E201 = Object.freeze({
|
|
84
|
+
code: "E201",
|
|
85
|
+
title: "invalid statement",
|
|
86
|
+
hint: "unknown statement"
|
|
87
|
+
});
|
|
88
|
+
const E301 = Object.freeze({
|
|
89
|
+
code: "E301",
|
|
90
|
+
title: "invalid rect arguments",
|
|
91
|
+
hint: "usage: rect: [x, y, width, height]"
|
|
92
|
+
});
|
|
93
|
+
const E302 = Object.freeze({
|
|
94
|
+
code: "E302",
|
|
95
|
+
title: "invalid strokeStyle",
|
|
96
|
+
hint: "usage: strokeStyle: light / heavy / double / round"
|
|
97
|
+
});
|
|
98
|
+
const E303 = Object.freeze({
|
|
99
|
+
code: "E303",
|
|
100
|
+
title: "invalid print arguments",
|
|
101
|
+
hint: "usage: print: [text, x, y]"
|
|
102
|
+
});
|
|
103
|
+
const E304 = Object.freeze({
|
|
104
|
+
code: "E304",
|
|
105
|
+
title: "invalid cls usage",
|
|
106
|
+
hint: "usage: cls:"
|
|
107
|
+
});
|
|
108
|
+
const E305 = Object.freeze({
|
|
109
|
+
code: "E305",
|
|
110
|
+
title: "invalid color",
|
|
111
|
+
hint: "color must be 0-15"
|
|
112
|
+
});
|
|
113
|
+
const E401 = Object.freeze({
|
|
114
|
+
code: "E401",
|
|
115
|
+
title: "invalid expression",
|
|
116
|
+
hint: "unknown expression"
|
|
117
|
+
});
|
|
118
|
+
const ERROR_CODES = Object.freeze({
|
|
119
|
+
E001,
|
|
120
|
+
E101,
|
|
121
|
+
E102,
|
|
122
|
+
E103,
|
|
123
|
+
E104,
|
|
124
|
+
E201,
|
|
125
|
+
E301,
|
|
126
|
+
E302,
|
|
127
|
+
E303,
|
|
128
|
+
E304,
|
|
129
|
+
E305,
|
|
130
|
+
E401
|
|
131
|
+
});
|
|
132
|
+
function mapZodCodeToErrorCode(zodCode, path$1) {
|
|
133
|
+
if (zodCode === "invalid_type") return E101;
|
|
134
|
+
if (zodCode === "invalid_union") {
|
|
135
|
+
const lastKey = path$1[path$1.length - 1];
|
|
136
|
+
if (lastKey === "rect") return E301;
|
|
137
|
+
if (lastKey === "strokeStyle") return E302;
|
|
138
|
+
if (lastKey === "print") return E303;
|
|
139
|
+
if (lastKey === "cls") return E304;
|
|
140
|
+
return E201;
|
|
141
|
+
}
|
|
142
|
+
if (zodCode === "too_small" || zodCode === "too_big") {
|
|
143
|
+
const lastKey = path$1[path$1.length - 1];
|
|
144
|
+
if (lastKey === "fill" || lastKey === "stroke") return E305;
|
|
145
|
+
return E104;
|
|
146
|
+
}
|
|
147
|
+
if (zodCode === "unrecognized_keys") return E103;
|
|
148
|
+
return E101;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
//#endregion
|
|
152
|
+
//#region src/core/safe-key.ts
|
|
153
|
+
const DANGEROUS_KEYS = new Set([
|
|
154
|
+
"__proto__",
|
|
155
|
+
"constructor",
|
|
156
|
+
"prototype",
|
|
157
|
+
"__defineGetter__",
|
|
158
|
+
"__defineSetter__",
|
|
159
|
+
"__lookupGetter__",
|
|
160
|
+
"__lookupSetter__"
|
|
161
|
+
]);
|
|
162
|
+
function isSafeKey(key) {
|
|
163
|
+
return !DANGEROUS_KEYS.has(key);
|
|
164
|
+
}
|
|
165
|
+
function isSafePath(path$1) {
|
|
166
|
+
return path$1.split(".").every(isSafeKey);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
//#endregion
|
|
170
|
+
//#region src/compiler/evaluate.ts
|
|
171
|
+
const MAX_DEPTH = 100;
|
|
172
|
+
const VARIABLE_PATTERN = /^[a-zA-Z_][a-zA-Z0-9_]*(\.[a-zA-Z_][a-zA-Z0-9_]*)*$/;
|
|
173
|
+
function isVariableName(str) {
|
|
174
|
+
return VARIABLE_PATTERN.test(str);
|
|
175
|
+
}
|
|
176
|
+
function getVariable(state, path$1) {
|
|
177
|
+
if (!isSafePath(path$1)) return 0;
|
|
178
|
+
const parts = path$1.split(".");
|
|
179
|
+
let current = state;
|
|
180
|
+
for (const part of parts) {
|
|
181
|
+
if (typeof current !== "object" || current === null || Array.isArray(current)) return 0;
|
|
182
|
+
current = current[part] ?? 0;
|
|
183
|
+
}
|
|
184
|
+
return current;
|
|
185
|
+
}
|
|
186
|
+
function evaluate(expr, state, depth = 0) {
|
|
187
|
+
if (depth > MAX_DEPTH) return 0;
|
|
188
|
+
if (typeof expr === "number") return expr;
|
|
189
|
+
if (typeof expr === "string") {
|
|
190
|
+
if (isVariableName(expr)) return getVariable(state, expr);
|
|
191
|
+
return expr;
|
|
192
|
+
}
|
|
193
|
+
if (Array.isArray(expr)) {
|
|
194
|
+
const d = depth + 1;
|
|
195
|
+
return Object.freeze(expr.map((item) => evaluate(item, state, d)));
|
|
196
|
+
}
|
|
197
|
+
if (typeof expr === "object" && expr !== null) {
|
|
198
|
+
const d = depth + 1;
|
|
199
|
+
if ("add" in expr) return evaluate(expr.add[0], state, d) + evaluate(expr.add[1], state, d);
|
|
200
|
+
if ("sub" in expr) return evaluate(expr.sub[0], state, d) - evaluate(expr.sub[1], state, d);
|
|
201
|
+
if ("mul" in expr) return evaluate(expr.mul[0], state, d) * evaluate(expr.mul[1], state, d);
|
|
202
|
+
if ("div" in expr) {
|
|
203
|
+
const left = evaluate(expr.div[0], state, d);
|
|
204
|
+
const right = evaluate(expr.div[1], state, d);
|
|
205
|
+
return right !== 0 ? left / right : 0;
|
|
206
|
+
}
|
|
207
|
+
if ("mod" in expr) {
|
|
208
|
+
const left = evaluate(expr.mod[0], state, d);
|
|
209
|
+
const right = evaluate(expr.mod[1], state, d);
|
|
210
|
+
return right !== 0 ? left % right : 0;
|
|
211
|
+
}
|
|
212
|
+
if ("abs" in expr) return Math.abs(evaluate(expr.abs, state, d));
|
|
213
|
+
if ("neg" in expr) return -evaluate(expr.neg, state, d);
|
|
214
|
+
if ("floor" in expr) return Math.floor(evaluate(expr.floor, state, d));
|
|
215
|
+
if ("ceil" in expr) return Math.ceil(evaluate(expr.ceil, state, d));
|
|
216
|
+
if ("round" in expr) return Math.round(evaluate(expr.round, state, d));
|
|
217
|
+
if ("sqrt" in expr) return Math.sqrt(evaluate(expr.sqrt, state, d));
|
|
218
|
+
if ("sin" in expr) return Math.sin(evaluate(expr.sin, state, d));
|
|
219
|
+
if ("cos" in expr) return Math.cos(evaluate(expr.cos, state, d));
|
|
220
|
+
if ("pow" in expr) return evaluate(expr.pow[0], state, d) ** evaluate(expr.pow[1], state, d);
|
|
221
|
+
if ("min" in expr) {
|
|
222
|
+
const left = evaluate(expr.min[0], state, d);
|
|
223
|
+
const right = evaluate(expr.min[1], state, d);
|
|
224
|
+
return Math.min(left, right);
|
|
225
|
+
}
|
|
226
|
+
if ("max" in expr) {
|
|
227
|
+
const left = evaluate(expr.max[0], state, d);
|
|
228
|
+
const right = evaluate(expr.max[1], state, d);
|
|
229
|
+
return Math.max(left, right);
|
|
230
|
+
}
|
|
231
|
+
if ("rnd" in expr) {
|
|
232
|
+
const min = evaluate(expr.rnd[0], state, d);
|
|
233
|
+
const max = evaluate(expr.rnd[1], state, d);
|
|
234
|
+
return Math.floor(Math.random() * (max - min + 1)) + min;
|
|
235
|
+
}
|
|
236
|
+
if ("eq" in expr) return evaluate(expr.eq[0], state, d) === evaluate(expr.eq[1], state, d) ? 1 : 0;
|
|
237
|
+
if ("lt" in expr) return evaluate(expr.lt[0], state, d) < evaluate(expr.lt[1], state, d) ? 1 : 0;
|
|
238
|
+
if ("lte" in expr) return evaluate(expr.lte[0], state, d) <= evaluate(expr.lte[1], state, d) ? 1 : 0;
|
|
239
|
+
if ("gt" in expr) return evaluate(expr.gt[0], state, d) > evaluate(expr.gt[1], state, d) ? 1 : 0;
|
|
240
|
+
if ("gte" in expr) return evaluate(expr.gte[0], state, d) >= evaluate(expr.gte[1], state, d) ? 1 : 0;
|
|
241
|
+
if ("and" in expr) {
|
|
242
|
+
const args = expr.and;
|
|
243
|
+
for (const arg of args) if (!evaluate(arg, state, d)) return 0;
|
|
244
|
+
return 1;
|
|
245
|
+
}
|
|
246
|
+
if ("or" in expr) {
|
|
247
|
+
const args = expr.or;
|
|
248
|
+
for (const arg of args) if (evaluate(arg, state, d)) return 1;
|
|
249
|
+
return 0;
|
|
250
|
+
}
|
|
251
|
+
if ("not" in expr) return evaluate(expr.not, state, d) ? 0 : 1;
|
|
252
|
+
if ("at" in expr) {
|
|
253
|
+
const arr = evaluate(expr.at[0], state, d);
|
|
254
|
+
const idx = evaluate(expr.at[1], state, d);
|
|
255
|
+
if (Array.isArray(arr) && Number.isInteger(idx) && idx >= 0) return arr[idx] ?? 0;
|
|
256
|
+
return 0;
|
|
257
|
+
}
|
|
258
|
+
if ("len" in expr) {
|
|
259
|
+
const arr = evaluate(expr.len, state, d);
|
|
260
|
+
if (Array.isArray(arr)) return arr.length;
|
|
261
|
+
return 0;
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
return 0;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
//#endregion
|
|
268
|
+
//#region src/compiler/diagnostic.ts
|
|
269
|
+
function categorize(zodCode, path$1) {
|
|
270
|
+
if (zodCode === "invalid_type") return "TYPE_MISMATCH";
|
|
271
|
+
if (zodCode === "invalid_union") {
|
|
272
|
+
const lastKey = path$1[path$1.length - 1];
|
|
273
|
+
if (lastKey === "rect" || lastKey === "circ" || lastKey === "print" || lastKey === "cls") return "INVALID_ARGS";
|
|
274
|
+
return "INVALID_STATEMENT";
|
|
275
|
+
}
|
|
276
|
+
if (zodCode === "too_small" || zodCode === "too_big") {
|
|
277
|
+
if (path$1[path$1.length - 1] === "cls") return "INVALID_COLOR";
|
|
278
|
+
return "INVALID_ARGS";
|
|
279
|
+
}
|
|
280
|
+
if (zodCode === "unrecognized_keys") return "UNKNOWN_FIELD";
|
|
281
|
+
return "TYPE_MISMATCH";
|
|
282
|
+
}
|
|
283
|
+
function categoryTitle(category) {
|
|
284
|
+
return {
|
|
285
|
+
TYPE_MISMATCH: "TYPE MISMATCH",
|
|
286
|
+
MISSING_FIELD: "MISSING FIELD",
|
|
287
|
+
UNKNOWN_FIELD: "UNKNOWN FIELD",
|
|
288
|
+
INVALID_SYNTAX: "INVALID SYNTAX",
|
|
289
|
+
INVALID_STATEMENT: "INVALID STATEMENT",
|
|
290
|
+
INVALID_ARGS: "INVALID ARGUMENTS",
|
|
291
|
+
INVALID_COLOR: "INVALID COLOR"
|
|
292
|
+
}[category];
|
|
293
|
+
}
|
|
294
|
+
function extractDiagnostic(issue, _sourceLine) {
|
|
295
|
+
const path$1 = issue.path.filter((p) => typeof p === "string" || typeof p === "number");
|
|
296
|
+
const errorCode = mapZodCodeToErrorCode(issue.code, path$1);
|
|
297
|
+
const category = categorize(issue.code, path$1);
|
|
298
|
+
let expected;
|
|
299
|
+
let received;
|
|
300
|
+
const issueAny = issue;
|
|
301
|
+
if (issue.code === "invalid_type") {
|
|
302
|
+
expected = issueAny.expected;
|
|
303
|
+
received = issueAny.received;
|
|
304
|
+
}
|
|
305
|
+
return Object.freeze({
|
|
306
|
+
category,
|
|
307
|
+
code: errorCode.code,
|
|
308
|
+
title: categoryTitle(category),
|
|
309
|
+
hint: errorCode.hint,
|
|
310
|
+
path: Object.freeze(path$1),
|
|
311
|
+
expected,
|
|
312
|
+
received
|
|
313
|
+
});
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
//#endregion
|
|
317
|
+
//#region src/compiler/source-map/create.ts
|
|
318
|
+
function createSourceMap(source) {
|
|
319
|
+
const lineCounter = new LineCounter();
|
|
320
|
+
const doc = parseDocument(source, { lineCounter });
|
|
321
|
+
const entries = [];
|
|
322
|
+
const lines = Object.freeze(source.split("\n"));
|
|
323
|
+
function traverse(node, currentPath) {
|
|
324
|
+
if (isMap(node)) {
|
|
325
|
+
const map = node;
|
|
326
|
+
for (const item of map.items) if (isPair(item)) {
|
|
327
|
+
const pair = item;
|
|
328
|
+
const key = isScalar(pair.key) ? pair.key.value : null;
|
|
329
|
+
if (key !== null && typeof key === "string") {
|
|
330
|
+
const keyPath = Object.freeze([...currentPath, key]);
|
|
331
|
+
const value = pair.value;
|
|
332
|
+
if (value?.range) {
|
|
333
|
+
const range = value.range;
|
|
334
|
+
const pos = lineCounter.linePos(range[0]);
|
|
335
|
+
entries.push(Object.freeze({
|
|
336
|
+
path: keyPath,
|
|
337
|
+
location: Object.freeze({
|
|
338
|
+
line: pos.line,
|
|
339
|
+
column: pos.col,
|
|
340
|
+
offset: range[0],
|
|
341
|
+
length: range[1] - range[0]
|
|
342
|
+
})
|
|
343
|
+
}));
|
|
344
|
+
}
|
|
345
|
+
traverse(pair.value, keyPath);
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
if (isSeq(node)) {
|
|
350
|
+
const seq = node;
|
|
351
|
+
for (let i = 0; i < seq.items.length; i++) {
|
|
352
|
+
const item = seq.items[i];
|
|
353
|
+
const indexPath = Object.freeze([...currentPath, i]);
|
|
354
|
+
if (item?.range) {
|
|
355
|
+
const range = item.range;
|
|
356
|
+
const pos = lineCounter.linePos(range[0]);
|
|
357
|
+
entries.push(Object.freeze({
|
|
358
|
+
path: indexPath,
|
|
359
|
+
location: Object.freeze({
|
|
360
|
+
line: pos.line,
|
|
361
|
+
column: pos.col,
|
|
362
|
+
offset: range[0],
|
|
363
|
+
length: range[1] - range[0]
|
|
364
|
+
})
|
|
365
|
+
}));
|
|
366
|
+
}
|
|
367
|
+
traverse(item, indexPath);
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
traverse(doc.contents, Object.freeze([]));
|
|
372
|
+
return Object.freeze({
|
|
373
|
+
entries: Object.freeze(entries),
|
|
374
|
+
source,
|
|
375
|
+
lines
|
|
376
|
+
});
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
//#endregion
|
|
380
|
+
//#region src/compiler/source-map/query.ts
|
|
381
|
+
function pathEquals(a, b) {
|
|
382
|
+
if (a.length !== b.length) return false;
|
|
383
|
+
for (let i = 0; i < a.length; i++) if (a[i] !== b[i]) return false;
|
|
384
|
+
return true;
|
|
385
|
+
}
|
|
386
|
+
function findLocation(sourceMap, path$1) {
|
|
387
|
+
for (const entry of sourceMap.entries) if (pathEquals(entry.path, path$1)) return entry.location;
|
|
388
|
+
for (let len = path$1.length - 1; len > 0; len--) {
|
|
389
|
+
const partialPath = path$1.slice(0, len);
|
|
390
|
+
for (const entry of sourceMap.entries) if (pathEquals(entry.path, partialPath)) return entry.location;
|
|
391
|
+
}
|
|
392
|
+
return null;
|
|
393
|
+
}
|
|
394
|
+
function getSourceLine(sourceMap, lineNumber) {
|
|
395
|
+
if (lineNumber < 1 || lineNumber > sourceMap.lines.length) return "";
|
|
396
|
+
return sourceMap.lines[lineNumber - 1] ?? "";
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
//#endregion
|
|
400
|
+
//#region src/core/errors.ts
|
|
401
|
+
var HasciiError = class extends Error {
|
|
402
|
+
constructor(message) {
|
|
403
|
+
super(message);
|
|
404
|
+
this.name = "HasciiError";
|
|
405
|
+
}
|
|
406
|
+
};
|
|
407
|
+
var HasciiParseError = class extends HasciiError {
|
|
408
|
+
source;
|
|
409
|
+
constructor(message, source = "") {
|
|
410
|
+
super(message);
|
|
411
|
+
this.name = "HasciiParseError";
|
|
412
|
+
this.source = source;
|
|
413
|
+
Object.freeze(this);
|
|
414
|
+
}
|
|
415
|
+
};
|
|
416
|
+
var HasciiValidationError = class extends HasciiError {
|
|
417
|
+
issues;
|
|
418
|
+
source;
|
|
419
|
+
constructor(message, issues = [], source = "") {
|
|
420
|
+
super(message);
|
|
421
|
+
this.name = "HasciiValidationError";
|
|
422
|
+
this.issues = Object.freeze([...issues]);
|
|
423
|
+
this.source = source;
|
|
424
|
+
Object.freeze(this);
|
|
425
|
+
}
|
|
426
|
+
};
|
|
427
|
+
var HasciiRuntimeError = class extends HasciiError {
|
|
428
|
+
constructor(message) {
|
|
429
|
+
super(message);
|
|
430
|
+
this.name = "HasciiRuntimeError";
|
|
431
|
+
Object.freeze(this);
|
|
432
|
+
}
|
|
433
|
+
};
|
|
434
|
+
|
|
435
|
+
//#endregion
|
|
436
|
+
//#region src/compiler/formatter.ts
|
|
437
|
+
const HEADER_WIDTH = 60;
|
|
438
|
+
function formatHeader(title) {
|
|
439
|
+
const padding = HEADER_WIDTH - title.length - 6;
|
|
440
|
+
const left = Math.floor(padding / 2);
|
|
441
|
+
const right = padding - left;
|
|
442
|
+
return `${COLORS.category}${line(BOX.HORIZ, left)} ${title} ${line(BOX.HORIZ, right)}${ANSI.RESET}`;
|
|
443
|
+
}
|
|
444
|
+
function formatValidationDiagnostic(issue, sourceMap) {
|
|
445
|
+
const location = findLocation(sourceMap, issue.path);
|
|
446
|
+
const diag = extractDiagnostic(issue, location ? getSourceLine(sourceMap, location.line) : void 0);
|
|
447
|
+
const lines = [];
|
|
448
|
+
lines.push(formatHeader(diag.title));
|
|
449
|
+
lines.push("");
|
|
450
|
+
if (diag.category === "TYPE_MISMATCH" && diag.expected && diag.received) lines.push(`I was expecting a ${COLORS.expected}${diag.expected}${ANSI.RESET} but found a ${COLORS.received}${diag.received}${ANSI.RESET}:`);
|
|
451
|
+
else if (diag.category === "INVALID_STATEMENT") lines.push("I don't recognize this statement:");
|
|
452
|
+
else if (diag.category === "INVALID_ARGS") lines.push("The arguments for this command are not right:");
|
|
453
|
+
else if (diag.category === "INVALID_COLOR") lines.push("This color is not in the palette:");
|
|
454
|
+
else lines.push("I found a problem here:");
|
|
455
|
+
lines.push("");
|
|
456
|
+
if (location) {
|
|
457
|
+
const gutter = " ".repeat(String(location.line).length);
|
|
458
|
+
lines.push(` ${COLORS.gutter}${BOX.TOP}${BOX.HORIZ}${ANSI.RESET} ${COLORS.location}${location.line}:${location.column}${ANSI.RESET}`);
|
|
459
|
+
lines.push(` ${COLORS.gutter}${BOX.VERT}${ANSI.RESET}`);
|
|
460
|
+
const sourceLine = getSourceLine(sourceMap, location.line);
|
|
461
|
+
lines.push(`${COLORS.lineNumber}${location.line}${ANSI.RESET} ${COLORS.gutter}${BOX.VERT}${ANSI.RESET} ${sourceLine}`);
|
|
462
|
+
const underlineLength = Math.max(1, Math.min(location.length, 20));
|
|
463
|
+
const padding = " ".repeat(Math.max(0, location.column - 1));
|
|
464
|
+
const underline = "^".repeat(underlineLength);
|
|
465
|
+
lines.push(`${gutter} ${COLORS.gutter}${BOX.VERT}${ANSI.RESET} ${padding}${COLORS.arrow}${underline}${ANSI.RESET}`);
|
|
466
|
+
lines.push(` ${COLORS.gutter}${BOX.VERT}${ANSI.RESET}`);
|
|
467
|
+
}
|
|
468
|
+
lines.push(` ${COLORS.gutter}${BOX.BOTTOM}${BOX.HORIZ}${ANSI.RESET} ${COLORS.hint}ℹ${ANSI.RESET} ${diag.hint}`);
|
|
469
|
+
return lines.join("\n");
|
|
470
|
+
}
|
|
471
|
+
function formatParseDiagnostic(message, sourceMap) {
|
|
472
|
+
const lines = [];
|
|
473
|
+
const lineMatch = message.match(/at line (\d+)/i);
|
|
474
|
+
const colMatch = message.match(/column (\d+)/i);
|
|
475
|
+
const lineNum = lineMatch ? parseInt(lineMatch[1], 10) : 1;
|
|
476
|
+
const column = colMatch ? parseInt(colMatch[1], 10) : 1;
|
|
477
|
+
const gutter = " ".repeat(String(lineNum).length);
|
|
478
|
+
lines.push(formatHeader("INVALID SYNTAX"));
|
|
479
|
+
lines.push("");
|
|
480
|
+
lines.push("I couldn't parse this YAML:");
|
|
481
|
+
lines.push("");
|
|
482
|
+
lines.push(` ${COLORS.gutter}${BOX.TOP}${BOX.HORIZ}${ANSI.RESET} ${COLORS.location}${lineNum}:${column}${ANSI.RESET}`);
|
|
483
|
+
lines.push(` ${COLORS.gutter}${BOX.VERT}${ANSI.RESET}`);
|
|
484
|
+
const sourceLine = getSourceLine(sourceMap, lineNum);
|
|
485
|
+
lines.push(`${COLORS.lineNumber}${lineNum}${ANSI.RESET} ${COLORS.gutter}${BOX.VERT}${ANSI.RESET} ${sourceLine}`);
|
|
486
|
+
const padding = " ".repeat(Math.max(0, column - 1));
|
|
487
|
+
lines.push(`${gutter} ${COLORS.gutter}${BOX.VERT}${ANSI.RESET} ${padding}${COLORS.arrow}^${ANSI.RESET}`);
|
|
488
|
+
lines.push(` ${COLORS.gutter}${BOX.VERT}${ANSI.RESET}`);
|
|
489
|
+
lines.push(` ${COLORS.gutter}${BOX.BOTTOM}${BOX.HORIZ}${ANSI.RESET} ${COLORS.hint}ℹ${ANSI.RESET} Check YAML indentation and bracket matching`);
|
|
490
|
+
return lines.join("\n");
|
|
491
|
+
}
|
|
492
|
+
function formatError(error) {
|
|
493
|
+
const sourceMap = createSourceMap(error.source);
|
|
494
|
+
if (error instanceof HasciiValidationError) return error.issues.map((issue) => formatValidationDiagnostic(issue, sourceMap)).join("\n\n");
|
|
495
|
+
return formatParseDiagnostic(error.message, sourceMap);
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
//#endregion
|
|
499
|
+
//#region src/core/models.ts
|
|
500
|
+
const colorSchema = z.number().int().min(0).max(15);
|
|
501
|
+
const strokeStyleSchema = z.enum([
|
|
502
|
+
"light",
|
|
503
|
+
"heavy",
|
|
504
|
+
"double",
|
|
505
|
+
"round"
|
|
506
|
+
]);
|
|
507
|
+
const valueSchema = z.lazy(() => z.union([
|
|
508
|
+
z.number(),
|
|
509
|
+
z.string(),
|
|
510
|
+
z.boolean(),
|
|
511
|
+
z.array(valueSchema),
|
|
512
|
+
z.record(z.string(), valueSchema)
|
|
513
|
+
]));
|
|
514
|
+
const expressionSchema = z.lazy(() => z.union([
|
|
515
|
+
z.number(),
|
|
516
|
+
z.string(),
|
|
517
|
+
z.object({ add: z.tuple([expressionSchema, expressionSchema]) }),
|
|
518
|
+
z.object({ sub: z.tuple([expressionSchema, expressionSchema]) }),
|
|
519
|
+
z.object({ mul: z.tuple([expressionSchema, expressionSchema]) }),
|
|
520
|
+
z.object({ div: z.tuple([expressionSchema, expressionSchema]) }),
|
|
521
|
+
z.object({ mod: z.tuple([expressionSchema, expressionSchema]) }),
|
|
522
|
+
z.object({ pow: z.tuple([expressionSchema, expressionSchema]) }),
|
|
523
|
+
z.object({ abs: expressionSchema }),
|
|
524
|
+
z.object({ neg: expressionSchema }),
|
|
525
|
+
z.object({ floor: expressionSchema }),
|
|
526
|
+
z.object({ ceil: expressionSchema }),
|
|
527
|
+
z.object({ round: expressionSchema }),
|
|
528
|
+
z.object({ sqrt: expressionSchema }),
|
|
529
|
+
z.object({ sin: expressionSchema }),
|
|
530
|
+
z.object({ cos: expressionSchema }),
|
|
531
|
+
z.object({ min: z.tuple([expressionSchema, expressionSchema]) }),
|
|
532
|
+
z.object({ max: z.tuple([expressionSchema, expressionSchema]) }),
|
|
533
|
+
z.object({ rnd: z.tuple([expressionSchema, expressionSchema]) }),
|
|
534
|
+
z.object({ eq: z.tuple([expressionSchema, expressionSchema]) }),
|
|
535
|
+
z.object({ lt: z.tuple([expressionSchema, expressionSchema]) }),
|
|
536
|
+
z.object({ lte: z.tuple([expressionSchema, expressionSchema]) }),
|
|
537
|
+
z.object({ gt: z.tuple([expressionSchema, expressionSchema]) }),
|
|
538
|
+
z.object({ gte: z.tuple([expressionSchema, expressionSchema]) }),
|
|
539
|
+
z.object({ and: z.array(expressionSchema).min(2) }),
|
|
540
|
+
z.object({ or: z.array(expressionSchema).min(2) }),
|
|
541
|
+
z.object({ not: expressionSchema }),
|
|
542
|
+
z.object({ at: z.tuple([expressionSchema, expressionSchema]) }),
|
|
543
|
+
z.object({ len: expressionSchema })
|
|
544
|
+
]));
|
|
545
|
+
const setValueSchema = z.lazy(() => z.union([expressionSchema, z.array(setValueSchema)]));
|
|
546
|
+
const setStatementSchema = z.object({ set: z.record(z.string(), setValueSchema) }).strict();
|
|
547
|
+
const ifStatementSchema = z.object({
|
|
548
|
+
if: expressionSchema,
|
|
549
|
+
then: z.array(z.lazy(() => statementSchema)).optional(),
|
|
550
|
+
else: z.array(z.lazy(() => statementSchema)).optional(),
|
|
551
|
+
set: z.record(z.string(), setValueSchema).optional()
|
|
552
|
+
});
|
|
553
|
+
const eachStatementSchema = z.object({
|
|
554
|
+
each: z.string(),
|
|
555
|
+
as: z.string(),
|
|
556
|
+
index: z.string().optional(),
|
|
557
|
+
do: z.array(z.lazy(() => statementSchema))
|
|
558
|
+
});
|
|
559
|
+
const pushStatementSchema = z.object({ push: z.object({
|
|
560
|
+
to: z.string(),
|
|
561
|
+
item: expressionSchema
|
|
562
|
+
}) });
|
|
563
|
+
const filterStatementSchema = z.object({ filter: z.object({
|
|
564
|
+
list: z.string(),
|
|
565
|
+
as: z.string(),
|
|
566
|
+
cond: expressionSchema
|
|
567
|
+
}) });
|
|
568
|
+
const fillStatementSchema = z.object({ fill: expressionSchema });
|
|
569
|
+
const strokeStatementSchema = z.object({ stroke: expressionSchema });
|
|
570
|
+
const noFillStatementSchema = z.object({ noFill: z.union([z.literal(true), z.null()]) }).strict();
|
|
571
|
+
const noStrokeStatementSchema = z.object({ noStroke: z.union([z.literal(true), z.null()]) }).strict();
|
|
572
|
+
const strokeStyleStatementSchema = z.object({ strokeStyle: strokeStyleSchema });
|
|
573
|
+
const clsStatementSchema = z.object({ cls: z.union([z.literal(true), z.null()]) }).strict();
|
|
574
|
+
const rectStatementSchema = z.object({ rect: z.tuple([
|
|
575
|
+
expressionSchema,
|
|
576
|
+
expressionSchema,
|
|
577
|
+
expressionSchema,
|
|
578
|
+
expressionSchema
|
|
579
|
+
]) });
|
|
580
|
+
const printStatementSchema = z.object({ print: z.tuple([
|
|
581
|
+
expressionSchema,
|
|
582
|
+
expressionSchema,
|
|
583
|
+
expressionSchema
|
|
584
|
+
]) });
|
|
585
|
+
const waveTypeSchema = z.enum([
|
|
586
|
+
"tri",
|
|
587
|
+
"saw",
|
|
588
|
+
"sqr",
|
|
589
|
+
"pls",
|
|
590
|
+
"org",
|
|
591
|
+
"noi",
|
|
592
|
+
"pha",
|
|
593
|
+
"sin"
|
|
594
|
+
]);
|
|
595
|
+
const effectTypeSchema = z.enum([
|
|
596
|
+
"pitchUp",
|
|
597
|
+
"vibrato",
|
|
598
|
+
"pitchDown",
|
|
599
|
+
"fadeIn",
|
|
600
|
+
"fadeOut"
|
|
601
|
+
]);
|
|
602
|
+
const sfxStatementSchema = z.object({ sfx: z.object({
|
|
603
|
+
wave: waveTypeSchema,
|
|
604
|
+
freq: expressionSchema,
|
|
605
|
+
length: expressionSchema,
|
|
606
|
+
volume: expressionSchema,
|
|
607
|
+
effect: effectTypeSchema.optional()
|
|
608
|
+
}) });
|
|
609
|
+
const statementSchema = z.lazy(() => z.union([
|
|
610
|
+
setStatementSchema,
|
|
611
|
+
ifStatementSchema,
|
|
612
|
+
eachStatementSchema,
|
|
613
|
+
pushStatementSchema,
|
|
614
|
+
filterStatementSchema,
|
|
615
|
+
fillStatementSchema,
|
|
616
|
+
strokeStatementSchema,
|
|
617
|
+
noFillStatementSchema,
|
|
618
|
+
noStrokeStatementSchema,
|
|
619
|
+
strokeStyleStatementSchema,
|
|
620
|
+
clsStatementSchema,
|
|
621
|
+
rectStatementSchema,
|
|
622
|
+
printStatementSchema,
|
|
623
|
+
sfxStatementSchema
|
|
624
|
+
]));
|
|
625
|
+
const channelNoteSchema = z.object({
|
|
626
|
+
pitch: z.number().int().min(-1).max(11),
|
|
627
|
+
octave: z.number().int().min(2).max(5).optional(),
|
|
628
|
+
volume: z.number().int().min(0).max(7).optional(),
|
|
629
|
+
effect: z.number().int().min(0).max(5).optional()
|
|
630
|
+
});
|
|
631
|
+
const patternSchema = z.object({ steps: z.array(z.array(channelNoteSchema)) });
|
|
632
|
+
const trackerSchema = z.object({
|
|
633
|
+
bpm: z.number().int().min(60).max(480).optional(),
|
|
634
|
+
items: z.array(patternSchema).optional()
|
|
635
|
+
});
|
|
636
|
+
const musicNoteSchema = z.object({
|
|
637
|
+
time: z.number().min(0),
|
|
638
|
+
pitch: z.number().int().min(0).max(11),
|
|
639
|
+
octave: z.number().int().min(2).max(5).optional(),
|
|
640
|
+
duration: z.number().min(.0625).optional(),
|
|
641
|
+
volume: z.number().int().min(0).max(7).optional()
|
|
642
|
+
});
|
|
643
|
+
const musicTrackSchema = z.object({
|
|
644
|
+
channel: z.number().int().min(0).max(3).optional(),
|
|
645
|
+
wave: waveTypeSchema.optional(),
|
|
646
|
+
notes: z.array(musicNoteSchema)
|
|
647
|
+
});
|
|
648
|
+
const songSchema = z.object({
|
|
649
|
+
name: z.string().max(16).optional(),
|
|
650
|
+
bpm: z.number().int().min(60).max(480).optional(),
|
|
651
|
+
tracks: z.array(musicTrackSchema)
|
|
652
|
+
});
|
|
653
|
+
const musicSchema = z.array(songSchema).max(8);
|
|
654
|
+
const descriptionSchema = z.string().max(80).regex(/^[a-zA-Z0-9 .,!?'\-:]*$/);
|
|
655
|
+
const baseMetaSchema = z.object({
|
|
656
|
+
title: z.string().min(2).max(8),
|
|
657
|
+
frame: colorSchema,
|
|
658
|
+
bg: colorSchema,
|
|
659
|
+
art: z.array(z.string()).length(5),
|
|
660
|
+
description: descriptionSchema.optional()
|
|
661
|
+
});
|
|
662
|
+
const gameCartridgeSchema = z.object({
|
|
663
|
+
meta: baseMetaSchema.extend({ type: z.literal("game") }).optional(),
|
|
664
|
+
init: z.record(z.string(), valueSchema).optional().default({}),
|
|
665
|
+
update: z.array(statementSchema).optional().default([]),
|
|
666
|
+
draw: z.array(statementSchema).optional().default([])
|
|
667
|
+
});
|
|
668
|
+
const trackerCartridgeSchema = z.object({
|
|
669
|
+
meta: baseMetaSchema.extend({ type: z.literal("tracker") }),
|
|
670
|
+
tracker: trackerSchema
|
|
671
|
+
});
|
|
672
|
+
const musicCartridgeSchema = z.object({
|
|
673
|
+
meta: baseMetaSchema.extend({ type: z.literal("music") }),
|
|
674
|
+
music: musicSchema
|
|
675
|
+
});
|
|
676
|
+
const legacyCartridgeSchema = z.object({
|
|
677
|
+
meta: baseMetaSchema.optional(),
|
|
678
|
+
init: z.record(z.string(), valueSchema).optional().default({}),
|
|
679
|
+
update: z.array(statementSchema).optional().default([]),
|
|
680
|
+
draw: z.array(statementSchema).optional().default([])
|
|
681
|
+
});
|
|
682
|
+
const hasciiProgramSchema = z.union([
|
|
683
|
+
trackerCartridgeSchema,
|
|
684
|
+
musicCartridgeSchema,
|
|
685
|
+
gameCartridgeSchema,
|
|
686
|
+
legacyCartridgeSchema
|
|
687
|
+
]);
|
|
688
|
+
|
|
689
|
+
//#endregion
|
|
690
|
+
//#region src/compiler/parser.ts
|
|
691
|
+
function compile(source) {
|
|
692
|
+
let doc;
|
|
693
|
+
try {
|
|
694
|
+
doc = parse(source);
|
|
695
|
+
} catch (e) {
|
|
696
|
+
return new HasciiParseError(e instanceof Error ? e.message : String(e), source);
|
|
697
|
+
}
|
|
698
|
+
const result = hasciiProgramSchema.safeParse(doc);
|
|
699
|
+
if (!result.success) return new HasciiValidationError(result.error.message, result.error.issues, source);
|
|
700
|
+
return Object.freeze(result.data);
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
//#endregion
|
|
704
|
+
//#region src/runtime/button-state.ts
|
|
705
|
+
const EMPTY_BUTTON_STATE = Object.freeze({
|
|
706
|
+
left: 0,
|
|
707
|
+
right: 0,
|
|
708
|
+
up: 0,
|
|
709
|
+
down: 0,
|
|
710
|
+
a: 0,
|
|
711
|
+
b: 0
|
|
712
|
+
});
|
|
713
|
+
const EMPTY_KEY_TIMESTAMPS = Object.freeze({
|
|
714
|
+
left: 0,
|
|
715
|
+
right: 0,
|
|
716
|
+
up: 0,
|
|
717
|
+
down: 0,
|
|
718
|
+
a: 0,
|
|
719
|
+
b: 0
|
|
720
|
+
});
|
|
721
|
+
function createButtonState() {
|
|
722
|
+
return EMPTY_BUTTON_STATE;
|
|
723
|
+
}
|
|
724
|
+
function createKeyTimestamps() {
|
|
725
|
+
return EMPTY_KEY_TIMESTAMPS;
|
|
726
|
+
}
|
|
727
|
+
function updateKeyTimestamps(timestamps, btn, now) {
|
|
728
|
+
return Object.freeze({
|
|
729
|
+
left: btn.left ? now : timestamps.left,
|
|
730
|
+
right: btn.right ? now : timestamps.right,
|
|
731
|
+
up: btn.up ? now : timestamps.up,
|
|
732
|
+
down: btn.down ? now : timestamps.down,
|
|
733
|
+
a: btn.a ? now : timestamps.a,
|
|
734
|
+
b: btn.b ? now : timestamps.b
|
|
735
|
+
});
|
|
736
|
+
}
|
|
737
|
+
function getActiveButtons(timestamps, now, holdMs) {
|
|
738
|
+
return Object.freeze({
|
|
739
|
+
left: now - timestamps.left < holdMs ? 1 : 0,
|
|
740
|
+
right: now - timestamps.right < holdMs ? 1 : 0,
|
|
741
|
+
up: now - timestamps.up < holdMs ? 1 : 0,
|
|
742
|
+
down: now - timestamps.down < holdMs ? 1 : 0,
|
|
743
|
+
a: now - timestamps.a < holdMs ? 1 : 0,
|
|
744
|
+
b: now - timestamps.b < holdMs ? 1 : 0
|
|
745
|
+
});
|
|
746
|
+
}
|
|
747
|
+
function getPressedButtons(prev, curr) {
|
|
748
|
+
return Object.freeze({
|
|
749
|
+
left: prev.left === 0 && curr.left === 1 ? 1 : 0,
|
|
750
|
+
right: prev.right === 0 && curr.right === 1 ? 1 : 0,
|
|
751
|
+
up: prev.up === 0 && curr.up === 1 ? 1 : 0,
|
|
752
|
+
down: prev.down === 0 && curr.down === 1 ? 1 : 0,
|
|
753
|
+
a: prev.a === 0 && curr.a === 1 ? 1 : 0,
|
|
754
|
+
b: prev.b === 0 && curr.b === 1 ? 1 : 0
|
|
755
|
+
});
|
|
756
|
+
}
|
|
757
|
+
const VALID_KEYS = Object.freeze(new Set(["z", "x"]));
|
|
758
|
+
const ARROW_UP = "\x1B[A";
|
|
759
|
+
const ARROW_DOWN = "\x1B[B";
|
|
760
|
+
const ARROW_RIGHT = "\x1B[C";
|
|
761
|
+
const ARROW_LEFT = "\x1B[D";
|
|
762
|
+
function isValidKey(key) {
|
|
763
|
+
return VALID_KEYS.has(key.toLowerCase());
|
|
764
|
+
}
|
|
765
|
+
function processKeyInput(key) {
|
|
766
|
+
const seq = key.toString();
|
|
767
|
+
const lower = seq.toLowerCase();
|
|
768
|
+
if (seq === ARROW_UP) return Object.freeze({
|
|
769
|
+
...EMPTY_BUTTON_STATE,
|
|
770
|
+
up: 1
|
|
771
|
+
});
|
|
772
|
+
if (seq === ARROW_DOWN) return Object.freeze({
|
|
773
|
+
...EMPTY_BUTTON_STATE,
|
|
774
|
+
down: 1
|
|
775
|
+
});
|
|
776
|
+
if (seq === ARROW_LEFT) return Object.freeze({
|
|
777
|
+
...EMPTY_BUTTON_STATE,
|
|
778
|
+
left: 1
|
|
779
|
+
});
|
|
780
|
+
if (seq === ARROW_RIGHT) return Object.freeze({
|
|
781
|
+
...EMPTY_BUTTON_STATE,
|
|
782
|
+
right: 1
|
|
783
|
+
});
|
|
784
|
+
if (!VALID_KEYS.has(lower)) return EMPTY_BUTTON_STATE;
|
|
785
|
+
return Object.freeze({
|
|
786
|
+
left: 0,
|
|
787
|
+
right: 0,
|
|
788
|
+
up: 0,
|
|
789
|
+
down: 0,
|
|
790
|
+
a: lower === "z" ? 1 : 0,
|
|
791
|
+
b: lower === "x" ? 1 : 0
|
|
792
|
+
});
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
//#endregion
|
|
796
|
+
//#region src/core/constants.ts
|
|
797
|
+
const CANVAS_WIDTH = 80;
|
|
798
|
+
const CANVAS_HEIGHT = 22;
|
|
799
|
+
const VIEW_WIDTH = 80;
|
|
800
|
+
const VIEW_HEIGHT = 22;
|
|
801
|
+
const ENABLE_SPRITE_EDITOR = false;
|
|
802
|
+
const ENABLE_MAP_EDITOR = false;
|
|
803
|
+
const DEFAULT_VOLUME = 4;
|
|
804
|
+
const DEFAULT_OCTAVE = 4;
|
|
805
|
+
|
|
806
|
+
//#endregion
|
|
807
|
+
//#region src/runtime/set-variable.ts
|
|
808
|
+
function setVariable(state, path$1, value) {
|
|
809
|
+
if (!isSafePath(path$1)) return state;
|
|
810
|
+
const parts = path$1.split(".");
|
|
811
|
+
if (parts.length === 1) {
|
|
812
|
+
if (!isSafeKey(path$1)) return state;
|
|
813
|
+
return {
|
|
814
|
+
...state,
|
|
815
|
+
[path$1]: value
|
|
816
|
+
};
|
|
817
|
+
}
|
|
818
|
+
const newState = { ...state };
|
|
819
|
+
let current = newState;
|
|
820
|
+
for (let i = 0; i < parts.length - 1; i++) {
|
|
821
|
+
const part = parts[i];
|
|
822
|
+
if (!part || !isSafeKey(part)) return state;
|
|
823
|
+
if (typeof current[part] !== "object" || Array.isArray(current[part])) current[part] = {};
|
|
824
|
+
current[part] = { ...current[part] };
|
|
825
|
+
current = current[part];
|
|
826
|
+
}
|
|
827
|
+
const lastPart = parts[parts.length - 1];
|
|
828
|
+
if (!lastPart || !isSafeKey(lastPart)) return state;
|
|
829
|
+
current[lastPart] = value;
|
|
830
|
+
return newState;
|
|
831
|
+
}
|
|
832
|
+
|
|
833
|
+
//#endregion
|
|
834
|
+
//#region src/runtime/interpreter/execute.ts
|
|
835
|
+
const DEFAULT_DRAW_STATE = Object.freeze({
|
|
836
|
+
fill: 7,
|
|
837
|
+
stroke: 7,
|
|
838
|
+
strokeStyle: "light"
|
|
839
|
+
});
|
|
840
|
+
function createDrawState() {
|
|
841
|
+
return DEFAULT_DRAW_STATE;
|
|
842
|
+
}
|
|
843
|
+
function executeStatements(statements, state, drawState = DEFAULT_DRAW_STATE) {
|
|
844
|
+
return statements.reduce((acc, stmt) => {
|
|
845
|
+
const result = executeStatement(stmt, acc.state, acc.drawState);
|
|
846
|
+
return Object.freeze({
|
|
847
|
+
state: result.state,
|
|
848
|
+
commands: Object.freeze([...acc.commands, ...result.commands]),
|
|
849
|
+
drawState: result.drawState
|
|
850
|
+
});
|
|
851
|
+
}, Object.freeze({
|
|
852
|
+
state,
|
|
853
|
+
commands: Object.freeze([]),
|
|
854
|
+
drawState
|
|
855
|
+
}));
|
|
856
|
+
}
|
|
857
|
+
function executeStatement(stmt, state, drawState) {
|
|
858
|
+
const emptyResult = Object.freeze({
|
|
859
|
+
state,
|
|
860
|
+
commands: Object.freeze([]),
|
|
861
|
+
drawState
|
|
862
|
+
});
|
|
863
|
+
if ("set" in stmt && !("if" in stmt)) {
|
|
864
|
+
const newState = Object.keys(stmt.set).reduce((s, key) => {
|
|
865
|
+
return setVariable(s, key, evaluate(stmt.set[key], s));
|
|
866
|
+
}, state);
|
|
867
|
+
return Object.freeze({
|
|
868
|
+
...emptyResult,
|
|
869
|
+
state: newState
|
|
870
|
+
});
|
|
871
|
+
}
|
|
872
|
+
if ("if" in stmt) {
|
|
873
|
+
if (evaluate(stmt.if, state)) {
|
|
874
|
+
let currentState = state;
|
|
875
|
+
if (stmt.set) currentState = Object.keys(stmt.set).reduce((s, key) => {
|
|
876
|
+
return setVariable(s, key, evaluate(stmt.set?.[key], s));
|
|
877
|
+
}, state);
|
|
878
|
+
if (stmt.then) return executeStatements(stmt.then, currentState, drawState);
|
|
879
|
+
return Object.freeze({
|
|
880
|
+
...emptyResult,
|
|
881
|
+
state: currentState
|
|
882
|
+
});
|
|
883
|
+
}
|
|
884
|
+
if (stmt.else) return executeStatements(stmt.else, state, drawState);
|
|
885
|
+
return emptyResult;
|
|
886
|
+
}
|
|
887
|
+
if ("each" in stmt) {
|
|
888
|
+
const arr = state[stmt.each];
|
|
889
|
+
if (!Array.isArray(arr)) return emptyResult;
|
|
890
|
+
return arr.slice(0, 1e3).reduce((acc, item, i) => {
|
|
891
|
+
if (item === void 0) return acc;
|
|
892
|
+
const loopState = Object.freeze({
|
|
893
|
+
...acc.state,
|
|
894
|
+
[stmt.as]: item,
|
|
895
|
+
...stmt.index ? { [stmt.index]: i } : {}
|
|
896
|
+
});
|
|
897
|
+
const result = executeStatements(stmt.do, loopState, acc.drawState);
|
|
898
|
+
const currentArr = acc.state[stmt.each];
|
|
899
|
+
const updatedItem = result.state[stmt.as];
|
|
900
|
+
const newState = Array.isArray(currentArr) && updatedItem !== void 0 ? Object.freeze({
|
|
901
|
+
...result.state,
|
|
902
|
+
[stmt.each]: Object.freeze(currentArr.map((v, idx) => idx === i ? updatedItem : v))
|
|
903
|
+
}) : result.state;
|
|
904
|
+
return Object.freeze({
|
|
905
|
+
state: newState,
|
|
906
|
+
commands: Object.freeze([...acc.commands, ...result.commands]),
|
|
907
|
+
drawState: result.drawState
|
|
908
|
+
});
|
|
909
|
+
}, Object.freeze({
|
|
910
|
+
state,
|
|
911
|
+
commands: Object.freeze([]),
|
|
912
|
+
drawState
|
|
913
|
+
}));
|
|
914
|
+
}
|
|
915
|
+
if ("push" in stmt) {
|
|
916
|
+
const arr = state[stmt.push.to];
|
|
917
|
+
if (!Array.isArray(arr)) return emptyResult;
|
|
918
|
+
const item = evaluate(stmt.push.item, state);
|
|
919
|
+
const newArr = Object.freeze([...arr, item]);
|
|
920
|
+
return Object.freeze({
|
|
921
|
+
...emptyResult,
|
|
922
|
+
state: Object.freeze({
|
|
923
|
+
...state,
|
|
924
|
+
[stmt.push.to]: newArr
|
|
925
|
+
})
|
|
926
|
+
});
|
|
927
|
+
}
|
|
928
|
+
if ("filter" in stmt) {
|
|
929
|
+
const arr = state[stmt.filter.list];
|
|
930
|
+
if (!Array.isArray(arr)) return emptyResult;
|
|
931
|
+
const filtered = arr.filter((item, idx) => {
|
|
932
|
+
const loopState = Object.freeze({
|
|
933
|
+
...state,
|
|
934
|
+
[stmt.filter.as]: item,
|
|
935
|
+
i: idx
|
|
936
|
+
});
|
|
937
|
+
const result = evaluate(stmt.filter.cond, loopState);
|
|
938
|
+
return Boolean(result);
|
|
939
|
+
});
|
|
940
|
+
return Object.freeze({
|
|
941
|
+
...emptyResult,
|
|
942
|
+
state: Object.freeze({
|
|
943
|
+
...state,
|
|
944
|
+
[stmt.filter.list]: Object.freeze(filtered)
|
|
945
|
+
})
|
|
946
|
+
});
|
|
947
|
+
}
|
|
948
|
+
if ("fill" in stmt) {
|
|
949
|
+
const fillValue = evaluate(stmt.fill, state);
|
|
950
|
+
const fill = typeof fillValue === "number" ? Math.floor(fillValue) % 16 : 0;
|
|
951
|
+
return Object.freeze({
|
|
952
|
+
...emptyResult,
|
|
953
|
+
drawState: Object.freeze({
|
|
954
|
+
...drawState,
|
|
955
|
+
fill
|
|
956
|
+
})
|
|
957
|
+
});
|
|
958
|
+
}
|
|
959
|
+
if ("stroke" in stmt) {
|
|
960
|
+
const strokeValue = evaluate(stmt.stroke, state);
|
|
961
|
+
const stroke = typeof strokeValue === "number" ? Math.floor(strokeValue) % 16 : 7;
|
|
962
|
+
return Object.freeze({
|
|
963
|
+
...emptyResult,
|
|
964
|
+
drawState: Object.freeze({
|
|
965
|
+
...drawState,
|
|
966
|
+
stroke
|
|
967
|
+
})
|
|
968
|
+
});
|
|
969
|
+
}
|
|
970
|
+
if ("noFill" in stmt) return Object.freeze({
|
|
971
|
+
...emptyResult,
|
|
972
|
+
drawState: Object.freeze({
|
|
973
|
+
...drawState,
|
|
974
|
+
fill: null
|
|
975
|
+
})
|
|
976
|
+
});
|
|
977
|
+
if ("noStroke" in stmt) return Object.freeze({
|
|
978
|
+
...emptyResult,
|
|
979
|
+
drawState: Object.freeze({
|
|
980
|
+
...drawState,
|
|
981
|
+
stroke: null
|
|
982
|
+
})
|
|
983
|
+
});
|
|
984
|
+
if ("strokeStyle" in stmt) return Object.freeze({
|
|
985
|
+
...emptyResult,
|
|
986
|
+
drawState: Object.freeze({
|
|
987
|
+
...drawState,
|
|
988
|
+
strokeStyle: stmt.strokeStyle
|
|
989
|
+
})
|
|
990
|
+
});
|
|
991
|
+
if ("cls" in stmt) {
|
|
992
|
+
const bg = drawState.fill ?? 0;
|
|
993
|
+
return Object.freeze({
|
|
994
|
+
...emptyResult,
|
|
995
|
+
commands: Object.freeze([{
|
|
996
|
+
type: "cls",
|
|
997
|
+
bg
|
|
998
|
+
}])
|
|
999
|
+
});
|
|
1000
|
+
}
|
|
1001
|
+
if ("rect" in stmt) {
|
|
1002
|
+
const x = evaluate(stmt.rect[0], state);
|
|
1003
|
+
const y = evaluate(stmt.rect[1], state);
|
|
1004
|
+
const w = evaluate(stmt.rect[2], state);
|
|
1005
|
+
const h = evaluate(stmt.rect[3], state);
|
|
1006
|
+
return Object.freeze({
|
|
1007
|
+
...emptyResult,
|
|
1008
|
+
commands: Object.freeze([{
|
|
1009
|
+
type: "rect",
|
|
1010
|
+
x,
|
|
1011
|
+
y,
|
|
1012
|
+
w,
|
|
1013
|
+
h,
|
|
1014
|
+
fill: drawState.fill,
|
|
1015
|
+
stroke: drawState.stroke,
|
|
1016
|
+
strokeStyle: drawState.strokeStyle
|
|
1017
|
+
}])
|
|
1018
|
+
});
|
|
1019
|
+
}
|
|
1020
|
+
if ("print" in stmt) {
|
|
1021
|
+
const text = String(evaluate(stmt.print[0], state));
|
|
1022
|
+
const x = evaluate(stmt.print[1], state);
|
|
1023
|
+
const y = evaluate(stmt.print[2], state);
|
|
1024
|
+
return Object.freeze({
|
|
1025
|
+
...emptyResult,
|
|
1026
|
+
commands: Object.freeze([{
|
|
1027
|
+
type: "print",
|
|
1028
|
+
text,
|
|
1029
|
+
x,
|
|
1030
|
+
y,
|
|
1031
|
+
fg: drawState.stroke ?? 7,
|
|
1032
|
+
bg: drawState.fill
|
|
1033
|
+
}])
|
|
1034
|
+
});
|
|
1035
|
+
}
|
|
1036
|
+
if ("sfx" in stmt) {
|
|
1037
|
+
const freq = evaluate(stmt.sfx.freq, state);
|
|
1038
|
+
const length = evaluate(stmt.sfx.length, state);
|
|
1039
|
+
const volume = evaluate(stmt.sfx.volume, state);
|
|
1040
|
+
return Object.freeze({
|
|
1041
|
+
...emptyResult,
|
|
1042
|
+
commands: Object.freeze([{
|
|
1043
|
+
type: "sfx",
|
|
1044
|
+
wave: stmt.sfx.wave,
|
|
1045
|
+
freq,
|
|
1046
|
+
length,
|
|
1047
|
+
volume,
|
|
1048
|
+
effect: stmt.sfx.effect ?? null
|
|
1049
|
+
}])
|
|
1050
|
+
});
|
|
1051
|
+
}
|
|
1052
|
+
return emptyResult;
|
|
1053
|
+
}
|
|
1054
|
+
|
|
1055
|
+
//#endregion
|
|
1056
|
+
//#region src/runtime/game.ts
|
|
1057
|
+
/**
|
|
1058
|
+
* Game engine - Pure functions for game state management
|
|
1059
|
+
*/
|
|
1060
|
+
/**
|
|
1061
|
+
* Create a new game instance from a program
|
|
1062
|
+
*/
|
|
1063
|
+
function createGame(program) {
|
|
1064
|
+
return Object.freeze({
|
|
1065
|
+
program,
|
|
1066
|
+
gameState: Object.freeze({ ...program.init }),
|
|
1067
|
+
btn: createButtonState(),
|
|
1068
|
+
prevBtn: createButtonState()
|
|
1069
|
+
});
|
|
1070
|
+
}
|
|
1071
|
+
/**
|
|
1072
|
+
* Tick the game: update state and generate draw commands
|
|
1073
|
+
* This is a pure function - no side effects
|
|
1074
|
+
*/
|
|
1075
|
+
function tick(game, btn) {
|
|
1076
|
+
const btnPressed = getPressedButtons(game.btn, btn);
|
|
1077
|
+
const stateWithBtn = Object.freeze({
|
|
1078
|
+
...game.gameState,
|
|
1079
|
+
width: CANVAS_WIDTH,
|
|
1080
|
+
height: CANVAS_HEIGHT,
|
|
1081
|
+
btn,
|
|
1082
|
+
btnPressed
|
|
1083
|
+
});
|
|
1084
|
+
const updateResult = executeStatements(game.program.update, stateWithBtn);
|
|
1085
|
+
const { btn: _b, btnPressed: _bp, width: _w, height: _h, ...newGameState } = updateResult.state;
|
|
1086
|
+
const drawState = Object.freeze({
|
|
1087
|
+
...newGameState,
|
|
1088
|
+
width: CANVAS_WIDTH,
|
|
1089
|
+
height: CANVAS_HEIGHT,
|
|
1090
|
+
btn,
|
|
1091
|
+
btnPressed
|
|
1092
|
+
});
|
|
1093
|
+
const drawResult = executeStatements(game.program.draw, drawState);
|
|
1094
|
+
const allCommands = Object.freeze([...updateResult.commands, ...drawResult.commands]);
|
|
1095
|
+
return Object.freeze({
|
|
1096
|
+
game: Object.freeze({
|
|
1097
|
+
program: game.program,
|
|
1098
|
+
gameState: Object.freeze(newGameState),
|
|
1099
|
+
btn,
|
|
1100
|
+
prevBtn: game.btn
|
|
1101
|
+
}),
|
|
1102
|
+
commands: allCommands
|
|
1103
|
+
});
|
|
1104
|
+
}
|
|
1105
|
+
|
|
1106
|
+
//#endregion
|
|
1107
|
+
//#region src/runtime/raw-mode.ts
|
|
1108
|
+
function enableRawMode() {
|
|
1109
|
+
if (process.stdin.isTTY) process.stdin.setRawMode(true);
|
|
1110
|
+
process.stdin.resume();
|
|
1111
|
+
}
|
|
1112
|
+
function disableRawMode() {
|
|
1113
|
+
if (process.stdin.isTTY) process.stdin.setRawMode(false);
|
|
1114
|
+
}
|
|
1115
|
+
|
|
1116
|
+
//#endregion
|
|
1117
|
+
//#region src/runtime/render/palette.ts
|
|
1118
|
+
const HASCII_PALETTE = Object.freeze([
|
|
1119
|
+
"\x1B[48;2;0;0;0m",
|
|
1120
|
+
"\x1B[48;2;128;0;0m",
|
|
1121
|
+
"\x1B[48;2;0;128;0m",
|
|
1122
|
+
"\x1B[48;2;128;128;0m",
|
|
1123
|
+
"\x1B[48;2;0;0;128m",
|
|
1124
|
+
"\x1B[48;2;128;0;128m",
|
|
1125
|
+
"\x1B[48;2;0;128;128m",
|
|
1126
|
+
"\x1B[48;2;192;192;192m",
|
|
1127
|
+
"\x1B[48;2;128;128;128m",
|
|
1128
|
+
"\x1B[48;2;255;0;0m",
|
|
1129
|
+
"\x1B[48;2;0;255;0m",
|
|
1130
|
+
"\x1B[48;2;255;255;0m",
|
|
1131
|
+
"\x1B[48;2;0;0;255m",
|
|
1132
|
+
"\x1B[48;2;255;0;255m",
|
|
1133
|
+
"\x1B[48;2;0;255;255m",
|
|
1134
|
+
"\x1B[48;2;255;255;255m"
|
|
1135
|
+
]);
|
|
1136
|
+
const FG_PALETTE = Object.freeze([
|
|
1137
|
+
"\x1B[38;2;0;0;0m",
|
|
1138
|
+
"\x1B[38;2;128;0;0m",
|
|
1139
|
+
"\x1B[38;2;0;128;0m",
|
|
1140
|
+
"\x1B[38;2;128;128;0m",
|
|
1141
|
+
"\x1B[38;2;0;0;128m",
|
|
1142
|
+
"\x1B[38;2;128;0;128m",
|
|
1143
|
+
"\x1B[38;2;0;128;128m",
|
|
1144
|
+
"\x1B[38;2;192;192;192m",
|
|
1145
|
+
"\x1B[38;2;128;128;128m",
|
|
1146
|
+
"\x1B[38;2;255;0;0m",
|
|
1147
|
+
"\x1B[38;2;0;255;0m",
|
|
1148
|
+
"\x1B[38;2;255;255;0m",
|
|
1149
|
+
"\x1B[38;2;0;0;255m",
|
|
1150
|
+
"\x1B[38;2;255;0;255m",
|
|
1151
|
+
"\x1B[38;2;0;255;255m",
|
|
1152
|
+
"\x1B[38;2;255;255;255m"
|
|
1153
|
+
]);
|
|
1154
|
+
const RESET = "\x1B[0m";
|
|
1155
|
+
const MARGIN_COLOR = "\x1B[48;2;30;30;30m";
|
|
1156
|
+
|
|
1157
|
+
//#endregion
|
|
1158
|
+
//#region src/runtime/render/buffer.ts
|
|
1159
|
+
Object.freeze({
|
|
1160
|
+
char: " ",
|
|
1161
|
+
fg: 7,
|
|
1162
|
+
bg: 0
|
|
1163
|
+
});
|
|
1164
|
+
function createBuffer(width, height, bg) {
|
|
1165
|
+
const cell = Object.freeze({
|
|
1166
|
+
char: " ",
|
|
1167
|
+
fg: 7,
|
|
1168
|
+
bg
|
|
1169
|
+
});
|
|
1170
|
+
return Object.freeze(Array.from({ length: height }, () => Object.freeze(Array.from({ length: width }, () => cell))));
|
|
1171
|
+
}
|
|
1172
|
+
function setCell(buffer, x, y, cell) {
|
|
1173
|
+
if (y < 0 || y >= buffer.length) return buffer;
|
|
1174
|
+
const row = buffer[y];
|
|
1175
|
+
if (!row || x < 0 || x >= row.length) return buffer;
|
|
1176
|
+
return Object.freeze(buffer.map((r, ry) => ry === y ? Object.freeze(r.map((c, rx) => rx === x ? cell : c)) : r));
|
|
1177
|
+
}
|
|
1178
|
+
const BOX_CHARS = {
|
|
1179
|
+
light: Object.freeze({
|
|
1180
|
+
h: "─",
|
|
1181
|
+
v: "│",
|
|
1182
|
+
tl: "┌",
|
|
1183
|
+
tr: "┐",
|
|
1184
|
+
bl: "└",
|
|
1185
|
+
br: "┘"
|
|
1186
|
+
}),
|
|
1187
|
+
heavy: Object.freeze({
|
|
1188
|
+
h: "━",
|
|
1189
|
+
v: "┃",
|
|
1190
|
+
tl: "┏",
|
|
1191
|
+
tr: "┓",
|
|
1192
|
+
bl: "┗",
|
|
1193
|
+
br: "┛"
|
|
1194
|
+
}),
|
|
1195
|
+
double: Object.freeze({
|
|
1196
|
+
h: "═",
|
|
1197
|
+
v: "║",
|
|
1198
|
+
tl: "╔",
|
|
1199
|
+
tr: "╗",
|
|
1200
|
+
bl: "╚",
|
|
1201
|
+
br: "╝"
|
|
1202
|
+
}),
|
|
1203
|
+
round: Object.freeze({
|
|
1204
|
+
h: "─",
|
|
1205
|
+
v: "│",
|
|
1206
|
+
tl: "╭",
|
|
1207
|
+
tr: "╮",
|
|
1208
|
+
bl: "╰",
|
|
1209
|
+
br: "╯"
|
|
1210
|
+
})
|
|
1211
|
+
};
|
|
1212
|
+
function applyCommand(buffer, cmd, width, height) {
|
|
1213
|
+
if (cmd.type === "cls") return createBuffer(width, height, cmd.bg);
|
|
1214
|
+
if (cmd.type === "rect") {
|
|
1215
|
+
let result = buffer;
|
|
1216
|
+
const x = Math.floor(cmd.x);
|
|
1217
|
+
const y = Math.floor(cmd.y);
|
|
1218
|
+
const w = Math.floor(cmd.w);
|
|
1219
|
+
const h = Math.floor(cmd.h);
|
|
1220
|
+
if (cmd.fill !== null) for (let dy = 0; dy < h; dy++) for (let dx = 0; dx < w; dx++) {
|
|
1221
|
+
const px = x + dx;
|
|
1222
|
+
const py = y + dy;
|
|
1223
|
+
const existingRow = result[py];
|
|
1224
|
+
const existingCell = existingRow ? existingRow[px] : null;
|
|
1225
|
+
const cell = Object.freeze({
|
|
1226
|
+
char: existingCell?.char ?? " ",
|
|
1227
|
+
fg: existingCell?.fg ?? 7,
|
|
1228
|
+
bg: cmd.fill
|
|
1229
|
+
});
|
|
1230
|
+
result = setCell(result, px, py, cell);
|
|
1231
|
+
}
|
|
1232
|
+
if (cmd.stroke !== null && w >= 2 && h >= 2) {
|
|
1233
|
+
const chars = BOX_CHARS[cmd.strokeStyle];
|
|
1234
|
+
const fg = cmd.stroke;
|
|
1235
|
+
for (let dx = 1; dx < w - 1; dx++) {
|
|
1236
|
+
const existingRow = result[y];
|
|
1237
|
+
const existingCell = existingRow ? existingRow[x + dx] : null;
|
|
1238
|
+
result = setCell(result, x + dx, y, Object.freeze({
|
|
1239
|
+
char: chars.h,
|
|
1240
|
+
fg,
|
|
1241
|
+
bg: existingCell?.bg ?? null
|
|
1242
|
+
}));
|
|
1243
|
+
}
|
|
1244
|
+
for (let dx = 1; dx < w - 1; dx++) {
|
|
1245
|
+
const existingRow = result[y + h - 1];
|
|
1246
|
+
const existingCell = existingRow ? existingRow[x + dx] : null;
|
|
1247
|
+
result = setCell(result, x + dx, y + h - 1, Object.freeze({
|
|
1248
|
+
char: chars.h,
|
|
1249
|
+
fg,
|
|
1250
|
+
bg: existingCell?.bg ?? null
|
|
1251
|
+
}));
|
|
1252
|
+
}
|
|
1253
|
+
for (let dy = 1; dy < h - 1; dy++) {
|
|
1254
|
+
const existingRow = result[y + dy];
|
|
1255
|
+
const existingCell = existingRow ? existingRow[x] : null;
|
|
1256
|
+
result = setCell(result, x, y + dy, Object.freeze({
|
|
1257
|
+
char: chars.v,
|
|
1258
|
+
fg,
|
|
1259
|
+
bg: existingCell?.bg ?? null
|
|
1260
|
+
}));
|
|
1261
|
+
}
|
|
1262
|
+
for (let dy = 1; dy < h - 1; dy++) {
|
|
1263
|
+
const existingRow = result[y + dy];
|
|
1264
|
+
const existingCell = existingRow ? existingRow[x + w - 1] : null;
|
|
1265
|
+
result = setCell(result, x + w - 1, y + dy, Object.freeze({
|
|
1266
|
+
char: chars.v,
|
|
1267
|
+
fg,
|
|
1268
|
+
bg: existingCell?.bg ?? null
|
|
1269
|
+
}));
|
|
1270
|
+
}
|
|
1271
|
+
const tlRow = result[y];
|
|
1272
|
+
const tlCell = tlRow ? tlRow[x] : null;
|
|
1273
|
+
result = setCell(result, x, y, Object.freeze({
|
|
1274
|
+
char: chars.tl,
|
|
1275
|
+
fg,
|
|
1276
|
+
bg: tlCell?.bg ?? null
|
|
1277
|
+
}));
|
|
1278
|
+
const trRow = result[y];
|
|
1279
|
+
const trCell = trRow ? trRow[x + w - 1] : null;
|
|
1280
|
+
result = setCell(result, x + w - 1, y, Object.freeze({
|
|
1281
|
+
char: chars.tr,
|
|
1282
|
+
fg,
|
|
1283
|
+
bg: trCell?.bg ?? null
|
|
1284
|
+
}));
|
|
1285
|
+
const blRow = result[y + h - 1];
|
|
1286
|
+
const blCell = blRow ? blRow[x] : null;
|
|
1287
|
+
result = setCell(result, x, y + h - 1, Object.freeze({
|
|
1288
|
+
char: chars.bl,
|
|
1289
|
+
fg,
|
|
1290
|
+
bg: blCell?.bg ?? null
|
|
1291
|
+
}));
|
|
1292
|
+
const brRow = result[y + h - 1];
|
|
1293
|
+
const brCell = brRow ? brRow[x + w - 1] : null;
|
|
1294
|
+
result = setCell(result, x + w - 1, y + h - 1, Object.freeze({
|
|
1295
|
+
char: chars.br,
|
|
1296
|
+
fg,
|
|
1297
|
+
bg: brCell?.bg ?? null
|
|
1298
|
+
}));
|
|
1299
|
+
}
|
|
1300
|
+
return result;
|
|
1301
|
+
}
|
|
1302
|
+
if (cmd.type === "print") {
|
|
1303
|
+
let result = buffer;
|
|
1304
|
+
const x = Math.floor(cmd.x);
|
|
1305
|
+
const y = Math.floor(cmd.y);
|
|
1306
|
+
const chars = Array.from(cmd.text);
|
|
1307
|
+
for (let i = 0; i < chars.length; i++) {
|
|
1308
|
+
const char = chars[i];
|
|
1309
|
+
if (char === void 0) continue;
|
|
1310
|
+
result = setCell(result, x + i, y, Object.freeze({
|
|
1311
|
+
char,
|
|
1312
|
+
fg: cmd.fg,
|
|
1313
|
+
bg: cmd.bg
|
|
1314
|
+
}));
|
|
1315
|
+
}
|
|
1316
|
+
return result;
|
|
1317
|
+
}
|
|
1318
|
+
return buffer;
|
|
1319
|
+
}
|
|
1320
|
+
function bufferToString(buffer, canvasWidth, canvasHeight, viewWidth, viewHeight, hints) {
|
|
1321
|
+
const offsetX = Math.floor((viewWidth - canvasWidth) / 2);
|
|
1322
|
+
const offsetY = Math.floor((viewHeight - canvasHeight) / 2);
|
|
1323
|
+
const hintRowTop = offsetY - 1;
|
|
1324
|
+
const hintRowBottom = offsetY + canvasHeight;
|
|
1325
|
+
const lines = [];
|
|
1326
|
+
for (let vy = 0; vy < viewHeight; vy++) {
|
|
1327
|
+
const cy = vy - offsetY;
|
|
1328
|
+
if (hints?.top && vy === hintRowTop) {
|
|
1329
|
+
const padding = " ".repeat(offsetX);
|
|
1330
|
+
lines.push(padding + hints.top);
|
|
1331
|
+
continue;
|
|
1332
|
+
}
|
|
1333
|
+
if (hints?.bottom && vy === hintRowBottom) {
|
|
1334
|
+
const textLen = hints.bottom.replace(/\x1b\[[0-9;]*m/g, "").length;
|
|
1335
|
+
const rightPadding = Math.max(0, canvasWidth - textLen);
|
|
1336
|
+
const padding = " ".repeat(offsetX + rightPadding);
|
|
1337
|
+
lines.push(padding + hints.bottom);
|
|
1338
|
+
continue;
|
|
1339
|
+
}
|
|
1340
|
+
let line$1 = "";
|
|
1341
|
+
for (let vx = 0; vx < viewWidth; vx++) {
|
|
1342
|
+
const cx = vx - offsetX;
|
|
1343
|
+
if (cx >= 0 && cx < canvasWidth && cy >= 0 && cy < canvasHeight) {
|
|
1344
|
+
const row = buffer[cy];
|
|
1345
|
+
const cell = row ? row[cx] : null;
|
|
1346
|
+
if (cell) {
|
|
1347
|
+
const fgCode = FG_PALETTE[cell.fg % FG_PALETTE.length];
|
|
1348
|
+
const bgCode = cell.bg !== null ? HASCII_PALETTE[cell.bg % HASCII_PALETTE.length] : "";
|
|
1349
|
+
line$1 += `${bgCode}${fgCode}${cell.char}${RESET}`;
|
|
1350
|
+
} else line$1 += `${HASCII_PALETTE[0]} ${RESET}`;
|
|
1351
|
+
} else line$1 += `${MARGIN_COLOR} ${RESET}`;
|
|
1352
|
+
}
|
|
1353
|
+
lines.push(line$1);
|
|
1354
|
+
}
|
|
1355
|
+
return lines.join("\n");
|
|
1356
|
+
}
|
|
1357
|
+
|
|
1358
|
+
//#endregion
|
|
1359
|
+
//#region src/runtime/render/index.ts
|
|
1360
|
+
function buildHintTop(termCols, termRows, canvasWidth, canvasHeight, actualHeight, offsetX) {
|
|
1361
|
+
if (termCols < canvasWidth || termRows < canvasHeight + 2) return `${" ".repeat(Math.max(0, offsetX))} \x1b[43m\x1b[30m Too small! \x1b[0m`;
|
|
1362
|
+
const esc = "\x1B[44m ESC \x1B[104m Exit \x1B[0m";
|
|
1363
|
+
const zKey = "\x1B[44m Z \x1B[104m OK \x1B[0m";
|
|
1364
|
+
const xKey = "\x1B[44m X \x1B[104m Cancel \x1B[0m";
|
|
1365
|
+
const leftPad = " ".repeat(offsetX);
|
|
1366
|
+
const middleSpace = actualHeight - 1 - 11 - 21;
|
|
1367
|
+
return `${leftPad} ${esc}${" ".repeat(Math.max(0, middleSpace))}${zKey} ${xKey} `;
|
|
1368
|
+
}
|
|
1369
|
+
function buildHintBottom(termCols, termRows, canvasWidth, canvasHeight, offsetX) {
|
|
1370
|
+
const leftPad = " ".repeat(Math.max(0, offsetX));
|
|
1371
|
+
if (termCols < canvasWidth || termRows < canvasHeight + 2) return `${leftPad} \x1b[43m\x1b[30m Need ${canvasWidth}x${canvasHeight + 2} but ${termCols}x${termRows} \x1b[0m`;
|
|
1372
|
+
return `${leftPad} \x1b[100m Hascii - Fantasy Console \x1b[0m`;
|
|
1373
|
+
}
|
|
1374
|
+
const MIN_WIDTH = 40;
|
|
1375
|
+
const MIN_HEIGHT = 20;
|
|
1376
|
+
function render(commands, canvasWidth, canvasHeight) {
|
|
1377
|
+
const termCols = process.stdout.columns || 80;
|
|
1378
|
+
const termRows = process.stdout.rows || 24;
|
|
1379
|
+
if (termCols < MIN_WIDTH || termRows < MIN_HEIGHT) process.exit(0);
|
|
1380
|
+
const actualWidth = Math.min(canvasWidth, termCols);
|
|
1381
|
+
const actualHeight = Math.min(canvasHeight, termRows - 2);
|
|
1382
|
+
const availableHeight = termRows - 2;
|
|
1383
|
+
const offsetY = Math.max(0, Math.floor((availableHeight - actualHeight) / 2));
|
|
1384
|
+
const output = bufferToString(commands.reduce((buf, cmd) => applyCommand(buf, cmd, actualWidth, actualHeight), createBuffer(actualWidth, actualHeight, 0)), actualWidth, actualHeight, termCols, actualHeight);
|
|
1385
|
+
const offsetX = Math.floor((termCols - actualWidth) / 2);
|
|
1386
|
+
const hintTop = buildHintTop(termCols, termRows, canvasWidth, canvasHeight, actualWidth, offsetX);
|
|
1387
|
+
const hintBottom = buildHintBottom(termCols, termRows, canvasWidth, canvasHeight, offsetX);
|
|
1388
|
+
const emptyLine = " ".repeat(termCols);
|
|
1389
|
+
const lines = [];
|
|
1390
|
+
for (let i = 0; i < offsetY && lines.length < termRows; i++) lines.push(emptyLine);
|
|
1391
|
+
if (lines.length < termRows) lines.push(hintTop.padEnd(termCols));
|
|
1392
|
+
const canvasLines = output.split("\n");
|
|
1393
|
+
for (const line$1 of canvasLines) {
|
|
1394
|
+
if (lines.length >= termRows) break;
|
|
1395
|
+
lines.push(line$1);
|
|
1396
|
+
}
|
|
1397
|
+
if (lines.length < termRows) lines.push(hintBottom.padEnd(termCols));
|
|
1398
|
+
while (lines.length < termRows) lines.push(emptyLine);
|
|
1399
|
+
process.stdout.write("\x1B[H");
|
|
1400
|
+
process.stdout.write(lines.join("\n"));
|
|
1401
|
+
process.stdout.write("\x1B[H");
|
|
1402
|
+
}
|
|
1403
|
+
|
|
1404
|
+
//#endregion
|
|
1405
|
+
//#region src/runtime/render/canvas.ts
|
|
1406
|
+
/**
|
|
1407
|
+
* Canvas renderer - Pure function to render draw commands to ANSI string
|
|
1408
|
+
*/
|
|
1409
|
+
/**
|
|
1410
|
+
* Render draw commands to an ANSI string.
|
|
1411
|
+
* Pure function - no side effects.
|
|
1412
|
+
*
|
|
1413
|
+
* @param commands - Draw commands from game tick
|
|
1414
|
+
* @param region - Where to render the canvas (screen coordinates)
|
|
1415
|
+
* @returns ANSI escape sequence string
|
|
1416
|
+
*/
|
|
1417
|
+
function renderCanvas(commands, region) {
|
|
1418
|
+
const canvasWidth = Math.min(CANVAS_WIDTH, region.width);
|
|
1419
|
+
const canvasHeight = Math.min(CANVAS_HEIGHT, region.height);
|
|
1420
|
+
const lines = bufferToString(commands.reduce((buf, cmd) => applyCommand(buf, cmd, canvasWidth, canvasHeight), createBuffer(canvasWidth, canvasHeight, 0)), canvasWidth, canvasHeight, region.width, canvasHeight).split("\n");
|
|
1421
|
+
let output = "";
|
|
1422
|
+
for (let y = 0; y < lines.length && y < region.height; y++) output += `\x1b[${region.y + y + 1};${region.x + 1}H${lines[y]}`;
|
|
1423
|
+
return output;
|
|
1424
|
+
}
|
|
1425
|
+
|
|
1426
|
+
//#endregion
|
|
1427
|
+
//#region src/audio/wave.ts
|
|
1428
|
+
/**
|
|
1429
|
+
* Wave generation for Hascii audio
|
|
1430
|
+
*/
|
|
1431
|
+
const SAMPLE_RATE = 44100;
|
|
1432
|
+
function noteToFreq(note, octave) {
|
|
1433
|
+
return 440 * 2 ** (((octave + 1) * 12 + note - 69) / 12);
|
|
1434
|
+
}
|
|
1435
|
+
function generateWave(wave, freq, dur, vol, fx) {
|
|
1436
|
+
const n = Math.floor(SAMPLE_RATE * dur);
|
|
1437
|
+
const out = new Int16Array(n);
|
|
1438
|
+
if (freq === 0 || vol === 0) return out;
|
|
1439
|
+
const amp = 32767 * (vol / 7) * .8;
|
|
1440
|
+
for (let i = 0; i < n; i++) {
|
|
1441
|
+
const t = i / SAMPLE_RATE;
|
|
1442
|
+
const p = i / n;
|
|
1443
|
+
let f = freq;
|
|
1444
|
+
if (fx === 1) f *= 1 + p * .5;
|
|
1445
|
+
if (fx === 2) f *= 1 + Math.sin(t * 30) * .02;
|
|
1446
|
+
if (fx === 3) f *= 1 - p * .5;
|
|
1447
|
+
const ph = t * f % 1;
|
|
1448
|
+
let v = 0;
|
|
1449
|
+
switch (wave) {
|
|
1450
|
+
case 0:
|
|
1451
|
+
v = 4 * Math.abs(ph - .5) - 1;
|
|
1452
|
+
break;
|
|
1453
|
+
case 1:
|
|
1454
|
+
v = 2 * ph - 1;
|
|
1455
|
+
break;
|
|
1456
|
+
case 2:
|
|
1457
|
+
v = ph < .5 ? 1 : -1;
|
|
1458
|
+
break;
|
|
1459
|
+
case 3:
|
|
1460
|
+
v = ph < .25 ? 1 : -1;
|
|
1461
|
+
break;
|
|
1462
|
+
case 4:
|
|
1463
|
+
v = Math.sin(2 * Math.PI * ph) * .6 + Math.sin(4 * Math.PI * ph) * .3;
|
|
1464
|
+
break;
|
|
1465
|
+
case 5:
|
|
1466
|
+
v = Math.random() * 2 - 1;
|
|
1467
|
+
break;
|
|
1468
|
+
case 6:
|
|
1469
|
+
v = Math.sin(2 * Math.PI * ph + Math.sin(t * 8) * 2);
|
|
1470
|
+
break;
|
|
1471
|
+
case 7:
|
|
1472
|
+
v = Math.sin(2 * Math.PI * ph);
|
|
1473
|
+
break;
|
|
1474
|
+
}
|
|
1475
|
+
let env = Math.min(1, (n - i) / (SAMPLE_RATE * .02));
|
|
1476
|
+
if (fx === 4) env = p;
|
|
1477
|
+
if (fx === 5) env = 1 - p;
|
|
1478
|
+
out[i] = Math.floor(v * amp * env);
|
|
1479
|
+
}
|
|
1480
|
+
return out;
|
|
1481
|
+
}
|
|
1482
|
+
function createWav(samples) {
|
|
1483
|
+
const buf = Buffer.alloc(44 + samples.length * 2);
|
|
1484
|
+
buf.write("RIFF", 0);
|
|
1485
|
+
buf.writeUInt32LE(36 + samples.length * 2, 4);
|
|
1486
|
+
buf.write("WAVE", 8);
|
|
1487
|
+
buf.write("fmt ", 12);
|
|
1488
|
+
buf.writeUInt32LE(16, 16);
|
|
1489
|
+
buf.writeUInt16LE(1, 20);
|
|
1490
|
+
buf.writeUInt16LE(1, 22);
|
|
1491
|
+
buf.writeUInt32LE(SAMPLE_RATE, 24);
|
|
1492
|
+
buf.writeUInt32LE(SAMPLE_RATE * 2, 28);
|
|
1493
|
+
buf.writeUInt16LE(2, 32);
|
|
1494
|
+
buf.writeUInt16LE(16, 34);
|
|
1495
|
+
buf.write("data", 36);
|
|
1496
|
+
buf.writeUInt32LE(samples.length * 2, 40);
|
|
1497
|
+
for (let i = 0; i < samples.length; i++) buf.writeInt16LE(samples[i] ?? 0, 44 + i * 2);
|
|
1498
|
+
return buf;
|
|
1499
|
+
}
|
|
1500
|
+
|
|
1501
|
+
//#endregion
|
|
1502
|
+
//#region src/audio/player.ts
|
|
1503
|
+
/**
|
|
1504
|
+
* Audio player for Hascii
|
|
1505
|
+
* Uses afplay on macOS, aplay on Linux
|
|
1506
|
+
*/
|
|
1507
|
+
let currentProcess = null;
|
|
1508
|
+
let tempFile = null;
|
|
1509
|
+
let stoppedManually = false;
|
|
1510
|
+
function getPlayCommand() {
|
|
1511
|
+
const platform$1 = os.platform();
|
|
1512
|
+
if (platform$1 === "darwin") return ["afplay"];
|
|
1513
|
+
if (platform$1 === "linux") return ["aplay", "-q"];
|
|
1514
|
+
return ["afplay"];
|
|
1515
|
+
}
|
|
1516
|
+
function cleanup() {
|
|
1517
|
+
if (tempFile && fs.existsSync(tempFile)) {
|
|
1518
|
+
try {
|
|
1519
|
+
fs.unlinkSync(tempFile);
|
|
1520
|
+
} catch {}
|
|
1521
|
+
tempFile = null;
|
|
1522
|
+
}
|
|
1523
|
+
}
|
|
1524
|
+
function stopAudio() {
|
|
1525
|
+
stoppedManually = true;
|
|
1526
|
+
if (currentProcess) {
|
|
1527
|
+
currentProcess.kill();
|
|
1528
|
+
currentProcess = null;
|
|
1529
|
+
}
|
|
1530
|
+
cleanup();
|
|
1531
|
+
}
|
|
1532
|
+
const WAVE_MAP = {
|
|
1533
|
+
tri: 0,
|
|
1534
|
+
saw: 1,
|
|
1535
|
+
sqr: 2,
|
|
1536
|
+
pls: 3,
|
|
1537
|
+
org: 4,
|
|
1538
|
+
noi: 5,
|
|
1539
|
+
pha: 6,
|
|
1540
|
+
sin: 7
|
|
1541
|
+
};
|
|
1542
|
+
const EFFECT_MAP = {
|
|
1543
|
+
pitchUp: 1,
|
|
1544
|
+
vibrato: 2,
|
|
1545
|
+
pitchDown: 3,
|
|
1546
|
+
fadeIn: 4,
|
|
1547
|
+
fadeOut: 5
|
|
1548
|
+
};
|
|
1549
|
+
const SFX_MASTER = .1;
|
|
1550
|
+
const MUSIC_MASTER = .05;
|
|
1551
|
+
function playSfx(waveName, freq, length, volume, effect = null) {
|
|
1552
|
+
stopAudio();
|
|
1553
|
+
const wav = createWav(generateWave(WAVE_MAP[waveName] ?? 0, freq, length, Math.round(volume * SFX_MASTER / 100 * 7), effect ? EFFECT_MAP[effect] ?? 0 : 0));
|
|
1554
|
+
tempFile = path.join(os.tmpdir(), `hascii_${Date.now()}.wav`);
|
|
1555
|
+
fs.writeFileSync(tempFile, wav);
|
|
1556
|
+
const [cmd, ...args] = getPlayCommand();
|
|
1557
|
+
if (!cmd) return;
|
|
1558
|
+
currentProcess = spawn(cmd, [...args, tempFile]);
|
|
1559
|
+
currentProcess.on("close", () => {
|
|
1560
|
+
currentProcess = null;
|
|
1561
|
+
cleanup();
|
|
1562
|
+
});
|
|
1563
|
+
}
|
|
1564
|
+
function isPlaying() {
|
|
1565
|
+
return currentProcess !== null;
|
|
1566
|
+
}
|
|
1567
|
+
/**
|
|
1568
|
+
* Convert step-based pattern to timeline-based Song for playback
|
|
1569
|
+
* This ensures EDIT and PLAY use the same audio generation
|
|
1570
|
+
*/
|
|
1571
|
+
function patternToSong(pattern, channelWaves, bpm, startStep = 0) {
|
|
1572
|
+
const tracks = [];
|
|
1573
|
+
const channels = pattern.length;
|
|
1574
|
+
const totalSteps = pattern[0]?.length ?? 32;
|
|
1575
|
+
for (let ch = 0; ch < channels; ch++) {
|
|
1576
|
+
const notes = [];
|
|
1577
|
+
const wave = channelWaves[ch] ?? 0;
|
|
1578
|
+
for (let step = startStep; step < totalSteps; step++) {
|
|
1579
|
+
const n = pattern[ch]?.[step];
|
|
1580
|
+
if (n && n.pitch >= 0) notes.push({
|
|
1581
|
+
time: (step - startStep) * .5,
|
|
1582
|
+
pitch: n.pitch,
|
|
1583
|
+
octave: n.octave,
|
|
1584
|
+
duration: .5,
|
|
1585
|
+
volume: n.volume
|
|
1586
|
+
});
|
|
1587
|
+
}
|
|
1588
|
+
tracks.push({
|
|
1589
|
+
channel: ch,
|
|
1590
|
+
wave,
|
|
1591
|
+
notes
|
|
1592
|
+
});
|
|
1593
|
+
}
|
|
1594
|
+
return {
|
|
1595
|
+
name: "Pattern",
|
|
1596
|
+
bpm,
|
|
1597
|
+
tracks
|
|
1598
|
+
};
|
|
1599
|
+
}
|
|
1600
|
+
function playMusic(song, onComplete) {
|
|
1601
|
+
stopAudio();
|
|
1602
|
+
stoppedManually = false;
|
|
1603
|
+
const beatDuration = 60 / song.bpm;
|
|
1604
|
+
let maxTime = 0;
|
|
1605
|
+
for (const track of song.tracks) for (const note of track.notes) {
|
|
1606
|
+
const endTime = note.time + note.duration;
|
|
1607
|
+
if (endTime > maxTime) maxTime = endTime;
|
|
1608
|
+
}
|
|
1609
|
+
const totalDuration = maxTime * beatDuration;
|
|
1610
|
+
const totalSamples = Math.ceil(totalDuration * SAMPLE_RATE);
|
|
1611
|
+
const buf = new Int16Array(totalSamples);
|
|
1612
|
+
for (const track of song.tracks) {
|
|
1613
|
+
const wave = track.wave;
|
|
1614
|
+
for (const note of track.notes) {
|
|
1615
|
+
const startSample = Math.floor(note.time * beatDuration * SAMPLE_RATE);
|
|
1616
|
+
const noteDuration = note.duration * beatDuration;
|
|
1617
|
+
const samples = generateWave(wave, noteToFreq(note.pitch, note.octave), noteDuration, note.volume, 0);
|
|
1618
|
+
for (let i = 0; i < samples.length && startSample + i < totalSamples; i++) {
|
|
1619
|
+
const idx = startSample + i;
|
|
1620
|
+
buf[idx] = Math.max(-32768, Math.min(32767, (buf[idx] ?? 0) + (samples[i] ?? 0)));
|
|
1621
|
+
}
|
|
1622
|
+
}
|
|
1623
|
+
}
|
|
1624
|
+
for (let i = 0; i < buf.length; i++) buf[i] = Math.floor((buf[i] ?? 0) * MUSIC_MASTER);
|
|
1625
|
+
const wav = createWav(buf);
|
|
1626
|
+
tempFile = path.join(os.tmpdir(), `hascii_${Date.now()}.wav`);
|
|
1627
|
+
fs.writeFileSync(tempFile, wav);
|
|
1628
|
+
const [cmd, ...args] = getPlayCommand();
|
|
1629
|
+
if (!cmd) return;
|
|
1630
|
+
currentProcess = spawn(cmd, [...args, tempFile]);
|
|
1631
|
+
currentProcess.on("close", () => {
|
|
1632
|
+
currentProcess = null;
|
|
1633
|
+
cleanup();
|
|
1634
|
+
if (!stoppedManually) onComplete?.();
|
|
1635
|
+
stoppedManually = false;
|
|
1636
|
+
});
|
|
1637
|
+
}
|
|
1638
|
+
|
|
1639
|
+
//#endregion
|
|
1640
|
+
//#region src/runtime/state.ts
|
|
1641
|
+
const CURSOR_HOME$1 = "\x1B[H";
|
|
1642
|
+
function createInitialState(program) {
|
|
1643
|
+
return Object.freeze({
|
|
1644
|
+
gameState: Object.freeze({ ...program.init }),
|
|
1645
|
+
btn: createButtonState(),
|
|
1646
|
+
prevBtn: createButtonState()
|
|
1647
|
+
});
|
|
1648
|
+
}
|
|
1649
|
+
function processSfxCommands(commands) {
|
|
1650
|
+
for (const cmd of commands) if (cmd.type === "sfx") playSfx(cmd.wave, cmd.freq, cmd.length, cmd.volume, cmd.effect);
|
|
1651
|
+
}
|
|
1652
|
+
function updateGameState(program, state, btn) {
|
|
1653
|
+
const btnPressed = getPressedButtons(state.btn, btn);
|
|
1654
|
+
const stateWithBtn = Object.freeze({
|
|
1655
|
+
...state.gameState,
|
|
1656
|
+
width: CANVAS_WIDTH,
|
|
1657
|
+
height: CANVAS_HEIGHT,
|
|
1658
|
+
btn,
|
|
1659
|
+
btnPressed
|
|
1660
|
+
});
|
|
1661
|
+
const updateResult = executeStatements(program.update, stateWithBtn);
|
|
1662
|
+
processSfxCommands(updateResult.commands);
|
|
1663
|
+
const { btn: _b, btnPressed: _bp, width: _w, height: _h, ...newGameState } = updateResult.state;
|
|
1664
|
+
return Object.freeze({
|
|
1665
|
+
gameState: Object.freeze(newGameState),
|
|
1666
|
+
btn,
|
|
1667
|
+
prevBtn: state.btn
|
|
1668
|
+
});
|
|
1669
|
+
}
|
|
1670
|
+
function renderFrame(program, state) {
|
|
1671
|
+
const btnPressed = getPressedButtons(state.prevBtn, state.btn);
|
|
1672
|
+
const drawState = Object.freeze({
|
|
1673
|
+
...state.gameState,
|
|
1674
|
+
width: CANVAS_WIDTH,
|
|
1675
|
+
height: CANVAS_HEIGHT,
|
|
1676
|
+
btn: state.btn,
|
|
1677
|
+
btnPressed
|
|
1678
|
+
});
|
|
1679
|
+
const drawResult = executeStatements(program.draw, drawState);
|
|
1680
|
+
process.stdout.write(CURSOR_HOME$1);
|
|
1681
|
+
render(drawResult.commands, CANVAS_WIDTH, CANVAS_HEIGHT);
|
|
1682
|
+
}
|
|
1683
|
+
|
|
1684
|
+
//#endregion
|
|
1685
|
+
//#region src/runtime/run.ts
|
|
1686
|
+
const HIDE_CURSOR = "\x1B[?25l";
|
|
1687
|
+
const SHOW_CURSOR = "\x1B[?25h";
|
|
1688
|
+
const CURSOR_HOME = "\x1B[H";
|
|
1689
|
+
function clearScreen() {
|
|
1690
|
+
const height = process.stdout.rows ?? 24;
|
|
1691
|
+
let out = "";
|
|
1692
|
+
for (let y = 0; y < height; y++) out += `\x1b[${y + 1};1H\x1b[2K`;
|
|
1693
|
+
return out;
|
|
1694
|
+
}
|
|
1695
|
+
const FPS = 30;
|
|
1696
|
+
const ESC_KEY = "\x1B";
|
|
1697
|
+
function run(program) {
|
|
1698
|
+
runWithCallback(program, {});
|
|
1699
|
+
}
|
|
1700
|
+
function runWithCallback(program, options) {
|
|
1701
|
+
let state = createInitialState(program);
|
|
1702
|
+
let intervalId = null;
|
|
1703
|
+
let currentBtn = createButtonState();
|
|
1704
|
+
let lastInputTime = 0;
|
|
1705
|
+
enableRawMode();
|
|
1706
|
+
console.clear();
|
|
1707
|
+
process.stdout.write(HIDE_CURSOR + clearScreen() + CURSOR_HOME);
|
|
1708
|
+
const cleanup$1 = () => {
|
|
1709
|
+
if (intervalId !== null) {
|
|
1710
|
+
clearInterval(intervalId);
|
|
1711
|
+
intervalId = null;
|
|
1712
|
+
}
|
|
1713
|
+
stopAudio();
|
|
1714
|
+
disableRawMode();
|
|
1715
|
+
process.stdout.write(SHOW_CURSOR + clearScreen() + CURSOR_HOME);
|
|
1716
|
+
process.stdin.removeAllListeners("data");
|
|
1717
|
+
};
|
|
1718
|
+
process.on("exit", cleanup$1);
|
|
1719
|
+
process.on("SIGINT", () => {
|
|
1720
|
+
cleanup$1();
|
|
1721
|
+
process.exit(0);
|
|
1722
|
+
});
|
|
1723
|
+
process.on("SIGTERM", () => {
|
|
1724
|
+
cleanup$1();
|
|
1725
|
+
process.exit(0);
|
|
1726
|
+
});
|
|
1727
|
+
process.stdout.on("resize", () => {
|
|
1728
|
+
process.stdout.write(clearScreen() + CURSOR_HOME);
|
|
1729
|
+
});
|
|
1730
|
+
process.stdin.on("data", (key) => {
|
|
1731
|
+
const seq = key.toString();
|
|
1732
|
+
if (seq === "" || seq === "q" || seq === ESC_KEY) {
|
|
1733
|
+
cleanup$1();
|
|
1734
|
+
if (seq === ESC_KEY && options.onEscape) options.onEscape();
|
|
1735
|
+
else process.exit(0);
|
|
1736
|
+
return;
|
|
1737
|
+
}
|
|
1738
|
+
currentBtn = processKeyInput(key);
|
|
1739
|
+
lastInputTime = Date.now();
|
|
1740
|
+
});
|
|
1741
|
+
const loop = () => {
|
|
1742
|
+
const btn = Date.now() - lastInputTime < 100 ? currentBtn : createButtonState();
|
|
1743
|
+
state = updateGameState(program, state, btn);
|
|
1744
|
+
renderFrame(program, state);
|
|
1745
|
+
};
|
|
1746
|
+
intervalId = setInterval(loop, 1e3 / FPS);
|
|
1747
|
+
}
|
|
1748
|
+
|
|
1749
|
+
//#endregion
|
|
1750
|
+
export { line as $, getActiveButtons as A, HasciiRuntimeError as B, DEFAULT_VOLUME as C, VIEW_WIDTH as D, VIEW_HEIGHT as E, compile as F, evaluate as G, findLocation as H, hasciiProgramSchema as I, ERROR_CODES as J, isSafeKey as K, formatError as L, isValidKey as M, processKeyInput as N, createButtonState as O, updateKeyTimestamps as P, COLORS as Q, HasciiError as R, DEFAULT_OCTAVE as S, ENABLE_SPRITE_EDITOR as T, getSourceLine as U, HasciiValidationError as V, createSourceMap as W, ANSI as X, mapZodCodeToErrorCode as Y, BOX as Z, createDrawState as _, updateGameState as a, CANVAS_HEIGHT as b, playMusic as c, renderCanvas as d, stripAnsi as et, render as f, tick as g, createGame as h, renderFrame as i, getPressedButtons as j, createKeyTimestamps as k, playSfx as l, enableRawMode as m, runWithCallback as n, isPlaying as o, disableRawMode as p, isSafePath as q, createInitialState as r, patternToSong as s, run as t, supportsColor as tt, stopAudio as u, executeStatements as v, ENABLE_MAP_EDITOR as w, CANVAS_WIDTH as x, setVariable as y, HasciiParseError as z };
|