diff --git a/config.lua b/config.lua index 9adb047..967cad7 100644 --- a/config.lua +++ b/config.lua @@ -35,7 +35,8 @@ _M.Filename = _M.PoolDir .. _M.State[1] _M.StartPowerup = 0 _M.NeatConfig = { -Threads = 8, +Threads = 2, +ThreadDontQuit = true, --Filename = "DP1.state", SaveFile = _M.Filename .. ".pool", Filename = _M.Filename, diff --git a/neat-donk.lua b/neat-donk.lua index e927178..2dc72ea 100644 --- a/neat-donk.lua +++ b/neat-donk.lua @@ -2,8 +2,6 @@ local base = string.gsub(@@LUA_SCRIPT_FILENAME@@, "(.*[/\\])(.*)", "%1") -local game = dofile(base.."/game.lua") -local config = dofile(base.."/config.lua") local pool = dofile(base.."/pool.lua") local util = dofile(base.."/util.lua") diff --git a/pool.lua b/pool.lua index 9cb109a..6fee97f 100644 --- a/pool.lua +++ b/pool.lua @@ -414,13 +414,13 @@ local function loadFile(filename, after) return end local contents = file:read("*all") - local obj, err = loadstring(libDeflate:DecompressDeflate(contents:sub(11, #contents - 8))) - if err ~= nil then - message(string.format("Error parsing: %s", err), 0x00990000) + 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 local function savePool() diff --git a/promise.lua b/promise.lua new file mode 100644 index 0000000..8fd9c17 --- /dev/null +++ b/promise.lua @@ -0,0 +1,299 @@ +--[[ +The MIT License (MIT) +===================== + +Copyright © `2015` `Colin Fein` + +Permission is hereby granted, free of charge, to any person +obtaining a copy of this software and associated documentation +files (the “Software”), to deal in the Software without +restriction, including without limitation the rights to use, +copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the +Software is furnished to do so, subject to the following +conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +OTHER DEALINGS IN THE SOFTWARE. +]] + +-- Port of https://github.com/rhysbrettbowen/promise_impl/blob/master/promise.js +-- and https://github.com/rhysbrettbowen/Aplus +-- +local queue = {} + +local State = { + PENDING = 'pending', + FULFILLED = 'fulfilled', + REJECTED = 'rejected', +} + +local passthrough = function(x) return x end +local errorthrough = function(x) error(x) end + +local function callable_table(callback) + local mt = getmetatable(callback) + return type(mt) == 'table' and type(mt.__call) == 'function' +end + +local function is_callable(value) + local t = type(value) + return t == 'function' or (t == 'table' and callable_table(value)) +end + +local transition, resolve, run + +local Promise = { + is_promise = true, + state = State.PENDING +} +Promise.mt = { __index = Promise } + +local do_async = function(callback) + if Promise.async then + Promise.async(callback) + else + table.insert(queue, callback) + end +end + +local reject = function(promise, reason) + transition(promise, State.REJECTED, reason) +end + +local fulfill = function(promise, value) + transition(promise, State.FULFILLED, value) +end + +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 + + promise.state = state + promise.value = value + run(promise) +end + +function Promise:next(on_fulfilled, on_rejected) + local promise = Promise.new() + + table.insert(self.queue, { + fulfill = is_callable(on_fulfilled) and on_fulfilled or nil, + reject = is_callable(on_rejected) and on_rejected or nil, + promise = promise + }) + + run(self) + + return promise +end + +resolve = function(promise, x) + if promise == x then + reject(promise, 'TypeError: cannot resolve a promise with itself') + return + end + + local x_type = type(x) + + if x_type ~= 'table' then + fulfill(promise, x) + return + end + + -- x is a promise in the current implementation + if x.is_promise then + -- 2.3.2.1 if x is pending, resolve or reject this promise after completion + if x.state == State.PENDING then + x:next( + function(value) + resolve(promise, value) + end, + function(reason) + reject(promise, reason) + end + ) + return + end + -- if x is not pending, transition promise to x's state and value + transition(promise, x.state, x.value) + return + end + + local called = false + -- 2.3.3.1. Catches errors thrown by __index metatable + local success, reason = pcall(function() + local next = x.next + if is_callable(next) then + next( + x, + function(y) + if not called then + resolve(promise, y) + called = true + end + end, + function(r) + if not called then + reject(promise, r) + called = true + end + end + ) + else + fulfill(promise, x) + end + end) + + if not success then + if not called then + reject(promise, reason) + end + end +end + +run = function(promise) + if promise.state == State.PENDING then return end + + do_async(function() + -- drain promise.queue while allowing pushes from within callbacks + local q = promise.queue + local i = 0 + while i < #q do + i = i + 1 + local obj = q[i] + local success, result = pcall(function() + local success = obj.fulfill or passthrough + local failure = obj.reject or errorthrough + local callback = promise.state == State.FULFILLED and success or failure + return callback(promise.value) + end) + + if not success then + reject(obj.promise, result) + else + resolve(obj.promise, result) + end + end + for j = 1, i do + q[j] = nil + end + end) +end + +function Promise.new(callback) + local instance = { + queue = {} + } + setmetatable(instance, Promise.mt) + + if callback then + callback( + function(value) + resolve(instance, value) + end, + function(reason) + reject(instance, reason) + end + ) + end + + return instance +end + +function Promise:catch(callback) + return self:next(nil, callback) +end + +function Promise:resolve(value) + fulfill(self, value) +end + +function Promise:reject(reason) + reject(self, reason) +end + +function Promise.update() + while true do + local async = table.remove(queue, 1) + + if not async then + break + end + + async() + end +end + +-- resolve when all promises complete +function Promise.all(...) + local promises = {...} + local results = {} + local state = State.FULFILLED + local remaining = #promises + + local promise = Promise.new() + + local check_finished = function() + if remaining > 0 then + return + end + transition(promise, state, results) + end + + for i,p in ipairs(promises) do + p:next( + function(value) + results[i] = value + remaining = remaining - 1 + check_finished() + end, + function(value) + results[i] = value + remaining = remaining - 1 + state = State.REJECTED + check_finished() + end + ) + end + + check_finished() + + return promise +end + +-- resolve with first promise to complete +function Promise.race(...) + local promises = {...} + local promise = Promise.new() + + Promise.all(...):next(nil, function(value) + reject(promise, value) + end) + + local success = function(value) + fulfill(promise, value) + end + + for _,p in ipairs(promises) do + p:next(success) + end + + return promise +end + +return Promise \ No newline at end of file diff --git a/runner-process.lua b/runner-process.lua index 488f871..e84cf39 100644 --- a/runner-process.lua +++ b/runner-process.lua @@ -5,16 +5,14 @@ local serpent = dofile(base.."/serpent.lua") local util = dofile(base.."/util.lua") local runnerDataFile = io.open(os.getenv("RUNNER_DATA"), 'r') -local runnerData, err = loadstring(runnerDataFile:read('*a')) +local ok, runnerData = serpent.load(runnerDataFile:read('*a')) runnerDataFile:close() -if err ~= nil then - print(err) +if not ok then + print("Deserialization error") return end -runnerData = runnerData() - local species = runnerData[1] local speciesId = species.id @@ -110,6 +108,8 @@ runner.run( ) outFile:write(table.concat(outContents, "\n")) outFile:close() - exec('quit-emulator') + if os.getenv("RUNNER_DONT_QUIT") == nil then + exec('quit-emulator') + end end ) diff --git a/runner-wrapper.lua b/runner-wrapper.lua index a6e1525..8dc68ee 100644 --- a/runner-wrapper.lua +++ b/runner-wrapper.lua @@ -1,8 +1,25 @@ local base = string.gsub(@@LUA_SCRIPT_FILENAME@@, "(.*[/\\])(.*)", "%1") +local util = dofile(base.."/util.lua") local config = dofile(base.."/config.lua") local serpent = dofile(base.."/serpent.lua") -local tmpFileName = "/tmp/donk_runner_"..tostring(math.floor(random.integer(0, 0xffffffffffffffff))):hex() +local temps = { + os.getenv("TMPDIR"), + os.getenv("TEMP"), + os.getenv("TEMPDIR"), + os.getenv("TMP"), +} + +local tempDir = "/tmp" +for i=1,#temps,1 do + local temp = temps[i] + if temp ~= nil and temp ~= "" then + tempDir = temps[i] + break + end +end + +local tmpFileName = tempDir.."donk_runner" local function message(_M, msg, color) if color == nil then @@ -70,13 +87,40 @@ return function() local outputFileName = tmpFileName..'_output_'..i local inputFileName = tmpFileName.."_input_"..i + print(inputFileName) local inputFile = io.open(inputFileName, 'w') inputFile:write(serpent.dump({species[i], generationIdx, outputFileName})) inputFile:close() - local cmd = "RUNNER_DATA=\""..inputFileName.."\" lsnes \"--rom="..config.ROM.."\" --unpause \"--lua="..base.."/runner-process.lua\"" - message(_M, cmd) - local poppet = io.popen(cmd, 'r') + local proc = "lsnes" + if util.isWin then + local checkParent = io.popen('powershell "(Get-WmiObject Win32_Process -Filter ProcessId=$((Get-WmiObject Win32_Process -Filter ProcessId=$((Get-WmiObject Win32_Process -Filter ProcessId=$PID).ParentProcessId)).ParentProcessId)").ExecutablePath') + proc = checkParent:read("*l") + checkParent:close() + else + -- FIXME Linux + end + print(proc) + local cmd = "\""..proc.."\" \"--rom="..config.ROM.."\" --unpause \"--lua="..base.."/runner-process.lua\"" + local envs = { + RUNNER_DATA = inputFileName + } + if config.NeatConfig.ThreadDontQuit then + envs.RUNNER_DONT_QUIT = "1" + end + + local cmdParts = {} + for k,v in pairs(envs) do + if util.isWin then + table.insert(cmdParts, string.format("set %s=%s &&", k, v)) + else + table.insert(cmdParts, string.format("%s='%s'", k, v)) + end + end + table.insert(cmdParts, cmd) + local fullCmd = table.concat(cmdParts, " ") + message(_M, fullCmd) + local poppet = io.popen(fullCmd, 'r') table.insert(poppets, poppet) end @@ -91,13 +135,11 @@ return function() local outputFile = io.open(outputFileName, "r") local line = "" repeat - local obj, err = loadstring(line) - if err ~= nil then + local ok, obj = serpent.load(line) + if not ok then goto continue end - obj = obj() - if obj == nil then goto continue end diff --git a/runner.lua b/runner.lua index 5d4ca6b..a5de554 100644 --- a/runner.lua +++ b/runner.lua @@ -557,7 +557,7 @@ local function mainLoop(_M, genome) _M.genomeCallback(genome, _M.currentGenomeIndex) end - message(_M, string.format("Gen %d species %d genome %d fitness: %d", _M.currentGenerationIndex, _M.currentSpecies.id, _M.currentGenomeIndex, fitness)) + message(_M, string.format("Gen %d species %d genome %d fitness: %d", _M.currentGenerationIndex, _M.currentSpecies.id, _M.currentGenomeIndex, math.floor(fitness))) _M.currentGenomeIndex = 1 while fitnessAlreadyMeasured(_M) do _M.currentGenomeIndex = _M.currentGenomeIndex + 1