diff --git a/src/site/snake/canvas.ts b/src/site/snake/canvas.ts index 0e08bea..88de27e 100644 --- a/src/site/snake/canvas.ts +++ b/src/site/snake/canvas.ts @@ -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) { diff --git a/src/site/snake/neat.ts b/src/site/snake/neat.ts index 65a5061..8b23a03 100644 --- a/src/site/snake/neat.ts +++ b/src/site/snake/neat.ts @@ -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, config: CrossConfig) {