Felladrin's picture
Add the simulator files
17fd0c2 verified
import { World, EVENT_TYPES } from "./world.js";
import { EntityManager } from "./entityManager.js";
import { Entity } from "./entities.js";
import { ParticleSystem } from "./particles.js";
import { StatsTracker } from "./stats.js";
import { UI } from "./ui.js";
import { createGenome, ENTITY_TYPES } from "./genetics.js";
import { randomRange } from "./utils.js";
const SAVE_KEY = "digital-life-simulator-state";
const TARGET_FPS = 60;
const MIN_POPULATION = 15;
const MAX_POPULATION = 80;
const INITIAL_POPULATION = 35;
const GENERATION_INTERVAL = 45;
export class Ecosystem {
constructor() {
this.world = null;
this.entityManager = null;
this.particles = null;
this.stats = null;
this.ui = null;
this.simCanvas = null;
this.simCtx = null;
this.particleCanvas = null;
this.particleCtx = null;
this.running = false;
this._rafId = null;
this._lastTime = 0;
this._fpsAccum = 0;
this._fpsFrames = 0;
this._currentFps = 60;
this._genTimer = 0;
this._saveTimer = 0;
this._eventCheckTimer = 0;
this._lastEventCount = 0;
}
init() {
this.simCanvas = document.getElementById("simulation-canvas");
this.particleCanvas = document.getElementById("particle-canvas");
if (!this.simCanvas || !this.particleCanvas) {
console.error("Canvas elements not found");
return;
}
this.simCtx = this.simCanvas.getContext("2d");
this.particleCtx = this.particleCanvas.getContext("2d");
this._resizeCanvases();
window.addEventListener("resize", () => this._resizeCanvases());
const w = this.simCanvas.width;
const h = this.simCanvas.height;
const saved = this._loadState();
if (saved) {
this.world = World.fromJSON(saved.world);
this.stats = StatsTracker.fromJSON(saved.stats);
this.entityManager = new EntityManager(w, h);
if (saved.entities && saved.entities.length > 0) {
for (const ed of saved.entities) {
try {
const { Genome } = await_import_workaround();
} catch (_) {}
}
}
this.entityManager.spawnRandom(INITIAL_POPULATION, this.world);
} else {
this.world = new World(w, h);
this.stats = new StatsTracker();
this.entityManager = new EntityManager(w, h);
this.entityManager.spawnRandom(INITIAL_POPULATION, this.world);
}
this.particles = new ParticleSystem(2000);
const container = document.getElementById("app");
this.ui = new UI(container);
this.ui.init();
this.ui.addEvent("Ecosystem initialized", "milestone");
this.ui.announce("Digital Life Simulator Active");
const types = Object.keys(ENTITY_TYPES);
for (const type of types) {
const count = this.entityManager.entities.filter(
(e) => e.type === type,
).length;
if (count > 0) {
this.ui.addEvent(
`${ENTITY_TYPES[type].name}: ${count} spawned`,
"birth",
);
}
}
}
start() {
if (this.running) return;
this.running = true;
this._lastTime = performance.now();
this._loop(this._lastTime);
}
stop() {
this.running = false;
if (this._rafId) {
cancelAnimationFrame(this._rafId);
this._rafId = null;
}
}
_loop(timestamp) {
if (!this.running) return;
this._rafId = requestAnimationFrame((t) => this._loop(t));
const rawDt = (timestamp - this._lastTime) / 1000;
const dt = Math.min(rawDt, 0.05);
this._lastTime = timestamp;
this._fpsAccum += rawDt;
this._fpsFrames++;
if (this._fpsAccum >= 0.5) {
this._currentFps = this._fpsFrames / this._fpsAccum;
this._fpsFrames = 0;
this._fpsAccum = 0;
}
this._update(dt);
this._render();
}
_update(dt) {
this.world.update(dt);
this._eventCheckTimer += dt;
if (this._eventCheckTimer >= 1) {
this._eventCheckTimer = 0;
if (this.world.events.length > this._lastEventCount) {
const newEvent = this.world.events[this.world.events.length - 1];
this.ui.announceWorldEvent(newEvent);
this._lastEventCount = this.world.events.length;
}
if (this.world.events.length < this._lastEventCount) {
this._lastEventCount = this.world.events.length;
}
}
this.entityManager.update(dt, this.world, this.particles, this.stats);
this.particles.update(dt);
for (const event of this.world.events) {
if (event.type === "storm" || event.type === "bloom") {
this.particles.emitWeather(event, dt);
}
}
this._managePopulation(dt);
this._genTimer += dt;
if (this._genTimer >= GENERATION_INTERVAL) {
this._genTimer = 0;
this.stats.onGeneration();
this.ui.addEvent(
`Generation ${this.stats.generationCount} reached`,
"evolution",
);
this.ui.announce(`Generation ${this.stats.generationCount}`);
const genEl = document.getElementById("gen-count");
if (genEl) {
genEl.classList.remove("generation-flash");
void genEl.offsetWidth;
genEl.classList.add("generation-flash");
}
}
this.stats.record(
this.entityManager.entities,
this.world,
dt,
this._currentFps,
this.particles.count,
);
this._saveTimer += dt;
if (this._saveTimer >= 30) {
this._saveTimer = 0;
this._saveState();
}
this.ui.update(this.stats, this.world, this.entityManager, dt);
}
_render() {
const simCtx = this.simCtx;
const partCtx = this.particleCtx;
const w = this.world.width;
const h = this.world.height;
simCtx.fillStyle = "#06060f";
simCtx.fillRect(0, 0, w, h);
partCtx.clearRect(0, 0, w, h);
this.world.render(simCtx);
this._renderFrame = (this._renderFrame || 0) + 1;
if (this._renderFrame % 15 === 0) {
this.stats.renderHeatmap(simCtx, w, h);
}
this.entityManager.render(simCtx);
this.particles.render(partCtx);
}
_managePopulation(dt) {
const alive = this.entityManager.entities.length;
if (alive < MIN_POPULATION) {
const deficit = MIN_POPULATION - alive;
const toSpawn = Math.min(deficit, 5);
if (alive >= 2) {
this.entityManager.spawnFromPopulation(
this.entityManager.entities,
toSpawn,
this.world,
);
this.ui.addEvent(
`${toSpawn} entities evolved from survivors`,
"evolution",
);
} else {
this.entityManager.spawnRandom(toSpawn, this.world);
this.ui.addEvent(`${toSpawn} new entities seeded`, "birth");
}
}
this._diversityTimer = (this._diversityTimer || 0) + dt;
if (this._diversityTimer >= 8) {
this._diversityTimer = 0;
const types = Object.keys(ENTITY_TYPES);
const typeCounts = {};
for (const t of types) typeCounts[t] = 0;
for (const e of this.entityManager.entities) {
if (e.alive) typeCounts[e.type]++;
}
for (const type of types) {
if (typeCounts[type] < 2 && alive < MAX_POPULATION - 3) {
const count = Math.min(3, MAX_POPULATION - alive);
for (let i = 0; i < count; i++) {
const genome = createGenome(type);
const x = randomRange(40, this.world.width - 40);
const y = randomRange(40, this.world.height - 40);
const entity = new Entity(x, y, genome, this.stats.generationCount);
this.entityManager.add(entity);
}
this.ui.addEvent(
`${ENTITY_TYPES[type].name} migration wave (${count})`,
"birth",
);
}
}
}
if (alive > MAX_POPULATION) {
const excess = alive - MAX_POPULATION;
const sorted = [...this.entityManager.entities].sort(
(a, b) => a.fitness - b.fitness,
);
for (let i = 0; i < excess && i < sorted.length; i++) {
sorted[i].alive = false;
sorted[i].energy = 0;
}
}
}
_resizeCanvases() {
const w = window.innerWidth;
const h = window.innerHeight;
const dpr = Math.min(window.devicePixelRatio || 1, 2);
for (const canvas of [this.simCanvas, this.particleCanvas]) {
if (!canvas) continue;
canvas.width = w * dpr;
canvas.height = h * dpr;
canvas.style.width = w + "px";
canvas.style.height = h + "px";
const ctx = canvas.getContext("2d");
ctx.scale(dpr, dpr);
}
if (this.world) {
this.world.width = w;
this.world.height = h;
}
if (this.entityManager) {
this.entityManager.spatialGrid = new (Object.getPrototypeOf(
this.entityManager.spatialGrid,
).constructor)(w, h, 60);
}
}
_saveState() {
try {
const state = {
world: this.world.toJSON(),
stats: this.stats.toJSON(),
version: 1,
};
localStorage.setItem(SAVE_KEY, JSON.stringify(state));
} catch (e) {}
}
_loadState() {
try {
const raw = localStorage.getItem(SAVE_KEY);
if (!raw) return null;
return JSON.parse(raw);
} catch (e) {
return null;
}
}
}
function await_import_workaround() {
return null;
}
export function bootstrap() {
const eco = new Ecosystem();
eco.init();
eco.start();
window.__ecosystem = eco;
return eco;
}