import { NeuralNetwork } from "./neuralNetwork.js"; import { randomRange, randomGaussian, clamp } from "./utils.js"; /** Neural network topology shared by all entities */ export const NET_TOPOLOGY = [14, 12, 8]; /** Entity archetype definitions */ export const ENTITY_TYPES = { gatherer: { name: "Gatherer", baseTraits: { speed: 2.5, size: 8, vision: 120, attack: 0.2, defense: 0.5, metabolism: 0.8, }, color: "#00ff88", fitnessWeights: { survival: 1.0, energy: 2.0, offspring: 1.5, kills: 0.0 }, description: "Herbivores that seek food resources", }, predator: { name: "Predator", baseTraits: { speed: 3.5, size: 10, vision: 140, attack: 1.5, defense: 0.3, metabolism: 1.2, }, color: "#ff3366", fitnessWeights: { survival: 0.5, energy: 1.0, offspring: 1.0, kills: 2.5 }, description: "Carnivores that hunt other entities", }, builder: { name: "Builder", baseTraits: { speed: 1.8, size: 9, vision: 100, attack: 0.1, defense: 1.5, metabolism: 0.6, }, color: "#3399ff", fitnessWeights: { survival: 2.0, energy: 1.5, offspring: 2.0, kills: 0.0 }, description: "Defensive entities that build and fortify", }, explorer: { name: "Explorer", baseTraits: { speed: 4.0, size: 6, vision: 180, attack: 0.3, defense: 0.3, metabolism: 1.0, }, color: "#ffaa00", fitnessWeights: { survival: 1.5, energy: 1.0, offspring: 1.0, kills: 0.5 }, description: "Fast scouts with wide vision range", }, hybrid: { name: "Hybrid", baseTraits: { speed: 3.0, size: 8, vision: 130, attack: 0.8, defense: 0.8, metabolism: 1.0, }, color: "#cc66ff", fitnessWeights: { survival: 1.0, energy: 1.0, offspring: 1.5, kills: 1.0 }, description: "Balanced generalists that adapt to conditions", }, }; /** Trait constraints to prevent runaway evolution */ const TRAIT_BOUNDS = { speed: { min: 0.5, max: 8.0 }, size: { min: 2.0, max: 14.0 }, vision: { min: 30, max: 250 }, attack: { min: 0.0, max: 3.0 }, defense: { min: 0.0, max: 3.0 }, metabolism: { min: 0.2, max: 2.5 }, }; /** * Genome encapsulates an entity's heritable traits and neural weights. */ export class Genome { /** * @param {string} type - Entity archetype key * @param {object} [traits] - Physical traits (or auto-generated) * @param {number[]} [neuralWeights] - Flat weight array (or auto-generated) */ constructor(type, traits = null, neuralWeights = null) { this.type = type; const base = ENTITY_TYPES[type].baseTraits; if (traits) { this.traits = { ...traits }; } else { this.traits = {}; for (const key of Object.keys(base)) { const variance = base[key] * 0.15; this.traits[key] = clamp( base[key] + randomGaussian(0, variance), TRAIT_BOUNDS[key].min, TRAIT_BOUNDS[key].max, ); } } if (neuralWeights) { this.neuralWeights = neuralWeights.slice(); } else { const tempNet = new NeuralNetwork(NET_TOPOLOGY); this.neuralWeights = tempNet.serialize(); } /** Communication symbol preference (0-7), evolvable */ this.signalPreference = traits ? traits._signal || Math.floor(Math.random() * 8) : Math.floor(Math.random() * 8); } /** Create a brain (NeuralNetwork) from this genome's stored weights */ buildBrain() { const nn = new NeuralNetwork(NET_TOPOLOGY); nn.deserialize(this.neuralWeights); return nn; } /** Deep copy */ clone() { const g = new Genome( this.type, { ...this.traits }, this.neuralWeights.slice(), ); g.signalPreference = this.signalPreference; return g; } /** * Single-point crossover of two genomes. * Child inherits type from the fitter parent. * @param {Genome} other * @param {string} [childType] - Override child type * @returns {Genome} */ crossover(other, childType = null) { const type = childType || this.type; const childTraits = {}; for (const key of Object.keys(this.traits)) { if (key === "_signal") continue; const t = Math.random(); const raw = this.traits[key] * t + other.traits[key] * (1 - t); childTraits[key] = clamp( raw, TRAIT_BOUNDS[key].min, TRAIT_BOUNDS[key].max, ); } const w1 = this.neuralWeights; const w2 = other.neuralWeights; const len = Math.min(w1.length, w2.length); const childWeights = new Array(len); const crossPoint = Math.floor(Math.random() * len); for (let i = 0; i < len; i++) { childWeights[i] = i < crossPoint ? w1[i] : w2[i]; } const child = new Genome(type, childTraits, childWeights); child.signalPreference = Math.random() < 0.5 ? this.signalPreference : other.signalPreference; return child; } /** * Apply mutations to traits and weights. * @param {number} traitRate - Probability per trait (e.g. 0.3) * @param {number} weightRate - Probability per weight (e.g. 0.05) * @param {number} magnitude - Gaussian std for weight mutation */ mutate(traitRate = 0.3, weightRate = 0.05, magnitude = 0.3) { const base = ENTITY_TYPES[this.type].baseTraits; for (const key of Object.keys(this.traits)) { if (key === "_signal") continue; if (Math.random() < traitRate) { const scale = base[key] * 0.1; this.traits[key] = clamp( this.traits[key] + randomGaussian(0, scale), TRAIT_BOUNDS[key].min, TRAIT_BOUNDS[key].max, ); } } for (let i = 0; i < this.neuralWeights.length; i++) { if (Math.random() < weightRate) { this.neuralWeights[i] += randomGaussian(0, magnitude); } } if (Math.random() < 0.05) { this.signalPreference = Math.floor(Math.random() * 8); } } /** Serialize to a plain object (for LocalStorage) */ toJSON() { return { type: this.type, traits: { ...this.traits }, neuralWeights: this.neuralWeights, signalPreference: this.signalPreference, }; } /** Deserialize from a plain object */ static fromJSON(data) { const g = new Genome(data.type, data.traits, data.neuralWeights); g.signalPreference = data.signalPreference || 0; return g; } } /** * Create a new random genome of a given type. * @param {string} type - Archetype key * @returns {Genome} */ export function createGenome(type) { return new Genome(type); } /** * Tournament selection: pick the fittest from a random subset. * @param {Array<{genome: Genome, fitness: number}>} population * @param {number} tournamentSize * @returns {Genome} */ export function tournamentSelect(population, tournamentSize = 3) { let best = null; let bestFitness = -Infinity; for (let i = 0; i < tournamentSize; i++) { const idx = Math.floor(Math.random() * population.length); const candidate = population[idx]; if (candidate.fitness > bestFitness) { best = candidate; bestFitness = candidate.fitness; } } return best.genome; } /** * Calculate fitness score for an entity's lifetime stats. * @param {string} type - Entity type * @param {object} stats - { survivalTime, energyGathered, offspring, kills } * @returns {number} */ export function calculateFitness(type, stats) { const w = ENTITY_TYPES[type].fitnessWeights; return ( w.survival * stats.survivalTime + w.energy * stats.energyGathered + w.offspring * stats.offspring * 10 + w.kills * stats.kills * 5 ); } /** * Measure genetic diversity as average pairwise trait distance. * @param {Genome[]} genomes * @returns {number} 0-1 normalized diversity score */ export function measureDiversity(genomes) { if (genomes.length < 2) return 0; const traitKeys = Object.keys(TRAIT_BOUNDS); let totalDist = 0; let pairs = 0; const sampleSize = Math.min(genomes.length, 30); for (let i = 0; i < sampleSize; i++) { for (let j = i + 1; j < sampleSize; j++) { let dist = 0; for (const key of traitKeys) { const range = TRAIT_BOUNDS[key].max - TRAIT_BOUNDS[key].min; const diff = Math.abs(genomes[i].traits[key] - genomes[j].traits[key]) / range; dist += diff * diff; } totalDist += Math.sqrt(dist / traitKeys.length); pairs++; } } return pairs > 0 ? totalDist / pairs : 0; } /** * Produce a child genome from the population using selection + crossover + mutation. * @param {Array<{genome: Genome, fitness: number}>} population * @returns {Genome} */ export function reproduce(population) { const parent1 = tournamentSelect(population, 3); const parent2 = tournamentSelect(population, 3); const child = parent1.crossover(parent2); child.mutate(0.25, 0.05, 0.2); return child; }