Add fitness graph
This commit is contained in:
@@ -7,10 +7,6 @@ import Tips from './Tips';
|
||||
import BestSnakeDisplay from './BestSnakeDisplay';
|
||||
import {
|
||||
createPopulation,
|
||||
evaluatePopulation,
|
||||
evolveGeneration,
|
||||
getBestIndividual,
|
||||
getAverageFitness,
|
||||
type Population,
|
||||
} from '../../lib/snakeAI/evolution';
|
||||
import type { EvolutionConfig } from '../../lib/snakeAI/types';
|
||||
@@ -24,6 +20,8 @@ const DEFAULT_CONFIG: EvolutionConfig = {
|
||||
maxGameSteps: 20000,
|
||||
};
|
||||
|
||||
import EvolutionWorker from '../../lib/snakeAI/evolution.worker?worker';
|
||||
|
||||
export default function SnakeAI() {
|
||||
const [population, setPopulation] = useState<Population>(() =>
|
||||
createPopulation(DEFAULT_CONFIG)
|
||||
@@ -32,30 +30,85 @@ export default function SnakeAI() {
|
||||
const [isRunning, setIsRunning] = useState(false);
|
||||
const [speed, setSpeed] = useState(5);
|
||||
const [gamesPlayed, setGamesPlayed] = useState(0);
|
||||
const [fitnessHistory, setFitnessHistory] = useState<Array<{ generation: number, best: number, average: number }>>([]);
|
||||
|
||||
// Compute derived values from population
|
||||
const bestIndividual = getBestIndividual(population);
|
||||
const averageFitness = getAverageFitness(population);
|
||||
// Keep a ref to population for the worker
|
||||
const populationRef = useRef(population);
|
||||
useEffect(() => {
|
||||
populationRef.current = population;
|
||||
}, [population]);
|
||||
|
||||
const animationFrameRef = useRef<number>();
|
||||
const lastUpdateRef = useRef<number>(0);
|
||||
|
||||
const runGeneration = useCallback(() => {
|
||||
setPopulation((prev) => {
|
||||
try {
|
||||
// Evaluate current generation
|
||||
const evaluated = evaluatePopulation(prev, config);
|
||||
// Compute derived values for display
|
||||
// If we have stats from the last generation, use them. Otherwise default to 0.
|
||||
const currentBestFitness = population.lastGenerationStats?.bestFitness || 0;
|
||||
const currentAverageFitness = population.lastGenerationStats?.averageFitness || 0;
|
||||
|
||||
// Evolve to next generation
|
||||
const nextGen = evolveGeneration(evaluated, config);
|
||||
const workerRef = useRef<Worker | null>(null);
|
||||
const isProcessingRef = useRef(false);
|
||||
|
||||
return nextGen;
|
||||
} catch (error) {
|
||||
console.error("SnakeAI: Generation update failed", error);
|
||||
return prev;
|
||||
useEffect(() => {
|
||||
workerRef.current = new EvolutionWorker();
|
||||
workerRef.current.onmessage = (e) => {
|
||||
const { type, payload } = e.data; // payload is the NEW population
|
||||
if (type === 'SUCCESS') {
|
||||
// Critical: Update ref immediately to prevent race condition with next animation frame
|
||||
populationRef.current = payload;
|
||||
setPopulation(payload);
|
||||
|
||||
// Update history if we have stats
|
||||
if (payload.lastGenerationStats) {
|
||||
setFitnessHistory(prev => {
|
||||
const newEntry = {
|
||||
generation: payload.generation - 1, // The stats are for the gen that just finished
|
||||
best: payload.lastGenerationStats!.bestFitness,
|
||||
average: payload.lastGenerationStats!.averageFitness
|
||||
};
|
||||
// Keep last 100 generations to avoid memory issues if running for eternity
|
||||
const newHistory = [...prev, newEntry];
|
||||
if (newHistory.length > 100) return newHistory.slice(newHistory.length - 100);
|
||||
return newHistory;
|
||||
});
|
||||
}
|
||||
|
||||
isProcessingRef.current = false;
|
||||
} else {
|
||||
console.error("Worker error:", payload);
|
||||
isProcessingRef.current = false;
|
||||
}
|
||||
};
|
||||
|
||||
return () => {
|
||||
workerRef.current?.terminate();
|
||||
};
|
||||
}, []);
|
||||
|
||||
const runGeneration = useCallback((generations: number = 1) => {
|
||||
if (isProcessingRef.current || !workerRef.current) return;
|
||||
|
||||
isProcessingRef.current = true;
|
||||
// We need to send the *current* population.
|
||||
// Since this is inside a callback, we need to be careful about closure staleness.
|
||||
// However, we can't easily access the "latest" state inside a callback without refs or dependency.
|
||||
// But 'population' is in the dependency array of the effect calling this? No.
|
||||
// The animate loop calls this.
|
||||
|
||||
// Let's use a functional update approach? No, we need to SEND data.
|
||||
// We will use a ref to track current population for the worker to ensure we always send latest
|
||||
// OR rely on the fact that 'population' is in dependency of runGeneration (it wasn't before).
|
||||
|
||||
// Wait, 'runGeneration' lines 43-58 previously used setPopulation(prev => ...).
|
||||
// It didn't need 'population' in dependency.
|
||||
// Now we need it.
|
||||
|
||||
workerRef.current.postMessage({
|
||||
population: populationRef.current, // Use a ref for latest population
|
||||
config,
|
||||
generations
|
||||
});
|
||||
}, [config]);
|
||||
}, [config]); // populationRef will be handled separately
|
||||
|
||||
// Update stats when generation changes
|
||||
useEffect(() => {
|
||||
@@ -93,7 +146,7 @@ export default function SnakeAI() {
|
||||
}
|
||||
|
||||
if (elapsed >= updateInterval) {
|
||||
runGeneration();
|
||||
runGeneration(1);
|
||||
lastUpdateRef.current = timestamp;
|
||||
}
|
||||
} else {
|
||||
@@ -102,9 +155,9 @@ export default function SnakeAI() {
|
||||
// Speed 100 -> 10 gens per frame (~600 eps)
|
||||
const gensPerFrame = Math.floor((speed - 10) / 10);
|
||||
|
||||
for (let i = 0; i < gensPerFrame; i++) {
|
||||
runGeneration();
|
||||
}
|
||||
// For turbo mode, we just fire once per frame (or whenever the worker is ready)
|
||||
// asking for multiple generations
|
||||
runGeneration(gensPerFrame);
|
||||
lastUpdateRef.current = timestamp;
|
||||
}
|
||||
|
||||
@@ -122,7 +175,9 @@ export default function SnakeAI() {
|
||||
|
||||
const handleReset = () => {
|
||||
setIsRunning(false);
|
||||
setPopulation(createPopulation(config));
|
||||
const newPop = createPopulation(config);
|
||||
populationRef.current = newPop;
|
||||
setPopulation(newPop);
|
||||
setGamesPlayed(0);
|
||||
};
|
||||
|
||||
@@ -162,10 +217,11 @@ export default function SnakeAI() {
|
||||
|
||||
<Stats
|
||||
generation={population.generation}
|
||||
bestFitness={bestIndividual.fitness}
|
||||
bestFitness={currentBestFitness}
|
||||
bestFitnessEver={population.bestFitnessEver}
|
||||
averageFitness={averageFitness}
|
||||
averageFitness={currentAverageFitness}
|
||||
gamesPlayed={gamesPlayed}
|
||||
history={fitnessHistory}
|
||||
/>
|
||||
|
||||
<Tips />
|
||||
|
||||
Reference in New Issue
Block a user