| | import { NeuralNetwork } from "./neuralNetwork.js"; |
| | import { randomRange, randomGaussian, clamp } from "./utils.js"; |
| |
|
| | |
| | export const NET_TOPOLOGY = [14, 12, 8]; |
| |
|
| | |
| | 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", |
| | }, |
| | }; |
| |
|
| | |
| | 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 }, |
| | }; |
| |
|
| | |
| | |
| | |
| | export class Genome { |
| | |
| | |
| | |
| | |
| | |
| | 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(); |
| | } |
| |
|
| | |
| | this.signalPreference = traits |
| | ? traits._signal || Math.floor(Math.random() * 8) |
| | : Math.floor(Math.random() * 8); |
| | } |
| |
|
| | |
| | buildBrain() { |
| | const nn = new NeuralNetwork(NET_TOPOLOGY); |
| | nn.deserialize(this.neuralWeights); |
| | return nn; |
| | } |
| |
|
| | |
| | clone() { |
| | const g = new Genome( |
| | this.type, |
| | { ...this.traits }, |
| | this.neuralWeights.slice(), |
| | ); |
| | g.signalPreference = this.signalPreference; |
| | return g; |
| | } |
| |
|
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | 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; |
| | } |
| |
|
| | |
| | |
| | |
| | |
| | |
| | |
| | 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); |
| | } |
| | } |
| |
|
| | |
| | toJSON() { |
| | return { |
| | type: this.type, |
| | traits: { ...this.traits }, |
| | neuralWeights: this.neuralWeights, |
| | signalPreference: this.signalPreference, |
| | }; |
| | } |
| |
|
| | |
| | static fromJSON(data) { |
| | const g = new Genome(data.type, data.traits, data.neuralWeights); |
| | g.signalPreference = data.signalPreference || 0; |
| | return g; |
| | } |
| | } |
| |
|
| | |
| | |
| | |
| | |
| | |
| | export function createGenome(type) { |
| | return new Genome(type); |
| | } |
| |
|
| | |
| | |
| | |
| | |
| | |
| | |
| | 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; |
| | } |
| |
|
| | |
| | |
| | |
| | |
| | |
| | |
| | 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 |
| | ); |
| | } |
| |
|
| | |
| | |
| | |
| | |
| | |
| | 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; |
| | } |
| |
|
| | |
| | |
| | |
| | |
| | |
| | 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; |
| | } |
| |
|