generated from michael/webpack-base
these snakes are lazy!
This commit is contained in:
parent
f2c2e6d86b
commit
8e3404b8c6
@ -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);
|
||||
|
||||
|
@ -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);
|
||||
|
@ -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;
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user