Add neat arena

This commit is contained in:
Peter Stockings
2026-01-12 08:58:45 +11:00
parent e9cb8b52df
commit 840e597413
39 changed files with 5717 additions and 193 deletions

214
src/lib/neatArena/genome.ts Normal file
View File

@@ -0,0 +1,214 @@
/**
* 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
*/
export interface Genome {
nodes: NodeGene[];
connections: ConnectionGene[];
fitness: number;
}
/**
* Global innovation tracker for historical markings
*/
export class InnovationTracker {
private currentInnovation: number = 0;
private innovationHistory: Map<string, number> = 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;
}
}
/**
* 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)
for (let i = 0; i < inputCount; i++) {
nodes.push({
id: i,
type: 'input',
activation: 'linear',
});
}
// Create output nodes (IDs starting from inputCount)
for (let i = 0; i < outputCount; i++) {
nodes.push({
id: inputCount + i,
type: 'output',
activation: 'tanh',
});
}
// Create fully connected minimal genome
for (let i = 0; i < inputCount; i++) {
const inputNode = i; // Assuming inputNode refers to the ID
for (let o = 0; o < outputCount; o++) {
const outputNode = inputCount + o; // Assuming outputNode refers to the ID
const innovation = innovationTracker.getInnovation(inputNode, outputNode);
connections.push({
innovation,
from: inputNode,
to: outputNode,
weight: (Math.random() * 4) - 2, // Random weight in [-2, 2] for initial diversity
enabled: true,
});
}
}
return {
nodes,
connections,
fitness: 0,
};
}
/**
* Clone a genome (deep copy)
*/
export function cloneGenome(genome: Genome): Genome {
return {
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<number, number[]>();
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<number>();
const recStack = new Set<number>();
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);
}