pi-extensions 0.1.9

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.
Files changed (135) hide show
  1. package/.ralph/import-cc-codex.md +31 -0
  2. package/.ralph/import-cc-codex.state.json +14 -0
  3. package/.ralph/mario-not-impl.md +69 -0
  4. package/.ralph/mario-not-impl.state.json +14 -0
  5. package/.ralph/mario-not-spec.md +163 -0
  6. package/.ralph/mario-not-spec.state.json +14 -0
  7. package/LICENSE +21 -0
  8. package/README.md +65 -0
  9. package/RELEASING.md +34 -0
  10. package/agent-guidance/CHANGELOG.md +4 -0
  11. package/agent-guidance/README.md +102 -0
  12. package/agent-guidance/agent-guidance.ts +147 -0
  13. package/agent-guidance/package.json +22 -0
  14. package/agent-guidance/setup.sh +75 -0
  15. package/agent-guidance/templates/CLAUDE.md +5 -0
  16. package/agent-guidance/templates/CODEX.md +92 -0
  17. package/agent-guidance/templates/GEMINI.md +5 -0
  18. package/arcade/CHANGELOG.md +4 -0
  19. package/arcade/README.md +85 -0
  20. package/arcade/assets/picman.png +0 -0
  21. package/arcade/assets/ping.png +0 -0
  22. package/arcade/assets/spice-invaders.png +0 -0
  23. package/arcade/assets/tetris.png +0 -0
  24. package/arcade/mario-not/README.md +30 -0
  25. package/arcade/mario-not/boss.js +103 -0
  26. package/arcade/mario-not/camera.js +59 -0
  27. package/arcade/mario-not/collision.js +91 -0
  28. package/arcade/mario-not/colors.js +36 -0
  29. package/arcade/mario-not/constants.js +97 -0
  30. package/arcade/mario-not/core.js +39 -0
  31. package/arcade/mario-not/death.js +77 -0
  32. package/arcade/mario-not/effects.js +84 -0
  33. package/arcade/mario-not/enemies.js +31 -0
  34. package/arcade/mario-not/engine.js +171 -0
  35. package/arcade/mario-not/fireballs.js +98 -0
  36. package/arcade/mario-not/items.js +24 -0
  37. package/arcade/mario-not/levels.js +403 -0
  38. package/arcade/mario-not/logic.js +104 -0
  39. package/arcade/mario-not/mario-not.ts +297 -0
  40. package/arcade/mario-not/player.js +244 -0
  41. package/arcade/mario-not/render.js +257 -0
  42. package/arcade/mario-not/spec.md +548 -0
  43. package/arcade/mario-not/state.js +246 -0
  44. package/arcade/mario-not/tests/e2e.test.js +855 -0
  45. package/arcade/mario-not/tests/engine.test.js +888 -0
  46. package/arcade/mario-not/tests/fixtures/story0-frame.txt +4 -0
  47. package/arcade/mario-not/tests/fixtures/story1-camera.txt +4 -0
  48. package/arcade/mario-not/tests/fixtures/story1-glyphs.txt +4 -0
  49. package/arcade/mario-not/tests/fixtures/story10-item.txt +4 -0
  50. package/arcade/mario-not/tests/fixtures/story11-hazards.txt +4 -0
  51. package/arcade/mario-not/tests/fixtures/story12-used-block.txt +4 -0
  52. package/arcade/mario-not/tests/fixtures/story13-pipes.txt +4 -0
  53. package/arcade/mario-not/tests/fixtures/story14-goal.txt +4 -0
  54. package/arcade/mario-not/tests/fixtures/story15-hud-narrow.txt +2 -0
  55. package/arcade/mario-not/tests/fixtures/story16-unknown-tile.txt +4 -0
  56. package/arcade/mario-not/tests/fixtures/story17-mix.txt +4 -0
  57. package/arcade/mario-not/tests/fixtures/story18-hud-score.txt +2 -0
  58. package/arcade/mario-not/tests/fixtures/story19-cue.txt +4 -0
  59. package/arcade/mario-not/tests/fixtures/story2-enemy.txt +4 -0
  60. package/arcade/mario-not/tests/fixtures/story20-camera-offset.txt +4 -0
  61. package/arcade/mario-not/tests/fixtures/story21-hud-zero.txt +2 -0
  62. package/arcade/mario-not/tests/fixtures/story22-big-viewport.txt +4 -0
  63. package/arcade/mario-not/tests/fixtures/story23-camera-negative.txt +4 -0
  64. package/arcade/mario-not/tests/fixtures/story24-camera-width.txt +4 -0
  65. package/arcade/mario-not/tests/fixtures/story25-camera-positive.txt +4 -0
  66. package/arcade/mario-not/tests/fixtures/story26-hud-lives.txt +2 -0
  67. package/arcade/mario-not/tests/fixtures/story27-hud-coins.txt +2 -0
  68. package/arcade/mario-not/tests/fixtures/story28-item-viewport.txt +4 -0
  69. package/arcade/mario-not/tests/fixtures/story29-enemy-viewport.txt +4 -0
  70. package/arcade/mario-not/tests/fixtures/story3-hud.txt +2 -0
  71. package/arcade/mario-not/tests/fixtures/story30-hud-score.txt +2 -0
  72. package/arcade/mario-not/tests/fixtures/story31-particles-viewport.txt +4 -0
  73. package/arcade/mario-not/tests/fixtures/story32-paused-frame.txt +4 -0
  74. package/arcade/mario-not/tests/fixtures/story4-big.txt +4 -0
  75. package/arcade/mario-not/tests/fixtures/story5-resume-hud.txt +2 -0
  76. package/arcade/mario-not/tests/fixtures/story6-particles.txt +4 -0
  77. package/arcade/mario-not/tests/fixtures/story6-paused.txt +4 -0
  78. package/arcade/mario-not/tests/fixtures/story7-powerup.txt +4 -0
  79. package/arcade/mario-not/tests/fixtures/story8-hud-time.txt +2 -0
  80. package/arcade/mario-not/tests/fixtures/story9-hud-level.txt +2 -0
  81. package/arcade/mario-not/tiles.js +79 -0
  82. package/arcade/mario-not/tsconfig.json +14 -0
  83. package/arcade/mario-not/types.js +225 -0
  84. package/arcade/package.json +26 -0
  85. package/arcade/picman.ts +328 -0
  86. package/arcade/ping.ts +594 -0
  87. package/arcade/spice-invaders.ts +1104 -0
  88. package/arcade/tetris.ts +662 -0
  89. package/code-actions/CHANGELOG.md +4 -0
  90. package/code-actions/README.md +65 -0
  91. package/code-actions/actions.ts +107 -0
  92. package/code-actions/index.ts +148 -0
  93. package/code-actions/package.json +22 -0
  94. package/code-actions/search.ts +79 -0
  95. package/code-actions/snippets.ts +179 -0
  96. package/code-actions/ui.ts +120 -0
  97. package/files-widget/CHANGELOG.md +90 -0
  98. package/files-widget/DESIGN.md +452 -0
  99. package/files-widget/README.md +122 -0
  100. package/files-widget/TODO.md +141 -0
  101. package/files-widget/browser.ts +922 -0
  102. package/files-widget/comment.ts +5 -0
  103. package/files-widget/constants.ts +18 -0
  104. package/files-widget/demo.svg +1 -0
  105. package/files-widget/file-tree.ts +224 -0
  106. package/files-widget/file-viewer.ts +93 -0
  107. package/files-widget/git.ts +107 -0
  108. package/files-widget/index.ts +140 -0
  109. package/files-widget/input-utils.ts +3 -0
  110. package/files-widget/package.json +22 -0
  111. package/files-widget/types.ts +28 -0
  112. package/files-widget/utils.ts +26 -0
  113. package/files-widget/viewer.ts +424 -0
  114. package/import-cc-codex/research/import-chats-from-other-agents.md +135 -0
  115. package/import-cc-codex/spec.md +79 -0
  116. package/package.json +29 -0
  117. package/ralph-wiggum/CHANGELOG.md +7 -0
  118. package/ralph-wiggum/README.md +96 -0
  119. package/ralph-wiggum/SKILL.md +73 -0
  120. package/ralph-wiggum/index.ts +792 -0
  121. package/ralph-wiggum/package.json +25 -0
  122. package/raw-paste/CHANGELOG.md +7 -0
  123. package/raw-paste/README.md +52 -0
  124. package/raw-paste/index.ts +112 -0
  125. package/raw-paste/package.json +22 -0
  126. package/tab-status/CHANGELOG.md +4 -0
  127. package/tab-status/README.md +61 -0
  128. package/tab-status/assets/tab-status.png +0 -0
  129. package/tab-status/package.json +22 -0
  130. package/tab-status/tab-status.ts +179 -0
  131. package/usage-extension/CHANGELOG.md +17 -0
  132. package/usage-extension/README.md +120 -0
  133. package/usage-extension/index.ts +628 -0
  134. package/usage-extension/package.json +22 -0
  135. package/usage-extension/screenshot.png +0 -0
@@ -0,0 +1,855 @@
1
+ "use strict";
2
+
3
+ const test = require("node:test");
4
+ const assert = require("node:assert/strict");
5
+ const fs = require("node:fs");
6
+ const path = require("node:path");
7
+ const {
8
+ makeLevel,
9
+ createGame,
10
+ stepGame,
11
+ renderFrame,
12
+ renderViewport,
13
+ renderHud,
14
+ saveState,
15
+ loadState,
16
+ setPaused,
17
+ } = require("../engine.js");
18
+
19
+ test("e2e: one step right renders expected frame", () => {
20
+ const level = makeLevel([
21
+ " ",
22
+ " ",
23
+ " ",
24
+ "########",
25
+ ]);
26
+ const state = createGame({
27
+ level,
28
+ startX: 1,
29
+ startY: 2,
30
+ config: { dt: 1, gravity: 0 },
31
+ });
32
+ state.player.onGround = true;
33
+ stepGame(state, { right: true });
34
+ const frame = renderFrame(state);
35
+ const expected = fs
36
+ .readFileSync(path.join(__dirname, "fixtures", "story0-frame.txt"), "utf8")
37
+ .trimEnd();
38
+ assert.equal(frame, expected);
39
+ });
40
+
41
+ test("e2e: camera clamps to right edge", () => {
42
+ const level = makeLevel([
43
+ " ",
44
+ " ",
45
+ " ",
46
+ "############",
47
+ ]);
48
+ const state = createGame({
49
+ level,
50
+ startX: 1,
51
+ startY: 2,
52
+ config: { dt: 1, gravity: 0, viewportWidth: 8 },
53
+ });
54
+ state.player.onGround = true;
55
+ for (let i = 0; i < 3; i += 1) {
56
+ stepGame(state, { right: true });
57
+ }
58
+ const frame = renderViewport(state, 8, 4);
59
+ const expected = fs
60
+ .readFileSync(path.join(__dirname, "fixtures", "story1-camera.txt"), "utf8")
61
+ .trimEnd();
62
+ assert.equal(frame, expected);
63
+ });
64
+
65
+ test("e2e: camera uses state offset", () => {
66
+ const level = makeLevel([
67
+ "B?oG##",
68
+ " ",
69
+ " ",
70
+ "######",
71
+ ]);
72
+ const state = createGame({
73
+ level,
74
+ startX: 10,
75
+ startY: 10,
76
+ config: { dt: 1, gravity: 0, viewportWidth: 4 },
77
+ });
78
+ state.cameraX = 2;
79
+ const frame = renderViewport(state, 4, 4)
80
+ .split("\n")
81
+ .map((line) => line.trimEnd())
82
+ .join("\n");
83
+ const expected = fs
84
+ .readFileSync(path.join(__dirname, "fixtures", "story20-camera-offset.txt"), "utf8")
85
+ .trimEnd();
86
+ assert.equal(frame, expected);
87
+ });
88
+
89
+ test("e2e: camera clamps negative offset", () => {
90
+ const level = makeLevel([
91
+ "B?oG",
92
+ " ",
93
+ " ",
94
+ "####",
95
+ ]);
96
+ const state = createGame({
97
+ level,
98
+ startX: 10,
99
+ startY: 10,
100
+ config: { dt: 1, gravity: 0, viewportWidth: 4 },
101
+ });
102
+ state.cameraX = -3;
103
+ const frame = renderViewport(state, 4, 4)
104
+ .split("\n")
105
+ .map((line) => line.trimEnd())
106
+ .join("\n");
107
+ const expected = fs
108
+ .readFileSync(path.join(__dirname, "fixtures", "story23-camera-negative.txt"), "utf8")
109
+ .trimEnd();
110
+ assert.equal(frame, expected);
111
+ });
112
+
113
+ test("e2e: camera clamps positive offset", () => {
114
+ const level = makeLevel([
115
+ "B?oGTP^~",
116
+ " ",
117
+ " ",
118
+ "########",
119
+ ]);
120
+ const state = createGame({
121
+ level,
122
+ startX: 10,
123
+ startY: 10,
124
+ config: { dt: 1, gravity: 0, viewportWidth: 4 },
125
+ });
126
+ state.cameraX = 20;
127
+ const frame = renderViewport(state, 4, 4)
128
+ .split("\n")
129
+ .map((line) => line.trimEnd())
130
+ .join("\n");
131
+ const expected = fs
132
+ .readFileSync(path.join(__dirname, "fixtures", "story25-camera-positive.txt"), "utf8")
133
+ .trimEnd();
134
+ assert.equal(frame, expected);
135
+ });
136
+
137
+ test("e2e: camera uses viewport width param", () => {
138
+ const level = makeLevel([
139
+ "B?oGTP^~",
140
+ " ",
141
+ " ",
142
+ "########",
143
+ ]);
144
+ const state = createGame({
145
+ level,
146
+ startX: 10,
147
+ startY: 10,
148
+ config: { dt: 1, gravity: 0, viewportWidth: 6 },
149
+ });
150
+ state.cameraX = 3;
151
+ const frame = renderViewport(state, 4, 4)
152
+ .split("\n")
153
+ .map((line) => line.trimEnd())
154
+ .join("\n");
155
+ const expected = fs
156
+ .readFileSync(path.join(__dirname, "fixtures", "story24-camera-width.txt"), "utf8")
157
+ .trimEnd();
158
+ assert.equal(frame, expected);
159
+ });
160
+
161
+ test("e2e: tile glyphs render consistently", () => {
162
+ const level = makeLevel([
163
+ " B?oG ",
164
+ " ",
165
+ " ",
166
+ "######",
167
+ ]);
168
+ const state = createGame({
169
+ level,
170
+ startX: 1,
171
+ startY: 2,
172
+ config: { dt: 1, gravity: 0 },
173
+ });
174
+ state.player.onGround = true;
175
+ const frame = renderFrame(state);
176
+ const expected = fs
177
+ .readFileSync(path.join(__dirname, "fixtures", "story1-glyphs.txt"), "utf8")
178
+ .trimEnd();
179
+ assert.equal(frame, expected);
180
+ });
181
+
182
+ test("e2e: unknown tiles render blank", () => {
183
+ const level = makeLevel([
184
+ "! ",
185
+ " ",
186
+ " ",
187
+ "###",
188
+ ]);
189
+ const state = createGame({
190
+ level,
191
+ startX: 10,
192
+ startY: 10,
193
+ config: { dt: 1, gravity: 0 },
194
+ });
195
+ const frame = renderFrame(state)
196
+ .split("\n")
197
+ .map((line) => line.trimEnd())
198
+ .join("\n");
199
+ const expected = fs
200
+ .readFileSync(path.join(__dirname, "fixtures", "story16-unknown-tile.txt"), "utf8")
201
+ .trimEnd();
202
+ assert.equal(frame, expected);
203
+ });
204
+
205
+ test("e2e: enemy and item render together", () => {
206
+ const level = makeLevel([
207
+ " ",
208
+ " ",
209
+ " E ",
210
+ "####",
211
+ ]);
212
+ const state = createGame({
213
+ level,
214
+ startX: 3,
215
+ startY: 2,
216
+ config: { dt: 1, gravity: 0 },
217
+ });
218
+ state.items.push({ x: 2, y: 2, vx: 0, vy: 0, alive: true, onGround: true });
219
+ state.player.onGround = true;
220
+ const frame = renderFrame(state)
221
+ .split("\n")
222
+ .map((line) => line.trimEnd())
223
+ .join("\n");
224
+ const expected = fs
225
+ .readFileSync(path.join(__dirname, "fixtures", "story17-mix.txt"), "utf8")
226
+ .trimEnd();
227
+ assert.equal(frame, expected);
228
+ });
229
+
230
+ test("e2e: enemy renders in viewport", () => {
231
+ const level = makeLevel([
232
+ " ",
233
+ " ",
234
+ " E ",
235
+ "######",
236
+ ]);
237
+ const state = createGame({
238
+ level,
239
+ startX: 10,
240
+ startY: 10,
241
+ config: { dt: 1, gravity: 0, viewportWidth: 4 },
242
+ });
243
+ state.cameraX = 1;
244
+ const frame = renderViewport(state, 4, 4)
245
+ .split("\n")
246
+ .map((line) => line.trimEnd())
247
+ .join("\n");
248
+ const expected = fs
249
+ .readFileSync(path.join(__dirname, "fixtures", "story29-enemy-viewport.txt"), "utf8")
250
+ .trimEnd();
251
+ assert.equal(frame, expected);
252
+ });
253
+
254
+ test("e2e: item renders in viewport", () => {
255
+ const level = makeLevel([
256
+ " ",
257
+ " ",
258
+ " ",
259
+ "######",
260
+ ]);
261
+ const state = createGame({
262
+ level,
263
+ startX: 10,
264
+ startY: 10,
265
+ config: { dt: 1, gravity: 0, viewportWidth: 4 },
266
+ });
267
+ state.cameraX = 1;
268
+ state.items.push({ x: 3, y: 1, vx: 0, vy: 0, alive: true, onGround: true });
269
+ const frame = renderViewport(state, 4, 4)
270
+ .split("\n")
271
+ .map((line) => line.trimEnd())
272
+ .join("\n");
273
+ const expected = fs
274
+ .readFileSync(path.join(__dirname, "fixtures", "story28-item-viewport.txt"), "utf8")
275
+ .trimEnd();
276
+ assert.equal(frame, expected);
277
+ });
278
+
279
+ test("e2e: hazard glyphs render", () => {
280
+ const level = makeLevel([
281
+ " ^~ ",
282
+ " ",
283
+ " ",
284
+ "####",
285
+ ]);
286
+ const state = createGame({
287
+ level,
288
+ startX: 3,
289
+ startY: 2,
290
+ config: { dt: 1, gravity: 0 },
291
+ });
292
+ state.player.onGround = true;
293
+ const frame = renderFrame(state)
294
+ .split("\n")
295
+ .map((line) => line.trimEnd())
296
+ .join("\n");
297
+ const expected = fs
298
+ .readFileSync(path.join(__dirname, "fixtures", "story11-hazards.txt"), "utf8")
299
+ .trimEnd();
300
+ assert.equal(frame, expected);
301
+ });
302
+
303
+ test("e2e: pipe glyphs render", () => {
304
+ const level = makeLevel([
305
+ " T ",
306
+ " P ",
307
+ " ",
308
+ "###",
309
+ ]);
310
+ const state = createGame({
311
+ level,
312
+ startX: 10,
313
+ startY: 10,
314
+ config: { dt: 1, gravity: 0 },
315
+ });
316
+ const frame = renderFrame(state)
317
+ .split("\n")
318
+ .map((line) => line.trimEnd())
319
+ .join("\n");
320
+ const expected = fs
321
+ .readFileSync(path.join(__dirname, "fixtures", "story13-pipes.txt"), "utf8")
322
+ .trimEnd();
323
+ assert.equal(frame, expected);
324
+ });
325
+
326
+ test("e2e: goal glyph renders", () => {
327
+ const level = makeLevel([
328
+ " G ",
329
+ " ",
330
+ " ",
331
+ "###",
332
+ ]);
333
+ const state = createGame({
334
+ level,
335
+ startX: 10,
336
+ startY: 10,
337
+ config: { dt: 1, gravity: 0 },
338
+ });
339
+ const frame = renderFrame(state)
340
+ .split("\n")
341
+ .map((line) => line.trimEnd())
342
+ .join("\n");
343
+ const expected = fs
344
+ .readFileSync(path.join(__dirname, "fixtures", "story14-goal.txt"), "utf8")
345
+ .trimEnd();
346
+ assert.equal(frame, expected);
347
+ });
348
+
349
+ test("e2e: used block glyph renders", () => {
350
+ const level = makeLevel([
351
+ " U ",
352
+ " ",
353
+ " ",
354
+ "###",
355
+ ]);
356
+ const state = createGame({
357
+ level,
358
+ startX: 10,
359
+ startY: 10,
360
+ config: { dt: 1, gravity: 0 },
361
+ });
362
+ const frame = renderFrame(state)
363
+ .split("\n")
364
+ .map((line) => line.trimEnd())
365
+ .join("\n");
366
+ const expected = fs
367
+ .readFileSync(path.join(__dirname, "fixtures", "story12-used-block.txt"), "utf8")
368
+ .trimEnd();
369
+ assert.equal(frame, expected);
370
+ });
371
+
372
+ test("e2e: enemy renders with goomba glyph", () => {
373
+ const level = makeLevel([
374
+ " ",
375
+ " ",
376
+ " E ",
377
+ "####",
378
+ ]);
379
+ const state = createGame({
380
+ level,
381
+ startX: 3,
382
+ startY: 2,
383
+ config: { dt: 1, gravity: 0 },
384
+ });
385
+ state.player.onGround = true;
386
+ const frame = renderFrame(state);
387
+ const expected = fs
388
+ .readFileSync(path.join(__dirname, "fixtures", "story2-enemy.txt"), "utf8")
389
+ .trimEnd();
390
+ assert.equal(frame, expected);
391
+ });
392
+
393
+ test("e2e: hud shows score and coins", () => {
394
+ const level = makeLevel([
395
+ " ",
396
+ " ",
397
+ " o ",
398
+ "####",
399
+ ]);
400
+ const state = createGame({
401
+ level,
402
+ startX: 1,
403
+ startY: 2,
404
+ config: { dt: 1, gravity: 0 },
405
+ });
406
+ state.player.onGround = true;
407
+ stepGame(state, {});
408
+ const hud = renderHud(state, 30)
409
+ .split("\n")
410
+ .map((line) => line.trimEnd())
411
+ .join("\n");
412
+ const expected = fs
413
+ .readFileSync(path.join(__dirname, "fixtures", "story3-hud.txt"), "utf8")
414
+ .trimEnd();
415
+ assert.equal(hud, expected);
416
+ });
417
+
418
+ test("e2e: hud rounds time up", () => {
419
+ const level = makeLevel([
420
+ " ",
421
+ " ",
422
+ " ",
423
+ "####",
424
+ ]);
425
+ const state = createGame({
426
+ level,
427
+ startX: 1,
428
+ startY: 2,
429
+ config: { dt: 1, gravity: 0 },
430
+ });
431
+ state.time = 12.1;
432
+ const hud = renderHud(state, 30)
433
+ .split("\n")
434
+ .map((line) => line.trimEnd())
435
+ .join("\n");
436
+ const expected = fs
437
+ .readFileSync(path.join(__dirname, "fixtures", "story8-hud-time.txt"), "utf8")
438
+ .trimEnd();
439
+ assert.equal(hud, expected);
440
+ });
441
+
442
+ test("e2e: hud clamps negative time", () => {
443
+ const level = makeLevel([
444
+ " ",
445
+ " ",
446
+ " ",
447
+ "####",
448
+ ]);
449
+ const state = createGame({
450
+ level,
451
+ startX: 1,
452
+ startY: 2,
453
+ config: { dt: 1, gravity: 0 },
454
+ });
455
+ state.time = -5;
456
+ const hud = renderHud(state, 30)
457
+ .split("\n")
458
+ .map((line) => line.trimEnd())
459
+ .join("\n");
460
+ const expected = fs
461
+ .readFileSync(path.join(__dirname, "fixtures", "story21-hud-zero.txt"), "utf8")
462
+ .trimEnd();
463
+ assert.equal(hud, expected);
464
+ });
465
+
466
+ test("e2e: hud truncates to width", () => {
467
+ const level = makeLevel([
468
+ " ",
469
+ " ",
470
+ " ",
471
+ "####",
472
+ ]);
473
+ const state = createGame({
474
+ level,
475
+ startX: 1,
476
+ startY: 2,
477
+ config: { dt: 1, gravity: 0 },
478
+ });
479
+ const hud = renderHud(state, 10)
480
+ .split("\n")
481
+ .map((line) => line.trimEnd())
482
+ .join("\n");
483
+ const expected = fs
484
+ .readFileSync(path.join(__dirname, "fixtures", "story15-hud-narrow.txt"), "utf8")
485
+ .trimEnd();
486
+ assert.equal(hud, expected);
487
+ });
488
+
489
+ test("e2e: hud shows level index", () => {
490
+ const level = makeLevel([
491
+ " ",
492
+ " ",
493
+ " ",
494
+ "####",
495
+ ]);
496
+ const state = createGame({
497
+ level,
498
+ startX: 1,
499
+ startY: 2,
500
+ config: { dt: 1, gravity: 0 },
501
+ levelIndex: 3,
502
+ });
503
+ const hud = renderHud(state, 30)
504
+ .split("\n")
505
+ .map((line) => line.trimEnd())
506
+ .join("\n");
507
+ const expected = fs
508
+ .readFileSync(path.join(__dirname, "fixtures", "story9-hud-level.txt"), "utf8")
509
+ .trimEnd();
510
+ assert.equal(hud, expected);
511
+ });
512
+
513
+ test("e2e: hud pads score and coins", () => {
514
+ const level = makeLevel([
515
+ " ",
516
+ " ",
517
+ " ",
518
+ "####",
519
+ ]);
520
+ const state = createGame({
521
+ level,
522
+ startX: 1,
523
+ startY: 2,
524
+ config: { dt: 1, gravity: 0 },
525
+ levelIndex: 2,
526
+ });
527
+ state.score = 42;
528
+ state.coins = 3;
529
+ state.lives = 5;
530
+ state.time = 123;
531
+ const hud = renderHud(state, 30)
532
+ .split("\n")
533
+ .map((line) => line.trimEnd())
534
+ .join("\n");
535
+ const expected = fs
536
+ .readFileSync(path.join(__dirname, "fixtures", "story18-hud-score.txt"), "utf8")
537
+ .trimEnd();
538
+ assert.equal(hud, expected);
539
+ });
540
+
541
+ test("e2e: hud shows lives", () => {
542
+ const level = makeLevel([
543
+ " ",
544
+ " ",
545
+ " ",
546
+ "####",
547
+ ]);
548
+ const state = createGame({
549
+ level,
550
+ startX: 1,
551
+ startY: 2,
552
+ config: { dt: 1, gravity: 0 },
553
+ });
554
+ state.lives = 7;
555
+ const hud = renderHud(state, 30)
556
+ .split("\n")
557
+ .map((line) => line.trimEnd())
558
+ .join("\n");
559
+ const expected = fs
560
+ .readFileSync(path.join(__dirname, "fixtures", "story26-hud-lives.txt"), "utf8")
561
+ .trimEnd();
562
+ assert.equal(hud, expected);
563
+ });
564
+
565
+ test("e2e: hud shows score", () => {
566
+ const level = makeLevel([
567
+ " ",
568
+ " ",
569
+ " ",
570
+ "####",
571
+ ]);
572
+ const state = createGame({
573
+ level,
574
+ startX: 1,
575
+ startY: 2,
576
+ config: { dt: 1, gravity: 0 },
577
+ });
578
+ state.score = 7;
579
+ const hud = renderHud(state, 30)
580
+ .split("\n")
581
+ .map((line) => line.trimEnd())
582
+ .join("\n");
583
+ const expected = fs
584
+ .readFileSync(path.join(__dirname, "fixtures", "story30-hud-score.txt"), "utf8")
585
+ .trimEnd();
586
+ assert.equal(hud, expected);
587
+ });
588
+
589
+ test("e2e: hud shows coins", () => {
590
+ const level = makeLevel([
591
+ " ",
592
+ " ",
593
+ " ",
594
+ "####",
595
+ ]);
596
+ const state = createGame({
597
+ level,
598
+ startX: 1,
599
+ startY: 2,
600
+ config: { dt: 1, gravity: 0 },
601
+ });
602
+ state.coins = 9;
603
+ const hud = renderHud(state, 30)
604
+ .split("\n")
605
+ .map((line) => line.trimEnd())
606
+ .join("\n");
607
+ const expected = fs
608
+ .readFileSync(path.join(__dirname, "fixtures", "story27-hud-coins.txt"), "utf8")
609
+ .trimEnd();
610
+ assert.equal(hud, expected);
611
+ });
612
+
613
+ test("e2e: cue overlays frame", () => {
614
+ const level = makeLevel([
615
+ " ",
616
+ " ",
617
+ " ",
618
+ "####",
619
+ ]);
620
+ const state = createGame({
621
+ level,
622
+ startX: 10,
623
+ startY: 10,
624
+ config: { dt: 1, gravity: 0 },
625
+ });
626
+ state.cue = { text: "READY", ttl: 1, persist: false };
627
+ const frame = renderFrame(state)
628
+ .split("\n")
629
+ .map((line) => line.trimEnd())
630
+ .join("\n");
631
+ const expected = fs
632
+ .readFileSync(path.join(__dirname, "fixtures", "story19-cue.txt"), "utf8")
633
+ .trimEnd();
634
+ assert.equal(frame, expected);
635
+ });
636
+
637
+ test("e2e: item renders with mushroom glyph", () => {
638
+ const level = makeLevel([
639
+ " ",
640
+ " ",
641
+ " ",
642
+ "####",
643
+ ]);
644
+ const state = createGame({
645
+ level,
646
+ startX: 3,
647
+ startY: 2,
648
+ config: { dt: 1, gravity: 0 },
649
+ });
650
+ state.items.push({ x: 1, y: 2, vx: 0, vy: 0, alive: true, onGround: true });
651
+ const frame = renderFrame(state)
652
+ .split("\n")
653
+ .map((line) => line.trimEnd())
654
+ .join("\n");
655
+ const expected = fs
656
+ .readFileSync(path.join(__dirname, "fixtures", "story10-item.txt"), "utf8")
657
+ .trimEnd();
658
+ assert.equal(frame, expected);
659
+ });
660
+
661
+ test("e2e: big player renders tall", () => {
662
+ const level = makeLevel([
663
+ " ",
664
+ " ",
665
+ " ",
666
+ "####",
667
+ ]);
668
+ const state = createGame({
669
+ level,
670
+ startX: 1,
671
+ startY: 2,
672
+ config: { dt: 1, gravity: 0 },
673
+ });
674
+ state.player.size = "big";
675
+ state.player.onGround = true;
676
+ const frame = renderFrame(state)
677
+ .split("\n")
678
+ .map((line) => line.trimEnd())
679
+ .join("\n");
680
+ const expected = fs
681
+ .readFileSync(path.join(__dirname, "fixtures", "story4-big.txt"), "utf8")
682
+ .trimEnd();
683
+ assert.equal(frame, expected);
684
+ });
685
+
686
+ test("e2e: big player renders in viewport", () => {
687
+ const level = makeLevel([
688
+ " ",
689
+ " ",
690
+ " ",
691
+ "####",
692
+ ]);
693
+ const state = createGame({
694
+ level,
695
+ startX: 1,
696
+ startY: 2,
697
+ config: { dt: 1, gravity: 0, viewportWidth: 4 },
698
+ });
699
+ state.player.size = "big";
700
+ state.player.onGround = true;
701
+ const frame = renderViewport(state, 4, 4)
702
+ .split("\n")
703
+ .map((line) => line.trimEnd())
704
+ .join("\n");
705
+ const expected = fs
706
+ .readFileSync(path.join(__dirname, "fixtures", "story22-big-viewport.txt"), "utf8")
707
+ .trimEnd();
708
+ assert.equal(frame, expected);
709
+ });
710
+
711
+ test("e2e: save + load preserves hud", () => {
712
+ const level = makeLevel([
713
+ " ",
714
+ " ? ",
715
+ " o ",
716
+ "####",
717
+ ]);
718
+ const state = createGame({
719
+ level,
720
+ startX: 1,
721
+ startY: 2,
722
+ config: { dt: 1, gravity: 0 },
723
+ });
724
+ state.player.onGround = true;
725
+ stepGame(state, {});
726
+ state.player.vy = -1;
727
+ state.player.onGround = false;
728
+ stepGame(state, {});
729
+ const saved = saveState(state);
730
+ const loaded = loadState(saved, { config: { dt: 1, gravity: 0 } });
731
+ assert.ok(loaded);
732
+ const hud = renderHud(loaded, 30)
733
+ .split("\n")
734
+ .map((line) => line.trimEnd())
735
+ .join("\n");
736
+ const expected = fs
737
+ .readFileSync(path.join(__dirname, "fixtures", "story5-resume-hud.txt"), "utf8")
738
+ .trimEnd();
739
+ assert.equal(hud, expected);
740
+ });
741
+
742
+ test("e2e: paused cue renders centered", () => {
743
+ const level = makeLevel([
744
+ " ",
745
+ " ",
746
+ " ",
747
+ "########",
748
+ ]);
749
+ const state = createGame({
750
+ level,
751
+ startX: 1,
752
+ startY: 2,
753
+ config: { dt: 1, gravity: 0, viewportWidth: 8 },
754
+ });
755
+ setPaused(state, true);
756
+ const frame = renderViewport(state, 8, 4);
757
+ const expected = fs
758
+ .readFileSync(path.join(__dirname, "fixtures", "story6-paused.txt"), "utf8")
759
+ .trimEnd();
760
+ assert.equal(frame, expected);
761
+ });
762
+
763
+ test("e2e: paused cue renders in frame", () => {
764
+ const level = makeLevel([
765
+ " ",
766
+ " ",
767
+ " ",
768
+ "####",
769
+ ]);
770
+ const state = createGame({
771
+ level,
772
+ startX: 10,
773
+ startY: 10,
774
+ config: { dt: 1, gravity: 0 },
775
+ });
776
+ setPaused(state, true);
777
+ const frame = renderFrame(state)
778
+ .split("\n")
779
+ .map((line) => line.trimEnd())
780
+ .join("\n");
781
+ const expected = fs
782
+ .readFileSync(path.join(__dirname, "fixtures", "story32-paused-frame.txt"), "utf8")
783
+ .trimEnd();
784
+ assert.equal(frame, expected);
785
+ });
786
+
787
+ test("e2e: power up cue overlays", () => {
788
+ const level = makeLevel([
789
+ " ",
790
+ " ",
791
+ " ",
792
+ "########",
793
+ ]);
794
+ const state = createGame({
795
+ level,
796
+ startX: 1,
797
+ startY: 2,
798
+ config: { dt: 1, gravity: 0, viewportWidth: 8 },
799
+ });
800
+ state.cue = { text: "POWER UP", ttl: 1, persist: false };
801
+ const frame = renderViewport(state, 8, 4);
802
+ const expected = fs
803
+ .readFileSync(path.join(__dirname, "fixtures", "story7-powerup.txt"), "utf8")
804
+ .trimEnd();
805
+ assert.equal(frame, expected);
806
+ });
807
+
808
+ test("e2e: particle renders", () => {
809
+ const level = makeLevel([
810
+ " ",
811
+ " ",
812
+ " ",
813
+ "####",
814
+ ]);
815
+ const state = createGame({
816
+ level,
817
+ startX: 3,
818
+ startY: 2,
819
+ config: { dt: 1, gravity: 0 },
820
+ });
821
+ state.particles.push({ x: 1, y: 1, vx: 0, vy: 0, life: 1 });
822
+ const frame = renderFrame(state)
823
+ .split("\n")
824
+ .map((line) => line.trimEnd())
825
+ .join("\n");
826
+ const expected = fs
827
+ .readFileSync(path.join(__dirname, "fixtures", "story6-particles.txt"), "utf8")
828
+ .trimEnd();
829
+ assert.equal(frame, expected);
830
+ });
831
+
832
+ test("e2e: particle renders in viewport", () => {
833
+ const level = makeLevel([
834
+ " ",
835
+ " ",
836
+ " ",
837
+ "####",
838
+ ]);
839
+ const state = createGame({
840
+ level,
841
+ startX: 10,
842
+ startY: 10,
843
+ config: { dt: 1, gravity: 0, viewportWidth: 4 },
844
+ });
845
+ state.cameraX = 1;
846
+ state.particles.push({ x: 2, y: 1, vx: 0, vy: 0, life: 1 });
847
+ const frame = renderViewport(state, 4, 4)
848
+ .split("\n")
849
+ .map((line) => line.trimEnd())
850
+ .join("\n");
851
+ const expected = fs
852
+ .readFileSync(path.join(__dirname, "fixtures", "story31-particles-viewport.txt"), "utf8")
853
+ .trimEnd();
854
+ assert.equal(frame, expected);
855
+ });