intermediate commit -- working on makeLab

This commit is contained in:
Michael Peters 2024-08-29 16:39:08 -07:00
parent c224f1a609
commit c631383087
2 changed files with 65 additions and 64 deletions

View File

@ -4,6 +4,18 @@ import { clamp, Engine, Keys, randchoice, randint, UI, Vec2, vec2 } from './game
import { SnakeBrain } from './snake-brain';
import { SGSHashSet, Snake, SnakeGameState, SnakeGameStateWithHistory } from './types';
import { PipeRef } from '.';
import {
assignSpecies,
CompatibilityDistanceConfig,
CompatibilityDistanceThreshold,
CrossConfig,
FertilityConfig,
Genome,
MateChoiceConfig,
mutate,
MutateConfig,
} from './neat';
import { BASE_GENOME_SNAKE_BRAIN_NEAT, NEATSnakeBrain } from './neat-snake-brain';
const BOARD_SIZE = 600; // px
const SQUARE_SIZE = 30; // px
@ -45,6 +57,8 @@ export interface TrainerSnapshot {
// labs and mutation -----------------------------------------------------------
// TODO: random colors for each species
function makeLabColors({ hue, sat, lig }: { hue: number; sat: number; lig: number }): LabColors {
const head = `hsl(${hue},${sat}%,${lig}%)`;
const tail = `hsl(${hue},${sat - 10}%,${lig - 20}%)`;
@ -60,29 +74,12 @@ function makeRandomLabColors() {
return makeLabColors({ hue, sat, lig });
}
function mutateLabColors(prev: LabColors) {
const hue = (prev.hue + randint(-10, 10) + 360) % 360;
const sat = clamp(prev.sat + randint(-5, 5), 70, 100);
const lig = clamp(prev.lig + randint(-4, 4), 50, 70);
return makeLabColors({ hue, sat, lig });
}
function makeRandomLab({ id, hiddenLayerNodes }: { id: number; hiddenLayerNodes: number }) {
function makeLab(id: number, genome: Genome) {
return {
id,
colors: makeRandomLabColors(),
state: getStartSnakeGameState(),
brain: SnakeBrain.fromRandom({ hiddenLayerNodes }),
};
}
function mutateLab({ newId, ref, mc }: { newId: number; ref: SnakeGameTrainerLab; mc: MutationConfig }) {
return {
id: newId,
colors: mutateLabColors(ref.colors),
state: getStartSnakeGameState(),
brain: SnakeBrain.mutate(ref.brain, mc),
brain: NEATSnakeBrain.fromGenome(genome),
};
}
@ -133,53 +130,56 @@ export default function runCanvas(canvas: HTMLCanvasElement, pipeRef: MutableRef
// game logic --------------------------------------------------------------
const SNAKES = 5000;
const SNAKES = 150;
const STARVE_STEPS = BOARD_SQUARES * 6;
const CULL_RATIO = 0.8;
const CULL_N = Math.floor(SNAKES * CULL_RATIO);
const MUTATE_RATE = 0.03;
const MUTATE_MAG = 5.0;
// neat configuration ------------------------------------------------------
const FC: FertilityConfig = {
fertile_threshold: 0.6,
champion_min_species_size: 3,
};
const MCC: MateChoiceConfig = {
asexual_rate: 0.5,
interspecies_rate: 0.2,
};
const CC: CrossConfig = {
reenable_rate: 0.25,
};
const MC: MutateConfig = {
mutate_rate: 0.25,
assign_rate: 0.1,
assign_mag: 1.0,
perturb_mag: 0.1,
new_node_rate: 0.2,
new_connection_rate: 0.1,
};
const CDC: CompatibilityDistanceConfig = {
c1: 1,
c2: 1,
c3: 1,
};
const CDT: CompatibilityDistanceThreshold = 1.0;
// general simulation ------------------------------------------------------
const generation = 1;
// labs & initial population -----------------------------------------------
const initialGenomes = new Array(SNAKES).map(() => mutate(BASE_GENOME_SNAKE_BRAIN_NEAT, MC));
// assign initial species
const population = new Map();
const reps = new Map();
for (const genome of initialGenomes) {
const sid = assignSpecies(genome, reps, CDC, CDT);
population.set(genome, sid);
}
let iteration = 1;
let nextLabId = 1;
const trainer: SnakeGameTrainerState = {
labs: Array.from({ length: SNAKES }).map(_ => makeRandomLab({ id: nextLabId++, hiddenLayerNodes: 6 })),
};
function cullWeakFearStrong() {
// a snake is considered starved if on average, a snake traverses
// the width of the board 6 times
const STARVE_RATIO_CUTOFF = BOARD_SQUARES * 4;
function sortKey(lab: SnakeGameTrainerLab): number {
const length = lab.state.snake.length;
const duration = lab.state.history.length;
// const starve_ratio = length / duration;
// if (starve_ratio < STARVE_RATIO_CUTOFF) return -1;
return length;
}
pipeRef.current.addTrainerSnap({ iteration, labs: createLabSnapshots(trainer.labs) });
const labsRanked = trainer.labs.sort((a, b) => sortKey(a) - sortKey(b));
const labsSurvive = labsRanked.slice(CULL_N).map(l => ({ ...l, state: getStartSnakeGameState() }));
const labsMutated = Array.from({ length: CULL_N }).map(_ => {
const ref = randchoice(labsSurvive);
return mutateLab({
newId: nextLabId++,
ref,
mc: { rate: MUTATE_RATE, mag: MUTATE_MAG },
});
});
const newLabs = [...labsSurvive, ...labsMutated];
trainer.labs = newLabs;
iteration += 1;
}
function update() {
// press spacebar to slow down time
if (keys.isKeyPressed(' ')) {
@ -191,10 +191,11 @@ export default function runCanvas(canvas: HTMLCanvasElement, pipeRef: MutableRef
engine.setUpdateDelay(0);
}
// TODO: compute next generation when all snakes are dead
// cull weak when all snakes are dead
const allDead = trainer.labs.findIndex(l => !l.state.dead) === -1;
if (allDead) {
cullWeakFearStrong();
// cullWeakFearStrong();
}
for (const lab of trainer.labs) {

View File

@ -301,7 +301,7 @@ export function alignGenomes(mom: Genome, dad: Genome): GenomeAlignment {
return alignment;
}
interface CompatibilityDistanceConfig {
export interface CompatibilityDistanceConfig {
c1: number;
c2: number;
c3: number;
@ -316,7 +316,7 @@ export function compatibilityDistance(alignment: GenomeAlignment, { c1, c2, c3 }
return distance;
}
type CompatibilityDistanceThreshold = number;
export type CompatibilityDistanceThreshold = number;
export function assignSpecies(
genome: Genome,
@ -336,7 +336,7 @@ export function assignSpecies(
return sid;
}
interface FertilityConfig {
export interface FertilityConfig {
fertile_threshold: number;
champion_min_species_size: number;
}
@ -377,7 +377,7 @@ export function tournamentSelectionWithChampions(
return { winners, champions };
}
interface MateChoiceConfig {
export interface MateChoiceConfig {
asexual_rate: number;
interspecies_rate: number;
}
@ -414,7 +414,7 @@ export function chooseMate(genome: Genome, population: Population, config: MateC
return genome;
}
interface CrossConfig {
export interface CrossConfig {
reenable_rate: number;
}
export function crossGenomes(mom: Genome, dad: Genome, fitness: Map<Genome, number>, config: CrossConfig) {