Refactor to allow multithreading.
This commit is contained in:
parent
330cf035fa
commit
7aa80750f8
9 changed files with 1914 additions and 1459 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -6,3 +6,4 @@ crashsave*
|
|||
*.pool*
|
||||
*.lsvs
|
||||
config.lua
|
||||
*.sfc
|
||||
|
|
|
@ -10,6 +10,8 @@ _M.ScriptDir = base
|
|||
_M.StateDir = _M.ScriptDir .. "/state/"
|
||||
_M.PoolDir = _M.ScriptDir .. "/pool/"
|
||||
|
||||
_M.ROM = _M.ScriptDir .. "/rom.sfc"
|
||||
|
||||
--[[
|
||||
At the moment the first in list will get loaded.
|
||||
Rearrange for other savestates. (will be redone soon)
|
||||
|
@ -33,6 +35,7 @@ _M.Filename = _M.PoolDir .. _M.State[1]
|
|||
_M.StartPowerup = 0
|
||||
|
||||
_M.NeatConfig = {
|
||||
Threads = 4,
|
||||
--Filename = "DP1.state",
|
||||
SaveFile = _M.Filename .. ".pool",
|
||||
Filename = _M.Filename,
|
||||
|
|
58
game.lua
58
game.lua
|
@ -5,7 +5,19 @@ 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 _M = {}
|
||||
local _M = {
|
||||
leader = 0,
|
||||
tilePtr = 0,
|
||||
vertical = false,
|
||||
partyX = 0,
|
||||
partyY = 0,
|
||||
|
||||
cameraX = 0,
|
||||
cameraY = 0,
|
||||
|
||||
screenX = 0,
|
||||
screenY = 0,
|
||||
}
|
||||
|
||||
spritelist.InitSpriteList()
|
||||
spritelist.InitExtSpriteList()
|
||||
|
@ -33,17 +45,17 @@ local MAIN_AREA_NUMBER = 0x7e08a8
|
|||
local CURRENT_AREA_NUMBER = 0x7e08c8
|
||||
|
||||
function _M.getPositions()
|
||||
leader = memory.readword(LEAD_CHAR)
|
||||
tilePtr = memory.readhword(TILEDATA_POINTER)
|
||||
vertical = memory.readword(TILE_COLLISION_MATH_POINTER) == VERTICAL_POINTER
|
||||
partyX = memory.readword(PARTY_X)
|
||||
partyY = memory.readword(PARTY_Y)
|
||||
_M.leader = memory.readword(LEAD_CHAR)
|
||||
_M.tilePtr = memory.readhword(TILEDATA_POINTER)
|
||||
_M.vertical = memory.readword(TILE_COLLISION_MATH_POINTER) == VERTICAL_POINTER
|
||||
_M.partyX = memory.readword(PARTY_X)
|
||||
_M.partyY = memory.readword(PARTY_Y)
|
||||
|
||||
cameraX = memory.readword(CAMERA_X)
|
||||
cameraY = memory.readword(CAMERA_Y)
|
||||
_M.cameraX = memory.readword(CAMERA_X)
|
||||
_M.cameraY = memory.readword(CAMERA_Y)
|
||||
|
||||
_M.screenX = (partyX-256-cameraX)*2
|
||||
_M.screenY = (partyY-256-cameraY)*2
|
||||
_M.screenX = (_M.partyX-256-_M.cameraX)*2
|
||||
_M.screenY = (_M.partyY-256-_M.cameraY)*2
|
||||
end
|
||||
|
||||
function _M.getBananas()
|
||||
|
@ -100,7 +112,7 @@ function _M.getBoth()
|
|||
end
|
||||
|
||||
function _M.getVelocityY()
|
||||
local sprite = _M.getSprite(leader)
|
||||
local sprite = _M.getSprite(_M.leader)
|
||||
if sprite == nil then
|
||||
return 1900
|
||||
end
|
||||
|
@ -108,7 +120,7 @@ function _M.getVelocityY()
|
|||
end
|
||||
|
||||
function _M.getVelocityX()
|
||||
local sprite = _M.getSprite(leader)
|
||||
local sprite = _M.getSprite(_M.leader)
|
||||
if sprite == nil then
|
||||
return 1900
|
||||
end
|
||||
|
@ -216,12 +228,12 @@ function _M.tileIsSolid(x, y, tileVal, tileOffset)
|
|||
end
|
||||
|
||||
function _M.getTile(dx, dy)
|
||||
local tileX = math.floor((partyX + dx * TILE_SIZE) / TILE_SIZE) * TILE_SIZE
|
||||
local tileY = math.floor((partyY + dy * TILE_SIZE) / TILE_SIZE) * TILE_SIZE
|
||||
local tileX = math.floor((_M.partyX + dx * TILE_SIZE) / TILE_SIZE) * TILE_SIZE
|
||||
local tileY = math.floor((_M.partyY + dy * TILE_SIZE) / TILE_SIZE) * TILE_SIZE
|
||||
|
||||
local offset = _M.tileOffsetCalculation(tileX, tileY, vertical)
|
||||
local offset = _M.tileOffsetCalculation(tileX, tileY, _M.vertical)
|
||||
|
||||
local tile = memory.readword(tilePtr + offset)
|
||||
local tile = memory.readword(_M.tilePtr + offset)
|
||||
|
||||
if not _M.tileIsSolid(tileX, tileY, tile, offset) then
|
||||
return 0
|
||||
|
@ -235,7 +247,7 @@ function _M.getCurrentArea()
|
|||
end
|
||||
|
||||
function _M.getJumpHeight()
|
||||
local sprite = _M.getSprite(leader)
|
||||
local sprite = _M.getSprite(_M.leader)
|
||||
if sprite == nil then
|
||||
return 0
|
||||
end
|
||||
|
@ -255,8 +267,8 @@ function _M.getSprite(idx)
|
|||
local y = memory.readword(base_addr + 0x0a)
|
||||
local sprite = {
|
||||
control = control,
|
||||
screenX = x - 256 - cameraX - 256,
|
||||
screenY = y - 256 - cameraY - 256,
|
||||
screenX = x - 256 - _M.cameraX - 256,
|
||||
screenY = y - 256 - _M.cameraY - 256,
|
||||
jumpHeight = memory.readword(base_addr + 0x0e),
|
||||
-- style bits
|
||||
-- 0x4000 0: Right facing 1: Flipped
|
||||
|
@ -365,8 +377,8 @@ function _M.getInputs()
|
|||
|
||||
for i = 1,#sprites do
|
||||
local sprite = sprites[i]
|
||||
local distx = math.abs(sprite.x - (partyX+dx*TILE_SIZE))
|
||||
local disty = math.abs(sprite.y - (partyY+dy*TILE_SIZE))
|
||||
local distx = math.abs(sprite.x - (_M.partyX+dx*TILE_SIZE))
|
||||
local disty = math.abs(sprite.y - (_M.partyY+dy*TILE_SIZE))
|
||||
local dist = math.sqrt((distx * distx) + (disty * disty))
|
||||
if dist <= TILE_SIZE * 1.25 then
|
||||
inputs[#inputs] = sprite.good
|
||||
|
@ -379,8 +391,8 @@ function _M.getInputs()
|
|||
end
|
||||
|
||||
for i = 1,#extended do
|
||||
local distx = math.abs(extended[i]["x"]+cameraX - (partyX+dx*TILE_SIZE))
|
||||
local disty = math.abs(extended[i]["y"]+cameraY - (partyY+dy*TILE_SIZE))
|
||||
local distx = math.abs(extended[i]["x"]+_M.cameraX - (_M.partyX+dx*TILE_SIZE))
|
||||
local disty = math.abs(extended[i]["y"]+_M.cameraY - (_M.partyY+dy*TILE_SIZE))
|
||||
if distx < TILE_SIZE / 2 and disty < TILE_SIZE / 2 then
|
||||
|
||||
inputs[#inputs] = extended[i]["good"]
|
||||
|
|
1457
neat-donk.lua
1457
neat-donk.lua
File diff suppressed because it is too large
Load diff
766
pool.lua
Normal file
766
pool.lua
Normal file
|
@ -0,0 +1,766 @@
|
|||
local base = string.gsub(@@LUA_SCRIPT_FILENAME@@, "(.*[/\\])(.*)", "%1")
|
||||
|
||||
local config = dofile(base.."/config.lua")
|
||||
local Runner = nil
|
||||
if config.NeatConfig.Threads > 1 then
|
||||
Runner = dofile(base.."/runner-wrapper.lua")
|
||||
else
|
||||
Runner = dofile(base.."/runner.lua")
|
||||
end
|
||||
|
||||
local json = dofile(base.."/dkjson.lua")
|
||||
local libDeflate = dofile(base.."/LibDeflate.lua")
|
||||
|
||||
local Inputs = config.InputSize+1
|
||||
local Outputs = #config.ButtonNames
|
||||
|
||||
local _M = {
|
||||
saveLoadFile = config.NeatConfig.SaveFile,
|
||||
onMessageHandler = {},
|
||||
onRenderFormHandler = {},
|
||||
}
|
||||
|
||||
local pool = nil
|
||||
|
||||
local 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
|
||||
|
||||
local 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
|
||||
|
||||
local function newGene()
|
||||
local gene = {}
|
||||
gene.into = 0
|
||||
gene.out = 0
|
||||
gene.weight = 0.0
|
||||
gene.enabled = true
|
||||
gene.innovation = 0
|
||||
|
||||
return gene
|
||||
end
|
||||
|
||||
local 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
|
||||
|
||||
local function newInnovation()
|
||||
pool.innovation = pool.innovation + 1
|
||||
return pool.innovation
|
||||
end
|
||||
|
||||
local 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
|
||||
|
||||
local 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
|
||||
|
||||
local 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
|
||||
|
||||
local 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
|
||||
|
||||
local 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
|
||||
|
||||
local 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
|
||||
|
||||
local function basicGenome()
|
||||
local genome = newGenome()
|
||||
local innovation = 1
|
||||
|
||||
genome.maxneuron = Inputs
|
||||
mutate(genome)
|
||||
|
||||
return genome
|
||||
end
|
||||
|
||||
local function newPool()
|
||||
local pool = {}
|
||||
pool.species = {}
|
||||
pool.generation = 0
|
||||
pool.innovation = Outputs
|
||||
pool.maxFitness = 0
|
||||
|
||||
return pool
|
||||
end
|
||||
|
||||
local function newSpecies()
|
||||
local species = {}
|
||||
species.topFitness = 0
|
||||
species.staleness = 0
|
||||
species.genomes = {}
|
||||
species.averageFitness = 0
|
||||
|
||||
return species
|
||||
end
|
||||
|
||||
local 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
|
||||
|
||||
local 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
|
||||
|
||||
local 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
|
||||
|
||||
local 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
|
||||
|
||||
local function initializePool(after)
|
||||
pool = newPool()
|
||||
|
||||
for i=1,config.NeatConfig.Population do
|
||||
basic = basicGenome()
|
||||
addToSpecies(basic)
|
||||
end
|
||||
|
||||
after()
|
||||
end
|
||||
|
||||
local function bytes(x)
|
||||
local b4=x%256 x=(x-x%256)/256
|
||||
local b3=x%256 x=(x-x%256)/256
|
||||
local b2=x%256 x=(x-x%256)/256
|
||||
local b1=x%256 x=(x-x%256)/256
|
||||
return string.char(b1,b2,b3,b4)
|
||||
end
|
||||
|
||||
local function writeFile(filename)
|
||||
local file = io.open(filename, "w")
|
||||
local json = json.encode(pool)
|
||||
local zlib = libDeflate:CompressDeflate(json)
|
||||
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(#json % (2^32)))
|
||||
file:close()
|
||||
return
|
||||
end
|
||||
|
||||
-- FIXME Save/load mechanism has to be rethought with items running in parallel
|
||||
local function loadFile(filename, after)
|
||||
print("Loading pool from " .. filename)
|
||||
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 obj, pos, err = json.decode(libDeflate:DecompressDeflate(contents:sub(11, #contents - 8)))
|
||||
if err ~= nil then
|
||||
message(string.format("Error parsing: %s", err), 0x00990000)
|
||||
return
|
||||
end
|
||||
|
||||
pool = obj
|
||||
end
|
||||
|
||||
local function savePool()
|
||||
local filename = _M.saveLoadFile
|
||||
writeFile(filename)
|
||||
message(string.format("Saved \"%s\"!", filename:sub(#filename - 50)), 0x00009900)
|
||||
end
|
||||
|
||||
local function loadPool(after)
|
||||
loadFile(_M.saveLoadFile, after)
|
||||
end
|
||||
|
||||
local function message(msg, color)
|
||||
if color == nil then
|
||||
color = 0x00009900
|
||||
end
|
||||
|
||||
for i=#_M.onMessageHandler,1,-1 do
|
||||
_M.onMessageHandler[i](msg, color)
|
||||
end
|
||||
end
|
||||
|
||||
local function processRenderForm(form)
|
||||
for i=#_M.onRenderFormHandler,1,-1 do
|
||||
_M.onRenderFormHandler[i](form)
|
||||
end
|
||||
end
|
||||
|
||||
local 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
|
||||
|
||||
local 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
|
||||
|
||||
local 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
|
||||
|
||||
local 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
|
||||
|
||||
local function totalAverageFitness()
|
||||
local total = 0
|
||||
for s = 1,#pool.species do
|
||||
local species = pool.species[s]
|
||||
total = total + species.averageFitness
|
||||
end
|
||||
|
||||
return total
|
||||
end
|
||||
|
||||
local 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
|
||||
|
||||
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)]
|
||||
child = crossover(g1, g2)
|
||||
else
|
||||
g = species.genomes[math.random(1, #species.genomes)]
|
||||
child = copyGenome(g)
|
||||
end
|
||||
|
||||
mutate(child)
|
||||
|
||||
return child
|
||||
end
|
||||
|
||||
local 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
|
||||
|
||||
local 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
|
||||
|
||||
local 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(_M.saveLoadFile .. ".gen" .. pool.generation .. ".pool")
|
||||
end
|
||||
|
||||
local loadRequested = false
|
||||
local saveRequested = false
|
||||
local function mainLoop(currentSpecies)
|
||||
if loadRequested then
|
||||
loadRequested = false
|
||||
loadPool(mainLoop)
|
||||
return
|
||||
end
|
||||
|
||||
if saveRequested then
|
||||
saveRequested = false
|
||||
savePool()
|
||||
end
|
||||
|
||||
if topRequested then
|
||||
topRequested = false
|
||||
playTop()
|
||||
return
|
||||
end
|
||||
|
||||
if not config.Running then
|
||||
-- FIXME Tick?
|
||||
end
|
||||
|
||||
if currentSpecies == nil then
|
||||
currentSpecies = 1
|
||||
end
|
||||
local runner = Runner()
|
||||
runner.onMessage(function(msg, color)
|
||||
message(msg, color)
|
||||
end)
|
||||
runner.onSave(function(filename)
|
||||
_M.requestSave(filename)
|
||||
end)
|
||||
runner.onLoad(function(filename)
|
||||
_M.requestLoad(filename)
|
||||
end)
|
||||
runner.onRenderForm(function(form)
|
||||
processRenderForm(form)
|
||||
end)
|
||||
local slice = pool.species[currentSpecies]
|
||||
if config.NeatConfig.Threads > 1 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,
|
||||
currentSpecies,
|
||||
function()
|
||||
-- Genome callback
|
||||
end,
|
||||
function()
|
||||
if config.NeatConfig.Threads > 1 then
|
||||
finished = finished + 1
|
||||
if finished ~= #slice then
|
||||
return
|
||||
end
|
||||
currentSpecies = currentSpecies + #slice
|
||||
else
|
||||
currentSpecies = currentSpecies + 1
|
||||
end
|
||||
|
||||
if currentSpecies > #pool.species then
|
||||
newGeneration()
|
||||
currentSpecies = 1
|
||||
end
|
||||
mainLoop(currentSpecies)
|
||||
end
|
||||
)
|
||||
end
|
||||
|
||||
local topRequested = false
|
||||
local 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
|
||||
|
||||
-- FIXME genome
|
||||
mainLoop(maxs)
|
||||
end
|
||||
|
||||
function _M.requestLoad(filename)
|
||||
_M.saveLoadFile = filename
|
||||
loadRequested = true
|
||||
end
|
||||
|
||||
function _M.requestSave(filename)
|
||||
_M.saveLoadFile = filename
|
||||
saveRequested = true
|
||||
end
|
||||
|
||||
function _M.onMessage(handler)
|
||||
table.insert(_M.onMessageHandler, handler)
|
||||
end
|
||||
|
||||
function _M.onRenderForm(handler)
|
||||
table.insert(_M.onRenderFormHandler, handler)
|
||||
end
|
||||
|
||||
function _M.requestTop()
|
||||
topRequested = true
|
||||
end
|
||||
|
||||
function _M.run(reset)
|
||||
if pool == nil or reset == true then
|
||||
initializePool(function()
|
||||
writeFile(config.PoolDir.."temp.pool")
|
||||
mainLoop()
|
||||
end)
|
||||
else
|
||||
writeFile(config.PoolDir.."temp.pool")
|
||||
mainLoop()
|
||||
end
|
||||
end
|
||||
|
||||
return _M
|
107
runner-process.lua
Normal file
107
runner-process.lua
Normal file
|
@ -0,0 +1,107 @@
|
|||
local base = string.gsub(@@LUA_SCRIPT_FILENAME@@, "(.*[/\\])(.*)", "%1")
|
||||
|
||||
local Runner = dofile(base.."/runner.lua")
|
||||
local json = dofile(base.."/dkjson.lua")
|
||||
|
||||
local runnerData, pos, err = json.decode(os.getenv("RUNNER_DATA"))
|
||||
if err ~= nil then
|
||||
return
|
||||
end
|
||||
|
||||
local speciesIndex = runnerData[3]
|
||||
|
||||
local filename = runnerData[4]
|
||||
|
||||
local outFile = io.open(filename, "a")
|
||||
|
||||
local outContents = {}
|
||||
|
||||
local statusLine = nil
|
||||
local statusColor = 0x0000ff00
|
||||
|
||||
local runner = Runner()
|
||||
runner.onMessage(function(msg, color)
|
||||
statusLine = msg
|
||||
statusColor = color
|
||||
|
||||
table.insert(
|
||||
outContents,
|
||||
json.encode({
|
||||
type = 'onMessage',
|
||||
speciesIndex = speciesIndex,
|
||||
msg = msg,
|
||||
color = color,
|
||||
})
|
||||
)
|
||||
end)
|
||||
|
||||
local guiHeight = 0
|
||||
local guiWidth = 0
|
||||
runner.onRenderForm(function(form)
|
||||
guiWidth, guiHeight = gui.resolution()
|
||||
gui.left_gap(500)
|
||||
gui.top_gap(0)
|
||||
gui.bottom_gap(0)
|
||||
gui.right_gap(0)
|
||||
form:draw(-500, 0)
|
||||
|
||||
if statusLine ~= nil then
|
||||
gui.rectangle(-500, guiHeight - 20, 0, 20, 1, 0x00000000, statusColor)
|
||||
gui.text(-500, guiHeight - 20, statusLine, 0x00000000)
|
||||
end
|
||||
|
||||
-- This isn't passed up to the parent since we're handling the GUI.
|
||||
end)
|
||||
|
||||
runner.onSave(function(filename)
|
||||
table.insert(
|
||||
outContents,
|
||||
json.encode({
|
||||
type = 'onSave',
|
||||
speciesIndex = speciesIndex,
|
||||
})
|
||||
)
|
||||
|
||||
message("Will be saved once all currently active threads finish", 0x00990000)
|
||||
end)
|
||||
|
||||
runner.onLoad(function(filename)
|
||||
table.insert(
|
||||
outContents,
|
||||
json.encode({
|
||||
type = 'onLoad',
|
||||
speciesIndex = speciesIndex,
|
||||
})
|
||||
)
|
||||
|
||||
message("Will be loaded once all currently active threads finish", 0x00990000)
|
||||
end)
|
||||
|
||||
runner.run(
|
||||
runnerData[1],
|
||||
runnerData[2],
|
||||
speciesIndex,
|
||||
function(genome, index)
|
||||
table.insert(
|
||||
outContents,
|
||||
json.encode({
|
||||
type = 'onGenome',
|
||||
genome = genome,
|
||||
genomeIndex = index,
|
||||
speciesIndex = speciesIndex,
|
||||
})
|
||||
)
|
||||
end,
|
||||
function()
|
||||
table.insert(
|
||||
outContents,
|
||||
json.encode({
|
||||
type = 'onFinish',
|
||||
speciesIndex = speciesIndex,
|
||||
})
|
||||
)
|
||||
outFile:write(table.concat(outContents, "\n"))
|
||||
outFile:close()
|
||||
exec('quit-emulator')
|
||||
end
|
||||
)
|
94
runner-wrapper.lua
Normal file
94
runner-wrapper.lua
Normal file
|
@ -0,0 +1,94 @@
|
|||
local base = string.gsub(@@LUA_SCRIPT_FILENAME@@, "(.*[/\\])(.*)", "%1")
|
||||
|
||||
local json = dofile(base.."/dkjson.lua")
|
||||
local tmpFileName = "/tmp/donk_runner_"..tostring(math.floor(random.integer(0, 0xffffffffffffffff))):hex()
|
||||
|
||||
local function message(_M, msg, color)
|
||||
if color == nil then
|
||||
color = 0x00009900
|
||||
end
|
||||
|
||||
for i=#_M.onMessageHandler,1,-1 do
|
||||
_M.onMessageHandler[i](msg, color)
|
||||
end
|
||||
end
|
||||
|
||||
local function save(_M)
|
||||
for i=#_M.onSaveHandler,1,-1 do
|
||||
_M.onSaveHandler[i](_M.saveLoadFile)
|
||||
end
|
||||
end
|
||||
|
||||
local function onSave(_M, handler)
|
||||
table.insert(_M.onSaveHandler, handler)
|
||||
end
|
||||
|
||||
local function load(_M)
|
||||
for i=#_M.onLoadHandler,1,-1 do
|
||||
_M.onLoadHandler[i](_M.saveLoadFile)
|
||||
end
|
||||
end
|
||||
|
||||
local function onLoad(_M, handler)
|
||||
table.insert(_M.onLoadHandler, handler)
|
||||
end
|
||||
|
||||
return function()
|
||||
local _M = {
|
||||
onMessageHandler = {},
|
||||
}
|
||||
|
||||
_M.onRenderForm = function(handler)
|
||||
end
|
||||
|
||||
_M.onMessage = function(handler)
|
||||
onMessage(_M, handler)
|
||||
end
|
||||
|
||||
_M.run = function(species, generationIdx, speciesIdx, genomeCallback, finishCallback)
|
||||
local trunc = io.open(tmpFileName, 'w')
|
||||
trunc:close()
|
||||
|
||||
local poppets = {}
|
||||
for i=1,#species,1 do
|
||||
local poppet = io.popen("RUNNER_DATA='"..json.encode({species[i], generationIdx, speciesIdx + i - 1, tmpFileName}).."' lsnes --rom="..base.."/rom.sfc --unpause --lua="..base.."/runner-process.lua", 'r')
|
||||
table.insert(poppets, poppet)
|
||||
end
|
||||
|
||||
for i=1,#poppets,1 do
|
||||
local poppet = poppets[i]
|
||||
poppet:read('*a')
|
||||
poppet:close()
|
||||
end
|
||||
|
||||
local tmpFile = io.open(tmpFileName, "r")
|
||||
local line = ""
|
||||
repeat
|
||||
local obj, pos, err = json.decode(line)
|
||||
if err ~= nil then
|
||||
goto continue
|
||||
end
|
||||
|
||||
if obj.type == 'onMessage' then
|
||||
message(_M, obj.msg, obj.color)
|
||||
elseif obj.type == 'onGenome' then
|
||||
species[obj.speciesIndex - speciesIdx + 1].genomes[obj.genomeIndex] = obj.genome
|
||||
genomeCallback(obj.genome, obj.index)
|
||||
elseif obj.type == 'onFinish' then
|
||||
finishCallback()
|
||||
end
|
||||
|
||||
::continue::
|
||||
line = tmpFile:read()
|
||||
until(line == "")
|
||||
tmpFile:close()
|
||||
local ok, err = os.remove(tmpFileName)
|
||||
if err ~= nil then
|
||||
message(_M, err)
|
||||
elseif ok ~= true then
|
||||
message(_M, 'UNSPECIFIED ERROR ON REMOVAL')
|
||||
end
|
||||
end
|
||||
|
||||
return _M
|
||||
end
|
884
runner.lua
Normal file
884
runner.lua
Normal file
|
@ -0,0 +1,884 @@
|
|||
local base = string.gsub(@@LUA_SCRIPT_FILENAME@@, "(.*[/\\])(.*)", "%1")
|
||||
|
||||
local config = dofile(base.."/config.lua")
|
||||
local game = dofile(base.."/game.lua")
|
||||
local mathFunctions = dofile(base.."/mathFunctions.lua")
|
||||
|
||||
game.registerHandlers()
|
||||
|
||||
local Inputs = config.InputSize+1
|
||||
local Outputs = #config.ButtonNames
|
||||
|
||||
local guiWidth = 0
|
||||
local guiHeight = 0
|
||||
|
||||
local function message(_M, msg, color)
|
||||
if color == nil then
|
||||
color = 0x00009900
|
||||
end
|
||||
|
||||
for i=#_M.onMessageHandler,1,-1 do
|
||||
_M.onMessageHandler[i](msg, color)
|
||||
end
|
||||
end
|
||||
|
||||
local netPicture = nil
|
||||
local genomeCtx = gui.renderctx.new(470, 200)
|
||||
local function displayGenome(genome)
|
||||
genomeCtx:set()
|
||||
genomeCtx:clear()
|
||||
gui.solidrectangle(0, 0, 470, 200, 0x00606060)
|
||||
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 = 400
|
||||
cell.y = 20 + 14 * o
|
||||
cell.value = network.neurons[config.NeatConfig.MaxNodes + o].value
|
||||
cells[config.NeatConfig.MaxNodes+o] = cell
|
||||
local color
|
||||
if cell.value > 0 then
|
||||
color = 0x000000FF
|
||||
else
|
||||
color = 0x00000000
|
||||
end
|
||||
gui.text(403, 10+14*o, config.ButtonNames[o], color, 0xff000000)
|
||||
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.rectangle(
|
||||
50-config.BoxRadius*5-3,
|
||||
70-config.BoxRadius*5-3,
|
||||
config.BoxRadius*10+5,
|
||||
config.BoxRadius*10+5,
|
||||
2,
|
||||
0xFF000000,
|
||||
0x00808080
|
||||
)
|
||||
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 alpha = 0x50000000
|
||||
if cell.value == 0 then
|
||||
alpha = 0xFF000000
|
||||
end
|
||||
color = alpha + color*0x10000 + color*0x100 + color
|
||||
gui.rectangle(
|
||||
math.floor(cell.x-5),
|
||||
math.floor(cell.y-5),
|
||||
5,
|
||||
5,
|
||||
1,
|
||||
0x00,
|
||||
color
|
||||
)
|
||||
end
|
||||
end
|
||||
for _,gene in pairs(genome.genes) do
|
||||
if true then
|
||||
local c1 = cells[gene.into]
|
||||
local c2 = cells[gene.out]
|
||||
local alpha = 0x20000000
|
||||
if c1.value == 0 then
|
||||
alpha = 0xA0000000
|
||||
end
|
||||
|
||||
local color = 0x80-math.floor(math.abs(mathFunctions.sigmoid(gene.weight))*0x80)
|
||||
if gene.weight > 0 then
|
||||
color = alpha + 0x8000 + 0x10000*color
|
||||
else
|
||||
color = alpha + 0x800000 + 0x100*color
|
||||
end
|
||||
gui.line(
|
||||
math.floor(c1.x+1),
|
||||
math.floor(c1.y),
|
||||
math.floor(c2.x-3),
|
||||
math.floor(c2.y),
|
||||
color
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
gui.rectangle(
|
||||
49,
|
||||
71,
|
||||
2,
|
||||
7,
|
||||
0x00000000,
|
||||
0x00FF0000
|
||||
)
|
||||
|
||||
local pos = 100
|
||||
for mutation,rate in pairs(genome.mutationRates) do
|
||||
gui.text(100, pos, mutation .. ": " .. rate, 0x00000000, 0xff000000)
|
||||
|
||||
pos = pos + 14
|
||||
end
|
||||
netPicture = genomeCtx:render()
|
||||
gui.renderctx.setnull()
|
||||
end
|
||||
|
||||
local function advanceFrame(_M, after)
|
||||
table.insert(_M.onFrameAdvancedHandler, after)
|
||||
end
|
||||
|
||||
local function processFrameAdvanced(_M)
|
||||
for i=#_M.onFrameAdvancedHandler,1,-1 do
|
||||
table.remove(_M.onFrameAdvancedHandler, i)()
|
||||
end
|
||||
end
|
||||
|
||||
local buttons = nil
|
||||
local buttonCtx = gui.renderctx.new(500, 70)
|
||||
function displayButtons()
|
||||
buttonCtx:set()
|
||||
buttonCtx:clear()
|
||||
|
||||
gui.rectangle(0, 0, 500, 70, 1, 0x000000000, 0x00990099)
|
||||
local startStop = ""
|
||||
-- FIXME this won't work I think???
|
||||
if config.Running then
|
||||
startStop = "Stop"
|
||||
else
|
||||
startStop = "Start"
|
||||
end
|
||||
gui.text(5, 2, "[1] "..startStop)
|
||||
|
||||
gui.text(130, 2, "[4] Play Top")
|
||||
|
||||
gui.text(240, 2, "[6] Save")
|
||||
|
||||
|
||||
gui.text(320, 2, "[8] Load")
|
||||
gui.text(400, 2, "[9] Restart")
|
||||
|
||||
local insert = ""
|
||||
local confirm = "[Tab] Type in filename"
|
||||
if inputmode then
|
||||
insert = "_"
|
||||
confirm = "[Tab] Confirm filename"
|
||||
end
|
||||
|
||||
gui.text(5, 29, "..."..config.NeatConfig.SaveFile:sub(-55)..insert)
|
||||
|
||||
gui.text(5, 50, confirm)
|
||||
|
||||
buttons = buttonCtx:render()
|
||||
gui.renderctx.setnull()
|
||||
end
|
||||
|
||||
local frame = 0
|
||||
local formCtx = nil
|
||||
local form = nil
|
||||
function displayForm(_M)
|
||||
if #_M.onRenderFormHandler == 0 then
|
||||
return
|
||||
end
|
||||
|
||||
if form ~= nil and frame % 10 ~= 0 then
|
||||
for i=#_M.onRenderFormHandler,1,-1 do
|
||||
_M.onRenderFormHandler[i](form)
|
||||
end
|
||||
return
|
||||
end
|
||||
|
||||
formCtx:set()
|
||||
formCtx:clear()
|
||||
gui.rectangle(0, 0, 500, guiHeight, 1, 0x00ffffff, 0x00000000)
|
||||
--gui.circle(game.screenX-84, game.screenY-84, 192 / 2, 1, 0x50000000)
|
||||
|
||||
gui.text(5, 30, "Timeout: " .. _M.timeout)
|
||||
gui.text(5, 5, "Generation: " .. _M.currentGenerationIndex)
|
||||
gui.text(130, 5, "Species: " .. _M.currentSpeciesIndex)
|
||||
gui.text(230, 5, "Genome: " .. _M.currentGenomeIndex)
|
||||
gui.text(130, 30, "Max: " .. math.floor(_M.maxFitness))
|
||||
--gui.text(330, 5, "Measured: " .. math.floor(measured/total*100) .. "%")
|
||||
gui.text(5, 65, "Bananas: " .. (game.getBananas() - _M.startBananas))
|
||||
gui.text(5, 80, "KONG: " .. (game.getKong() - _M.startKong))
|
||||
gui.text(5, 95, "Krem: " .. (game.getKremCoins() - _M.startKrem))
|
||||
gui.text(130, 65, "Coins: " .. (game.getCoins() - _M.startCoins))
|
||||
gui.text(130, 80, "Lives: " .. game.getLives())
|
||||
gui.text(130, 95, "Bumps: " .. _M.bumps)
|
||||
gui.text(230, 65, "Damage: " .. _M.partyHitCounter)
|
||||
gui.text(230, 80, "PowerUp: " .. _M.powerUpCounter)
|
||||
gui.text(320, 65, string.format("Current Area: %04x", _M.currentArea))
|
||||
gui.text(320, 80, "Rightmost: ".._M.rightmost[_M.currentArea])
|
||||
|
||||
displayButtons()
|
||||
formCtx:set()
|
||||
buttons:draw(5, 130)
|
||||
|
||||
if netPicture ~= nil then
|
||||
netPicture:draw(5, 200)
|
||||
end
|
||||
|
||||
form = formCtx:render()
|
||||
gui.renderctx.setnull()
|
||||
|
||||
for i=#_M.onRenderFormHandler,1,-1 do
|
||||
_M.onRenderFormHandler[i](form)
|
||||
end
|
||||
end
|
||||
|
||||
local function painting(_M)
|
||||
guiWidth, guiHeight = gui.resolution()
|
||||
if formCtx == nil then
|
||||
formCtx = gui.renderctx.new(500, guiHeight)
|
||||
end
|
||||
frame = frame + 1
|
||||
displayForm(_M)
|
||||
end
|
||||
|
||||
local function evaluateNetwork(network, inputs, inputDeltas)
|
||||
table.insert(inputs, 1)
|
||||
table.insert(inputDeltas,99)
|
||||
if #inputs ~= Inputs then
|
||||
print("Incorrect number of neural network inputs.")
|
||||
return {}
|
||||
end
|
||||
|
||||
|
||||
for i=1,Inputs do
|
||||
network.neurons[i].value = inputs[i] * inputDeltas[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 = o - 1
|
||||
if network.neurons[config.NeatConfig.MaxNodes+o].value > 0 then
|
||||
outputs[button] = true
|
||||
else
|
||||
outputs[button] = false
|
||||
end
|
||||
end
|
||||
|
||||
return outputs
|
||||
end
|
||||
|
||||
local function evaluateCurrent(_M)
|
||||
local genome = _M.currentSpecies.genomes[_M.currentGenomeIndex]
|
||||
|
||||
local inputDeltas = {}
|
||||
inputs, inputDeltas = game.getInputs()
|
||||
|
||||
controller = evaluateNetwork(genome.network, inputs, inputDeltas)
|
||||
|
||||
if controller[6] and controller[7] then
|
||||
controller[6] = false
|
||||
controller[7] = false
|
||||
end
|
||||
if controller[4] and controller[5] then
|
||||
controller[4] = false
|
||||
controller[5] = false
|
||||
end
|
||||
|
||||
for b=0,#config.ButtonNames - 1,1 do
|
||||
if controller[b] then
|
||||
input.set(0, b, 1)
|
||||
else
|
||||
input.set(0, b, 0)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local function fitnessAlreadyMeasured(_M)
|
||||
local genome = _M.currentSpecies.genomes[_M.currentGenomeIndex]
|
||||
|
||||
return genome.fitness ~= 0
|
||||
end
|
||||
|
||||
local rew = movie.to_rewind(config.NeatConfig.Filename)
|
||||
|
||||
local function initializeRun(_M, after)
|
||||
message(_M, string.format("Total Genomes: %d", #_M.currentSpecies.genomes))
|
||||
|
||||
settings.set_speed("turbo")
|
||||
-- XXX Does this actually work or only affects new VM loads?
|
||||
settings.set('lua-maxmem', 1024)
|
||||
exec('enable-sound off')
|
||||
gui.subframe_update(false)
|
||||
table.insert(_M.runInitialized, after)
|
||||
movie.unsafe_rewind(rew)
|
||||
end
|
||||
|
||||
local function mainLoop(_M, genome)
|
||||
advanceFrame(_M, function()
|
||||
if genome ~= nil then
|
||||
_M.currentFrame = _M.currentFrame + 1
|
||||
end
|
||||
|
||||
genome = _M.currentSpecies.genomes[_M.currentGenomeIndex]
|
||||
|
||||
if frame % 10 == 0 then
|
||||
if not pcall(function()
|
||||
displayGenome(genome)
|
||||
end) then
|
||||
message(_M, "Could not render genome graph")
|
||||
end
|
||||
end
|
||||
|
||||
if _M.currentFrame%5 == 0 then
|
||||
evaluateCurrent(_M)
|
||||
end
|
||||
|
||||
for b=0,#config.ButtonNames - 1,1 do
|
||||
if controller[b] then
|
||||
input.set(0, b, 1)
|
||||
else
|
||||
input.set(0, b, 0)
|
||||
end
|
||||
end
|
||||
|
||||
game.getPositions()
|
||||
local timeoutConst = 0
|
||||
if game.vertical then
|
||||
timeoutConst = config.NeatConfig.TimeoutConstant * 10
|
||||
else
|
||||
timeoutConst = config.NeatConfig.TimeoutConstant
|
||||
end
|
||||
|
||||
-- Don't punish being launched by barrels
|
||||
-- FIXME Will this skew mine cart levels?
|
||||
if game.getVelocityY() < -2104 then
|
||||
message(_M, "BARREL! "..frame, 0x00ffff00)
|
||||
if _M.timeout < timeoutConst + 60 * 12 then
|
||||
_M.timeout = _M.timeout + 60 * 12
|
||||
end
|
||||
end
|
||||
|
||||
local nextArea = game.getCurrentArea()
|
||||
if nextArea ~= _M.lastArea then
|
||||
_M.lastArea = nextArea
|
||||
game.onceAreaLoaded(function()
|
||||
message(_M, "Loady")
|
||||
_M.timeout = _M.timeout + 60 * 5
|
||||
_M.currentArea = nextArea
|
||||
_M.lastArea = _M.currentArea
|
||||
if _M.rightmost[_M.currentArea] == nil then
|
||||
_M.rightmost[_M.currentArea] = 0
|
||||
_M.upmost[_M.currentArea] = 0
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
if not game.vertical then
|
||||
if game.partyX > _M.rightmost[_M.currentArea] then
|
||||
_M.rightmost[_M.currentArea] = game.partyX
|
||||
if _M.timeout < timeoutConst then
|
||||
_M.timeout = timeoutConst
|
||||
end
|
||||
end
|
||||
else
|
||||
if game.partyY > _M.upmost[_M.currentArea] then
|
||||
_M.upmost[_M.currentArea] = game.partyY
|
||||
if _M.timeout < timeoutConst then
|
||||
_M.timeout = timeoutConst
|
||||
end
|
||||
end
|
||||
end
|
||||
-- FIXME Measure distance to target / area exit
|
||||
-- We might not always be horizontal
|
||||
|
||||
local hitTimer = game.getHitTimer(_M.lastBoth)
|
||||
|
||||
if hitTimer > 0 then
|
||||
_M.partyHitCounter = _M.partyHitCounter + 1
|
||||
--message(_M, "party took damage, hit counter: " .. _M.partyHitCounter)
|
||||
end
|
||||
|
||||
local powerUp = game.getBoth()
|
||||
_M.lastBoth = powerUp
|
||||
if powerUp > 0 then
|
||||
if powerUp ~= _M.powerUpBefore then
|
||||
_M.powerUpCounter = _M.powerUpCounter + 1
|
||||
_M.powerUpBefore = powerUp
|
||||
end
|
||||
end
|
||||
|
||||
local krem = game.getKremCoins() - _M.startKrem
|
||||
if krem > _M.lastKrem then
|
||||
message(_M, string.format("Kremcoin grabbed: %d", _M.timeout), 0x00009900)
|
||||
_M.lastKrem = krem
|
||||
_M.timeout = _M.timeout + 60 * 10
|
||||
end
|
||||
|
||||
_M.timeout = _M.timeout - 1
|
||||
|
||||
-- Continue if we haven't timed out
|
||||
local timeoutBonus = _M.currentFrame / 4
|
||||
if _M.timeout + timeoutBonus > 0 then
|
||||
mainLoop(_M, genome)
|
||||
return
|
||||
end
|
||||
|
||||
-- Timeout calculations beyond this point
|
||||
-- Manipulating the timeout value won't have
|
||||
-- any effect
|
||||
local bananas = game.getBananas() - _M.startBananas
|
||||
local coins = game.getCoins() - _M.startCoins
|
||||
local kong = game.getKong()
|
||||
|
||||
message(_M, string.format("Bananas: %d, coins: %d, Krem: %d, KONG: %d", bananas, coins, krem, kong))
|
||||
|
||||
local bananaCoinsFitness = (krem * 100) + (kong * 60) + (bananas * 50) + (coins * 0.2)
|
||||
if (bananas + coins) > 0 then
|
||||
message(_M, "Bananas, Coins, KONG added " .. bananaCoinsFitness .. " fitness")
|
||||
end
|
||||
|
||||
local hitPenalty = _M.partyHitCounter * 100
|
||||
local bumpPenalty = _M.bumps * 100
|
||||
local powerUpBonus = _M.powerUpCounter * 100
|
||||
|
||||
local most = 0
|
||||
if not game.vertical then
|
||||
for k,v in pairs(_M.rightmost) do
|
||||
most = most + v
|
||||
end
|
||||
most = most - _M.currentFrame / 2
|
||||
else
|
||||
for k,v in pairs(_M.upmost) do
|
||||
most = most + v
|
||||
end
|
||||
most = most - _M.currentFrame / 2
|
||||
end
|
||||
|
||||
local fitness = bananaCoinsFitness - bumpPenalty - hitPenalty + powerUpBonus + most + game.getJumpHeight() / 100
|
||||
|
||||
local lives = game.getLives()
|
||||
|
||||
if _M.startLives < lives then
|
||||
local extraLiveBonus = (lives - _M.startLives)*1000
|
||||
fitness = fitness + extraLiveBonus
|
||||
message(_M, "Extra live bonus added " .. extraLiveBonus)
|
||||
end
|
||||
|
||||
if game.getGoalHit() then
|
||||
fitness = fitness + 1000
|
||||
message(_M, string.format("LEVEL WON! Fitness: %d", fitness), 0x0000ff00)
|
||||
end
|
||||
if fitness == 0 then
|
||||
fitness = -1
|
||||
end
|
||||
genome.fitness = fitness
|
||||
|
||||
if fitness > _M.maxFitness then
|
||||
_M.maxFitness = fitness
|
||||
end
|
||||
|
||||
if _M.genomeCallback ~= nil then
|
||||
_M.genomeCallback(genome, _M.currentGenomeIndex)
|
||||
end
|
||||
|
||||
message(_M, string.format("Gen %d species %d genome %d fitness: %d", _M.currentGenerationIndex, _M.currentSpeciesIndex, _M.currentGenomeIndex, fitness))
|
||||
_M.currentGenomeIndex = 1
|
||||
while fitnessAlreadyMeasured(_M) do
|
||||
_M.currentGenomeIndex = _M.currentGenomeIndex + 1
|
||||
if _M.currentGenomeIndex > #_M.currentSpecies.genomes then
|
||||
for i=#_M.dereg,1,-1 do
|
||||
local d = table.remove(_M.dereg, i)
|
||||
callback.unregister(d[1], d[2])
|
||||
end
|
||||
|
||||
input.keyhook("1", false)
|
||||
input.keyhook("4", false)
|
||||
input.keyhook("6", false)
|
||||
input.keyhook("8", false)
|
||||
input.keyhook("9", false)
|
||||
input.keyhook("tab", false)
|
||||
|
||||
_M.finishCallback()
|
||||
return
|
||||
end
|
||||
end
|
||||
initializeRun(_M, function()
|
||||
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
|
||||
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
|
||||
|
||||
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 }
|
||||
end
|
||||
|
||||
local function save(_M)
|
||||
for i=#_M.onSaveHandler,1,-1 do
|
||||
_M.onSaveHandler[i](_M.saveLoadFile)
|
||||
end
|
||||
end
|
||||
|
||||
local function onSave(_M, handler)
|
||||
table.insert(_M.onSaveHandler, handler)
|
||||
end
|
||||
|
||||
local function load(_M)
|
||||
for i=#_M.onLoadHandler,1,-1 do
|
||||
_M.onLoadHandler[i](_M.saveLoadFile)
|
||||
end
|
||||
end
|
||||
|
||||
local function onLoad(_M, handler)
|
||||
table.insert(_M.onLoadHandler, handler)
|
||||
end
|
||||
|
||||
local function keyhook (_M, key, state)
|
||||
if state.value == 1 then
|
||||
if key == "tab" then
|
||||
_M.inputmode = not _M.inputmode
|
||||
_M.helddown = key
|
||||
elseif inputmode then
|
||||
return
|
||||
elseif key == "1" then
|
||||
_M.helddown = key
|
||||
config.Running = not config.Running
|
||||
elseif key == "4" then
|
||||
_M.helddown = key
|
||||
pool.requestTop()
|
||||
elseif key == "6" then
|
||||
_M.helddown = key
|
||||
save(_M)
|
||||
elseif key == "8" then
|
||||
_M.helddown = key
|
||||
load(_M)
|
||||
elseif key == "9" then
|
||||
_M.helddown = key
|
||||
pool.run(true)
|
||||
end
|
||||
elseif state.value == 0 then
|
||||
helddown = nil
|
||||
end
|
||||
end
|
||||
|
||||
local function saveLoadInput(_M)
|
||||
local inputs = input.raw()
|
||||
if not inputmode then
|
||||
-- FIXME
|
||||
_M.saveLoadFile = config.NeatConfig.SaveFile
|
||||
return
|
||||
end
|
||||
if helddown == nil then
|
||||
local mapping = {
|
||||
backslash = "\\",
|
||||
colon = ":",
|
||||
comma = ",",
|
||||
exclaim = "!",
|
||||
dollar = "$",
|
||||
hash = "#",
|
||||
caret = "^",
|
||||
ampersand = "&",
|
||||
asterisk = "*",
|
||||
leftparen = "(",
|
||||
rightparen = ")",
|
||||
less = "<",
|
||||
greater = ">",
|
||||
quote = "'",
|
||||
quotedbl = "\"",
|
||||
semicolon = ";",
|
||||
slash = "/",
|
||||
question = "?",
|
||||
leftcurly = "{",
|
||||
leftbracket = "[",
|
||||
rightcurly = "}",
|
||||
rightbracket = "]",
|
||||
pipe = "|",
|
||||
tilde = "~",
|
||||
underscore = "_",
|
||||
at = "@",
|
||||
period = ".",
|
||||
equals = "=",
|
||||
plus = "+",
|
||||
}
|
||||
for k,v in pairs(inputs) do
|
||||
if v["type"] ~= "key" or v["value"] ~= 1 then
|
||||
goto continue
|
||||
end
|
||||
if k == "back" then
|
||||
config.NeatConfig.SaveFile = config.NeatConfig.SaveFile:sub(1, #config.NeatConfig.SaveFile-1)
|
||||
helddown = k
|
||||
goto continue
|
||||
end
|
||||
local m = k
|
||||
if mapping[k] ~= nil then
|
||||
m = mapping[k]
|
||||
end
|
||||
if #m ~= 1 then
|
||||
goto continue
|
||||
end
|
||||
config.NeatConfig.SaveFile = config.NeatConfig.SaveFile..m
|
||||
helddown = k
|
||||
::continue::
|
||||
end
|
||||
elseif helddown ~= nil and inputs[helddown]["value"] ~= 1 then
|
||||
helddown = nil
|
||||
end
|
||||
end
|
||||
|
||||
local function run(_M, species, generationIdx, speciesIdx, genomeCallback, finishCallback)
|
||||
_M.currentGenerationIndex = generationIdx
|
||||
_M.currentSpeciesIndex = speciesIdx
|
||||
_M.currentSpecies = species
|
||||
_M.currentGenomeIndex = 1
|
||||
_M.genomeCallback = genomeCallback
|
||||
_M.finishCallback = finishCallback
|
||||
register(_M, 'paint', function()
|
||||
painting(_M)
|
||||
end)
|
||||
register(_M, 'input', function()
|
||||
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)
|
||||
input.keyhook("6", true)
|
||||
input.keyhook("8", true)
|
||||
input.keyhook("9", true)
|
||||
input.keyhook("tab", true)
|
||||
|
||||
initializeRun(_M, function()
|
||||
mainLoop(_M)
|
||||
end)
|
||||
end
|
||||
|
||||
local function onMessage(_M, handler)
|
||||
table.insert(_M.onMessageHandler, handler)
|
||||
end
|
||||
|
||||
local function onRenderForm(_M, handler)
|
||||
table.insert(_M.onRenderFormHandler, handler)
|
||||
end
|
||||
|
||||
return function()
|
||||
local _M = {
|
||||
currentGenerationIndex = 1,
|
||||
currentSpeciesIndex = 1,
|
||||
currentSpecies = nil,
|
||||
finishCallback = nil,
|
||||
genomeCallback = nil,
|
||||
currentGenomeIndex = 1,
|
||||
currentFrame = 0,
|
||||
maxFitness = 0,
|
||||
|
||||
dereg = {},
|
||||
inputmode = false,
|
||||
helddown = nil,
|
||||
saveLoadFile = config.NeatConfig.SaveLoadFile,
|
||||
|
||||
timeout = 0,
|
||||
bumps = 0,
|
||||
startKong = 0,
|
||||
startBananas = 0,
|
||||
startKrem = 0,
|
||||
lastKrem = 0,
|
||||
startCoins = 0,
|
||||
startLives = 0,
|
||||
partyHitCounter = 0,
|
||||
powerUpCounter = 0,
|
||||
powerUpBefore = 0,
|
||||
currentArea = 0,
|
||||
lastArea = 0,
|
||||
rightmost = {},
|
||||
upmost = {},
|
||||
lastBoth = 0,
|
||||
|
||||
runInitialized = {},
|
||||
|
||||
onMessageHandler = {},
|
||||
onSaveHandler = {},
|
||||
onLoadHandler = {},
|
||||
onRenderFormHandler = {},
|
||||
onFrameAdvancedHandler = {},
|
||||
|
||||
}
|
||||
|
||||
_M.onRenderForm = function(handler)
|
||||
onRenderForm(_M, handler)
|
||||
end
|
||||
|
||||
_M.onMessage = function(handler)
|
||||
onMessage(_M, handler)
|
||||
end
|
||||
|
||||
_M.onSave = function(handler)
|
||||
onSave(_M, handler)
|
||||
end
|
||||
|
||||
_M.onLoad = function(handler)
|
||||
onLoad(_M, handler)
|
||||
end
|
||||
|
||||
_M.run = function(species, generationIdx, speciesIdx, genomeCallback, finishCallback)
|
||||
run(_M, species, generationIdx, speciesIdx, genomeCallback, finishCallback)
|
||||
end
|
||||
|
||||
return _M
|
||||
end
|
3
xpra-run.sh
Executable file
3
xpra-run.sh
Executable file
|
@ -0,0 +1,3 @@
|
|||
#! /bin/bash
|
||||
SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )"
|
||||
xpra start --bind-tcp=127.0.0.1:5309 --html=on --start-child="lsnes --lua=$SCRIPT_DIR/neat-donk.lua" --exit-with-child=yes --start-new-commands=no
|
Loading…
Add table
Reference in a new issue