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.
@@ -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 };