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,888 @@
1
+ "use strict";
2
+
3
+ const test = require("node:test");
4
+ const assert = require("node:assert/strict");
5
+ const {
6
+ createRng,
7
+ makeLevel,
8
+ createGame,
9
+ stepGame,
10
+ getCameraX,
11
+ updateCamera,
12
+ setPaused,
13
+ saveState,
14
+ loadState,
15
+ snapshotState,
16
+ } = require("../engine.js");
17
+ const { getTile, isHazardAt, isSolidAt, tileGlyph } = require("../tiles.js");
18
+ const { LEVEL_1_LINES, LEVEL_1_WIDTH, LEVEL_1_HEIGHT } = require("../levels.js");
19
+ const { GAME_MODES } = require("../constants.js");
20
+
21
+ test("rng deterministic", () => {
22
+ const rngA = createRng(123);
23
+ const rngB = createRng(123);
24
+ for (let i = 0; i < 5; i += 1) {
25
+ assert.equal(rngA(), rngB());
26
+ }
27
+ });
28
+
29
+ test("makeLevel validates rows", () => {
30
+ assert.throws(() => makeLevel([]), /non-empty/);
31
+ assert.throws(() => makeLevel([" ", " "]), /same width/);
32
+ });
33
+
34
+ test("createGame defaults level index", () => {
35
+ const level = makeLevel([
36
+ " ",
37
+ " ",
38
+ " ",
39
+ "####",
40
+ ]);
41
+ const state = createGame({
42
+ level,
43
+ startX: 1,
44
+ startY: 2,
45
+ config: { dt: 1, gravity: 0 },
46
+ });
47
+ assert.equal(state.levelIndex, 1);
48
+ });
49
+
50
+ test("createGame extracts enemy tiles", () => {
51
+ const level = makeLevel([
52
+ " ",
53
+ " ",
54
+ " E ",
55
+ "####",
56
+ ]);
57
+ const state = createGame({
58
+ level,
59
+ startX: 1,
60
+ startY: 2,
61
+ config: { dt: 1, gravity: 0 },
62
+ });
63
+ assert.equal(state.enemies.length, 1);
64
+ assert.equal(state.level.tiles[2][1], " ");
65
+ assert.equal(state.enemies[0].onGround, true);
66
+ });
67
+
68
+ test("createGame uses provided start coords", () => {
69
+ const level = makeLevel([
70
+ " ",
71
+ " ",
72
+ " ",
73
+ "####",
74
+ ]);
75
+ const state = createGame({
76
+ level,
77
+ startX: 2,
78
+ startY: 1,
79
+ config: { dt: 1, gravity: 0 },
80
+ });
81
+ assert.equal(state.player.x, 2);
82
+ assert.equal(state.player.y, 1);
83
+ });
84
+
85
+ test("stepGame clamps time at zero", () => {
86
+ const level = makeLevel([
87
+ " ",
88
+ " ",
89
+ " ",
90
+ "####",
91
+ ]);
92
+ const state = createGame({
93
+ level,
94
+ startX: 1,
95
+ startY: 2,
96
+ config: { dt: 1, gravity: 0 },
97
+ });
98
+ state.player.onGround = true;
99
+ state.time = 0.5;
100
+ stepGame(state, {});
101
+ assert.equal(state.time, 0);
102
+ });
103
+
104
+ test("stepGame reduces time by dt", () => {
105
+ const level = makeLevel([
106
+ " ",
107
+ " ",
108
+ " ",
109
+ "####",
110
+ ]);
111
+ const state = createGame({
112
+ level,
113
+ startX: 1,
114
+ startY: 2,
115
+ config: { dt: 1, gravity: 0 },
116
+ });
117
+ state.player.onGround = true;
118
+ state.time = 10;
119
+ stepGame(state, {});
120
+ assert.equal(state.time, 9);
121
+ });
122
+
123
+ test("paused step does not decrement time", () => {
124
+ const level = makeLevel([
125
+ " ",
126
+ " ",
127
+ " ",
128
+ "####",
129
+ ]);
130
+ const state = createGame({
131
+ level,
132
+ startX: 1,
133
+ startY: 2,
134
+ config: { dt: 1, gravity: 0 },
135
+ });
136
+ state.player.onGround = true;
137
+ setPaused(state, true);
138
+ stepGame(state, {});
139
+ assert.equal(state.time, 300);
140
+ });
141
+
142
+ test("createGame sets player onGround", () => {
143
+ const level = makeLevel([
144
+ " ",
145
+ " ",
146
+ " ",
147
+ "####",
148
+ ]);
149
+ const state = createGame({
150
+ level,
151
+ startX: 1,
152
+ startY: 2,
153
+ config: { dt: 1, gravity: 0 },
154
+ });
155
+ assert.equal(state.player.onGround, true);
156
+ });
157
+
158
+ test("loadState sets item onGround", () => {
159
+ const level = makeLevel([
160
+ " ",
161
+ " ",
162
+ " ",
163
+ "####",
164
+ ]);
165
+ const state = createGame({
166
+ level,
167
+ startX: 1,
168
+ startY: 2,
169
+ config: { dt: 1, gravity: 0 },
170
+ });
171
+ state.items.push({ x: 1, y: 2, vx: 0, vy: 0, alive: true, onGround: false });
172
+ const saved = saveState(state);
173
+ const loaded = loadState(saved, { config: { dt: 1, gravity: 0 } });
174
+ assert.ok(loaded);
175
+ assert.equal(loaded.items[0].onGround, true);
176
+ });
177
+
178
+ test("save/load preserves level index", () => {
179
+ const level = makeLevel([
180
+ " ",
181
+ " ",
182
+ " ",
183
+ "####",
184
+ ]);
185
+ const state = createGame({
186
+ level,
187
+ startX: 1,
188
+ startY: 2,
189
+ config: { dt: 1, gravity: 0 },
190
+ levelIndex: 4,
191
+ });
192
+ const saved = saveState(state);
193
+ const loaded = loadState(saved, { config: { dt: 1, gravity: 0 } });
194
+ assert.ok(loaded);
195
+ assert.equal(loaded.levelIndex, 4);
196
+ });
197
+
198
+ test("loadState sets enemy onGround", () => {
199
+ const level = makeLevel([
200
+ " ",
201
+ " ",
202
+ " ",
203
+ "####",
204
+ ]);
205
+ const state = createGame({
206
+ level,
207
+ startX: 1,
208
+ startY: 2,
209
+ config: { dt: 1, gravity: 0 },
210
+ });
211
+ state.enemies.push({ x: 2, y: 2, vx: 0, vy: 0, alive: true, onGround: false });
212
+ const saved = saveState(state);
213
+ const loaded = loadState(saved, { config: { dt: 1, gravity: 0 } });
214
+ assert.ok(loaded);
215
+ assert.equal(loaded.enemies[0].onGround, true);
216
+ });
217
+
218
+ test("save/load preserves player facing", () => {
219
+ const level = makeLevel([
220
+ " ",
221
+ " ",
222
+ " ",
223
+ "####",
224
+ ]);
225
+ const state = createGame({
226
+ level,
227
+ startX: 1,
228
+ startY: 2,
229
+ config: { dt: 1, gravity: 0 },
230
+ });
231
+ state.player.facing = -1;
232
+ const saved = saveState(state);
233
+ const loaded = loadState(saved, { config: { dt: 1, gravity: 0 } });
234
+ assert.ok(loaded);
235
+ assert.equal(loaded.player.facing, -1);
236
+ });
237
+
238
+ test("save/load preserves invuln", () => {
239
+ const level = makeLevel([
240
+ " ",
241
+ " ",
242
+ " ",
243
+ "####",
244
+ ]);
245
+ const state = createGame({
246
+ level,
247
+ startX: 1,
248
+ startY: 2,
249
+ config: { dt: 1, gravity: 0 },
250
+ });
251
+ state.player.invuln = 1.5;
252
+ const saved = saveState(state);
253
+ const loaded = loadState(saved, { config: { dt: 1, gravity: 0 } });
254
+ assert.ok(loaded);
255
+ assert.equal(loaded.player.invuln, 1.5);
256
+ });
257
+
258
+ test("save/load preserves player size", () => {
259
+ const level = makeLevel([
260
+ " ",
261
+ " ",
262
+ " ",
263
+ "####",
264
+ ]);
265
+ const state = createGame({
266
+ level,
267
+ startX: 1,
268
+ startY: 2,
269
+ config: { dt: 1, gravity: 0 },
270
+ });
271
+ state.player.size = "big";
272
+ const saved = saveState(state);
273
+ const loaded = loadState(saved, { config: { dt: 1, gravity: 0 } });
274
+ assert.ok(loaded);
275
+ assert.equal(loaded.player.size, "big");
276
+ });
277
+
278
+ test("stepGame moves right", () => {
279
+ const level = makeLevel([
280
+ " ",
281
+ " ",
282
+ " ",
283
+ "########",
284
+ ]);
285
+ const state = createGame({
286
+ level,
287
+ startX: 1,
288
+ startY: 2,
289
+ config: { dt: 1, gravity: 0 },
290
+ });
291
+ state.player.onGround = true;
292
+ stepGame(state, { right: true });
293
+ assert.ok(state.player.x > 1);
294
+ const snap = snapshotState(state);
295
+ assert.equal(snap.player.onGround, true);
296
+ });
297
+
298
+ test("pause freezes time and position", () => {
299
+ const level = makeLevel([
300
+ " ",
301
+ " ",
302
+ " ",
303
+ "########",
304
+ ]);
305
+ const state = createGame({
306
+ level,
307
+ startX: 1,
308
+ startY: 2,
309
+ config: { dt: 1, gravity: 0 },
310
+ });
311
+ state.player.onGround = true;
312
+ setPaused(state, true);
313
+ stepGame(state, { right: true });
314
+ assert.equal(state.player.x, 1);
315
+ assert.equal(state.time, 300);
316
+ assert.equal(state.mode, GAME_MODES.paused);
317
+ assert.equal(state.cue?.text, "PAUSED");
318
+ });
319
+
320
+ test("unpausing clears pause cue", () => {
321
+ const level = makeLevel([
322
+ " ",
323
+ " ",
324
+ " ",
325
+ "########",
326
+ ]);
327
+ const state = createGame({
328
+ level,
329
+ startX: 1,
330
+ startY: 2,
331
+ config: { dt: 1, gravity: 0 },
332
+ });
333
+ setPaused(state, true);
334
+ assert.equal(state.cue?.text, "PAUSED");
335
+ setPaused(state, false);
336
+ assert.equal(state.cue, null);
337
+ });
338
+
339
+ test("brick tiles block movement", () => {
340
+ const level = makeLevel([
341
+ " ",
342
+ " ",
343
+ " B ",
344
+ "####",
345
+ ]);
346
+ const state = createGame({
347
+ level,
348
+ startX: 1,
349
+ startY: 2,
350
+ config: { dt: 1, gravity: 0, walkSpeed: 1, runSpeed: 1, groundAccel: 1 },
351
+ });
352
+ state.player.onGround = true;
353
+ stepGame(state, { right: true });
354
+ assert.equal(state.player.x, 1);
355
+ });
356
+
357
+ test("big player blocks at head height", () => {
358
+ const level = makeLevel([
359
+ " ",
360
+ " B ",
361
+ " ",
362
+ "####",
363
+ ]);
364
+ const state = createGame({
365
+ level,
366
+ startX: 1,
367
+ startY: 2,
368
+ config: { dt: 1, gravity: 0, walkSpeed: 1, runSpeed: 1, groundAccel: 1 },
369
+ });
370
+ state.player.size = "big";
371
+ state.player.onGround = true;
372
+ stepGame(state, { right: true });
373
+ assert.equal(state.player.x, 1);
374
+ });
375
+
376
+ test("question blocks are solid", () => {
377
+ const level = makeLevel([
378
+ " ",
379
+ " ",
380
+ " ? ",
381
+ "####",
382
+ ]);
383
+ const state = createGame({
384
+ level,
385
+ startX: 1,
386
+ startY: 2,
387
+ config: { dt: 1, gravity: 0, walkSpeed: 1, runSpeed: 1, groundAccel: 1 },
388
+ });
389
+ state.player.onGround = true;
390
+ stepGame(state, { right: true });
391
+ assert.equal(state.player.x, 1);
392
+ });
393
+
394
+ test("coin pickup updates score and clears tile", () => {
395
+ const level = makeLevel([
396
+ " ",
397
+ " ",
398
+ " o ",
399
+ "####",
400
+ ]);
401
+ const state = createGame({
402
+ level,
403
+ startX: 1,
404
+ startY: 2,
405
+ config: { dt: 0.1, gravity: 0 },
406
+ });
407
+ state.player.onGround = true;
408
+ stepGame(state, {});
409
+ assert.equal(state.coins, 1);
410
+ assert.equal(state.score, 100);
411
+ assert.equal(state.level.tiles[2][1], " ");
412
+ assert.ok(state.particles.length > 0);
413
+ });
414
+
415
+ test("question block spawns mushroom and becomes used", () => {
416
+ const level = makeLevel([
417
+ " ",
418
+ " ? ",
419
+ " ",
420
+ "####",
421
+ ]);
422
+ const state = createGame({
423
+ level,
424
+ startX: 1,
425
+ startY: 2,
426
+ config: { dt: 1, gravity: 0 },
427
+ });
428
+ state.player.vy = -1;
429
+ state.player.onGround = false;
430
+ stepGame(state, {});
431
+ assert.equal(state.items.length, 1);
432
+ assert.equal(state.level.tiles[1][1], "U");
433
+ });
434
+
435
+ test("second question block spawns mushroom", () => {
436
+ const level = makeLevel([
437
+ " ",
438
+ " ? ?",
439
+ " ",
440
+ "####",
441
+ ]);
442
+ const state = createGame({
443
+ level,
444
+ startX: 1,
445
+ startY: 2,
446
+ config: { dt: 1, gravity: 0 },
447
+ });
448
+ state.player.vy = -1;
449
+ state.player.onGround = false;
450
+ stepGame(state, {});
451
+ assert.equal(state.items.length, 1);
452
+ state.player.x = 3;
453
+ state.player.vy = -1;
454
+ state.player.onGround = false;
455
+ stepGame(state, {});
456
+ assert.equal(state.items.length, 2);
457
+ assert.equal(state.level.tiles[1][1], "U");
458
+ assert.equal(state.level.tiles[1][3], "U");
459
+ });
460
+
461
+ test("mushroom pickup grows player", () => {
462
+ const level = makeLevel([
463
+ " ",
464
+ " ",
465
+ " ",
466
+ "####",
467
+ ]);
468
+ const state = createGame({
469
+ level,
470
+ startX: 1,
471
+ startY: 2,
472
+ config: { dt: 1, gravity: 0 },
473
+ });
474
+ state.items.push({ x: 1, y: 2, vx: 0, vy: 0, alive: true, onGround: false });
475
+ state.player.onGround = true;
476
+ stepGame(state, {});
477
+ assert.equal(state.player.size, "big");
478
+ assert.equal(state.items.length, 0);
479
+ });
480
+
481
+ test("mushroom pickup grants score if already big", () => {
482
+ const level = makeLevel([
483
+ " ",
484
+ " ",
485
+ " ",
486
+ "####",
487
+ ]);
488
+ const state = createGame({
489
+ level,
490
+ startX: 1,
491
+ startY: 2,
492
+ config: { dt: 1, gravity: 0 },
493
+ });
494
+ state.player.size = "big";
495
+ state.items.push({ x: 1, y: 2, vx: 0, vy: 0, alive: true, onGround: false });
496
+ state.player.onGround = true;
497
+ stepGame(state, {});
498
+ assert.equal(state.score, 1000);
499
+ });
500
+
501
+ test("stomp defeats enemy", () => {
502
+ const level = makeLevel([
503
+ " ",
504
+ " ",
505
+ " E ",
506
+ "####",
507
+ ]);
508
+ const state = createGame({
509
+ level,
510
+ startX: 1,
511
+ startY: 1,
512
+ config: { dt: 1, gravity: 0, jumpVel: 10, enemySpeed: 0 },
513
+ });
514
+ state.player.vy = 1;
515
+ state.player.onGround = false;
516
+ stepGame(state, {});
517
+ assert.equal(state.enemies.length, 1);
518
+ assert.equal(state.enemies[0].alive, false);
519
+ assert.ok(state.player.vy < 0);
520
+ assert.equal(state.score, 50);
521
+ });
522
+
523
+ test("side collision kills player", () => {
524
+ const level = makeLevel([
525
+ " ",
526
+ " ",
527
+ " E ",
528
+ "####",
529
+ ]);
530
+ const state = createGame({
531
+ level,
532
+ startX: 0,
533
+ startY: 2,
534
+ config: {
535
+ dt: 1,
536
+ gravity: 0,
537
+ walkSpeed: 1,
538
+ runSpeed: 1,
539
+ groundAccel: 1,
540
+ enemySpeed: 0,
541
+ },
542
+ });
543
+ state.player.onGround = true;
544
+ stepGame(state, { right: true });
545
+ assert.equal(state.mode, GAME_MODES.dead);
546
+ });
547
+
548
+ test("big player shrinks on enemy hit", () => {
549
+ const level = makeLevel([
550
+ " ",
551
+ " ",
552
+ " E ",
553
+ "####",
554
+ ]);
555
+ const state = createGame({
556
+ level,
557
+ startX: 1,
558
+ startY: 2,
559
+ config: { dt: 1, gravity: 0, enemySpeed: 0 },
560
+ });
561
+ state.player.size = "big";
562
+ state.player.onGround = true;
563
+ state.player.invuln = 0;
564
+ stepGame(state, {});
565
+ assert.equal(state.player.size, "small");
566
+ assert.equal(state.mode, GAME_MODES.playing);
567
+ });
568
+
569
+ test("hazard tiles kill player", () => {
570
+ const level = makeLevel([
571
+ " ",
572
+ " ",
573
+ " ^ ",
574
+ "####",
575
+ ]);
576
+ const state = createGame({
577
+ level,
578
+ startX: 1,
579
+ startY: 2,
580
+ config: { dt: 1, gravity: 0 },
581
+ });
582
+ stepGame(state, {});
583
+ assert.equal(state.mode, GAME_MODES.dead);
584
+ });
585
+
586
+ test("falling into pit kills player", () => {
587
+ const level = makeLevel([
588
+ " ",
589
+ " ",
590
+ " ",
591
+ "# #",
592
+ ]);
593
+ const state = createGame({
594
+ level,
595
+ startX: 2,
596
+ startY: 2,
597
+ config: { dt: 0.1, gravity: 20, maxFall: 20 },
598
+ });
599
+ const startLives = state.lives;
600
+ for (let i = 0; i < 40; i += 1) {
601
+ stepGame(state, {});
602
+ if (state.mode === GAME_MODES.dead) break;
603
+ }
604
+ assert.equal(state.mode, GAME_MODES.dead);
605
+ assert.equal(state.lives, startLives - 1);
606
+ });
607
+
608
+ test("death respawns at spawn with life loss", () => {
609
+ const level = makeLevel([
610
+ " ",
611
+ " ",
612
+ " ^ ",
613
+ "####",
614
+ ]);
615
+ const state = createGame({
616
+ level,
617
+ startX: 1,
618
+ startY: 2,
619
+ config: { dt: 0.1, gravity: 20, maxFall: 20 },
620
+ });
621
+ state.player.onGround = true;
622
+ const startLives = state.lives;
623
+ stepGame(state, {});
624
+ assert.equal(state.mode, GAME_MODES.dead);
625
+ assert.equal(state.lives, startLives - 1);
626
+ for (let i = 0; i < 40; i += 1) {
627
+ stepGame(state, {});
628
+ if (state.mode !== GAME_MODES.dead) break;
629
+ }
630
+ assert.equal(state.mode, GAME_MODES.playing);
631
+ assert.equal(state.player.x, state.spawnX);
632
+ assert.equal(state.player.y, state.spawnY);
633
+ });
634
+
635
+ test("enemy falls into pit and despawns", () => {
636
+ const level = makeLevel([
637
+ " ",
638
+ " ",
639
+ " E ",
640
+ "# #",
641
+ ]);
642
+ const state = createGame({
643
+ level,
644
+ startX: 1,
645
+ startY: 2,
646
+ config: { dt: 0.1, gravity: 20, maxFall: 20, enemySpeed: 0 },
647
+ });
648
+ const enemy = state.enemies[0];
649
+ assert.equal(enemy.alive, true);
650
+ for (let i = 0; i < 60; i += 1) {
651
+ stepGame(state, {});
652
+ if (!enemy.alive) break;
653
+ }
654
+ assert.equal(enemy.alive, false);
655
+ });
656
+
657
+ test("goal tile clears level with score bonus", () => {
658
+ const level = makeLevel([
659
+ " ",
660
+ " ",
661
+ " G ",
662
+ "####",
663
+ ]);
664
+ const state = createGame({
665
+ level,
666
+ startX: 1,
667
+ startY: 2,
668
+ config: { dt: 1, gravity: 0, walkSpeed: 1, runSpeed: 1, groundAccel: 1 },
669
+ });
670
+ state.player.onGround = true;
671
+ const scoreBefore = state.score;
672
+ stepGame(state, { right: true });
673
+ assert.equal(state.mode, GAME_MODES.levelClear);
674
+ assert.ok(state.cue?.text.startsWith("+"), "cue should show score bonus");
675
+ assert.ok(state.score > scoreBefore, "score should increase");
676
+ });
677
+
678
+ test("save and load restores progress", () => {
679
+ const level = makeLevel([
680
+ " ",
681
+ " ? ",
682
+ " o ",
683
+ "####",
684
+ ]);
685
+ const state = createGame({
686
+ level,
687
+ startX: 1,
688
+ startY: 2,
689
+ config: { dt: 1, gravity: 0 },
690
+ });
691
+ state.player.onGround = true;
692
+ stepGame(state, {});
693
+ state.player.vy = -1;
694
+ state.player.onGround = false;
695
+ stepGame(state, {});
696
+ state.enemies.push({ x: 3, y: 2, vx: 0, vy: 0, alive: false, onGround: true });
697
+ state.items.push({ x: 2, y: 2, vx: 0, vy: 0, alive: true, onGround: true });
698
+ const saved = saveState(state);
699
+ const loaded = loadState(saved, { config: { dt: 1, gravity: 0 } });
700
+ assert.ok(loaded);
701
+ assert.equal(loaded.score, state.score);
702
+ assert.equal(loaded.coins, state.coins);
703
+ assert.equal(loaded.level.tiles[2][1], " ");
704
+ assert.equal(loaded.level.tiles[1][1], "U");
705
+ assert.equal(loaded.enemies[0].alive, false);
706
+ assert.equal(loaded.items.length, 2);
707
+ });
708
+
709
+ test("loadState rejects invalid data", () => {
710
+ const invalidVersion = loadState({ version: 2 }, { config: { dt: 1, gravity: 0 } });
711
+ assert.equal(invalidVersion, null);
712
+ const missingLevel = loadState({ version: 1, level: {} }, { config: { dt: 1, gravity: 0 } });
713
+ assert.equal(missingLevel, null);
714
+ });
715
+
716
+ test("save/load preserves mushroom spawn flag", () => {
717
+ const level = makeLevel([
718
+ " ",
719
+ " ",
720
+ " ",
721
+ "####",
722
+ ]);
723
+ const state = createGame({
724
+ level,
725
+ startX: 1,
726
+ startY: 2,
727
+ config: { dt: 1, gravity: 0 },
728
+ });
729
+ state.mushroomSpawned = true;
730
+ const saved = saveState(state);
731
+ const loaded = loadState(saved, { config: { dt: 1, gravity: 0 } });
732
+ assert.ok(loaded);
733
+ assert.equal(loaded.mushroomSpawned, true);
734
+ });
735
+
736
+ test("hazard tiles include spikes and water", () => {
737
+ const level = makeLevel([
738
+ " ^~ ",
739
+ " ",
740
+ " ",
741
+ "####",
742
+ ]);
743
+ assert.equal(isHazardAt(level, 1, 0), true);
744
+ assert.equal(isHazardAt(level, 2, 0), true);
745
+ assert.equal(isHazardAt(level, 0, 0), false);
746
+ });
747
+
748
+ test("solid check treats out-of-bounds as solid", () => {
749
+ const level = makeLevel([
750
+ " ",
751
+ " ",
752
+ " ",
753
+ "####",
754
+ ]);
755
+ assert.equal(isSolidAt(level, -1, 0), true);
756
+ assert.equal(isSolidAt(level, 4, 0), true);
757
+ assert.equal(isSolidAt(level, 0, -1), true);
758
+ assert.equal(isSolidAt(level, 0, 4), true);
759
+ assert.equal(isSolidAt(level, 1, 1), false);
760
+ });
761
+
762
+ test("getTile returns empty for out-of-bounds", () => {
763
+ const level = makeLevel([
764
+ " ",
765
+ " ",
766
+ " ",
767
+ "####",
768
+ ]);
769
+ assert.equal(getTile(level, -1, 0), " ");
770
+ assert.equal(getTile(level, 4, 0), " ");
771
+ assert.equal(getTile(level, 0, -1), " ");
772
+ assert.equal(getTile(level, 0, 4), " ");
773
+ assert.equal(getTile(level, 1, 1), " ");
774
+ });
775
+
776
+ test("tileGlyph defaults to blanks for unknown tiles", () => {
777
+ assert.equal(tileGlyph("!"), " ");
778
+ });
779
+
780
+ test("saveState includes version", () => {
781
+ const level = makeLevel([
782
+ " ",
783
+ " ",
784
+ " ",
785
+ "####",
786
+ ]);
787
+ const state = createGame({
788
+ level,
789
+ startX: 1,
790
+ startY: 2,
791
+ config: { dt: 1, gravity: 0 },
792
+ });
793
+ const saved = saveState(state);
794
+ assert.equal(saved.version, 1);
795
+ assert.equal(saved.level.lines.length, 4);
796
+ });
797
+
798
+ test("snapshotState rounds numeric fields", () => {
799
+ const level = makeLevel([
800
+ " ",
801
+ " ",
802
+ " ",
803
+ "####",
804
+ ]);
805
+ const state = createGame({
806
+ level,
807
+ startX: 1,
808
+ startY: 2,
809
+ config: { dt: 1, gravity: 0 },
810
+ });
811
+ state.player.x = 1.23456;
812
+ state.player.y = 2.34567;
813
+ state.player.vx = 0.123456;
814
+ state.player.vy = -0.987654;
815
+ state.player.invuln = 1.23456;
816
+ state.time = 12.34567;
817
+ const snap = snapshotState(state);
818
+ assert.equal(snap.player.x, 1.235);
819
+ assert.equal(snap.player.y, 2.346);
820
+ assert.equal(snap.player.vx, 0.123);
821
+ assert.equal(snap.player.vy, -0.988);
822
+ assert.equal(snap.player.invuln, 1.235);
823
+ assert.equal(snap.time, 12.346);
824
+ });
825
+
826
+ test("camera clamps within bounds", () => {
827
+ const level = makeLevel([
828
+ " ",
829
+ " ",
830
+ " ",
831
+ "############",
832
+ ]);
833
+ const state = createGame({
834
+ level,
835
+ startX: 1,
836
+ startY: 2,
837
+ config: { dt: 1, gravity: 0, viewportWidth: 8 },
838
+ });
839
+ state.player.x = 1;
840
+ updateCamera(state);
841
+ assert.equal(getCameraX(state, 8), 0);
842
+ state.player.x = 10;
843
+ updateCamera(state);
844
+ assert.equal(getCameraX(state, 8), 4);
845
+ });
846
+
847
+ test("getCameraX honors override width", () => {
848
+ const level = makeLevel([
849
+ " ",
850
+ " ",
851
+ " ",
852
+ "########",
853
+ ]);
854
+ const state = createGame({
855
+ level,
856
+ startX: 1,
857
+ startY: 2,
858
+ config: { dt: 1, gravity: 0, viewportWidth: 6 },
859
+ });
860
+ state.cameraX = 3;
861
+ assert.equal(getCameraX(state, 4), 3);
862
+ });
863
+
864
+ test("camera dead-zone holds until edge", () => {
865
+ const level = makeLevel([
866
+ " ",
867
+ " ",
868
+ " ",
869
+ "####################",
870
+ ]);
871
+ const state = createGame({
872
+ level,
873
+ startX: 1,
874
+ startY: 2,
875
+ config: { dt: 1, gravity: 0, viewportWidth: 10 },
876
+ });
877
+ state.player.x = 5;
878
+ updateCamera(state);
879
+ assert.equal(state.cameraX, 3);
880
+ state.player.x = 8;
881
+ updateCamera(state);
882
+ assert.equal(state.cameraX, 6);
883
+ });
884
+
885
+ test("level1 dimensions match spec", () => {
886
+ assert.equal(LEVEL_1_LINES.length, LEVEL_1_HEIGHT);
887
+ assert.equal(LEVEL_1_LINES[0].length, LEVEL_1_WIDTH);
888
+ });