commit c63f6a9325e085f9a739643b46e6aa28713c6efb Author: empathicqubit Date: Thu Feb 25 18:24:37 2021 -0500 Initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..7585af4 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +*.lsmv +*.log +catchem/ diff --git a/README.md b/README.md new file mode 100644 index 0000000..6c7f01c --- /dev/null +++ b/README.md @@ -0,0 +1,2 @@ +# Neat[Tinkering] +MarI/O for DKC2/lsnes. Not usable yet. diff --git a/config.lua b/config.lua new file mode 100644 index 0000000..6d727c6 --- /dev/null +++ b/config.lua @@ -0,0 +1,69 @@ +local _M = {} + +--[[ + Change BizhawkDir to your BizHawk directory. +--]] +--_M.BizhawkDir = "C:/Users/mmill/Downloads/BizHawk-2.2/" +_M.BizhawkDir = "X:/B2_BizHawkLab/BizHawk-2.2.2/" + +_M.StateDir = _M.BizhawkDir .. "Lua/SNES/neat-mario/state/" +_M.PoolDir = _M.BizhawkDir .. "Lua/SNES/neat-mario/pool/" + +--[[ + At the moment the first in list will get loaded. + Rearrange for other savestates. (will be redone soon) +--]] +_M.State = { + "DP1.state", -- Donut Plains 1 + "YI1.state", -- Yoshi's Island 1 + "YI2.state", -- Yoshi's Island 2 +} + +--[[ + Start game with specific powerup. + 0 = No powerup + 1 = Mushroom + 2 = Feather + 3 = Flower + Comment out to disable. +--]] +_M.StartPowerup = 0 + +_M.NeatConfig = { +--Filename = "DP1.state", +Filename = _M.PoolDir .. _M.State[1], +Population = 300, +DeltaDisjoint = 2.0, +DeltaWeights = 0.4, +DeltaThreshold = 1.0, +StaleSpecies = 15, +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, +TimeoutConstant = 20, +MaxNodes = 1000000, +} + +_M.ButtonNames = { + "A", + "B", + "X", + "Y", + "Up", + "Down", + "Left", + "Right", + } + +_M.BoxRadius = 6 +_M.InputSize = (_M.BoxRadius*2+1)*(_M.BoxRadius*2+1) + +_M.Running = false + +return _M \ No newline at end of file diff --git a/donkutil.lua b/donkutil.lua new file mode 100644 index 0000000..496d88b --- /dev/null +++ b/donkutil.lua @@ -0,0 +1,283 @@ +count = 0 +detailsidx = -1 +helddown = false +floatmode = false +pokemon = false +pokecount = 0 +showhelp = false +locked = false +lockdata = nil +incsprite = 0 +fgcolor = 0x00ffffff +bgcolor = 0x99000000 +function table_to_string(tbl) + local result = "{" + local keys = {} + for k in pairs(tbl) do + table.insert(keys, k) + end + table.sort(keys) + for _, k in ipairs(keys) do + local v = tbl[k] + if type(v) == "number" and v == 0 then + goto continue + end + + -- Check the key type (ignore any numerical keys - assume its an array) + if type(k) == "string" then + result = result.."[\""..k.."\"]".."=" + end + + -- Check the value type + if type(v) == "table" then + result = result..table_to_string(v) + elseif type(v) == "boolean" then + result = result..tostring(v) + else + result = result.."\""..v.."\"" + end + result = result..",\n" + ::continue:: + end + -- Remove leading commas from the result + if result ~= "" then + result = result:sub(1, result:len()-1) + end + return result.."}" +end + +function on_keyhook (key, state) + if not helddown and state["value"] == 1 then + if key == "1" and not locked then + helddown = true + detailsidx = detailsidx - 1 + if detailsidx < -1 then + detailsidx = 20 + end + elseif key == "2" and not locked then + helddown = true + detailsidx = detailsidx + 1 + if detailsidx > 20 then + detailsidx = -1 + end + elseif key == "3" then + helddown = true + incsprite = -1 + elseif key == "4" then + helddown = true + incsprite = 1 + elseif key == "5" then + helddown = true + if not locked then + locked = true + else + locked = false + lockdata = nil + end + elseif key == "6" then + helddown = true + pokemon = not pokemon + elseif key == "7" then + helddown = true + floatmode = not floatmode + elseif key == "0" then + showhelp = true + end + elseif state["value"] == 0 then + helddown = false + showhelp = false + end +end + +function on_input (subframe) + if floatmode then + memory.writebyte(0x7e19ce, 0x16) + memory.writebyte(0x7e0e12, 0x99) + memory.writebyte(0x7e0e70, 0x99) + if input.get(0, 6) == 1 then + memory.writeword(0x7e0e02, -0x5ff) + memory.writeword(0x7e0e60, -0x5ff) + + memory.writeword(0x7e0e06, 0) + memory.writeword(0x7e0e64, 0) + elseif input.get(0, 7) == 1 then + memory.writeword(0x7e0e02, 0x5ff) + memory.writeword(0x7e0e60, 0x5ff) + + memory.writeword(0x7e0e06, 0) + memory.writeword(0x7e0e64, 0) + end + + if input.get(0, 4) == 1 then + memory.writeword(0x7e0e06, -0x05ff) + memory.writeword(0x7e0e64, -0x05ff) + elseif input.get(0, 5) == 1 then + memory.writeword(0x7e0e06, 0x5ff) + memory.writeword(0x7e0e64, 0x5ff) + end + end +end + +function file_exists(name) + local f=io.open(name,"r") + if f~=nil then io.close(f) return true else return false end +end + +function get_sprite(base_addr) + return { + ["control"] = memory.readword(base_addr), + ["draworder"] = memory.readword(base_addr + 0x02), + ["x"] = memory.readword(base_addr + 0x06), + ["y"] = memory.readword(base_addr + 0x0a), + ["jumpheight"] = memory.readword(base_addr + 0x0e), + ["style"] = memory.readword(base_addr + 0x12), + ["currentframe"] = memory.readword(base_addr + 0x18), + ["nextframe"] = memory.readword(base_addr + 0x1a), + ["state"] = memory.readword(base_addr + 0x1e), + ["velox"] = memory.readsword(base_addr + 0x20), + ["veloy"] = memory.readsword(base_addr + 0x24), + ["velomaxx"] = memory.readsword(base_addr + 0x26), + ["velomaxy"] = memory.readsword(base_addr + 0x2a), + ["motion"] = memory.readword(base_addr + 0x2e), + ["attr"] = memory.readword(base_addr + 0x30), + ["animnum"] = memory.readword(base_addr + 0x36), + ["remainingframe"] = memory.readword(base_addr + 0x38), + ["animcontrol"] = memory.readword(base_addr + 0x3a), + ["animreadpos"] = memory.readword(base_addr + 0x3c), + ["animcontrol2"] = memory.readword(base_addr + 0x3e), + ["animformat"] = memory.readword(base_addr + 0x40), + ["damage1"] = memory.readword(base_addr + 0x44), + ["damage2"] = memory.readword(base_addr + 0x46), + ["damage3"] = memory.readword(base_addr + 0x48), + ["damage4"] = memory.readword(base_addr + 0x4a), + ["damage5"] = memory.readword(base_addr + 0x4c), + ["damage6"] = memory.readword(base_addr + 0x4e), + ["spriteparam"] = memory.readword(base_addr + 0x58), + } +end + +function sprite_details(idx) + local base_addr = idx * 94 + 0x7e0e9e + + local sprite = get_sprite(base_addr) + + if sprite["control"] == 0 then + gui.text(0, 0, "Sprite "..idx.." (Empty)", fgcolor, bgcolor) + incsprite = 0 + locked = false + lockdata = nil + return + end + + if incsprite ~= 0 then + memory.writeword(base_addr + 0x36, sprite["animnum"] + incsprite) + + lockdata = nil + incsprite = 0 + end + + if locked and lockdata == nil then + lockdata = memory.readregion(base_addr, 94) + end + + if lockdata ~= nil and locked then + memory.writeregion(base_addr, 94, lockdata) + end + + gui.text(0, 0, "Sprite "..idx..(locked and " (Locked)" or "")..":\n\n"..table_to_string(sprite), fgcolor, bgcolor) +end + +function on_paint (not_synth) + count = count + 1 + + local guiWidth, guiHeight = gui.resolution() + + if showhelp then + gui.text(0, 0, [[ +Keyboard Help +=============== + +Sprite Details: + +[1] Next sprite slot +[2] Previous sprite slot +[3] Change to next sprite animation +[4] Change to previous sprite animation +[5] Lock current sprite + +[6] Enable / Disable Pokemon mode (take screenshots of enemies) +[7] Enable / Disable float mode (fly with up/down) +]], fgcolor, bgcolor) + return + end + + gui.text(guiWidth - 75, 0, "Help [0]", fgcolor, bgcolor) + + local stats = "" + + if pokemon then + stats = stats.."Pokemon: "..pokecount.."\n" + end + + if floatmode then + stats = stats.."Float on\n" + end + + gui.text(0, guiHeight - 40, stats, fgcolor, bgcolor) + + stats = stats.."\nPokemon: "..pokecount + + local cameraX = memory.readword(0x7e17ba) - 256 + local cameraY = memory.readword(0x7e17c0) - 256 + + local partyScreenX = (memory.readword(0x7e0a2a) - 256 - cameraX) * 2 + local partyScreenY = (memory.readword(0x7e0a2c) - 256 - cameraY) * 2 + + if detailsidx ~= -1 then + sprite_details(detailsidx) + else + gui.text(0, 0, "[1] <- Sprite Details Off -> [2]", fgcolor, bgcolor) + end + + gui.text(guiWidth - 200, guiHeight - 20, "Camera: "..tostring(cameraX)..","..tostring(cameraY), fgcolor, bgcolor) + + gui.text(partyScreenX, partyScreenY, "Party", fgcolor, bgcolor) + + local sprites = {} + for idx = 0,20,1 do + local base_addr = idx * 94 + 0x7e0e9e + + local sprite = get_sprite(base_addr) + + sprites[idx] = sprite + + if sprite["control"] == 0 then + goto continue + end + + local spriteScreenX = (sprite["x"] - 256 - cameraX) * 2 + local spriteScreenY = (sprite["y"] - 256 - cameraY) * 2 + + local sprcolor = bgcolor + if detailsidx == idx then + sprcolor = 0x00ff0000 + end + gui.text(spriteScreenX, spriteScreenY, sprite["animnum"]..","..sprite["attr"], fgcolor, sprcolor) + + local filename = os.getenv("HOME").."/neat-donk/catchem/"..sprite["animnum"]..","..sprite["attr"]..".png" + if pokemon and spriteScreenX > (guiWidth / 4) and spriteScreenX < (guiWidth / 4) * 3 and spriteScreenY > (guiHeight / 3) and spriteScreenY < guiHeight and not file_exists(filename) then + gui.screenshot(filename) + pokecount = pokecount + 1 + end + ::continue:: + end +end + +input.keyhook("1", true) +input.keyhook("2", true) +input.keyhook("3", true) +input.keyhook("4", true) +input.keyhook("5", true) +input.keyhook("6", true) +input.keyhook("7", true) +input.keyhook("0", true) \ No newline at end of file diff --git a/game.lua b/game.lua new file mode 100644 index 0000000..3cf43e0 --- /dev/null +++ b/game.lua @@ -0,0 +1,164 @@ +--Notes here +config = require "config" +spritelist = require "spritelist" +local _M = {} + +function _M.getPositions() + partyX = memory.readword(0x7e0a2a) - 256 + partyY = memory.readword(0x7e0a2c) - 256 + + local cameraX = memory.readword(0x7e17ba) - 256 + local cameraY = memory.readword(0x7e17c0) - 256 + + _M.screenX = partyX-layer1x + _M.screenY = partyY-layer1y +end + +function _M.getBananas() + local bananas = memory.readword(0x7e08bc) + return bananas +end + +function _M.getCoins() + local coins = memory.readword(0x7e08ca) + return coins +end + +function _M.getLives() + local lives = memory.readsbyte(0x7e08be) + 1 + return lives +end + +function _M.writeLives(lives) + memory.writebyte(0x7e08be, lives - 1) + memory.writebyte(0x7e08c0, lives - 1) +end + +function _M.getPowerup() + return 0 +end + +function _M.writePowerup(powerup) + return + -- memory.writebyte(0x0019, powerup) +end + + +function _M.getHit(alreadyHit) + return not alreadyHit and memory.readbyte(0x7e08be) < memory.readbyte(0x7e08c0) +end + +function _M.getHitTimer() + return memory.readbyte(0x7e08c0) - memory.readbyte(0x7e08be) +end + +function _M.getTile(dx, dy) + local partyScreenX = (partyX - cameraX) * 2 + local partyScreenY = (partyY - cameraY) * 2 + + x = math.floor((partyX+dx+8)/16) + y = math.floor((partyY+dy)/16) + + return memory.readbyte(0x1C800 + math.floor(x/0x10)*0x1B0 + y*0x10 + x%0x10) +end + +function _M.getSprites() + local sprites = {} + for slot=0,11 do + local status = memory.readbyte(0x14C8+slot) + if status ~= 0 then + spritex = memory.readbyte(0xE4+slot) + memory.readbyte(0x14E0+slot)*256 + spritey = memory.readbyte(0xD8+slot) + memory.readbyte(0x14D4+slot)*256 + sprites[#sprites+1] = {["x"]=spritex, ["y"]=spritey, ["good"] = spritelist.Sprites[memory.readbyte(0x009e + slot) + 1]} + end + end + + return sprites +end + +function _M.getExtendedSprites() + local extended = {} + for slot=0,11 do + local number = memory.readbyte(0x170B+slot) + if number ~= 0 then + spritex = memory.readbyte(0x171F+slot) + memory.readbyte(0x1733+slot)*256 + spritey = memory.readbyte(0x1715+slot) + memory.readbyte(0x1729+slot)*256 + extended[#extended+1] = {["x"]=spritex, ["y"]=spritey, ["good"] = spritelist.extSprites[memory.readbyte(0x170B + slot) + 1]} + end + end + + return extended +end + +function _M.getInputs() + _M.getPositions() + + sprites = _M.getSprites() + extended = _M.getExtendedSprites() + + local inputs = {} + local inputDeltaDistance = {} + + local layer1x = memory.read_s16_le(0x1A); + local layer1y = memory.read_s16_le(0x1C); + + + for dy=-config.BoxRadius*16,config.BoxRadius*16,16 do + for dx=-config.BoxRadius*16,config.BoxRadius*16,16 do + inputs[#inputs+1] = 0 + inputDeltaDistance[#inputDeltaDistance+1] = 1 + + tile = _M.getTile(dx, dy) + if tile == 1 and partyY+dy < 0x1B0 then + inputs[#inputs] = 1 + end + + for i = 1,#sprites do + distx = math.abs(sprites[i]["x"] - (partyX+dx)) + disty = math.abs(sprites[i]["y"] - (partyY+dy)) + if distx <= 8 and disty <= 8 then + inputs[#inputs] = sprites[i]["good"] + + local dist = math.sqrt((distx * distx) + (disty * disty)) + if dist > 8 then + inputDeltaDistance[#inputDeltaDistance] = mathFunctions.squashDistance(dist) + --gui.drawLine(screenX, screenY, sprites[i]["x"] - layer1x, sprites[i]["y"] - layer1y, 0x50000000) + end + end + end + + for i = 1,#extended do + distx = math.abs(extended[i]["x"] - (partyX+dx)) + disty = math.abs(extended[i]["y"] - (partyY+dy)) + if distx < 8 and disty < 8 then + + --console.writeline(screenX .. "," .. screenY .. " to " .. extended[i]["x"]-layer1x .. "," .. extended[i]["y"]-layer1y) + inputs[#inputs] = extended[i]["good"] + local dist = math.sqrt((distx * distx) + (disty * disty)) + if dist > 8 then + inputDeltaDistance[#inputDeltaDistance] = mathFunctions.squashDistance(dist) + --gui.drawLine(screenX, screenY, extended[i]["x"] - layer1x, extended[i]["y"] - layer1y, 0x50000000) + end + --if dist > 100 then + --dw = mathFunctions.squashDistance(dist) + --console.writeline(dist .. " to " .. dw) + --gui.drawLine(screenX, screenY, extended[i]["x"] - layer1x, extended[i]["y"] - layer1y, 0x50000000) + --end + --inputs[#inputs] = {["value"]=-1, ["dw"]=dw} + end + end + end + end + + return inputs, inputDeltaDistance +end + +function _M.clearJoypad() + controller = {} + for b = 1,#config.ButtonNames do + controller["P1 " .. config.ButtonNames[b]] = false + end + joypad.set(controller) +end + +return _M diff --git a/mathFunctions.lua b/mathFunctions.lua new file mode 100644 index 0000000..e9c9823 --- /dev/null +++ b/mathFunctions.lua @@ -0,0 +1,29 @@ +--Notes here + +local _M = {} + +function _M.sigmoid(x) + return 2/(1+math.exp(-4.9*x))-1 +end + +function _M.squashDistance(x) + local window = 0.20 + local delta = 0.25 + + local dist = (x-8) + local newDist = 1 + + while dist > 0 do + newDist = newDist - (window*delta) + dist = dist - 1 + end + + if newDist < 0.80 then + newDist = 0.80 + end + + return newDist +end + + +return _M \ No newline at end of file diff --git a/neat-donk.lua b/neat-donk.lua new file mode 100644 index 0000000..ef36548 --- /dev/null +++ b/neat-donk.lua @@ -0,0 +1,1163 @@ +--Update to Seth-Bling's MarI/O app + +config = require "config" +spritelist = require "spritelist" +game = require "game" +mathFunctions = require "mathFunctions" + +Inputs = config.InputSize+1 +Outputs = #config.ButtonNames + +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 + + return pool +end + +function newSpecies() + local species = {} + species.topFitness = 0 + species.staleness = 0 + species.genomes = {} + species.averageFitness = 0 + + return species +end + +function newGenome() + local genome = {} + genome.genes = {} + genome.fitness = 0 + genome.adjustedFitness = 0 + genome.network = {} + genome.maxneuron = 0 + genome.globalRank = 0 + genome.mutationRates = {} + genome.mutationRates["connections"] = config.NeatConfig.MutateConnectionsChance + genome.mutationRates["link"] = config.NeatConfig.LinkMutationChance + genome.mutationRates["bias"] = config.NeatConfig.BiasMutationChance + genome.mutationRates["node"] = config.NeatConfig.NodeMutationChance + genome.mutationRates["enable"] = config.NeatConfig.EnableMutationChance + genome.mutationRates["disable"] = config.NeatConfig.DisableMutationChance + genome.mutationRates["step"] = config.NeatConfig.StepSize + + return genome +end + +function copyGenome(genome) + local genome2 = newGenome() + for g=1,#genome.genes do + table.insert(genome2.genes, copyGene(genome.genes[g])) + 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"] + + 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.dw = 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[config.NeatConfig.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, inputDeltas) + table.insert(inputs, 1) + table.insert(inputDeltas,99) + if #inputs ~= Inputs then + console.writeline("Incorrect number of neural network inputs.") + return {} + end + + + for i=1,Inputs do + network.neurons[i].value = inputs[i] * inputDeltas[i] + --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 + neuron.value = mathFunctions.sigmoid(sum) + end + end + + local outputs = {} + for o=1,Outputs do + local button = "P1 " .. config.ButtonNames[o] + if network.neurons[config.NeatConfig.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() + + 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[config.NeatConfig.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() < config.NeatConfig.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 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 +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 = config.NeatConfig.DeltaDisjoint*disjoint(genome1.genes, genome2.genes) + local dw = config.NeatConfig.DeltaWeights*weights(genome1.genes, genome2.genes) + return dd + dw < config.NeatConfig.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() < config.NeatConfig.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 removeStaleSpecies() + local survived = {} + + 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 < config.NeatConfig.StaleSpecies or species.topFitness >= pool.maxFitness then + 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 * config.NeatConfig.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() + 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 * config.NeatConfig.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 + while #children + #pool.species < config.NeatConfig.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 + + --writeFile("backup." .. pool.generation .. "." .. forms.gettext(saveLoadFile)) + writeFile(forms.gettext(saveLoadFile) .. ".gen" .. pool.generation .. ".pool") +end + +function initializePool() + pool = newPool() + + for i=1,config.NeatConfig.Population do + basic = basicGenome() + addToSpecies(basic) + end + + initializeRun() +end + +function initializeRun() + savestate.load(config.NeatConfig.Filename); + if config.StartPowerup ~= NIL then + game.writePowerup(config.StartPowerup) + end + rightmost = 0 + pool.currentFrame = 0 + timeout = config.NeatConfig.TimeoutConstant + game.clearJoypad() + startBananas = game.getBananas() + startCoins = game.getCoins() + startLives = game.getLives() + checkMarioCollision = true + marioHitCounter = 0 + powerUpCounter = 0 + powerUpBefore = game.getPowerup() + local species = pool.species[pool.currentSpecies] + local genome = species.genomes[pool.currentGenome] + generateNetwork(genome) + evaluateCurrent() +end + +function evaluateCurrent() + local species = pool.species[pool.currentSpecies] + local genome = species.genomes[pool.currentGenome] + + local inputDeltas = {} + inputs, inputDeltas = game.getInputs() + + controller = evaluateNetwork(genome.network, inputs, inputDeltas) + + if controller["P1 Left"] and controller["P1 Right"] then + controller["P1 Left"] = false + controller["P1 Right"] = false + end + if controller["P1 Up"] and controller["P1 Down"] then + controller["P1 Up"] = false + controller["P1 Down"] = false + end + + joypad.set(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 + +form = forms.newform(500, 500, "Mario-Neat") +netPicture = forms.pictureBox(form, 5, 250,470, 200) + + +--int forms.pictureBox(int formhandle, [int? x = null], [int? y = null], [int? width = null], [int? height = null]) + +function displayGenome(genome) + forms.clear(netPicture,0x80808080) + local network = genome.network + local cells = {} + local i = 1 + local cell = {} + for dy=-config.BoxRadius,config.BoxRadius do + for dx=-config.BoxRadius,config.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].value + cells[Inputs] = biasCell + + for o = 1,Outputs do + cell = {} + cell.x = 220 + cell.y = 30 + 8 * o + cell.value = network.neurons[config.NeatConfig.MaxNodes + o].value + cells[config.NeatConfig.MaxNodes+o] = cell + local color + if cell.value > 0 then + color = 0xFF0000FF + else + color = 0xFF000000 + end + --gui.drawText(223, 24+8*o, config.ButtonNames[o], color, 9) + forms.drawText(netPicture,223, 24+8*o, config.ButtonNames[o], color, 9) + end + + for n,neuron in pairs(network.neurons) do + cell = {} + if n > Inputs and n <= config.NeatConfig.MaxNodes then + cell.x = 140 + cell.y = 40 + cell.value = neuron.value + 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 <= config.NeatConfig.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 <= config.NeatConfig.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-config.BoxRadius*5-3,70-config.BoxRadius*5-3,50+config.BoxRadius*5+2,70+config.BoxRadius*5+2,0xFF000000, 0x80808080) + forms.drawBox(netPicture, 50-config.BoxRadius*5-3,70-config.BoxRadius*5-3,50+config.BoxRadius*5+2,70+config.BoxRadius*5+2,0xFF000000, 0x80808080) + --oid forms.drawBox(int componenthandle, int x, int y, int x2, int y2, [color? line = null], [color? background = null]) + 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 + forms.drawBox(netPicture,cell.x-2,cell.y-2,cell.x+2,cell.y+2,opacity,color) + --gui.drawBox(cell.x-2,cell.y-2,cell.x+2,cell.y+2,opacity,color) + 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 = 0xA0000000 + if c1.value == 0 then + opacity = 0x20000000 + end + + local color = 0x80-math.floor(math.abs(mathFunctions.sigmoid(gene.weight))*0x80) + if gene.weight > 0 then + color = opacity + 0x8000 + 0x10000*color + else + color = opacity + 0x800000 + 0x100*color + end + --gui.drawLine(c1.x+1, c1.y, c2.x-3, c2.y, color) + forms.drawLine(netPicture,c1.x+1, c1.y, c2.x-3, c2.y, color) + end + end + + --gui.drawBox(49,71,51,78,0x00000000,0x80FF0000) + forms.drawBox(netPicture, 49,71,51,78,0x00000000,0x80FF0000) + --if forms.ischecked(showMutationRates) then + local pos = 100 + for mutation,rate in pairs(genome.mutationRates) do + --gui.drawText(100, pos, mutation .. ": " .. rate, 0xFF000000, 10) + forms.drawText(netPicture,100, pos, mutation .. ": " .. rate, 0xFF000000, 10) + --forms.drawText(pictureBox,400,pos, mutation .. ": " .. rate) + + --void forms.drawText(int componenthandle, int x, int y, string message, [color? forecolor = null], [color? backcolor = null], [int? fontsize = null], [string fontfamily = null], [string fontstyle = null], [string horizalign = null], [string vertalign = null]) + + pos = pos + 8 + end + --end + forms.refresh(netPicture) +end + +function writeFile(filename) + local file = io.open(filename, "w") + file:write(pool.generation .. "\n") + file:write(pool.maxFitness .. "\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 + end + end + file:close() +end + +function savePool() + local filename = forms.gettext(saveLoadFile) + print(filename) + writeFile(filename) +end + +function mysplit(inputstr, sep) + if sep == nil then + sep = "%s" + end + local t={} ; i=1 + for str in string.gmatch(inputstr, "([^"..sep.."]+)") do + t[i] = str + i = i + 1 + end + return t +end + +function loadFile(filename) + print("Loading pool from " .. filename) + local file = io.open(filename, "r") + pool = newPool() + pool.generation = file:read("*number") + pool.maxFitness = file:read("*number") + forms.settext(MaxLabel, "Max Fitness: " .. math.floor(pool.maxFitness)) + 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() + local enabled + + local geneStr = file:read("*line") + local geneArr = mysplit(geneStr) + gene.into = tonumber(geneArr[1]) + gene.out = tonumber(geneArr[2]) + gene.weight = tonumber(geneArr[3]) + gene.innovation = tonumber(geneArr[4]) + enabled = tonumber(geneArr[5]) + + + if enabled == 0 then + gene.enabled = false + else + gene.enabled = true + end + + table.insert(genome.genes, gene) + end + end + end + file:close() + + while fitnessAlreadyMeasured() do + nextGenome() + end + initializeRun() + pool.currentFrame = pool.currentFrame + 1 + print("Pool loaded.") +end + +function flipState() + if config.Running == true then + config.Running = false + forms.settext(startButton, "Start") + else + config.Running = true + forms.settext(startButton, "Stop") + end +end + +function loadPool() + filename = forms.openfile("DP1.state.pool",config.PoolDir) + --local filename = forms.gettext(saveLoadFile) + forms.settext(saveLoadFile, filename) + loadFile(filename) +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 + forms.settext(MaxLabel, "Max Fitness: " .. math.floor(pool.maxFitness)) + initializeRun() + pool.currentFrame = pool.currentFrame + 1 + return +end + +function onExit() + forms.destroy(form) +end + +writeFile(config.PoolDir.."temp.pool") + +event.onexit(onExit) + +GenerationLabel = forms.label(form, "Generation: " .. pool.generation, 5, 5) +SpeciesLabel = forms.label(form, "Species: " .. pool.currentSpecies, 130, 5) +GenomeLabel = forms.label(form, "Genome: " .. pool.currentGenome, 230, 5) +MeasuredLabel = forms.label(form, "Measured: " .. "", 330, 5) + +FitnessLabel = forms.label(form, "Fitness: " .. "", 5, 30) +MaxLabel = forms.label(form, "Max: " .. "", 130, 30) + +BananasLabel = forms.label(form, "Bananas: " .. "", 5, 65) +CoinsLabel = forms.label(form, "Coins: " .. "", 130, 65, 90, 14) +LivesLabel = forms.label(form, "Lives: " .. "", 130, 80, 90, 14) +DmgLabel = forms.label(form, "Damage: " .. "", 230, 65, 110, 14) +PowerUpLabel = forms.label(form, "PowerUp: " .. "", 230, 80, 110, 14) + +startButton = forms.button(form, "Start", flipState, 155, 102) + +restartButton = forms.button(form, "Restart", initializePool, 155, 102) +saveButton = forms.button(form, "Save", savePool, 5, 102) +loadButton = forms.button(form, "Load", loadPool, 80, 102) +playTopButton = forms.button(form, "Play Top", playTop, 230, 102) + +saveLoadFile = forms.textbox(form, config.NeatConfig.Filename .. ".pool", 170, 25, nil, 5, 148) +saveLoadLabel = forms.label(form, "Save/Load:", 5, 129) +spritelist.InitSpriteList() +spritelist.InitExtSpriteList() +while true do + + if config.Running == true then + + local species = pool.species[pool.currentSpecies] + local genome = species.genomes[pool.currentGenome] + + displayGenome(genome) + + if pool.currentFrame%5 == 0 then + evaluateCurrent() + end + + joypad.set(controller) + + game.getPositions() + if partyX > rightmost then + rightmost = partyX + timeout = config.NeatConfig.TimeoutConstant + end + + local hitTimer = game.getHitTimer() + + if checkMarioCollision == true then + if hitTimer > 0 then + marioHitCounter = marioHitCounter + 1 + --console.writeline("Mario took damage, hit counter: " .. marioHitCounter) + checkMarioCollision = false + end + end + + if hitTimer == 0 then + checkMarioCollision = true + end + + powerUp = game.getPowerup() + if powerUp > 0 then + if powerUp ~= powerUpBefore then + powerUpCounter = powerUpCounter+1 + powerUpBefore = powerUp + end + end + + Lives = game.getLives() + + timeout = timeout - 1 + + local timeoutBonus = pool.currentFrame / 4 + if timeout + timeoutBonus <= 0 then + + local bananas = game.getBananas() - startBananas + local coins = game.getCoins() - startCoins + + --console.writeline("Bananas: " .. bananas .. " coins: " .. coins) + + local bananaCoinsFitness = (bananas * 50) + (coins * 0.2) + if (bananas + coins) > 0 then + console.writeline("Bananas and Coins added " .. bananaCoinsFitness .. " fitness") + end + + local hitPenalty = marioHitCounter * 100 + local powerUpBonus = powerUpCounter * 100 + + local fitness = bananaCoinsFitness - hitPenalty + powerUpBonus + rightmost - pool.currentFrame / 2 + + if startLives < Lives then + local ExtraLiveBonus = (Lives - startLives)*1000 + fitness = fitness + ExtraLiveBonus + console.writeline("ExtraLiveBonus added " .. ExtraLiveBonus) + end + + if rightmost > 4816 then + fitness = fitness + 1000 + console.writeline("!!!!!!Beat level!!!!!!!") + end + if fitness == 0 then + fitness = -1 + end + genome.fitness = fitness + + if fitness > pool.maxFitness then + pool.maxFitness = fitness + --writeFile("backup." .. pool.generation .. "." .. forms.gettext(saveLoadFile)) + writeFile(forms.gettext(saveLoadFile) .. ".gen" .. pool.generation .. ".pool") + end + + console.writeline("Gen " .. pool.generation .. " species " .. pool.currentSpecies .. " genome " .. pool.currentGenome .. " fitness: " .. fitness) + pool.currentSpecies = 1 + pool.currentGenome = 1 + while fitnessAlreadyMeasured() do + 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.drawEllipse(game.screenX-84, game.screenY-84, 192, 192, 0x50000000) + forms.settext(FitnessLabel, "Fitness: " .. math.floor(rightmost - (pool.currentFrame) / 2 - (timeout + timeoutBonus)*2/3)) + forms.settext(GenerationLabel, "Generation: " .. pool.generation) + forms.settext(SpeciesLabel, "Species: " .. pool.currentSpecies) + forms.settext(GenomeLabel, "Genome: " .. pool.currentGenome) + forms.settext(MaxLabel, "Max: " .. math.floor(pool.maxFitness)) + forms.settext(MeasuredLabel, "Measured: " .. math.floor(measured/total*100) .. "%") + forms.settext(BananasLabel, "Bananas: " .. (game.getBananas() - startBananas)) + forms.settext(CoinsLabel, "Coins: " .. (game.getCoins() - startCoins)) + forms.settext(LivesLabel, "Lives: " .. Lives) + forms.settext(DmgLabel, "Damage: " .. marioHitCounter) + forms.settext(PowerUpLabel, "PowerUp: " .. powerUpCounter) + + pool.currentFrame = pool.currentFrame + 1 + + end + emu.frameadvance(); + +end diff --git a/spritelist.lua b/spritelist.lua new file mode 100644 index 0000000..1094bfb --- /dev/null +++ b/spritelist.lua @@ -0,0 +1,276 @@ +-- Idea from: https://github.com/kevino5233/MarIO_Enhanced/ +-- Spritelist from: https://www.smwcentral.net/?p=viewthread&t=7562 +-- Extended spritelist: https://web.archive.org/web/20170709102356/www.smwiki.net/wiki/RAM_Address/$7E:170B +-- + +local _M = {} + +_M.Sprites = {} + +-- Make sure this list is sorted before initialization. +_M.NeutralSprites = { + 0x0E, -- Keyhole. + 0x2C, -- Yoshi egg Red/Blue/Yellow/Blue (X&3). + 0x2D, -- Baby green Yoshi. + 0x2F, -- Portable spring board. + 0x35, -- Green Yoshi. + 0x3E, -- POW, blue/silver (X&1). + 0x41, -- Dolphin, horizontal. + 0x42, -- Dolphin2, horizontal. + 0x43, -- Dolphin, vertical. + 0x49, -- Growing/shrinking pipe end. + 0x4A, -- Goal Point Question Sphere. + 0x52, -- Moving ledge hole in ghost house. + 0x53, -- ??? + 0x54, -- Climbing net door, use with object 0x4A-E. + 0x55, -- Checkerboard platform, horizontal. + 0x56, -- Flying rock platform, horizontal. + 0x57, -- Checkerboard platform, vertical. + 0x58, -- Flying rock platform, vertical. + 0x59, -- Turn block bridge, horizontal and vertical. + 0x5A, -- Turn block bridge, horizontal. + 0x5B, -- Brown platform floating in water. + 0x5C, -- Checkerboard platform that falls. + 0x5D, -- Orange platform floating in water. + 0x5E, -- Orange platform, goes on forever. + 0x5F, -- Brown platform on a chain. + 0x60, -- Flat green switch palace switch. + 0x61, -- Floating skulls. + 0x62, -- Brown platform, line-guided. + 0x63, -- Checker/brown platform, line-guided (X&1). + 0x64, -- Rope mechanism, line-guided (X&1). + 0x6A, -- Coin game cloud. + 0x6B, -- Spring board, left wall. + 0x6C, -- Spring board, right wall. + 0x6D, -- Invisible solid block. + 0x79, -- Growing Vine. + 0x7C, -- ??? + 0x80, -- Key. + 0x81, -- Changing item from a translucent block. + 0x87, -- Lakitu's cloud, no time limit. (!) + 0x8A, -- Bird from Yoshi's house, max of 4. + 0x8B, -- Puff of smoke from Yoshi's house. + 0xA3, -- Grey platform on chain, clockwise/counter (X&1). + 0xBA, -- Timed lift, 4 sec/1 sec (X&1). + 0xC0, -- Grey platform on lava, sinks. + 0xC4, -- Grey platform that falls. + 0xC8, -- Light switch block for dark room. + 0xC9, -- ??? + 0xDA, -- Green Koopa shell. + 0xDB, -- Red Koopa shell. + 0xDC, -- Blue Koopa shell. + 0xDD, -- Yellow Koopa shell. + 0xDF, -- Green shell, won't use Special World color. + 0xE0 -- 3 platforms on chains, clockwise/counter (X&1). + } + +-- Make sure this list is sorted before initialization. +_M.GoodSprites = { + 0x21, -- Moving coin. + 0x45, -- Directional coins, no time limit. + 0x74, -- Mushroom. + 0x75, -- Flower. + 0x76, -- Star. + 0x77, -- Feather. + 0x78, -- 1-UP. + 0x7B, -- Standard Goal Point. + -- "Secret" Goal Point. + 0x83, -- Left flying question block, coin/flower/feather/1-UP (X&3). + 0x84, -- Flying question block, coin/flower/feather/1-UP (X&3). + 0xC1, -- Flying grey turnblocks, first up/down (X&1). + 0xC7 -- Invisible mushroom. + } + +-- Currently not used. +_M.BadSprites = { + 0x00, -- Green Koopa, no shell. + 0x01, -- Red Koopa, no shell. + 0x02, -- Blue Koopa, no shell. + 0x03, -- Yellow Koopa, no shell. + 0x04, -- Green Koopa. + 0x05, -- Red Koopa. + 0x06, -- Blue Koopa. + 0x07, -- Yellow Koopa. + 0x08, -- Green Koopa, flying left. + 0x09, -- Green bouncing Koopa (Y&1). + 0x0A, -- Red vertical flying Koopa. + 0x0B, -- Red horizontal flying Koopa. + 0x0C, -- Yellow Koopa with wings. + 0x0F, -- Goomba. + 0x10, -- Bouncing Goomba with wings. + 0x1A, -- Classic Pirhana Plant (use ExGFX). + 0x1C, -- Bullet Bill. + 0x4F, -- Jumping Pirhana Plant. + 0x50, -- Jumping Pirhana Plant, spit fire. + 0x7E, -- Flying Red coin, worth 5 coins. + 0x7F, -- Flying Yellow 1-UP. + 0xB1, -- Creating/Eating block (X&1). + 0xB9, -- Info Box, message 1/2 (X&1). + 0xBD, -- Sliding Koopa without a shell. + 0x0D, -- Bob-omb. + 0x11, -- Buzzy Beetle. + 0x13, -- Spiny. + 0x14, -- Spiny falling. + 0x15, -- Fish, horizontal. + 0x16, -- Fish, vertical. + 0x18, -- Surface jumping fish. + 0x1B, -- Bouncing football in place. + 0x1D, -- Hopping flame. + 0x1E, -- Lakitu Normal/Fish (X&1). + 0x1F, -- Magikoopa. + 0x20, -- Magikoopa's magic, stationary. + 0x22, -- Green vertical net Koopa, below/above (X&1). + 0x23, -- Red fast vertical net Koopa, below/above (X&1). + 0x24, -- Green horizontal net Koopa, below/above (X&1). + 0x25, -- Red fast horizontal net Koopa, below/above (X&1). + 0x26, -- Thwomp. + 0x27, -- Thwimp. + 0x28, -- Big Boo. + 0x29, -- Koopa Kid (place at X=12, Y=0 to 6). + 0x2A, -- Upside down Piranha Plant. + 0x2B, -- Sumo Brother's fire lightning. + 0x2E, -- Spike Top. + 0x30, -- Dry Bones, throws bones. + 0x31, -- Bony Beetle. + 0x32, -- Dry Bones, stay on ledge. + 0x33, -- Fireball, vertical. Requires buoyancy! + 0x34, -- Boss fireball, stationary. + 0x37, -- Boo. + 0x38, -- Eerie. + 0x39, -- Eerie, wave motion. + 0x3A, -- Urchin, fixed vertical/horizontal (X&1). + 0x3B, -- Urchin, wall detect v/h (X&1). + 0x3C, -- Urchin, wall follow clockwise/counter (X&1). + 0x3D, -- Rip Van Fish. + 0x3F, -- Para-Goomba. + 0x40, -- Para-Bomb. + 0x44, -- Torpedo Ted. + 0x47, -- Swimming/Jumping fish, doesn't need water. (!) + 0x48, -- Diggin' Chuck's rock. + 0x46, -- Diggin' Chuck. + 0x4B, -- Pipe dwelling Lakitu. + 0x4C, -- Exploding Block, fish/goomba/Koopa/Koopa with shell (X&3). + 0x4D, -- Ground dwelling Monty Mole, follow/hop (X&1). + 0x4E, -- Ledge dwelling Monty Mole, follow/hop (X&1). + 0x51, -- Ninji. + 0x65, -- Chainsaw, line-guided, right/left (X&1). + 0x66, -- Upside down chainsaw, line-guided, null/left (X&1). + 0x67, -- Grinder, line-guided, right/left (X&1). + 0x68, -- Fuzz Ball, line-guided, right/left (X&1). + 0x6E, -- Dino Rhino. + 0x6F, -- Dino Torch. + 0x70, -- Pokey. + 0x71, -- Super Koopa, red cape, swoop. + 0x72, -- Super Koopa, yellow cape, swoop. + 0x73, -- Super Koopa, feather/yellow cape (X&1). + 0x7A, -- Firework, makes Mario temporarily invisible. + 0x86, -- Wiggler. + 0x8D, -- Ghost house exit sign and door. + 0x8E, -- Invisible "Warp Hole" blocks. (!) + 0x8F, -- Scale platforms, long/short between (X&1). + 0x90, -- Large green gas bubble. + 0x91, -- Chargin' Chuck. + 0x92, -- Splitin' Chuck. + 0x93, -- Bouncin' Chuck. + 0x94, -- Whistlin' Chuck, fish/Koopa (X&1). + 0x95, -- Clapin' Chuck. + 0x97, -- Puntin' Chuck. + 0x98, -- Pitchin' Chuck. + 0x99, -- Volcano Lotus. + 0x9A, -- Sumo Brother. + 0x9B, -- Hammer Brother (requires sprite 9C). + 0x9C, -- Flying blocks for Hammer Brother. + 0x9D, -- Bubble with Goomba/bomb/fish/mushroom (X&3). + 0x9E, -- Ball and Chain, clockwise/counter (X&1). + 0x9F, -- Banzai Bill. + 0xA2, -- MechaKoopa. + 0xA4, -- Floating Spike ball, slow/fast (X&1). + 0xA5, -- Fuzzball/Sparky, ground-guided, left/right (X&1). + 0xA6, -- HotHead, ground-guided, left/right (X&1). + 0xA8, -- Blargg. + 0xAA, -- Fishbone. + 0xAB, -- Rex. + 0xAC, -- Wooden Spike, moving down and up. + 0xAD, -- Wooden Spike, moving up/down first (X&1). + 0xAE, -- Fishin' Boo. + 0xAF, -- Boo Block. + 0xB0, -- Reflecting stream of Boo Buddies. + 0xB2, -- Falling Spike. + 0xB3, -- Bowser statue fireball. + 0xB4, -- Grinder, non-line-guided. + 0xB6, -- Reflecting fireball. + 0xB7, -- Carrot Top lift, upper right. + 0xB8, -- Carrot Top lift, upper left. + 0xBB, -- Grey moving castle block, horizontal. + 0xBC, -- Bowser statue, normal/fire/leap (X&3). + 0xBE, -- Swooper Bat, hang/fly/fly/fly (X&3). + 0xBF, -- Mega Mole. + 0xC2, -- Blurp fish. + 0xC3, -- A Porcu-Puffer fish. + 0xC5, -- Big Boo Boss. + 0xC6, -- Dark room with spot light. + 0xDE, -- Group of 5 eeries, wave motion. + 0xE2, -- Boo Buddies, counter-clockwise. + 0xE3 -- Boo Buddies, clockwise. + } + +function _M.InitSpriteList() + local k = 1 + local j = 1 + for i=1, 256 do + local isGood = (k <= #_M.GoodSprites) and (_M.GoodSprites[k] == i - 1) + local isNeutral = (j <= #_M.NeutralSprites) and (_M.NeutralSprites[j] == i - 1) + if isGood then + k = k + 1 + _M.Sprites[#_M.Sprites + 1] = 1 + elseif isNeutral then + j = j + 1 + _M.Sprites[#_M.Sprites + 1] = 0 + else + _M.Sprites[#_M.Sprites + 1] = -1 + end + end +end + +_M.extSprites = {} + +-- Make sure this list is sorted before initialization. +_M.ExtNeutralSprites = { + 0x01, -- Puff of smoke with various objects. + 0x03, -- Flame left by hopping flame. + 0x05, -- Player fireball. + 0x0A, -- Coin from coin cloud game. + 0x0F, -- Trail of smoke (yellow Yoshi stomping the ground). + 0x10, -- Spinjump stars. + 0x11, -- Yoshi fireballs. + 0x12 -- Water bubble. +} + +-- Currently not used. +_M.ExtBadSprites = { + 0x02, -- Reznor fireball. + 0x04, -- Hammer. + 0x06, -- Bone from Dry Bones. + 0x07, -- Lava splash. + 0x08, -- Torpedo Ted shooter's arm. + 0x09, -- Unknown flickering object + 0x0B, -- Piranha Plant fireball. + 0x0C, -- Lava Lotus's fiery objects. + 0x0D, -- Baseball. + 0x0E -- Wiggler's flower. +} + +function _M.InitExtSpriteList() + local j = 1 + for i=1, 21 do + local isExtNeutral = (j <= #_M.ExtNeutralSprites) and (_M.ExtNeutralSprites[j] == i - 1) + if isExtNeutral then + j = j + 1 + _M.extSprites[#_M.extSprites + 1] = 0 + else + _M.extSprites[#_M.extSprites + 1] = -1 + end + end +end + +return _M \ No newline at end of file