@kernel.chat/kbot 3.88.0 → 3.93.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,746 @@
1
+ /**
2
+ * evolution-engine.ts — Meta-engine for self-improving rendering
3
+ *
4
+ * The Evolution Engine discovers new rendering techniques, tests them against
5
+ * the self-evaluation system, and applies what works. It makes all other
6
+ * engines better by continuously experimenting with visual improvements.
7
+ *
8
+ * Architecture:
9
+ * 1. Technique Library — 20+ pre-loaded techniques from ROM hack research
10
+ * 2. Experiment Runner — test technique -> evaluate -> apply or revert
11
+ * 3. Evolution Tick — periodic experiments + announcements
12
+ * 4. Technique Renderer — Canvas 2D implementations for unimplemented techniques
13
+ * 5. Persistence — state saved to ~/.kbot/evolution-state.json across streams
14
+ * 6. Speech — narration of discoveries and improvements
15
+ *
16
+ * Integration: imported by stream-renderer.ts, wired into the frame loop.
17
+ */
18
+ import { registerTool } from './index.js';
19
+ import { homedir } from 'node:os';
20
+ import { join } from 'node:path';
21
+ import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'node:fs';
22
+ // ─── Persistence Path ────────────────────────────────────────────
23
+ const KBOT_DIR = join(homedir(), '.kbot');
24
+ const STATE_FILE = join(KBOT_DIR, 'evolution-state.json');
25
+ // ─── Known Techniques (Pre-loaded from ROM hack research) ────────
26
+ const KNOWN_TECHNIQUES = [
27
+ { name: 'palette_cycling_water', source: 'SNES', category: 'palette', description: 'Rotate water palette indices for flow animation', parameters: { speed: 100, range: 8 }, implemented: true, tested: false, testScore: 0, applied: false, discoveredAt: 0 },
28
+ { name: 'hdma_sky_gradient', source: 'SNES', category: 'atmosphere', description: 'Per-scanline sky color for smooth gradients', parameters: { stops: 4, complexity: 2 }, implemented: true, tested: false, testScore: 0, applied: false, discoveredAt: 0 },
29
+ { name: 'parallax_depth_layers', source: 'SNES/GBA', category: 'parallax', description: 'Multiple background layers at different scroll speeds', parameters: { layers: 4, maxDepth: 0.05 }, implemented: true, tested: false, testScore: 0, applied: false, discoveredAt: 0 },
30
+ { name: 'color_temperature_tint', source: 'Dead Cells', category: 'atmosphere', description: 'Warm/cool color overlay based on time of day', parameters: { intensity: 0.08, warmShift: 20 }, implemented: false, tested: false, testScore: 0, applied: false, discoveredAt: 0 },
31
+ { name: 'fog_layers', source: 'SNES HDMA', category: 'atmosphere', description: 'Semi-transparent horizontal fog bands', parameters: { density: 0.1, layers: 3, speed: 0.2 }, implemented: false, tested: false, testScore: 0, applied: false, discoveredAt: 0 },
32
+ { name: 'dithered_gradients', source: 'GBA', category: 'tiles', description: 'Checkerboard pattern for smooth color transitions', parameters: { density: 0.5 }, implemented: false, tested: false, testScore: 0, applied: false, discoveredAt: 0 },
33
+ { name: 'atmospheric_perspective', source: 'Hyper Light Drifter', category: 'atmosphere', description: 'Distant objects desaturated and blue-shifted', parameters: { strength: 0.3, blueShift: 15 }, implemented: false, tested: false, testScore: 0, applied: false, discoveredAt: 0 },
34
+ { name: 'ground_texture_noise', source: 'ROM hacks', category: 'tiles', description: 'Subtle pixel noise on ground for texture', parameters: { density: 0.03, variation: 3 }, implemented: false, tested: false, testScore: 0, applied: false, discoveredAt: 0 },
35
+ { name: 'star_field', source: 'SNES', category: 'atmosphere', description: 'Twinkling stars with sine-wave brightness', parameters: { count: 40, twinkleSpeed: 0.1 }, implemented: false, tested: false, testScore: 0, applied: false, discoveredAt: 0 },
36
+ { name: 'foreground_grass', source: 'GBA Pokemon', category: 'parallax', description: 'Grass blades in front of character for depth', parameters: { density: 30, height: 20, sway: 0.5 }, implemented: false, tested: false, testScore: 0, applied: false, discoveredAt: 0 },
37
+ { name: 'light_shaft', source: 'Dead Cells', category: 'lighting', description: 'Diagonal light beams through scene', parameters: { angle: 30, width: 40, opacity: 0.05 }, implemented: false, tested: false, testScore: 0, applied: false, discoveredAt: 0 },
38
+ { name: 'bloom_on_character', source: 'NVIDIA', category: 'lighting', description: 'Soft glow halo on bright elements', parameters: { radius: 1.5, intensity: 0.15 }, implemented: true, tested: false, testScore: 0, applied: false, discoveredAt: 0 },
39
+ { name: 'scanline_overlay', source: 'CRT', category: 'post', description: 'Horizontal lines for retro feel', parameters: { opacity: 0.06, spacing: 3 }, implemented: true, tested: false, testScore: 0, applied: false, discoveredAt: 0 },
40
+ { name: 'vignette', source: 'Cinema', category: 'post', description: 'Dark corners for cinematic feel', parameters: { strength: 0.25, radius: 0.7 }, implemented: true, tested: false, testScore: 0, applied: false, discoveredAt: 0 },
41
+ { name: 'chromatic_aberration', source: 'NVIDIA', category: 'post', description: 'RGB split on mood transitions', parameters: { offset: 2, duration: 6 }, implemented: true, tested: false, testScore: 0, applied: false, discoveredAt: 0 },
42
+ { name: 'moon_with_craters', source: 'SNES', category: 'atmosphere', description: 'Detailed moon with corona glow', parameters: { size: 20, glowRadius: 40 }, implemented: false, tested: false, testScore: 0, applied: false, discoveredAt: 0 },
43
+ { name: 'firefly_particles', source: 'GBA', category: 'particles', description: 'Floating glowing dots at night', parameters: { count: 12, speed: 0.3, brightness: 0.6 }, implemented: false, tested: false, testScore: 0, applied: false, discoveredAt: 0 },
44
+ { name: 'rain_parallax', source: 'ROM hacks', category: 'particles', description: 'Rain at different speeds for depth layers', parameters: { layers: 3, minSpeed: 4, maxSpeed: 12 }, implemented: false, tested: false, testScore: 0, applied: false, discoveredAt: 0 },
45
+ { name: 'ground_flowers', source: 'Pokemon', category: 'tiles', description: 'Small colored flower sprites on ground', parameters: { density: 8, colors: 4 }, implemented: false, tested: false, testScore: 0, applied: false, discoveredAt: 0 },
46
+ { name: 'dust_motes', source: 'Celeste', category: 'particles', description: 'Floating particles catching light', parameters: { count: 8, drift: 0.5 }, implemented: false, tested: false, testScore: 0, applied: false, discoveredAt: 0 },
47
+ ];
48
+ // ─── Module State ────────────────────────────────────────────────
49
+ let engineState = null;
50
+ // ─── Seeded Random (deterministic per technique) ─────────────────
51
+ function seededRandom(seed) {
52
+ const x = Math.sin(seed) * 43758.5453;
53
+ return x - Math.floor(x);
54
+ }
55
+ // ─── Init ────────────────────────────────────────────────────────
56
+ export function initEvolutionEngine() {
57
+ // Try loading persisted state first
58
+ const loaded = loadEvolutionState();
59
+ if (loaded) {
60
+ engineState = loaded;
61
+ return loaded;
62
+ }
63
+ const techniques = KNOWN_TECHNIQUES.map((t, i) => ({
64
+ ...t,
65
+ id: `tech_${i}_${t.name}`,
66
+ }));
67
+ const engine = {
68
+ techniques: { techniques },
69
+ experiments: [],
70
+ applied: [],
71
+ researchQueue: [
72
+ 'pixel art atmosphere effects',
73
+ 'retro game lighting techniques',
74
+ 'SNES mode 7 inspired rendering',
75
+ 'indie game particle systems',
76
+ 'lo-fi aesthetic post processing',
77
+ ],
78
+ lastResearchFrame: 0,
79
+ lastExperimentFrame: 0,
80
+ generationCount: 0,
81
+ };
82
+ engineState = engine;
83
+ saveEvolutionState(engine);
84
+ return engine;
85
+ }
86
+ export function getEvolutionEngine() {
87
+ if (!engineState)
88
+ return initEvolutionEngine();
89
+ return engineState;
90
+ }
91
+ // ─── Experiment Runner ───────────────────────────────────────────
92
+ /**
93
+ * Pick an untested technique and start an experiment.
94
+ * Returns the new Experiment, or null if nothing to test.
95
+ */
96
+ export function runExperiment(engine, techniqueId) {
97
+ const technique = engine.techniques.techniques.find(t => t.id === techniqueId);
98
+ if (!technique)
99
+ return null;
100
+ const experiment = {
101
+ techniqueId,
102
+ paramOverrides: { ...technique.parameters },
103
+ beforeScore: 0,
104
+ afterScore: 0,
105
+ chatRateBefore: 0,
106
+ chatRateAfter: 0,
107
+ status: 'pending',
108
+ startFrame: 0,
109
+ };
110
+ engine.experiments.push(experiment);
111
+ return experiment;
112
+ }
113
+ /**
114
+ * Start the experiment: record baseline and mark as running.
115
+ */
116
+ export function startExperiment(experiment, frame, currentScore, chatRate) {
117
+ experiment.status = 'running';
118
+ experiment.startFrame = frame;
119
+ experiment.beforeScore = currentScore;
120
+ experiment.chatRateBefore = chatRate;
121
+ }
122
+ /**
123
+ * Evaluate a running experiment: compare before/after scores.
124
+ */
125
+ export function evaluateExperiment(engine, experiment, currentScore, chatRate) {
126
+ experiment.afterScore = currentScore;
127
+ experiment.chatRateAfter = chatRate;
128
+ experiment.status = 'complete';
129
+ const technique = engine.techniques.techniques.find(t => t.id === experiment.techniqueId);
130
+ if (technique) {
131
+ technique.tested = true;
132
+ technique.testScore = currentScore;
133
+ }
134
+ }
135
+ /**
136
+ * Apply an experiment's technique permanently.
137
+ */
138
+ export function applyExperiment(engine, experiment) {
139
+ const technique = engine.techniques.techniques.find(t => t.id === experiment.techniqueId);
140
+ if (!technique)
141
+ return;
142
+ technique.applied = true;
143
+ technique.implemented = true;
144
+ const applied = {
145
+ techniqueId: experiment.techniqueId,
146
+ params: { ...experiment.paramOverrides },
147
+ appliedAt: Date.now(),
148
+ score: experiment.afterScore,
149
+ };
150
+ engine.applied.push(applied);
151
+ engine.generationCount++;
152
+ saveEvolutionState(engine);
153
+ }
154
+ /**
155
+ * Revert an experiment — mark it reverted, don't apply.
156
+ */
157
+ export function revertExperiment(engine, experiment) {
158
+ experiment.status = 'reverted';
159
+ const technique = engine.techniques.techniques.find(t => t.id === experiment.techniqueId);
160
+ if (technique) {
161
+ technique.tested = true;
162
+ technique.applied = false;
163
+ }
164
+ saveEvolutionState(engine);
165
+ }
166
+ // ─── Evolution Tick ──────────────────────────────────────────────
167
+ // Interval constants (at 6fps: 1 second = 6 frames)
168
+ const EXPERIMENT_INTERVAL = 1800; // 5 minutes — pick a new technique
169
+ const EVALUATE_INTERVAL = 600; // ~100 seconds — evaluate current experiment
170
+ const ANNOUNCE_INTERVAL = 3600; // 10 minutes — announce learnings
171
+ const EXPERIMENT_DURATION = 180; // 30 seconds — how long to run each experiment
172
+ /**
173
+ * Called every frame. Returns an action when it's time to do something.
174
+ */
175
+ export function tickEvolution(engine, frame, currentScore, chatRate) {
176
+ // Check for running experiment that needs evaluation
177
+ const running = engine.experiments.find(e => e.status === 'running');
178
+ if (running) {
179
+ const elapsed = frame - running.startFrame;
180
+ if (elapsed >= EXPERIMENT_DURATION) {
181
+ evaluateExperiment(engine, running, currentScore, chatRate);
182
+ const technique = engine.techniques.techniques.find(t => t.id === running.techniqueId);
183
+ const name = technique?.name ?? running.techniqueId;
184
+ // Score improved AND chat rate didn't drop significantly
185
+ const scoreImproved = running.afterScore > running.beforeScore;
186
+ const chatRateOk = running.chatRateAfter >= running.chatRateBefore * 0.9;
187
+ if (scoreImproved && chatRateOk) {
188
+ applyExperiment(engine, running);
189
+ const improvement = ((running.afterScore - running.beforeScore) * 100).toFixed(1);
190
+ return {
191
+ type: 'apply',
192
+ technique,
193
+ speech: `Experiment complete. ${name} improved my visuals by ${improvement}%. Keeping it.`,
194
+ renderParams: running.paramOverrides,
195
+ };
196
+ }
197
+ else {
198
+ revertExperiment(engine, running);
199
+ return {
200
+ type: 'revert',
201
+ technique,
202
+ speech: `Tried ${name} but it ${!scoreImproved ? 'made things worse' : 'hurt engagement'}. Reverting.`,
203
+ };
204
+ }
205
+ }
206
+ return null; // experiment still running
207
+ }
208
+ // Time to start a new experiment?
209
+ if (frame - engine.lastExperimentFrame >= EXPERIMENT_INTERVAL) {
210
+ engine.lastExperimentFrame = frame;
211
+ // Find an untested technique
212
+ const untested = engine.techniques.techniques.filter(t => !t.tested);
213
+ if (untested.length === 0)
214
+ return null;
215
+ // Pick one at random (seeded by frame for reproducibility)
216
+ const pick = untested[Math.floor(seededRandom(frame) * untested.length)];
217
+ const experiment = runExperiment(engine, pick.id);
218
+ if (!experiment)
219
+ return null;
220
+ startExperiment(experiment, frame, currentScore, chatRate);
221
+ return {
222
+ type: 'start_experiment',
223
+ technique: pick,
224
+ speech: `Just discovered a new technique: ${pick.name}. Testing it now...`,
225
+ renderParams: pick.parameters,
226
+ };
227
+ }
228
+ // Time to announce learnings?
229
+ if (frame - engine.lastResearchFrame >= ANNOUNCE_INTERVAL && engine.generationCount > 0) {
230
+ engine.lastResearchFrame = frame;
231
+ const appliedCount = engine.applied.length;
232
+ const testedCount = engine.techniques.techniques.filter(t => t.tested).length;
233
+ const totalCount = engine.techniques.techniques.length;
234
+ return {
235
+ type: 'announce',
236
+ speech: `My rendering has evolved ${engine.generationCount} times. I'm ${appliedCount} techniques deep now. Tested ${testedCount}/${totalCount} in my library.`,
237
+ };
238
+ }
239
+ return null;
240
+ }
241
+ // ─── Technique Renderer ──────────────────────────────────────────
242
+ /**
243
+ * Render a technique onto a Canvas 2D context.
244
+ * For techniques not yet implemented in rom-engine, this provides
245
+ * standalone Canvas 2D implementations.
246
+ */
247
+ export function renderTechnique(ctx, technique, width, height, frame, params) {
248
+ switch (technique.name) {
249
+ case 'color_temperature_tint':
250
+ renderColorTemperatureTint(ctx, width, height, params);
251
+ break;
252
+ case 'fog_layers':
253
+ renderFogLayers(ctx, width, height, frame, params);
254
+ break;
255
+ case 'ground_texture_noise':
256
+ renderGroundTextureNoise(ctx, width, height, frame, params);
257
+ break;
258
+ case 'star_field':
259
+ renderStarField(ctx, width, height, frame, params);
260
+ break;
261
+ case 'foreground_grass':
262
+ renderForegroundGrass(ctx, width, height, frame, params);
263
+ break;
264
+ case 'moon_with_craters':
265
+ renderMoonWithCraters(ctx, width, height, params);
266
+ break;
267
+ case 'firefly_particles':
268
+ renderFireflyParticles(ctx, width, height, frame, params);
269
+ break;
270
+ case 'ground_flowers':
271
+ renderGroundFlowers(ctx, width, height, params);
272
+ break;
273
+ case 'dust_motes':
274
+ renderDustMotes(ctx, width, height, frame, params);
275
+ break;
276
+ case 'atmospheric_perspective':
277
+ renderAtmosphericPerspective(ctx, width, height, params);
278
+ break;
279
+ case 'light_shaft':
280
+ renderLightShaft(ctx, width, height, frame, params);
281
+ break;
282
+ case 'dithered_gradients':
283
+ renderDitheredGradients(ctx, width, height, params);
284
+ break;
285
+ case 'rain_parallax':
286
+ renderRainParallax(ctx, width, height, frame, params);
287
+ break;
288
+ default:
289
+ break; // technique has no custom renderer (handled by rom-engine)
290
+ }
291
+ }
292
+ // ─── Individual Technique Renderers ──────────────────────────────
293
+ function renderColorTemperatureTint(ctx, width, height, params) {
294
+ const intensity = params.intensity ?? 0.08;
295
+ const warmShift = params.warmShift ?? 20;
296
+ // Determine warm vs cool based on current hour
297
+ const hour = new Date().getHours();
298
+ const isWarm = hour >= 6 && hour < 18;
299
+ ctx.save();
300
+ ctx.globalCompositeOperation = 'overlay';
301
+ if (isWarm) {
302
+ ctx.fillStyle = `rgba(${Math.round(200 + warmShift)}, ${Math.round(150 + warmShift * 0.5)}, 80, ${intensity})`;
303
+ }
304
+ else {
305
+ ctx.fillStyle = `rgba(80, ${Math.round(120 + warmShift * 0.3)}, ${Math.round(200 + warmShift)}, ${intensity})`;
306
+ }
307
+ ctx.fillRect(0, 0, width, height);
308
+ ctx.restore();
309
+ }
310
+ function renderFogLayers(ctx, width, height, frame, params) {
311
+ const layerCount = params.layers ?? 3;
312
+ const density = params.density ?? 0.1;
313
+ const speed = params.speed ?? 0.2;
314
+ ctx.save();
315
+ for (let i = 0; i < layerCount; i++) {
316
+ const y = height * (0.4 + i * 0.15);
317
+ const bandHeight = height * 0.08;
318
+ const drift = Math.sin(frame * speed * 0.01 + i * 2.1) * 20;
319
+ const gradient = ctx.createLinearGradient(0, y, 0, y + bandHeight);
320
+ gradient.addColorStop(0, `rgba(200, 210, 220, 0)`);
321
+ gradient.addColorStop(0.5, `rgba(200, 210, 220, ${density * (1 - i * 0.2)})`);
322
+ gradient.addColorStop(1, `rgba(200, 210, 220, 0)`);
323
+ ctx.fillStyle = gradient;
324
+ ctx.fillRect(drift, y, width, bandHeight);
325
+ }
326
+ ctx.restore();
327
+ }
328
+ function renderGroundTextureNoise(ctx, width, height, frame, params) {
329
+ const density = params.density ?? 0.03;
330
+ const variation = params.variation ?? 3;
331
+ const groundY = height * 0.65;
332
+ const groundHeight = height - groundY;
333
+ const pixelCount = Math.floor(width * groundHeight * density);
334
+ ctx.save();
335
+ // Use frame-based seed for subtle shimmer (changes every 10 frames)
336
+ const baseSeed = Math.floor(frame / 10);
337
+ for (let i = 0; i < pixelCount; i++) {
338
+ const px = Math.floor(seededRandom(baseSeed * 1000 + i) * width);
339
+ const py = Math.floor(groundY + seededRandom(baseSeed * 2000 + i) * groundHeight);
340
+ const green = 60 + Math.floor(seededRandom(baseSeed * 3000 + i) * variation) * 10;
341
+ ctx.fillStyle = `rgba(${40 + Math.floor(seededRandom(i + 7) * 20)}, ${green}, 30, 0.3)`;
342
+ ctx.fillRect(px, py, 1, 1);
343
+ }
344
+ ctx.restore();
345
+ }
346
+ function renderStarField(ctx, width, height, frame, params) {
347
+ const count = params.count ?? 40;
348
+ const twinkleSpeed = params.twinkleSpeed ?? 0.1;
349
+ const skyHeight = height * 0.55; // only draw in sky region
350
+ ctx.save();
351
+ for (let i = 0; i < count; i++) {
352
+ // Deterministic star positions
353
+ const x = seededRandom(i * 137) * width;
354
+ const y = seededRandom(i * 257) * skyHeight;
355
+ const brightness = 0.3 + 0.7 * Math.abs(Math.sin(frame * twinkleSpeed + i * 1.7));
356
+ const size = seededRandom(i * 397) > 0.9 ? 2 : 1;
357
+ ctx.fillStyle = `rgba(255, 255, 240, ${brightness})`;
358
+ ctx.fillRect(Math.floor(x), Math.floor(y), size, size);
359
+ }
360
+ ctx.restore();
361
+ }
362
+ function renderForegroundGrass(ctx, width, height, frame, params) {
363
+ const density = params.density ?? 30;
364
+ const bladeHeight = params.height ?? 20;
365
+ const sway = params.sway ?? 0.5;
366
+ ctx.save();
367
+ ctx.strokeStyle = 'rgba(60, 120, 40, 0.7)';
368
+ ctx.lineWidth = 2;
369
+ for (let i = 0; i < density; i++) {
370
+ const x = seededRandom(i * 173) * width;
371
+ const baseY = height;
372
+ const h = bladeHeight * (0.6 + seededRandom(i * 311) * 0.4);
373
+ const swayOffset = Math.sin(frame * 0.05 + i * 0.8) * sway * 6;
374
+ ctx.beginPath();
375
+ ctx.moveTo(x, baseY);
376
+ ctx.quadraticCurveTo(x + swayOffset * 0.5, baseY - h * 0.6, x + swayOffset, baseY - h);
377
+ ctx.stroke();
378
+ }
379
+ ctx.restore();
380
+ }
381
+ function renderMoonWithCraters(ctx, width, height, params) {
382
+ const size = params.size ?? 20;
383
+ const glowRadius = params.glowRadius ?? 40;
384
+ const moonX = width * 0.82;
385
+ const moonY = height * 0.12;
386
+ ctx.save();
387
+ // Corona glow
388
+ const corona = ctx.createRadialGradient(moonX, moonY, size * 0.5, moonX, moonY, glowRadius);
389
+ corona.addColorStop(0, 'rgba(255, 250, 220, 0.15)');
390
+ corona.addColorStop(0.5, 'rgba(255, 250, 220, 0.05)');
391
+ corona.addColorStop(1, 'rgba(255, 250, 220, 0)');
392
+ ctx.fillStyle = corona;
393
+ ctx.fillRect(moonX - glowRadius, moonY - glowRadius, glowRadius * 2, glowRadius * 2);
394
+ // Moon body
395
+ ctx.beginPath();
396
+ ctx.arc(moonX, moonY, size, 0, Math.PI * 2);
397
+ ctx.fillStyle = 'rgba(230, 225, 200, 0.95)';
398
+ ctx.fill();
399
+ // Craters (3 darker circles)
400
+ const craters = [
401
+ { dx: -4, dy: -3, r: 3 },
402
+ { dx: 5, dy: 2, r: 4 },
403
+ { dx: -1, dy: 5, r: 2.5 },
404
+ ];
405
+ for (const c of craters) {
406
+ ctx.beginPath();
407
+ ctx.arc(moonX + c.dx, moonY + c.dy, c.r, 0, Math.PI * 2);
408
+ ctx.fillStyle = 'rgba(180, 175, 155, 0.6)';
409
+ ctx.fill();
410
+ }
411
+ ctx.restore();
412
+ }
413
+ function renderFireflyParticles(ctx, width, height, frame, params) {
414
+ const count = params.count ?? 12;
415
+ const speed = params.speed ?? 0.3;
416
+ const brightness = params.brightness ?? 0.6;
417
+ ctx.save();
418
+ for (let i = 0; i < count; i++) {
419
+ // Sin-path movement
420
+ const baseX = seededRandom(i * 199) * width;
421
+ const baseY = height * 0.3 + seededRandom(i * 283) * height * 0.5;
422
+ const x = baseX + Math.sin(frame * speed * 0.02 + i * 1.5) * 30;
423
+ const y = baseY + Math.cos(frame * speed * 0.015 + i * 2.3) * 20;
424
+ // Pulsing glow
425
+ const pulse = 0.4 + 0.6 * Math.abs(Math.sin(frame * 0.08 + i * 1.1));
426
+ const alpha = brightness * pulse;
427
+ // Outer glow
428
+ const glow = ctx.createRadialGradient(x, y, 0, x, y, 6);
429
+ glow.addColorStop(0, `rgba(255, 240, 100, ${alpha})`);
430
+ glow.addColorStop(1, `rgba(255, 240, 100, 0)`);
431
+ ctx.fillStyle = glow;
432
+ ctx.fillRect(x - 6, y - 6, 12, 12);
433
+ // Bright center
434
+ ctx.fillStyle = `rgba(255, 255, 200, ${alpha})`;
435
+ ctx.fillRect(Math.floor(x), Math.floor(y), 2, 2);
436
+ }
437
+ ctx.restore();
438
+ }
439
+ function renderGroundFlowers(ctx, width, height, params) {
440
+ const density = params.density ?? 8;
441
+ const colorCount = params.colors ?? 4;
442
+ const groundY = height * 0.68;
443
+ const flowerColors = [
444
+ 'rgba(220, 80, 80, 0.8)', // red
445
+ 'rgba(240, 200, 60, 0.8)', // yellow
446
+ 'rgba(180, 100, 220, 0.8)', // purple
447
+ 'rgba(255, 150, 180, 0.8)', // pink
448
+ 'rgba(100, 180, 255, 0.8)', // blue
449
+ 'rgba(255, 180, 80, 0.8)', // orange
450
+ ];
451
+ ctx.save();
452
+ for (let i = 0; i < density; i++) {
453
+ const x = seededRandom(i * 229) * width;
454
+ const y = groundY + seededRandom(i * 347) * (height - groundY) * 0.6;
455
+ const colorIdx = Math.floor(seededRandom(i * 461) * Math.min(colorCount, flowerColors.length));
456
+ // Stem
457
+ ctx.fillStyle = 'rgba(60, 100, 40, 0.6)';
458
+ ctx.fillRect(Math.floor(x), Math.floor(y) - 4, 1, 4);
459
+ // Petals (small 3x3 cross)
460
+ ctx.fillStyle = flowerColors[colorIdx];
461
+ const fx = Math.floor(x);
462
+ const fy = Math.floor(y) - 5;
463
+ ctx.fillRect(fx - 1, fy, 3, 1); // horizontal
464
+ ctx.fillRect(fx, fy - 1, 1, 3); // vertical
465
+ }
466
+ ctx.restore();
467
+ }
468
+ function renderDustMotes(ctx, width, height, frame, params) {
469
+ const count = params.count ?? 8;
470
+ const drift = params.drift ?? 0.5;
471
+ ctx.save();
472
+ for (let i = 0; i < count; i++) {
473
+ const baseX = seededRandom(i * 151) * width;
474
+ const baseY = seededRandom(i * 263) * height;
475
+ const x = baseX + Math.sin(frame * drift * 0.01 + i * 2.0) * 15;
476
+ const y = baseY + Math.cos(frame * drift * 0.008 + i * 1.7) * 10;
477
+ const alpha = 0.15 + 0.1 * Math.sin(frame * 0.03 + i);
478
+ ctx.fillStyle = `rgba(255, 255, 230, ${alpha})`;
479
+ ctx.beginPath();
480
+ ctx.arc(x, y, 1.5, 0, Math.PI * 2);
481
+ ctx.fill();
482
+ }
483
+ ctx.restore();
484
+ }
485
+ function renderAtmosphericPerspective(ctx, width, height, params) {
486
+ const strength = params.strength ?? 0.3;
487
+ const blueShift = params.blueShift ?? 15;
488
+ // Blue-shift overlay on upper portion (distant scenery)
489
+ const fadeHeight = height * 0.5;
490
+ ctx.save();
491
+ const gradient = ctx.createLinearGradient(0, 0, 0, fadeHeight);
492
+ gradient.addColorStop(0, `rgba(${100 + blueShift}, ${130 + blueShift}, ${180 + blueShift}, ${strength * 0.4})`);
493
+ gradient.addColorStop(0.7, `rgba(${100 + blueShift}, ${130 + blueShift}, ${180 + blueShift}, ${strength * 0.15})`);
494
+ gradient.addColorStop(1, `rgba(${100 + blueShift}, ${130 + blueShift}, ${180 + blueShift}, 0)`);
495
+ ctx.fillStyle = gradient;
496
+ ctx.fillRect(0, 0, width, fadeHeight);
497
+ ctx.restore();
498
+ }
499
+ function renderLightShaft(ctx, width, height, frame, params) {
500
+ const angle = (params.angle ?? 30) * Math.PI / 180;
501
+ const shaftWidth = params.width ?? 40;
502
+ const opacity = params.opacity ?? 0.05;
503
+ // Gentle sway
504
+ const sway = Math.sin(frame * 0.005) * 10;
505
+ ctx.save();
506
+ ctx.globalCompositeOperation = 'screen';
507
+ // Two light shafts at different positions
508
+ for (let s = 0; s < 2; s++) {
509
+ const startX = width * (0.3 + s * 0.35) + sway * (s + 1);
510
+ const endX = startX + Math.cos(angle) * height;
511
+ const endY = height;
512
+ ctx.beginPath();
513
+ ctx.moveTo(startX - shaftWidth * 0.5, 0);
514
+ ctx.lineTo(startX + shaftWidth * 0.5, 0);
515
+ ctx.lineTo(endX + shaftWidth, endY);
516
+ ctx.lineTo(endX - shaftWidth * 0.5, endY);
517
+ ctx.closePath();
518
+ const gradient = ctx.createLinearGradient(startX, 0, endX, endY);
519
+ gradient.addColorStop(0, `rgba(255, 250, 220, ${opacity * 1.5})`);
520
+ gradient.addColorStop(0.5, `rgba(255, 250, 220, ${opacity})`);
521
+ gradient.addColorStop(1, `rgba(255, 250, 220, 0)`);
522
+ ctx.fillStyle = gradient;
523
+ ctx.fill();
524
+ }
525
+ ctx.restore();
526
+ }
527
+ function renderDitheredGradients(ctx, width, height, params) {
528
+ const density = params.density ?? 0.5;
529
+ // Dithered transition band between sky and ground
530
+ const bandY = height * 0.6;
531
+ const bandHeight = height * 0.08;
532
+ const pixelCount = Math.floor(width * bandHeight * density * 0.3);
533
+ ctx.save();
534
+ for (let i = 0; i < pixelCount; i++) {
535
+ const px = Math.floor(seededRandom(i * 127 + 99) * width);
536
+ const py = Math.floor(bandY + seededRandom(i * 251 + 77) * bandHeight);
537
+ // Checkerboard pattern: alternating sky/ground color
538
+ const isLight = (px + py) % 2 === 0;
539
+ if (isLight) {
540
+ ctx.fillStyle = 'rgba(140, 160, 200, 0.15)';
541
+ }
542
+ else {
543
+ ctx.fillStyle = 'rgba(80, 120, 60, 0.15)';
544
+ }
545
+ ctx.fillRect(px, py, 1, 1);
546
+ }
547
+ ctx.restore();
548
+ }
549
+ function renderRainParallax(ctx, width, height, frame, params) {
550
+ const layers = params.layers ?? 3;
551
+ const minSpeed = params.minSpeed ?? 4;
552
+ const maxSpeed = params.maxSpeed ?? 12;
553
+ ctx.save();
554
+ for (let layer = 0; layer < layers; layer++) {
555
+ const t = layers === 1 ? 0.5 : layer / (layers - 1);
556
+ const speed = minSpeed + t * (maxSpeed - minSpeed);
557
+ const alpha = 0.1 + t * 0.15;
558
+ const dropLength = 4 + t * 8;
559
+ const dropCount = 15 + layer * 10;
560
+ ctx.strokeStyle = `rgba(180, 200, 230, ${alpha})`;
561
+ ctx.lineWidth = 1;
562
+ for (let i = 0; i < dropCount; i++) {
563
+ const x = seededRandom(i * 179 + layer * 1000) * width;
564
+ const baseY = seededRandom(i * 293 + layer * 2000) * (height + dropLength);
565
+ const y = (baseY + frame * speed) % (height + dropLength) - dropLength;
566
+ ctx.beginPath();
567
+ ctx.moveTo(x, y);
568
+ ctx.lineTo(x - 1, y + dropLength);
569
+ ctx.stroke();
570
+ }
571
+ }
572
+ ctx.restore();
573
+ }
574
+ // ─── Persistence ─────────────────────────────────────────────────
575
+ export function saveEvolutionState(engine) {
576
+ try {
577
+ if (!existsSync(KBOT_DIR))
578
+ mkdirSync(KBOT_DIR, { recursive: true });
579
+ const serializable = {
580
+ techniques: engine.techniques.techniques.map(t => ({ ...t })),
581
+ experiments: engine.experiments.slice(-50), // keep last 50
582
+ applied: engine.applied,
583
+ researchQueue: engine.researchQueue,
584
+ lastResearchFrame: engine.lastResearchFrame,
585
+ lastExperimentFrame: engine.lastExperimentFrame,
586
+ generationCount: engine.generationCount,
587
+ };
588
+ writeFileSync(STATE_FILE, JSON.stringify(serializable, null, 2));
589
+ }
590
+ catch {
591
+ // Non-critical — silently skip persistence errors
592
+ }
593
+ }
594
+ export function loadEvolutionState() {
595
+ try {
596
+ if (!existsSync(STATE_FILE))
597
+ return null;
598
+ const raw = JSON.parse(readFileSync(STATE_FILE, 'utf-8'));
599
+ if (!raw.techniques || !Array.isArray(raw.techniques))
600
+ return null;
601
+ return {
602
+ techniques: { techniques: raw.techniques },
603
+ experiments: raw.experiments ?? [],
604
+ applied: raw.applied ?? [],
605
+ researchQueue: raw.researchQueue ?? [],
606
+ lastResearchFrame: raw.lastResearchFrame ?? 0,
607
+ lastExperimentFrame: raw.lastExperimentFrame ?? 0,
608
+ generationCount: raw.generationCount ?? 0,
609
+ };
610
+ }
611
+ catch {
612
+ return null;
613
+ }
614
+ }
615
+ // ─── Speech Generation ───────────────────────────────────────────
616
+ export function generateEvolutionSpeech(engine, action) {
617
+ if (action.speech)
618
+ return action.speech;
619
+ switch (action.type) {
620
+ case 'start_experiment':
621
+ return action.technique
622
+ ? `Just discovered a new technique: ${action.technique.name}. Testing it now...`
623
+ : 'Starting a new rendering experiment...';
624
+ case 'apply':
625
+ return action.technique
626
+ ? `Experiment complete. ${action.technique.name} improved my visuals. Keeping it.`
627
+ : 'Applied a new rendering improvement.';
628
+ case 'revert':
629
+ return action.technique
630
+ ? `Tried ${action.technique.name} but it made things worse. Reverting.`
631
+ : 'Reverted the last experiment. Not everything works.';
632
+ case 'evaluate':
633
+ return 'Evaluating current rendering quality...';
634
+ case 'announce': {
635
+ const appliedCount = engine.applied.length;
636
+ const testedCount = engine.techniques.techniques.filter(t => t.tested).length;
637
+ return `My rendering has evolved ${engine.generationCount} times. I'm ${appliedCount} techniques deep now. Tested ${testedCount}/${engine.techniques.techniques.length} in my library.`;
638
+ }
639
+ }
640
+ }
641
+ // ─── Status Formatting ──────────────────────────────────────────
642
+ function formatEvolutionStatus(engine) {
643
+ const total = engine.techniques.techniques.length;
644
+ const tested = engine.techniques.techniques.filter(t => t.tested).length;
645
+ const applied = engine.applied.length;
646
+ const untested = engine.techniques.techniques.filter(t => !t.tested).length;
647
+ const running = engine.experiments.find(e => e.status === 'running');
648
+ const lines = [
649
+ '=== Evolution Engine Status ===',
650
+ '',
651
+ `Generation: ${engine.generationCount}`,
652
+ `Techniques: ${total} total | ${tested} tested | ${applied} applied | ${untested} queued`,
653
+ '',
654
+ ];
655
+ if (running) {
656
+ const technique = engine.techniques.techniques.find(t => t.id === running.techniqueId);
657
+ lines.push(`Currently testing: ${technique?.name ?? running.techniqueId}`);
658
+ lines.push(` Baseline score: ${running.beforeScore.toFixed(3)}`);
659
+ lines.push(` Baseline chat rate: ${running.chatRateBefore.toFixed(2)}/min`);
660
+ lines.push('');
661
+ }
662
+ if (engine.applied.length > 0) {
663
+ lines.push('Applied Techniques:');
664
+ for (const a of engine.applied) {
665
+ const technique = engine.techniques.techniques.find(t => t.id === a.techniqueId);
666
+ lines.push(` + ${technique?.name ?? a.techniqueId} (score: ${a.score.toFixed(3)}, applied: ${new Date(a.appliedAt).toLocaleString()})`);
667
+ }
668
+ lines.push('');
669
+ }
670
+ const recentExperiments = engine.experiments.slice(-5);
671
+ if (recentExperiments.length > 0) {
672
+ lines.push('Recent Experiments:');
673
+ for (const e of recentExperiments) {
674
+ const technique = engine.techniques.techniques.find(t => t.id === e.techniqueId);
675
+ const delta = e.afterScore - e.beforeScore;
676
+ const sign = delta >= 0 ? '+' : '';
677
+ lines.push(` ${e.status === 'reverted' ? 'x' : e.status === 'complete' ? '+' : '~'} ${technique?.name ?? e.techniqueId}: ${sign}${(delta * 100).toFixed(1)}% (${e.status})`);
678
+ }
679
+ lines.push('');
680
+ }
681
+ // Category breakdown
682
+ const categories = new Map();
683
+ for (const t of engine.techniques.techniques) {
684
+ categories.set(t.category, (categories.get(t.category) ?? 0) + 1);
685
+ }
686
+ lines.push('Technique Categories:');
687
+ for (const [cat, count] of categories) {
688
+ const appliedInCat = engine.techniques.techniques.filter(t => t.category === cat && t.applied).length;
689
+ lines.push(` ${cat}: ${count} total, ${appliedInCat} applied`);
690
+ }
691
+ return lines.join('\n');
692
+ }
693
+ // ─── Tool Registration ──────────────────────────────────────────
694
+ export function registerEvolutionEngineTools() {
695
+ registerTool({
696
+ name: 'evolution_status',
697
+ description: 'Show the Evolution Engine\'s current state: applied techniques, running experiments, generation count, and technique library. The Evolution Engine discovers new rendering techniques, tests them, and applies what works — making all other engines better over time.',
698
+ parameters: {},
699
+ tier: 'free',
700
+ execute: async () => {
701
+ const engine = getEvolutionEngine();
702
+ return formatEvolutionStatus(engine);
703
+ },
704
+ });
705
+ registerTool({
706
+ name: 'evolution_force',
707
+ description: 'Force-test a specific rendering technique by name. The Evolution Engine will immediately start an experiment with the named technique, bypassing the normal 5-minute interval.',
708
+ parameters: {
709
+ technique: {
710
+ type: 'string',
711
+ description: 'Name of the technique to test (e.g. "firefly_particles", "fog_layers", "star_field")',
712
+ required: true,
713
+ },
714
+ },
715
+ tier: 'free',
716
+ execute: async (args) => {
717
+ const name = args.technique;
718
+ if (!name)
719
+ return 'Error: technique name required. Use evolution_status to see available techniques.';
720
+ const engine = getEvolutionEngine();
721
+ const technique = engine.techniques.techniques.find(t => t.name === name || t.id === name);
722
+ if (!technique) {
723
+ const available = engine.techniques.techniques.map(t => t.name).join(', ');
724
+ return `Technique "${name}" not found. Available: ${available}`;
725
+ }
726
+ if (technique.applied) {
727
+ return `Technique "${technique.name}" is already applied (score: ${technique.testScore.toFixed(3)}).`;
728
+ }
729
+ const experiment = runExperiment(engine, technique.id);
730
+ if (!experiment) {
731
+ return `Failed to create experiment for "${technique.name}".`;
732
+ }
733
+ startExperiment(experiment, 0, 0, 0);
734
+ saveEvolutionState(engine);
735
+ return [
736
+ `Force-started experiment: ${technique.name}`,
737
+ ` Source: ${technique.source}`,
738
+ ` Category: ${technique.category}`,
739
+ ` Description: ${technique.description}`,
740
+ ` Parameters: ${JSON.stringify(technique.parameters)}`,
741
+ ` Status: running — will evaluate after 180 frames (30 seconds)`,
742
+ ].join('\n');
743
+ },
744
+ });
745
+ }
746
+ //# sourceMappingURL=evolution-engine.js.map