these snakes are lazy!

This commit is contained in:
Michael Peters 2024-09-08 14:30:02 -07:00
parent f2c2e6d86b
commit 8e3404b8c6
3 changed files with 119 additions and 29 deletions

View File

@ -3,6 +3,7 @@ import { Engine, Keys, randint, UI, Vec2, vec2 } from './game-engine';
import { SGSHashSet, Snake, SnakeGameState, SnakeGameStateWithHistory } from './types';
import { PipeRef } from '.';
import {
alignGenomes,
assignSpecies,
CompatibilityDistanceConfig,
CompatibilityDistanceThreshold,
@ -15,9 +16,10 @@ import {
MutateConfig,
NextGenerationConfig,
resetGlobalIDs,
SpeciesID,
} from './neat';
import { BASE_GENOME_SNAKE_BRAIN_NEAT, NEATSnakeBrain } from './neat-snake-brain';
import { mapInvert, mapMap } from './util';
import { histogram, mapInvert, mapMap } from './util';
const BOARD_SIZE = 600; // px
const SQUARE_SIZE = 30; // px
@ -132,37 +134,37 @@ export default function runCanvas(canvas: HTMLCanvasElement, pipeRef: MutableRef
// game logic --------------------------------------------------------------
const SNAKES = 150;
const SNAKES = 1000;
const STARVE_STEPS = BOARD_SQUARES * 6;
// neat configuration ------------------------------------------------------
// TODO: this configuration is not learning
// figure out why, maybe scoring is not proper -- or speciation is too forced
const FC: FertilityConfig = {
fertile_threshold: 0.6,
fertile_threshold: 0.8,
champion_min_species_size: 3,
};
const MCC: MateChoiceConfig = {
asexual_rate: 0.5,
interspecies_rate: 0.2,
asexual_rate: 1.0,
interspecies_rate: 0.0,
};
const CC: CrossConfig = {
reenable_rate: 0.25,
};
const MC: MutateConfig = {
mutate_rate: 0.25,
assign_rate: 0.1,
mutate_rate: 0.1,
assign_rate: 0.9,
assign_mag: 1.0,
perturb_mag: 0.1,
new_node_rate: 0.2,
new_connection_rate: 0.1,
perturb_mag: 0.2,
new_node_rate: 0.1,
new_connection_rate: 0.35,
};
const CDC: CompatibilityDistanceConfig = {
c1: 1,
c2: 1,
c3: 1,
c1: 1.0,
c2: 1.0,
c3: 0.4,
};
const CDT: CompatibilityDistanceThreshold = 1.0;
const CDT: CompatibilityDistanceThreshold = 100.0;
const NGC: NextGenerationConfig = {
fc: FC,
mcc: MCC,
@ -172,6 +174,15 @@ export default function runCanvas(canvas: HTMLCanvasElement, pipeRef: MutableRef
cdt: CDT,
};
const MC_INITIAL_POPULATION: MutateConfig = {
mutate_rate: 1.0,
assign_rate: 1.0,
assign_mag: 1.0,
perturb_mag: 0.0,
new_node_rate: 0.0,
new_connection_rate: 0.0,
};
// general simulation ------------------------------------------------------
let generation = 1;
@ -179,11 +190,13 @@ export default function runCanvas(canvas: HTMLCanvasElement, pipeRef: MutableRef
resetGlobalIDs({ node_id: 1, innovation_number: 1, species_id: 1 });
// eslint-disable-next-line prefer-spread
const initialGenomes = Array.apply(null, Array(SNAKES)).map(() => mutate(BASE_GENOME_SNAKE_BRAIN_NEAT, MC));
const initialGenomes = Array.apply(null, Array(SNAKES)).map(() =>
mutate(BASE_GENOME_SNAKE_BRAIN_NEAT, MC_INITIAL_POPULATION),
);
// assign initial species
let population = new Map();
let reps = new Map();
let population = new Map<Genome, SpeciesID>();
let reps = new Map<SpeciesID, Genome>();
for (const genome of initialGenomes) {
const sid = assignSpecies(genome, reps, CDC, CDT);
population.set(genome, sid);
@ -208,20 +221,54 @@ export default function runCanvas(canvas: HTMLCanvasElement, pipeRef: MutableRef
const allDead = trainer.labs.findIndex(l => !l.state.dead) === -1;
if (allDead) {
console.log('computing next gen');
// compute next generation
// get fitness into the correct structure
const fitness = new Map<Genome, number>();
for (const lab of trainer.labs) {
fitness.set(lab.brain.brain.genome, lab.state.snake.length);
}
// log some stats
const avgFitness = Array.from(fitness.values()).reduce((a, b) => a + b) / fitness.size;
const speciesSizes = mapMap(mapInvert(population), (k, v) => [k, v.size]);
let speciesSizesStr = '';
for (const [sid, size] of speciesSizes.entries()) {
speciesSizesStr += ` ${sid}: ${size}\n`;
}
const top10Fitness = Array.from(fitness.values())
.sort((a, b) => a - b)
.slice(-10);
console.log(
`g${generation}\naverage fitness: ${avgFitness}\nspecies sizes:\n${speciesSizesStr}top 10 fitness: ${top10Fitness.join(', ')}`,
);
// compute next generation
const { nextPopulation, nextReps } = computeNextGeneration(population, fitness, NGC);
population = nextPopulation;
reps = nextReps;
console.log({ nextPopulationSizes: mapMap(mapInvert(nextPopulation), (k, v) => [k, v.size]) });
let repsStr = 'reps:\n';
for (const [sid, rep] of reps) {
const { excess } = alignGenomes(BASE_GENOME_SNAKE_BRAIN_NEAT, rep);
const excessStr = excess.map(n => `${n.dad?.src_id} -> ${n.dad?.dst_id}`).join(', ');
repsStr += `${sid}: [${excessStr}]\n`;
}
console.log(repsStr);
trainer = {
labs: Array.from(population.keys()).map(g => makeLab(nextLabId++, g)),
};
generation++;
// pause after given generation
if (generation === 20) {
const weights = Array.from(population.keys()).flatMap(g => g.map(n => n.data.weight));
const weightsHistogram = histogram(weights, { bucketCount: 100 });
let weightsHistoStr = '';
for (const [range, count] of weightsHistogram.entries()) {
weightsHistoStr += `${range}: ${count}\n`;
}
console.log(weightsHistoStr);
debugger;
}
}
for (const lab of trainer.labs) {
@ -292,6 +339,8 @@ export default function runCanvas(canvas: HTMLCanvasElement, pipeRef: MutableRef
ui.setFillStyle('#333333');
ui.clearCanvas();
return;
const labsAlive = trainer.labs.filter(l => !l.state.dead);
const labsRanked = labsAlive.slice().sort((a, b) => a.state.snake.length - b.state.snake.length);

View File

@ -239,7 +239,7 @@ interface GenomeAlignment {
genomes: { mom: Genome; dad: Genome };
}
type SpeciesID = number;
export type SpeciesID = number;
export type Population = Map<Genome, SpeciesID>;
let g_node_id = 1;
@ -365,13 +365,14 @@ export function tournamentSelectionWithChampions(
const species = mapInvert(population);
// compute adjusted fitness by scaling fitness by species size
const adjFitness = mapMap(fitness, (k, v) => {
const sid = population.get(k)!;
const spec = species.get(sid)!;
if (!spec) debugger;
const speciesSize = species.get(sid)!.size;
return [k, v / speciesSize];
});
const adjFitness = fitness;
// const adjFitness = mapMap(fitness, (k, v) => {
// const sid = population.get(k)!;
// const spec = species.get(sid)!;
// if (!spec) debugger;
// const speciesSize = species.get(sid)!.size;
// return [k, v / speciesSize];
// });
const winners = new Set<Genome>();
@ -407,6 +408,7 @@ export function chooseMate(genome: Genome, population: Population, config: MateC
const genomeSID = population.get(genome)!;
if (Math.random() < interspecies_rate) {
console.log('interspecies');
// mate with an organism from a different species if possible
const otherSpeciesOrganisms = [];
for (const [sid, organisms] of species.entries()) {
@ -624,13 +626,18 @@ export function computeNextGeneration(
const winners = Array.from(winners_); // convert to an array once for randchoice()
const winnersPopulation = new Map(winners.map(w => [w, population.get(w)!]));
const populationFitness = Array.from(population.keys()).reduce((p, w) => p + fitness.get(w)!, 0);
const winnersFitness = winners.reduce((p, w) => p + fitness.get(w)!, 0);
console.log({ populationFitness, winnersFitnessScaled: (winnersFitness * population.size) / winners.length });
// copy over champions to the next generation and use them as representatives for their species
const nextPopulation = mapMap(champions, (sid, c) => [c, sid]);
const nextReps = new Map(champions);
while (nextPopulation.size < population.size) {
// mate
const mom = randchoice(winners);
const mom = randchoice(winners.flatMap(w => [...Array(5)].map(_ => w)));
const dad = chooseMate(mom, winnersPopulation, mcc);
const crossed = crossGenomes(mom, dad, fitness, cc);
const baby = mutate(crossed, mc);

View File

@ -1,3 +1,5 @@
const EPSILON = 0.0001;
// [-1, 1)
export function randomNegPos() {
return 2 * (Math.random() - 0.5);
@ -72,3 +74,35 @@ export function setDifference<T>(a: Set<T>, b: Set<T>) {
for (const e of b) diff.delete(e);
return diff;
}
export function histogram(arr: number[], config: { bucketCount: number }) {
const { bucketCount } = config;
const min = Math.min(...arr);
const max = Math.max(...arr);
const range = max - min;
console.log('min:', min, 'max:', max);
const buckets = new Map<number, number>();
for (const n of arr) {
const bucket = Math.floor(((n - min) / range) * bucketCount);
const prev = buckets.get(bucket) || 1;
buckets.set(bucket, prev + 1);
}
const bucketsOrdered = new Map<number, number>();
for (const bucket of Array.from(buckets.keys()).sort((a, b) => a - b)) {
bucketsOrdered.set(bucket, buckets.get(bucket)!);
}
const bucketsBmin = mapMap(bucketsOrdered, (k, v) => {
const bmin = ((k + 0) * range) / bucketCount + min;
const bmax = ((k + 1) * range) / bucketCount + min;
return [`${bmin}->${bmax}`, v];
});
return bucketsBmin;
}