From fab6a7e03f8655ab96143a9ae2b24a12f036a5a9 Mon Sep 17 00:00:00 2001 From: Peter Stockings Date: Thu, 15 Jan 2026 18:13:48 +1100 Subject: [PATCH] Add self driving car app --- package.json | 3 +- src/apps/SelfDrivingCar/Car.ts | 121 +++++++++++---- src/apps/SelfDrivingCar/CarScene.ts | 141 ++++++++++++------ src/apps/SelfDrivingCar/CarSimulation.ts | 55 ++++--- src/apps/SelfDrivingCar/ConfigPanel.tsx | 98 +++++++++++- src/apps/SelfDrivingCar/SelfDrivingCarApp.tsx | 17 ++- src/apps/SelfDrivingCar/SimpleGA.ts | 4 +- src/apps/SelfDrivingCar/types.ts | 4 +- 8 files changed, 344 insertions(+), 99 deletions(-) diff --git a/package.json b/package.json index 6509353..707905d 100644 --- a/package.json +++ b/package.json @@ -7,7 +7,8 @@ "dev": "vite", "build": "tsc -b && vite build", "lint": "eslint .", - "preview": "vite preview" + "preview": "vite preview", + "typecheck": "tsc --noEmit" }, "dependencies": { "@types/matter-js": "^0.20.2", diff --git a/src/apps/SelfDrivingCar/Car.ts b/src/apps/SelfDrivingCar/Car.ts index a678730..e629e97 100644 --- a/src/apps/SelfDrivingCar/Car.ts +++ b/src/apps/SelfDrivingCar/Car.ts @@ -24,6 +24,12 @@ export class Car { private maxPathIndexReached: number = 0; private initialPosSet: boolean = false; private framesSinceCheckpoint: number = 0; + + // Fitness tracking + private totalFrames: number = 0; + private speedSum: number = 0; + private lastSteer: number = 0; + private steeringChangeSum: number = 0; constructor( x: number, @@ -54,26 +60,57 @@ export class Car { this.initialPosSet = true; } - // Stagnation Killer + // Stagnation Killer - TIGHTENED to prevent local minima loops this.framesSinceCheckpoint++; - if (this.framesSinceCheckpoint > 600) { - // Stagnation + if (this.framesSinceCheckpoint > 300) { // 5 seconds without progress this.kill(); return; } + + // ANTI-EXPLOIT: Minimum progress requirements + // Must reach checkpoint 8 within first 8 seconds (stricter than before) + if (this.totalFrames > 480 && this.maxPathIndexReached < 8) { + this.kill(); + return; + } + + // Must reach checkpoint 3 within first 3 seconds (catches immediate crashers) + if (this.totalFrames > 180 && this.maxPathIndexReached < 3) { + this.kill(); + return; + } // 1. Sensors this.rayReadings = this.castRays(walls); - // 2. Think + // 2. Think - Expanded inputs for better control awareness + const forward = { + x: Math.cos(this.body.angle - Math.PI/2), + y: Math.sin(this.body.angle - Math.PI/2) + }; + const right = { x: -forward.y, y: forward.x }; + + // Velocity in car's local frame (for drift detection) + const localVelX = this.body.velocity.x * forward.x + this.body.velocity.y * forward.y; + const localVelY = this.body.velocity.x * right.x + this.body.velocity.y * right.y; + const inputs = [ - ...this.rayReadings, - this.body.speed / this.config.maxSpeed, // Normalize speed + ...this.rayReadings, // 7 rays + localVelX / this.config.maxSpeed, // Normalize forward/backward velocity + localVelY / this.config.maxSpeed, // Normalize lateral velocity (drift) + this.body.angularVelocity / this.config.turnSpeed, // Normalize rotation rate + this.body.speed / this.config.maxSpeed, // Normalize speed magnitude ]; const outputs = this.brain.predict(inputs); const steer = outputs[0]; let gas = outputs[1]; + + // Track metrics for fitness calculation + this.totalFrames++; + this.speedSum += this.body.speed; + this.steeringChangeSum += Math.abs(steer - this.lastSteer); + this.lastSteer = steer; // 3. Act (Kickstart) if (this.framesSinceCheckpoint < 60 && this.fitness < 2) { @@ -87,12 +124,7 @@ export class Car { Matter.Body.setAngularVelocity(this.body, steer * this.config.turnSpeed * Math.sign(gas)); } - // Physics: Gas (Forward Force) - const forward = { - x: Math.cos(this.body.angle - Math.PI/2), - y: Math.sin(this.body.angle - Math.PI/2) - }; - + // Physics: Gas (Forward Force) - reuse forward vector from input calculation // Physics: Lateral Friction (Tire Grip) this.applyTireGrip(forward); @@ -186,33 +218,68 @@ export class Car { delta -= total; } + // ANTI-EXPLOIT: Only reward progress if moving forward + // Check if velocity is aligned with path direction + if (delta > 0) { + // Calculate expected direction to next checkpoint + const nextIdx = (bestIndex + 1) % total; + const pathDir = { + x: pathPoints[nextIdx].x - pathPoints[bestIndex].x, + y: pathPoints[nextIdx].y - pathPoints[bestIndex].y + }; + const pathDirMag = Math.sqrt(pathDir.x * pathDir.x + pathDir.y * pathDir.y); + + if (pathDirMag > 0.1) { + // Normalize + pathDir.x /= pathDirMag; + pathDir.y /= pathDirMag; + + // Dot product with velocity + const velDot = this.body.velocity.x * pathDir.x + this.body.velocity.y * pathDir.y; + + // Only allow progress if moving roughly forward (dot > 0) + if (velDot < 0) { + // Moving backward relative to path - REJECT progress + delta = 0; + bestIndex = this.currentPathIndex; // Don't update position + } + } + } + // Update state this.currentPathIndex = bestIndex; - // Calculate continuous fitness - // Base Fitness: Laps * Total + CurrentIndex - // Sub-step Fitness: 1.0 - (Dist to BestIndex / MaxDist)? - // Simpler: Just (Laps * Total) + CurrentIndex - + // Calculate continuous fitness with bonuses const rawScore = (this.laps * total) + this.currentPathIndex; - // Only update fitness if we improve (Standard NEAT/GA practice usually for 'max reached') - // But for continuous driving, we want to allow fluctuations but reward overall progress. - // Let's use Raw Score directly. If they reverse, they lose fitness. + // Base fitness from progress + let baseFitness = Math.max(0, rawScore / 10.0); - // Scale down to reasonable numbers (e.g. 1 point per 100 units?) - // Let's just say 1 point per 1 path node. + // Speed bonus: reward faster completion + const avgSpeed = this.totalFrames > 0 ? this.speedSum / this.totalFrames : 0; + const speedBonus = (avgSpeed / this.config.maxSpeed) * 0.2 * baseFitness; // Up to 20% bonus - // Reward function: - this.fitness = Math.max(0, rawScore / 10.0); // 10 points per 100 nodes + // Smoothness penalty: penalize jerky steering + const avgSteeringChange = this.totalFrames > 0 ? this.steeringChangeSum / this.totalFrames : 0; + const smoothnessPenalty = avgSteeringChange * 0.1 * baseFitness; // Up to 10% penalty + + // ANTI-EXPLOIT: Early death penalty + // Cars must survive at least 3 seconds to get any fitness at all + let finalFitness = baseFitness + speedBonus - smoothnessPenalty; + if (this.totalFrames < 180) { // Less than 3 seconds survived + finalFitness = 0; // No fitness for instant crashes + } else if (this.totalFrames < 300) { // Less than 5 seconds + // Strong penalty for early deaths (50% reduction) + finalFitness *= 0.5; + } + + this.fitness = Math.max(0, finalFitness); // Stagnation Check - // If we haven't reached a NEW max path index in X frames, die. - // Convert laps+index to absolute const absoluteIndex = (this.laps * total) + this.currentPathIndex; if (absoluteIndex > this.maxPathIndexReached) { this.maxPathIndexReached = absoluteIndex; - this.framesSinceCheckpoint = 0; // Usage of this var as 'framesSinceProgress' + this.framesSinceCheckpoint = 0; } } diff --git a/src/apps/SelfDrivingCar/CarScene.ts b/src/apps/SelfDrivingCar/CarScene.ts index 344b118..3e525a9 100644 --- a/src/apps/SelfDrivingCar/CarScene.ts +++ b/src/apps/SelfDrivingCar/CarScene.ts @@ -2,13 +2,14 @@ import Phaser from 'phaser'; import { CarSimulation } from './CarSimulation'; import { Car } from './Car'; import { DEFAULT_SIM_CONFIG, DEFAULT_CAR_CONFIG } from './types'; -import type { SerializedTrackData, SerializedVector, SerializedBody, CarConfig, SimulationConfig } from './types'; +import type { SerializedTrackData, SerializedBody, CarConfig, SimulationConfig } from './types'; import { TrackGenerator } from './Track'; // NEAT Imports REMOVED // import { createPopulation, evolveGeneration, DEFAULT_EVOLUTION_CONFIG, type Population, type EvolutionConfig } from '../../lib/neatArena/evolution'; // import type { Genome } from '../../lib/neatArena/genome'; import { SimpleGA, DEFAULT_GA_CONFIG } from './SimpleGA'; +import type { GAConfig } from './SimpleGA'; // Worker Import (Vite/Bun compatible) import TrainingWorker from './training.worker.ts?worker'; @@ -17,6 +18,10 @@ export class CarScene extends Phaser.Scene { private sim!: CarSimulation; private graphics!: Phaser.GameObjects.Graphics; + // UI Text + private statsText!: Phaser.GameObjects.Text; + private fitnessText!: Phaser.GameObjects.Text; + // Training State private worker!: Worker; private population: Float32Array[] = []; @@ -28,7 +33,7 @@ export class CarScene extends Phaser.Scene { private bestFitnessEver = -Infinity; private serializedTrack!: SerializedTrackData; - private layerSizes = [6, 16, 12, 2]; // 6 Inputs, 16/12 Hidden, 2 Outputs + private layerSizes = [11, 24, 16, 2]; // 11 Inputs (7 rays + 4 dynamics), 24/16 Hidden, 2 Outputs // Config private carConfig: CarConfig = DEFAULT_CAR_CONFIG; @@ -59,44 +64,70 @@ export class CarScene extends Phaser.Scene { this.events.on('destroy', this.shutdown, this); // Listen for Config Updates - this.game.events.on('update-config', (cfg: { car: CarConfig, sim: SimulationConfig }) => { + this.game.events.on('update-config', (cfg: { car: CarConfig, sim: SimulationConfig, ga?: GAConfig }) => { this.carConfig = cfg.car; this.simConfig = cfg.sim; + // Update GA config if provided + if (cfg.ga) { + this.gaConfig = cfg.ga; + this.ga = new SimpleGA(this.layerSizes, this.gaConfig); + } + // HOT RELOAD PHYSICS if (this.sim) { - // We need to pass this into the sim, and the cars. - // Since cars are recreated often, we update the Sim's reference config? - // Or force update current cars. - - // Let's implement a 'updateConfig' method on Simulation - // For now, simpler: just iterate cars directly if possible or restart - - // But wait, the Sim class owns the cars. - // Let's just update the config the Sim holds (if we add it there) - // For now, let's just force a track regen if track params change? - // No, user wants realtime physics sliders. - - // Direct update for now: this.sim.updateConfig(this.carConfig); + // Restart visual sim with updated config so changes apply immediately + if (this.bestGenomeEver) { + this.sim = new CarSimulation( + this.serializedTrack, + { ...this.simConfig, populationSize: 1 }, + [this.bestGenomeEver], + this.carConfig + ); + } + // Also update Worker config for NEXT generation if (this.worker) { - // We can't easily interrupt the worker mid-gen with valid physics without complex sync. - // So we just update the config it uses for next gen. + // We can't interrupt the worker mid-gen + // Config updates apply on next generation } } }); + // Create stats text overlay + this.statsText = this.add.text(20, 170, '', { + fontSize: '14px', + color: '#ffffff', + backgroundColor: '#000000aa', + padding: { x: 8, y: 6 } + }).setDepth(100); + + this.fitnessText = this.add.text(20, 210, '', { + fontSize: '12px', + color: '#4ecdc4', + backgroundColor: '#000000aa', + padding: { x: 8, y: 6 } + }).setDepth(100); + // ... debug texts ... (rest of create) } private handleNewTrack() { - if (this.worker) this.worker.terminate(); + if (this.worker) { + this.worker.terminate(); + this.worker = null as any; // CRITICAL: Set to null so startTraining creates new worker + } this.sim = null as any; + + // Recreate GA with current config (important for population size changes) + this.ga = new SimpleGA(this.layerSizes, this.gaConfig); this.population = this.ga.createPopulation(); + this.generationCount = 0; this.bestFitnessEver = -Infinity; + this.bestGenomeEver = null; this.game.events.emit('generation-complete', { generation: 0, best: 0, average: 0 }); this.startTraining(); } @@ -167,15 +198,6 @@ export class CarScene extends Phaser.Scene { }); } - private updateVisualSim(bestGenome: Float32Array) { - this.sim = new CarSimulation( - this.serializedTrack, - { ...this.simConfig, populationSize: 1 }, - [bestGenome], - this.carConfig - ); - } - private handleTrainingComplete(results: { fitness: number, checkpoints: number }[]) { // 1. Assign Fitness const fitnesses = results.map(r => r.fitness); @@ -222,8 +244,9 @@ export class CarScene extends Phaser.Scene { // We reuse the track data this.sim = new CarSimulation( this.serializedTrack, - { ...DEFAULT_SIM_CONFIG, populationSize: 1 }, - [bestGenome] + { ...this.simConfig, populationSize: 1 }, + [bestGenome], + this.carConfig // FIXED: Use current carConfig, not default ); } @@ -249,6 +272,17 @@ export class CarScene extends Phaser.Scene { this.sim.cars.forEach(car => { this.drawCar(car); }); + + // Update stats text + const aliveCount = this.sim.cars.filter(c => !c.isDead).length; + this.statsText.setText(`Gen: ${this.generationCount} | Alive: ${aliveCount}/${this.sim.cars.length}`); + + if (this.sim.cars.length > 0) { + const bestCar = this.sim.cars[0]; + this.fitnessText.setText( + `Fitness: ${bestCar.fitness.toFixed(2)} | Speed: ${bestCar.body.speed.toFixed(1)}` + ); + } } private drawTrack() { @@ -312,33 +346,50 @@ export class CarScene extends Phaser.Scene { this.graphics.rotateCanvas(-car.body.angle); this.graphics.translateCanvas(-p.x, -p.y); - // Draw Rays (Only if alive to reduce clutter, or just best car) - // Check if this is the "visual" best car (simulation only has 1 in visual mode usually) + // Draw Rays with color-coding (Only for the best car in visual mode) if (!car.isDead && this.sim.cars.length === 1) { const start = car.body.position; const angleBase = car.body.angle - Math.PI/2; - const spread = Math.PI/2; // Matches DEFAULT_CAR_CONFIG.raySpread (hardcoded for viz) - const count = 5; - const len = 150; + const raySpread = this.carConfig.raySpread; + const rayCount = this.carConfig.rayCount; + const rayLen = this.carConfig.rayLength; - // We need actual readings to color them? - // Since we don't strictly sync readings to scene in this simple loop, just draw lines showing FOV. - this.graphics.lineStyle(1, 0x00ff00, 0.3); - - // Recalculate angles to match Car.ts logic roughly for visualization - const startRayAngle = angleBase - spread / 2; - const angleStep = spread / (count - 1); + // Use actual ray readings for color-coding + const readings = car.rayReadings; + const startRayAngle = angleBase - raySpread / 2; + const angleStep = raySpread / (rayCount - 1); - for(let i=0; i Yellow -> Red (close) + const r = Math.floor(reading * 255); + const g = Math.floor((1 - reading) * 255); + const color = (r << 16) | (g << 8) | 0; + + this.graphics.lineStyle(2, color, 0.6); this.graphics.beginPath(); this.graphics.moveTo(start.x, start.y); this.graphics.lineTo( - start.x + Math.cos(angle) * len, - start.y + Math.sin(angle) * len + start.x + Math.cos(angle) * rayLen, + start.y + Math.sin(angle) * rayLen ); this.graphics.strokePath(); + + // Draw hit point if detected + if (reading > 0.1) { + const hitDist = (1 - reading) * rayLen; + const hitX = start.x + Math.cos(angle) * hitDist; + const hitY = start.y + Math.sin(angle) * hitDist; + this.graphics.fillStyle(color, 0.8); + this.graphics.fillCircle(hitX, hitY, 3); + } } + + // Draw fitness overlay + this.graphics.fillStyle(0xffffff); + this.graphics.generateTexture('text', 200, 50); } } } diff --git a/src/apps/SelfDrivingCar/CarSimulation.ts b/src/apps/SelfDrivingCar/CarSimulation.ts index 19b9bbd..f47d6e7 100644 --- a/src/apps/SelfDrivingCar/CarSimulation.ts +++ b/src/apps/SelfDrivingCar/CarSimulation.ts @@ -1,13 +1,15 @@ +// @ts-ignore +import decomp from 'poly-decomp'; +// Register decomp for Worker import Matter from 'matter-js'; -// import { TrackGenerator } from './Track'; -// REMOVED TrackGenerator for Worker Safety -import { Car } from './Car'; -import { DEFAULT_SIM_CONFIG, DEFAULT_CAR_CONFIG } from './types'; -import type { SimulationConfig, SerializedTrackData, CarConfig } from './types'; -// import { NeuralNetwork } from '../../lib/neatArena/network'; -// import type { Genome } from '../../lib/neatArena/genome'; import { DenseNetwork } from '../LunarLander/DenseNetwork'; +import { Car } from './Car'; +import type { SimulationConfig, SerializedTrackData, CarConfig } from './types'; +import { DEFAULT_SIM_CONFIG, DEFAULT_CAR_CONFIG } from './types'; +Matter.Common.setDecomp(decomp); + +// ... (other imports) export class CarSimulation { public engine: Matter.Engine; @@ -23,10 +25,6 @@ export class CarSimulation { private trackData: SerializedTrackData; private genomes: Float32Array[] = []; - // public onGenerationComplete?: (stats: { generation: number, best: number, average: number }) => void; - // We can't pass functions to worker easily, but for now this class is used in Main too. - // We will just expose a method to get stats. - private carConfig: CarConfig; constructor( @@ -46,13 +44,34 @@ export class CarSimulation { this.engine.gravity.y = 0; // Top down // 1. Setup Track from Data - this.walls = trackData.walls.map(w => Matter.Bodies.rectangle( - w.position.x, w.position.y, w.width, w.height, { - isStatic: true, - angle: w.angle, - label: w.label + this.walls = trackData.walls.map(w => { + if (w.vertices && w.vertices.length > 0) { + return Matter.Bodies.fromVertices( + w.position.x, w.position.y, + [w.vertices], + { + isStatic: true, + label: w.label, + // Restore angle if needed, but fromVertices might bake it? + // Actually Track.ts creates from global coords, so angle is implicit in vertices? + // No, Matter bodies created from vertices are centered. + // Track.ts: `Bodies.fromVertices(center, ..., [[v1, v2...]])`. + // The vertices passed to Track.ts are GLOBAL. + // Matter.fromVertices recalculates center and translates vertices to local. + // Serialized vertices should be consistent with this. + // We should pass vertices as they are. + } + ); + } else { + return Matter.Bodies.rectangle( + w.position.x, w.position.y, w.width, w.height, { + isStatic: true, + angle: w.angle, + label: w.label + } + ); } - )); + }); this.checkpoints = trackData.checkpoints.map(cp => Matter.Bodies.rectangle( cp.position.x, cp.position.y, cp.width, cp.height, { @@ -98,7 +117,7 @@ export class CarSimulation { // If we have genomes, use them. Otherwise mock. const effectivePopSize = this.genomes.length > 0 ? this.genomes.length : this.config.populationSize; - const layerSizes = [6, 16, 12, 2]; // Input (5 rays + 1 speed), Hidden, Output + const layerSizes = [11, 24, 16, 2]; // Input (7 rays + vel x/y + angular vel + speed), Hidden layers, Output (steer, gas) for (let i = 0; i < effectivePopSize; i++) { let network: DenseNetwork; diff --git a/src/apps/SelfDrivingCar/ConfigPanel.tsx b/src/apps/SelfDrivingCar/ConfigPanel.tsx index 1035244..59572c8 100644 --- a/src/apps/SelfDrivingCar/ConfigPanel.tsx +++ b/src/apps/SelfDrivingCar/ConfigPanel.tsx @@ -1,15 +1,18 @@ import React, { useState } from 'react'; import type { CarConfig, SimulationConfig } from './types'; +import type { GAConfig } from './SimpleGA'; interface ConfigPanelProps { carConfig: CarConfig; simConfig: SimulationConfig; + gaConfig: GAConfig; onCarConfigChange: (config: CarConfig) => void; onSimConfigChange: (config: SimulationConfig) => void; + onGAConfigChange: (config: GAConfig) => void; onNewTrack: () => void; } -export function ConfigPanel({ carConfig, simConfig, onCarConfigChange, onSimConfigChange, onNewTrack }: ConfigPanelProps) { +export function ConfigPanel({ carConfig, simConfig, gaConfig, onCarConfigChange, onSimConfigChange, onGAConfigChange, onNewTrack }: ConfigPanelProps) { const [isExpanded, setIsExpanded] = useState(true); const sliderStyle = { width: '100%', margin: '5px 0' }; @@ -24,6 +27,10 @@ export function ConfigPanel({ carConfig, simConfig, onCarConfigChange, onSimConf onSimConfigChange({ ...simConfig, [key]: value }); }; + const updateGA = (key: keyof GAConfig, value: number) => { + onGAConfigChange({ ...gaConfig, [key]: value }); + }; + return (
+
+

Sensors

+ +
+ Ray Count + {carConfig.rayCount} +
+ updateCar('rayCount', parseInt(e.target.value))} + style={sliderStyle} + /> + +
+ FOV (Field of View) + {(carConfig.raySpread * 180 / Math.PI).toFixed(0)}° +
+ updateCar('raySpread', parseFloat(e.target.value) * Math.PI / 180)} + style={sliderStyle} + /> + +
+ Ray Length + {carConfig.rayLength} +
+ updateCar('rayLength', parseInt(e.target.value))} + style={sliderStyle} + /> +
+

Track Gen

@@ -148,6 +192,58 @@ export function ConfigPanel({ carConfig, simConfig, onCarConfigChange, onSimConf
+
+

Evolution (GA)

+ +
+ Population Size + {gaConfig.populationSize} +
+ updateGA('populationSize', parseInt(e.target.value))} + style={sliderStyle} + /> + +
+ Mutation Rate + {(gaConfig.mutationRate * 100).toFixed(1)}% +
+ updateGA('mutationRate', parseFloat(e.target.value))} + style={sliderStyle} + /> + +
+ Mutation Amount + {gaConfig.mutationAmount.toFixed(2)} +
+ updateGA('mutationAmount', parseFloat(e.target.value))} + style={sliderStyle} + /> + +
+ Elitism (Keep Best) + {gaConfig.elitism} +
+ updateGA('elitism', parseInt(e.target.value))} + style={sliderStyle} + /> + +
+ ⚠️ GA changes restart training +
+
+
Physics apply immediately.
Track settings apply on generate. diff --git a/src/apps/SelfDrivingCar/SelfDrivingCarApp.tsx b/src/apps/SelfDrivingCar/SelfDrivingCarApp.tsx index 4f55996..66a2b99 100644 --- a/src/apps/SelfDrivingCar/SelfDrivingCarApp.tsx +++ b/src/apps/SelfDrivingCar/SelfDrivingCarApp.tsx @@ -3,7 +3,9 @@ import { CarScene } from './CarScene'; import { ConfigPanel } from './ConfigPanel'; import FitnessGraph from './FitnessGraph'; import { DEFAULT_CAR_CONFIG, DEFAULT_SIM_CONFIG } from './types'; +import { DEFAULT_GA_CONFIG } from './SimpleGA'; import type { CarConfig, SimulationConfig } from './types'; +import type { GAConfig } from './SimpleGA'; export function SelfDrivingCarApp() { const gameContainer = useRef(null); @@ -13,6 +15,7 @@ export function SelfDrivingCarApp() { // Config State const [carConfig, setCarConfig] = useState(DEFAULT_CAR_CONFIG); const [simConfig, setSimConfig] = useState(DEFAULT_SIM_CONFIG); + const [gaConfig, setGAConfig] = useState(DEFAULT_GA_CONFIG); useEffect(() => { if (!gameContainer.current || gameInstance.current) return; @@ -47,7 +50,6 @@ export function SelfDrivingCarApp() { const onGenerationComplete = (stats: { generation: number, best: number, average: number }) => { setHistory(prev => { const newHistory = [...prev, stats]; - if (newHistory.length > 50) return newHistory.slice(newHistory.length - 50); return newHistory; }); }; @@ -66,16 +68,23 @@ export function SelfDrivingCarApp() { // Sync Config to Scene useEffect(() => { if (gameInstance.current) { - gameInstance.current.events.emit('update-config', { car: carConfig, sim: simConfig }); + gameInstance.current.events.emit('update-config', { car: carConfig, sim: simConfig, ga: gaConfig }); } - }, [carConfig, simConfig]); + }, [carConfig, simConfig, gaConfig]); const handleNewTrack = () => { if (gameInstance.current) { gameInstance.current.events.emit('new-track'); + setHistory([]); // Clear fitness history on restart } }; + // Restart training when GA config changes + const handleGAConfigChange = (newConfig: GAConfig) => { + setGAConfig(newConfig); + handleNewTrack(); // Restart training with new GA settings + }; + return (
{/* Top Bar for Graph */} @@ -102,8 +111,10 @@ export function SelfDrivingCarApp() {
diff --git a/src/apps/SelfDrivingCar/SimpleGA.ts b/src/apps/SelfDrivingCar/SimpleGA.ts index 1404aa2..7bc42b5 100644 --- a/src/apps/SelfDrivingCar/SimpleGA.ts +++ b/src/apps/SelfDrivingCar/SimpleGA.ts @@ -59,8 +59,8 @@ export class SimpleGA { // 3. Fill rest while (nextPop.length < popSize) { // Diversity Injection (Random Immigrants) - // 5% chance to just insert a completely fresh brain to maintain diversity - if (Math.random() < 0.05) { + // Increased from 5% to 15% to combat stagnation + if (Math.random() < 0.15) { const dn = new DenseNetwork(this.layerSizes); nextPop.push(dn.getWeights()); continue; diff --git a/src/apps/SelfDrivingCar/types.ts b/src/apps/SelfDrivingCar/types.ts index fe1c08e..58f2be4 100644 --- a/src/apps/SelfDrivingCar/types.ts +++ b/src/apps/SelfDrivingCar/types.ts @@ -52,9 +52,9 @@ export const DEFAULT_CAR_CONFIG: CarConfig = { height: 40, maxSpeed: 12, turnSpeed: 0.15, // Increased from 0.08 for sharper turning - rayCount: 5, + rayCount: 7, // Increased from 5 for better peripheral vision rayLength: 150, - raySpread: Math.PI / 2, + raySpread: Math.PI * 5 / 6, // 150° FOV (increased from 90°) // Default Physics (Drifty) frictionAir: 0.02,