/** * NEAT Genome Implementation * * Represents a neural network genome with node genes and connection genes. * Implements the core NEAT genome structure as described in the original paper. */ export type NodeType = 'input' | 'hidden' | 'output'; export type ActivationFunction = 'tanh' | 'sigmoid' | 'relu' | 'linear'; /** * Node gene - represents a neuron */ export interface NodeGene { id: number; type: NodeType; activation: ActivationFunction; } /** * Connection gene - represents a synapse */ export interface ConnectionGene { innovation: number; from: number; to: number; weight: number; enabled: boolean; } /** * Complete genome */ /** * Complete genome */ export interface Genome { id: number; nodes: NodeGene[]; connections: ConnectionGene[]; fitness: number; } /** * Global innovation tracker for historical markings */ export class InnovationTracker { private currentInnovation: number = 0; private innovationHistory: Map = new Map(); /** * Get or create innovation number for a connection */ getInnovation(from: number, to: number): number { const key = `${from}->${to}`; if (this.innovationHistory.has(key)) { return this.innovationHistory.get(key)!; } const innovation = this.currentInnovation++; this.innovationHistory.set(key, innovation); return innovation; } /** * Reset innovation tracking (useful for new experiments) */ reset(): void { this.currentInnovation = 0; this.innovationHistory.clear(); } /** * Get current innovation count */ getCurrentInnovation(): number { return this.currentInnovation; } } let nextGenomeId = 0; /** * Create a minimal genome with only input and output nodes, fully connected */ export function createMinimalGenome( inputCount: number, outputCount: number, innovationTracker: InnovationTracker ): Genome { const nodes: NodeGene[] = []; const connections: ConnectionGene[] = []; // Create input nodes (IDs 0 to inputCount-1) // PLUS one extra for Bias for (let i = 0; i < inputCount + 1; i++) { nodes.push({ id: i, type: 'input', activation: 'linear', }); } // Create output nodes (IDs starting from inputCount + 1) // Fix: Bias node uses ID `inputCount`, so outputs must start at `inputCount + 1` for (let i = 0; i < outputCount; i++) { nodes.push({ id: inputCount + 1 + i, type: 'output', activation: 'tanh', }); } // Create fully connected minimal genome // Iterate through all inputs INCLUDING Bias for (let i = 0; i < inputCount + 1; i++) { const inputNode = i; for (let o = 0; o < outputCount; o++) { const outputNode = inputCount + 1 + o; // target the shifted output IDs const innovation = innovationTracker.getInnovation(inputNode, outputNode); let weight = (Math.random() * 2.0) - 1.0; // FORCE AGGRESSION: // If connection is from BIAS node (index == inputCount) TO SHOOT node (index 3 of output) // Warning: Output indices are 0..4 relative to output block. // Shoot is 4th output (moveX, moveY, turn, shoot, reserved). if (inputNode === inputCount && o === 3) { weight = 1.0 + Math.random(); // Range [1.0, 2.0] -> Strong Positive Bias } connections.push({ innovation, from: inputNode, to: outputNode, weight, enabled: true, }); } } return { id: nextGenomeId++, nodes, connections, fitness: 0, }; } /** * Clone a genome (deep copy) */ export function cloneGenome(genome: Genome): Genome { return { id: nextGenomeId++, nodes: genome.nodes.map(n => ({ ...n })), connections: genome.connections.map(c => ({ ...c })), fitness: genome.fitness, }; } /** * Get next available node ID */ export function getNextNodeId(genome: Genome): number { return Math.max(...genome.nodes.map(n => n.id)) + 1; } /** * Check if a connection already exists */ export function connectionExists(genome: Genome, from: number, to: number): boolean { return genome.connections.some(c => c.from === from && c.to === to); } /** * Check if adding a connection would create a cycle (for feedforward networks) */ export function wouldCreateCycle(genome: Genome, from: number, to: number): boolean { // Build adjacency list const adj = new Map(); for (const node of genome.nodes) { adj.set(node.id, []); } for (const conn of genome.connections) { if (!conn.enabled) continue; if (!adj.has(conn.from)) adj.set(conn.from, []); adj.get(conn.from)!.push(conn.to); } // Add the proposed connection if (!adj.has(from)) adj.set(from, []); adj.get(from)!.push(to); // DFS to detect cycle const visited = new Set(); const recStack = new Set(); const hasCycle = (nodeId: number): boolean => { visited.add(nodeId); recStack.add(nodeId); const neighbors = adj.get(nodeId) || []; for (const neighbor of neighbors) { if (!visited.has(neighbor)) { if (hasCycle(neighbor)) return true; } else if (recStack.has(neighbor)) { return true; } } recStack.delete(nodeId); return false; }; // Check from the 'from' node return hasCycle(from); } /** * Serialize genome to JSON */ export function serializeGenome(genome: Genome): string { return JSON.stringify(genome, null, 2); } /** * Deserialize genome from JSON */ export function deserializeGenome(json: string): Genome { return JSON.parse(json); }