diff --git a/game.lua b/game.lua index 5fb7fed..8dfdfab 100644 --- a/game.lua +++ b/game.lua @@ -6,7 +6,7 @@ local base = string.gsub(@@LUA_SCRIPT_FILENAME@@, "(.*[/\\])(.*)", "%1") local mathFunctions = dofile(base.."/mathFunctions.lua") local config = dofile(base.."/config.lua") local spritelist = dofile(base.."/spritelist.lua") -local util = dofile(base.."/util.lua") +local util = dofile(base.."/util.lua")() local mem = dofile(base.."/mem.lua") local _M = { leader = 0, diff --git a/neat-donk.lua b/neat-donk.lua index 0d9ccde..cddc956 100644 --- a/neat-donk.lua +++ b/neat-donk.lua @@ -4,7 +4,6 @@ local gui = gui local base = string.gsub(@@LUA_SCRIPT_FILENAME@@, "(.*[/\\])(.*)", "%1") local pool = dofile(base.."/pool.lua") -local util = dofile(base.."/util.lua") local statusLine = nil local statusColor = 0x0000ff00 @@ -30,4 +29,10 @@ pool.onRenderForm(function(form) gui.text(-500, guiHeight - 20, statusLine, 0x00000000) end end) -pool.run() + +pool.run():next(function() + print("The pool finished running!!!") +end):catch(function(error) + io.stderr:write(string.format("There was a problem running the pool: %s", error)) + print(string.format("There was a problem running the pool: %s", error)) +end) \ No newline at end of file diff --git a/pool.lua b/pool.lua index 0d59a64..50b2ba3 100644 --- a/pool.lua +++ b/pool.lua @@ -1,7 +1,17 @@ +local callback, set_timer_timeout = callback, set_timer_timeout + local base = string.gsub(@@LUA_SCRIPT_FILENAME@@, "(.*[/\\])(.*)", "%1") +local Promise = dofile(base.."/promise.lua") +-- Only the parent should manage ticks! +callback.register('timer', function() + Promise.update() + set_timer_timeout(1) +end) +set_timer_timeout(1) + local config = dofile(base.."/config.lua") -local util = dofile(base.."/util.lua") +local util = dofile(base.."/util.lua")(Promise) local serpent = dofile(base.."/serpent.lua") local libDeflate = dofile(base.."/LibDeflate.lua") @@ -386,15 +396,17 @@ local function addToSpecies(child) end end -local function initializePool(after) - pool = newPool() +local function initializePool() + local promise = Promise.new() + promise:resolve() + return promise:next(function() + pool = newPool() - for i=1,config.NeatConfig.Population do - basic = basicGenome() - addToSpecies(basic) - end - - after() + for i=1,config.NeatConfig.Population do + local basic = basicGenome() + addToSpecies(basic) + end + end) end local function bytes(x) @@ -406,33 +418,40 @@ local function bytes(x) end local function writeFile(filename) - local file = io.open(filename, "w") - local dump = serpent.dump(pool) - local zlib = libDeflate:CompressDeflate(dump) - file:write("\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\x00") - file:write(zlib) - file:write(string.char(0,0,0,0)) - file:write(bytes(#dump % (2^32))) - file:close() - return + local promise = Promise.new() + promise:resolve() + return promise:next(function () + local file = io.open(filename, "w") + local dump = serpent.dump(pool) + local zlib = libDeflate:CompressDeflate(dump) + file:write("\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\x00") + file:write(zlib) + file:write(string.char(0,0,0,0)) + file:write(bytes(#dump % (2^32))) + file:close() + end) end --- FIXME Save/load mechanism has to be rethought with items running in parallel -local function loadFile(filename, after) - message("Loading pool from " .. filename, 0x00999900) - local file = io.open(filename, "r") - if file == nil then - message("File could not be loaded", 0x00990000) - return - end - local contents = file:read("*all") - local ok, obj = serpent.load(libDeflate:DecompressDeflate(contents:sub(11, #contents - 8))) - if not ok then - message("Error parsing pool file", 0x00990000) - return - end +-- FIXME This isn't technically asynchronous. Probably can't be though. +local function loadFile(filename) + local promise = Promise.new() + promise:resolve() + return promise:next(function() + message("Loading pool from " .. filename, 0x00999900) + local file = io.open(filename, "r") + if file == nil then + message("File could not be loaded", 0x00990000) + return + end + local contents = file:read("*all") + local ok, obj = serpent.load(libDeflate:DecompressDeflate(contents:sub(11, #contents - 8))) + if not ok then + message("Error parsing pool file", 0x00990000) + return + end - pool = obj + pool = obj + end) end local function savePool() @@ -441,9 +460,8 @@ local function savePool() message(string.format("Saved \"%s\"!", filename:sub(#filename - 50)), 0x00009900) end -local function loadPool(after) - loadFile(_M.saveLoadFile, after) - after() +local function loadPool() + return loadFile(_M.saveLoadFile) end local function processRenderForm(form) @@ -471,7 +489,7 @@ end local function crossover(g1, g2) -- Make sure g1 is the higher fitness genome if g2.fitness > g1.fitness then - tempg = g1 + local tempg = g1 g1 = g2 g2 = tempg end @@ -562,11 +580,11 @@ end local 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)] + local g1 = species.genomes[math.random(1, #species.genomes)] + local g2 = species.genomes[math.random(1, #species.genomes)] child = crossover(g1, g2) else - g = species.genomes[math.random(1, #species.genomes)] + local g = species.genomes[math.random(1, #species.genomes)] child = copyGenome(g) end @@ -605,7 +623,7 @@ local function removeWeakSpecies() local sum = totalAverageFitness() for s = 1,#pool.species do local species = pool.species[s] - breed = math.floor(species.averageFitness / sum * config.NeatConfig.Population) + local breed = math.floor(species.averageFitness / sum * config.NeatConfig.Population) if breed >= 1 then table.insert(survived, species) end @@ -628,7 +646,7 @@ local function newGeneration() local children = {} for s = 1,#pool.species do local species = pool.species[s] - breed = math.floor(species.averageFitness / sum * config.NeatConfig.Population) - 1 + local breed = math.floor(species.averageFitness / sum * config.NeatConfig.Population) - 1 for i=1,breed do table.insert(children, breedChild(species)) end @@ -652,7 +670,7 @@ local function newGeneration() writeFile(_M.saveLoadFile .. ".gen" .. pool.generation .. ".pool") end -local runner = Runner() +local runner = Runner(Promise) runner.onMessage(function(msg, color) message(msg, color) end) @@ -671,68 +689,72 @@ local topRequested = false local loadRequested = false local saveRequested = false -local function mainLoop(currentSpecies) - if loadRequested then - loadRequested = false - loadPool(mainLoop) - return - end +local function mainLoop(currentSpecies, topGenome) + if currentSpecies == nil then + currentSpecies = 1 + end - if saveRequested then - saveRequested = false - savePool() - end + local slice = pool.species[currentSpecies] + local promise = Promise.new() + promise:resolve() + return promise:next(function() + if loadRequested then + loadRequested = false + currentSpecies = nil + -- FIXME + return loadPool() + end - if topRequested then - topRequested = false - playTop() - return - end + if saveRequested then + saveRequested = false + savePool() + end - if not config.Running then - -- FIXME Tick? - end + if topRequested then + topRequested = false + return playTop() + end - if currentSpecies == nil then - currentSpecies = 1 - end + if not config.Running then + -- FIXME Tick? + end - local slice = pool.species[currentSpecies] - if hasThreads then - slice = {} - for i=currentSpecies, currentSpecies + config.NeatConfig.Threads - 1, 1 do - if pool.species[i] == nil then - break - end + if hasThreads then + slice = {} + for i=currentSpecies, currentSpecies + config.NeatConfig.Threads - 1, 1 do + if pool.species[i] == nil then + break + end - table.insert(slice, pool.species[i]) - end - end - local finished = 0 - runner.run( - slice, - pool.generation, - function() - -- Genome callback - end, - function() - if hasThreads then - finished = finished + 1 - if finished ~= #slice then - return - end - currentSpecies = currentSpecies + #slice - else - currentSpecies = currentSpecies + 1 - end + table.insert(slice, pool.species[i]) + end + end + local finished = 0 - if currentSpecies > #pool.species then - newGeneration() - currentSpecies = 1 - end - mainLoop(currentSpecies) - end - ) + return runner.run( + slice, + pool.generation, + function() + -- Genome callback + -- FIXME Should we do something here??? What was your plan, past me? + end + ):next(function() + if hasThreads then + currentSpecies = currentSpecies + #slice + else + currentSpecies = currentSpecies + 1 + end + + if currentSpecies > #pool.species then + newGeneration() + currentSpecies = 1 + end + end) + end):next(function () + if topGenome == nil then + return mainLoop(currentSpecies) + end + end) end playTop = function() @@ -749,7 +771,7 @@ playTop = function() end -- FIXME genome - mainLoop(maxs) + return mainLoop(maxs, maxg) end function _M.requestLoad(filename) @@ -775,15 +797,19 @@ function _M.requestTop() end function _M.run(reset) + local promise = nil if pool == nil or reset == true then - initializePool(function() - writeFile(config.PoolDir.."temp.pool") - mainLoop() - end) - else - writeFile(config.PoolDir.."temp.pool") - mainLoop() + promise = initializePool() + else + promise = Promise.new() + promise:resolve() end + + return promise:next(function() + return writeFile(config.PoolDir.."temp.pool") + end):next(function () + return mainLoop() + end) end return _M diff --git a/promise.lua b/promise.lua index 8fd9c17..31943db 100644 --- a/promise.lua +++ b/promise.lua @@ -78,7 +78,6 @@ transition = function(promise, state, value) if promise.state == state or promise.state ~= State.PENDING or ( state ~= State.FULFILLED and state ~= State.REJECTED ) - or value == nil then return end diff --git a/runner-process.lua b/runner-process.lua index cfd5a1a..420e360 100644 --- a/runner-process.lua +++ b/runner-process.lua @@ -1,18 +1,22 @@ -local gui, utime = gui, utime +local gui, utime, callback, set_timer_timeout = gui, utime, callback, set_timer_timeout local base = string.gsub(@@LUA_SCRIPT_FILENAME@@, "(.*[/\\])(.*)", "%1") local Promise = dofile(base.."/promise.lua") +-- Only the parent should manage ticks! +callback.register('timer', function() + Promise.update() + set_timer_timeout(1) +end) +set_timer_timeout(1) local Runner = dofile(base.."/runner.lua") local serpent = dofile(base.."/serpent.lua") -local util = dofile(base.."/util.lua") +local util = dofile(base.."/util.lua")(Promise) local inputFilePath = os.getenv("RUNNER_INPUT_FILE") local outputFilePath = os.getenv("RUNNER_OUTPUT_FILE") -local first = false - local outContents = {} local statusLine = nil @@ -22,7 +26,7 @@ local species = nil local speciesId = -1 local generationIndex = nil -local runner = Runner() +local runner = Runner(Promise) runner.onMessage(function(msg, color) statusLine = msg statusColor = color @@ -78,32 +82,7 @@ runner.onLoad(function(filename) ) end) -local waiter = nil -if not util.isWin then - waiter = util.startWaiting(inputFilePath) -end - local function waitLoop() - if not first then - local sec, usec = utime() - local ts = sec * 1000000 + usec - - local outFile = io.open(outputFilePath, "w") - outFile:write(serpent.dump({ type = 'onInit', ts = ts })) - outFile:close() - - print(string.format('Wrote init to output at %d', ts)) - - - first = true - end - - print('Waiting for input from master process') - - if not util.isWin then - util.finishWaiting(waiter) - end - local inputData = nil local ok = false while not ok or inputData == nil or speciesId == inputData[1].id do @@ -128,7 +107,7 @@ local function waitLoop() print('Running') - runner.run( + return runner.run( species, generationIndex, function(genome, index) @@ -141,33 +120,55 @@ local function waitLoop() speciesId = speciesId, }) ) - end, - function() - table.insert( - outContents, - serpent.dump({ - type = 'onFinish', - speciesId = speciesId, - }) - ) - - -- Truncate the input file to reduce the amount of time - -- wasted if we reopen it too early - local inputFile = io.open(inputFilePath, "w") - inputFile:close() - - if not util.isWin then - waiter = util.startWaiting(inputFilePath) - end - - -- Write the result - local outFile = io.open(outputFilePath, "w") - outFile:write(table.concat(outContents, "\n")) - outFile:close() - - waitLoop() end - ) + ):next(function() + table.insert( + outContents, + serpent.dump({ + type = 'onFinish', + speciesId = speciesId, + }) + ) + + -- Truncate the input file to reduce the amount of time + -- wasted if we reopen it too early + local inputFile = io.open(inputFilePath, "w") + inputFile:close() + + local waiter = nil + if util.isWin then + waiter = Promise.new() + waiter:resolve() + else + waiter = util.waitForFiles(inputFilePath) + end + + -- Write the result + local outFile = io.open(outputFilePath, "w") + outFile:write(table.concat(outContents, "\n")) + outFile:close() + + return waiter + end):next(waitLoop) end -waitLoop() \ No newline at end of file +local waiter = nil +if util.isWin then + waiter = Promise.new() + waiter:resolve() +else + waiter = util.waitForFiles(inputFilePath) +end + +local sec, usec = utime() +local ts = sec * 1000000 + usec + +local outFile = io.open(outputFilePath, "w") +outFile:write(serpent.dump({ type = 'onInit', ts = ts })) +outFile:close() + +print(string.format('Wrote init to output at %d', ts)) + +waiter:next(waitLoop):catch(function(error) + print('ERROR: '..error) +end) \ No newline at end of file diff --git a/runner-wrapper.lua b/runner-wrapper.lua index 8299d3c..6793c69 100644 --- a/runner-wrapper.lua +++ b/runner-wrapper.lua @@ -2,7 +2,9 @@ local random = random local base = string.gsub(@@LUA_SCRIPT_FILENAME@@, "(.*[/\\])(.*)", "%1") -local util = dofile(base.."/util.lua") +local Promise = nil + +local util = nil local config = dofile(base.."/config.lua") local serpent = dofile(base.."/serpent.lua") local temps = { @@ -25,6 +27,9 @@ local tmpFileName = tempDir.."/donk_runner_".. string.hex(math.floor(random.integer(0, 0xffffffff))).. string.hex(math.floor(random.integer(0, 0xffffffff))) +local inputPrefix = tmpFileName..'_input_' +local outputPrefix = tmpFileName..'_output_' + local function message(_M, msg, color) if color == nil then color = 0x00009900 @@ -59,7 +64,46 @@ local function onMessage(_M, handler) table.insert(_M.onMessageHandler, handler) end -return function() +--- Launches the child processes +---@param _M table The instance +---@param count integer Number of processes needed +---@return Promise Promise A promise that resolves when all the processes are ready +local function launchChildren(_M, count) + local children = {} + while #_M.poppets < count do + local i = #_M.poppets+1 + local outputFileName = outputPrefix..i + local inputFileName = inputPrefix..i + + local settingsDir = nil + if util.isWin then + settingsDir = tempDir.."/donk_runner_settings_"..i + util.mkdir(settingsDir) + end + + local envs = { + RUNNER_INPUT_FILE = inputFileName, + RUNNER_OUTPUT_FILE = outputFileName, + APPDATA = settingsDir, + } + + local child = util.waitForFiles(outputFileName) + + local cmd = '"'.._M.hostProcess..'" "--rom='..config.ROM..'" --unpause "--lua='..base..'/runner-process.lua"' + local poppet = util.popenCmd(cmd, nil, envs) + table.insert(_M.poppets, poppet) + + table.insert(children, child) + end + + return Promise.all(table.unpack(children)) +end + +return function(promise) + -- FIXME Should this be a global??? + Promise = promise + util = dofile(base.."/util.lua")(Promise) + -- FIXME Maybe don't do this in the "constructor"? if util.isWin then util.downloadFile('https://github.com/watchexec/watchexec/releases/download/1.13.1/watchexec-1.13.1-x86_64-pc-windows-gnu.zip', base..'/watchexec.zip') util.unzip(base..'/watchexec.zip', base) @@ -102,111 +146,91 @@ return function() onLoad(_M, handler) end - _M.run = function(species, generationIdx, genomeCallback, finishCallback) - local inputPrefix = tmpFileName..'_input_' - local outputPrefix = tmpFileName..'_output_' - - -- Create the input files and output files - for i=1,#species,1 do - local inputFileName = inputPrefix..i - local inputFile = io.open(inputFileName, 'a') - inputFile:close() + _M.run = function(speciesSlice, generationIdx, genomeCallback) + local promise = Promise.new() + promise:resolve() + return promise:next(function() + -- Create the input files and output files + for i=1,#speciesSlice,1 do + local inputFileName = inputPrefix..i + local inputFile = io.open(inputFileName, 'a') + inputFile:close() - local outputFileName = outputPrefix..i - local outputFile = io.open(outputFileName, 'a') - outputFile:close() - end - - while #_M.poppets < #species do - local i = #_M.poppets+1 - local outputFileName = outputPrefix..i - local inputFileName = inputPrefix..i - - message(_M, _M.hostProcess) - - local settingsDir = nil - if isWin then - settingsDir = tempDir.."/donk_runner_settings_"..i - util.mkdir(settingsDir) + local outputFileName = outputPrefix..i + local outputFile = io.open(outputFileName, 'a') + outputFile:close() end - local envs = { - RUNNER_INPUT_FILE = inputFileName, - RUNNER_OUTPUT_FILE = outputFileName, - APPDATA = settingsDir, - } + return launchChildren(_M, #speciesSlice) + end):next(function() + local outputFileNames = {} + for i=1,#speciesSlice,1 do + table.insert(outputFileNames, outputPrefix..i) + end - local waiter = util.startWaiting(outputFileName) + local waiter = util.waitForFiles(outputFileNames, nil, tmpFileName.."_output_*") - local cmd = '"'.._M.hostProcess..'" "--rom='..config.ROM..'" --unpause "--lua='..base..'/runner-process.lua"' - local poppet = util.popenCmd(cmd, nil, envs) - table.insert(_M.poppets, poppet) + message(_M, 'Setting up child processes') - util.finishWaiting(waiter) - end + for i=1,#speciesSlice,1 do - local waiters = {} - for i=1,#species,1 do - table.insert(waiters, outputPrefix..i) - end + local inputFileName = tmpFileName.."_input_"..i + local inputFile = io.open(inputFileName, 'w') + inputFile:write(serpent.dump({speciesSlice[i], generationIdx})) + inputFile:close() + end - local waiter = util.startWaiting(waiters, nil, tmpFileName.."_output_*") + message(_M, 'Waiting for child processes to finish') - message(_M, 'Setting up child processes') + return waiter + end):next(function() + message(_M, 'Child processes finished') - for i=1,#species,1 do + local finished = 0 + for i=1,#speciesSlice,1 do + message(_M, "Processing output "..i) + local outputFileName = tmpFileName..'_output_'..i + local outputFile = io.open(outputFileName, "r") + local line = "" + repeat + local ok, obj = serpent.load(line) + if not ok then + goto continue + end - local inputFileName = tmpFileName.."_input_"..i - local inputFile = io.open(inputFileName, 'w') - inputFile:write(serpent.dump({species[i], generationIdx})) - inputFile:close() - end + if obj == nil then + goto continue + end - message(_M, 'Waiting for child processes to finish') - - util.finishWaiting(waiter, #waiters) - - message(_M, 'Child processes finished') - - for i=1,#species,1 do - message(_M, "Processing output "..i) - local outputFileName = tmpFileName..'_output_'..i - local outputFile = io.open(outputFileName, "r") - local line = "" - repeat - local ok, obj = serpent.load(line) - if not ok then - goto continue - end - - if obj == nil then - goto continue - end - - if obj.type == 'onMessage' then - message(_M, obj.msg, obj.color) - elseif obj.type == 'onLoad' then - load(_M, obj.filename) - elseif obj.type == 'onSave' then - save(_M, obj.filename) - elseif obj.type == 'onGenome' then - for i=1,#species,1 do - local s = species[i] - if s.id == obj.speciesId then - s.genomes[obj.genomeIndex] = obj.genome - break + if obj.type == 'onMessage' then + message(_M, obj.msg, obj.color) + elseif obj.type == 'onLoad' then + load(_M, obj.filename) + elseif obj.type == 'onSave' then + save(_M, obj.filename) + elseif obj.type == 'onGenome' then + for i=1,#speciesSlice,1 do + local s = speciesSlice[i] + if s.id == obj.speciesId then + s.genomes[obj.genomeIndex] = obj.genome + break + end + end + genomeCallback(obj.genome, obj.index) + elseif obj.type == 'onFinish' then + finished = finished + 1 + if finished == #speciesSlice then + outputFile:close() + return end end - genomeCallback(obj.genome, obj.index) - elseif obj.type == 'onFinish' then - finishCallback() - end - ::continue:: - line = outputFile:read() - until(line == "" or line == nil) - outputFile:close() - end + ::continue:: + line = outputFile:read() + until(line == "" or line == nil) + end + error(string.format("Some processes never finished? Saw %d terminations.", finished)) + end) end return _M diff --git a/runner.lua b/runner.lua index 9ea2bd1..52d51f4 100644 --- a/runner.lua +++ b/runner.lua @@ -12,6 +12,8 @@ local Outputs = #config.ButtonNames local guiWidth = 0 local guiHeight = 0 +local Promise = nil + local function message(_M, msg, color) if color == nil then color = 0x00009900 @@ -213,19 +215,21 @@ local function displayGenome(genome) gui.renderctx.setnull() end -local function advanceFrame(_M, after) - table.insert(_M.onFrameAdvancedHandler, after) +local function advanceFrame(_M) + local promise = Promise.new() + table.insert(_M.onFrameAdvancedHandler, promise) + return promise end local function processFrameAdvanced(_M) for i=#_M.onFrameAdvancedHandler,1,-1 do - table.remove(_M.onFrameAdvancedHandler, i)() + table.remove(_M.onFrameAdvancedHandler, i):resolve() end end local buttons = nil local buttonCtx = gui.renderctx.new(500, 70) -function displayButtons() +local function displayButtons(_M) buttonCtx:set() buttonCtx:clear() @@ -249,7 +253,7 @@ function displayButtons() local insert = "" local confirm = "[Tab] Type in filename" - if inputmode then + if _M.inputmode then insert = "_" confirm = "[Tab] Confirm filename" end @@ -264,7 +268,7 @@ end local formCtx = nil local form = nil -function displayForm(_M) +local function displayForm(_M) if #_M.onRenderFormHandler == 0 then return end @@ -303,7 +307,7 @@ function displayForm(_M) gui.text(320, 65, string.format("Current Area: %04x", _M.currentArea)) gui.text(320, 80, "Rightmost: "..rightmost) - displayButtons() + displayButtons(_M) formCtx:set() buttons:draw(5, 130) @@ -373,11 +377,12 @@ local function evaluateNetwork(_M, network, inputs, inputDeltas) return outputs end +local controller = nil local function evaluateCurrent(_M) local genome = _M.currentSpecies.genomes[_M.currentGenomeIndex] local inputDeltas = {} - inputs, inputDeltas = game.getInputs() + local inputs, inputDeltas = game.getInputs() controller = evaluateNetwork(_M, genome.network, inputs, inputDeltas) @@ -405,9 +410,70 @@ local function fitnessAlreadyMeasured(_M) return genome.fitness ~= 0 end +local rewinds = {} local rew = movie.to_rewind(config.NeatConfig.Filename) +local function rewind() + local promise = Promise.new() + movie.unsafe_rewind(rew) + table.insert(rewinds, promise) + return promise +end -local function initializeRun(_M, after) +local function rewound() + for i=#rewinds,1,-1 do + local promise = table.remove(rewinds, i) + promise:resolve() + end +end + +local function newNeuron() + local neuron = {} + neuron.incoming = {} + neuron.value = 0.0 + --neuron.dw = 1 + return neuron +end + +local function generateNetwork(genome) + local network = {} + network.neurons = {} + + for i=1,Inputs do + network.neurons[i] = newNeuron() + end + + for o=1,Outputs do + if o == 4 then + goto continue + end + + network.neurons[config.NeatConfig.MaxNodes+o] = newNeuron() + + ::continue:: + 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 + + +local function initializeRun(_M) message(_M, string.format("Total Genomes: %d", #_M.currentSpecies.genomes)) settings.set_speed("turbo") @@ -419,12 +485,46 @@ local function initializeRun(_M, after) end exec('enable-sound '..enableSound) gui.subframe_update(false) - table.insert(_M.runInitialized, after) - movie.unsafe_rewind(rew) + + return rewind():next(function() + if config.StartPowerup ~= nil then + game.writePowerup(config.StartPowerup) + end + _M.currentFrame = 0 + _M.timeout = config.NeatConfig.TimeoutConstant + -- Kill the run if we go back to the map screen + game.onceMapLoaded(function() + _M.timeout = -100000 + end) + _M.bumps = 0 + -- Penalize player for collisions that do not result in enemy deaths + game.onEmptyHit(function() + _M.bumps = _M.bumps + 1 + end) + game.clearJoypad() + _M.startKong = game.getKong() + _M.startBananas = game.getBananas() + _M.startKrem = game.getKremCoins() + _M.lastKrem = _M.startKrem + _M.startCoins = game.getCoins() + _M.startLives = game.getLives() + _M.partyHitCounter = 0 + _M.powerUpCounter = 0 + _M.powerUpBefore = game.getBoth() + _M.currentArea = game.getCurrentArea() + _M.lastArea = _M.currentArea + _M.rightmost = { [_M.currentArea] = 0 } + _M.upmost = { [_M.currentArea] = 0 } + local genome = _M.currentSpecies.genomes[_M.currentGenomeIndex] + generateNetwork(genome) + evaluateCurrent(_M) + end) end +local frame = 0 + local function mainLoop(_M, genome) - advanceFrame(_M, function() + return advanceFrame(_M):next(function() if genome ~= nil then _M.currentFrame = _M.currentFrame + 1 end @@ -432,11 +532,7 @@ local function mainLoop(_M, genome) genome = _M.currentSpecies.genomes[_M.currentGenomeIndex] if _M.drawFrame % 10 == 0 then - --if not pcall(function() - displayGenome(genome) - --end) then - --message(_M, "Could not render genome graph", 0x00990000) - --end + displayGenome(genome) end if _M.currentFrame%5 == 0 then @@ -529,8 +625,7 @@ local function mainLoop(_M, genome) -- Continue if we haven't timed out local timeoutBonus = _M.currentFrame / 4 if _M.timeout + timeoutBonus > 0 then - mainLoop(_M, genome) - return + return mainLoop(_M, genome) end -- Timeout calculations beyond this point @@ -609,103 +704,16 @@ local function mainLoop(_M, genome) input.keyhook("9", false) input.keyhook("tab", false) - _M.finishCallback() return end end - initializeRun(_M, function() - mainLoop(_M, genome) + + return initializeRun(_M):next(function() + return mainLoop(_M, genome) end) end) end -local function newNeuron() - local neuron = {} - neuron.incoming = {} - neuron.value = 0.0 - --neuron.dw = 1 - return neuron -end - -local function generateNetwork(genome) - local network = {} - network.neurons = {} - - for i=1,Inputs do - network.neurons[i] = newNeuron() - end - - for o=1,Outputs do - if o == 4 then - goto continue - end - - network.neurons[config.NeatConfig.MaxNodes+o] = newNeuron() - - ::continue:: - 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 - -local function elapsed(_M) - if config.StartPowerup ~= nil then - game.writePowerup(config.StartPowerup) - end - _M.currentFrame = 0 - _M.timeout = config.NeatConfig.TimeoutConstant - -- Kill the run if we go back to the map screen - game.onceMapLoaded(function() - _M.timeout = -100000 - end) - _M.bumps = 0 - -- Penalize player for collisions that do not result in enemy deaths - game.onEmptyHit(function() - _M.bumps = _M.bumps + 1 - end) - game.clearJoypad() - _M.startKong = game.getKong() - _M.startBananas = game.getBananas() - _M.startKrem = game.getKremCoins() - _M.lastKrem = _M.startKrem - _M.startCoins = game.getCoins() - _M.startLives = game.getLives() - _M.partyHitCounter = 0 - _M.powerUpCounter = 0 - _M.powerUpBefore = game.getBoth() - _M.currentArea = game.getCurrentArea() - _M.lastArea = _M.currentArea - _M.rightmost = { [_M.currentArea] = 0 } - _M.upmost = { [_M.currentArea] = 0 } - local genome = _M.currentSpecies.genomes[_M.currentGenomeIndex] - generateNetwork(genome) - evaluateCurrent(_M) - for i=#_M.runInitialized,1,-1 do - table.remove(_M.runInitialized, i)() - end -end - -local function rewound() - set_timer_timeout(1) -end - local function register(_M, name, func) callback.register(name, func) _M.dereg[#_M.dereg+1] = { name, func } @@ -740,7 +748,7 @@ local function keyhook (_M, key, state) if key == "tab" then _M.inputmode = not _M.inputmode _M.helddown = key - elseif inputmode then + elseif _M.inputmode then return elseif key == "1" then _M.helddown = key @@ -761,13 +769,13 @@ local function keyhook (_M, key, state) pool.run(true) end elseif state.value == 0 then - helddown = nil + _M.helddown = nil end end local function saveLoadInput(_M) local inputs = input.raw() - if not inputmode then + if not _M.inputmode then -- FIXME _M.saveLoadFile = config.NeatConfig.SaveFile return @@ -829,30 +837,25 @@ local function saveLoadInput(_M) end end -local function run(_M, species, generationIdx, genomeCallback, finishCallback) +local function run(_M, species, generationIdx, genomeCallback) game.registerHandlers() _M.currentGenerationIndex = generationIdx _M.currentSpecies = species _M.currentGenomeIndex = 1 _M.genomeCallback = genomeCallback - _M.finishCallback = finishCallback register(_M, 'paint', function() painting(_M) end) register(_M, 'input', function() + frame = frame + 1 processFrameAdvanced(_M) - end) - register(_M, 'input', function() saveLoadInput(_M) end) register(_M, 'keyhook', function(key, state) keyhook(_M, key, state) end) register(_M, 'post_rewind', rewound) - register(_M, 'timer', function() - elapsed(_M) - end) input.keyhook("1", true) input.keyhook("4", true) @@ -861,8 +864,8 @@ local function run(_M, species, generationIdx, genomeCallback, finishCallback) input.keyhook("9", true) input.keyhook("tab", true) - initializeRun(_M, function() - mainLoop(_M) + return initializeRun(_M):next(function() + return mainLoop(_M) end) end @@ -874,11 +877,11 @@ local function onRenderForm(_M, handler) table.insert(_M.onRenderFormHandler, handler) end -return function() +return function(promise) + Promise = promise local _M = { currentGenerationIndex = 1, currentSpecies = nil, - finishCallback = nil, genomeCallback = nil, currentGenomeIndex = 1, currentFrame = 0, @@ -907,8 +910,6 @@ return function() upmost = {}, lastBoth = 0, - runInitialized = {}, - onMessageHandler = {}, onSaveHandler = {}, onLoadHandler = {}, @@ -933,8 +934,8 @@ return function() onLoad(_M, handler) end - _M.run = function(species, generationIdx, genomeCallback, finishCallback) - run(_M, species, generationIdx, genomeCallback, finishCallback) + _M.run = function(species, generationIdx, genomeCallback) + return run(_M, species, generationIdx, genomeCallback) end return _M diff --git a/tools/bsnes-launcher.lua b/tools/bsnes-launcher.lua index d7feace..6fe849e 100644 --- a/tools/bsnes-launcher.lua +++ b/tools/bsnes-launcher.lua @@ -1,6 +1,6 @@ local base = string.gsub(@@LUA_SCRIPT_FILENAME@@, "(.*[/\\])(.*)", "%1").."/.." -local util = dofile(base.."/util.lua") +local util = dofile(base.."/util.lua")() local config = dofile(base.."/config.lua") local mem = dofile(base.."/mem.lua") diff --git a/tools/status-overlay.lua b/tools/status-overlay.lua index 817d0a6..aa4e221 100644 --- a/tools/status-overlay.lua +++ b/tools/status-overlay.lua @@ -6,7 +6,7 @@ local warn = '========== The ROM must be running before running this script' io.stderr:write(warn) print(warn) -local util = dofile(base.."/util.lua") +local util = dofile(base.."/util.lua")() local mem = dofile(base.."/mem.lua") local spritelist = dofile(base.."/spritelist.lua") local game = dofile(base.."/game.lua") @@ -436,3 +436,17 @@ input.keyhook("9", true) input.keyhook("0", true) set_timer_timeout(100 * 1000) + +for i=0,22,1 do + memory2.BUS:registerwrite(mem.addr.spriteBase + mem.size.sprite * i + mem.offset.sprite.x, function(addr, val) + print(memory.getregister('pc')) + end) +end + +-- fe0a58 crate: near bunch and klomp on barrels +-- fe0a58: Crate X position +-- fe0a60: Crate Y position + +-- fe0a70 bunch: near crate and klomp on barrels +-- fe0a70: X position +-- fe0a72: Y position \ No newline at end of file diff --git a/util.lua b/util.lua index e74c2fd..1dd4b96 100644 --- a/util.lua +++ b/util.lua @@ -2,6 +2,8 @@ local utime = utime local base = string.gsub(@@LUA_SCRIPT_FILENAME@@, "(.*[/\\])(.*)", "%1") +local Promise = nil + local _M = {} _M.isWin = package.config:sub(1, 1) == '\\' @@ -102,27 +104,10 @@ function _M.closeCmd(handle) end end -function _M.finishWaiting(waiter, count) - if count == nil then - count = 1 - end +function _M.waitForFiles(filenames, count, wild) + local promise = Promise.new() + promise:resolve() - if _M.isWin then - local i = 1 - while i <= count do - local line = waiter:read("*l") - for chr in line:gmatch(";") do - i = i + 1 - end - i = i + 1 - end - else - waiter:read("*a") - _M.closeCmd(waiter) - end -end - -function _M.startWaiting(filenames, count, wild) if type(filenames) == 'string' then if wild == nil then wild = filenames @@ -135,16 +120,15 @@ function _M.startWaiting(filenames, count, wild) count = #filenames end + local poppet = nil if _M.isWin then local sec, usec = utime() print(string.format('Starting watching file at %d', sec * 1000000 + usec)) local cmd = '"'..base..'/watchexec/watchexec.exe" "-w" "'..table.concat(filenames, '" "-w" "')..'" "echo" "%WATCHEXEC_WRITTEN_PATH%"' - local poppet = _M.popenCmd(cmd, base) + poppet = _M.popenCmd(cmd, base) poppet:read("*l") - - return poppet else local watchCmd = '' if count == 1 then @@ -167,8 +151,24 @@ done ) & wait EOF]] end - return _M.popenCmd(watchCmd) + poppet = _M.popenCmd(watchCmd) end + + return promise:next(function() + if _M.isWin then + local i = 1 + while i <= count do + local line = poppet:read("*l") + for chr in line:gmatch(";") do + i = i + 1 + end + i = i + 1 + end + else + print(poppet:read("*a")) + _M.closeCmd(poppet) + end + end) end function _M.table_to_string(tbl) @@ -214,4 +214,7 @@ function _M.file_exists(name) if f~=nil then io.close(f) return true else return false end end -return _M +return function(promise) + Promise = promise + return _M +end \ No newline at end of file