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.
- package/kits/kitarcade/kitdef.js +2709 -0
- package/kits/kitascii/kitdef.js +839 -0
- package/kits/kitlife/kitdef.js +575 -0
- package/kits/kitpet/kitdef.js +752 -0
- package/package.json +1 -1
|
@@ -0,0 +1,575 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* kitlife — Conway's Game of Life
|
|
5
|
+
*
|
|
6
|
+
* Full interactive terminal Life sim with:
|
|
7
|
+
* - Toroidal (wrapping) or bounded grid
|
|
8
|
+
* - Speed control (1–10)
|
|
9
|
+
* - 30+ named preset patterns
|
|
10
|
+
* - Draw mode: place/erase cells with cursor
|
|
11
|
+
* - Stats: generation, population, births, deaths, peak
|
|
12
|
+
* - Pattern library browser
|
|
13
|
+
* - Random fill
|
|
14
|
+
* - Step mode (advance one generation at a time)
|
|
15
|
+
*
|
|
16
|
+
* kitdef API:
|
|
17
|
+
* .start() reset to blank/preset
|
|
18
|
+
* .input(key) see controls below
|
|
19
|
+
* .tick(dt) advance simulation
|
|
20
|
+
* .frame() → ANSI string
|
|
21
|
+
* .state → 'running'|'paused'|'drawing'
|
|
22
|
+
* .score → generation count
|
|
23
|
+
*
|
|
24
|
+
* Controls:
|
|
25
|
+
* SPACE pause/resume
|
|
26
|
+
* ENTER step one generation (while paused)
|
|
27
|
+
* Arrow keys move cursor (in draw/pattern mode)
|
|
28
|
+
* D toggle draw mode (click cells alive/dead)
|
|
29
|
+
* X toggle cell under cursor (draw mode)
|
|
30
|
+
* C clear grid
|
|
31
|
+
* R random fill
|
|
32
|
+
* +/- speed up / slow down
|
|
33
|
+
* P open pattern browser
|
|
34
|
+
* T toggle toroidal wrapping
|
|
35
|
+
* W/H (with shift) resize grid (cycle presets)
|
|
36
|
+
*/
|
|
37
|
+
|
|
38
|
+
// ─── ANSI / CANVAS ───────────────────────────────────────────────────────────
|
|
39
|
+
|
|
40
|
+
const FC = {
|
|
41
|
+
blk:30,red:31,grn:32,yel:33,blu:34,mag:35,cyn:36,wht:37,
|
|
42
|
+
BLK:90,RED:91,GRN:92,YEL:93,BLU:94,MAG:95,CYN:96,WHT:97,
|
|
43
|
+
};
|
|
44
|
+
const BC = {
|
|
45
|
+
blk:40,red:41,grn:42,yel:43,blu:44,mag:45,cyn:46,wht:47,
|
|
46
|
+
BLK:100,RED:101,GRN:102,YEL:103,BLU:104,MAG:105,CYN:106,WHT:107,
|
|
47
|
+
};
|
|
48
|
+
const RST = '\x1b[0m';
|
|
49
|
+
const BLD = '\x1b[1m';
|
|
50
|
+
|
|
51
|
+
function makeCanvas(W, H) {
|
|
52
|
+
const ch = Array.from({length:H}, () => new Array(W).fill(' '));
|
|
53
|
+
const fg = Array.from({length:H}, () => new Array(W).fill(FC.wht));
|
|
54
|
+
const bg = Array.from({length:H}, () => new Array(W).fill(BC.blk));
|
|
55
|
+
const cv = {
|
|
56
|
+
W, H,
|
|
57
|
+
clear(c=' ', f=FC.wht, b=BC.blk) {
|
|
58
|
+
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; }
|
|
59
|
+
},
|
|
60
|
+
set(x, y, c, f=FC.wht, b=BC.blk) {
|
|
61
|
+
if (x<0||x>=W||y<0||y>=H) return;
|
|
62
|
+
ch[y][x]=(String(c)[0])||' '; fg[y][x]=f; bg[y][x]=b;
|
|
63
|
+
},
|
|
64
|
+
put(x, y, s, f=FC.wht, b=BC.blk) {
|
|
65
|
+
for (let i=0;i<s.length;i++) cv.set(x+i, y, s[i], f, b);
|
|
66
|
+
},
|
|
67
|
+
fill(x, y, w, h, c, f=FC.wht, b=BC.blk) {
|
|
68
|
+
for (let dy=0;dy<h;dy++) for (let dx=0;dx<w;dx++) cv.set(x+dx,y+dy,c,f,b);
|
|
69
|
+
},
|
|
70
|
+
box(x, y, w, h, f=FC.wht, b=BC.blk) {
|
|
71
|
+
cv.put(x, y, '┌'+'─'.repeat(w-2)+'┐', f, b);
|
|
72
|
+
for (let dy=1;dy<h-1;dy++) { cv.set(x,y+dy,'│',f,b); cv.set(x+w-1,y+dy,'│',f,b); }
|
|
73
|
+
cv.put(x, y+h-1, '└'+'─'.repeat(w-2)+'┘', f, b);
|
|
74
|
+
},
|
|
75
|
+
render() {
|
|
76
|
+
const lines = [];
|
|
77
|
+
for (let y=0;y<H;y++) {
|
|
78
|
+
let row='', cf=-1, cb=-1;
|
|
79
|
+
for (let x=0;x<W;x++) {
|
|
80
|
+
const f=fg[y][x], b=bg[y][x];
|
|
81
|
+
if (f!==cf||b!==cb) { row+=`\x1b[${f};${b}m`; cf=f; cb=b; }
|
|
82
|
+
row+=ch[y][x];
|
|
83
|
+
}
|
|
84
|
+
lines.push(row+RST);
|
|
85
|
+
}
|
|
86
|
+
return lines.join('\n');
|
|
87
|
+
},
|
|
88
|
+
};
|
|
89
|
+
return cv;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function makeEmitter() {
|
|
93
|
+
const L = new Map();
|
|
94
|
+
return {
|
|
95
|
+
on(ev,fn) { if(!L.has(ev)) L.set(ev,[]); L.get(ev).push(fn); },
|
|
96
|
+
off(ev,fn) { if(L.has(ev)) L.set(ev,L.get(ev).filter(f=>f!==fn)); },
|
|
97
|
+
emit(ev,...a) { (L.get(ev)??[]).forEach(fn=>fn(...a)); },
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const rng = n => Math.random()*n|0;
|
|
102
|
+
const clamp = (x,lo,hi) => Math.max(lo,Math.min(hi,x));
|
|
103
|
+
|
|
104
|
+
// ─── PATTERN LIBRARY ─────────────────────────────────────────────────────────
|
|
105
|
+
// Patterns stored as arrays of [col, row] offsets from center, or RLE strings.
|
|
106
|
+
// Using coordinate arrays for simplicity.
|
|
107
|
+
|
|
108
|
+
const PATTERNS = {
|
|
109
|
+
// ── Still lifes ──
|
|
110
|
+
'Block': {
|
|
111
|
+
cells: [[0,0],[1,0],[0,1],[1,1]],
|
|
112
|
+
desc: 'Simplest still life',
|
|
113
|
+
},
|
|
114
|
+
'Beehive': {
|
|
115
|
+
cells: [[1,0],[2,0],[0,1],[3,1],[1,2],[2,2]],
|
|
116
|
+
desc: 'Common still life',
|
|
117
|
+
},
|
|
118
|
+
'Loaf': {
|
|
119
|
+
cells: [[1,0],[2,0],[0,1],[3,1],[1,2],[3,2],[2,3]],
|
|
120
|
+
desc: 'Still life',
|
|
121
|
+
},
|
|
122
|
+
'Boat': {
|
|
123
|
+
cells: [[0,0],[1,0],[0,1],[2,1],[1,2]],
|
|
124
|
+
desc: 'Still life',
|
|
125
|
+
},
|
|
126
|
+
|
|
127
|
+
// ── Oscillators ──
|
|
128
|
+
'Blinker': {
|
|
129
|
+
cells: [[0,1],[1,1],[2,1]],
|
|
130
|
+
desc: 'Period-2 oscillator',
|
|
131
|
+
},
|
|
132
|
+
'Toad': {
|
|
133
|
+
cells: [[1,0],[2,0],[3,0],[0,1],[1,1],[2,1]],
|
|
134
|
+
desc: 'Period-2 oscillator',
|
|
135
|
+
},
|
|
136
|
+
'Beacon': {
|
|
137
|
+
cells: [[0,0],[1,0],[0,1],[3,2],[2,3],[3,3]],
|
|
138
|
+
desc: 'Period-2 oscillator',
|
|
139
|
+
},
|
|
140
|
+
'Pulsar': {
|
|
141
|
+
cells: [
|
|
142
|
+
[2,0],[3,0],[4,0],[8,0],[9,0],[10,0],
|
|
143
|
+
[0,2],[5,2],[7,2],[12,2],
|
|
144
|
+
[0,3],[5,3],[7,3],[12,3],
|
|
145
|
+
[0,4],[5,4],[7,4],[12,4],
|
|
146
|
+
[2,5],[3,5],[4,5],[8,5],[9,5],[10,5],
|
|
147
|
+
[2,7],[3,7],[4,7],[8,7],[9,7],[10,7],
|
|
148
|
+
[0,8],[5,8],[7,8],[12,8],
|
|
149
|
+
[0,9],[5,9],[7,9],[12,9],
|
|
150
|
+
[0,10],[5,10],[7,10],[12,10],
|
|
151
|
+
[2,12],[3,12],[4,12],[8,12],[9,12],[10,12],
|
|
152
|
+
],
|
|
153
|
+
desc: 'Period-3 oscillator',
|
|
154
|
+
},
|
|
155
|
+
'Pentadecathlon': {
|
|
156
|
+
cells: [[1,0],[2,0],[0,1],[3,1],[1,2],[2,2],[1,3],[2,3],[1,4],[2,4],[1,5],[2,5],[0,6],[3,6],[1,7],[2,7]],
|
|
157
|
+
desc: 'Period-15 oscillator',
|
|
158
|
+
},
|
|
159
|
+
'Clock': {
|
|
160
|
+
cells: [[1,0],[2,1],[0,1],[1,2],[2,2],[3,1]],
|
|
161
|
+
desc: 'Period-2 oscillator',
|
|
162
|
+
},
|
|
163
|
+
|
|
164
|
+
// ── Spaceships ──
|
|
165
|
+
'Glider': {
|
|
166
|
+
cells: [[1,0],[2,1],[0,2],[1,2],[2,2]],
|
|
167
|
+
desc: 'Simplest spaceship',
|
|
168
|
+
},
|
|
169
|
+
'LWSS': {
|
|
170
|
+
cells: [[1,0],[4,0],[0,1],[0,2],[4,2],[0,3],[1,3],[2,3],[3,3]],
|
|
171
|
+
desc: 'Light-weight spaceship',
|
|
172
|
+
},
|
|
173
|
+
'MWSS': {
|
|
174
|
+
cells: [[2,0],[0,1],[4,1],[0,2],[0,3],[4,3],[0,4],[1,4],[2,4],[3,4]],
|
|
175
|
+
desc: 'Middle-weight spaceship',
|
|
176
|
+
},
|
|
177
|
+
'HWSS': {
|
|
178
|
+
cells: [[2,0],[3,0],[0,1],[5,1],[0,2],[0,3],[5,3],[0,4],[1,4],[2,4],[3,4],[4,4]],
|
|
179
|
+
desc: 'Heavy-weight spaceship',
|
|
180
|
+
},
|
|
181
|
+
|
|
182
|
+
// ── Guns ──
|
|
183
|
+
'Gosper Glider Gun': {
|
|
184
|
+
cells: [
|
|
185
|
+
[24,0],
|
|
186
|
+
[22,1],[24,1],
|
|
187
|
+
[12,2],[13,2],[20,2],[21,2],[34,2],[35,2],
|
|
188
|
+
[11,3],[15,3],[20,3],[21,3],[34,3],[35,3],
|
|
189
|
+
[0,4],[1,4],[10,4],[16,4],[20,4],[21,4],
|
|
190
|
+
[0,5],[1,5],[10,5],[14,5],[16,5],[17,5],[22,5],[24,5],
|
|
191
|
+
[10,6],[16,6],[24,6],
|
|
192
|
+
[11,7],[15,7],
|
|
193
|
+
[12,8],[13,8],
|
|
194
|
+
],
|
|
195
|
+
desc: 'Produces a glider every 30 generations',
|
|
196
|
+
},
|
|
197
|
+
|
|
198
|
+
// ── Methuselahs ──
|
|
199
|
+
'R-pentomino': {
|
|
200
|
+
cells: [[1,0],[2,0],[0,1],[1,1],[1,2]],
|
|
201
|
+
desc: 'Lives 1103 generations',
|
|
202
|
+
},
|
|
203
|
+
'Diehard': {
|
|
204
|
+
cells: [[6,0],[0,1],[1,1],[1,2],[5,2],[6,2],[7,2]],
|
|
205
|
+
desc: 'Dies after 130 generations',
|
|
206
|
+
},
|
|
207
|
+
'Acorn': {
|
|
208
|
+
cells: [[1,0],[3,1],[0,2],[1,2],[4,2],[5,2],[6,2]],
|
|
209
|
+
desc: 'Lives 5206 generations',
|
|
210
|
+
},
|
|
211
|
+
|
|
212
|
+
// ── Fun patterns ──
|
|
213
|
+
'Glider Fleet': {
|
|
214
|
+
cells: [
|
|
215
|
+
[1,0],[2,1],[0,2],[1,2],[2,2],
|
|
216
|
+
[11,0],[12,1],[10,2],[11,2],[12,2],
|
|
217
|
+
[21,0],[22,1],[20,2],[21,2],[22,2],
|
|
218
|
+
[1,10],[2,11],[0,12],[1,12],[2,12],
|
|
219
|
+
[11,10],[12,11],[10,12],[11,12],[12,12],
|
|
220
|
+
],
|
|
221
|
+
desc: 'Five gliders in formation',
|
|
222
|
+
},
|
|
223
|
+
'Cross': {
|
|
224
|
+
cells: [
|
|
225
|
+
[2,0],[2,1],[0,2],[1,2],[2,2],[3,2],[4,2],[2,3],[2,4],
|
|
226
|
+
],
|
|
227
|
+
desc: 'Simple cross — evolves interestingly',
|
|
228
|
+
},
|
|
229
|
+
'Pi heptomino': {
|
|
230
|
+
cells: [[0,0],[1,0],[2,0],[0,1],[2,1],[0,2],[1,2],[2,2]],
|
|
231
|
+
desc: 'Chaotic methuselah',
|
|
232
|
+
},
|
|
233
|
+
'Infinite growth': {
|
|
234
|
+
cells: [
|
|
235
|
+
[0,0],[1,0],[2,0],[3,0],[4,0],[5,0],[6,0],[7,0],
|
|
236
|
+
[9,0],[10,0],[11,0],[12,0],[13,0],
|
|
237
|
+
[17,0],[18,0],[19,0],
|
|
238
|
+
[26,0],[27,0],[28,0],[29,0],[30,0],[31,0],[32,0],
|
|
239
|
+
[34,0],[35,0],[36,0],[37,0],[38,0],
|
|
240
|
+
],
|
|
241
|
+
desc: 'Population grows without bound',
|
|
242
|
+
},
|
|
243
|
+
};
|
|
244
|
+
|
|
245
|
+
const PATTERN_KEYS = Object.keys(PATTERNS);
|
|
246
|
+
|
|
247
|
+
// ─── LIFE ENGINE ─────────────────────────────────────────────────────────────
|
|
248
|
+
|
|
249
|
+
function makeLife(opts = {}) {
|
|
250
|
+
const GW = opts.width ?? 80;
|
|
251
|
+
const GH = opts.height ?? 40;
|
|
252
|
+
const em = makeEmitter();
|
|
253
|
+
|
|
254
|
+
// Use two flat Uint8Arrays and ping-pong
|
|
255
|
+
let cells = new Uint8Array(GW * GH);
|
|
256
|
+
let next_ = new Uint8Array(GW * GH);
|
|
257
|
+
|
|
258
|
+
let gen, population, births, deaths, peakPop;
|
|
259
|
+
let toroidal, drawMode, speed, tickAcc, state;
|
|
260
|
+
let curX, curY;
|
|
261
|
+
let patternMenu, patternIdx;
|
|
262
|
+
let score;
|
|
263
|
+
let ageCells; // for color-by-age rendering
|
|
264
|
+
|
|
265
|
+
function idx(x, y) { return y * GW + x; }
|
|
266
|
+
function wrap(x, y) {
|
|
267
|
+
if (toroidal) return [(x + GW) % GW, (y + GH) % GH];
|
|
268
|
+
return [clamp(x, 0, GW-1), clamp(y, 0, GH-1)];
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
function aliveAt(x, y) {
|
|
272
|
+
if (!toroidal && (x < 0 || x >= GW || y < 0 || y >= GH)) return 0;
|
|
273
|
+
const [wx, wy] = wrap(x, y);
|
|
274
|
+
return cells[idx(wx, wy)];
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
function countNeighbors(x, y) {
|
|
278
|
+
return aliveAt(x-1,y-1) + aliveAt(x,y-1) + aliveAt(x+1,y-1) +
|
|
279
|
+
aliveAt(x-1,y) + aliveAt(x+1,y) +
|
|
280
|
+
aliveAt(x-1,y+1) + aliveAt(x,y+1) + aliveAt(x+1,y+1);
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
function step() {
|
|
284
|
+
let born = 0, died = 0;
|
|
285
|
+
for (let y = 0; y < GH; y++) {
|
|
286
|
+
for (let x = 0; x < GW; x++) {
|
|
287
|
+
const n = countNeighbors(x, y);
|
|
288
|
+
const was = cells[idx(x, y)];
|
|
289
|
+
let now = 0;
|
|
290
|
+
if (was) {
|
|
291
|
+
now = (n === 2 || n === 3) ? 1 : 0;
|
|
292
|
+
} else {
|
|
293
|
+
now = n === 3 ? 1 : 0;
|
|
294
|
+
}
|
|
295
|
+
next_[idx(x, y)] = now;
|
|
296
|
+
if (!was && now) { born++; ageCells[idx(x,y)] = 0; }
|
|
297
|
+
if ( was && !now) { died++; ageCells[idx(x,y)] = 0; }
|
|
298
|
+
if ( was && now) ageCells[idx(x,y)] = Math.min(ageCells[idx(x,y)] + 1, 15);
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
// swap buffers
|
|
302
|
+
[cells, next_] = [next_, cells];
|
|
303
|
+
births += born;
|
|
304
|
+
deaths += died;
|
|
305
|
+
gen += 1;
|
|
306
|
+
population = cells.reduce((s, v) => s + v, 0);
|
|
307
|
+
peakPop = Math.max(peakPop, population);
|
|
308
|
+
score = gen;
|
|
309
|
+
em.emit('score', score);
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
function setCell(x, y, v) {
|
|
313
|
+
if (x < 0 || x >= GW || y < 0 || y >= GH) return;
|
|
314
|
+
cells[idx(x, y)] = v;
|
|
315
|
+
if (v) ageCells[idx(x, y)] = 0;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
function placePattern(key, cx, cy) {
|
|
319
|
+
const pat = PATTERNS[key];
|
|
320
|
+
if (!pat) return;
|
|
321
|
+
for (const [dx, dy] of pat.cells) {
|
|
322
|
+
setCell(cx + dx, cy + dy, 1);
|
|
323
|
+
}
|
|
324
|
+
population = cells.reduce((s,v)=>s+v,0);
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
function randomFill(density = 0.3) {
|
|
328
|
+
for (let i = 0; i < GW * GH; i++) {
|
|
329
|
+
cells[i] = Math.random() < density ? 1 : 0;
|
|
330
|
+
ageCells[i] = 0;
|
|
331
|
+
}
|
|
332
|
+
population = cells.reduce((s,v)=>s+v,0);
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
function clearGrid() {
|
|
336
|
+
cells.fill(0);
|
|
337
|
+
ageCells.fill(0);
|
|
338
|
+
population = 0;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
function start(presetKey) {
|
|
342
|
+
cells = new Uint8Array(GW * GH);
|
|
343
|
+
next_ = new Uint8Array(GW * GH);
|
|
344
|
+
ageCells = new Uint8Array(GW * GH);
|
|
345
|
+
gen = 0; births = 0; deaths = 0; population = 0; peakPop = 0;
|
|
346
|
+
toroidal = opts.toroidal ?? true;
|
|
347
|
+
drawMode = false;
|
|
348
|
+
speed = opts.speed ?? 5; // 1..10
|
|
349
|
+
tickAcc = 0;
|
|
350
|
+
state = 'running';
|
|
351
|
+
curX = GW >> 1;
|
|
352
|
+
curY = GH >> 1;
|
|
353
|
+
patternMenu = false;
|
|
354
|
+
patternIdx = 0;
|
|
355
|
+
score = 0;
|
|
356
|
+
|
|
357
|
+
const key = presetKey ?? opts.preset ?? 'Gosper Glider Gun';
|
|
358
|
+
if (PATTERNS[key]) {
|
|
359
|
+
const cx = (GW >> 1) - 20;
|
|
360
|
+
const cy = (GH >> 1) - 6;
|
|
361
|
+
placePattern(key, cx, cy);
|
|
362
|
+
} else {
|
|
363
|
+
randomFill();
|
|
364
|
+
}
|
|
365
|
+
population = cells.reduce((s,v)=>s+v,0);
|
|
366
|
+
em.emit('start');
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
// ── speed → ms per generation ──────────────────────────────────────────────
|
|
370
|
+
function msPerGen() {
|
|
371
|
+
// speed 1 = 1000ms, speed 10 = 30ms
|
|
372
|
+
return Math.round(1000 / (speed * speed * 0.1 + 0.1));
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
// ── input ──────────────────────────────────────────────────────────────────
|
|
376
|
+
function input(key) {
|
|
377
|
+
// Pattern menu navigation
|
|
378
|
+
if (patternMenu) {
|
|
379
|
+
if (key === 'escape' || key === 'p') { patternMenu = false; return; }
|
|
380
|
+
if (key === 'up') { patternIdx = (patternIdx - 1 + PATTERN_KEYS.length) % PATTERN_KEYS.length; return; }
|
|
381
|
+
if (key === 'down') { patternIdx = (patternIdx + 1) % PATTERN_KEYS.length; return; }
|
|
382
|
+
if (key === 'enter' || key === 'space') {
|
|
383
|
+
clearGrid();
|
|
384
|
+
placePattern(PATTERN_KEYS[patternIdx], (GW>>1)-10, (GH>>1)-7);
|
|
385
|
+
gen=0; births=0; deaths=0; score=0;
|
|
386
|
+
patternMenu = false;
|
|
387
|
+
state = 'paused';
|
|
388
|
+
}
|
|
389
|
+
return;
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
if (key === 'p') { patternMenu = true; return; }
|
|
393
|
+
if (key === 'space') { state = state === 'running' ? 'paused' : 'running'; return; }
|
|
394
|
+
if (key === 'enter' && state==='paused') { step(); return; }
|
|
395
|
+
if (key === 'd') { drawMode = !drawMode; return; }
|
|
396
|
+
if (key === 't') { toroidal = !toroidal; return; }
|
|
397
|
+
if (key === 'c') { clearGrid(); gen=0; births=0; deaths=0; return; }
|
|
398
|
+
if (key === 'r') { randomFill(); gen=0; births=0; deaths=0; return; }
|
|
399
|
+
if (key === '+' || key === '=') { speed = clamp(speed+1, 1, 10); return; }
|
|
400
|
+
if (key === '-') { speed = clamp(speed-1, 1, 10); return; }
|
|
401
|
+
|
|
402
|
+
// cursor movement
|
|
403
|
+
const step_ = drawMode ? 1 : 3;
|
|
404
|
+
if (key === 'up') curY = clamp(curY - step_, 0, GH-1);
|
|
405
|
+
if (key === 'down') curY = clamp(curY + step_, 0, GH-1);
|
|
406
|
+
if (key === 'left') curX = clamp(curX - step_, 0, GW-1);
|
|
407
|
+
if (key === 'right') curX = clamp(curX + step_, 0, GW-1);
|
|
408
|
+
|
|
409
|
+
// draw mode: x toggles cell, f fills 3x3, e erases 3x3
|
|
410
|
+
if (drawMode) {
|
|
411
|
+
if (key === 'x' || key === 'z') {
|
|
412
|
+
setCell(curX, curY, cells[idx(curX,curY)] ? 0 : 1);
|
|
413
|
+
population = cells.reduce((s,v)=>s+v,0);
|
|
414
|
+
}
|
|
415
|
+
if (key === 'f') {
|
|
416
|
+
for (let dy=-1;dy<=1;dy++) for (let dx=-1;dx<=1;dx++) setCell(curX+dx, curY+dy, 1);
|
|
417
|
+
population = cells.reduce((s,v)=>s+v,0);
|
|
418
|
+
}
|
|
419
|
+
if (key === 'e') {
|
|
420
|
+
for (let dy=-2;dy<=2;dy++) for (let dx=-2;dx<=2;dx++) setCell(curX+dx, curY+dy, 0);
|
|
421
|
+
population = cells.reduce((s,v)=>s+v,0);
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
// ── tick ───────────────────────────────────────────────────────────────────
|
|
427
|
+
function tick(dt = 16) {
|
|
428
|
+
if (state !== 'running') return;
|
|
429
|
+
tickAcc += dt;
|
|
430
|
+
const threshold = msPerGen();
|
|
431
|
+
while (tickAcc >= threshold) {
|
|
432
|
+
tickAcc -= threshold;
|
|
433
|
+
step();
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
// ── frame ──────────────────────────────────────────────────────────────────
|
|
438
|
+
|
|
439
|
+
// Map age → color (young=bright, old=dim)
|
|
440
|
+
const AGE_COLORS = [
|
|
441
|
+
FC.WHT, FC.CYN, FC.GRN, FC.YEL, FC.yel,
|
|
442
|
+
FC.grn, FC.cyn, FC.BLK, FC.BLK, FC.BLK,
|
|
443
|
+
];
|
|
444
|
+
|
|
445
|
+
function frame() {
|
|
446
|
+
// We render the grid using half-block trick: each terminal row = 2 grid rows
|
|
447
|
+
// '▀' = top cell alive, '▄' = bottom cell alive, '█' = both, ' ' = neither
|
|
448
|
+
const PANEL_W = 22;
|
|
449
|
+
const DISP_W = GW; // grid cols = terminal cols (1 char per cell for speed)
|
|
450
|
+
const DISP_H = Math.ceil(GH / 2); // half-block rows
|
|
451
|
+
const TOTAL_W = DISP_W + PANEL_W + 1;
|
|
452
|
+
const TOTAL_H = DISP_H + 3;
|
|
453
|
+
|
|
454
|
+
const cv = makeCanvas(TOTAL_W, TOTAL_H);
|
|
455
|
+
cv.clear(' ', FC.wht, BC.blk);
|
|
456
|
+
|
|
457
|
+
// ── header ──
|
|
458
|
+
const stateStr = patternMenu ? 'PATTERN' : state.toUpperCase();
|
|
459
|
+
const modeStr = drawMode ? ' DRAW' : '';
|
|
460
|
+
cv.put(0, 0,
|
|
461
|
+
` LIFE Gen:${String(gen).padStart(6,' ')} Pop:${String(population).padStart(5,' ')}` +
|
|
462
|
+
` Spd:${speed} ${stateStr}${modeStr} ${toroidal?'⟳ Wrap':'■ Bound'}`,
|
|
463
|
+
FC.YEL, BC.blk);
|
|
464
|
+
cv.put(0, 1, '─'.repeat(Math.min(TOTAL_W, 80)), FC.BLK, BC.blk);
|
|
465
|
+
|
|
466
|
+
if (patternMenu) {
|
|
467
|
+
// ── pattern browser overlay ──
|
|
468
|
+
const W2=50, H2=Math.min(PATTERN_KEYS.length+4, 30);
|
|
469
|
+
const ox=(TOTAL_W-W2)>>1, oy=2;
|
|
470
|
+
cv.fill(ox, oy, W2, H2, ' ', FC.wht, BC.blk);
|
|
471
|
+
cv.box(ox, oy, W2, H2, FC.CYN, BC.blk);
|
|
472
|
+
cv.put(ox+2, oy, ' PATTERN LIBRARY ', FC.YEL, BC.blk);
|
|
473
|
+
const visible = 20;
|
|
474
|
+
const start_ = Math.max(0, patternIdx - (visible>>1));
|
|
475
|
+
for (let i=0; i<visible; i++) {
|
|
476
|
+
const ki = start_ + i;
|
|
477
|
+
if (ki >= PATTERN_KEYS.length) break;
|
|
478
|
+
const k = PATTERN_KEYS[ki];
|
|
479
|
+
const sel = ki === patternIdx;
|
|
480
|
+
const pat = PATTERNS[k];
|
|
481
|
+
cv.put(ox+2, oy+1+i,
|
|
482
|
+
`${sel?'▶':' '} ${k.padEnd(22,' ')} ${pat.desc.slice(0,18)}`,
|
|
483
|
+
sel ? FC.YEL : FC.wht, BC.blk);
|
|
484
|
+
}
|
|
485
|
+
cv.put(ox+2, oy+H2-2, '↑↓ browse ENTER place ESC cancel', FC.BLK, BC.blk);
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
// ── grid ──
|
|
489
|
+
for (let row = 0; row < DISP_H; row++) {
|
|
490
|
+
const y0 = row * 2;
|
|
491
|
+
const y1 = y0 + 1;
|
|
492
|
+
for (let x = 0; x < GW; x++) {
|
|
493
|
+
const a = cells[idx(x, y0)];
|
|
494
|
+
const b = (y1 < GH) ? cells[idx(x, y1)] : 0;
|
|
495
|
+
const age = ageCells[idx(x, y0)];
|
|
496
|
+
const col = AGE_COLORS[Math.min(age, AGE_COLORS.length-1)];
|
|
497
|
+
const isCur = drawMode && x === curX && (y0 === curY || y1 === curY);
|
|
498
|
+
if (isCur) {
|
|
499
|
+
cv.set(x, row+2, '◎', FC.RED, BC.blk);
|
|
500
|
+
} else if (a && b) {
|
|
501
|
+
cv.set(x, row+2, '█', col, BC.blk);
|
|
502
|
+
} else if (a) {
|
|
503
|
+
cv.set(x, row+2, '▀', col, BC.blk);
|
|
504
|
+
} else if (b) {
|
|
505
|
+
cv.set(x, row+2, '▄', col, BC.blk);
|
|
506
|
+
} else {
|
|
507
|
+
cv.set(x, row+2, ' ', FC.BLK, BC.blk);
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
// ── side panel ──
|
|
513
|
+
const px = DISP_W + 1;
|
|
514
|
+
let py = 2;
|
|
515
|
+
const P = (s, f=FC.wht) => { cv.put(px, py++, s.slice(0,PANEL_W), f, BC.blk); };
|
|
516
|
+
|
|
517
|
+
P(`┌─ STATS ──────────┐`, FC.BLK);
|
|
518
|
+
P(` Gen: ${String(gen).padStart(9,' ')}`, FC.CYN);
|
|
519
|
+
P(` Pop: ${String(population).padStart(9,' ')}`, FC.GRN);
|
|
520
|
+
P(` Peak: ${String(peakPop).padStart(9,' ')}`, FC.YEL);
|
|
521
|
+
P(` Born: ${String(births).padStart(9,' ')}`, FC.GRN);
|
|
522
|
+
P(` Died: ${String(deaths).padStart(9,' ')}`, FC.RED);
|
|
523
|
+
P(` Speed: ${'▰'.repeat(speed)}${'▱'.repeat(10-speed)}`, FC.MAG);
|
|
524
|
+
P(` ${toroidal?'Toroidal wrap ':'Bounded grid '}`, FC.BLK);
|
|
525
|
+
P(`└──────────────────┘`, FC.BLK);
|
|
526
|
+
py++;
|
|
527
|
+
P(`┌─ CONTROLS ───────┐`, FC.BLK);
|
|
528
|
+
P(` SPACE pause/run `, FC.wht);
|
|
529
|
+
P(` ENTER step `, FC.wht);
|
|
530
|
+
P(` +/- speed `, FC.wht);
|
|
531
|
+
P(` D draw mode `, drawMode?FC.YEL:FC.wht);
|
|
532
|
+
P(` X toggle cell`, FC.wht);
|
|
533
|
+
P(` F/E fill/erase`, FC.wht);
|
|
534
|
+
P(` C clear `, FC.wht);
|
|
535
|
+
P(` R random `, FC.wht);
|
|
536
|
+
P(` P patterns `, FC.wht);
|
|
537
|
+
P(` T wrap `, FC.wht);
|
|
538
|
+
P(`└──────────────────┘`, FC.BLK);
|
|
539
|
+
|
|
540
|
+
// legend
|
|
541
|
+
cv.put(0, TOTAL_H-1,
|
|
542
|
+
' New:' + `\x1b[${FC.WHT}m██\x1b[0m` +
|
|
543
|
+
' Mid:' + `\x1b[${FC.GRN}m██\x1b[0m` +
|
|
544
|
+
' Old:' + `\x1b[${FC.BLK}m██\x1b[0m` +
|
|
545
|
+
` Cursor:${curX},${curY}`,
|
|
546
|
+
FC.BLK, BC.blk);
|
|
547
|
+
|
|
548
|
+
return cv.render();
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
return {
|
|
552
|
+
start, input, tick, frame,
|
|
553
|
+
on: em.on.bind(em),
|
|
554
|
+
off: em.off.bind(em),
|
|
555
|
+
get state() { return state; },
|
|
556
|
+
get score() { return score; },
|
|
557
|
+
get gen() { return gen; },
|
|
558
|
+
get pop() { return population; },
|
|
559
|
+
patterns: PATTERN_KEYS,
|
|
560
|
+
};
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
// ─── EXPORT ──────────────────────────────────────────────────────────────────
|
|
564
|
+
|
|
565
|
+
const kitdef = {
|
|
566
|
+
Life: (opts) => {
|
|
567
|
+
const g = makeLife(opts);
|
|
568
|
+
g.start(opts?.preset);
|
|
569
|
+
return g;
|
|
570
|
+
},
|
|
571
|
+
patterns: PATTERN_KEYS,
|
|
572
|
+
makeLife, PATTERNS
|
|
573
|
+
};
|
|
574
|
+
|
|
575
|
+
module.exports = { kitdef };
|