diff --git a/src/site/snake/neat-snake-brain.ts b/src/site/snake/neat-snake-brain.ts index 3586494..fac15bc 100644 --- a/src/site/snake/neat-snake-brain.ts +++ b/src/site/snake/neat-snake-brain.ts @@ -25,10 +25,10 @@ const FEATURES = { HEAD_Y: 'HEAD_Y', APPLE_REL_X: 'APPLE_REL_X', APPLE_REL_Y: 'APPLE_REL_Y', - TAIL_ABOVE: 'TAIL_ABOVE', - TAIL_BELOW: 'TAIL_BELOW', - TAIL_LEFT: 'TAIL_LEFT', - TAIL_RIGHT: 'TAIL_RIGHT', + // TAIL_ABOVE: 'TAIL_ABOVE', + // TAIL_BELOW: 'TAIL_BELOW', + // TAIL_LEFT: 'TAIL_LEFT', + // TAIL_RIGHT: 'TAIL_RIGHT', }; const OUTPUTS = { U: 'U', @@ -43,10 +43,10 @@ const _BASE_GENOME_SNAKE_BRAIN_NEAT_EDGES = completeBipartiteEdges( FEATURES.HEAD_Y, FEATURES.APPLE_REL_X, FEATURES.APPLE_REL_Y, - FEATURES.TAIL_ABOVE, - FEATURES.TAIL_BELOW, - FEATURES.TAIL_LEFT, - FEATURES.TAIL_RIGHT, + // FEATURES.TAIL_ABOVE, + // FEATURES.TAIL_BELOW, + // FEATURES.TAIL_LEFT, + // FEATURES.TAIL_RIGHT, ], [OUTPUTS.U, OUTPUTS.D, OUTPUTS.L, OUTPUTS.R], ); @@ -108,10 +108,10 @@ export class NEATSnakeBrain { act.set(FEATURES.HEAD_Y, head.y); act.set(FEATURES.APPLE_REL_X, appleRel.x); act.set(FEATURES.APPLE_REL_Y, appleRel.y); - act.set(FEATURES.TAIL_ABOVE, Math.max(...above, BOARD_SQUARES)); - act.set(FEATURES.TAIL_BELOW, Math.max(...below, BOARD_SQUARES)); - act.set(FEATURES.TAIL_LEFT, Math.max(...left, BOARD_SQUARES)); - act.set(FEATURES.TAIL_RIGHT, Math.max(...right, BOARD_SQUARES)); + // act.set(FEATURES.TAIL_ABOVE, Math.max(...above, BOARD_SQUARES)); + // act.set(FEATURES.TAIL_BELOW, Math.max(...below, BOARD_SQUARES)); + // act.set(FEATURES.TAIL_LEFT, Math.max(...left, BOARD_SQUARES)); + // act.set(FEATURES.TAIL_RIGHT, Math.max(...right, BOARD_SQUARES)); this.brain.think(act); diff --git a/src/site/snake/neat.ts b/src/site/snake/neat.ts index a09676d..c4e7751 100644 --- a/src/site/snake/neat.ts +++ b/src/site/snake/neat.ts @@ -228,7 +228,7 @@ * - Effectively pass data from inputs, through hidden nodes, to outputs */ -import { edgesToNodes, NodeID, traceParents, RawEdge } from './network'; +import { edgesToNodes, NodeID, traceParents, RawEdge, traceChildren } from './network'; import { keyMax, mapInvert, mapMap, randchoice, randint, randomNegPos, setDifference, setMap } from './util'; export interface GeneData { @@ -258,6 +258,33 @@ let g_node_id = 1; let g_innovation_number = 1; let g_species_id = 1; +function hashGenome(genome: Genome) { + const cyrb53 = (str: string, seed = 0) => { + let h1 = 0xdeadbeef ^ seed; + let h2 = 0x41c6ce57 ^ seed; + for (let i = 0, ch; i < str.length; i++) { + ch = str.charCodeAt(i); + h1 = Math.imul(h1 ^ ch, 2654435761); + h2 = Math.imul(h2 ^ ch, 1597334677); + } + h1 = Math.imul(h1 ^ (h1 >>> 16), 2246822507); + h1 ^= Math.imul(h2 ^ (h2 >>> 13), 3266489909); + h2 = Math.imul(h2 ^ (h2 >>> 16), 2246822507); + h2 ^= Math.imul(h1 ^ (h1 >>> 13), 3266489909); + + return 4294967296 * (2097151 & h2) + (h1 >>> 0); + }; + let hash = 0; + for (const gene of genome) { + hash += cyrb53(gene.src_id); + hash += cyrb53(gene.dst_id); + hash += gene.data.weight; + hash += gene.data.enabled ? 1 : 0; + hash += gene.data.innovation; + } + return hash; +} + export function resetGlobalIDs(config: { node_id: number; innovation_number: number; species_id: number }) { g_node_id = config.node_id; g_innovation_number = config.innovation_number; @@ -508,7 +535,8 @@ export function mutateNewConn(newConn: { src_id: NodeID; dst_id: NodeID }, newWe }; } -export function findAcyclicNewConns(rawEdges: RawEdge[]): { src_id: NodeID; dst_id: NodeID }[] { +// TODO: improve name since also excluding source nodes +export function findAcyclicNonSourceNewConns(rawEdges: RawEdge[]): { src_id: NodeID; dst_id: NodeID }[] { // finds potential new connections that are acyclic and are not already connected // NOTE: there's some performance stuff here that could definitely be improved @@ -518,12 +546,22 @@ export function findAcyclicNewConns(rawEdges: RawEdge[]): { src_id // a node that is connected to one of its parents creates a cycle const allNodeIDs = new Set(nodes.keys()); const parents = traceParents(nodes); + const children = traceChildren(nodes); + + // TODO: think on this more: + // adding a new connection needs to not connect a source -> source or a sink -> sink + // or else the innovation number system gets messed up + const sources = new Set(Array.from(parents.entries()).flatMap(([k, v]) => (v.size === 0 ? [k] : []))); + const sinks = new Set(Array.from(children.entries()).flatMap(([k, v]) => (v.size === 0 ? [k] : []))); + console.log({ sources, sinks }); + const acyclic = new Map>(); for (const [nodeID, nodeParents] of parents.entries()) { const nodeParentIDs = setMap(nodeParents, n => n.id); const nodeIDsAcyclic = setDifference(allNodeIDs, nodeParentIDs); nodeIDsAcyclic.delete(nodeID); - acyclic.set(nodeID, nodeIDsAcyclic); + const nodeIDsAcyclicNonSource = setDifference(nodeIDsAcyclic, sources); + acyclic.set(nodeID, nodeIDsAcyclicNonSource); } // flatten @@ -552,6 +590,9 @@ export interface MutateConfig { export function mutate(genome: Genome, config: MutateConfig): Genome { const { mutate_rate, assign_rate, assign_mag, perturb_mag, new_node_rate, new_connection_rate } = config; + console.log('mutating: ', hashGenome(genome), genome); + findAcyclicNonSourceNewConns(genome); + const newGenome = genome.map(gene => { if (Math.random() >= mutate_rate) return gene; // this connection should not be mutated if (Math.random() < assign_rate) { @@ -574,8 +615,10 @@ export function mutate(genome: Genome, config: MutateConfig): Genome { } if (Math.random() < new_connection_rate) { + // TODO: figure out why we're not hitting a + // TODO: disallow "source" nodes from being new connection destinations // create a new connection between two *previously unconnected* nodes - const options = findAcyclicNewConns(newGenome); + const options = findAcyclicNonSourceNewConns(newGenome); if (options.length === 0) { // TODO: remove this warn once this starts working // this is mostly a sanity check / useful for metrics @@ -583,10 +626,17 @@ export function mutate(genome: Genome, config: MutateConfig): Genome { } else { // choose a random connection const newConn = randchoice(options); + console.log('adding new connection...', hashGenome(genome), genome, newConn); + if (new Set(['U', 'D', 'L', 'R']).has(newConn.dst_id)) { + debugger; + } const newGene = mutateNewConn(newConn, assign_mag * randomNegPos()); newGenome.push(newGene); } + + findAcyclicNonSourceNewConns(newGenome); } + console.log('created: ', hashGenome(newGenome), newGenome); return newGenome; } @@ -619,6 +669,7 @@ export function computeNextGeneration( const mom = randchoice(winners); const dad = chooseMate(mom, winnersPopulation, mcc); const crossed = crossGenomes(mom, dad, fitness, cc); + // TODO: crossed has cycles! const baby = mutate(crossed, mc); // assign to a species + add to next generation diff --git a/src/site/snake/network.ts b/src/site/snake/network.ts index 2d1050b..981851e 100644 --- a/src/site/snake/network.ts +++ b/src/site/snake/network.ts @@ -80,7 +80,7 @@ export function traceParents(nodes: Network>) { function traceNodeParents(node: Node, depth: number): Set> { console.log('tracing: ', node.id, depth); // TODO: there's a particularly nasty issue where maybe nodes are not just from their own genome? - if (depth > 1000) { + if (depth > 20) { debugger; } if (parents.has(node.id)) return parents.get(node.id)!; diff --git a/src/test/test-neat.ts b/src/test/test-neat.ts index 451d3ac..e3d4dae 100644 --- a/src/test/test-neat.ts +++ b/src/test/test-neat.ts @@ -4,7 +4,7 @@ import { tournamentSelectionWithChampions, compatibilityDistance, crossGenomes, - findAcyclicNewConns, + findAcyclicNonSourceNewConns, Genome, mutateAssign, mutateNewConn, @@ -357,7 +357,7 @@ function testMutateNewConn() { } addTest(testMutateNewConn); -function testFindAcyclicNewConns() { +function testFindAcyclicNonSourceNewConns() { /* * all edges pointing down * @@ -377,22 +377,22 @@ function testFindAcyclicNewConns() { { src_id: 'D', dst_id: 'F', data: null }, ]; - const options = findAcyclicNewConns(edges); + const options = findAcyclicNonSourceNewConns(edges); const expected = [ - { src_id: 'A', dst_id: 'B' }, + // not A -> B - B is a source { src_id: 'A', dst_id: 'F' }, - { src_id: 'B', dst_id: 'A' }, + // not B -> A - A is a source { src_id: 'B', dst_id: 'C' }, { src_id: 'B', dst_id: 'E' }, { src_id: 'B', dst_id: 'F' }, - { src_id: 'C', dst_id: 'B' }, + // not C -> B - B is a source { src_id: 'C', dst_id: 'D' }, { src_id: 'C', dst_id: 'F' }, { src_id: 'D', dst_id: 'C' }, - // not E -> B + // not E -> B - B is a parent of E { src_id: 'E', dst_id: 'F' }, - // not F -> A - // not F -> B + // not F -> A - A is a parent of F + // not F -> B - B is a parent of F { src_id: 'F', dst_id: 'C' }, { src_id: 'F', dst_id: 'E' }, ]; @@ -406,7 +406,7 @@ function testFindAcyclicNewConns() { assertDeepEqual(options, expected); } -addTest(testFindAcyclicNewConns); +addTest(testFindAcyclicNonSourceNewConns); function testComputeNextGeneration() { function makeGenome(weight: number) {