@kernel.chat/kbot 3.88.0 → 3.94.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/dist/tools/audio-engine.d.ts +72 -0
- package/dist/tools/audio-engine.js +426 -0
- package/dist/tools/coordination-engine.d.ts +127 -0
- package/dist/tools/coordination-engine.js +543 -0
- package/dist/tools/evolution-engine.d.ts +102 -0
- package/dist/tools/evolution-engine.js +746 -0
- package/dist/tools/foundation-engines.d.ts +111 -0
- package/dist/tools/foundation-engines.js +520 -0
- package/dist/tools/index.js +9 -0
- package/dist/tools/living-world.d.ts +161 -0
- package/dist/tools/living-world.js +1054 -0
- package/dist/tools/narrative-engine.d.ts +58 -0
- package/dist/tools/narrative-engine.js +681 -0
- package/dist/tools/render-engine.js +5 -5
- package/dist/tools/research-engine.d.ts +58 -0
- package/dist/tools/research-engine.js +550 -0
- package/dist/tools/rom-engine.d.ts +130 -0
- package/dist/tools/rom-engine.js +1208 -0
- package/dist/tools/social-engine.d.ts +100 -0
- package/dist/tools/social-engine.js +540 -0
- package/dist/tools/sprite-engine.js +40 -26
- package/dist/tools/stream-brain.js +4 -4
- package/dist/tools/stream-intelligence.d.ts +6 -0
- package/dist/tools/stream-intelligence.js +239 -49
- package/dist/tools/stream-renderer.js +805 -639
- package/dist/tools/stream-self-eval.d.ts +96 -0
- package/dist/tools/stream-self-eval.js +764 -0
- package/dist/tools/tile-world.d.ts +40 -0
- package/dist/tools/tile-world.js +1070 -0
- package/package.json +1 -1
|
@@ -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
|