novac 2.3.0 → 2.4.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,2709 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * kitarcade — Terminal games kit
5
+ *
6
+ * Every game object shares this API:
7
+ * .start() restart / begin
8
+ * .input(key) key names: 'up' 'down' 'left' 'right' 'space' 'enter'
9
+ * 'escape' 'tab' and any single char ('a'–'z', '0'–'9')
10
+ * .tick(dt = 16) advance by dt ms; drive at your preferred fps
11
+ * .frame() → ANSI string ready to write to terminal
12
+ * .state → 'idle' | 'playing' | 'paused' | 'gameover' | 'won'
13
+ * .score → number
14
+ * .on(ev, fn) events: 'start' 'gameover' 'win' 'score'
15
+ * .off(ev, fn)
16
+ *
17
+ * Quick-start:
18
+ * const { kitdef } = require('./kitarcade');
19
+ * const game = kitdef.Snake();
20
+ * game.start();
21
+ * process.stdin.setRawMode(true);
22
+ * process.stdin.on('data', buf => {
23
+ * const k = buf.toString();
24
+ * if (k === '\x1b[A') game.input('up');
25
+ * else if (k === '\x1b[B') game.input('down');
26
+ * else if (k === '\x1b[C') game.input('right');
27
+ * else if (k === '\x1b[D') game.input('left');
28
+ * else if (k === ' ') game.input('space');
29
+ * else if (k === '\r') game.input('enter');
30
+ * else if (k === '\x1b') game.input('escape');
31
+ * else game.input(k.toLowerCase());
32
+ * });
33
+ * setInterval(() => {
34
+ * process.stdout.write('\x1b[H\x1b[2J' + game.frame());
35
+ * if (game.state === 'gameover') process.exit();
36
+ * }, 50);
37
+ *
38
+ * // or simply:
39
+ * kitdef.RunGame(kitdef.Snake());
40
+ *
41
+ * the RunGame helper sets up the stdin and tick loop for you, and handles clean exit on gameover.
42
+ * the manual setup is there if you want more control, e.g. for a custom tick rate, or to run multiple games at once, or even custom input handling.
43
+ */
44
+
45
+ // ─── SHARED UTILITIES ────────────────────────────────────────────────────────
46
+
47
+ const FC = {
48
+ blk:30,red:31,grn:32,yel:33,blu:34,mag:35,cyn:36,wht:37,
49
+ BLK:90,RED:91,GRN:92,YEL:93,BLU:94,MAG:95,CYN:96,WHT:97,
50
+ };
51
+ const BC = {
52
+ blk:40,red:41,grn:42,yel:43,blu:44,mag:45,cyn:46,wht:47,
53
+ BLK:100,RED:101,GRN:102,YEL:103,BLU:104,MAG:105,CYN:106,WHT:107,
54
+ };
55
+ const RST = '\x1b[0m';
56
+ const BLD = '\x1b[1m';
57
+ const DIM = '\x1b[2m';
58
+
59
+ function makeCanvas(W, H) {
60
+ const ch = Array.from({length:H}, () => new Array(W).fill(' '));
61
+ const fg = Array.from({length:H}, () => new Array(W).fill(FC.wht));
62
+ const bg = Array.from({length:H}, () => new Array(W).fill(BC.blk));
63
+ const cv = {
64
+ W, H,
65
+ clear(c=' ', f=FC.wht, b=BC.blk) {
66
+ for (let y=0;y<H;y++) for (let x=0;x<W;x++) { ch[y][x]=c;fg[y][x]=f;bg[y][x]=b; }
67
+ },
68
+ set(x, y, c, f=FC.wht, b=BC.blk) {
69
+ if (x<0||x>=W||y<0||y>=H) return;
70
+ ch[y][x] = (String(c)[0])||' '; fg[y][x]=f; bg[y][x]=b;
71
+ },
72
+ put(x, y, s, f=FC.wht, b=BC.blk) {
73
+ for (let i=0;i<s.length;i++) cv.set(x+i, y, s[i], f, b);
74
+ },
75
+ fill(x, y, w, h, c, f=FC.wht, b=BC.blk) {
76
+ for (let dy=0;dy<h;dy++) for (let dx=0;dx<w;dx++) cv.set(x+dx,y+dy,c,f,b);
77
+ },
78
+ box(x, y, w, h, f=FC.wht, b=BC.blk) {
79
+ cv.put(x, y, '┌'+'─'.repeat(w-2)+'┐', f, b);
80
+ for (let dy=1;dy<h-1;dy++) { cv.set(x,y+dy,'│',f,b); cv.set(x+w-1,y+dy,'│',f,b); }
81
+ cv.put(x, y+h-1, '└'+'─'.repeat(w-2)+'┘', f, b);
82
+ },
83
+ render() {
84
+ const lines = [];
85
+ for (let y=0;y<H;y++) {
86
+ let row='', cf=-1, cb=-1;
87
+ for (let x=0;x<W;x++) {
88
+ const f=fg[y][x], b=bg[y][x];
89
+ if (f!==cf||b!==cb) { row+=`\x1b[${f};${b}m`; cf=f; cb=b; }
90
+ row += ch[y][x];
91
+ }
92
+ lines.push(row+RST);
93
+ }
94
+ return lines.join('\n');
95
+ },
96
+ };
97
+ return cv;
98
+ }
99
+
100
+ function makeEmitter() {
101
+ const L = new Map();
102
+ return {
103
+ on(ev, fn) { if(!L.has(ev)) L.set(ev,[]); L.get(ev).push(fn); },
104
+ off(ev, fn) { if(L.has(ev)) L.set(ev, L.get(ev).filter(f=>f!==fn)); },
105
+ emit(ev, ...a) { (L.get(ev)??[]).forEach(fn=>fn(...a)); },
106
+ };
107
+ }
108
+
109
+ const rng = (n) => Math.random() * n | 0;
110
+ const clamp = (x, lo, hi) => Math.max(lo, Math.min(hi, x));
111
+ function shuffle(a) {
112
+ const b = [...a];
113
+ for (let i=b.length-1;i>0;i--) { const j=rng(i+1); [b[i],b[j]]=[b[j],b[i]]; }
114
+ return b;
115
+ }
116
+
117
+ // ─── WORD BANKS ───────────────────────────────────────────────────────────────
118
+
119
+ const WORDLE_WORDS = [
120
+ 'about','above','abuse','actor','acute','admit','adopt','adult','after','again',
121
+ 'agent','agree','ahead','alarm','album','alert','alike','alive','alley','allow',
122
+ 'alone','along','angel','anger','angle','angry','ankle','apply','arena','argue',
123
+ 'arise','armor','arson','array','aside','asset','atlas','attic','audio','avoid',
124
+ 'award','aware','awful','bacon','badge','baker','basic','batch','beard','beast',
125
+ 'bench','birth','black','blade','blank','blast','blaze','bleed','bless','blend',
126
+ 'blind','blood','bloom','blues','board','bonus','boost','bound','boxer','brain',
127
+ 'brand','brave','bread','break','breed','bride','brief','bring','broad','broke',
128
+ 'brush','buddy','build','built','bunch','burst','buyer','cabin','cable','carry',
129
+ 'catch','cause','chain','chaos','charm','check','chest','chose','chunk','civil',
130
+ 'claim','class','clean','clear','click','cliff','climb','clock','clone','close',
131
+ 'cloud','coach','coast','coral','could','count','cover','crack','craft','crane',
132
+ 'crazy','crime','crisp','cross','crowd','crown','crush','cubic','curve','cycle',
133
+ 'daily','dance','death','debut','delay','delta','dense','depot','derby','devil',
134
+ 'dirty','diver','dizzy','dodge','doing','doubt','dough','douse','draft','drain',
135
+ 'drama','dream','dress','drift','drink','drive','drone','drove','dwarf','dying',
136
+ 'eagle','early','earth','eight','elite','empty','enemy','enjoy','enter','entry',
137
+ 'equal','error','event','every','exact','exist','extra','fable','fairy','faith',
138
+ 'false','fancy','fatal','fault','feast','fence','field','fifth','fifty','fight',
139
+ ];
140
+
141
+ const HANGMAN_WORDS = [
142
+ 'elephant','giraffe','penguin','dolphin','universe','mountain','keyboard','chocolate',
143
+ 'adventure','symphony','democracy','laboratory','philosophy','electricity','caterpillar',
144
+ 'metamorphosis','catastrophe','bibliophile','serendipity','extraordinary','constellation',
145
+ 'hippopotamus','kaleidoscope','neighbourhood','thunderstorm','championship','algorithm',
146
+ 'certificate','competition','deliberately','entertainment','establishment','infrastructure',
147
+ 'investigation','knowledgeable','magnificent','opportunities','pharmaceutical','quarterback',
148
+ 'refrigerator','subscription','transformation','uncomfortable','vulnerability','xylophone',
149
+ 'accomplished','battlefield','comfortable','development','furthermore','government',
150
+ 'helicopter','immediately','journalism','kindergarten','linguistics','masterpiece',
151
+ 'notification','observatory','perspective','quadrilateral','refreshment','successfully',
152
+ ];
153
+
154
+ const TYPING_PHRASES = [
155
+ 'the quick brown fox jumps over the lazy dog and keeps running through the forest',
156
+ 'programming is the art of telling another human what one wants the computer to do',
157
+ 'any sufficiently advanced technology is indistinguishable from magic according to clarke',
158
+ 'in theory there is no difference between theory and practice but in practice there is',
159
+ 'first solve the problem then write the code otherwise you are just writing bad code faster',
160
+ 'the best error message is the one that never shows up because you prevented the bug',
161
+ 'debugging is like being the detective in a crime movie where you are also the murderer',
162
+ 'code is like humor when you have to explain it it is not that good at all really',
163
+ ];
164
+
165
+ // ─── SUDOKU PUZZLES ───────────────────────────────────────────────────────────
166
+
167
+ const SUDOKU_PUZZLES = [
168
+ '53..7....6..195....98....6.8...6...34..8.3..17...2...6.6....28....419..5....8..79',
169
+ '.......1.4.........2...........5.4.7..8...3....1.9....3..4..2...5.1........8.6...',
170
+ '..9748...7.........2.1.9.....7...24..64.1.59..98...3.....8.3.2.........6...2759..',
171
+ '1....7.9..3..2...8..96..5....53..9...1..8...26....4...3......1..4......7..7...3..',
172
+ '.3.5..8.5.5.9...2...7.8.1.7.4...5.......3..1...7...6.....5..........8.....6.....',
173
+ ];
174
+
175
+ // ─── GAME: SNAKE ─────────────────────────────────────────────────────────────
176
+
177
+ function makeSnake(opts = {}) {
178
+ const GW = opts.width ?? 30;
179
+ const GH = opts.height ?? 20;
180
+ const em = makeEmitter();
181
+ let snake, dir, nextDir, food, score, state, elapsed, speed;
182
+
183
+ function spawnFood() {
184
+ let fx, fy;
185
+ do { fx=rng(GW); fy=rng(GH); } while (snake.some(s=>s.x===fx&&s.y===fy));
186
+ food = {x:fx, y:fy};
187
+ }
188
+
189
+ function start() {
190
+ snake = [{x:5,y:GH>>1},{x:4,y:GH>>1},{x:3,y:GH>>1}];
191
+ dir = {dx:1,dy:0}; nextDir = {dx:1,dy:0};
192
+ score = 0; elapsed = 0; speed = 150; state = 'playing';
193
+ spawnFood(); em.emit('start');
194
+ }
195
+
196
+ function input(key) {
197
+ if (state === 'gameover' && key === 'enter') { start(); return; }
198
+ if (state !== 'playing') return;
199
+ const nd = { up:{dx:0,dy:-1}, down:{dx:0,dy:1}, left:{dx:-1,dy:0}, right:{dx:1,dy:0} }[key];
200
+ if (nd && !(nd.dx===-dir.dx && nd.dy===-dir.dy)) nextDir = nd;
201
+ }
202
+
203
+ function step() {
204
+ dir = nextDir;
205
+ const head = snake[0];
206
+ const nx = head.x + dir.dx, ny = head.y + dir.dy;
207
+ if (nx<0||nx>=GW||ny<0||ny>=GH||snake.some(s=>s.x===nx&&s.y===ny)) {
208
+ state = 'gameover'; em.emit('gameover', score); return;
209
+ }
210
+ snake.unshift({x:nx,y:ny});
211
+ if (nx===food.x && ny===food.y) {
212
+ score += 10; speed = Math.max(60, speed - 2);
213
+ em.emit('score', score); spawnFood();
214
+ } else { snake.pop(); }
215
+ }
216
+
217
+ function tick(dt=16) {
218
+ if (state !== 'playing') return;
219
+ elapsed += dt;
220
+ while (elapsed >= speed) { elapsed -= speed; step(); if (state!=='playing') break; }
221
+ }
222
+
223
+ function frame() {
224
+ const cv = makeCanvas(GW*2+2, GH+3);
225
+ cv.clear(' ', FC.wht, BC.blk);
226
+ // title
227
+ cv.put(0, 0, ` SNAKE Score: ${score} Length: ${snake.length} [WASD/Arrows]`, FC.YEL, BC.blk);
228
+ // border
229
+ cv.box(0, 1, GW*2+2, GH+2, FC.wht, BC.blk);
230
+ // food
231
+ cv.put(food.x*2+1, food.y+2, '()', FC.RED, BC.blk);
232
+ // snake
233
+ for (let i=snake.length-1;i>=0;i--) {
234
+ const s=snake[i];
235
+ const isHead = i===0;
236
+ const col = isHead ? FC.GRN : (i%2===0 ? FC.grn : FC.GRN);
237
+ cv.put(s.x*2+1, s.y+2, isHead?'◉◉':'██', col, BC.blk);
238
+ }
239
+ if (state==='gameover') {
240
+ const msg=' GAME OVER Press ENTER ';
241
+ cv.put((GW*2+2-msg.length)>>1, (GH>>1)+2, msg, FC.WHT, BC.red);
242
+ }
243
+ return cv.render();
244
+ }
245
+
246
+ return { start, input, tick, frame,
247
+ on:em.on.bind(em), off:em.off.bind(em),
248
+ get state(){return state;}, get score(){return score;} };
249
+ }
250
+
251
+ // ─── GAME: TETRIS ─────────────────────────────────────────────────────────────
252
+
253
+ const PIECES = {
254
+ I:{col:FC.CYN,shapes:[[[0,0,0,0],[1,1,1,1],[0,0,0,0],[0,0,0,0]],[[0,0,1,0],[0,0,1,0],[0,0,1,0],[0,0,1,0]],[[0,0,0,0],[0,0,0,0],[1,1,1,1],[0,0,0,0]],[[0,1,0,0],[0,1,0,0],[0,1,0,0],[0,1,0,0]]]},
255
+ O:{col:FC.YEL,shapes:[[[1,1],[1,1]],[[1,1],[1,1]],[[1,1],[1,1]],[[1,1],[1,1]]]},
256
+ T:{col:FC.MAG,shapes:[[[0,1,0],[1,1,1],[0,0,0]],[[0,1,0],[0,1,1],[0,1,0]],[[0,0,0],[1,1,1],[0,1,0]],[[0,1,0],[1,1,0],[0,1,0]]]},
257
+ S:{col:FC.GRN,shapes:[[[0,1,1],[1,1,0],[0,0,0]],[[0,1,0],[0,1,1],[0,0,1]],[[0,0,0],[0,1,1],[1,1,0]],[[1,0,0],[1,1,0],[0,1,0]]]},
258
+ Z:{col:FC.RED,shapes:[[[1,1,0],[0,1,1],[0,0,0]],[[0,0,1],[0,1,1],[0,1,0]],[[0,0,0],[1,1,0],[0,1,1]],[[0,1,0],[1,1,0],[1,0,0]]]},
259
+ J:{col:FC.BLU,shapes:[[[1,0,0],[1,1,1],[0,0,0]],[[0,1,1],[0,1,0],[0,1,0]],[[0,0,0],[1,1,1],[0,0,1]],[[0,1,0],[0,1,0],[1,1,0]]]},
260
+ L:{col:FC.yel,shapes:[[[0,0,1],[1,1,1],[0,0,0]],[[0,1,0],[0,1,0],[0,1,1]],[[0,0,0],[1,1,1],[1,0,0]],[[1,1,0],[0,1,0],[0,1,0]]]},
261
+ };
262
+ const PIECE_KEYS = Object.keys(PIECES);
263
+ const SCORE_TABLE = [0,100,300,500,800];
264
+
265
+ function makeTetris(opts = {}) {
266
+ const BW=10, BH=20;
267
+ const em = makeEmitter();
268
+ let board, colors, current, rotation, px, py, hold, holdUsed, bag, next;
269
+ let score, level, lines, state, elapsed, gravity;
270
+
271
+ function newBag() { return shuffle(PIECE_KEYS); }
272
+ function nextPiece() {
273
+ if (!bag.length) bag = newBag();
274
+ return bag.shift();
275
+ }
276
+
277
+ function spawn(key) {
278
+ current = key; rotation = 0;
279
+ const shape = PIECES[key].shapes[0];
280
+ px = (BW - shape[0].length) >> 1; py = 0;
281
+ if (collides(px, py, rotation)) { state='gameover'; em.emit('gameover',score); }
282
+ }
283
+
284
+ function collides(tx, ty, rot) {
285
+ const shape = PIECES[current].shapes[rot];
286
+ for (let r=0;r<shape.length;r++)
287
+ for (let c=0;c<shape[r].length;c++)
288
+ if (shape[r][c]) {
289
+ const nx=tx+c, ny=ty+r;
290
+ if (nx<0||nx>=BW||ny>=BH) return true;
291
+ if (ny>=0 && board[ny*BW+nx]) return true;
292
+ }
293
+ return false;
294
+ }
295
+
296
+ function lock() {
297
+ const shape = PIECES[current].shapes[rotation];
298
+ const col = PIECES[current].col;
299
+ for (let r=0;r<shape.length;r++)
300
+ for (let c=0;c<shape[r].length;c++)
301
+ if (shape[r][c] && py+r>=0) {
302
+ board[(py+r)*BW+(px+c)] = 1;
303
+ colors[(py+r)*BW+(px+c)] = col;
304
+ }
305
+ // clear lines
306
+ let cleared = 0;
307
+ for (let r=BH-1;r>=0;r--) {
308
+ if ([...Array(BW)].every((_,c)=>board[r*BW+c])) {
309
+ board.splice(r*BW, BW); colors.splice(r*BW, BW);
310
+ board.unshift(...new Array(BW).fill(0));
311
+ colors.unshift(...new Array(BW).fill(0));
312
+ cleared++; r++;
313
+ }
314
+ }
315
+ if (cleared) {
316
+ lines += cleared; score += SCORE_TABLE[cleared] * level;
317
+ level = Math.floor(lines/10)+1; gravity = Math.max(50, 1000-level*90);
318
+ em.emit('score', score);
319
+ }
320
+ holdUsed = false;
321
+ spawn(nextPiece());
322
+ }
323
+
324
+ function start() {
325
+ board = new Array(BW*BH).fill(0);
326
+ colors = new Array(BW*BH).fill(0);
327
+ bag = newBag(); next = newBag(); hold = null; holdUsed = false;
328
+ score = 0; level = 1; lines = 0; elapsed = 0; gravity = 1000; state = 'playing';
329
+ spawn(nextPiece()); em.emit('start');
330
+ }
331
+
332
+ function doHold() {
333
+ if (holdUsed) return;
334
+ holdUsed = true;
335
+ const prev = hold;
336
+ hold = current;
337
+ spawn(prev ?? nextPiece());
338
+ }
339
+
340
+ function input(key) {
341
+ if (state==='gameover'&&key==='enter') { start(); return; }
342
+ if (state!=='playing') return;
343
+ if (key==='left' && !collides(px-1,py,rotation)) px--;
344
+ if (key==='right' && !collides(px+1,py,rotation)) px++;
345
+ if (key==='down' && !collides(px,py+1,rotation)) { py++; score+=1; }
346
+ if (key==='up'||key==='z') {
347
+ const nr=(rotation+1)%4;
348
+ if (!collides(px,py,nr)) rotation=nr;
349
+ else if (!collides(px-1,py,nr)) { px--; rotation=nr; }
350
+ else if (!collides(px+1,py,nr)) { px++; rotation=nr; }
351
+ }
352
+ if (key==='space') { // hard drop
353
+ while (!collides(px,py+1,rotation)) { py++; score+=2; }
354
+ lock();
355
+ }
356
+ if (key==='c'||key==='tab') doHold();
357
+ }
358
+
359
+ function tick(dt=16) {
360
+ if (state!=='playing') return;
361
+ elapsed+=dt;
362
+ if (elapsed>=gravity) { elapsed=0; if (!collides(px,py+1,rotation)) py++; else lock(); }
363
+ }
364
+
365
+ function ghostY() {
366
+ let gy=py;
367
+ while (!collides(px,gy+1,rotation)) gy++;
368
+ return gy;
369
+ }
370
+
371
+ function frame() {
372
+ const SIDE=14; const CW=BW*2+2+SIDE+2;
373
+ const cv=makeCanvas(CW, BH+3); cv.clear(' ',FC.wht,BC.blk);
374
+ cv.put(0,0,`${BLD} TETRIS ${RST} Lv:${level} Lines:${lines} Score:${score} [←→↓ Move ↑ Rotate Space Hard-drop C Hold]`,FC.YEL,BC.blk);
375
+ // board border
376
+ cv.box(0,1,BW*2+2,BH+2,FC.wht,BC.blk);
377
+ // board cells
378
+ for (let r=0;r<BH;r++) for (let c=0;c<BW;c++) {
379
+ if (board[r*BW+c]) cv.put(c*2+1,r+2,'██',colors[r*BW+c],BC.blk);
380
+ }
381
+ // ghost
382
+ const gy=ghostY();
383
+ const gs=PIECES[current].shapes[rotation];
384
+ for (let r=0;r<gs.length;r++) for (let c=0;c<gs[r].length;c++)
385
+ if (gs[r][c] && gy+r>=0) cv.put((px+c)*2+1,gy+r+2,'░░',FC.BLK,BC.blk);
386
+ // current piece
387
+ for (let r=0;r<gs.length;r++) for (let c=0;c<gs[r].length;c++)
388
+ if (gs[r][c] && py+r>=0) cv.put((px+c)*2+1,py+r+2,'██',PIECES[current].col,BC.blk);
389
+ // sidebar
390
+ const sx=BW*2+3;
391
+ cv.put(sx,2,'NEXT:',FC.CYN,BC.blk);
392
+ if (bag.length) {
393
+ const nk=bag[0], ns=PIECES[nk].shapes[0];
394
+ for (let r=0;r<ns.length;r++) for (let c=0;c<ns[r].length;c++)
395
+ if (ns[r][c]) cv.put(sx+c*2,3+r,'██',PIECES[nk].col,BC.blk);
396
+ }
397
+ cv.put(sx,8,`HOLD: ${hold??'---'}`,FC.CYN,BC.blk);
398
+ if (hold) {
399
+ const hs=PIECES[hold].shapes[0];
400
+ for (let r=0;r<hs.length;r++) for (let c=0;c<hs[r].length;c++)
401
+ if (hs[r][c]) cv.put(sx+c*2,9+r,'██',holdUsed?FC.BLK:PIECES[hold].col,BC.blk);
402
+ }
403
+ if (state==='gameover') cv.put(1,BH/2+2|0,' GAME OVER ENTER to restart ',FC.WHT,BC.red);
404
+ return cv.render();
405
+ }
406
+
407
+ return { start, input, tick, frame,
408
+ on:em.on.bind(em), off:em.off.bind(em),
409
+ get state(){return state;}, get score(){return score;} };
410
+ }
411
+
412
+ // ─── GAME: DINO RUN ──────────────────────────────────────────────────────────
413
+
414
+ function makeDinoRun(opts = {}) {
415
+ const W=80, H=9;
416
+ const GROUND=6, DINO_X=5;
417
+ const em=makeEmitter();
418
+ let dinoY, velY, onGround, obstacles, clouds, score, speed, elapsed, state, frame_n;
419
+
420
+ function start() {
421
+ dinoY=GROUND; velY=0; onGround=true;
422
+ obstacles=[]; clouds=[{x:60,y:1},{x:40,y:2},{x:20,y:1}];
423
+ score=0; speed=0.15; elapsed=0; frame_n=0; state='playing';
424
+ em.emit('start');
425
+ }
426
+
427
+ function input(key) {
428
+ if (state==='gameover'&&key==='enter') { start(); return; }
429
+ if (state!=='playing') return;
430
+ if ((key==='space'||key==='up')&&onGround) { velY=-1.4; onGround=false; }
431
+ if ((key==='down')&&!onGround) velY+=0.5; // fast fall
432
+ }
433
+
434
+ function tick(dt=16) {
435
+ if (state!=='playing') return;
436
+ elapsed+=dt;
437
+ if (elapsed<50) return; elapsed=0; frame_n++;
438
+
439
+ // physics
440
+ velY+=0.2; dinoY+=velY;
441
+ if (dinoY>=GROUND) { dinoY=GROUND; velY=0; onGround=true; }
442
+
443
+ // scroll
444
+ speed = Math.min(0.45, 0.15 + score*0.0003);
445
+ const dx = Math.ceil(speed*10);
446
+
447
+ // obstacles
448
+ for (const o of obstacles) o.x -= dx;
449
+ obstacles = obstacles.filter(o=>o.x>-3);
450
+ if (!obstacles.length || obstacles[obstacles.length-1].x < 60-rng(30)) {
451
+ const h = 1+rng(2);
452
+ obstacles.push({x:W+2, h, w:1+rng(2)});
453
+ }
454
+
455
+ // clouds
456
+ for (const c of clouds) c.x -= 1;
457
+ clouds = clouds.filter(c=>c.x>-5);
458
+ if (clouds.length<3) clouds.push({x:W+5, y:1+rng(3)});
459
+
460
+ // score
461
+ score++; em.emit('score', score);
462
+
463
+ // collision: dino occupies cols DINO_X..DINO_X+1, rows dinoY-1..dinoY
464
+ const dy0=Math.round(dinoY)-1, dy1=Math.round(dinoY);
465
+ for (const o of obstacles) {
466
+ const ox0=o.x, ox1=o.x+o.w-1;
467
+ const oy0=GROUND-o.h, oy1=GROUND;
468
+ if (ox0<=DINO_X+1 && ox1>=DINO_X && oy0<=dy1 && oy1>=dy0) {
469
+ state='gameover'; em.emit('gameover',score); return;
470
+ }
471
+ }
472
+ }
473
+
474
+ function frame() {
475
+ const cv=makeCanvas(W,H); cv.clear(' ',FC.wht,BC.blk);
476
+ cv.put(0,0,` DINO RUN Score: ${score} Speed: x${speed.toFixed(2)} SPACE/UP to jump ENTER restart`,FC.YEL,BC.blk);
477
+ // clouds
478
+ for (const c of clouds) if (c.x>=0&&c.x<W) cv.put(c.x,c.y,'~~~',FC.BLK,BC.blk);
479
+ // ground
480
+ cv.put(0,GROUND+1,'─'.repeat(W),FC.wht,BC.blk);
481
+ cv.put(0,GROUND+2,'═'.repeat(W),FC.BLK,BC.blk);
482
+ // dino
483
+ const dy=Math.round(dinoY);
484
+ const blink = frame_n%4<2;
485
+ cv.put(DINO_X,dy-1, blink?'(Ö)':'(Ȯ)', FC.GRN,BC.blk);
486
+ cv.put(DINO_X,dy, onGround?(frame_n%4<2?'/|\\':'|/\\'):'(_)', FC.GRN,BC.blk);
487
+ // obstacles
488
+ for (const o of obstacles) {
489
+ if (o.x<0||o.x>=W) continue;
490
+ for (let h=0;h<o.h;h++) {
491
+ const row=GROUND-h;
492
+ for (let w=0;w<o.w;w++) cv.put(o.x+w,row,h===0?'╤':'║',FC.grn,BC.blk);
493
+ }
494
+ }
495
+ if (state==='gameover') cv.put((W-28)>>1,4,' GAME OVER ENTER to restart ',FC.WHT,BC.red);
496
+ return cv.render();
497
+ }
498
+
499
+ return { start, input, tick, frame,
500
+ on:em.on.bind(em), off:em.off.bind(em),
501
+ get state(){return state;}, get score(){return score;} };
502
+ }
503
+
504
+ // ─── GAME: BREAKOUT ──────────────────────────────────────────────────────────
505
+
506
+ function makeBreakout(opts = {}) {
507
+ const W=50, H=24;
508
+ const ROWS=5, COLS=10, PAD_W=8, BALL_SPD=1;
509
+ const BROW_COLORS=[FC.RED,FC.YEL,FC.GRN,FC.CYN,FC.BLU];
510
+ const em=makeEmitter();
511
+ let bricks, ball, padX, score, lives, state, elapsed;
512
+
513
+ function makeBricks() {
514
+ return Array.from({length:ROWS}, (_,r)=>
515
+ Array.from({length:COLS}, (_,c)=>({alive:true, row:r, col:c}))
516
+ ).flat();
517
+ }
518
+
519
+ function start() {
520
+ bricks=makeBricks();
521
+ padX=(W-PAD_W)>>1;
522
+ ball={x:25,y:18,dx:1,dy:-1};
523
+ score=0; lives=3; elapsed=0; state='playing';
524
+ em.emit('start');
525
+ }
526
+
527
+ function input(key) {
528
+ if (state==='gameover'&&key==='enter'){ start(); return; }
529
+ if (state==='won'&&key==='enter'){ start(); return; }
530
+ if (state!=='playing') return;
531
+ if (key==='left') padX=Math.max(0,padX-2);
532
+ if (key==='right') padX=Math.min(W-PAD_W,padX+2);
533
+ }
534
+
535
+ function step() {
536
+ let nx=ball.x+ball.dx, ny=ball.y+ball.dy;
537
+ // wall bounces
538
+ if (nx<=0||nx>=W-1) { ball.dx=-ball.dx; nx=ball.x+ball.dx; }
539
+ if (ny<=1) { ball.dy=-ball.dy; ny=ball.y+ball.dy; }
540
+ // paddle
541
+ if (ny>=H-3 && nx>=padX && nx<padX+PAD_W && ball.dy>0) {
542
+ ball.dy=-ball.dy;
543
+ ball.dx = ((nx-padX)/PAD_W*4-2)|0 || ball.dx;
544
+ ball.dx = clamp(ball.dx,-2,2); if(!ball.dx) ball.dx=1;
545
+ ny=ball.y+ball.dy;
546
+ }
547
+ // miss
548
+ if (ny>=H-1) {
549
+ lives--; if (!lives) { state='gameover'; em.emit('gameover',score); return; }
550
+ ball={x:25,y:18,dx:ball.dx>0?1:-1,dy:-1};
551
+ return;
552
+ }
553
+ // brick collision
554
+ for (const b of bricks) {
555
+ if (!b.alive) continue;
556
+ const bx=(b.col*5)+0, by=b.row+2;
557
+ if (nx>=bx&&nx<bx+5&&ny===by) {
558
+ b.alive=false; ball.dy=-ball.dy; ny=ball.y+ball.dy;
559
+ score+=10*(ROWS-b.row); em.emit('score',score); break;
560
+ }
561
+ }
562
+ ball.x=nx; ball.y=ny;
563
+ if (bricks.every(b=>!b.alive)) { state='won'; em.emit('win',score); }
564
+ }
565
+
566
+ function tick(dt=16) {
567
+ if (state!=='playing') return;
568
+ elapsed+=dt;
569
+ while (elapsed>=80) { elapsed-=80; step(); }
570
+ }
571
+
572
+ function frame() {
573
+ const cv=makeCanvas(W,H); cv.clear(' ',FC.wht,BC.blk);
574
+ cv.put(0,0,` BREAKOUT Score:${score} Lives:${'♥'.repeat(lives)} [←→ Move]`,FC.YEL,BC.blk);
575
+ cv.put(0,1,'─'.repeat(W),FC.wht,BC.blk);
576
+ // bricks
577
+ for (const b of bricks) {
578
+ if (!b.alive) continue;
579
+ const bx=b.col*5, by=b.row+2;
580
+ cv.put(bx,by,'▓▓▓▓▓',BROW_COLORS[b.row],BC.blk);
581
+ }
582
+ // ball
583
+ cv.set(ball.x,ball.y,'●',FC.WHT,BC.blk);
584
+ // paddle
585
+ cv.put(padX,H-2,'▀'.repeat(PAD_W),FC.CYN,BC.blk);
586
+ cv.put(0,H-1,'─'.repeat(W),FC.BLK,BC.blk);
587
+ if (state==='gameover') cv.put((W-28)>>1,H>>1,' GAME OVER ENTER to restart ',FC.WHT,BC.red);
588
+ if (state==='won') cv.put((W-24)>>1,H>>1,' YOU WIN! ENTER to play again ',FC.WHT,BC.grn);
589
+ return cv.render();
590
+ }
591
+
592
+ return { start, input, tick, frame,
593
+ on:em.on.bind(em), off:em.off.bind(em),
594
+ get state(){return state;}, get score(){return score;} };
595
+ }
596
+
597
+ // ─── GAME: SPACE INVADERS ────────────────────────────────────────────────────
598
+
599
+ function makeSpaceInvaders(opts = {}) {
600
+ const W=60,H=24,COLS=10,ROWS=4;
601
+ const em=makeEmitter();
602
+ let aliens,alienDir,alienDx,playerX,bullets,aBullets,score,lives,state,elapsed,fireTimer;
603
+
604
+ const ALIEN_CH = ['◉◉','▓▓','░░','▒▒'];
605
+
606
+ function spawnAliens() {
607
+ aliens=[];
608
+ for (let r=0;r<ROWS;r++) for (let c=0;c<COLS;c++)
609
+ aliens.push({x:c*5+5,y:r*2+2,alive:true,row:r});
610
+ alienDir=1; alienDx=0;
611
+ }
612
+
613
+ function start() {
614
+ spawnAliens(); playerX=W>>1; bullets=[]; aBullets=[]; fireTimer=0;
615
+ score=0; lives=3; elapsed=0; state='playing'; em.emit('start');
616
+ }
617
+
618
+ function input(key) {
619
+ if (state==='gameover'&&key==='enter'){ start(); return; }
620
+ if (state!=='playing') return;
621
+ if (key==='left') playerX=Math.max(1,playerX-3);
622
+ if (key==='right') playerX=Math.min(W-2,playerX+3);
623
+ if (key==='space'||key==='z') bullets.push({x:playerX,y:H-4});
624
+ }
625
+
626
+ function step() {
627
+ const alive=aliens.filter(a=>a.alive);
628
+ if (!alive.length) { state='won'; em.emit('win',score); return; }
629
+
630
+ // move aliens
631
+ alienDx+=alienDir;
632
+ const minX=Math.min(...alive.map(a=>a.x));
633
+ const maxX=Math.max(...alive.map(a=>a.x))+2;
634
+ if (maxX>=W-1||minX<=1) {
635
+ alienDir=-alienDir;
636
+ for (const a of aliens) a.y++;
637
+ alienDx=0;
638
+ } else {
639
+ for (const a of aliens) a.x+=alienDir;
640
+ }
641
+
642
+ // aliens reached bottom
643
+ if (alive.some(a=>a.y>=H-4)) { state='gameover'; em.emit('gameover',score); return; }
644
+
645
+ // alien fire
646
+ fireTimer--;
647
+ if (fireTimer<=0) {
648
+ fireTimer=8+rng(12);
649
+ const shooter=alive[rng(alive.length)];
650
+ if (shooter) aBullets.push({x:shooter.x+1,y:shooter.y+1});
651
+ }
652
+
653
+ // move bullets
654
+ bullets = bullets.filter(b=>b.y>1).map(b=>({...b,y:b.y-2}));
655
+ aBullets = aBullets.filter(b=>b.y<H-2).map(b=>({...b,y:b.y+1}));
656
+
657
+ // bullet hits alien
658
+ for (const b of [...bullets]) {
659
+ for (const a of alive) {
660
+ if (Math.abs(b.x-a.x)<=1&&Math.abs(b.y-a.y)<=1) {
661
+ a.alive=false; bullets=bullets.filter(x=>x!==b);
662
+ score+=(ROWS-a.row)*10; em.emit('score',score); break;
663
+ }
664
+ }
665
+ }
666
+
667
+ // alien bullet hits player
668
+ for (const b of aBullets) {
669
+ if (Math.abs(b.x-playerX)<=1&&b.y>=H-4) {
670
+ aBullets=aBullets.filter(x=>x!==b); lives--;
671
+ if (!lives) { state='gameover'; em.emit('gameover',score); return; }
672
+ }
673
+ }
674
+ }
675
+
676
+ function tick(dt=16) {
677
+ if (state!=='playing') return;
678
+ elapsed+=dt;
679
+ const sp=Math.max(60,200-score/2|0);
680
+ while (elapsed>=sp) { elapsed-=sp; step(); if(state!=='playing') break; }
681
+ }
682
+
683
+ function frame() {
684
+ const cv=makeCanvas(W,H); cv.clear(' ',FC.wht,BC.blk);
685
+ cv.put(0,0,` SPACE INVADERS Score:${score} Lives:${'♥'.repeat(lives)} [←→ Move SPACE Fire]`,FC.YEL,BC.blk);
686
+ cv.put(0,1,'═'.repeat(W),FC.BLU,BC.blk);
687
+ for (const a of aliens) if (a.alive)
688
+ cv.put(a.x,a.y,ALIEN_CH[a.row],FC.GRN-a.row,BC.blk);
689
+ for (const b of bullets) cv.set(b.x,b.y,'|',FC.WHT,BC.blk);
690
+ for (const b of aBullets) cv.set(b.x,b.y,'!',FC.RED,BC.blk);
691
+ cv.put(playerX-1,H-3,'╔═╗',FC.CYN,BC.blk);
692
+ cv.put(playerX-2,H-2,'▓▓▓▓▓',FC.CYN,BC.blk);
693
+ cv.put(0,H-1,'═'.repeat(W),FC.wht,BC.blk);
694
+ if (state==='gameover') cv.put((W-28)>>1,H>>1,' GAME OVER ENTER to restart ',FC.WHT,BC.red);
695
+ if (state==='won') cv.put((W-24)>>1,H>>1,' CLEARED! ENTER for more ',FC.WHT,BC.grn);
696
+ return cv.render();
697
+ }
698
+
699
+ return { start, input, tick, frame,
700
+ on:em.on.bind(em), off:em.off.bind(em),
701
+ get state(){return state;}, get score(){return score;} };
702
+ }
703
+
704
+ // ─── GAME: FLAPPY BIRD ───────────────────────────────────────────────────────
705
+
706
+ function makeFlappyBird(opts = {}) {
707
+ const W=60,H=20,BIRD_X=10,GAP=6;
708
+ const em=makeEmitter();
709
+ let birdY,velY,pipes,score,state,elapsed,speed;
710
+
711
+ function start() {
712
+ birdY=H>>1; velY=0; pipes=[]; score=0; elapsed=0; speed=120; state='playing';
713
+ pipes.push({x:W+5, gap:4+rng(H-GAP-4)});
714
+ em.emit('start');
715
+ }
716
+
717
+ function input(key) {
718
+ if (state==='gameover'&&key==='enter'){ start(); return; }
719
+ if (state==='idle'&&(key==='space'||key==='enter')){ state='playing'; return; }
720
+ if (state!=='playing') return;
721
+ if (key==='space'||key==='up') velY=-1.1;
722
+ }
723
+
724
+ function step() {
725
+ velY+=0.25; birdY+=velY;
726
+ if (birdY<0||birdY>=H-1) { state='gameover'; em.emit('gameover',score); return; }
727
+
728
+ for (const p of pipes) {
729
+ p.x--;
730
+ if (Math.abs(p.x-BIRD_X)<2) {
731
+ if (birdY<p.gap||birdY>p.gap+GAP) { state='gameover'; em.emit('gameover',score); return; }
732
+ }
733
+ if (p.x===BIRD_X-1) { score++; speed=Math.max(60,speed-3); em.emit('score',score); }
734
+ }
735
+ pipes=pipes.filter(p=>p.x>-2);
736
+ if (!pipes.length||pipes[pipes.length-1].x<W-15+rng(5))
737
+ pipes.push({x:W+2,gap:2+rng(H-GAP-3)});
738
+ }
739
+
740
+ function tick(dt=16) {
741
+ if (state!=='playing') return;
742
+ elapsed+=dt;
743
+ while (elapsed>=speed) { elapsed-=speed; step(); if(state!=='playing') break; }
744
+ }
745
+
746
+ function frame() {
747
+ const cv=makeCanvas(W,H); cv.clear(' ',FC.BLU,BC.blk);
748
+ cv.put(0,0,` FLAPPY BIRD Score:${score} SPACE to flap`,FC.YEL,BC.blk);
749
+ for (const p of pipes) {
750
+ if (p.x<0||p.x>=W) continue;
751
+ for (let y=1;y<H;y++) {
752
+ if (y<p.gap||y>p.gap+GAP)
753
+ cv.put(p.x,y,'│▓',FC.grn,BC.blk);
754
+ }
755
+ cv.put(p.x,p.gap,'╙─',FC.GRN,BC.blk);
756
+ cv.put(p.x,p.gap+GAP,'╒─',FC.GRN,BC.blk);
757
+ }
758
+ const by=Math.round(birdY);
759
+ cv.put(BIRD_X,by, velY<0?'>O)':'>o)',FC.YEL,BC.blk);
760
+ if (state==='gameover') cv.put((W-28)>>1,H>>1,' GAME OVER ENTER to restart ',FC.WHT,BC.red);
761
+ return cv.render();
762
+ }
763
+
764
+ return { start, input, tick, frame,
765
+ on:em.on.bind(em), off:em.off.bind(em),
766
+ get state(){return state;}, get score(){return score;} };
767
+ }
768
+
769
+ // ─── GAME: ASTEROIDS (DODGE) ─────────────────────────────────────────────────
770
+
771
+ function makeAsteroids(opts = {}) {
772
+ const W=60,H=20;
773
+ const em=makeEmitter();
774
+ let px,py,rocks,score,state,elapsed,speed;
775
+ const ROCK_CHARS=['●','◉','◎','○','◈','◇'];
776
+
777
+ function spawnRock() {
778
+ return {x:W-1, y:rng(H-2)+1, w:1+rng(2), speed:1+rng(3)/3, ch:ROCK_CHARS[rng(ROCK_CHARS.length)]};
779
+ }
780
+
781
+ function start() {
782
+ px=10; py=H>>1; rocks=[]; score=0; elapsed=0; speed=60; state='playing';
783
+ for (let i=0;i<3;i++) rocks.push({...spawnRock(),x:W-5-rng(30)});
784
+ em.emit('start');
785
+ }
786
+
787
+ function input(key) {
788
+ if (state==='gameover'&&key==='enter'){ start(); return; }
789
+ if (state!=='playing') return;
790
+ if (key==='up') py=Math.max(1,py-1);
791
+ if (key==='down') py=Math.min(H-2,py+1);
792
+ if (key==='left') px=Math.max(0,px-2);
793
+ if (key==='right') px=Math.min(W-3,px+2);
794
+ }
795
+
796
+ function tick(dt=16) {
797
+ if (state!=='playing') return;
798
+ elapsed+=dt;
799
+ if (elapsed<speed) return; elapsed=0;
800
+ score++; speed=Math.max(30,60-score/30|0);
801
+
802
+ for (const r of rocks) r.x-=r.speed;
803
+ rocks=rocks.filter(r=>r.x>-3);
804
+ while (rocks.length<4+((score/100)|0)) rocks.push(spawnRock());
805
+
806
+ // collision
807
+ for (const r of rocks) {
808
+ if (Math.abs(r.x-px)<=2&&Math.abs(r.y-py)<=1) {
809
+ state='gameover'; em.emit('gameover',score); return;
810
+ }
811
+ }
812
+ em.emit('score',score);
813
+ }
814
+
815
+ function frame() {
816
+ const cv=makeCanvas(W,H); cv.clear('·',FC.BLK,BC.blk);
817
+ cv.put(0,0,` ASTEROIDS Score:${score} [WASD/Arrows dodge rocks]`,FC.YEL,BC.blk);
818
+ for (const r of rocks) {
819
+ const rx=Math.round(r.x);
820
+ if (rx>=0&&rx<W) cv.put(rx,r.y,r.ch.repeat(r.w),FC.RED,BC.blk);
821
+ }
822
+ cv.put(px,py,'<Ö>',FC.CYN,BC.blk);
823
+ if (state==='gameover') cv.put((W-28)>>1,H>>1,' GAME OVER ENTER to restart ',FC.WHT,BC.red);
824
+ return cv.render();
825
+ }
826
+
827
+ return { start, input, tick, frame,
828
+ on:em.on.bind(em), off:em.off.bind(em),
829
+ get state(){return state;}, get score(){return score;} };
830
+ }
831
+
832
+ // ─── GAME: 2048 ──────────────────────────────────────────────────────────────
833
+
834
+ function makeTwentyFortyEight(opts = {}) {
835
+ const SIZE=4;
836
+ const em=makeEmitter();
837
+ let board, score, best, state;
838
+
839
+ const TILE_COLORS = {
840
+ 0:FC.BLK, 2:FC.wht, 4:FC.yel, 8:FC.YEL, 16:FC.mag, 32:FC.MAG,
841
+ 64:FC.red, 128:FC.RED, 256:FC.grn, 512:FC.GRN, 1024:FC.cyn, 2048:FC.CYN,
842
+ };
843
+
844
+ function newBoard() { return Array.from({length:SIZE},()=>new Array(SIZE).fill(0)); }
845
+ function spawnTile(b) {
846
+ const empty=[];
847
+ for (let r=0;r<SIZE;r++) for (let c=0;c<SIZE;c++) if(!b[r][c]) empty.push([r,c]);
848
+ if (!empty.length) return;
849
+ const [r,c]=empty[rng(empty.length)];
850
+ b[r][c]=Math.random()<0.9?2:4;
851
+ }
852
+
853
+ function slide(row) {
854
+ const r=row.filter(x=>x);
855
+ let gain=0;
856
+ for (let i=0;i<r.length-1;i++) if(r[i]===r[i+1]) { r[i]*=2; gain+=r[i]; r.splice(i+1,1); }
857
+ while (r.length<SIZE) r.push(0);
858
+ return {row:r, gain};
859
+ }
860
+
861
+ function move(dir) {
862
+ let moved=false, gain=0;
863
+ const nb=newBoard();
864
+ for (let r=0;r<SIZE;r++) for (let c=0;c<SIZE;c++) nb[r][c]=board[r][c];
865
+
866
+ if (dir==='left'||dir==='right') {
867
+ for (let r=0;r<SIZE;r++) {
868
+ let row=nb[r]; if (dir==='right') row=[...row].reverse();
869
+ const {row:nr,gain:g}=slide(row);
870
+ if (dir==='right') nr.reverse();
871
+ if (nr.some((v,i)=>v!==nb[r][i])) moved=true;
872
+ nb[r]=nr; gain+=g;
873
+ }
874
+ } else {
875
+ for (let c=0;c<SIZE;c++) {
876
+ let col=nb.map(r=>r[c]); if (dir==='down') col=[...col].reverse();
877
+ const {row:nc,gain:g}=slide(col);
878
+ if (dir==='down') nc.reverse();
879
+ nc.forEach((v,r)=>{ if(v!==nb[r][c]) moved=true; nb[r][c]=v; });
880
+ gain+=g;
881
+ }
882
+ }
883
+ if (!moved) return false;
884
+ board=nb; score+=gain; best=Math.max(best,score);
885
+ spawnTile(board); em.emit('score',score);
886
+ // check win
887
+ if (board.flat().includes(2048)) { state='won'; em.emit('win',score); }
888
+ // check game over
889
+ else if (!canMove()) { state='gameover'; em.emit('gameover',score); }
890
+ return true;
891
+ }
892
+
893
+ function canMove() {
894
+ for (let r=0;r<SIZE;r++) for (let c=0;c<SIZE;c++) {
895
+ if (!board[r][c]) return true;
896
+ if (c<SIZE-1&&board[r][c]===board[r][c+1]) return true;
897
+ if (r<SIZE-1&&board[r][c]===board[r+1][c]) return true;
898
+ }
899
+ return false;
900
+ }
901
+
902
+ function start() {
903
+ board=newBoard(); score=0; best=best??0; state='playing';
904
+ spawnTile(board); spawnTile(board); em.emit('start');
905
+ }
906
+
907
+ function input(key) {
908
+ if ((state==='gameover'||state==='won')&&key==='enter'){ start(); return; }
909
+ if (state!=='playing') return;
910
+ if (['up','down','left','right'].includes(key)) move(key);
911
+ }
912
+
913
+ function frame() {
914
+ const CW=5*SIZE+3, CH=SIZE*2+4;
915
+ const cv=makeCanvas(CW+20,CH); cv.clear(' ',FC.wht,BC.blk);
916
+ cv.put(0,0,` 2048 Score:${score} Best:${best} [Arrows]`,FC.YEL,BC.blk);
917
+ cv.box(0,1,CW,CH-1,FC.wht,BC.blk);
918
+ for (let r=0;r<SIZE;r++) for (let c=0;c<SIZE;c++) {
919
+ const v=board[r][c];
920
+ const col=TILE_COLORS[v]??FC.WHT;
921
+ const s=(v?String(v):'·').padStart(4,' ');
922
+ cv.put(c*5+1,r*2+2,s,col,BC.blk);
923
+ if (r<SIZE-1) cv.put(c*5+1,r*2+3,'────',FC.BLK,BC.blk);
924
+ }
925
+ if (state==='gameover') cv.put(1,(CH>>1),' GAME OVER ENTER restart ',FC.WHT,BC.red);
926
+ if (state==='won') cv.put(1,(CH>>1),' 2048! ENTER continue ',FC.WHT,BC.grn);
927
+ return cv.render();
928
+ }
929
+
930
+ return { start, input, tick(){}, frame,
931
+ on:em.on.bind(em), off:em.off.bind(em),
932
+ get state(){return state;}, get score(){return score;} };
933
+ }
934
+
935
+ // ─── GAME: MINESWEEPER ───────────────────────────────────────────────────────
936
+
937
+ function makeMinesweeper(opts = {}) {
938
+ const GW=opts.w??9,GH=opts.h??9,MINES=opts.mines??10;
939
+ const em=makeEmitter();
940
+ let cells,cx,cy,state,score,revealed,firstClick;
941
+
942
+ function idx(x,y){return y*GW+x;}
943
+ function neighbors(x,y){
944
+ const n=[];
945
+ for(let dy=-1;dy<=1;dy++) for(let dx=-1;dx<=1;dx++) {
946
+ if(dx||dy) { const nx=x+dx,ny=y+dy; if(nx>=0&&nx<GW&&ny>=0&&ny<GH) n.push([nx,ny]); }
947
+ }
948
+ return n;
949
+ }
950
+
951
+ function placeMines(safeX,safeY) {
952
+ let placed=0;
953
+ while(placed<MINES) {
954
+ const x=rng(GW),y=rng(GH);
955
+ if(!cells[idx(x,y)].mine&&!(Math.abs(x-safeX)<=1&&Math.abs(y-safeY)<=1)) {
956
+ cells[idx(x,y)].mine=true; placed++;
957
+ }
958
+ }
959
+ for(let y=0;y<GH;y++) for(let x=0;x<GW;x++)
960
+ cells[idx(x,y)].adj=neighbors(x,y).filter(([nx,ny])=>cells[idx(nx,ny)].mine).length;
961
+ }
962
+
963
+ function reveal(x,y) {
964
+ const c=cells[idx(x,y)];
965
+ if(c.revealed||c.flagged) return;
966
+ c.revealed=true; revealed++;
967
+ if(c.mine) { state='gameover'; em.emit('gameover',score); return; }
968
+ if(!c.adj) neighbors(x,y).forEach(([nx,ny])=>reveal(nx,ny));
969
+ if(revealed===GW*GH-MINES) { state='won'; score=1000; em.emit('win',score); }
970
+ }
971
+
972
+ function start() {
973
+ cells=Array.from({length:GW*GH},()=>({mine:false,revealed:false,flagged:false,adj:0}));
974
+ cx=GW>>1; cy=GH>>1; revealed=0; score=0; state='playing'; firstClick=true;
975
+ em.emit('start');
976
+ }
977
+
978
+ function input(key) {
979
+ if((state==='gameover'||state==='won')&&key==='enter'){start();return;}
980
+ if(state!=='playing') return;
981
+ if(key==='up') cy=Math.max(0,cy-1);
982
+ if(key==='down') cy=Math.min(GH-1,cy+1);
983
+ if(key==='left') cx=Math.max(0,cx-1);
984
+ if(key==='right') cx=Math.min(GW-1,cx+1);
985
+ if(key==='space'||key==='enter') {
986
+ if(firstClick) { firstClick=false; placeMines(cx,cy); }
987
+ reveal(cx,cy);
988
+ }
989
+ if(key==='f') {
990
+ const c=cells[idx(cx,cy)];
991
+ if(!c.revealed) { c.flagged=!c.flagged; score+=c.flagged?5:-5; }
992
+ }
993
+ }
994
+
995
+ function frame() {
996
+ const W=GW*3+4, H=GH+4;
997
+ const cv=makeCanvas(W+20,H); cv.clear(' ',FC.wht,BC.blk);
998
+ const flags=cells.filter(c=>c.flagged).length;
999
+ cv.put(0,0,` MINESWEEPER Mines:${MINES-flags} [Arrows Move SPACE reveal F flag ENTER restart]`,FC.YEL,BC.blk);
1000
+ const numColors=[FC.wht,FC.BLU,FC.GRN,FC.RED,FC.BLU,FC.RED,FC.CYN,FC.MAG,FC.wht];
1001
+ for(let y=0;y<GH;y++) {
1002
+ cv.put(0,y+2,'│',FC.wht,BC.blk);
1003
+ for(let x=0;x<GW;x++) {
1004
+ const c=cells[idx(x,y)];
1005
+ const isCur=x===cx&&y===cy;
1006
+ let disp=' ·',fg=FC.BLK;
1007
+ if(c.flagged&&!c.revealed) { disp=' ⚑'; fg=FC.RED; }
1008
+ else if(!c.revealed) { disp=isCur?' ▪':' ▫'; fg=isCur?FC.YEL:FC.BLK; }
1009
+ else if(c.mine) { disp=' ✸'; fg=FC.RED; }
1010
+ else if(c.adj) { disp=` ${c.adj}`; fg=numColors[c.adj]; }
1011
+ else { disp=' '; fg=FC.BLK; }
1012
+ cv.put(x*3+1,y+2,disp,fg,isCur&&!c.revealed?BC.BLK:BC.blk);
1013
+ }
1014
+ cv.put(GW*3+1,y+2,'│',FC.wht,BC.blk);
1015
+ }
1016
+ cv.put(0,1,'├'+'─'.repeat(GW*3)+'┤',FC.wht,BC.blk);
1017
+ cv.put(0,GH+2,'└'+'─'.repeat(GW*3)+'┘',FC.wht,BC.blk);
1018
+ if(state==='gameover') {
1019
+ for(const c of cells) if(c.mine&&!c.flagged) c.revealed=true;
1020
+ cv.put(1,GH+3,' BOOM! ENTER to restart ',FC.WHT,BC.red);
1021
+ }
1022
+ if(state==='won') cv.put(1,GH+3,' CLEARED! ENTER again ',FC.WHT,BC.grn);
1023
+ return cv.render();
1024
+ }
1025
+
1026
+ return { start, input, tick(){}, frame,
1027
+ on:em.on.bind(em), off:em.off.bind(em),
1028
+ get state(){return state;}, get score(){return score;} };
1029
+ }
1030
+
1031
+ // ─── GAME: WORDLE ────────────────────────────────────────────────────────────
1032
+
1033
+ function makeWordle(opts = {}) {
1034
+ const em=makeEmitter();
1035
+ let target,guesses,current,state,score,msg;
1036
+
1037
+ function start() {
1038
+ target=WORDLE_WORDS[rng(WORDLE_WORDS.length)].toUpperCase();
1039
+ guesses=[]; current=''; state='playing'; score=0; msg='';
1040
+ em.emit('start');
1041
+ }
1042
+
1043
+ function check(word) {
1044
+ const result=[];
1045
+ const tArr=[...target];
1046
+ const used=new Array(5).fill(false);
1047
+ // greens
1048
+ for(let i=0;i<5;i++) {
1049
+ if(word[i]===tArr[i]) { result[i]='green'; used[i]=true; }
1050
+ }
1051
+ // yellows
1052
+ for(let i=0;i<5;i++) {
1053
+ if(result[i]) continue;
1054
+ const j=tArr.findIndex((c,k)=>c===word[i]&&!used[k]);
1055
+ if(j>=0) { result[i]='yellow'; used[j]=true; }
1056
+ else result[i]='gray';
1057
+ }
1058
+ return result;
1059
+ }
1060
+
1061
+ function input(key) {
1062
+ if((state==='gameover'||state==='won')&&key==='enter'){start();return;}
1063
+ if(state!=='playing') return;
1064
+ if(key==='enter') {
1065
+ if(current.length<5) { msg='Too short!'; return; }
1066
+ if(!WORDLE_WORDS.includes(current.toLowerCase())&&!WORDLE_WORDS.includes(current.toUpperCase())) { msg='Not in word list'; return; }
1067
+ const colors=check(current);
1068
+ guesses.push({word:current,colors});
1069
+ if(current===target) {
1070
+ score=Math.max(10,(7-guesses.length)*100); state='won';
1071
+ msg='Brilliant!'; em.emit('win',score); return;
1072
+ }
1073
+ if(guesses.length>=6) { state='gameover'; msg=`Word was: ${target}`; em.emit('gameover',0); return; }
1074
+ current=''; msg='';
1075
+ } else if(key==='backspace'||key==='delete') {
1076
+ current=current.slice(0,-1);
1077
+ } else if(/^[a-z]$/.test(key)&&current.length<5) {
1078
+ current+=key.toUpperCase();
1079
+ }
1080
+ }
1081
+
1082
+ const GC={green:FC.GRN,yellow:FC.YEL,gray:FC.BLK};
1083
+
1084
+ function frame() {
1085
+ const W=40,H=20;
1086
+ const cv=makeCanvas(W,H); cv.clear(' ',FC.wht,BC.blk);
1087
+ cv.put(0,0,' WORDLE [Type guess, ENTER submit, BACKSPACE]',FC.YEL,BC.blk);
1088
+ // grid
1089
+ for(let g=0;g<6;g++) {
1090
+ const guess=guesses[g];
1091
+ for(let l=0;l<5;l++) {
1092
+ const bx=l*6+2, by=g*2+2;
1093
+ if(guess) {
1094
+ const col=GC[guess.colors[l]];
1095
+ cv.put(bx,by,`[${guess.word[l]}]`,col,BC.blk);
1096
+ } else if(g===guesses.length&&l<current.length) {
1097
+ cv.put(bx,by,`[${current[l]}]`,FC.WHT,BC.blk);
1098
+ } else {
1099
+ cv.put(bx,by,'[ ]',FC.BLK,BC.blk);
1100
+ }
1101
+ }
1102
+ }
1103
+ // keyboard hint
1104
+ const kb='QWERTYUIOPASDFGHJKLZXCVBNM';
1105
+ const usedLetters=new Map();
1106
+ for(const g of guesses) {
1107
+ for(let i=0;i<5;i++) {
1108
+ const l=g.word[i], c=g.colors[i];
1109
+ if(!usedLetters.has(l)||c==='green'||(c==='yellow'&&usedLetters.get(l)==='gray'))
1110
+ usedLetters.set(l,c);
1111
+ }
1112
+ }
1113
+ const rows=['QWERTYUIOP','ASDFGHJKL','ZXCVBNM'];
1114
+ for(let r=0;r<3;r++) {
1115
+ let x=2+r*3;
1116
+ for(const ch of rows[r]) {
1117
+ const col=GC[usedLetters.get(ch)]??FC.wht;
1118
+ cv.put(x,14+r,ch,col,BC.blk); x+=3;
1119
+ }
1120
+ }
1121
+ if(msg) cv.put(2,13,msg.padEnd(30,' '),FC.RED,BC.blk);
1122
+ return cv.render();
1123
+ }
1124
+
1125
+ return { start, input, tick(){}, frame,
1126
+ on:em.on.bind(em), off:em.off.bind(em),
1127
+ get state(){return state;}, get score(){return score;} };
1128
+ }
1129
+
1130
+ // ─── GAME: CONNECT FOUR ──────────────────────────────────────────────────────
1131
+
1132
+ function makeConnectFour(opts = {}) {
1133
+ const COLS=7,ROWS=6;
1134
+ const em=makeEmitter();
1135
+ let board,curCol,turn,state,score,aiMode,msg;
1136
+
1137
+ function start() {
1138
+ board=Array.from({length:ROWS},()=>new Array(COLS).fill(0));
1139
+ curCol=3; turn=1; state='playing'; score=0; msg='';
1140
+ aiMode=opts.ai??true;
1141
+ em.emit('start');
1142
+ }
1143
+
1144
+ function dropPiece(col,player) {
1145
+ for(let r=ROWS-1;r>=0;r--) {
1146
+ if(!board[r][col]) { board[r][col]=player; return r; }
1147
+ }
1148
+ return -1;
1149
+ }
1150
+
1151
+ function checkWin(player) {
1152
+ // horizontal/vertical/diagonal
1153
+ for(let r=0;r<ROWS;r++) for(let c=0;c<COLS;c++) {
1154
+ const dirs=[[0,1],[1,0],[1,1],[1,-1]];
1155
+ for(const [dr,dc] of dirs) {
1156
+ let cnt=0;
1157
+ for(let i=0;i<4;i++) {
1158
+ const nr=r+dr*i,nc=c+dc*i;
1159
+ if(nr>=0&&nr<ROWS&&nc>=0&&nc<COLS&&board[nr][nc]===player) cnt++;
1160
+ else break;
1161
+ }
1162
+ if(cnt===4) return true;
1163
+ }
1164
+ }
1165
+ return false;
1166
+ }
1167
+
1168
+ function aiMove() {
1169
+ const valid=[];
1170
+ for(let c=0;c<COLS;c++) if(board[0][c]===0) valid.push(c);
1171
+ if(!valid.length) return;
1172
+ // check win
1173
+ for(const c of valid) {
1174
+ const r=dropPiece(c,2); if(checkWin(2)) { board[r][c]=0; return c; } board[r][c]=0;
1175
+ }
1176
+ // block player win
1177
+ for(const c of valid) {
1178
+ const r=dropPiece(c,1); if(checkWin(1)) { board[r][c]=0; return c; } board[r][c]=0;
1179
+ }
1180
+ // prefer center
1181
+ const prio=[3,2,4,1,5,0,6].filter(c=>valid.includes(c));
1182
+ return prio[0];
1183
+ }
1184
+
1185
+ function play(col) {
1186
+ if(board[0][col]!==0) { msg='Column full!'; return; }
1187
+ const r=dropPiece(col,turn);
1188
+ if(checkWin(turn)) {
1189
+ msg=`Player ${turn} wins!`; score=turn===1?100:0; state='gameover';
1190
+ em.emit('gameover',score); return;
1191
+ }
1192
+ if(board.every(row=>row.every(c=>c!==0))) { msg='Draw!'; state='gameover'; em.emit('gameover',0); return; }
1193
+ turn=turn===1?2:1;
1194
+ if(aiMode&&turn===2) {
1195
+ const ac=aiMove();
1196
+ if(ac!==undefined) {
1197
+ const ar=dropPiece(ac,2);
1198
+ if(checkWin(2)) { msg='AI wins!'; state='gameover'; em.emit('gameover',0); return; }
1199
+ turn=1;
1200
+ }
1201
+ }
1202
+ msg='';
1203
+ }
1204
+
1205
+ function input(key) {
1206
+ if(state==='gameover'&&key==='enter'){start();return;}
1207
+ if(state!=='playing') return;
1208
+ if(key==='left') curCol=Math.max(0,curCol-1);
1209
+ if(key==='right') curCol=Math.min(COLS-1,curCol+1);
1210
+ if(key==='space'||key==='down'||key==='enter') play(curCol);
1211
+ }
1212
+
1213
+ function frame() {
1214
+ const W=COLS*4+4,H=ROWS+6;
1215
+ const cv=makeCanvas(W,H); cv.clear(' ',FC.wht,BC.blk);
1216
+ cv.put(0,0,` CONNECT FOUR ${aiMode?'vs AI':'2P'} [←→ column SPACE/↓ drop]`,FC.YEL,BC.blk);
1217
+ // cursor
1218
+ cv.put(curCol*4+2,1,'▼',turn===1?FC.RED:FC.YEL,BC.blk);
1219
+ // board
1220
+ cv.put(0,2,'┌'+'───┬'.repeat(COLS-1)+'───┐',FC.wht,BC.blk);
1221
+ for(let r=0;r<ROWS;r++) {
1222
+ let row='│';
1223
+ for(let c=0;c<COLS;c++) {
1224
+ const v=board[r][c];
1225
+ const ch=v===0?' ○ ':v===1?' ● ':' ◉ ';
1226
+ const col=v===0?FC.BLK:v===1?FC.RED:FC.YEL;
1227
+ cv.put(row.length,r*2+3,ch,col,BC.blk); row+=ch+'│';
1228
+ }
1229
+ cv.put(0,r*2+3,'│',FC.wht,BC.blk); cv.put(W-1,r*2+3,'│',FC.wht,BC.blk);
1230
+ if(r<ROWS-1) cv.put(0,r*2+4,'├'+'───┼'.repeat(COLS-1)+'───┤',FC.wht,BC.blk);
1231
+ }
1232
+ cv.put(0,ROWS*2+3,'└'+'───┴'.repeat(COLS-1)+'───┘',FC.wht,BC.blk);
1233
+ if(msg) cv.put(1,ROWS*2+4,' '+msg.padEnd(W-2,' ')+'',FC.WHT,BC.blk);
1234
+ if(state==='gameover') cv.put(2,ROWS*2+5,'ENTER to play again',FC.GRN,BC.blk);
1235
+ return cv.render();
1236
+ }
1237
+
1238
+ return { start, input, tick(){}, frame,
1239
+ on:em.on.bind(em), off:em.off.bind(em),
1240
+ get state(){return state;}, get score(){return score;} };
1241
+ }
1242
+
1243
+ // ─── GAME: BATTLESHIP ────────────────────────────────────────────────────────
1244
+
1245
+ function makeBattleship(opts = {}) {
1246
+ const SZ=10;
1247
+ const SHIPS=[{n:'Carrier',sz:5},{n:'Battleship',sz:4},{n:'Cruiser',sz:3},{n:'Submarine',sz:3},{n:'Destroyer',sz:2}];
1248
+ const em=makeEmitter();
1249
+ let pBoard,pHits,aBoard,aHits,aCursor,pCursor,state,score,msg,phase,aHunt,aTargets;
1250
+
1251
+ function makeBoard(){return new Array(SZ*SZ).fill(0);}
1252
+ function placeShips(board){
1253
+ for(const s of SHIPS){
1254
+ let placed=false;
1255
+ while(!placed){
1256
+ const horiz=Math.random()<0.5;
1257
+ const x=rng(horiz?SZ-s.sz+1:SZ);
1258
+ const y=rng(horiz?SZ:SZ-s.sz+1);
1259
+ let ok=true;
1260
+ for(let i=0;i<s.sz;i++){
1261
+ const nx=x+(horiz?i:0),ny=y+(horiz?0:i);
1262
+ if(board[ny*SZ+nx]){ok=false;break;}
1263
+ }
1264
+ if(ok){for(let i=0;i<s.sz;i++){const nx=x+(horiz?i:0),ny=y+(horiz?0:i);board[ny*SZ+nx]=1;}placed=true;}
1265
+ }
1266
+ }
1267
+ }
1268
+
1269
+ function start(){
1270
+ pBoard=makeBoard(); placeShips(pBoard);
1271
+ aBoard=makeBoard(); placeShips(aBoard);
1272
+ pHits=new Array(SZ*SZ).fill(0); aHits=new Array(SZ*SZ).fill(0);
1273
+ pCursor=[0,0]; aCursor=[0,0];
1274
+ aHunt=[]; aTargets=[]; score=0; state='playing'; msg='Your turn! [Arrows aim, SPACE fire]';
1275
+ em.emit('start');
1276
+ }
1277
+
1278
+ function aiShoot(){
1279
+ let x,y;
1280
+ if(aTargets.length){
1281
+ [x,y]=aTargets.shift();
1282
+ } else {
1283
+ if(!aHunt.length){
1284
+ for(let i=0;i<SZ*SZ;i++) aHunt.push([i%SZ,i/SZ|0]);
1285
+ aHunt=shuffle(aHunt);
1286
+ }
1287
+ [x,y]=aHunt.shift();
1288
+ }
1289
+ aCursor=[x,y];
1290
+ if(aHits[y*SZ+x]) return aiShoot();
1291
+ aHits[y*SZ+x]=pBoard[y*SZ+x]?2:1;
1292
+ if(pBoard[y*SZ+x]){
1293
+ msg='AI hit your ship!';
1294
+ [[x-1,y],[x+1,y],[x,y-1],[x,y+1]].filter(([nx,ny])=>nx>=0&&nx<SZ&&ny>=0&&ny<SZ&&!aHits[ny*SZ+nx])
1295
+ .forEach(p=>aTargets.push(p));
1296
+ } else msg='AI missed.';
1297
+ if(aHits.filter((v,i)=>v===2&&pBoard[i]).length===SHIPS.reduce((s,sh)=>s+sh.sz,0)){
1298
+ state='gameover'; msg='AI wins!'; em.emit('gameover',0);
1299
+ }
1300
+ }
1301
+
1302
+ function input(key){
1303
+ if(state==='gameover'&&key==='enter'){start();return;}
1304
+ if(state!=='playing') return;
1305
+ const [cx,cy]=pCursor;
1306
+ if(key==='up') pCursor=[cx,Math.max(0,cy-1)];
1307
+ if(key==='down') pCursor=[cx,Math.min(SZ-1,cy+1)];
1308
+ if(key==='left') pCursor=[Math.max(0,cx-1),cy];
1309
+ if(key==='right') pCursor=[Math.min(SZ-1,cx+1),cy];
1310
+ if(key==='space'||key==='enter'){
1311
+ const [px,py]=pCursor;
1312
+ if(pHits[py*SZ+px]){msg='Already fired there!';return;}
1313
+ pHits[py*SZ+px]=aBoard[py*SZ+px]?2:1;
1314
+ if(aBoard[py*SZ+px]){
1315
+ score+=50; msg='Hit!';
1316
+ if(pHits.filter((v,i)=>v===2&&aBoard[i]).length===SHIPS.reduce((s,sh)=>s+sh.sz,0)){
1317
+ state='won'; msg='You win!'; em.emit('win',score); return;
1318
+ }
1319
+ } else msg='Miss.';
1320
+ aiShoot();
1321
+ }
1322
+ }
1323
+
1324
+ function renderBoard(cv,ox,oy,board,hits,cursor,showShips,label){
1325
+ cv.put(ox,oy,label,FC.CYN,BC.blk);
1326
+ for(let y=0;y<SZ;y++) for(let x=0;x<SZ;x++){
1327
+ const h=hits[y*SZ+x];
1328
+ const isCur=cursor&&cursor[0]===x&&cursor[1]===y;
1329
+ const ship=board[y*SZ+x];
1330
+ let ch='·',col=FC.BLK;
1331
+ if(h===2){ch='✸';col=FC.RED;}
1332
+ else if(h===1){ch='○';col=FC.BLU;}
1333
+ else if(showShips&&ship){ch='▓';col=FC.GRN;}
1334
+ if(isCur&&!h){ch='◎';col=FC.YEL;}
1335
+ cv.set(ox+x*2,oy+y+1,ch,col,BC.blk);
1336
+ cv.set(ox+x*2+1,oy+y+1,' ',FC.wht,BC.blk);
1337
+ }
1338
+ }
1339
+
1340
+ function frame(){
1341
+ const W=50,H=18;
1342
+ const cv=makeCanvas(W,H); cv.clear(' ',FC.wht,BC.blk);
1343
+ cv.put(0,0,` BATTLESHIP Score:${score} [Arrows aim SPACE fire]`,FC.YEL,BC.blk);
1344
+ renderBoard(cv,1,1,aBoard,pHits,pCursor,false,'ENEMY WATERS:');
1345
+ renderBoard(cv,26,1,pBoard,aHits,aCursor,true,'YOUR FLEET:');
1346
+ cv.put(0,H-2,(' '+msg).padEnd(W,' '),FC.WHT,BC.blk);
1347
+ if(state==='gameover'||state==='won') cv.put(0,H-1,' ENTER to play again',FC.GRN,BC.blk);
1348
+ return cv.render();
1349
+ }
1350
+
1351
+ return { start, input, tick(){}, frame,
1352
+ on:em.on.bind(em), off:em.off.bind(em),
1353
+ get state(){return state;}, get score(){return score;} };
1354
+ }
1355
+
1356
+ // ─── GAME: MAZE ──────────────────────────────────────────────────────────────
1357
+
1358
+ function makeMaze(opts = {}) {
1359
+ const MW=opts.w??21,MH=opts.h??21;
1360
+ const em=makeEmitter();
1361
+ let maze,px,py,steps,state,score,startTime;
1362
+
1363
+ function generate(){
1364
+ // 2D boolean grid: true = wall
1365
+ const m=Array.from({length:MH},()=>new Array(MW).fill(true));
1366
+ // recursive backtracker (odd coords = cells, even = walls)
1367
+ function carve(x,y){
1368
+ m[y][x]=false;
1369
+ const dirs=shuffle([[0,-2],[0,2],[-2,0],[2,0]]);
1370
+ for(const [dx,dy] of dirs){
1371
+ const nx=x+dx,ny=y+dy;
1372
+ if(nx>0&&nx<MW-1&&ny>0&&ny<MH-1&&m[ny][nx]){
1373
+ m[y+dy/2|0][x+dx/2|0]=false;
1374
+ carve(nx,ny);
1375
+ }
1376
+ }
1377
+ }
1378
+ carve(1,1); return m;
1379
+ }
1380
+
1381
+ function start(){
1382
+ maze=generate(); px=1; py=1; steps=0; score=0;
1383
+ maze[MH-2][MW-2]=false; // exit always open
1384
+ state='playing'; startTime=Date.now();
1385
+ em.emit('start');
1386
+ }
1387
+
1388
+ function input(key){
1389
+ if(state==='won'&&key==='enter'){start();return;}
1390
+ if(state!=='playing') return;
1391
+ const moves={up:[0,-1],down:[0,1],left:[-1,0],right:[1,0]};
1392
+ const m=moves[key]; if(!m) return;
1393
+ const nx=px+m[0],ny=py+m[1];
1394
+ if(nx>=0&&nx<MW&&ny>=0&&ny<MH&&!maze[ny][nx]){ px=nx; py=ny; steps++; }
1395
+ if(px===MW-2&&py===MH-2){
1396
+ const t=Math.max(1,(Date.now()-startTime)/1000|0);
1397
+ score=Math.max(10,500-steps-t); state='won'; em.emit('win',score);
1398
+ }
1399
+ }
1400
+
1401
+ function frame(){
1402
+ const cv=makeCanvas(MW+24,MH+3); cv.clear(' ',FC.wht,BC.blk);
1403
+ cv.put(0,0,` MAZE Steps:${steps} [Arrows to navigate reach ★]`,FC.YEL,BC.blk);
1404
+ for(let y=0;y<MH;y++) for(let x=0;x<MW;x++){
1405
+ if(x===px&&y===py) cv.set(x,y+2,'@',FC.YEL,BC.blk);
1406
+ else if(x===MW-2&&y===MH-2) cv.set(x,y+2,'★',FC.GRN,BC.blk);
1407
+ else cv.set(x,y+2,maze[y][x]?'█':' ',maze[y][x]?FC.BLU:FC.wht,BC.blk);
1408
+ }
1409
+ cv.put(MW+2,2,`Player: ${px},${py}`,FC.CYN,BC.blk);
1410
+ cv.put(MW+2,3,`Exit: ${MW-2},${MH-2}`,FC.CYN,BC.blk);
1411
+ if(state==='won'){cv.put(MW+2,5,'YOU MADE IT!',FC.GRN,BC.blk);cv.put(MW+2,6,`Score: ${score}`,FC.GRN,BC.blk);cv.put(MW+2,7,'ENTER new maze',FC.GRN,BC.blk);}
1412
+ return cv.render();
1413
+ }
1414
+
1415
+ return { start, input, tick(){}, frame,
1416
+ on:em.on.bind(em), off:em.off.bind(em),
1417
+ get state(){return state;}, get score(){return score;} };
1418
+ }
1419
+
1420
+ // ─── GAME: LIGHTS OUT ────────────────────────────────────────────────────────
1421
+
1422
+ function makeLightsOut(opts = {}) {
1423
+ const SZ=opts.size??5, SCRAMBLE=opts.scramble??15;
1424
+ const em=makeEmitter();
1425
+ let grid,cx,cy,moves,state,score;
1426
+
1427
+ function toggle(gx,gy){
1428
+ [[0,0],[0,-1],[0,1],[-1,0],[1,0]].forEach(([dx,dy])=>{
1429
+ const nx=gx+dx,ny=gy+dy;
1430
+ if(nx>=0&&nx<SZ&&ny>=0&&ny<SZ) grid[ny*SZ+nx]^=1;
1431
+ });
1432
+ }
1433
+
1434
+ function start(){
1435
+ grid=new Array(SZ*SZ).fill(0);
1436
+ for(let i=0;i<SCRAMBLE;i++) toggle(rng(SZ),rng(SZ));
1437
+ cx=SZ>>1; cy=SZ>>1; moves=0; score=0; state='playing';
1438
+ em.emit('start');
1439
+ }
1440
+
1441
+ function input(key){
1442
+ if(state==='won'&&key==='enter'){start();return;}
1443
+ if(state!=='playing') return;
1444
+ if(key==='up') cy=Math.max(0,cy-1);
1445
+ if(key==='down') cy=Math.min(SZ-1,cy+1);
1446
+ if(key==='left') cx=Math.max(0,cx-1);
1447
+ if(key==='right') cx=Math.min(SZ-1,cx+1);
1448
+ if(key==='space'||key==='enter'){
1449
+ toggle(cx,cy); moves++;
1450
+ if(grid.every(v=>v===0)){
1451
+ score=Math.max(10,500-moves*10); state='won'; em.emit('win',score);
1452
+ }
1453
+ }
1454
+ }
1455
+
1456
+ function frame(){
1457
+ const W=SZ*6+4,H=SZ*3+6;
1458
+ const cv=makeCanvas(W,H); cv.clear(' ',FC.wht,BC.blk);
1459
+ cv.put(0,0,` LIGHTS OUT Moves:${moves} [Arrows SPACE toggle — turn all lights off]`,FC.YEL,BC.blk);
1460
+ for(let y=0;y<SZ;y++) for(let x=0;x<SZ;x++){
1461
+ const on=grid[y*SZ+x];
1462
+ const isCur=x===cx&&y===cy;
1463
+ const bx=x*6+1,by=y*3+2;
1464
+ const bcol=on?BC.YEL:BC.blk, fcol=on?FC.blk:FC.BLK;
1465
+ cv.fill(bx,by,5,2,on?'█':' ',fcol,bcol);
1466
+ if(isCur) cv.put(bx+1,by+0,' ◉ ',FC.WHT,bcol);
1467
+ }
1468
+ cv.put(1,H-2,`Score: ${score}`,FC.CYN,BC.blk);
1469
+ if(state==='won'){cv.put(1,H-1,' ALL OFF! ENTER new puzzle ',FC.WHT,BC.grn);}
1470
+ return cv.render();
1471
+ }
1472
+
1473
+ return { start, input, tick(){}, frame,
1474
+ on:em.on.bind(em), off:em.off.bind(em),
1475
+ get state(){return state;}, get score(){return score;} };
1476
+ }
1477
+
1478
+ // ─── GAME: SUDOKU ────────────────────────────────────────────────────────────
1479
+
1480
+ function makeSudoku(opts = {}) {
1481
+ const em=makeEmitter();
1482
+ let puzzle,board,given,cx,cy,state,score,errors,puzzleIdx;
1483
+
1484
+ function parsePuzzle(str){
1485
+ return Array.from(str).map(c=>c==='.'?0:+c);
1486
+ }
1487
+
1488
+ function start(){
1489
+ puzzleIdx=(puzzleIdx??-1)+1; if(puzzleIdx>=SUDOKU_PUZZLES.length) puzzleIdx=0;
1490
+ puzzle=parsePuzzle(SUDOKU_PUZZLES[puzzleIdx]);
1491
+ board=[...puzzle]; given=puzzle.map(v=>v!==0);
1492
+ cx=0; cy=0; errors=0; score=0; state='playing';
1493
+ em.emit('start');
1494
+ }
1495
+
1496
+ function isValid(b,pos,val){
1497
+ const r=pos/9|0,c=pos%9;
1498
+ for(let i=0;i<9;i++){
1499
+ if(i!==c&&b[r*9+i]===val) return false;
1500
+ if(i!==r&&b[i*9+c]===val) return false;
1501
+ }
1502
+ const br=r/3|0*3,bc=c/3|0*3;
1503
+ for(let dr=0;dr<3;dr++) for(let dc=0;dc<3;dc++){
1504
+ const ni=(br+dr)*9+(bc+dc);
1505
+ if(ni!==pos&&b[ni]===val) return false;
1506
+ }
1507
+ return true;
1508
+ }
1509
+
1510
+ function input(key){
1511
+ if(state==='won'&&key==='enter'){start();return;}
1512
+ if(state!=='playing') return;
1513
+ if(key==='up') cy=Math.max(0,cy-1);
1514
+ if(key==='down') cy=Math.min(8,cy+1);
1515
+ if(key==='left') cx=Math.max(0,cx-1);
1516
+ if(key==='right') cx=Math.min(8,cx+1);
1517
+ const pos=cy*9+cx;
1518
+ if(/^[1-9]$/.test(key)&&!given[pos]){
1519
+ const v=+key;
1520
+ if(!isValid(board,pos,v)) errors++;
1521
+ board[pos]=v;
1522
+ if(board.every((v,i)=>v!==0&&isValid(board,i,v))){
1523
+ score=Math.max(10,1000-errors*50); state='won'; em.emit('win',score);
1524
+ }
1525
+ }
1526
+ if((key==='delete'||key==='backspace'||key==='0')&&!given[pos]) board[pos]=0;
1527
+ }
1528
+
1529
+ function frame(){
1530
+ const W=42,H=22;
1531
+ const cv=makeCanvas(W,H); cv.clear(' ',FC.wht,BC.blk);
1532
+ cv.put(0,0,` SUDOKU Errors:${errors} [Arrows move, 1-9 enter, DEL clear]`,FC.YEL,BC.blk);
1533
+ // grid
1534
+ for(let r=0;r<9;r++){
1535
+ for(let c=0;c<9;c++){
1536
+ const pos=r*9+c;
1537
+ const v=board[pos];
1538
+ const isCur=c===cx&&r===cy;
1539
+ const isGiv=given[pos];
1540
+ const bad=v&&!isValid(board,pos,v);
1541
+ const ch=v?String(v):'.';
1542
+ const fg=bad?FC.RED:isGiv?FC.WHT:FC.CYN;
1543
+ const bg=isCur?BC.BLU:BC.blk;
1544
+ cv.put(c*4+(c>5?2:c>2?1:0)+1, r*2+(r>5?2:r>2?1:0)+2, ` ${ch} `, fg, bg);
1545
+ }
1546
+ }
1547
+ // grid lines
1548
+ for(let i=0;i<3;i++) {
1549
+ const row=i*6+1; const col_x=i*12+4;
1550
+ cv.put(0,row+2,'─'.repeat(38),FC.BLK,BC.blk);
1551
+ for(let r=0;r<10;r++) cv.set(col_x,r*2+2,'│',FC.BLK,BC.blk);
1552
+ }
1553
+ cv.put(1,H-2,`Score: ${score} Puzzle: ${puzzleIdx+1}/${SUDOKU_PUZZLES.length}`,FC.CYN,BC.blk);
1554
+ if(state==='won') cv.put(1,H-1,' SOLVED! ENTER next puzzle ',FC.WHT,BC.grn);
1555
+ return cv.render();
1556
+ }
1557
+
1558
+ return { start, input, tick(){}, frame,
1559
+ on:em.on.bind(em), off:em.off.bind(em),
1560
+ get state(){return state;}, get score(){return score;} };
1561
+ }
1562
+
1563
+ // ─── GAME: BLACKJACK ─────────────────────────────────────────────────────────
1564
+
1565
+ function makeBlackjack(opts = {}) {
1566
+ const em=makeEmitter();
1567
+ let deck,pHand,dHand,chips,bet,state,score,msg;
1568
+
1569
+ const SUITS=['♠','♥','♦','♣'],RANKS=['A','2','3','4','5','6','7','8','9','10','J','Q','K'];
1570
+ const SUIT_COL={'♠':FC.WHT,'♥':FC.RED,'♦':FC.RED,'♣':FC.WHT};
1571
+
1572
+ function makeDeck(){
1573
+ const d=[];
1574
+ for(const s of SUITS) for(const r of RANKS) d.push({r,s});
1575
+ return shuffle(d);
1576
+ }
1577
+
1578
+ function cardVal(r){ return r==='A'?11:['J','Q','K'].includes(r)?10:+r; }
1579
+ function handVal(h){
1580
+ let v=h.reduce((s,c)=>s+cardVal(c.r),0), aces=h.filter(c=>c.r==='A').length;
1581
+ while(v>21&&aces>0){v-=10;aces--;}
1582
+ return v;
1583
+ }
1584
+ function deal(){return deck.pop()??makeDeck().pop();}
1585
+
1586
+ function start(){
1587
+ deck=makeDeck(); chips=opts.chips??100; bet=10; state='idle'; msg='Place your bet with +/- then ENTER to deal';
1588
+ em.emit('start');
1589
+ }
1590
+
1591
+ function startRound(){
1592
+ if(chips<=0){state='gameover';msg='Broke! Game over.';em.emit('gameover',chips);return;}
1593
+ if(bet>chips) bet=chips;
1594
+ pHand=[deal(),deal()]; dHand=[deal(),deal()];
1595
+ state='playing'; msg='H=hit S=stand D=double ENTER=stand';
1596
+ if(handVal(pHand)===21){stand();}
1597
+ }
1598
+
1599
+ function stand(){
1600
+ while(handVal(dHand)<17) dHand.push(deal());
1601
+ const pv=handVal(pHand),dv=handVal(dHand);
1602
+ if(dv>21||pv>dv){chips+=bet;msg=`You win! +${bet} (${pv} vs ${dv})`;}
1603
+ else if(pv===dv){msg=`Push! Tie (${pv} vs ${dv})`;}
1604
+ else{chips-=bet;msg=`You lose! -${bet} (${pv} vs ${dv})`;}
1605
+ score=chips; state='roundover'; em.emit('score',score);
1606
+ }
1607
+
1608
+ function input(key){
1609
+ if(state==='gameover'&&key==='enter'){start();return;}
1610
+ if(state==='idle'){
1611
+ if(key==='+') bet=Math.min(chips,bet+10);
1612
+ if(key==='-') bet=Math.max(5,bet-10);
1613
+ if(key==='enter'||key==='space') startRound();
1614
+ return;
1615
+ }
1616
+ if(state==='roundover'){
1617
+ if(key==='enter'||key==='space'){ state='idle'; msg='Place your bet with +/- then ENTER to deal'; }
1618
+ return;
1619
+ }
1620
+ if(state!=='playing') return;
1621
+ if(key==='h'||key==='up'){
1622
+ pHand.push(deal());
1623
+ if(handVal(pHand)>21){chips-=bet;msg=`Bust! -${bet}`;score=chips;state='roundover';em.emit('score',score);}
1624
+ }
1625
+ if(key==='s'||key==='enter'||key==='down') stand();
1626
+ if(key==='d'){
1627
+ bet=Math.min(bet*2,chips); pHand.push(deal());
1628
+ if(handVal(pHand)>21){chips-=bet;msg=`Double bust! -${bet}`;score=chips;state='roundover';}
1629
+ else stand();
1630
+ }
1631
+ }
1632
+
1633
+ function cardStr(c,hidden=false){
1634
+ return hidden?'[??]':`[${c.r}${c.s}]`;
1635
+ }
1636
+
1637
+ function frame(){
1638
+ const W=50,H=18;
1639
+ const cv=makeCanvas(W,H); cv.clear(' ',FC.wht,BC.blk);
1640
+ cv.put(0,0,` BLACKJACK Chips:${chips} Bet:${bet} [H=hit S=stand D=double +/- bet]`,FC.YEL,BC.blk);
1641
+ cv.put(1,2,'DEALER: ',FC.CYN,BC.blk);
1642
+ let dx=9;
1643
+ const showAll=state==='roundover'||state==='gameover';
1644
+ for(let i=0;i<dHand.length;i++){
1645
+ const c=dHand[i], hidden=!showAll&&i===1;
1646
+ const cs=cardStr(c,hidden);
1647
+ cv.put(dx,2,cs,hidden?FC.BLK:SUIT_COL[c.s],BC.blk); dx+=cs.length+1;
1648
+ }
1649
+ if(showAll) cv.put(dx+1,2,`= ${handVal(dHand)}`,FC.CYN,BC.blk);
1650
+ cv.put(1,5,'PLAYER: ',FC.CYN,BC.blk);
1651
+ let px2=9;
1652
+ for(const c of pHand){ const cs=cardStr(c); cv.put(px2,5,cs,SUIT_COL[c.s],BC.blk); px2+=cs.length+1; }
1653
+ cv.put(px2+1,5,`= ${handVal(pHand)}`,FC.GRN,BC.blk);
1654
+ cv.put(1,8,msg.padEnd(W-2,' '),FC.WHT,BC.blk);
1655
+ if(state==='gameover') cv.put(1,10,'ENTER to restart',FC.RED,BC.blk);
1656
+ return cv.render();
1657
+ }
1658
+
1659
+ return { start, input, tick(){}, frame,
1660
+ on:em.on.bind(em), off:em.off.bind(em),
1661
+ get state(){return state;}, get score(){return score??chips;} };
1662
+ }
1663
+
1664
+ // ─── GAME: SLOTS ─────────────────────────────────────────────────────────────
1665
+
1666
+ function makeSlots(opts = {}) {
1667
+ const em=makeEmitter();
1668
+ const SYMS=[' 7 ','BAR','BEL','CHR','LMN','ORG','PLM'];
1669
+ const SYM_COL=[FC.YEL,FC.WHT,FC.YEL,FC.RED,FC.YEL,FC.YEL,FC.MAG];
1670
+ const WEIGHTS=[1,2,3,4,4,3,3];
1671
+ const PAY={'7 7 7':500,'BARBAR':200,'BELBEL':100,'CHRCHR':50,'LMNLMN':30,'ORGORG':30,'PLMPLM':20};
1672
+ let reels,spinning,spinFrames,result,credits,bet,state,score,msg;
1673
+
1674
+ function pickSym(){
1675
+ const total=WEIGHTS.reduce((a,b)=>a+b,0);
1676
+ let r=rng(total),i=0;
1677
+ for(;i<WEIGHTS.length;i++){r-=WEIGHTS[i];if(r<0)break;}
1678
+ return SYMS[i]??SYMS[0];
1679
+ }
1680
+
1681
+ function start(){
1682
+ reels=[[pickSym(),pickSym(),pickSym()],[pickSym(),pickSym(),pickSym()],[pickSym(),pickSym(),pickSym()]];
1683
+ credits=opts.credits??100; bet=10; spinning=false; spinFrames=0;
1684
+ state='idle'; score=credits; msg='SPACE to spin +/- adjust bet';
1685
+ em.emit('start');
1686
+ }
1687
+
1688
+ function spin(){
1689
+ if(credits<bet){msg='Not enough credits!';return;}
1690
+ credits-=bet; spinning=true; spinFrames=20; msg='Spinning...';
1691
+ }
1692
+
1693
+ function evalResult(){
1694
+ const r0=reels[0][1],r1=reels[1][1],r2=reels[2][1];
1695
+ const line=r0.trim()+r1.trim()+r2.trim();
1696
+ const key=Object.keys(PAY).find(k=>line.startsWith(k));
1697
+ if(key){const w=PAY[key]*bet/10|0;credits+=w;msg=`${key}! Win ${w}!`;}
1698
+ else if(r0.trim()==='CHR'){credits+=bet/2|0;msg=`Cherry! Win ${bet/2|0}`;}
1699
+ else msg='No win. Try again!';
1700
+ score=credits; em.emit('score',credits);
1701
+ if(credits<=0){state='gameover';em.emit('gameover',0);}
1702
+ }
1703
+
1704
+ function input(key){
1705
+ if(state==='gameover'&&key==='enter'){start();return;}
1706
+ if(spinning) return;
1707
+ if(key==='space') spin();
1708
+ if(key==='+'||key==='up') bet=Math.min(credits,bet+5);
1709
+ if(key==='-'||key==='down') bet=Math.max(5,bet-5);
1710
+ if(key==='enter') spin();
1711
+ }
1712
+
1713
+ function tick(dt=16){
1714
+ if(!spinning) return;
1715
+ spinFrames--;
1716
+ if(spinFrames>0){
1717
+ for(let i=0;i<3;i++){
1718
+ if(spinFrames>(i*5)){
1719
+ reels[i]=[pickSym(),pickSym(),pickSym()];
1720
+ }
1721
+ }
1722
+ } else {
1723
+ spinning=false; evalResult();
1724
+ }
1725
+ }
1726
+
1727
+ function frame(){
1728
+ const W=40,H=14;
1729
+ const cv=makeCanvas(W,H); cv.clear(' ',FC.wht,BC.blk);
1730
+ cv.put(0,0,` SLOTS Credits:${credits} Bet:${bet} [SPACE spin +/- bet]`,FC.YEL,BC.blk);
1731
+ // machine frame
1732
+ cv.box(4,2,32,9,FC.WHT,BC.blk);
1733
+ // three reels
1734
+ for(let reel=0;reel<3;reel++){
1735
+ const rx=reel*10+5;
1736
+ for(let row=0;row<3;row++){
1737
+ const sym=reels[reel][row];
1738
+ const idx=SYMS.indexOf(sym);
1739
+ const col=SYM_COL[idx]??FC.wht;
1740
+ const hl=row===1;
1741
+ cv.put(rx,row+3,sym,hl?col:FC.BLK,hl?BC.blk:BC.blk);
1742
+ }
1743
+ if(reel<2) for(let y=3;y<6;y++) cv.set(rx+3,y,'│',FC.BLK,BC.blk);
1744
+ }
1745
+ // payline indicator
1746
+ cv.put(4,4,'►',FC.RED,BC.blk); cv.put(35,4,'◄',FC.RED,BC.blk);
1747
+ cv.put(2,H-3,msg.padEnd(W-4,' '),FC.WHT,BC.blk);
1748
+ if(state==='gameover') cv.put(2,H-2,' BROKE! ENTER restart ',FC.WHT,BC.red);
1749
+ return cv.render();
1750
+ }
1751
+
1752
+ return { start, input, tick, frame,
1753
+ on:em.on.bind(em), off:em.off.bind(em),
1754
+ get state(){return state;}, get score(){return score??credits;} };
1755
+ }
1756
+
1757
+ // ─── GAME: VIDEO POKER ───────────────────────────────────────────────────────
1758
+
1759
+ function makeVideoPoker(opts = {}) {
1760
+ const em=makeEmitter();
1761
+ const SUITS=['♠','♥','♦','♣'],RANKS=['2','3','4','5','6','7','8','9','10','J','Q','K','A'];
1762
+ const SUIT_COL={'♠':FC.WHT,'♥':FC.RED,'♦':FC.RED,'♣':FC.WHT};
1763
+ const HANDS=[
1764
+ ['Royal Flush',800],['Straight Flush',50],['Four of a Kind',25],
1765
+ ['Full House',9],['Flush',6],['Straight',4],
1766
+ ['Three of a Kind',3],['Two Pair',2],['Jacks or Better',1],['Nothing',0],
1767
+ ];
1768
+ let deck,hand,held,credits,bet,state,score,msg,phase;
1769
+
1770
+ function makeDeck(){ return shuffle(SUITS.flatMap(s=>RANKS.map(r=>({r,s})))); }
1771
+ function deal(){ return deck.pop()??makeDeck().pop(); }
1772
+
1773
+ function evalHand(h){
1774
+ const ri=h.map(c=>RANKS.indexOf(c.r)).sort((a,b)=>a-b);
1775
+ const flush=h.every(c=>c.s===h[0].s);
1776
+ const straight=ri.every((v,i)=>i===0||v===ri[i-1]+1)||
1777
+ (ri.join(',')===('0,1,2,3,12')); // A-2-3-4-5
1778
+ const freq=Object.values(ri.reduce((m,v)=>{m[v]=(m[v]||0)+1;return m;},{})).sort((a,b)=>b-a);
1779
+ const pairs=freq.filter(f=>f===2).length;
1780
+ const hasJackOrBetter=pairs>=1&&h.some(c=>['J','Q','K','A'].includes(c.r)&&
1781
+ h.filter(x=>x.r===c.r).length===2);
1782
+ if(flush&&straight&&ri[4]===12&&ri[0]===8) return 'Royal Flush';
1783
+ if(flush&&straight) return 'Straight Flush';
1784
+ if(freq[0]===4) return 'Four of a Kind';
1785
+ if(freq[0]===3&&freq[1]===2) return 'Full House';
1786
+ if(flush) return 'Flush';
1787
+ if(straight) return 'Straight';
1788
+ if(freq[0]===3) return 'Three of a Kind';
1789
+ if(pairs===2) return 'Two Pair';
1790
+ if(hasJackOrBetter) return 'Jacks or Better';
1791
+ return 'Nothing';
1792
+ }
1793
+
1794
+ function start(){
1795
+ deck=makeDeck(); credits=opts.credits??100; bet=10;
1796
+ hand=[]; held=[false,false,false,false,false];
1797
+ state='idle'; score=credits; msg='ENTER to deal +/- adjust bet';
1798
+ em.emit('start');
1799
+ }
1800
+
1801
+ function startRound(){
1802
+ if(credits<bet){msg='Not enough credits!';return;}
1803
+ credits-=bet;
1804
+ hand=[deal(),deal(),deal(),deal(),deal()];
1805
+ held=[false,false,false,false,false];
1806
+ phase='hold'; state='playing'; msg='1-5 toggle hold ENTER draw';
1807
+ }
1808
+
1809
+ function draw(){
1810
+ for(let i=0;i<5;i++) if(!held[i]) hand[i]=deal();
1811
+ const hn=evalHand(hand);
1812
+ const [,mult]=HANDS.find(([n])=>n===hn)??['',0];
1813
+ const win=mult*bet;
1814
+ credits+=win; score=credits; em.emit('score',credits);
1815
+ msg=`${hn}${win?` — Win ${win}!`:''}`;
1816
+ phase='result'; state='roundover';
1817
+ if(credits<=0){state='gameover';em.emit('gameover',0);}
1818
+ }
1819
+
1820
+ function input(key){
1821
+ if(state==='gameover'&&key==='enter'){start();return;}
1822
+ if(state==='roundover'&&key==='enter'){state='idle';msg='ENTER to deal +/- bet';return;}
1823
+ if(state==='idle'){
1824
+ if(key==='+') bet=Math.min(credits,bet+5);
1825
+ if(key==='-') bet=Math.max(5,bet-5);
1826
+ if(key==='enter'||key==='space') startRound();
1827
+ return;
1828
+ }
1829
+ if(state!=='playing') return;
1830
+ if(/^[1-5]$/.test(key)) held[+key-1]=!held[+key-1];
1831
+ if(key==='enter'||key==='space') draw();
1832
+ }
1833
+
1834
+ function frame(){
1835
+ const W=50,H=18;
1836
+ const cv=makeCanvas(W,H); cv.clear(' ',FC.wht,BC.blk);
1837
+ cv.put(0,0,` VIDEO POKER (Jacks or Better) Credits:${credits} Bet:${bet}`,FC.YEL,BC.blk);
1838
+ // paytable
1839
+ const pOx=35;
1840
+ HANDS.forEach(([n,m],i)=>{
1841
+ cv.put(pOx,i+2,`${n}: ${m*bet}`,m?FC.YEL:FC.BLK,BC.blk);
1842
+ });
1843
+ // cards
1844
+ for(let i=0;i<5;i++){
1845
+ const cx2=i*7+1, cy2=3;
1846
+ if(hand[i]){
1847
+ cv.box(cx2,cy2,6,5,FC.wht,BC.blk);
1848
+ cv.put(cx2+1,cy2+1,`${hand[i].r}`,SUIT_COL[hand[i].s],BC.blk);
1849
+ cv.put(cx2+2,cy2+2,`${hand[i].s}`,SUIT_COL[hand[i].s],BC.blk);
1850
+ cv.put(cx2+1,cy2+3,held[i]?'HELD':' ',held[i]?FC.YEL:FC.BLK,BC.blk);
1851
+ } else {
1852
+ cv.box(cx2,cy2,6,5,FC.BLK,BC.blk);
1853
+ }
1854
+ cv.put(cx2+2,cy2+5,` ${i+1} `,FC.BLK,BC.blk);
1855
+ }
1856
+ cv.put(1,9,msg.padEnd(33,' '),FC.WHT,BC.blk);
1857
+ if(state==='idle') cv.put(1,10,'ENTER to deal',FC.GRN,BC.blk);
1858
+ if(state==='gameover') cv.put(1,10,' BROKE! ENTER restart ',FC.WHT,BC.red);
1859
+ return cv.render();
1860
+ }
1861
+
1862
+ return { start, input, tick(){}, frame,
1863
+ on:em.on.bind(em), off:em.off.bind(em),
1864
+ get state(){return state;}, get score(){return score??credits;} };
1865
+ }
1866
+
1867
+ // ─── GAME: HANGMAN ───────────────────────────────────────────────────────────
1868
+
1869
+ function makeHangman(opts = {}) {
1870
+ const em=makeEmitter();
1871
+ let word,guessed,wrong,state,score;
1872
+
1873
+ const GALLOWS=[
1874
+ [' ',' ',' ',' ',' ',' ',' '],
1875
+ [' ',' ',' ',' ',' ',' ','═════'],
1876
+ [' ╔ ',' ║ ',' ',' ',' ',' ','═════'],
1877
+ [' ╔══',' ║ ',' ',' ',' ',' ','═════'],
1878
+ [' ╔══',' ║ ',' ○ ',' ',' ',' ','═════'],
1879
+ [' ╔══',' ║ ',' ○ ',' │ ',' ',' ','═════'],
1880
+ [' ╔══',' ║ ',' ○ ',' /│ ',' ',' ','═════'],
1881
+ [' ╔══',' ║ ',' ○ ',' /│\\ ',' ',' ','═════'],
1882
+ [' ╔══',' ║ ',' ○ ',' /│\\ ',' / ',' ','═════'],
1883
+ [' ╔══',' ║ ',' ○ ',' /│\\ ',' / \\ ',' ','═════'],
1884
+ ];
1885
+
1886
+ function start(){
1887
+ word=HANGMAN_WORDS[rng(HANGMAN_WORDS.length)].toUpperCase();
1888
+ guessed=new Set(); wrong=0; state='playing'; score=0;
1889
+ em.emit('start');
1890
+ }
1891
+
1892
+ function input(key){
1893
+ if((state==='gameover'||state==='won')&&key==='enter'){start();return;}
1894
+ if(state!=='playing') return;
1895
+ if(/^[a-z]$/.test(key)){
1896
+ const ch=key.toUpperCase();
1897
+ if(guessed.has(ch)) return;
1898
+ guessed.add(ch);
1899
+ if(!word.includes(ch)){
1900
+ wrong++; if(wrong>=9){state='gameover';em.emit('gameover',0);}
1901
+ } else {
1902
+ if([...word].every(c=>guessed.has(c))){
1903
+ score=Math.max(10,(9-wrong)*100); state='won'; em.emit('win',score);
1904
+ }
1905
+ }
1906
+ }
1907
+ }
1908
+
1909
+ function frame(){
1910
+ const W=50,H=14;
1911
+ const cv=makeCanvas(W,H); cv.clear(' ',FC.wht,BC.blk);
1912
+ cv.put(0,0,` HANGMAN Wrong:${wrong}/9 [a-z to guess ENTER restart]`,FC.YEL,BC.blk);
1913
+ // gallows
1914
+ const stage=GALLOWS[Math.min(wrong,9)];
1915
+ for(let i=0;i<stage.length;i++) cv.put(1,i+2,stage[i],FC.wht,BC.blk);
1916
+ // word display
1917
+ const display=[...word].map(c=>guessed.has(c)?c:'_').join(' ');
1918
+ cv.put(10,4,display,FC.CYN,BC.blk);
1919
+ // guessed letters
1920
+ const sorted=[...guessed].sort();
1921
+ cv.put(10,6,`Guessed: ${sorted.join(' ')}`,FC.BLK,BC.blk);
1922
+ if(state==='won') cv.put(10,8,` YOU WIN! The word was ${word} `,FC.WHT,BC.grn);
1923
+ if(state==='gameover') cv.put(10,8,` GAME OVER! Word: ${word} `,FC.WHT,BC.red);
1924
+ if(state!=='idle') cv.put(10,9,`Hint: ${word.length} letters`,FC.BLK,BC.blk);
1925
+ return cv.render();
1926
+ }
1927
+
1928
+ return { start, input, tick(){}, frame,
1929
+ on:em.on.bind(em), off:em.off.bind(em),
1930
+ get state(){return state;}, get score(){return score;} };
1931
+ }
1932
+
1933
+ // ─── GAME: TYPING TEST ───────────────────────────────────────────────────────
1934
+
1935
+ function makeTypingTest(opts = {}) {
1936
+ const em=makeEmitter();
1937
+ let phrase,typed,startTime,endTime,wpm,accuracy,state,score,phraseIdx;
1938
+
1939
+ function start(){
1940
+ phraseIdx=rng(TYPING_PHRASES.length);
1941
+ phrase=TYPING_PHRASES[phraseIdx];
1942
+ typed=''; startTime=null; endTime=null; wpm=0; accuracy=100;
1943
+ state='playing'; score=0;
1944
+ em.emit('start');
1945
+ }
1946
+
1947
+ function input(key){
1948
+ if((state==='won'||state==='gameover')&&key==='enter'){start();return;}
1949
+ if(state!=='playing') return;
1950
+ if(!startTime) startTime=Date.now();
1951
+ if(key==='backspace'||key==='delete'){ typed=typed.slice(0,-1); return; }
1952
+ if(key.length===1) {
1953
+ typed+=key;
1954
+ if(typed.length===phrase.length){
1955
+ endTime=Date.now();
1956
+ const elapsed=(endTime-startTime)/60000;
1957
+ const words=phrase.split(' ').length;
1958
+ wpm=Math.round(words/elapsed);
1959
+ const correct=[...typed].filter((c,i)=>c===phrase[i]).length;
1960
+ accuracy=Math.round(correct/phrase.length*100);
1961
+ score=wpm*accuracy/100|0;
1962
+ state='won'; em.emit('win',score);
1963
+ }
1964
+ }
1965
+ }
1966
+
1967
+ function frame(){
1968
+ const W=60,H=14;
1969
+ const cv=makeCanvas(W,H); cv.clear(' ',FC.wht,BC.blk);
1970
+ const elapsed=startTime?((Date.now()-startTime)/1000).toFixed(1):'0.0';
1971
+ cv.put(0,0,` TYPING TEST Time:${elapsed}s WPM:${wpm} Accuracy:${accuracy}%`,FC.YEL,BC.blk);
1972
+ cv.put(1,2,'Type the phrase below:',FC.CYN,BC.blk);
1973
+ // wrap phrase at 56 chars
1974
+ const wrap=56;
1975
+ let py=4;
1976
+ for(let i=0;i<phrase.length;i+=wrap){
1977
+ const chunk=phrase.slice(i,i+wrap);
1978
+ for(let j=0;j<chunk.length;j++){
1979
+ const gi=i+j;
1980
+ let col=FC.BLK;
1981
+ if(gi<typed.length) col=typed[gi]===phrase[gi]?FC.GRN:FC.RED;
1982
+ cv.set(j+2,py,phrase[gi],col,BC.blk);
1983
+ }
1984
+ // show cursor
1985
+ if(typed.length>=i&&typed.length<i+wrap) cv.set(typed.length-i+2,py,'▌',FC.WHT,BC.blk);
1986
+ py++;
1987
+ }
1988
+ if(state==='won'){
1989
+ cv.put(1,py+1,` Done! WPM:${wpm} Accuracy:${accuracy}% Score:${score} ENTER again `,FC.WHT,BC.grn);
1990
+ } else {
1991
+ cv.put(1,py+1,' [Type above. Backspace to correct]',FC.BLK,BC.blk);
1992
+ }
1993
+ return cv.render();
1994
+ }
1995
+
1996
+ return { start, input, tick(){}, frame,
1997
+ on:em.on.bind(em), off:em.off.bind(em),
1998
+ get state(){return state;}, get score(){return score;} };
1999
+ }
2000
+
2001
+ // ─── GAME: SIMON SAYS ────────────────────────────────────────────────────────
2002
+
2003
+ function makeSimon(opts = {}) {
2004
+ const em=makeEmitter();
2005
+ const COLORS=['RED','GRN','BLU','YEL'];
2006
+ const FC_MAP={RED:FC.RED,GRN:FC.GRN,BLU:FC.BLU,YEL:FC.YEL};
2007
+ const BC_MAP={RED:BC.red,GRN:BC.grn,BLU:BC.blu,YEL:BC.yel};
2008
+ let sequence,playerIdx,showIdx,showing,showTimer,state,score,lit,msg,speed;
2009
+
2010
+ function start(){
2011
+ sequence=[COLORS[rng(4)]]; playerIdx=0; showIdx=0; showing=true;
2012
+ showTimer=0; state='playing'; score=0; lit=null; speed=800; msg='Watch...';
2013
+ em.emit('start');
2014
+ }
2015
+
2016
+ function input(key){
2017
+ if(state==='gameover'&&key==='enter'){start();return;}
2018
+ if(state!=='playing'||showing) return;
2019
+ const map={'1':'RED','2':'GRN','3':'BLU','4':'YEL'};
2020
+ const chosen=map[key]; if(!chosen) return;
2021
+ lit=chosen; setTimeout(()=>{lit=null;},200);
2022
+ if(chosen===sequence[playerIdx]){
2023
+ playerIdx++;
2024
+ if(playerIdx===sequence.length){
2025
+ score+=sequence.length; em.emit('score',score);
2026
+ sequence.push(COLORS[rng(4)]); speed=Math.max(300,speed-30);
2027
+ showIdx=0; playerIdx=0; showing=true; msg='Watch...';
2028
+ }
2029
+ } else {
2030
+ state='gameover'; msg=`Wrong! The sequence was: ${sequence.join(' ')}`; em.emit('gameover',score);
2031
+ }
2032
+ }
2033
+
2034
+ function tick(dt=16){
2035
+ if(state!=='playing'||!showing) return;
2036
+ showTimer+=dt;
2037
+ if(showTimer>=speed){
2038
+ showTimer=0; lit=null;
2039
+ if(showIdx<sequence.length){
2040
+ lit=sequence[showIdx]; showIdx++;
2041
+ } else {
2042
+ showing=false; lit=null; msg=`Round ${score/sequence.length+1}! Press 1-4`;
2043
+ }
2044
+ } else if(showTimer<speed*0.6){
2045
+ if(showIdx>0&&showIdx<=sequence.length) lit=sequence[showIdx-1];
2046
+ } else {
2047
+ lit=null;
2048
+ }
2049
+ }
2050
+
2051
+ function frame(){
2052
+ const W=40,H=18;
2053
+ const cv=makeCanvas(W,H); cv.clear(' ',FC.wht,BC.blk);
2054
+ cv.put(0,0,` SIMON SAYS Round:${sequence.length} Score:${score}`,FC.YEL,BC.blk);
2055
+ // 4 color pads: 2×2 layout
2056
+ const pads=[['RED',1,2],['GRN',21,2],['BLU',1,10],['YEL',21,10]];
2057
+ const labels=[['RED','[1]'],['GRN','[2]'],['BLU','[3]'],['YEL','[4]']];
2058
+ for(let i=0;i<4;i++){
2059
+ const [col,ox,oy]=pads[i];
2060
+ const active=lit===col;
2061
+ const fc=active?FC.WHT:FC_MAP[col];
2062
+ const bc=active?BC_MAP[col]:BC.blk;
2063
+ cv.fill(ox,oy,16,6,' ',fc,bc);
2064
+ cv.put(ox+5,oy+2,` ${col} `,fc,bc);
2065
+ cv.put(ox+5,oy+3,` ${labels[i][1]} `,fc,bc);
2066
+ }
2067
+ cv.put(1,H-3,msg.padEnd(W-2,' '),FC.WHT,BC.blk);
2068
+ if(state==='gameover') cv.put(1,H-2,' GAME OVER ENTER restart ',FC.WHT,BC.red);
2069
+ return cv.render();
2070
+ }
2071
+
2072
+ return { start, input, tick, frame,
2073
+ on:em.on.bind(em), off:em.off.bind(em),
2074
+ get state(){return state;}, get score(){return score;} };
2075
+ }
2076
+
2077
+ // ─── GAME: ROCK PAPER SCISSORS LIZARD SPOCK ─────────────────────────────────
2078
+
2079
+ function makeRockPaperScissors(opts = {}) {
2080
+ const em=makeEmitter();
2081
+ const CHOICES=['Rock','Paper','Scissors','Lizard','Spock'];
2082
+ const BEATS={
2083
+ Rock:['Scissors','Lizard'], Paper:['Rock','Spock'],
2084
+ Scissors:['Paper','Lizard'], Lizard:['Spock','Paper'], Spock:['Rock','Scissors'],
2085
+ };
2086
+ const VERBS={
2087
+ 'Rock-Scissors':'crushes','Rock-Lizard':'crushes',
2088
+ 'Paper-Rock':'covers','Paper-Spock':'disproves',
2089
+ 'Scissors-Paper':'cuts','Scissors-Lizard':'decapitates',
2090
+ 'Lizard-Spock':'poisons','Lizard-Paper':'eats',
2091
+ 'Spock-Rock':'vaporizes','Spock-Scissors':'smashes',
2092
+ };
2093
+ let curIdx,stats,state,score,msg,lastResult;
2094
+
2095
+ function start(){
2096
+ curIdx=0; stats={wins:0,losses:0,draws:0};
2097
+ state='playing'; score=0; msg='Choose your weapon!'; lastResult=null;
2098
+ em.emit('start');
2099
+ }
2100
+
2101
+ function play(choice){
2102
+ const ai=CHOICES[rng(5)];
2103
+ if(choice===ai){ stats.draws++; msg=`DRAW! Both chose ${choice}.`; }
2104
+ else if(BEATS[choice].includes(ai)){
2105
+ stats.wins++; score+=10; em.emit('score',score);
2106
+ const verb=VERBS[`${choice}-${ai}`]??'beats';
2107
+ msg=`WIN! ${choice} ${verb} ${ai}!`;
2108
+ } else {
2109
+ stats.losses++;
2110
+ const verb=VERBS[`${ai}-${choice}`]??'beats';
2111
+ msg=`LOSE! ${ai} ${verb} ${choice}.`;
2112
+ }
2113
+ lastResult={choice,ai};
2114
+ }
2115
+
2116
+ function input(key){
2117
+ if(state!=='playing') return;
2118
+ if(key==='left') curIdx=(curIdx-1+CHOICES.length)%CHOICES.length;
2119
+ if(key==='right') curIdx=(curIdx+1)%CHOICES.length;
2120
+ if(key==='enter'||key==='space') play(CHOICES[curIdx]);
2121
+ // shortcuts
2122
+ const shortcuts={r:0,p:1,s:2,l:3,v:4};
2123
+ if(shortcuts[key]!==undefined){ curIdx=shortcuts[key]; play(CHOICES[curIdx]); }
2124
+ }
2125
+
2126
+ function frame(){
2127
+ const W=50,H=16;
2128
+ const cv=makeCanvas(W,H); cv.clear(' ',FC.wht,BC.blk);
2129
+ cv.put(0,0,` RPS-LS Score:${score} W:${stats.wins} L:${stats.losses} D:${stats.draws}`,FC.YEL,BC.blk);
2130
+ cv.put(1,2,'Rock Paper Scissors Lizard Spock',FC.BLK,BC.blk);
2131
+ cv.put(1,3,'[R] [P] [S] [L] [V]',FC.BLK,BC.blk);
2132
+ // selector
2133
+ const offsets=[0,6,13,23,30];
2134
+ cv.put(offsets[curIdx]+1,2,CHOICES[curIdx],FC.CYN,BC.blk);
2135
+ cv.put(offsets[curIdx]+1,4,'▲',FC.YEL,BC.blk);
2136
+ // icons
2137
+ const ICONS=['🪨','📄','✂️','🦎','🖖'];
2138
+ const txts=['Rock ','Paper','Sciss','Lizrd','Spock'];
2139
+ cv.put(1,5,'[←→ select ENTER choose or R/P/S/L/V]',FC.BLK,BC.blk);
2140
+ if(msg) cv.put(1,7,msg.padEnd(W-2,' '),FC.WHT,BC.blk);
2141
+ if(lastResult){
2142
+ cv.put(1,9,`You: ${lastResult.choice.padEnd(10,' ')} AI: ${lastResult.ai}`,FC.CYN,BC.blk);
2143
+ }
2144
+ cv.put(1,11,'Win conditions:',FC.BLK,BC.blk);
2145
+ cv.put(1,12,'Rock crushes Scissors/Lizard',FC.BLK,BC.blk);
2146
+ cv.put(1,13,'Paper covers Rock, disproves Spock',FC.BLK,BC.blk);
2147
+ cv.put(1,14,'Scissors cuts Paper, decapitates Lizard',FC.BLK,BC.blk);
2148
+ cv.put(1,15,'Lizard poisons Spock, eats Paper',FC.BLK,BC.blk);
2149
+ return cv.render();
2150
+ }
2151
+
2152
+ return { start, input, tick(){}, frame,
2153
+ on:em.on.bind(em), off:em.off.bind(em),
2154
+ get state(){return state;}, get score(){return score;} };
2155
+ }
2156
+
2157
+ // ─── GAME: DUNGEON CRAWLER ───────────────────────────────────────────────────
2158
+
2159
+ function makeDungeon(opts = {}) {
2160
+ const em=makeEmitter();
2161
+ const W=5, H=4; // room grid
2162
+ const MONSTERS=[
2163
+ {name:'Slime',hp:8,atk:2,xp:15,gold:5},
2164
+ {name:'Goblin',hp:14,atk:4,xp:25,gold:10},
2165
+ {name:'Orc',hp:22,atk:6,xp:40,gold:20},
2166
+ {name:'Troll',hp:35,atk:8,xp:60,gold:35},
2167
+ {name:'Dragon',hp:60,atk:12,xp:120,gold:80},
2168
+ ];
2169
+ const ITEMS=[
2170
+ {name:'Health Potion',effect:'heal',val:20,cost:15},
2171
+ {name:'Sword',effect:'atk',val:3,cost:30},
2172
+ {name:'Shield',effect:'def',val:2,cost:25},
2173
+ {name:'Elixir',effect:'heal',val:50,cost:40},
2174
+ ];
2175
+ let rooms,px,py,player,combat,state,score,msg,log;
2176
+
2177
+ function makeRooms(){
2178
+ const types=['empty','empty','empty','monster','monster','monster','monster','treasure','shop','boss'];
2179
+ const shuffled=shuffle(types);
2180
+ const grid=Array.from({length:H},(_,r)=>Array.from({length:W},(_,c)=>{
2181
+ const idx=r*W+c;
2182
+ if(r===0&&c===0) return {type:'empty',visited:false,cleared:true,monster:null,item:null};
2183
+ const t=shuffled[idx%shuffled.length];
2184
+ let monster=null, item=null;
2185
+ if(t==='monster'||t==='boss'){
2186
+ const mi=t==='boss'?4:rng(4);
2187
+ monster={...MONSTERS[mi],maxHp:MONSTERS[mi].hp,hp:MONSTERS[mi].hp};
2188
+ }
2189
+ if(t==='treasure') item=ITEMS[rng(ITEMS.length)];
2190
+ return {type:t,visited:false,cleared:t==='empty',monster,item};
2191
+ }));
2192
+ // place exit at bottom-right
2193
+ grid[H-1][W-1]={type:'exit',visited:false,cleared:true};
2194
+ return grid;
2195
+ }
2196
+
2197
+ function start(){
2198
+ rooms=makeRooms(); px=0; py=0; rooms[0][0].visited=true;
2199
+ player={hp:30,maxHp:30,atk:5,def:2,level:1,xp:0,xpNext:100,gold:20,inv:[]};
2200
+ combat=null; state='playing'; score=0; log=[]; msg='Find the exit ★!';
2201
+ em.emit('start');
2202
+ }
2203
+
2204
+ function addLog(s){ log.unshift(s); if(log.length>5) log.pop(); }
2205
+
2206
+ function enterRoom(){
2207
+ const room=rooms[py][px];
2208
+ room.visited=true;
2209
+ if(room.type==='exit'){ score=player.gold+player.xp; state='won'; em.emit('win',score); return; }
2210
+ if(room.type==='treasure'&&room.item&&!room.cleared){
2211
+ const it=room.item;
2212
+ player.inv.push(it.name);
2213
+ if(it.effect==='heal') player.hp=Math.min(player.maxHp,player.hp+it.val);
2214
+ if(it.effect==='atk') player.atk+=it.val;
2215
+ if(it.effect==='def') player.def+=it.val;
2216
+ addLog(`Found ${it.name}!`); room.cleared=true; return;
2217
+ }
2218
+ if((room.type==='monster'||room.type==='boss')&&room.monster&&!room.cleared){
2219
+ combat={...room.monster}; msg=`Fighting ${combat.name}! A=attack F=flee`;
2220
+ }
2221
+ }
2222
+
2223
+ function attackRound(flee=false){
2224
+ if(!combat) return;
2225
+ if(flee){
2226
+ if(Math.random()<0.4){
2227
+ combat=null; px=clamp(px+(rng(2)?1:-1),0,W-1); py=clamp(py+(rng(2)?1:-1),0,H-1);
2228
+ addLog('Fled!'); return;
2229
+ } else { addLog('Couldn\'t flee!'); }
2230
+ }
2231
+ // player attacks
2232
+ const pdmg=Math.max(1,(Math.random()*player.atk|0)+1);
2233
+ combat.hp-=pdmg; addLog(`Hit ${combat.name} for ${pdmg}`);
2234
+ if(combat.hp<=0){
2235
+ const room=rooms[py][px];
2236
+ room.cleared=true; room.monster=null;
2237
+ player.xp+=combat.xp; player.gold+=combat.gold;
2238
+ addLog(`${combat.name} defeated! +${combat.xp}xp +${combat.gold}g`);
2239
+ while(player.xp>=player.xpNext){
2240
+ player.xp-=player.xpNext; player.level++; player.xpNext=player.level*100;
2241
+ player.atk+=2; player.def+=1; player.maxHp+=10; player.hp=player.maxHp;
2242
+ addLog(`Level up! Now level ${player.level}`);
2243
+ }
2244
+ combat=null; msg='Room cleared!'; return;
2245
+ }
2246
+ // monster attacks
2247
+ const mdmg=Math.max(1,(Math.random()*combat.atk|0)+1-player.def);
2248
+ player.hp-=mdmg; addLog(`${combat.name} hits you for ${mdmg}`);
2249
+ if(player.hp<=0){ player.hp=0; state='gameover'; em.emit('gameover',score); msg='You died!'; combat=null; }
2250
+ }
2251
+
2252
+ function usePotion(){
2253
+ const idx=player.inv.indexOf('Health Potion');
2254
+ if(idx<0){ addLog('No potions!'); return; }
2255
+ player.inv.splice(idx,1);
2256
+ player.hp=Math.min(player.maxHp,player.hp+20);
2257
+ addLog('Used Health Potion +20 HP');
2258
+ }
2259
+
2260
+ function input(key){
2261
+ if((state==='gameover'||state==='won')&&key==='enter'){start();return;}
2262
+ if(state!=='playing') return;
2263
+ if(combat){
2264
+ if(key==='a'||key==='space') attackRound(false);
2265
+ if(key==='f') attackRound(true);
2266
+ if(key==='p') usePotion();
2267
+ return;
2268
+ }
2269
+ let moved=false;
2270
+ if(key==='up' &&py>0) { py--; moved=true; }
2271
+ if(key==='down' &&py<H-1) { py++; moved=true; }
2272
+ if(key==='left' &&px>0) { px--; moved=true; }
2273
+ if(key==='right'&&px<W-1) { px++; moved=true; }
2274
+ if(moved) enterRoom();
2275
+ if(key==='p') usePotion();
2276
+ }
2277
+
2278
+ function frame(){
2279
+ const CW=W*6+4,CH=H*4+2, totalW=CW+30, totalH=Math.max(CH,18)+3;
2280
+ const cv=makeCanvas(totalW,totalH); cv.clear(' ',FC.wht,BC.blk);
2281
+ cv.put(0,0,` DUNGEON Level:${player.level} Gold:${player.gold} XP:${player.xp}/${player.xpNext}`,FC.YEL,BC.blk);
2282
+ // map
2283
+ const icons={empty:' ',monster:'M',boss:'B',treasure:'T',shop:'$',exit:'★'};
2284
+ const icols={empty:FC.BLK,monster:FC.RED,boss:FC.RED,treasure:FC.YEL,shop:FC.GRN,exit:FC.GRN};
2285
+ for(let r=0;r<H;r++) for(let c=0;c<W;c++){
2286
+ const room=rooms[r][c];
2287
+ const isHere=c===px&&r===py;
2288
+ const bx=c*6+1, by=r*4+2;
2289
+ cv.box(bx,by,5,3,isHere?FC.YEL:FC.BLK,BC.blk);
2290
+ if(room.visited){
2291
+ const ic=room.cleared?icons[room.type]??'?':icons[room.type]??'?';
2292
+ const col=room.cleared&&room.type!=='exit'?FC.BLK:icols[room.type]??FC.wht;
2293
+ cv.set(bx+2,by+1,isHere?'@':ic,isHere?FC.YEL:col,BC.blk);
2294
+ } else {
2295
+ cv.set(bx+2,by+1,'?',FC.BLK,BC.blk);
2296
+ }
2297
+ }
2298
+ // stats
2299
+ const sx=CW+2;
2300
+ cv.put(sx,2, `HP: ${player.hp}/${player.maxHp}`,player.hp<10?FC.RED:FC.GRN,BC.blk);
2301
+ cv.put(sx,3, `ATK:${player.atk} DEF:${player.def}`,FC.CYN,BC.blk);
2302
+ cv.put(sx,4, `Inv: ${player.inv.slice(0,3).join(', ')||'empty'}`,FC.wht,BC.blk);
2303
+ cv.put(sx,6, 'LOG:',FC.BLK,BC.blk);
2304
+ for(let i=0;i<Math.min(log.length,5);i++) cv.put(sx,7+i,log[i].slice(0,24),FC.wht,BC.blk);
2305
+ // combat
2306
+ if(combat){
2307
+ cv.put(sx,13,`COMBAT: ${combat.name}`,FC.RED,BC.blk);
2308
+ cv.put(sx,14,`Enemy HP: ${combat.hp}/${combat.maxHp}`,FC.RED,BC.blk);
2309
+ cv.put(sx,15,'A=attack F=flee P=potion',FC.YEL,BC.blk);
2310
+ } else {
2311
+ cv.put(sx,13,'[Arrows move P=potion]',FC.BLK,BC.blk);
2312
+ cv.put(sx,14,msg.slice(0,24),FC.wht,BC.blk);
2313
+ }
2314
+ cv.put(0,totalH-1,' M=Monster T=Treasure $=Shop ★=Exit ',FC.BLK,BC.blk);
2315
+ if(state==='gameover') cv.put(sx,16,' DEAD ENTER restart ',FC.WHT,BC.red);
2316
+ if(state==='won') cv.put(sx,16,` ESCAPED! Score:${score} ENTER again `,FC.WHT,BC.grn);
2317
+ return cv.render();
2318
+ }
2319
+
2320
+ return { start, input, tick(){}, frame,
2321
+ on:em.on.bind(em), off:em.off.bind(em),
2322
+ get state(){return state;}, get score(){return score;} };
2323
+ }
2324
+
2325
+ // ─── GAME: LEARNING CHATBOT ──────────────────────────────────────────────────
2326
+
2327
+ function makeLearningChatbot(opts = {}) {
2328
+ const em=makeEmitter();
2329
+
2330
+ const SLANG_SET=new Set(['lol','lmao','lmfao','omg','ngl','tbh','imo','idk','bruh','fr','nah','yea','ye','sus','vibes','slay','lowkey','highkey','based','cringe','bussin','no cap','deadass','bet','fire','snatched','rent free','understood the assignment','it hits different',
2331
+ 'main character','no printer','say less','big yikes','ick','rizz','simp','NPC',
2332
+ 'W','L','ratio','mid','slaps','hits different','touch grass','ate that']);
2333
+
2334
+ const FORMAL_MAP={
2335
+ 'lol':'haha','lmao':'haha','lmfao':'haha','omg':'oh my','ngl':'honestly',
2336
+ 'tbh':'to be honest','imo':'in my opinion','idk':'I don\'t know','bruh':'come on',
2337
+ 'fr':'for real','nah':'no','yea':'yes','ye':'yes','sus':'suspicious',
2338
+ 'bet':'okay','W':'win','L':'loss','mid':'mediocre','rizz':'charm',
2339
+ 'simp':'someone who is overly devoted','NPC':'background character',
2340
+ 'ratio':'when a reply gets more likes than the original post',
2341
+ 'no cap':'no lie','deadass':'seriously','bussin':'delicious',
2342
+ 'slay':'do something excellently','lowkey':'somewhat secretly',
2343
+ 'highkey':'very openly','based':'confident and unapologetically yourself',
2344
+ 'cringe':'embarrassing','vibes':'atmosphere or feeling',
2345
+ 'touch grass':'go outside and engage with real life',
2346
+ 'understood the assignment':'performed perfectly',
2347
+ 'it hits different':'feels uniquely special',
2348
+ 'main character':'acts like they\'re the protagonist of life',
2349
+ 'rent free':'occupying your thoughts without effort',
2350
+ 'fire':'amazing','snatched':'looking great','ick':'sudden repulsion',
2351
+ 'say less':'understood, no more needs to be said',
2352
+ 'big yikes':'that\'s very unfortunate','slaps':'is really good',
2353
+ 'ate that':'did something perfectly',
2354
+ };
2355
+
2356
+ // conversation state
2357
+ let msgs, learnQueue, score, state, inputBuf, mode;
2358
+ // mode: 'chat' | 'quiz'
2359
+ let quizWord, quizAnswer, quizStreak;
2360
+
2361
+ function start() {
2362
+ msgs = [
2363
+ { from:'bot', text:'Hey! I\'m SlangBot. I learn new slang from you and quiz you on it.' },
2364
+ { from:'bot', text:'Chat normally — I\'ll flag words I don\'t recognize. Type /quiz to test yourself, /list to see what I know.' },
2365
+ ];
2366
+ learnQueue = []; score = 0; state = 'playing'; inputBuf = ''; mode = 'chat';
2367
+ quizWord = null; quizAnswer = null; quizStreak = 0;
2368
+ em.emit('start');
2369
+ }
2370
+
2371
+ function botReply(text) { msgs.push({ from:'bot', text }); if (msgs.length > 30) msgs.shift(); }
2372
+ function userMsg(text) { msgs.push({ from:'usr', text }); if (msgs.length > 30) msgs.shift(); }
2373
+
2374
+ function tokenize(s) { return s.toLowerCase().match(/[a-z']+/g) ?? []; }
2375
+
2376
+ function processChat(line) {
2377
+ const words = tokenize(line);
2378
+ const unknown = words.filter(w => !SLANG_SET.has(w) && !FORMAL_MAP[w] && w.length > 2 &&
2379
+ !['the','and','but','for','you','are','was','not','that','this','with','have',
2380
+ 'from','they','will','been','just','when','what','your','can','its','him',
2381
+ 'her','had','his','has','she','did','how','all','one','out','who','get',
2382
+ 'use','say','way','may','see','now','our','any','new','two','day','got',
2383
+ 'let','put','man','too','old','why','did','few','off','set','own','run',
2384
+ 'ive','im','its','id','dont','cant','wont','isnt','arent','wasnt',
2385
+ 'shouldnt','wouldnt','couldnt','aint'].includes(w));
2386
+
2387
+ // translate known slang
2388
+ const translated = words.map(w => FORMAL_MAP[w] ? `${FORMAL_MAP[w]}` : w).join(' ');
2389
+ const hasSlang = words.some(w => SLANG_SET.has(w) || FORMAL_MAP[w]);
2390
+
2391
+ if (hasSlang) {
2392
+ botReply(`Decoded: "${translated}"`);
2393
+ }
2394
+
2395
+ if (unknown.length > 0) {
2396
+ learnQueue.push(...unknown);
2397
+ botReply(`Hmm, I don't know: ${unknown.map(w=>`"${w}"`).join(', ')}. What do they mean? (type: word = meaning)`);
2398
+ } else if (!hasSlang) {
2399
+ // generic responses
2400
+ const responses = [
2401
+ 'Interesting! Tell me more.',
2402
+ 'Got it. Drop some slang and I\'ll decode it.',
2403
+ 'Cool. Try /quiz to test your slang knowledge.',
2404
+ 'Noted! I\'m always learning.',
2405
+ ];
2406
+ botReply(responses[msgs.length % responses.length]);
2407
+ }
2408
+ }
2409
+
2410
+ function processDefinition(line) {
2411
+ // format: word = meaning OR word: meaning
2412
+ const m = line.match(/^([a-z'\s]+?)\s*[=:]\s*(.+)$/i);
2413
+ if (!m) { botReply('Try: word = meaning'); return; }
2414
+ const word = m[1].trim().toLowerCase();
2415
+ const meaning = m[2].trim();
2416
+ if (FORMAL_MAP[word]) {
2417
+ botReply(`I already know "${word}" means "${FORMAL_MAP[word]}"!`);
2418
+ return;
2419
+ }
2420
+ FORMAL_MAP[word] = meaning;
2421
+ SLANG_SET.add(word);
2422
+ learnQueue = learnQueue.filter(w => w !== word);
2423
+ score += 5; em.emit('score', score);
2424
+ botReply(`Learned! "${word}" = "${meaning}" ✓ (+5 points)`);
2425
+ }
2426
+
2427
+ function startQuiz() {
2428
+ const known = Object.keys(FORMAL_MAP);
2429
+ if (known.length < 3) { botReply('I need to know at least 3 words first. Chat with me!'); return; }
2430
+ quizWord = known[rng(known.length)];
2431
+ quizAnswer = FORMAL_MAP[quizWord];
2432
+ mode = 'quiz';
2433
+ botReply(`QUIZ TIME! What does "${quizWord}" mean? (type your answer)`);
2434
+ }
2435
+
2436
+ function checkQuiz(line) {
2437
+ const ans = line.toLowerCase().trim();
2438
+ const correct = quizAnswer.toLowerCase();
2439
+ // fuzzy: share 2+ words or substring
2440
+ const aWords = new Set(ans.split(/\s+/));
2441
+ const cWords = correct.split(/\s+/);
2442
+ const overlap = cWords.filter(w => aWords.has(w) || ans.includes(w)).length;
2443
+ const pass = overlap >= 1 || ans.includes(correct.slice(0,5));
2444
+ if (pass) {
2445
+ quizStreak++; score += 10 * quizStreak; em.emit('score', score);
2446
+ botReply(`✓ Correct! "${quizWord}" = "${quizAnswer}". Streak: ${quizStreak}x! (+${10*quizStreak}pts)`);
2447
+ } else {
2448
+ quizStreak = 0;
2449
+ botReply(`✗ Nope! "${quizWord}" means "${quizAnswer}". Streak reset.`);
2450
+ }
2451
+ mode = 'chat'; quizWord = null;
2452
+ }
2453
+
2454
+ function input(key) {
2455
+ if (state !== 'playing') { if (key === 'enter') start(); return; }
2456
+ if (key === 'enter') {
2457
+ const line = inputBuf.trim(); inputBuf = '';
2458
+ if (!line) return;
2459
+ userMsg(line);
2460
+ const lower = line.toLowerCase();
2461
+ if (mode === 'quiz') { checkQuiz(line); return; }
2462
+ if (lower === '/quiz') { startQuiz(); return; }
2463
+ if (lower === '/list') {
2464
+ const known = Object.keys(FORMAL_MAP);
2465
+ botReply(`I know ${known.length} words: ${known.slice(-8).join(', ')}${known.length>8?' …':''}`)
2466
+ return;
2467
+ }
2468
+ if (lower === '/reset') { botReply('Starting fresh!'); start(); return; }
2469
+ if (line.match(/^[a-z'\s]+\s*[=:]\s*.+$/i) && learnQueue.length > 0) {
2470
+ processDefinition(line);
2471
+ } else {
2472
+ processChat(line);
2473
+ }
2474
+ } else if (key === 'backspace' || key === 'delete') {
2475
+ inputBuf = inputBuf.slice(0, -1);
2476
+ } else if (key.length === 1) {
2477
+ if (inputBuf.length < 58) inputBuf += key;
2478
+ }
2479
+ }
2480
+
2481
+ function frame() {
2482
+ const W = 62, H = 22;
2483
+ const cv = makeCanvas(W, H); cv.clear(' ', FC.wht, BC.blk);
2484
+ cv.put(0, 0, ` SLANGBOT Score:${score} Streak:${quizStreak}x /quiz /list /reset`, FC.YEL, BC.blk);
2485
+ cv.put(0, 1, '─'.repeat(W), FC.BLK, BC.blk);
2486
+ // messages
2487
+ const visible = msgs.slice(-16);
2488
+ for (let i = 0; i < visible.length; i++) {
2489
+ const m = visible[i];
2490
+ const isBot = m.from === 'bot';
2491
+ const prefix = isBot ? 'BOT: ' : 'YOU: ';
2492
+ const col = isBot ? FC.CYN : FC.GRN;
2493
+ const text = (prefix + m.text).slice(0, W - 1);
2494
+ cv.put(0, i + 2, text, col, BC.blk);
2495
+ }
2496
+ // input line
2497
+ cv.put(0, H - 2, '─'.repeat(W), FC.BLK, BC.blk);
2498
+ const prompt = `> ${inputBuf}${Math.floor(Date.now()/500)%2===0?'▌':' '}`;
2499
+ cv.put(0, H - 1, prompt.padEnd(W, ' '), FC.WHT, BC.blk);
2500
+ if (mode === 'quiz') cv.put(W - 12, 0, ' [QUIZ] ', FC.WHT, BC.red);
2501
+ return cv.render();
2502
+ }
2503
+
2504
+ return {
2505
+ start, input, tick() {}, frame,
2506
+ on: em.on.bind(em), off: em.off.bind(em),
2507
+ get state() { return state; }, get score() { return score; },
2508
+ };
2509
+ }
2510
+
2511
+ 'use strict';
2512
+
2513
+ /**
2514
+ * RunGame(game)
2515
+ *
2516
+ * Drives any kitarcade-compatible game object.
2517
+ * Handles: raw stdin key parsing, tick loop, frame rendering, clean exit.
2518
+ *
2519
+ * Usage:
2520
+ * RunGame(kitdef.Snake());
2521
+ *
2522
+ * // or pass a factory and let RunGame call it:
2523
+ * RunGame(Tetris);
2524
+ */
2525
+
2526
+ function RunGame(game) {
2527
+ // accept either a live game object or a factory function
2528
+ if (typeof game === 'function') game = game();
2529
+
2530
+ const FPS = 30;
2531
+ const INTERVAL = 1000 / FPS;
2532
+
2533
+ // ── terminal setup ────────────────────────────────────────────────────────
2534
+
2535
+ const { stdin, stdout } = process;
2536
+
2537
+ function hideCursor() { stdout.write('\x1b[?25l'); }
2538
+ function showCursor() { stdout.write('\x1b[?25h'); }
2539
+ function clearScreen() { stdout.write('\x1b[2J\x1b[H'); }
2540
+ function home() { stdout.write('\x1b[H'); }
2541
+ function altScreen() { stdout.write('\x1b[?1049h'); }
2542
+ function mainScreen() { stdout.write('\x1b[?1049l'); }
2543
+
2544
+ // ── cleanup / exit ────────────────────────────────────────────────────────
2545
+
2546
+ let exiting = false;
2547
+
2548
+ function exit(code = 0) {
2549
+ if (exiting) return;
2550
+ exiting = true;
2551
+ clearInterval(tickTimer);
2552
+ if (stdin.isTTY) {
2553
+ stdin.setRawMode(false);
2554
+ stdin.pause();
2555
+ }
2556
+ showCursor();
2557
+ mainScreen();
2558
+ process.exit(code);
2559
+ }
2560
+
2561
+ // ── key parsing ───────────────────────────────────────────────────────────
2562
+
2563
+ // Maps raw terminal bytes → game key names
2564
+ const KEY_MAP = {
2565
+ '\x1b[A': 'up',
2566
+ '\x1b[B': 'down',
2567
+ '\x1b[C': 'right',
2568
+ '\x1b[D': 'left',
2569
+ '\x1b[1;2A': 'up', // shift-up (some terms)
2570
+ '\x1b[1;2B': 'down',
2571
+ '\x1b[1;2C': 'right',
2572
+ '\x1b[1;2D': 'left',
2573
+ '\x1bOA': 'up', // application cursor keys
2574
+ '\x1bOB': 'down',
2575
+ '\x1bOC': 'right',
2576
+ '\x1bOD': 'left',
2577
+ '\r': 'enter',
2578
+ '\n': 'enter',
2579
+ ' ': 'space',
2580
+ '\t': 'tab',
2581
+ '\x1b': 'escape',
2582
+ '\x7f': 'backspace',
2583
+ '\x08': 'backspace',
2584
+ '\x1b[3~':'delete',
2585
+ };
2586
+
2587
+ function parseKey(buf) {
2588
+ const s = buf.toString();
2589
+ if (KEY_MAP[s]) return KEY_MAP[s];
2590
+ // single printable ASCII
2591
+ if (s.length === 1 && s >= ' ' && s <= '~') return s.toLowerCase();
2592
+ // ctrl-c / ctrl-d → exit
2593
+ if (s === '\x03' || s === '\x04') { exit(0); return null; }
2594
+ return null;
2595
+ }
2596
+
2597
+ // ── render ────────────────────────────────────────────────────────────────
2598
+
2599
+ let lastFrame = '';
2600
+
2601
+ function render() {
2602
+ const f = game.frame();
2603
+ // only redraw if output changed (avoids flicker on idle games)
2604
+ if (f === lastFrame) return;
2605
+ lastFrame = f;
2606
+ home();
2607
+ stdout.write(f);
2608
+ // clear any leftover lines below the frame
2609
+ stdout.write('\x1b[J');
2610
+ }
2611
+
2612
+ // ── tick loop ─────────────────────────────────────────────────────────────
2613
+
2614
+ let prev = Date.now();
2615
+ const tickTimer = setInterval(() => {
2616
+ const now = Date.now();
2617
+ const dt = now - prev;
2618
+ prev = now;
2619
+ game.tick(dt);
2620
+ render();
2621
+ }, INTERVAL);
2622
+
2623
+ // ── stdin ─────────────────────────────────────────────────────────────────
2624
+
2625
+ if (stdin.isTTY) {
2626
+ stdin.setRawMode(true);
2627
+ } else {
2628
+ // piped / non-TTY: still work, just no raw keys
2629
+ process.stderr.write('[RunGame] stdin is not a TTY — keyboard input unavailable\n');
2630
+ }
2631
+
2632
+ stdin.resume();
2633
+ stdin.on('data', (buf) => {
2634
+ const key = parseKey(buf);
2635
+ if (key === null) return;
2636
+ game.input(key);
2637
+ // immediate render on input so the game feels responsive
2638
+ render();
2639
+ });
2640
+
2641
+ // ── game events ───────────────────────────────────────────────────────────
2642
+
2643
+ game.on('gameover', (score) => {
2644
+ render(); // show gameover frame
2645
+ // don't auto-exit — player can press ENTER to restart per game convention
2646
+ });
2647
+
2648
+ game.on('win', (score) => {
2649
+ render();
2650
+ });
2651
+
2652
+ // ── go ────────────────────────────────────────────────────────────────────
2653
+
2654
+ altScreen();
2655
+ hideCursor();
2656
+ clearScreen();
2657
+ render();
2658
+
2659
+ // handle process signals
2660
+ process.on('SIGINT', () => exit(0));
2661
+ process.on('SIGTERM', () => exit(0));
2662
+ process.on('exit', () => { showCursor(); mainScreen(); });
2663
+
2664
+ // return a handle in case the caller wants to stop it programmatically
2665
+ return {
2666
+ stop: () => exit(0),
2667
+ game,
2668
+ };
2669
+ }
2670
+
2671
+ // ─── EXPORT ──────────────────────────────────────────────────────────────────
2672
+
2673
+ const kitdef = {
2674
+ Snake: (opts) => { const g = makeSnake(opts); g.start(); return g; },
2675
+ Tetris: (opts) => { const g = makeTetris(opts); g.start(); return g; },
2676
+ DinoRun: (opts) => { const g = makeDinoRun(opts); g.start(); return g; },
2677
+ Breakout: (opts) => { const g = makeBreakout(opts); g.start(); return g; },
2678
+ SpaceInvaders: (opts) => { const g = makeSpaceInvaders(opts); g.start(); return g; },
2679
+ FlappyBird: (opts) => { const g = makeFlappyBird(opts); g.start(); return g; },
2680
+ Asteroids: (opts) => { const g = makeAsteroids(opts); g.start(); return g; },
2681
+ TwentyFortyEight: (opts) => { const g = makeTwentyFortyEight(opts); g.start(); return g; },
2682
+ Minesweeper: (opts) => { const g = makeMinesweeper(opts); g.start(); return g; },
2683
+ Wordle: (opts) => { const g = makeWordle(opts); g.start(); return g; },
2684
+ ConnectFour: (opts) => { const g = makeConnectFour(opts); g.start(); return g; },
2685
+ Battleship: (opts) => { const g = makeBattleship(opts); g.start(); return g; },
2686
+ Maze: (opts) => { const g = makeMaze(opts); g.start(); return g; },
2687
+ LightsOut: (opts) => { const g = makeLightsOut(opts); g.start(); return g; },
2688
+ Sudoku: (opts) => { const g = makeSudoku(opts); g.start(); return g; },
2689
+ Blackjack: (opts) => { const g = makeBlackjack(opts); g.start(); return g; },
2690
+ Slots: (opts) => { const g = makeSlots(opts); g.start(); return g; },
2691
+ VideoPoker: (opts) => { const g = makeVideoPoker(opts); g.start(); return g; },
2692
+ Hangman: (opts) => { const g = makeHangman(opts); g.start(); return g; },
2693
+ TypingTest: (opts) => { const g = makeTypingTest(opts); g.start(); return g; },
2694
+ Simon: (opts) => { const g = makeSimon(opts); g.start(); return g; },
2695
+ RockPaperScissors:(opts) => { const g = makeRockPaperScissors(opts); g.start(); return g; },
2696
+ Dungeon: (opts) => { const g = makeDungeon(opts); g.start(); return g; },
2697
+ SlangBot: (opts) => { const g = makeLearningChatbot(opts); g.start(); return g; },
2698
+ RunGame, // export the runner too, in case someone wants to use it directly
2699
+
2700
+ // list all available game names
2701
+ games: [
2702
+ 'Snake','Tetris','DinoRun','Breakout','SpaceInvaders','FlappyBird',
2703
+ 'Asteroids','TwentyFortyEight','Minesweeper','Wordle','ConnectFour',
2704
+ 'Battleship','Maze','LightsOut','Sudoku','Blackjack','Slots','VideoPoker',
2705
+ 'Hangman','TypingTest','Simon','RockPaperScissors','Dungeon','SlangBot',
2706
+ ],
2707
+ };
2708
+
2709
+ module.exports = { kitdef };