-
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathEvoBlob.html
More file actions
285 lines (255 loc) · 53.7 KB
/
EvoBlob.html
File metadata and controls
285 lines (255 loc) · 53.7 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Hyper EvoBlob Simulation (Memory, Signals, Terrain) - Fixed</title>
<style>
body { font-family: sans-serif; display: flex; flex-direction: column; align-items: center; background-color: #f0f0f0; margin: 0; padding: 10px; }
#main-container { display: flex; flex-direction: row; align-items: flex-start; gap: 15px; }
#simulation-area { display: flex; flex-direction: column; align-items: center; }
#sidebar { width: 280px; font-size: 0.85em; background-color: #e8e8e8; padding: 10px; border-radius: 5px; max-height: 900px; overflow-y: auto;}
#simulationCanvas { border: 1px solid black; background-color: #dde8f8; width: 1200px; height: 800px; cursor: crosshair; }
.control-panel, #info, #genome-display, #species-info { margin-top: 8px; padding: 8px; background-color: #e0e0e0; border-radius: 5px; box-sizing: border-box; display: flex; flex-wrap: wrap; gap: 8px 15px; align-items: center; font-size: 0.9em; }
#info, #genome-display, #species-info { width: 1200px; }
.control-panel button, .control-panel label, .control-panel input { margin: 0 3px; padding: 4px 8px; font-size: 0.9em; }
.control-panel label { padding: 0;}
#info span, #species-info span { margin: 0 5px; font-weight: bold; white-space: nowrap; }
.stats-value { color: #0056b3; }
#tooltip { position: absolute; display: none; background: rgba(0, 0, 0, 0.8); color: white; padding: 6px; border-radius: 4px; font-size: 0.75em; pointer-events: none; z-index: 10; max-width: 250px; }
#genome-display { font-size: 0.75em; word-wrap: break-word; max-height: 40px; overflow-y: auto; }
#species-list { margin-top: 5px; max-height: 250px; overflow-y: auto; list-style: none; padding-left: 5px; }
#species-list li { margin-bottom: 3px; padding: 2px; background-color: #f8f8f8; border-radius: 2px;}
#sidebar h3 { margin-top: 0; margin-bottom: 5px; text-align: center; }
.food-depleted { opacity: 0.3; }
</style>
</head>
<body>
<div id="main-container">
<div id="simulation-area">
<h1>Hyper EvoBlob Simulation</h1>
<p style="font-size: 0.9em; margin-bottom: 5px;">Recurrent Memory, Signaling, Terrain, Replenishing Food, Speciation++.</p>
<div class="control-panel">
<button id="startButton">Start</button> <button id="stopButton">Stop</button> <button id="resetButton">Reset</button>
<label for="speedControl">Speed:</label> <input type="range" id="speedControl" min="1" max="10" value="1" step="1"> <span id="speedValue">1x</span>
<button id="addFoodButton">+30 Food</button> <button id="addThreatButton">+10 Threats</button>
<label for="visPheromones">Vis Phero:</label> <input type="checkbox" id="visPheromones" checked>
<label for="visGrid">Vis Grid:</label> <input type="checkbox" id="visGrid">
<label for="simpleDraw">Simple Draw:</label> <input type="checkbox" id="simpleDraw">
</div>
<canvas id="simulationCanvas"></canvas>
<div id="info">
<span>Step: <span id="simStepCount" class="stats-value">0</span></span> <span>Replacements: <span id="replacementCount" class="stats-value">0</span></span>
<span>Avg Fit: <span id="avgFitness" class="stats-value">N/A</span></span> <span>Best Fit: <span id="bestFitness" class="stats-value">N/A</span></span>
<span>Alive: <span id="aliveCount" class="stats-value">0</span>/<span id="populationSize" class="stats-value">0</span></span>
<span>Food: <span id="foodCountDisplay" class="stats-value">0</span></span> <span>Threats: <span id="threatCountDisplay" class="stats-value">0</span></span>
<span>Pheromones: <span id="pheromoneCountDisplay" class="stats-value">0</span></span>
</div>
<div id="species-info">
<span>Species Count: <span id="speciesCount" class="stats-value">0</span></span>
<span>Avg MutRate: <span id="avgMutationRate" class="stats-value">N/A</span></span>
<span>Avg WgtMutRate: <span id="avgWeightMutationRate" class="stats-value">N/A</span></span>
</div>
<div id="genome-display"> Best Genome Sample (Physical - Last Cycle): N/A </div>
</div>
<div id="sidebar">
<h3>Species Details</h3>
<ul id="species-list"><li>No species data yet.</li></ul>
</div>
</div>
<div id="tooltip">Blob Info</div>
<script>
// --- Activation Functions ---
function sigmoid(x){return 1/(1+Math.exp(-x))} function tanh(x){return Math.tanh(x)} function relu(x){return Math.max(0,x)}
// --- Neural Network (with recurrent connections) ---
class NeuralNetwork {
constructor(inputNodes, hiddenNodes, outputNodes, memoryNodes = 0, weights = null) {
this.inputNodes = inputNodes; this.hiddenNodes = hiddenNodes; this.outputNodes = outputNodes; this.memoryNodes = memoryNodes;
this.externalInputs = inputNodes - memoryNodes; this.externalOutputs = outputNodes - memoryNodes;
this.weightsIH_count = this.inputNodes * this.hiddenNodes; this.weightsHO_count = this.hiddenNodes * this.outputNodes; this.biasH_count = this.hiddenNodes; this.biasO_count = this.outputNodes;
this.totalWeights = this.weightsIH_count + this.weightsHO_count + this.biasH_count + this.biasO_count;
if(weights && weights.length === this.totalWeights){this.loadWeights(weights);}else{this.randomizeWeights();}
}
randomizeWeights(){this.weightsIH=Array.from({length:this.weightsIH_count},()=>random(-1,1)); this.weightsHO=Array.from({length:this.weightsHO_count},()=>random(-1,1)); this.biasH=Array.from({length:this.biasH_count},()=>random(-1,1)); this.biasO=Array.from({length:this.biasO_count},()=>random(-1,1));}
loadWeights(w){let i=0; this.weightsIH=w.slice(i,i+=this.weightsIH_count); this.weightsHO=w.slice(i,i+=this.weightsHO_count); this.biasH=w.slice(i,i+=this.biasH_count); this.biasO=w.slice(i,i+=this.biasO_count);}
getWeights(){return [...this.weightsIH,...this.weightsHO,...this.biasH,...this.biasO];}
feedForward(externalInputs, memoryState) {
if(externalInputs.length!==this.externalInputs){console.error(`NN ext input mismatch. E${this.externalInputs}, G${externalInputs.length}`);while(externalInputs.length<this.externalInputs)externalInputs.push(0);if(externalInputs.length>this.externalInputs)externalInputs.length=this.externalInputs;}
if(memoryState.length!==this.memoryNodes){console.error(`NN mem state mismatch. E${this.memoryNodes}, G${memoryState.length}`);while(memoryState.length<this.memoryNodes)memoryState.push(0);if(memoryState.length>this.memoryNodes)memoryState.length=this.memoryNodes;}
const fullInputs = [...externalInputs,...memoryState];
const hidden = new Array(this.hiddenNodes).fill(0);
for(let h=0; h<this.hiddenNodes; h++){let s=this.biasH[h]; for(let i=0; i<this.inputNodes; i++){s+=fullInputs[i]*this.weightsIH[h*this.inputNodes+i];} hidden[h]=tanh(s);}
const fullOutputs = new Array(this.outputNodes).fill(0);
for(let o=0; o<this.outputNodes; o++){let s=this.biasO[o]; for(let h=0; h<this.hiddenNodes; h++){s+=hidden[h]*this.weightsHO[o*this.hiddenNodes+h];} fullOutputs[o]=tanh(s);}
const externalActions = fullOutputs.slice(0, this.externalOutputs); const nextMemoryState = fullOutputs.slice(this.externalOutputs);
return { externalActions, nextMemoryState };
}
}
// --- Simulation Configuration ---
const CANVAS_WIDTH = 1200; const CANVAS_HEIGHT = 800;
const config = {
populationSize: 200, initialFoodCount: 180, initialThreatCount: 50, initialMovingThreatCount: 20, initialSlowZoneCount: 12,
worldRegionSize: 200, regionFoodDensityRange: [0.4, 1.6], regionThreatDensityRange: [0.4, 1.6],
baseMutationRate: 0.1, baseWeightMutationRate: 0.1, mutationAmount: 0.20, weightMutationAmount: 0.3,
minMutationRate: 0.005, maxMutationRate: 0.4, minWeightMutationRate: 0.005, maxWeightMutationRate: 0.5,
pt_maxSpeed: 0, pt_acceleration: 1, pt_turnRate: 2, pt_sensorRange: 3, pt_maxRadius: 4, pt_energyEfficiency: 5, pt_foodEnergyGain: 6, pt_reproductionUrge: 7, pt_pheromoneSensitivity: 8, pt_signalSensitivity: 9, pt_shedBoostMultiplier: 10, pt_shedCostMultiplier: 11, pt_mutationRate: 12, pt_weightMutationRate: 13,
numPhysicalTraits: 14,
numSensors: 8, sensorAngleSpread: Math.PI * 1.9, nnHiddenNodes: 20, nnMemoryNodes: 4, nnExternalOutputs: 4, // [thrust, turn, shed, signal]
numBaseSensorInputs: 4, numPheromoneSensorInputs: 2, numSignalSensorInputs: 1, numInternalStateInputs: 9, numRelativeTargetInputs: 8, numSelfTraitInputs: 2,
simulationTimeStep: 16, worldPadding: 10, initialEnergy: 100, maxEnergyStorageFactor: 2.5,
energyDecayBase: 0.035, energyCostPerThrust: 0.025, energyCostPerTurn: 0.012, energyCostSizeFactor: 0.6, energyCostSignalEmit: 0.5,
sizeChangeRate: 0.05, energyFromFoodBase: 50, foodValueVariance: 0.3, foodReplenishRate: 0.001, foodEvasionRate: 0.4, foodEvasionDistance: 50,
energyLossFromThreat: 80, energyLossFromMovingThreat: 120, minBlobRadius: 3, maxSpeedCap: 4.8, minSpeedCap: 0.2, maxAccelCap: 0.65, minAccelCap: 0.05, maxTurnRateCap: 0.2, minTurnRateCap: 0.01,
maxSensorRangeCap: 150, minSensorRangeCap: 20, maxMaxRadiusCap: 15, minMaxRadiusCap: 4, maxEfficiency: 1.7, minEfficiency: 0.25,
maxFoodGain: 1.9, minFoodGain: 0.5, maxReproductionUrge: 0.8, minReproductionUrge: 0.05,
maxPheromoneSensitivity: 2.5, minPheromoneSensitivity: 0.05, maxSignalSensitivity: 3.0, minSignalSensitivity: 0.1,
maxShedBoost: 2.5, minShedBoost: 1.2, maxShedCost: 0.3, minShedCost: 0.05, shedDurationSteps: 30,
terrainTypes: ['normal', 'slow', 'dense', 'toxic'], terrainEffects: { normal:{speed:1.0,sensor:1.0,decay:1.0}, slow:{speed:0.4,sensor:1.0,decay:1.0}, dense:{speed:0.9,sensor:0.5,decay:1.1}, toxic:{speed:1.0,sensor:1.0,decay:1.5} },
evaluationIntervalSteps: 1000, replacementPercentage: 0.15, eliteSpeciesCount: 1, reproductionEnergyThreshold: 1.2, reproductionEnergyCost: 0.4,
speciationDistanceThreshold: 7.0, genomeDistanceWeights: { physical: 1.0, nn: 0.4 }, speciesMinSizeForPenalty: 3, stagnationThresholdSteps: 20, stagnationCullingThreshold: 35, interSpeciesCrossoverRate: 0.01,
nicheRadius: 60, fitnessSharingAlpha: 1, optimalAgeFactor: 0.6, ageFitnessPenalty: 0.15,
pheromoneGridCellSize: 25, pheromoneFoodStrength: 0.5, pheromoneThreatStrength: 1.0, pheromoneSignalStrength: 1.5, pheromoneDecayRate: 0.98, signalDecayRate: 0.95, maxPheromoneValue: 5.0, maxSignalValue: 4.0, pheromoneEmitInterval: 5,
collisionElasticity: 0.6, collisionEnergyTransfer: 0.01, spatialGridCellSize: 40,
};
// --- Calculate Dependent Config Properties --- <<< CORRECTED SECTION
config.nnExternalInputs = (config.numBaseSensorInputs * config.numSensors) + (config.numPheromoneSensorInputs * config.numSensors) + (config.numSignalSensorInputs * config.numSensors) + config.numInternalStateInputs + config.numRelativeTargetInputs + config.numSelfTraitInputs;
config.nnInputNodes = config.nnExternalInputs + config.nnMemoryNodes;
config.nnOutputNodes = config.nnExternalOutputs + config.nnMemoryNodes; // << Calculation moved here
// Ensure nnTemplate is created *after* all NN related config values are set
const nnTemplate = new NeuralNetwork(config.nnInputNodes, config.nnHiddenNodes, config.nnOutputNodes, config.nnMemoryNodes);
config.genomeLength = config.numPhysicalTraits + nnTemplate.totalWeights;
config.maxEnergy = config.initialEnergy * config.maxEnergyStorageFactor;
config.numToReplace = Math.max(1, Math.floor(config.populationSize * config.replacementPercentage));
config.pheromoneGridCols = Math.ceil(CANVAS_WIDTH / config.pheromoneGridCellSize); config.pheromoneGridRows = Math.ceil(CANVAS_HEIGHT / config.pheromoneGridCellSize);
config.spatialGridCols = Math.ceil(CANVAS_WIDTH / config.spatialGridCellSize); config.spatialGridRows = Math.ceil(CANVAS_HEIGHT / config.spatialGridCellSize);
config.regionGridCols = Math.ceil(CANVAS_WIDTH / config.worldRegionSize); config.regionGridRows = Math.ceil(CANVAS_HEIGHT / config.worldRegionSize);
// --- End Dependent Calculations ---
// --- Globals ---
let canvas, ctx, tooltip, speciesListUL, offscreenPheroCanvas, offscreenPheroCtx;
let environment; let population = []; let species = [];
let totalReplacements = 0; let simulationStep = 0; let stepsSinceLastEval = 0;
let animationFrameId = null; let isRunning = false; let simSpeedMultiplier = 1;
let lastEvalStats = { avgFitness: 0, bestFitness: 0, bestGenomeSample: "N/A", avgMutRate: 0, avgWgtMutRate: 0 };
let mousePos = { x: 0, y: 0 }; let visPheromones = true; let visGrid = false; let simpleDraw = false;
let worldRegions = [];
// --- Utility Functions ---
function random(min, max) { return Math.random()*(max-min)+min } function randomInt(min, max) { return Math.floor(random(min,max+1)) } function distance(x1,y1,x2,y2) { const dx=x1-x2; const dy=y1-y2; return Math.sqrt(dx*dx+dy*dy) } function distanceSq(x1,y1,x2,y2) { const dx=x1-x2; const dy=y1-y2; return dx*dx+dy*dy } function clamp(v,min,max) { return Math.max(min,Math.min(v,max)) } function normalize(v,min,max) { return clamp(((v-min)||0)/((max-min)||1),0,1) }
// --- Spatial Hash Grid ---
class SpatialGrid{constructor(w,h,cs){this.w=w;this.h=h;this.cs=cs;this.cols=Math.ceil(w/cs);this.rows=Math.ceil(h/cs);this.grid=new Map()} _gc(x,y){return{col:Math.floor(x/this.cs),row:Math.floor(y/this.cs)}} _gi(c,r){return r*this.cols+c} clear(){this.grid.clear()} insert(o){const{col,row}=this._gc(o.x,o.y);const i=this._gi(col,row);if(!this.grid.has(i))this.grid.set(i,new Set());this.grid.get(i).add(o);o._sgc=i} queryRadius(x,y,r){const near=new Set();const cCol=Math.floor(x/this.cs);const cRow=Math.floor(y/this.cs);const sr=Math.ceil(r/this.cs);for(let ro=-sr;ro<=sr;ro++){for(let co=-sr;co<=sr;co++){const c=cCol+co;const r=cRow+ro;if(c>=0&&c<this.cols&&r>=0&&r<this.rows){const i=this._gi(c,r);if(this.grid.has(i))this.grid.get(i).forEach(o=>near.add(o))}}}return near} drawGrid(c){c.strokeStyle='rgba(0,0,0,0.1)';c.lineWidth=1;for(let cl=1;cl<this.cols;cl++){c.beginPath();c.moveTo(cl*this.cs,0);c.lineTo(cl*this.cs,this.h);c.stroke()}for(let rw=1;rw<this.rows;rw++){c.beginPath();c.moveTo(0,rw*this.cs);c.lineTo(this.w,rw*this.cs);c.stroke()}}}
// --- Environment Class ---
class Environment {
constructor(width, height) {
this.width=width; this.height=height; this.food=[]; this.threats=[]; this.movingThreats=[]; this.slowZones=[];
this.pheromoneGrid=Array.from({length:config.pheromoneGridCols*config.pheromoneGridRows},()=>({food:0,threat:0,signal:0}));
this.spatialGrid=new SpatialGrid(width,height,config.spatialGridCellSize);
this.generateWorldRegions(); this.reset();
}
generateWorldRegions(){worldRegions=[]; for(let r=0; r<config.regionGridRows; r++){for(let c=0; c<config.regionGridCols; c++){worldRegions.push({foodDensity:random(config.regionFoodDensityRange[0],config.regionFoodDensityRange[1]),threatDensity:random(config.regionThreatDensityRange[0],config.regionThreatDensityRange[1]),terrain:config.terrainTypes[randomInt(0,config.terrainTypes.length-1)]});}} console.log("Generated world regions with terrain.");}
getRegionProperties(x,y){const regCol=clamp(Math.floor(x/config.worldRegionSize),0,config.regionGridCols-1); const regRow=clamp(Math.floor(y/config.worldRegionSize),0,config.regionGridRows-1); const idx=regRow*config.regionGridCols+regCol; return worldRegions[idx]||{foodDensity:1,threatDensity:1,terrain:'normal'};}
getTerrainEffects(x,y){return config.terrainEffects[this.getRegionProperties(x,y).terrain]||config.terrainEffects.normal;}
reset() {
this.food=[]; this.threats=[]; this.movingThreats=[]; this.slowZones=[];
this.pheromoneGrid.forEach(cell=>{cell.food=0; cell.threat=0; cell.signal=0;});
this.spatialGrid.clear(); this.slowZones=Array.from({length:config.initialSlowZoneCount},()=>this._createSlowZone());
let cFood=0,cThreat=0,cMoving=0; const tFood=config.initialFoodCount,tThreat=config.initialThreatCount,tMoving=config.initialMovingThreatCount; const maxAtt=(tFood+tThreat+tMoving)*3;
for(let i=0; i<maxAtt && (cFood<tFood || cThreat<tThreat || cMoving<tMoving); i++) { const x=random(config.worldPadding,this.width-config.worldPadding); const y=random(config.worldPadding,this.height-config.worldPadding); const props=this.getRegionProperties(x,y); if(cFood<tFood && Math.random()<props.foodDensity/1.5){this._addItem(this._createFood(x,y)); cFood++;} if(cThreat<tThreat && Math.random()<props.threatDensity/1.5){this._addItem(this._createThreat(x,y)); cThreat++;} if(cMoving<tMoving && Math.random()<props.threatDensity/1.5){this._addItem(this._createMovingThreat(x,y)); cMoving++;} }
console.log(`Env Reset: Added ${cFood} Food, ${cThreat} Threats, ${cMoving} Moving Threats.`); this.rebuildSpatialGrid();
}
_createFood(x=null,y=null){x=x??random(config.worldPadding,this.width-config.worldPadding); y=y??random(config.worldPadding,this.height-config.worldPadding); const maxV=config.energyFromFoodBase*(1+random(-config.foodValueVariance,config.foodValueVariance)); return {x,y,radius:5,maxValue:maxV,currentValue:maxV,replenishTimer:0,vx:0,vy:0,type:'food'};}
_createThreat(x=null,y=null){x=x??random(config.worldPadding,this.width-config.worldPadding); y=y??random(config.worldPadding,this.height-config.worldPadding); return {x,y,radius:8,type:'threat'};}
_createMovingThreat(x=null,y=null){x=x??random(config.worldPadding,this.width-config.worldPadding); y=y??random(config.worldPadding,this.height-config.worldPadding); return {x,y,radius:10,vx:random(-1,1)*1.8,vy:random(-1,1)*1.8,type:'mthreat'};}
_createSlowZone(){const s=random(50,120); return {x:random(0,this.width-s),y:random(0,this.height-s),width:s,height:s};}
_addItem(item){if(item.type==='food')this.food.push(item); else if(item.type==='threat')this.threats.push(item); else if(item.type==='mthreat')this.movingThreats.push(item); this.spatialGrid.insert(item);}
addFoodItem(x=null,y=null){this._addItem(this._createFood(x,y));} addThreatItem(x=null,y=null){this._addItem(this._createThreat(x,y));} addMovingThreatItem(x=null,y=null){this._addItem(this._createMovingThreat(x,y));}
consumeFood(item){item.currentValue=0; item.replenishTimer=1;}
updateFood(nearbyBlobsMap){this.food.forEach(f=>{if(f.replenishTimer>0&&f.currentValue<f.maxValue){f.currentValue+=f.maxValue*config.foodReplenishRate; if(f.currentValue>=f.maxValue){f.currentValue=f.maxValue;f.replenishTimer=0;}} let clDistSq=Infinity; let evX=0,evY=0; const pEvaders=nearbyBlobsMap.get(f)||[]; for(const b of pEvaders){const dSq=distanceSq(f.x,f.y,b.x,b.y); if(dSq<config.foodEvasionDistance*config.foodEvasionDistance&&dSq<clDistSq){clDistSq=dSq; evX=f.x-b.x; evY=f.y-b.y;}} if(clDistSq!==Infinity){const dist=Math.sqrt(clDistSq); const evF=(config.foodEvasionRate/(dist+1)); f.vx+=(evX/dist)*evF; f.vy+=(evY/dist)*evF;} f.vx*=0.85; f.vy*=0.85; const sp=Math.sqrt(f.vx*f.vx+f.vy*f.vy); const maxSp=0.5; if(sp>maxSp){f.vx=(f.vx/sp)*maxSp; f.vy=(f.vy/sp)*maxSp;} f.x=clamp(f.x+f.vx,config.worldPadding,this.width-config.worldPadding); f.y=clamp(f.y+f.vy,config.worldPadding,this.height-config.worldPadding);});}
updateMovingThreats(){this.movingThreats.forEach(mt=>{mt.x+=mt.vx; mt.y+=mt.vy; if(mt.x<mt.radius||mt.x>this.width-mt.radius){mt.vx*=-1;mt.x=clamp(mt.x,mt.radius,this.width-mt.radius)} if(mt.y<mt.radius||mt.y>this.height-mt.radius){mt.vy*=-1;mt.y=clamp(mt.y,mt.radius,this.height-mt.radius)}});}
isInSlowZone(x,y){for(const z of this.slowZones){if(x>z.x&&x<z.x+z.width&&y>z.y&&y<z.y+z.height)return true;} return false;}
rebuildSpatialGrid(){this.spatialGrid.clear(); this.food.forEach(f=>this.spatialGrid.insert(f)); this.threats.forEach(t=>this.spatialGrid.insert(t)); this.movingThreats.forEach(mt=>this.spatialGrid.insert(mt)); population.forEach(b=>{if(b.isAlive)this.spatialGrid.insert(b)}); }
getGridCoordsPhero(x,y){const c=Math.floor(x/config.pheromoneGridCellSize);const r=Math.floor(y/config.pheromoneGridCellSize);return{col:clamp(c,0,config.pheromoneGridCols-1),row:clamp(r,0,config.pheromoneGridRows-1)};}
getGridIndexPhero(col,row){return row*config.pheromoneGridCols+col;}
addPheromone(x,y,type,strength){const{col,row}=this.getGridCoordsPhero(x,y); const idx=this.getGridIndexPhero(col,row); if(this.pheromoneGrid[idx]){const maxV=type==='signal'?config.maxSignalValue:config.maxPheromoneValue; this.pheromoneGrid[idx][type]=clamp(this.pheromoneGrid[idx][type]+strength,0,maxV);}}
getPheromone(x,y){const gX=x/config.pheromoneGridCellSize; const gY=y/config.pheromoneGridCellSize; const c0=Math.floor(gX-0.5); const c1=c0+1; const r0=Math.floor(gY-0.5); const r1=r0+1; const tx=gX-(c0+0.5); const ty=gY-(r0+0.5); const getV=(c,r,t)=>{c=clamp(c,0,config.pheromoneGridCols-1);r=clamp(r,0,config.pheromoneGridRows-1);const i=this.getGridIndexPhero(c,r);return this.pheromoneGrid[i]?this.pheromoneGrid[i][t]:0}; const lerp=(a,b,t)=>a+(b-a)*t; const p00={f:getV(c0,r0,'food'),t:getV(c0,r0,'threat'),s:getV(c0,r0,'signal')}; const p10={f:getV(c1,r0,'food'),t:getV(c1,r0,'threat'),s:getV(c1,r0,'signal')}; const p01={f:getV(c0,r1,'food'),t:getV(c0,r1,'threat'),s:getV(c0,r1,'signal')}; const p11={f:getV(c1,r1,'food'),t:getV(c1,r1,'threat'),s:getV(c1,r1,'signal')}; const food=lerp(lerp(p00.f,p10.f,tx),lerp(p01.f,p11.f,tx),ty); const threat=lerp(lerp(p00.t,p10.t,tx),lerp(p01.t,p11.t,tx),ty); const signal=lerp(lerp(p00.s,p10.s,tx),lerp(p01.s,p11.s,tx),ty); return{food,threat,signal};}
decayPheromones(){let actCnt=0; this.pheromoneGrid.forEach(cell=>{cell.food*=config.pheromoneDecayRate; cell.threat*=config.pheromoneDecayRate; cell.signal*=config.signalDecayRate; if(cell.food<0.01)cell.food=0; if(cell.threat<0.01)cell.threat=0; if(cell.signal<0.01)cell.signal=0; if(cell.food>0||cell.threat>0||cell.signal>0)actCnt++;}); document.getElementById('pheromoneCountDisplay').textContent=actCnt;}
drawPheromonesOffscreen(){if(!offscreenPheroCtx)return; const ctx=offscreenPheroCtx;const w=offscreenPheroCanvas.width; const h=offscreenPheroCanvas.height; ctx.clearRect(0,0,w,h); const cs=config.pheromoneGridCellSize; for(let r=0;r<config.pheromoneGridRows;r++){for(let c=0;c<config.pheromoneGridCols;c++){const idx=this.getGridIndexPhero(c,r); const cell=this.pheromoneGrid[idx]; const x=c*cs; const y=r*cs; if(cell.food>0.01){ctx.fillStyle=`rgba(0,255,0,${normalize(cell.food,0,config.maxPheromoneValue)*0.15})`; ctx.fillRect(x,y,cs,cs);} if(cell.threat>0.01){ctx.fillStyle=`rgba(255,0,0,${normalize(cell.threat,0,config.maxPheromoneValue)*0.15})`; ctx.fillRect(x,y,cs,cs);} if(cell.signal>0.01){ctx.fillStyle=`rgba(0,0,255,${normalize(cell.signal,0,config.maxSignalValue)*0.20})`; ctx.fillRect(x,y,cs,cs);}}}}
draw(ctx){if(visPheromones&&offscreenPheroCanvas)ctx.drawImage(offscreenPheroCanvas,0,0); ctx.fillStyle='rgba(139,69,19,0.25)'; this.slowZones.forEach(z=>{ctx.fillRect(z.x,z.y,z.width,z.height);}); this.food.forEach(f=>{const rF=0.3+0.7*(f.currentValue/f.maxValue); ctx.fillStyle=`rgb(0,${Math.round(180+75*(f.currentValue/f.maxValue))},0)`; ctx.globalAlpha=rF; ctx.beginPath(); ctx.arc(f.x,f.y,f.radius*rF,0,Math.PI*2); ctx.fill(); ctx.globalAlpha=1.0;}); ctx.fillStyle='red'; this.threats.forEach(t=>{ctx.beginPath(); ctx.arc(t.x,t.y,t.radius,0,Math.PI*2); ctx.fill();}); ctx.fillStyle='purple'; this.movingThreats.forEach(mt=>{ctx.beginPath(); ctx.arc(mt.x,mt.y,mt.radius,0,Math.PI*2); ctx.fill();}); if(visGrid)this.spatialGrid.drawGrid(ctx); document.getElementById('foodCountDisplay').textContent=this.food.filter(f=>f.currentValue>0).length; document.getElementById('threatCountDisplay').textContent=this.threats.length+this.movingThreats.length;}
}
// --- Species Class ---
class Species{constructor(rep){this.id=Math.random().toString(36).substring(2,9);this.representative=rep.genome;this.members=[rep];this.adjFitSum=0;this.avgFit=0;this.bestFit=-1;this.stag=0;rep.speciesId=this.id} addMember(b){this.members.push(b);b.speciesId=this.id} calculateAdjustedFitness(){this.adjFitSum=0;this.bestFit=-1;if(this.members.length===0){this.avgFit=0;return} this.members.forEach(m=>{if(m.isAlive){const sf=m.rawFitness/(m.nicheCount||1);m.adjustedFitness=sf;this.adjFitSum+=sf;if(sf>this.bestFit)this.bestFit=sf}else{m.adjustedFitness=0;}});const newAvg=this.adjFitSum/this.members.length;if(newAvg>this.avgFit){this.stag=0;this.avgFit=newAvg}else{this.stag++} this.avgFit=newAvg} selectParent(){let best=null;let bestF=-1;const ts=Math.min(5,this.members.length);for(let i=0;i<ts;i++){const rm=this.members[randomInt(0,this.members.length-1)];if(rm.isAlive&&rm.adjustedFitness>bestF&&rm.energy>=config.initialEnergy*config.reproductionEnergyThreshold){best=rm;bestF=rm.adjustedFitness;}} if(!best){const lm=this.members.filter(m=>m.isAlive);if(lm.length>0)best=lm[randomInt(0,lm.length-1)]} return best} reset(){this.members=[];this.adjFitSum=0}} // Minified version
// --- Blob Class ---
class Blob {
constructor(genome) {
this.genome=genome||this.createRandomGenome(); this.decodeGenome(); this.x=random(config.worldPadding,CANVAS_WIDTH-config.worldPadding); this.y=random(config.worldPadding,CANVAS_HEIGHT-config.worldPadding); this.angle=random(0,Math.PI*2); this.vx=0; this.vy=0; this.energy=config.initialEnergy; this.currentRadius=this.maxRadius/2; this.rawFitness=0; this.adjustedFitness=0; this.nicheCount=1; this.isAlive=true; this.sensorAngles=Array.from({length:config.numSensors},(_,i)=>(i/config.numSensors)*config.sensorAngleSpread-(config.sensorAngleSpread/2)); this.memoryState=new Array(config.nnMemoryNodes).fill(0); this.lastExternalOutputs=new Array(config.nnExternalOutputs).fill(0); this.id=Math.random().toString(36).substring(2,9); this.age=0; this.shedBoostTimer=0; this.currentMaxSpeed=this.maxSpeed; this.speciesId=null; this._sgc=-1; this.type='blob';
}
createRandomGenome() { const phys=[random(config.minSpeedCap,config.maxSpeedCap),random(config.minAccelCap,config.maxAccelCap),random(config.minTurnRateCap,config.maxTurnRateCap),random(config.minSensorRangeCap,config.maxSensorRangeCap),random(config.minMaxRadiusCap,config.maxMaxRadiusCap),random(config.minEfficiency,config.maxEfficiency),random(config.minFoodGain,config.maxFoodGain),random(config.minReproductionUrge,config.maxReproductionUrge),random(config.minPheromoneSensitivity,config.maxPheromoneSensitivity),random(config.minSignalSensitivity,config.maxSignalSensitivity),random(config.minShedBoost,config.maxShedBoost),random(config.minShedCost,config.maxShedCost),config.baseMutationRate,config.baseWeightMutationRate]; while(phys.length<config.numPhysicalTraits)phys.push(0); const nnW=Array.from({length:nnTemplate.totalWeights},()=>random(-1,1)); return[...phys,...nnW]; }
decodeGenome() { this.maxSpeed=clamp(this.genome[config.pt_maxSpeed],config.minSpeedCap,config.maxSpeedCap); this.currentMaxSpeed=this.maxSpeed; this.acceleration=clamp(this.genome[config.pt_acceleration],config.minAccelCap,config.maxAccelCap); this.turnRate=clamp(this.genome[config.pt_turnRate],config.minTurnRateCap,config.maxTurnRateCap); this.sensorRange=clamp(this.genome[config.pt_sensorRange],config.minSensorRangeCap,config.maxSensorRangeCap); this.maxRadius=clamp(this.genome[config.pt_maxRadius],config.minMaxRadiusCap,config.maxMaxRadiusCap); this.energyEfficiency=clamp(this.genome[config.pt_energyEfficiency],config.minEfficiency,config.maxEfficiency); this.foodEnergyGain=clamp(this.genome[config.pt_foodEnergyGain],config.minFoodGain,config.maxFoodGain); this.reproductionUrge=clamp(this.genome[config.pt_reproductionUrge],config.minReproductionUrge,config.maxReproductionUrge); this.pheromoneSensitivity=clamp(this.genome[config.pt_pheromoneSensitivity],config.minPheromoneSensitivity,config.maxPheromoneSensitivity); this.signalSensitivity=clamp(this.genome[config.pt_signalSensitivity],config.minSignalSensitivity,config.maxSignalSensitivity); this.shedBoostMultiplier=clamp(this.genome[config.pt_shedBoostMultiplier],config.minShedBoost,config.maxShedBoost); this.shedCostMultiplier=clamp(this.genome[config.pt_shedCostMultiplier],config.minShedCost,config.maxShedCost); this.mutationRate=clamp(this.genome[config.pt_mutationRate],config.minMutationRate,config.maxMutationRate); this.weightMutationRate=clamp(this.genome[config.pt_weightMutationRate],config.minWeightMutationRate,config.maxWeightMutationRate); const nnW=this.genome.slice(config.numPhysicalTraits); this.brain=new NeuralNetwork(config.nnInputNodes,config.nnHiddenNodes,config.nnOutputNodes,config.nnMemoryNodes,nnW); }
sense(environment) { const extInp=[]; const terrEff=environment.getTerrainEffects(this.x,this.y); const effSensRng=this.sensorRange*terrEff.sensor; const maxRng=effSensRng; const sensConeThr=0.707; let nearF=null,nearFDsq=maxRng*maxRng; let nearT=null,nearTDsq=maxRng*maxRng; const nearObj=environment.spatialGrid.queryRadius(this.x,this.y,maxRng); for(const relAng of this.sensorAngles){const absAng=this.angle+relAng; const dx=Math.cos(absAng); const dy=Math.sin(absAng); let fDist=maxRng, tDist=maxRng, mtDist=maxRng; nearObj.forEach(obj=>{if(obj===this)return; const dSq=distanceSq(this.x,this.y,obj.x,obj.y); const d=Math.sqrt(dSq); const vX=obj.x-this.x; const vY=obj.y-this.y; const dot=(dx*vX+dy*vY)/(d||1); if(dot>sensConeThr&&d<maxRng){if(obj.type==='food'&&obj.currentValue>0&&d<fDist)fDist=d; if(obj.type==='threat'&&d<tDist)tDist=d; if(obj.type==='mthreat'&&d<mtDist)mtDist=d;} if(obj.type==='food'&&obj.currentValue>0&&dSq<nearFDsq){nearF=obj;nearFDsq=dSq;} if((obj.type==='threat'||obj.type==='mthreat')&&dSq<nearTDsq){nearT=obj;nearTDsq=dSq;}}); let wallD=maxRng; if(dx!==0){const dX1=(0-this.x)/dx; const dX2=(environment.width-this.x)/dx; if(dX1>0&&dX1<wallD)wallD=dX1; if(dX2>0&&dX2<wallD)wallD=dX2;} if(dy!==0){const dY1=(0-this.y)/dy; const dY2=(environment.height-this.y)/dy; if(dY1>0&&dY1<wallD)wallD=dY1; if(dY2>0&&dY2<wallD)wallD=dY2;} wallD=Math.min(wallD,maxRng); extInp.push(1.0-normalize(fDist,0,maxRng)); extInp.push(1.0-normalize(tDist,0,maxRng)); extInp.push(1.0-normalize(mtDist,0,maxRng)); extInp.push(1.0-normalize(wallD,0,maxRng)); const sTipX=this.x+dx*maxRng*0.5; const sTipY=this.y+dy*maxRng*0.5; const phero=environment.getPheromone(sTipX,sTipY); extInp.push(normalize(phero.food,0,config.maxPheromoneValue)*this.pheromoneSensitivity); extInp.push(normalize(phero.threat,0,config.maxPheromoneValue)*this.pheromoneSensitivity); extInp.push(normalize(phero.signal,0,config.maxSignalValue)*this.signalSensitivity);} const hunger=1.0-normalize(this.energy,0,config.maxEnergy); extInp.push(normalize(this.energy,0,config.maxEnergy)); extInp.push(hunger); extInp.push(normalize(this.currentRadius,config.minBlobRadius,this.maxRadius)); extInp.push(normalize(this.vx,-this.currentMaxSpeed,this.currentMaxSpeed)); extInp.push(normalize(this.vy,-this.currentMaxSpeed,this.currentMaxSpeed)); extInp.push(this.lastExternalOutputs[0]); extInp.push(this.lastExternalOutputs[1]); extInp.push(this.lastExternalOutputs[2]>0?1:0); extInp.push(this.lastExternalOutputs[3]>0?1:0); const addRel=(t)=>{if(t){const rX=t.x-this.x,rY=t.y-this.y,tVX=t.vx||0,tVY=t.vy||0,rVX=tVX-this.vx,rVY=tVY-this.vy;extInp.push(normalize(rX,-maxRng,maxRng),normalize(rY,-maxRng,maxRng),normalize(rVX,-this.currentMaxSpeed*2,this.currentMaxSpeed*2),normalize(rVY,-this.currentMaxSpeed*2,this.currentMaxSpeed*2))}else{extInp.push(0,0,0,0)}}; addRel(nearF); addRel(nearT); extInp.push(normalize(this.mutationRate,config.minMutationRate,config.maxMutationRate)); extInp.push(normalize(this.weightMutationRate,config.minWeightMutationRate,config.maxWeightMutationRate)); if(extInp.length!==this.brain.externalInputs){console.error(`Ext inp len mismatch! E${this.brain.externalInputs}, A${extInp.length}`);while(extInp.length<this.brain.externalInputs)extInp.push(0);if(extInp.length>this.brain.externalInputs)extInp.length=this.brain.externalInputs;} return extInp; }
act(externalInputs, environment) { const{externalActions,nextMemoryState}=this.brain.feedForward(externalInputs,this.memoryState); this.lastExternalOutputs=[...externalActions]; this.memoryState=nextMemoryState; const thrust=externalActions[0]; const turn=externalActions[1]; const shedDecision=externalActions[2]; const emitSignalDecision=externalActions[3]; this.angle=(this.angle+turn*this.turnRate)%(Math.PI*2); const effAccel=this.acceleration; const tVX=Math.cos(this.angle)*thrust*effAccel; const tVY=Math.sin(this.angle)*thrust*effAccel; this.vx*=0.92; this.vy*=0.92; this.vx+=tVX; this.vy+=tVY; if(shedDecision>0&&this.shedBoostTimer<=0){const cost=config.initialEnergy*this.shedCostMultiplier;if(this.energy>cost+config.energyDecayBase*5){this.energy-=cost;this.shedBoostTimer=config.shedDurationSteps;this.currentMaxSpeed=this.maxSpeed*this.shedBoostMultiplier;const sizeRed=1.0-normalize(cost,0,config.initialEnergy);this.currentRadius=Math.max(this.currentRadius*sizeRed,config.minBlobRadius)}else{this.lastExternalOutputs[2]=-1}} if(this.shedBoostTimer>0){this.shedBoostTimer--;if(this.shedBoostTimer<=0){this.currentMaxSpeed=this.maxSpeed}} if(emitSignalDecision>0){const cost=config.energyCostSignalEmit; if(this.energy>cost+config.energyDecayBase*2){this.energy-=cost; environment.addPheromone(this.x,this.y,'signal',config.pheromoneSignalStrength);} else{this.lastExternalOutputs[3]=-1;}} const terrEff=environment.getTerrainEffects(this.x,this.y); const speedLimit=this.currentMaxSpeed*terrEff.speed*(environment.isInSlowZone(this.x,this.y)?config.slowZoneFactor:1); const speed=Math.sqrt(this.vx*this.vx+this.vy*this.vy); if(speed>speedLimit){this.vx=(this.vx/speed)*speedLimit; this.vy=(this.vy/speed)*speedLimit} const sizeCostMult=1+(normalize(this.currentRadius,config.minBlobRadius,this.maxRadius)*config.energyCostSizeFactor); this.energy-=(Math.abs(thrust)*config.energyCostPerThrust+Math.abs(turn)*config.energyCostPerTurn)*sizeCostMult; }
updateSize() {const er=this.energy/config.initialEnergy; let tr=this.maxRadius*normalize(er,0.5,1.5); tr=clamp(tr,config.minBlobRadius,this.maxRadius); if(tr>this.currentRadius){this.currentRadius+=config.sizeChangeRate*(tr-this.currentRadius)}else if(tr<this.currentRadius){this.currentRadius-=config.sizeChangeRate*(this.currentRadius-tr)} this.currentRadius=clamp(this.currentRadius,config.minBlobRadius,this.maxRadius);}
handleCollisions(nearbyBlobsIter) {for(const other of nearbyBlobsIter){if(this===other||!other.isAlive||other.type!=='blob')continue; const dSq=distanceSq(this.x,this.y,other.x,other.y); const combRad=this.currentRadius+other.currentRadius; if(dSq<combRad*combRad&&dSq>0){const dist=Math.sqrt(dSq);const overlap=combRad-dist; const nx=(this.x-other.x)/dist;const ny=(this.y-other.y)/dist;const push=overlap*0.5; this.x+=nx*push;this.y+=ny*push; other.x-=nx*push;other.y-=ny*push; const rvx=this.vx-other.vx;const rvy=this.vy-other.vy;const velNorm=rvx*nx+rvy*ny; if(velNorm>0)continue; const e=config.collisionElasticity; let j=-(1+e)*velNorm; const m1f=1+normalize(this.currentRadius,config.minBlobRadius,this.maxRadius); const m2f=1+normalize(other.currentRadius,config.minBlobRadius,other.maxRadius); j/=(1/m1f+1/m2f); const impX=j*nx; const impY=j*ny; this.vx+=impX/m1f; this.vy+=impY/m1f; other.vx-=impX/m2f; other.vy-=impY/m2f; const enDiff=this.energy-other.energy; const trans=Math.abs(enDiff)*config.collisionEnergyTransfer; if(enDiff>0){this.energy-=trans;other.energy+=trans}else{this.energy+=trans;other.energy-=trans} this.energy=clamp(this.energy,0,config.maxEnergy); other.energy=clamp(other.energy,0,config.maxEnergy);}}}
update(environment) { if(!this.isAlive)return; this.age++; const extSensInp=this.sense(environment); this.act(extSensInp,environment); this.updateSize(); const nearCheckRad=this.currentRadius*2+config.spatialGridCellSize; const nearObj=environment.spatialGrid.queryRadius(this.x,this.y,nearCheckRad); this.handleCollisions(nearObj.values()); this.x+=this.vx; this.y+=this.vy; if(this.x<this.currentRadius){this.x=this.currentRadius;this.vx*=-0.5} if(this.x>environment.width-this.currentRadius){this.x=environment.width-this.currentRadius;this.vx*=-0.5} if(this.y<this.currentRadius){this.y=this.currentRadius;this.vy*=-0.5} if(this.y>environment.height-this.currentRadius){this.y=environment.height-this.currentRadius;this.vy*=-0.5} let enChg=0; let ate=false; nearObj.forEach(item=>{if(item===this||item.type==='blob')return; const intDistSq=(this.currentRadius+item.radius)**2; if(distanceSq(this.x,this.y,item.x,item.y)<intDistSq){if(!ate&&item.type==='food'&&item.currentValue>0){const consumed=item.currentValue; const gain=consumed*this.foodEnergyGain; enChg+=gain; this.rawFitness+=15*normalize(gain,0,config.energyFromFoodBase*config.maxFoodGain*2); environment.consumeFood(item); ate=true;} else if(item.type==='threat'){enChg-=config.energyLossFromThreat; this.rawFitness-=5;} else if(item.type==='mthreat'){enChg-=config.energyLossFromMovingThreat; this.rawFitness-=8;}}}); const terrEff=environment.getTerrainEffects(this.x,this.y); const sizeCostMult=1+(normalize(this.currentRadius,config.minBlobRadius,this.maxRadius)*config.energyCostSizeFactor); const decay=config.energyDecayBase*this.energyEfficiency*sizeCostMult*terrEff.decay; enChg-=decay; this.energy=clamp(this.energy+enChg,0,config.maxEnergy); this.rawFitness+=0.05; this.rawFitness=Math.max(0,this.rawFitness); if(simulationStep%config.pheromoneEmitInterval===0){if(ate)environment.addPheromone(this.x,this.y,'food',config.pheromoneFoodStrength); else if(enChg<-decay*1.5)environment.addPheromone(this.x,this.y,'threat',config.pheromoneThreatStrength)} if(this.energy<=0||this.currentRadius<config.minBlobRadius){this.isAlive=false; this.rawFitness=Math.max(0,this.rawFitness-50);}}
draw(ctx) { const alpha=this.isAlive?clamp(this.energy/config.initialEnergy,0.3,1.0):0.1; const borderC=this.isAlive?(this.shedBoostTimer>0?'rgba(255,100,0,0.8)':'rgba(0,0,0,0.5)') : 'rgba(150,150,150,0.5)'; const hue=normalize(this.reproductionUrge,config.minReproductionUrge,config.maxReproductionUrge)*120+240; ctx.fillStyle=`hsla(${hue},70%,60%,${alpha})`; ctx.beginPath(); ctx.arc(this.x,this.y,this.currentRadius,0,Math.PI*2); ctx.fill(); if(!simpleDraw){ctx.strokeStyle=borderC; ctx.lineWidth=this.shedBoostTimer>0?2:1; ctx.stroke(); if(this.isAlive){ctx.strokeStyle='rgba(255,255,255,0.7)'; ctx.lineWidth=2; ctx.beginPath(); ctx.moveTo(this.x,this.y); const fX=this.x+Math.cos(this.angle)*(this.currentRadius+3); const fY=this.y+Math.sin(this.angle)*(this.currentRadius+3); ctx.lineTo(fX,fY); ctx.stroke();}} else if(this.isAlive){ctx.strokeStyle=borderC; ctx.lineWidth=1; ctx.stroke();} }
}
// --- GA / Speciation Functions ---
function calculateGenomeDistance(g1, g2) { let dSq=0; const len=Math.min(g1.length,g2.length); const wP=config.genomeDistanceWeights.physical; const wN=config.genomeDistanceWeights.nn; for(let i=0;i<len;i++){const diff=g1[i]-g2[i]; const w=(i<config.numPhysicalTraits)?wP:wN; dSq+=(diff*diff)*w;} return Math.sqrt(dSq); }
function speciatePopulation() { species.forEach(s=>s.reset()); let unassigned=[...population]; let nextSpecies=[]; while(unassigned.length>0){const current=unassigned.shift(); let assigned=false; for(const s of species){const dist=calculateGenomeDistance(current.genome,s.representative); if(dist<config.speciationDistanceThreshold){s.addMember(current); assigned=true; if(!nextSpecies.includes(s))nextSpecies.push(s); break;}} if(!assigned){const newS=new Species(current); species.push(newS); nextSpecies.push(newS);}} species=species.filter(s=>s.members.length>0); return species; }
function performEvolutionStep() { // Updated offspring allocation with culling
const activeSpecies = speciatePopulation();
population.forEach(b => b.nicheCount = 1); for(let i=0;i<population.length;i++){if(!population[i].isAlive)continue; const neighbors=environment.spatialGrid.queryRadius(population[i].x,population[i].y,config.nicheRadius); neighbors.forEach(n=>{if(n!==population[i]&&n.isAlive&&n.type==='blob'){const dSq=distanceSq(population[i].x,population[i].y,n.x,n.y);if(dSq<config.nicheRadius*config.nicheRadius){const sv=1.0-Math.pow(Math.sqrt(dSq)/config.nicheRadius,config.fitnessSharingAlpha); population[i].nicheCount+=sv;}}});}
population.forEach(b=>{if(b.isAlive){const ar=b.age/config.evaluationIntervalSteps; const ad=ar-config.optimalAgeFactor; const ap=Math.pow(ad/(1.0-config.optimalAgeFactor),2)*config.ageFitnessPenalty; b.rawFitness*=(1.0-ap); b.rawFitness=Math.max(0,b.rawFitness);}else{b.rawFitness=0;}});
activeSpecies.forEach(s => s.calculateAdjustedFitness());
let totalAdjFitSum = activeSpecies.reduce((sum,s)=>sum+s.adjFitSum,0); let offspringAlloc=[]; let speciesToKeep=[];
activeSpecies.sort((a,b)=>b.avgFit-a.avgFit); // Sort by avg adjusted fitness for elite check
if(totalAdjFitSum > 0){ activeSpecies.forEach((s,idx)=>{ let numOff=0; const isElite=idx<config.eliteSpeciesCount; if(s.stagnation>config.stagnationCullingThreshold&&!isElite){numOff=0; console.log(`Culling stagnant species ${s.id.substring(0,4)}`);} else {speciesToKeep.push(s); numOff=(s.adjFitSum/totalAdjFitSum)*config.populationSize; if(s.stagnation>config.stagnationThresholdSteps&&s.members.length>=config.speciesMinSizeForPenalty){numOff*=0.75;} numOff=Math.round(numOff);} if(numOff>0){offspringAlloc.push({species:s,count:numOff});}}); }
else { activeSpecies.forEach(s=>{offspringAlloc.push({species:s,count:Math.floor(config.populationSize/activeSpecies.length)}); speciesToKeep.push(s);}); }
species = speciesToKeep; // Update global species list
let currentSum=offspringAlloc.reduce((sum,alloc)=>sum+alloc.count,0); let diff=config.populationSize-currentSum; if(diff!==0&&offspringAlloc.length>0){offspringAlloc.sort((a,b)=>b.species.avgFit-a.species.avgFit); offspringAlloc[0].count=Math.max(0,offspringAlloc[0].count+diff);} currentSum=offspringAlloc.reduce((sum,alloc)=>sum+alloc.count,0); diff=config.populationSize-currentSum; if(diff!==0&&offspringAlloc.length>0){let i=0; while(diff>0){offspringAlloc[i%offspringAlloc.length].count++; diff--; i++;} while(diff<0){if(offspringAlloc[i%offspringAlloc.length].count>0){offspringAlloc[i%offspringAlloc.length].count--; diff++;} i++;}}
const nextPop=[]; population.sort((a,b)=>b.adjFit-a.adjFit); if(population.length>0&&population[0].isAlive){nextPop.push(new Blob(population[0].genome));} // Elitism
offspringAlloc.forEach(alloc=>{ const sp=alloc.species; for(let i=0;i<alloc.count;i++){ if(nextPop.length>=config.populationSize)break; let p1=sp.selectParent(); let p2=null; if(Math.random()<config.interSpeciesCrossoverRate&&species.length>1){let oS=species[randomInt(0,species.length-1)]; while(oS===sp&&species.length>1)oS=species[randomInt(0,species.length-1)]; p2=oS.selectParent();} else {p2=sp.selectParent();} if(p1&&!p2)p2=p1; if(!p1&&p2)p1=p2; if(!p1&&!p2){const lp=population.filter(b=>b.isAlive); if(lp.length>0){p1=lp[randomInt(0,lp.length-1)]; p2=p1;}else{p1=null;}} if(p1&&p2){let childG=crossover(p1.genome,p2.genome); const tempB=new Blob(childG); childG=mutate(childG,tempB.mutationRate,tempB.weightMutationRate); nextPop.push(new Blob(childG)); p1.energy=Math.max(0,p1.energy-config.initialEnergy*config.reproductionEnergyCost); if(p1!==p2)p2.energy=Math.max(0,p2.energy-config.initialEnergy*config.reproductionEnergyCost);}else{nextPop.push(new Blob());}}});
while(nextPop.length<config.populationSize){console.warn("Filling random blobs."); nextPop.push(new Blob());}
population = nextPop;
const livingNow=population.filter(b=>b.isAlive); lastEvalStats.avgFitness=livingNow.reduce((sum,b)=>sum+b.adjustedFitness,0)/livingNow.length||0; population.sort((a,b)=>b.adjustedFitness-a.adjustedFitness); lastEvalStats.bestFitness=population.length>0?population[0].adjustedFitness:0;
if(population.length>0&&population[0]){const bfG=population[0].genome.slice(0,config.numPhysicalTraits).map(v=>v.toFixed(2)).join(', '); lastEvalStats.bestGenomeSample=`[${bfG}]`;}else{lastEvalStats.bestGenomeSample="N/A";}
if(livingNow.length>0){lastEvalStats.avgMutRate=livingNow.reduce((s,b)=>s+b.mutationRate,0)/livingNow.length; lastEvalStats.avgWgtMutRate=livingNow.reduce((s,b)=>s+b.weightMutationRate,0)/livingNow.length;}else{lastEvalStats.avgMutRate=0; lastEvalStats.avgWgtMutRate=0;}
totalReplacements+=population.length; stepsSinceLastEval=0; updateSpeciesListUI(species);
}
function mutate(genome, physicalRate, weightRate){return genome.map((g,i)=>{if(i<config.numPhysicalTraits){if(Math.random()<physicalRate){const c=random(-config.mutationAmount,config.mutationAmount); return g*(1+c)}else return g}else{if(Math.random()<weightRate){const c=random(-config.weightMutationAmount,config.weightMutationAmount); return clamp(g+c,-5,5)}else return g}}); } // Minified
function crossover(g1,g2){const c=[];const a=0.5;for(let i=0;i<config.numPhysicalTraits;i++){c.push(a*g1[i]+(1-a)*g2[i])}const ws=config.numPhysicalTraits;for(let i=ws;i<config.genomeLength;i++){c.push(Math.random()<0.5?g1[i]:g2[i])}return c;} // Minified
// --- Simulation Loop ---
function gameLoop(timestamp) {
if(!isRunning)return; const elapsed=timestamp-(gameLoop.lastTimestamp||timestamp); gameLoop.lastTimestamp=timestamp; const effTStep=Math.min(elapsed,100)*simSpeedMultiplier; let steps=Math.max(1,Math.round(effTStep/config.simulationTimeStep)); steps=Math.min(steps,15);
for(let i=0;i<steps;i++){if(!isRunning)break; simulationStep++; stepsSinceLastEval++; environment.decayPheromones(); environment.updateMovingThreats(); const foodProxMap=new Map(); environment.food.forEach(f=>{const nearby=environment.spatialGrid.queryRadius(f.x,f.y,config.foodEvasionDistance); const blobs=[]; nearby.forEach(o=>{if(o.type==='blob'&&o.isAlive)blobs.push(o);}); if(blobs.length>0)foodProxMap.set(f,blobs);}); environment.updateFood(foodProxMap); environment.rebuildSpatialGrid(); let aliveCnt=0; population.forEach(blob=>{if(blob.isAlive){blob.update(environment); aliveCnt++;}}); updateAliveCount(aliveCnt); if(stepsSinceLastEval>=config.evaluationIntervalSteps){performEvolutionStep();}}
ctx.clearRect(0,0,CANVAS_WIDTH,CANVAS_HEIGHT); if(visPheromones)environment.drawPheromonesOffscreen(); environment.draw(ctx); population.forEach(blob=>{blob.draw(ctx);}); updateInfoPanel(); updateTooltip(); animationFrameId=requestAnimationFrame(gameLoop);
}
gameLoop.lastTimestamp = 0;
// --- UI Update Functions ---
function updateInfoPanel(){document.getElementById('simStepCount').textContent=simulationStep; document.getElementById('replacementCount').textContent=totalReplacements; document.getElementById('avgFitness').textContent=lastEvalStats.avgFitness.toFixed(3); document.getElementById('bestFitness').textContent=lastEvalStats.bestFitness.toFixed(3); document.getElementById('genome-display').textContent=`Best Sample (Phys): ${lastEvalStats.bestGenomeSample}`; document.getElementById('speciesCount').textContent=species.length; document.getElementById('avgMutationRate').textContent=lastEvalStats.avgMutRate.toFixed(4); document.getElementById('avgWeightMutationRate').textContent=lastEvalStats.avgWgtMutRate.toFixed(4); }
function updateAliveCount(count){document.getElementById('aliveCount').textContent=count;}
function updateTooltip(){ const rect=canvas.getBoundingClientRect(); const scaleX=CANVAS_WIDTH/rect.width; const scaleY=CANVAS_HEIGHT/rect.height; const cX=(mousePos.x-rect.left)*scaleX; const cY=(mousePos.y-rect.top)*scaleY; let foundBlob=null; let minDistSq=Infinity; const nearby=environment.spatialGrid.queryRadius(cX,cY,50); nearby.forEach(blob=>{if(blob.type!=='blob'||!blob.isAlive)return; const dSq=distanceSq(blob.x,blob.y,cX,cY); if(dSq<blob.currentRadius*blob.currentRadius&&dSq<minDistSq){minDistSq=dSq; foundBlob=blob;}}); if(foundBlob){const terrain=environment.getRegionProperties(foundBlob.x,foundBlob.y).terrain; const memoryStr=foundBlob.memoryState.map(m=>m.toFixed(2)).join(','); tooltip.style.display='block'; tooltip.style.left=`${mousePos.x+15}px`; tooltip.style.top=`${mousePos.y}px`; tooltip.innerHTML=`ID:${foundBlob.id.substring(0,5)} Spc:${foundBlob.speciesId?foundBlob.speciesId.substring(0,4):'N/A'}<br>E:${foundBlob.energy.toFixed(1)}/${config.maxEnergy.toFixed(0)} Fit(R):${foundBlob.rawFitness.toFixed(1)}(A:${foundBlob.adjustedFitness.toFixed(1)})<br>Age:${foundBlob.age} R:${foundBlob.currentRadius.toFixed(1)}(Max:${foundBlob.maxRadius.toFixed(1)})<br>Speed:${Math.sqrt(foundBlob.vx**2+foundBlob.vy**2).toFixed(2)}${foundBlob.shedBoostTimer>0?'(BOOST)':''}<br>Terr:${terrain} Mut:${foundBlob.mutationRate.toFixed(3)}/${foundBlob.weightMutationRate.toFixed(3)}<br>Mem:[${memoryStr}]`;} else {tooltip.style.display='none';}}
function updateSpeciesListUI(activeSpecies){ speciesListUL.innerHTML=''; activeSpecies.sort((a,b)=>b.avgFit-a.avgFit); activeSpecies.forEach(s=>{const li=document.createElement('li'); const avgRaw=s.members.reduce((sum,m)=>sum+m.rawFitness,0)/s.members.length||0; li.textContent=`ID:${s.id.substring(0,4)}|Mem:${s.members.length}|AvgAdj:${s.avgFit.toFixed(2)}|AvgRaw:${avgRaw.toFixed(2)}|Best:${s.bestFit.toFixed(2)}|Stag:${s.stag}`; speciesListUL.appendChild(li);}); if(activeSpecies.length===0){speciesListUL.innerHTML='<li>No active species.</li>';} }
// --- Control Functions ---
function startSimulation(){if(!isRunning){console.log("Starting...");isRunning=true;gameLoop.lastTimestamp=performance.now();animationFrameId=requestAnimationFrame(gameLoop)}} function stopSimulation(){if(isRunning){console.log("Stopping...");isRunning=false;if(animationFrameId){cancelAnimationFrame(animationFrameId);animationFrameId=null}}} function resetSimulation(){console.log("Resetting...");stopSimulation();simulationStep=0;stepsSinceLastEval=0;totalReplacements=0;species=[];lastEvalStats={avgFitness:0,bestFitness:0,bestGenomeSample:"N/A",avgMutRate:0,avgWgtMutRate:0};init();updateInfoPanel();updateAliveCount(0);document.getElementById('foodCountDisplay').textContent=config.initialFoodCount;document.getElementById('threatCountDisplay').textContent=config.initialThreatCount+config.initialMovingThreatCount;document.getElementById('pheromoneCountDisplay').textContent=0;updateSpeciesListUI([])}
// --- Initialization ---
function init() {
canvas=document.getElementById('simulationCanvas'); ctx=canvas.getContext('2d'); tooltip=document.getElementById('tooltip'); speciesListUL=document.getElementById('species-list');
canvas.width=CANVAS_WIDTH; canvas.height=CANVAS_HEIGHT; // Set canvas size directly
offscreenPheroCanvas=document.createElement('canvas'); offscreenPheroCanvas.width=CANVAS_WIDTH; offscreenPheroCanvas.height=CANVAS_HEIGHT; offscreenPheroCtx=offscreenPheroCanvas.getContext('2d');
environment=new Environment(CANVAS_WIDTH,CANVAS_HEIGHT);
population=[]; for(let i=0; i<config.populationSize; i++){population.push(new Blob());}
document.getElementById('populationSize').textContent=config.populationSize; updateAliveCount(config.populationSize);
const spdSlider=document.getElementById('speedControl'); const spdVal=document.getElementById('speedValue'); spdSlider.oninput=()=>{simSpeedMultiplier=parseInt(spdSlider.value);spdVal.textContent=`${simSpeedMultiplier}x`}; simSpeedMultiplier=parseInt(spdSlider.value);
visPheromones=document.getElementById('visPheromones').checked; visGrid=document.getElementById('visGrid').checked; simpleDraw=document.getElementById('simpleDraw').checked;
console.log("Hyper simulation initialized."); console.log(`NN Inputs:${config.nnInputNodes}(Ext:${config.nnExternalInputs},Mem:${config.nnMemoryNodes}), Outputs:${config.nnOutputNodes}(Ext:${config.nnExternalOutputs},Mem:${config.nnMemoryNodes}), Genome:${config.genomeLength}`); console.log(`Spatial Grid:${config.spatialGridCols}x${config.spatialGridRows}, Phero Grid:${config.pheromoneGridCols}x${config.pheromoneGridRows}`);
}
// --- Event Listeners ---
window.onload = () => {
init();
document.getElementById('startButton').addEventListener('click', startSimulation); document.getElementById('stopButton').addEventListener('click', stopSimulation); document.getElementById('resetButton').addEventListener('click', resetSimulation);
document.getElementById('addFoodButton').addEventListener('click', ()=>{for(let i=0;i<30;i++)environment.addFoodItem(); environment.rebuildSpatialGrid();});
document.getElementById('addThreatButton').addEventListener('click',()=>{for(let i=0;i<10;i++)environment.addThreatItem(); environment.rebuildSpatialGrid();});
document.getElementById('visPheromones').addEventListener('change',(e)=>{visPheromones=e.target.checked;}); document.getElementById('visGrid').addEventListener('change',(e)=>{visGrid=e.target.checked;}); document.getElementById('simpleDraw').addEventListener('change',(e)=>{simpleDraw=e.target.checked;});
canvas.addEventListener('mousemove',(e)=>{mousePos.x=e.clientX;mousePos.y=e.clientY;}); canvas.addEventListener('mouseout',()=>{tooltip.style.display='none';});
};
</script>
</body>
</html>