Code-Akisame icon

LuigI/O version 1

Code-Akisame | PRO | 05/18/18 12:29:08 PM UTC | 0 ⭐ | 1159 👁️ | Never ⏰ | []
text |

48.05 KB

|

None

|

0 👍

/

0 👎

-- 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

Comments

  •  icon
    01/01/70 12:00:00 AM UTC
    Plain Text |

    0 B

    |

    👍

    /

    👎