Advertisement
Not a member of Pastebin yet?
Sign Up,
it unlocks many cool features!
- -- MarI/O by SethBling rewritten for use with FCEUX by Akisame (used 3DI70R's version as a reference to save time but I have been told by him that he used juvester's script as a reference https://github.com/juvester/mari-o-fceux.)
- -- There is a version that is closer to the original. This is not that version. In this version I have changed various things to speed up the learning process and give mario more tool with which he can complete levels
- -- Pressing N will show the Neural net, pressing M will show the mutation rates, pressing L will load the last saved generation and pressing B will display the genome with the top fitness and T will activate the turbo.
- -- This file includes all the fixes from my bizhawk version (https://pastebin.com/dKYFKg2E) and an extra fix to hopefully prevent the formation of a monospecies when we encounter a major bottleneck
- -- If you want to start your own stream with this script I would appreciate it if you would include this file as well as SethBling's original version in the description
- -- Keep in mind that this code is provided for Non-Commercial usage and it is not permitted to request financial compensation (either by means of ads, donations, subscriptions or other means) for selling, streaming or otherwise previewing this code or its execution.
- -- This is still just an early version and it may be subjected to further changes in the future.
- -- To use this script just download FCEUX and create a savestate in state 1 for smb. click file -> lua -> 'new lua script window' and load the script.
- -- Have fun!
- --FCEUX doesn't allow for user string input but if you press 'L' on your keyboard it should load the latest generation that was saved
- --for loading specific generations replace nil with the filename as shown in the example below
- savedpool = nil
- --savedpool = 'backups/backup.429.SMB1-1.state.pool'
- --savedpool = "backups/backup.5.SMB1-1.state.pool"
- -- HUD options. These can be changed on the fly by pressing 'M' for the mutation rates and 'N' for the network
- mutation_rates_disp = false
- neural_net_disp = true
- --set to false if you want warning popups before loading anything that might cause you to lose your current progress
- nopopup = true
- savestate_slot = 1 -- Load the level from FCEUX savestate slot. Set to 3 if you are loading a level that mario has reached by completing another level
- SAVE_LOAD_FILE = "SMB1-1.state.pool" --filename to be used
- player = 2 --1 for mario 2 for luigi
- AllowAutoTurbo = true --Automatic turbo
- AllowSlowdownNearMax = true --This is slow down the turbo when it nears to furthest it has currently gotten
- FirstGenSpeedup = true --This will speed up the first generation
- BottleneckTurbo = true --Activate turbo on bottleneck detection
- TurboGenBottleneck = 20 --will start the turbo when it is in a bottleneck for 25 generations. Please adjust to match you taste
- os.execute("mkdir backups") --create the folder to save the pools in
- SavestateObj = savestate.object(savestate_slot)
- ButtonNames = {
- "A",
- "B",
- "up",
- "down",
- "left",
- "right",
- }
- BoxRadius = 6
- InputSize = (BoxRadius*2+1)*(BoxRadius*2+1)
- switchtime = {10,12,18}
- switchtimer = {}
- initialswitchvalue = {}
- for i=1,#switchtime do
- switchtimer[i] = switchtime[i]
- initialswitchvalue[i] = 1
- end
- Inputs = InputSize+1+#initialswitchvalue
- Outputs = #ButtonNames
- Population = 1000
- DeltaDisjoint = 2.0
- DeltaWeights = 0.4
- DeltaThreshold = 1.0
- keyflag = false --used for keyboard input
- endlevel = false
- preparenext = false
- secondbest = 0
- maxright = 0
- restoreturbo = false
- StaleSpecies = 25 --increased slightly to allow for longer evolution
- mariohole = false --flag for mario falling into a hole
- mariopipe = false --flag for mario exiting a pipe
- marioPipeEnter = false --new flag for entering a pipe
- offset = 0 --offset value for when mario exits a pipe
- timebonus = 0 --timebonus to prevent the hard drops in fitness when exiting a pipe
- maxcounter = 0 --species that reach the max fitness
- currentTracker = 0 --tracks location on screen
- loop = false --flag for when a loop is detected
- previousmaxfitness = 0
- bottleneckcounter = 0
- MutateConnectionsChance = 0.25
- PerturbChance = 0.90
- CrossoverChance = 0.75
- LinkMutationChance = 2.0
- NodeMutationChance = 0.50
- BiasMutationChance = 0.40
- StepSize = 0.1
- DisableMutationChance = 0.4
- EnableMutationChance = 0.2
- oscilationmutationchance = 0.2
- interbreedchance = 0.03
- TimeoutConstant = 120 --set to a higher value but see for yourself what is acceptable
- MaxNodes = 1000000
- function getPositions()
- marioX = memory.readbyte(0x6D) * 0x100 + memory.readbyte(0x86)
- marioY = memory.readbyte(0x03B8)+16
- mariostate = memory.readbyte(0x0E) --get mario's state. (entering exiting pipes, dying, picking up a mushroom etc etc)
- currentscreen = memory.readbyte(0x071A) --finds current screen for loop detection
- nextscreen = memory.readbyte(0x071B) --finds next screen
- CurrentWorld = memory.readbyte(0x075F) --finds the current world (value + 1 is world)
- CurrentLevel = memory.readbyte(0x0760) --finds the current level (0=1 2=2 3=3 4=4)
- demoruncheck = memory.readbyte(0x0770) --equals 0 for demo and 1 for normal run
- screenX = memory.readbyte(0x03AD)
- screenY = memory.readbyte(0x03B8)
- end
- function getTile(dx, dy)
- local x = marioX + dx + 8
- local y = marioY + dy - 16
- local page = math.floor(x/256)%2
- local subx = math.floor((x%256)/16)
- local suby = math.floor((y - 32)/16)
- local addr = 0x500 + page*13*16+suby*16+subx
- if suby >= 13 or suby < 0 then
- return 0
- end
- if memory.readbyte(addr) ~= 0 then
- return 1
- else
- return 0
- end
- end
- function getSprites()
- local sprites = {}
- for slot=0,4 do
- local enemy = memory.readbyte(0xF+slot)
- if enemy ~= 0 then
- local ex = memory.readbyte(0x6E + slot)*0x100 + memory.readbyte(0x87+slot)
- local ey = memory.readbyte(0xCF + slot)+36 --changed this to stop sprites from floating in mid air on the neural network
- sprites[#sprites+1] = {["x"]=ex,["y"]=ey}
- end
- end
- return sprites
- end
- function getInputs()
- getPositions()
- sprites = getSprites()
- local inputs = {}
- for dy=-BoxRadius*16,BoxRadius*16,16 do
- for dx=-BoxRadius*16,BoxRadius*16,16 do
- inputs[#inputs+1] = 0
- tile = getTile(dx, dy)
- if tile == 1 and marioY+dy < 0x1B0 then
- inputs[#inputs] = 1
- end
- for i = 1,#sprites do
- distx = math.abs(sprites[i]["x"] - (marioX+dx))
- disty = math.abs(sprites[i]["y"] - (marioY+dy+16))
- if distx <= 8 and disty <= 8 then
- inputs[#inputs] = -1
- end
- end
- end
- end
- --mariovx = memory.read_s8(0x7B)
- --mariovy = memory.read_s8(0x7D)
- return inputs
- end
- function sigmoid(x)
- return 2/(1+math.exp(-4.9*x))-1
- end
- function newInnovation()
- pool.innovation = pool.innovation + 1
- return pool.innovation
- end
- function newPool()
- local pool = {}
- pool.species = {}
- pool.generation = 0
- pool.innovation = Outputs
- pool.currentSpecies = 1
- pool.currentGenome = 1
- pool.currentFrame = 0
- pool.maxFitness = 0
- pool.maxcounter = 0 --counter for %max species reached
- return pool
- end
- function newSpecies()
- local species = {}
- species.topFitness = 0
- species.staleness = 0
- species.genomes = {}
- species.averageFitness = 0
- return species
- end
- function getCurrentGenome()
- local species = pool.species[pool.currentSpecies]
- return species.genomes[pool.currentGenome]
- end
- function toRGBA(ARGB)
- return bit.lshift(ARGB, 8) + bit.rshift(ARGB, 24)
- end
- function newGenome()
- local genome = {}
- genome.genes = {}
- genome.fitness = 0
- genome.adjustedFitness = 0
- genome.network = {}
- genome.maxneuron = 0
- genome.globalRank = 0
- genome.oscilations = {}
- for i=1,#switchtime do
- genome.oscilations[i] = switchtime[i]
- end
- genome.mutationRates = {}
- genome.mutationRates["connections"] = MutateConnectionsChance
- genome.mutationRates["link"] = LinkMutationChance
- genome.mutationRates["bias"] = BiasMutationChance
- genome.mutationRates["node"] = NodeMutationChance
- genome.mutationRates["enable"] = EnableMutationChance
- genome.mutationRates["disable"] = DisableMutationChance
- genome.mutationRates["step"] = StepSize
- genome.mutationRates["oscilation"] = oscilationmutationchance
- return genome
- end
- function copyGenome(genome)
- local genome2 = newGenome()
- for g=1,#genome.genes do
- table.insert(genome2.genes, copyGene(genome.genes[g]))
- end
- for i=1,#genome.oscilations do
- genome2.oscilations[i] = genome.oscilations[i]
- end
- genome2.maxneuron = genome.maxneuron
- genome2.mutationRates["connections"] = genome.mutationRates["connections"]
- genome2.mutationRates["link"] = genome.mutationRates["link"]
- genome2.mutationRates["bias"] = genome.mutationRates["bias"]
- genome2.mutationRates["node"] = genome.mutationRates["node"]
- genome2.mutationRates["enable"] = genome.mutationRates["enable"]
- genome2.mutationRates["disable"] = genome.mutationRates["disable"]
- genome2.mutationRates["oscilation"] = genome.mutationRates["oscilation"]
- return genome2
- end
- function basicGenome()
- local genome = newGenome()
- local innovation = 1
- genome.maxneuron = Inputs
- mutate(genome)
- return genome
- end
- function newGene()
- local gene = {}
- gene.into = 0
- gene.out = 0
- gene.weight = 0.0
- gene.enabled = true
- gene.innovation = 0
- return gene
- end
- function copyGene(gene)
- local gene2 = newGene()
- gene2.into = gene.into
- gene2.out = gene.out
- gene2.weight = gene.weight
- gene2.enabled = gene.enabled
- gene2.innovation = gene.innovation
- return gene2
- end
- function newNeuron()
- local neuron = {}
- neuron.incoming = {}
- neuron.value = 0.0
- neuron.switcher = false
- neuron.timer = 60
- neuron.multiplier = 1
- return neuron
- end
- function generateNetwork(genome)
- local network = {}
- network.neurons = {}
- for i=1,Inputs do
- network.neurons[i] = newNeuron()
- end
- for o=1,Outputs do
- network.neurons[MaxNodes+o] = newNeuron()
- end
- table.sort(genome.genes, function (a,b)
- return (a.out < b.out)
- end)
- for i=1,#genome.genes do
- local gene = genome.genes[i]
- if gene.enabled then
- if network.neurons[gene.out] == nil then
- network.neurons[gene.out] = newNeuron()
- end
- local neuron = network.neurons[gene.out]
- table.insert(neuron.incoming, gene)
- if network.neurons[gene.into] == nil then
- network.neurons[gene.into] = newNeuron()
- end
- end
- end
- genome.network = network
- end
- function evaluateNetwork(network, inputs, genome)
- table.insert(inputs, 1) -- bias input node
- for i = 1,#initialswitchvalue do
- if genome.oscilations[i] > 0 then
- if switchtimer[i] > 0 then
- switchtimer[i] = switchtimer[i] -1
- else
- switchtimer[i] = genome.oscilations[i]
- initialswitchvalue[i] = initialswitchvalue[i]*-1
- end
- end
- table.insert(inputs, initialswitchvalue[i])
- end
- if #inputs ~= Inputs then
- emu.print("Incorrect number of neural network inputs.")
- return {}
- end
- for i=1,Inputs do
- network.neurons[i].value = inputs[i]
- end
- for _,neuron in pairs(network.neurons) do
- local sum = 0
- for j = 1,#neuron.incoming do
- local incoming = neuron.incoming[j]
- local other = network.neurons[incoming.into]
- sum = sum + incoming.weight * other.value
- end
- if #neuron.incoming > 0 then
- if neuron.switcher and sigmoid(sum)>0 then
- neuron.timer = neuron.timer - 1
- elseif neuron.switcher then
- neuron.timer = 120
- neuron.multiplier = 1
- end
- if neuron.switcher and neuron.timer < 0 then
- neuron.multiplier = -neuron.multiplier
- neuron.timer = 10
- end
- neuron.value = sigmoid(sum) * neuron.multiplier
- end
- end
- local outputs = {}
- for o=1,Outputs do
- local button = ButtonNames[o]
- if network.neurons[MaxNodes+o].value > 0 then
- outputs[button] = true
- else
- outputs[button] = false
- end
- end
- return outputs
- end
- function crossover(g1, g2)
- -- Make sure g1 is the higher fitness genome
- if g2.fitness > g1.fitness then
- tempg = g1
- g1 = g2
- g2 = tempg
- end
- local child = newGenome()
- for i=1,#g1.oscilations do
- child.oscilations[i] = g1.oscilations[i]
- end
- local innovations2 = {}
- for i=1,#g2.genes do
- local gene = g2.genes[i]
- innovations2[gene.innovation] = gene
- end
- for i=1,#g1.genes do
- local gene1 = g1.genes[i]
- local gene2 = innovations2[gene1.innovation]
- if gene2 ~= nil and math.random(2) == 1 and gene2.enabled then
- table.insert(child.genes, copyGene(gene2))
- else
- table.insert(child.genes, copyGene(gene1))
- end
- end
- child.maxneuron = math.max(g1.maxneuron,g2.maxneuron)
- for mutation,rate in pairs(g1.mutationRates) do
- child.mutationRates[mutation] = rate
- end
- return child
- end
- function randomNeuron(genes, nonInput)
- local neurons = {}
- if not nonInput then
- for i=1,Inputs do
- neurons[i] = true
- end
- end
- for o=1,Outputs do
- neurons[MaxNodes+o] = true
- end
- for i=1,#genes do
- if (not nonInput) or genes[i].into > Inputs then
- neurons[genes[i].into] = true
- end
- if (not nonInput) or genes[i].out > Inputs then
- neurons[genes[i].out] = true
- end
- end
- local count = 0
- for _,_ in pairs(neurons) do
- count = count + 1
- end
- local n = math.random(1, count)
- for k,v in pairs(neurons) do
- n = n-1
- if n == 0 then
- return k
- end
- end
- return 0
- end
- function containsLink(genes, link)
- for i=1,#genes do
- local gene = genes[i]
- if gene.into == link.into and gene.out == link.out then
- return true
- end
- end
- end
- function pointMutate(genome)
- local step = genome.mutationRates["step"]
- for i=1,#genome.genes do
- local gene = genome.genes[i]
- if math.random() < PerturbChance then
- gene.weight = gene.weight + math.random() * step*2 - step
- else
- gene.weight = math.random()*4-2
- end
- end
- end
- function linkMutate(genome, forceBias)
- local neuron1 = randomNeuron(genome.genes, false)
- local neuron2 = randomNeuron(genome.genes, true)
- local newLink = newGene()
- if neuron1 <= Inputs and neuron2 <= Inputs then
- --Both input nodes
- return
- end
- if neuron2 <= Inputs then
- -- Swap output and input
- local temp = neuron1
- neuron1 = neuron2
- neuron2 = temp
- end
- newLink.into = neuron1
- newLink.out = neuron2
- if forceBias then
- newLink.into = Inputs
- end
- if containsLink(genome.genes, newLink) then
- return
- end
- newLink.innovation = newInnovation()
- newLink.weight = math.random()*4-2
- table.insert(genome.genes, newLink)
- end
- function nodeMutate(genome)
- if #genome.genes == 0 then
- return
- end
- genome.maxneuron = genome.maxneuron + 1
- local gene = genome.genes[math.random(1,#genome.genes)]
- if not gene.enabled then
- return
- end
- gene.enabled = false
- local gene1 = copyGene(gene)
- gene1.out = genome.maxneuron
- gene1.weight = 1.0
- gene1.innovation = newInnovation()
- gene1.enabled = true
- table.insert(genome.genes, gene1)
- local gene2 = copyGene(gene)
- gene2.into = genome.maxneuron
- gene2.innovation = newInnovation()
- gene2.enabled = true
- table.insert(genome.genes, gene2)
- end
- function enableDisableMutate(genome, enable)
- local candidates = {}
- for _,gene in pairs(genome.genes) do
- if gene.enabled == not enable then
- table.insert(candidates, gene)
- end
- end
- if #candidates == 0 then
- return
- end
- local gene = candidates[math.random(1,#candidates)]
- gene.enabled = not gene.enabled
- end
- function oscilationMutate(genome)
- mutationpoint = math.random(1,#genome.oscilations)
- if genome.oscilations[mutationpoint]>0 then
- if math.random(1,2) == 1 then
- genome.oscilations[mutationpoint] = genome.oscilations[mutationpoint] + 1
- else
- genome.oscilations[mutationpoint] = genome.oscilations[mutationpoint] - 1
- end
- end
- if genome.oscilations[mutationpoint]<0 then
- genome.oscilations[mutationpoint] = 0
- end
- end
- function mutate(genome)
- for mutation,rate in pairs(genome.mutationRates) do
- if math.random(1,2) == 1 then
- genome.mutationRates[mutation] = 0.95*rate
- else
- genome.mutationRates[mutation] = 1.05263*rate
- end
- end
- if math.random() < genome.mutationRates["connections"] then
- pointMutate(genome)
- end
- local p = genome.mutationRates["link"]
- while p > 0 do
- if math.random() < p then
- linkMutate(genome, false)
- end
- p = p - 1
- end
- p = genome.mutationRates["bias"]
- while p > 0 do
- if math.random() < p then
- linkMutate(genome, true)
- end
- p = p - 1
- end
- p = genome.mutationRates["node"]
- while p > 0 do
- if math.random() < p then
- nodeMutate(genome)
- end
- p = p - 1
- end
- p = genome.mutationRates["enable"]
- while p > 0 do
- if math.random() < p then
- enableDisableMutate(genome, true)
- end
- p = p - 1
- end
- p = genome.mutationRates["disable"]
- while p > 0 do
- if math.random() < p then
- enableDisableMutate(genome, false)
- end
- p = p - 1
- end
- p = genome.mutationRates["oscilation"]
- while p > 0 do
- if math.random() < p then
- oscilationMutate(genome)
- end
- p = p - 1
- end
- end
- function disjoint(genes1, genes2)
- local i1 = {}
- for i = 1,#genes1 do
- local gene = genes1[i]
- i1[gene.innovation] = true
- end
- local i2 = {}
- for i = 1,#genes2 do
- local gene = genes2[i]
- i2[gene.innovation] = true
- end
- local disjointGenes = 0
- for i = 1,#genes1 do
- local gene = genes1[i]
- if not i2[gene.innovation] then
- disjointGenes = disjointGenes+1
- end
- end
- for i = 1,#genes2 do
- local gene = genes2[i]
- if not i1[gene.innovation] then
- disjointGenes = disjointGenes+1
- end
- end
- local n = math.max(#genes1, #genes2)
- return disjointGenes / n
- end
- function weights(genes1, genes2)
- local i2 = {}
- for i = 1,#genes2 do
- local gene = genes2[i]
- i2[gene.innovation] = gene
- end
- local sum = 0
- local coincident = 0
- for i = 1,#genes1 do
- local gene = genes1[i]
- if i2[gene.innovation] ~= nil then
- local gene2 = i2[gene.innovation]
- sum = sum + math.abs(gene.weight - gene2.weight)
- coincident = coincident + 1
- end
- end
- return sum / coincident
- end
- function sameSpecies(genome1, genome2)
- local dd = DeltaDisjoint*disjoint(genome1.genes, genome2.genes)
- local dw = DeltaWeights*weights(genome1.genes, genome2.genes)
- return dd + dw < DeltaThreshold
- end
- function rankGlobally()
- local global = {}
- for s = 1,#pool.species do
- local species = pool.species[s]
- for g = 1,#species.genomes do
- table.insert(global, species.genomes[g])
- end
- end
- table.sort(global, function (a,b)
- return (a.fitness < b.fitness)
- end)
- for g=1,#global do
- global[g].globalRank = g
- end
- end
- function calculateAverageFitness(species)
- local total = 0
- for g=1,#species.genomes do
- local genome = species.genomes[g]
- total = total + genome.globalRank
- end
- species.averageFitness = total / #species.genomes
- end
- function totalAverageFitness()
- local total = 0
- for s = 1,#pool.species do
- local species = pool.species[s]
- total = total + species.averageFitness
- end
- return total
- end
- function cullSpecies(cutToOne)
- for s = 1,#pool.species do
- local species = pool.species[s]
- table.sort(species.genomes, function (a,b)
- return (a.fitness > b.fitness)
- end)
- local remaining = math.ceil(#species.genomes/2)
- if cutToOne then
- remaining = 1
- end
- while #species.genomes > remaining do
- table.remove(species.genomes)
- end
- end
- end
- function breedChild(species)
- local child = {}
- if math.random() < CrossoverChance then
- g1 = species.genomes[math.random(1, #species.genomes)]
- g2 = species.genomes[math.random(1, #species.genomes)]
- child = crossover(g1, g2)
- else
- g = species.genomes[math.random(1, #species.genomes)]
- child = copyGenome(g)
- end
- mutate(child)
- return child
- end
- function secondbestspecies() --added this to help prevent the formation of monospecies when MarI/O encounters a major bottleneck
- local bestfitness = 0
- local secondbestfitness = 0
- for s = 1,#pool.species do
- local species = pool.species[s]
- if species.topFitness > bestfitness then
- secondbestfitness = bestfitness
- bestfitness = species.topFitness
- elseif species.topFitness > secondbestfitness then
- secondbestfitness = species.topFitness
- end
- end
- return secondbestfitness
- end
- function removeStaleSpecies()
- local survived = {}
- secondbest = secondbestspecies()
- emu.print("Max Fitness: ".. pool.maxFitness ..". second best: ".. secondbest)
- for s = 1,#pool.species do
- local species = pool.species[s]
- table.sort(species.genomes, function (a,b)
- return (a.fitness > b.fitness)
- end)
- if species.genomes[1].fitness > species.topFitness then
- species.topFitness = species.genomes[1].fitness
- species.staleness = 0
- else
- species.staleness = species.staleness + 1
- end
- if species.staleness < StaleSpecies or species.topFitness >= secondbest then --originally species.topFitness >= pool.maxFitness
- table.insert(survived, species)
- end
- end
- pool.species = survived
- end
- function removeWeakSpecies()
- local survived = {}
- local sum = totalAverageFitness()
- for s = 1,#pool.species do
- local species = pool.species[s]
- breed = math.floor(species.averageFitness / sum * Population)
- if breed >= 1 then
- table.insert(survived, species)
- end
- end
- pool.species = survived
- end
- function addToSpecies(child)
- local foundSpecies = false
- for s=1,#pool.species do
- local species = pool.species[s]
- if not foundSpecies and sameSpecies(child, species.genomes[1]) then
- table.insert(species.genomes, child)
- foundSpecies = true
- end
- end
- if not foundSpecies then
- local childSpecies = newSpecies()
- table.insert(childSpecies.genomes, child)
- table.insert(pool.species, childSpecies)
- end
- end
- function newGeneration()
- if pool.maxFitness > previousmaxfitness then
- previousmaxfitness = pool.maxFitness
- bottleneckcounter = 0
- if AllowAutoTurbo then
- emu.speedmode("normal")
- turbo = false
- end
- else
- bottleneckcounter = bottleneckcounter + 1
- end
- if bottleneckcounter < 7 and Population > 300 then
- if pool.generation < 5 then
- Population = math.floor(Population * 0.9)
- else
- Population = math.floor(Population * 0.95)
- end
- if Population < 300 then
- Population = 300
- end
- elseif bottleneckcounter > 10 and Population <900 then
- Population = math.floor(Population * 1.10)
- if Population > 900 then
- Population = 900
- end
- end
- if bottleneckcounter > 20 then
- bottleneckcounter = 0
- end
- if bottleneckcounter > TurboGenBottleneck and AllowAutoTurbo and BottleneckTurbo then
- emu.speedmode("turbo")
- turbo = true
- end
- cullSpecies(false) -- Cull the bottom half of each species
- rankGlobally()
- removeStaleSpecies()
- rankGlobally()
- for s = 1,#pool.species do
- local species = pool.species[s]
- calculateAverageFitness(species)
- end
- removeWeakSpecies()
- local sum = totalAverageFitness()
- local children = {}
- for s = 1,#pool.species do
- local species = pool.species[s]
- breed = math.floor(species.averageFitness / sum * Population) - 1
- for i=1,breed do
- table.insert(children, breedChild(species))
- end
- end
- cullSpecies(true) -- Cull all but the top member of each species
- if #pool.species < 30 then
- interbreedchance = 0.1
- else
- interbreedchance = 0.03
- end
- for i=1,#pool.species do
- if math.random() < interbreedchance then
- local counterbreed = math.random(1, #pool.species)
- if counterbreed ~= i then
- table.insert(children, crossover(pool.species[i].genomes[1], pool.species[counterbreed].genomes[1]))
- end
- end
- end
- while #children + #pool.species < Population do
- local species = pool.species[math.random(1, #pool.species)]
- table.insert(children, breedChild(species))
- end
- for c=1,#children do
- local child = children[c]
- addToSpecies(child)
- end
- pool.generation = pool.generation + 1
- pool.maxcounter = 0 --reset max fitness counter
- writeFile("backups/backup." .. pool.generation .. "." .. SAVE_LOAD_FILE)
- writelatestgen() --tracker for the latest generation
- end
- function initializePool()
- pool = newPool()
- for i=1,Population do
- basic = basicGenome()
- addToSpecies(basic)
- end
- initializeRun()
- end
- function clearJoypad()
- controller = {}
- for b = 1,#ButtonNames do
- controller[ButtonNames[b]] = false
- end
- joypad.set(player, controller)
- end
- function initializeRun()
- savestate.load(SavestateObj);
- rightmost = 0
- pool.currentFrame = 0
- timeout = TimeoutConstant
- clearJoypad()
- currentTracker = memory.readbyte(0x071A) --sets the current tracker to the current screen
- local species = pool.species[pool.currentSpecies]
- local genome = species.genomes[pool.currentGenome]
- for i=1,#genome.oscilations do
- switchtimer[i] = genome.oscilations[i]
- initialswitchvalue[i] = 1
- end
- generateNetwork(genome)
- evaluateCurrent()
- end
- function evaluateCurrent()
- local species = pool.species[pool.currentSpecies]
- local genome = species.genomes[pool.currentGenome]
- inputs = getInputs()
- controller = evaluateNetwork(genome.network, inputs, genome)
- if controller["left"] and controller["right"] then
- controller["left"] = false
- controller["right"] = false
- end
- if controller["up"] and controller["down"] then
- controller["up"] = false
- controller["down"] = false
- end
- joypad.set(player, controller)
- end
- if pool == nil then
- initializePool()
- end
- function nextGenome()
- pool.currentGenome = pool.currentGenome + 1
- if pool.currentGenome > #pool.species[pool.currentSpecies].genomes then
- pool.currentGenome = 1
- pool.currentSpecies = pool.currentSpecies+1
- if pool.currentSpecies > #pool.species then
- newGeneration()
- pool.currentSpecies = 1
- end
- end
- end
- function fitnessAlreadyMeasured()
- local species = pool.species[pool.currentSpecies]
- local genome = species.genomes[pool.currentGenome]
- return genome.fitness ~= 0
- end
- function GenerateNewKeepBest(genome)
- Population = 1000
- previousbest = copyGenome(genome)
- previousmaxfitness = 1
- maxright = 1
- offset = 0 --offset in x coordinates for pipe
- timebonus = 0 --used to freeze fitness
- mariohole = false --reset the fallen into hole variable
- mariopipe = false --is mario exiting a pipe or just teleporting
- loop = false --is this a loop?
- marioPipeEnter = false --has mario entered a pipe
- currentTracker = 1 --tracker for the loop
- pool = newPool()
- addToSpecies(previousbest)
- for i=1,Population-1 do
- basic = basicGenome()
- addToSpecies(basic)
- end
- initializeRun()
- end
- function displayGenome(genome)
- gui.opacity(0.53)
- if not genome then return end
- local network = genome.network
- local cells = {}
- local i = 1
- local cell = {}
- for dy=-BoxRadius,BoxRadius do
- for dx=-BoxRadius,BoxRadius do
- cell = {}
- cell.x = 50+5*dx
- cell.y = 70+5*dy
- cell.value = network.neurons[i].value
- cells[i] = cell
- i = i + 1
- end
- end
- local biasCell = {}
- biasCell.x = 80
- biasCell.y = 110
- biasCell.value = network.neurons[Inputs-#initialswitchvalue].value
- cells[Inputs-#initialswitchvalue] = biasCell
- for i=0,#initialswitchvalue-1 do
- local switchCell = {}
- switchCell.x = 80-10*#initialswitchvalue+10*i
- switchCell.y = 110
- switchCell.value = network.neurons[Inputs-i].value
- cells[Inputs-i] = switchCell
- end
- gui.drawbox(215,32,245,82,toRGBA(0x80808080),toRGBA(0x00000000))
- gui.opacity(0.85)
- for o = 1,Outputs do
- cell = {}
- cell.x = 220
- cell.y = 30 + 8 * o
- cell.value = network.neurons[MaxNodes + o].value
- cells[MaxNodes+o] = cell
- local color
- if cell.value > 0 then
- color = 0xFF0000FF
- else
- color = 0xFF777777-- original color = 0xFF000000
- end
- gui.drawtext(225, 26+8*o, ButtonNames[o], toRGBA(color), 0x0) --moved slightly for better alignment
- end
- for n,neuron in pairs(network.neurons) do
- cell = {}
- if n > Inputs and n <= MaxNodes then
- cell.x = 140
- cell.y = 40
- cell.value = neuron.value
- -- if neuron.value >0 then
- -- neuron.switcher = true
- -- end
- cells[n] = cell
- end
- end
- for n=1,4 do
- for _,gene in pairs(genome.genes) do
- if gene.enabled then
- local c1 = cells[gene.into]
- local c2 = cells[gene.out]
- if gene.into > Inputs and gene.into <= MaxNodes then
- c1.x = 0.75*c1.x + 0.25*c2.x
- if c1.x >= c2.x then
- c1.x = c1.x - 40
- end
- if c1.x < 90 then
- c1.x = 90
- end
- if c1.x > 220 then
- c1.x = 220
- end
- c1.y = 0.75*c1.y + 0.25*c2.y
- end
- if gene.out > Inputs and gene.out <= MaxNodes then
- c2.x = 0.25*c1.x + 0.75*c2.x
- if c1.x >= c2.x then
- c2.x = c2.x + 40
- end
- if c2.x < 90 then
- c2.x = 90
- end
- if c2.x > 220 then
- c2.x = 220
- end
- c2.y = 0.25*c1.y + 0.75*c2.y
- end
- end
- end
- end
- gui.drawbox(50-BoxRadius*5-3,70-BoxRadius*5-3,50+BoxRadius*5+2,70+BoxRadius*5+2, toRGBA(0x80808080), toRGBA(0xFF000000))
- for n,cell in pairs(cells) do
- if n > Inputs or cell.value ~= 0 then
- local color = math.floor((cell.value+1)/2*256)
- if color > 255 then color = 255 end
- if color < 0 then color = 0 end
- local opacity = 0xFF000000
- if cell.value == 0 then
- opacity = 0x50000000
- end
- color = opacity + color*0x10000 + color*0x100 + color
- gui.drawbox(cell.x-2,cell.y-2,cell.x+2,cell.y+2,toRGBA(color), toRGBA(opacity))
- end
- end
- for _,gene in pairs(genome.genes) do
- if gene.enabled then
- local c1 = cells[gene.into]
- local c2 = cells[gene.out]
- local opacity = 0xF0000000
- if pool.generation > 50 then
- opacity = 0x80000000
- end
- if c1.value == 0 then
- if pool.generation <10 then --slowly reducing the visibility of non active connections
- opacity = 0x80000000
- elseif pool.generation < 15 then
- opacity = 0x60000000
- elseif pool.generation < 25 then
- opacity = 0x40000000
- end
- end
- local color = 0x80-math.floor(math.abs(sigmoid(gene.weight))*0x80)
- if gene.weight > 0 then
- color = opacity + 0x8000 + 0x10000*color
- else
- color = opacity + 0x800000 + 0x100*color
- end
- if c1.value ~= 0 or pool.generation < 50 then -- remove non active connections from generation 50 onwards
- gui.drawline(c1.x+1, c1.y, c2.x-3, c2.y, toRGBA(color))
- end
- end
- end
- gui.drawbox(49,71,51,78,toRGBA(0x80FF0000),toRGBA(0x00000000))
- if mutation_rates_disp then
- local pos = 120
- for mutation,rate in pairs(genome.mutationRates) do
- gui.drawtext(16, pos, mutation .. ": " .. rate, toRGBA(0xFF000000), 0x0)
- pos = pos + 8
- end
- end
- end
- function writeFile(filename)
- local file = io.open(filename, "w")
- file:write(pool.generation .. "\n")
- file:write(pool.maxFitness .. "\n")
- file:write(Population .. "\n")
- file:write(bottleneckcounter .. "\n")
- file:write(#pool.species .. "\n")
- for n,species in pairs(pool.species) do
- file:write(species.topFitness .. "\n")
- file:write(species.staleness .. "\n")
- file:write(#species.genomes .. "\n")
- for m,genome in pairs(species.genomes) do
- file:write(genome.fitness .. "\n")
- file:write(genome.maxneuron .. "\n")
- for mutation,rate in pairs(genome.mutationRates) do
- file:write(mutation .. "\n")
- file:write(rate .. "\n")
- end
- file:write("done\n")
- file:write(#genome.genes .. "\n")
- for l,gene in pairs(genome.genes) do
- file:write(gene.into .. " ")
- file:write(gene.out .. " ")
- file:write(gene.weight .. " ")
- file:write(gene.innovation .. " ")
- if(gene.enabled) then
- file:write("1\n")
- else
- file:write("0\n")
- end
- end
- file:write(#genome.oscilations .. "\n")
- for i=1,#genome.oscilations do
- file:write(genome.oscilations[i] .. "\n")
- end
- end
- end
- file:close()
- end
- function savePool() --used to save when a new max fitness is reached
- local filename = "backups/backup." .. pool.generation .. "." .. SAVE_LOAD_FILE
- writeFile(filename)
- emu.print("saved pool due to new max fitness") --suggestion by DeltaLeeds
- end
- function loadFile(filename)
- local file = io.open(filename, "r")
- pool = newPool()
- pool.generation = file:read("*number")
- pool.maxFitness = file:read("*number")
- Population = file:read("*number")
- bottleneckcounter = file:read("*number")
- local numSpecies = file:read("*number")
- for s=1,numSpecies do
- local species = newSpecies()
- table.insert(pool.species, species)
- species.topFitness = file:read("*number")
- species.staleness = file:read("*number")
- local numGenomes = file:read("*number")
- for g=1,numGenomes do
- local genome = newGenome()
- table.insert(species.genomes, genome)
- genome.fitness = file:read("*number")
- genome.maxneuron = file:read("*number")
- local line = file:read("*line")
- while line ~= "done" do
- genome.mutationRates[line] = file:read("*number")
- line = file:read("*line")
- end
- local numGenes = file:read("*number")
- for n=1,numGenes do
- local gene = newGene()
- table.insert(genome.genes, gene)
- local enabled
- --I am able to revert back to the original loading script with FCEUX
- gene.into, gene.out, gene.weight, gene.innovation, enabled = file:read("*number", "*number", "*number", "*number", "*number")
- if enabled == 0 then
- gene.enabled = false
- else
- gene.enabled = true
- end
- end
- local numosci = file:read("*number")
- for i=1,numosci do
- genome.oscilations[i] = file:read("*number")
- end
- end
- end
- file:close()
- while fitnessAlreadyMeasured() do
- nextGenome()
- end
- initializeRun()
- pool.currentFrame = pool.currentFrame + 1
- end
- function playTop()
- local maxfitness = 0
- local maxs, maxg
- for s,species in pairs(pool.species) do
- for g,genome in pairs(species.genomes) do
- if genome.fitness > maxfitness then
- maxfitness = genome.fitness
- maxs = s
- maxg = g
- end
- end
- end
- pool.currentSpecies = maxs
- pool.currentGenome = maxg
- pool.maxFitness = maxfitness
- initializeRun()
- pool.currentFrame = pool.currentFrame + 1
- return
- end
- function fitnesstracker()
- if pool.maxFitness > 0 then
- local file = io.open('fitnesstracker.txt', "a")
- file:write("Gen: " .. pool.generation .. " Species: " .. pool.currentSpecies .. " Genome: " .. pool.currentGenome .. " Fitness: ".. pool.maxFitness .. "\n")
- file:close()
- end
- end
- function writelatestgen() --used to write the generation tracker to a file
- local file = io.open('backups/latestgen', "w")
- file:write(SAVE_LOAD_FILE .. "\n")
- file:write(savestate_slot .. "\n")
- file:write(pool.generation)
- file:close()
- end
- function readlatestgen() --used to retrieve the latest generation from a file
- local file = io.open('backups/latestgen', "r")
- SAVE_LOAD_FILE = file:read("*line")
- savestate_slot = file:read("*number")
- SavestateObj = savestate.object(savestate_slot)
- local latestgen = file:read("*number")
- file:close()
- local name = 'backups/backup.'.. latestgen .. '.' .. SAVE_LOAD_FILE
- print('loaded: '.. name)
- return name
- end
- function keyboardinput()
- local keyboard = input.get()
- if keyboard['N'] and keyflag == false then
- if neural_net_disp then
- neural_net_disp = false
- else
- neural_net_disp = true
- end
- keyflag = true
- end
- if keyboard['M'] and keyflag == false then
- if mutation_rates_disp then
- mutation_rates_disp = false
- else
- mutation_rates_disp = true
- end
- keyflag = true
- end
- if keyboard['L'] and keyflag == false then
- if not nopopup then
- local loadyn = input.popup('Are you sure you want to load the last saved generation?')
- end
- if loadyn == 'yes' or nopopup then
- name = readlatestgen()
- loadFile(name)
- end
- keyflag = true
- end
- if keyboard['B'] and keyflag == false then
- if not nopopup then
- local loadyn = input.popup('Are you sure you want to show the topFitness genome?')
- end
- if loadyn == 'yes' or nopopup then
- playTop()
- end
- end
- if not (keyboard['N'] or keyboard['L'] or keyboard['M'] or keyboard['T'] or keyboard['B']) and keyflag == true then
- keyflag = false
- end
- if keyboard['T'] and keyflag == false then
- if turbo then
- emu.speedmode("normal")
- turbo = false
- print('stop turbo')
- elseif not turbo and not restoreturbo then
- emu.speedmode("turbo")
- turbo = true
- elseif restoreturbo then
- restoreturbo = false
- end
- keyflag = true
- end
- end
- if savedpool then
- loadFile(savedpool)
- end
- writeFile("temp.pool")
- while true do
- keyboardinput()
- local backgroundColor = toRGBA(0xD0FFFFFF)
- gui.drawbox(0, 0, 260, 30, backgroundColor, backgroundColor)
- if pool.maxcounter == nil then
- pool.maxcounter = 0
- emu.print("caught missing variable")
- end
- local species = pool.species[pool.currentSpecies]
- local genome = species.genomes[pool.currentGenome]
- if neural_net_disp then
- displayGenome(genome)
- end
- if pool.generation == 0 and pool.currentSpecies ~= 1 and not turbo and AllowAutoTurbo and FirstGenSpeedup then
- emu.speedmode("turbo")
- turbo = true
- -- elseif pool.generation == 2 and turbo and AllowAutoTurbo and FirstGenSpeedup then
- -- emu.speedmode("normal")
- -- turbo = false
- end
- if pool.currentFrame%5 == 0 then
- evaluateCurrent()
- end
- joypad.set(player, controller)
- getPositions()
- if mariostate == 2 or mariostate == 3 then --detects when mario enters a pipe
- marioPipeEnter = true
- end
- if (CurrentWorld == 3 and CurrentLevel == 4) or (CurrentWorld == 6 and CurrentLevel == 4) or (CurrentWorld == 7 and CurrentLevel == 3) then
- if currentTracker == currentscreen or currentTracker == nextscreen and not mariopipe then --follows the progress of mario
- currentTracker = nextscreen
- elseif currentTracker > nextscreen and not mariopipe and not marioPipeEnter and mariostate ~= 7 then --if mario suddenly goes back trigger loop detection
- loop = true
- elseif currentscreen == 0 and nextscreen == 0 and mariopipe and mariostate ~= 7 then -- if mario enters a piperoom negate loop detection
- loop = false
- currentTracker = nextscreen
- elseif nextscreen >= (currentTracker - 1) and mariopipe and mariostate ~= 7 then -- if mario goes forward in the level by entering a pipe negate loop detection
- loop = false
- currentTracker = nextscreen
- pipeexit = true
- else --if nothing is true then we are in a loop
- if not pipeexit then --prevents the extra one frame flicker of the tracker
- loop = true
- else
- pipeexit = false
- end
- end
- end
- if marioPipeEnter == true and mariostate == 7 then --set mariopipe when exiting the pipe
- mariopipe = true
- end
- if mariopipe == true and mariostate ~= 7 and rightmost > marioX and not mariohole then --calculate offset
- offset = rightmost - marioX
- mariopipe = false
- marioPipeEnter = false
- elseif mariopipe == true and mariostate ~= 7 and rightmost <= marioX and not mariohole then --if the exit coordinates are higher than rightmost after exiting a pipe set offset to 0
- offset = 0
- mariopipe = false
- marioPipeEnter = false
- end
- if marioY + memory.readbyte(0xB5)*255 > 512 then --added the fallen into a hole variable that will activate when mario goes below screen
- mariohole = true
- end
- if marioX + offset > rightmost and mariohole == false and loop == false and marioPipeEnter == false and not killtrigger then
- rightmost = marioX + offset
- --if TimeoutConstant - timeout > 20 then --this is required for 4-4 so walking left will not give a big penalty
- timebonus = timebonus + (TimeoutConstant - timeout) * 11/20
- --end
- timeout = TimeoutConstant
- end
- if marioPipeEnter == true then --freeze fitness and timer when mario enters a pipe
- timeout = timeout + 1
- timebonus = timebonus + 2/3
- end
- if mariostate == 4 or endlevel then
- timeout = timeout +1
- timebonus = timebonus + 2/3
- endlevel = true
- end
- if endlevel and mariostate == 8 then
- SAVE_LOAD_FILE = "SMB".. CurrentWorld+1 .."-".. CurrentLevel ..".state.pool"
- savestate_slot = 3
- Savestatebackup = savestate.object(3)
- savestate.save(Savestatebackup)
- savestate.persist(Savestatebackup)
- savestate.save(SavestateObj)
- endlevel = false
- preparenext = true
- end
- if CurrentWorld == 0 and CurrentLevel == 0 and demoruncheck == 0 then
- savestate.load(SavestateObj)
- end
- timeout = timeout - 1
- if preparenext then
- preparenext = false
- GenerateNewKeepBest(genome)
- end
- if rightmost + offset > maxright*0.85 then
- maxright = rightmost + offset
- if turbo and maxright > 200 and AllowSlowdownNearMax then
- emu.speedmode("normal")
- turbo = false
- restoreturbo = true
- end
- elseif restoreturbo then
- emu.speedmode("turbo")
- turbo = true
- restoreturbo = false
- end
- local timeoutBonus = pool.currentFrame / 4
- if (mariostate == 11 or mariohole or math.floor(rightmost - (pool.currentFrame) / 2 + - (timeout + timeoutBonus)*2/3 +20 + timebonus) < -50) and timeout > 40-timeoutBonus then
- local temptimeout = timeout
- timeout = 30 - timeoutBonus
- local difference = temptimeout - timeout
- pool.currentFrame = pool.currentFrame + difference
- killtrigger = true
- end
- local timeoutBonus = pool.currentFrame / 4
- if timeout + timeoutBonus <= 0 then
- local fitness = rightmost - pool.currentFrame / 2 +20 + timebonus
- if fitness == 0 then
- fitness = -1
- end
- genome.fitness = fitness
- if fitness > pool.maxFitness then
- pool.maxFitness = fitness
- fitnesstracker()
- savePool()
- end
- if fitness > pool.maxFitness - 30 then
- pool.maxcounter = pool.maxcounter + 1
- end
- emu.print("Gen " .. pool.generation .. " species " .. pool.currentSpecies .. " genome " .. pool.currentGenome .. " fitness: " .. fitness)
- pool.currentSpecies = 1
- pool.currentGenome = 1
- while fitnessAlreadyMeasured() do
- offset = 0 --offset in x coordinates for pipe
- timebonus = 0 --used to freeze fitness
- mariohole = false --reset the fallen into hole variable
- mariopipe = false --is mario exiting a pipe or just teleporting
- loop = false --is this a loop?
- marioPipeEnter = false --has mario entered a pipe
- currentTracker = 1 --tracker for the loop
- killtrigger = false
- nextGenome()
- end
- initializeRun()
- end
- local measured = 0
- local total = 0
- for _,species in pairs(pool.species) do
- for _,genome in pairs(species.genomes) do
- total = total + 1
- if genome.fitness ~= 0 then
- measured = measured + 1
- end
- end
- end
- gui.opacity(1) --made the top banner opaque. you can change this if you want a slightly transparent banner
- if turbo then
- gui.drawtext(250, 11, "T", toRGBA(0xFFFF0000), 0x0)
- end
- gui.drawtext(5, 11, "Gen " .. pool.generation .. " species " .. pool.currentSpecies .. " genome " .. pool.currentGenome .. " (" .. math.floor(measured/total*100) .. "%) ".. "Pop: ".. Population, toRGBA(0xFF000000), 0x0)
- gui.drawtext(5, 20, "Fitness: " .. math.floor(rightmost - (pool.currentFrame) / 2 + - (timeout + timeoutBonus)*2/3 +20 + timebonus), toRGBA(0xFF000000), 0x0) --the (timeout + timeoutBonus)*2/3 makes sure that the displayed fitness remains stable for the viewers pleasure
- gui.drawtext(80, 20, "Max Fitness:" .. math.floor(pool.maxFitness) .. " (".. math.floor(secondbest) .. ")".. " ".. "(" .. math.ceil(pool.maxcounter/Population*100) .. "%)", toRGBA(0xFF000000), 0x0)
- pool.currentFrame = pool.currentFrame + 1
- emu.frameadvance();
- end
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement