Add self driving car app

This commit is contained in:
Peter Stockings
2026-01-15 18:13:48 +11:00
parent dd561a4b32
commit fab6a7e03f
8 changed files with 344 additions and 99 deletions

View File

@@ -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",

View File

@@ -25,6 +25,12 @@ export class Car {
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,
y: number,
@@ -54,10 +60,22 @@ 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;
}
@@ -65,16 +83,35 @@ export class Car {
// 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) {
gas = 1.0;
@@ -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;
}
}

View File

@@ -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);
// Use actual ray readings for color-coding
const readings = car.rayReadings;
const startRayAngle = angleBase - raySpread / 2;
const angleStep = raySpread / (rayCount - 1);
// Recalculate angles to match Car.ts logic roughly for visualization
const startRayAngle = angleBase - spread / 2;
const angleStep = spread / (count - 1);
for(let i=0; i<count; i++) {
for(let i=0; i<rayCount; i++) {
const angle = startRayAngle + i * angleStep;
const reading = readings[i] || 0; // 0 = far, 1 = close
// Color interpolation: Green (far) -> 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);
}
}
}

View File

@@ -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(
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;

View File

@@ -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 (
<div style={{
position: 'absolute',
@@ -105,6 +112,43 @@ export function ConfigPanel({ carConfig, simConfig, onCarConfigChange, onSimConf
/>
</div>
<div style={groupStyle}>
<h4 style={{ margin: '5px 0', color: '#ffa726' }}>Sensors</h4>
<div style={labelStyle}>
<span>Ray Count</span>
<span>{carConfig.rayCount}</span>
</div>
<input
type="range" min="3" max="11" step="2"
value={carConfig.rayCount}
onChange={(e) => updateCar('rayCount', parseInt(e.target.value))}
style={sliderStyle}
/>
<div style={labelStyle}>
<span>FOV (Field of View)</span>
<span>{(carConfig.raySpread * 180 / Math.PI).toFixed(0)}°</span>
</div>
<input
type="range" min="60" max="180" step="10"
value={carConfig.raySpread * 180 / Math.PI}
onChange={(e) => updateCar('raySpread', parseFloat(e.target.value) * Math.PI / 180)}
style={sliderStyle}
/>
<div style={labelStyle}>
<span>Ray Length</span>
<span>{carConfig.rayLength}</span>
</div>
<input
type="range" min="50" max="300" step="10"
value={carConfig.rayLength}
onChange={(e) => updateCar('rayLength', parseInt(e.target.value))}
style={sliderStyle}
/>
</div>
<div style={groupStyle}>
<h4 style={{ margin: '5px 0', color: '#ff6b6b' }}>Track Gen</h4>
@@ -148,6 +192,58 @@ export function ConfigPanel({ carConfig, simConfig, onCarConfigChange, onSimConf
</button>
</div>
<div style={groupStyle}>
<h4 style={{ margin: '5px 0', color: '#a855f7' }}>Evolution (GA)</h4>
<div style={labelStyle}>
<span>Population Size</span>
<span>{gaConfig.populationSize}</span>
</div>
<input
type="range" min="10" max="200" step="10"
value={gaConfig.populationSize}
onChange={(e) => updateGA('populationSize', parseInt(e.target.value))}
style={sliderStyle}
/>
<div style={labelStyle}>
<span>Mutation Rate</span>
<span>{(gaConfig.mutationRate * 100).toFixed(1)}%</span>
</div>
<input
type="range" min="0.01" max="0.20" step="0.01"
value={gaConfig.mutationRate}
onChange={(e) => updateGA('mutationRate', parseFloat(e.target.value))}
style={sliderStyle}
/>
<div style={labelStyle}>
<span>Mutation Amount</span>
<span>{gaConfig.mutationAmount.toFixed(2)}</span>
</div>
<input
type="range" min="0.05" max="1.0" step="0.05"
value={gaConfig.mutationAmount}
onChange={(e) => updateGA('mutationAmount', parseFloat(e.target.value))}
style={sliderStyle}
/>
<div style={labelStyle}>
<span>Elitism (Keep Best)</span>
<span>{gaConfig.elitism}</span>
</div>
<input
type="range" min="0" max="20" step="1"
value={gaConfig.elitism}
onChange={(e) => updateGA('elitism', parseInt(e.target.value))}
style={sliderStyle}
/>
<div style={{ fontSize: '10px', color: '#f59e0b', marginTop: '8px', textAlign: 'center' }}>
GA changes restart training
</div>
</div>
<div style={{ fontSize: '10px', color: '#888', textAlign: 'center' }}>
Physics apply immediately.<br />
Track settings apply on generate.

View File

@@ -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<HTMLDivElement>(null);
@@ -13,6 +15,7 @@ export function SelfDrivingCarApp() {
// Config State
const [carConfig, setCarConfig] = useState<CarConfig>(DEFAULT_CAR_CONFIG);
const [simConfig, setSimConfig] = useState<SimulationConfig>(DEFAULT_SIM_CONFIG);
const [gaConfig, setGAConfig] = useState<GAConfig>(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 (
<div style={{ width: '100%', height: '100%', position: 'relative', display: 'flex', flexDirection: 'column', overflow: 'hidden' }}>
{/* Top Bar for Graph */}
@@ -102,8 +111,10 @@ export function SelfDrivingCarApp() {
<ConfigPanel
carConfig={carConfig}
simConfig={simConfig}
gaConfig={gaConfig}
onCarConfigChange={setCarConfig}
onSimConfigChange={setSimConfig}
onGAConfigChange={handleGAConfigChange}
onNewTrack={handleNewTrack}
/>
</div>

View File

@@ -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;

View File

@@ -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,