local pop
local pop_update
-local enemy_spawn = { 100, 100 }
-local enemy
+local update_speed = 30
+
+local fitness_font
+
+local stored_fitnesses = {}
+
+local enemies = {}
function love.load()
world, player = World.new()
- enemy = Enemy.new(unpack(enemy_spawn))
+ local enemy = Enemy.new(0, 0)
+ table.insert(enemies, enemy)
world:add_entity(enemy)
local wall1 = Wall.new(-20, -20, 840, 20)
input = Input:new()
pop = Population.new()
- pop:create_genomes(100, 16, 8)
+ pop:create_genomes(96, 16, 8)
pop_update = pop:evolve()
love.graphics.setBackgroundColor(CONF.BACK_COLOR)
+ fitness_font = love.graphics.newFont(24)
end
function love.keypressed(key)
input:keyup(key)
end
+local function get_random_pos()
+ local x = math.random(100) + math.random(100) + 600 * (math.random(2) - 1)
+ local y = math.random(100) + math.random(100) + 500 * (math.random(2) - 1)
+ return x, y
+end
+
local function network_input(ins, dt)
player.alive = true
- for _, v in ipairs(ins) do
- --print(v)
- end
- if ins[1] > 0.5 then input:keydown("w") else input:keyup("w") end
- if ins[2] > 0.5 then input:keydown("s") else input:keyup("s") end
- if ins[3] > 0.5 then input:keydown("a") else input:keyup("a") end
- if ins[4] > 0.5 then input:keydown("d") else input:keyup("d") end
- if ins[5] > 0.5 then input:keydown("left") else input:keyup("left") end
- if ins[6] > 0.5 then input:keydown("right") else input:keyup("right") end
- if ins[7] > 0.5 then input:keydown("up") else input:keyup("up") end
- if ins[8] > 0.5 then input:keydown("down") else input:keyup("down") end
-
+ if ins[1] > 0.35 then input:keydown("w") else input:keyup("w") end
+ if ins[2] > 0.35 then input:keydown("s") else input:keyup("s") end
+ if ins[3] > 0.35 then input:keydown("a") else input:keyup("a") end
+ if ins[4] > 0.35 then input:keydown("d") else input:keyup("d") end
+ if ins[5] > 0.35 then input:keydown("left") else input:keyup("left") end
+ if ins[6] > 0.35 then input:keydown("right") else input:keyup("right") end
+ if ins[7] > 0.35 then input:keydown("up") else input:keyup("up") end
+ if ins[8] > 0.35 then input:keydown("down") else input:keyup("down") end
+
+ local last_x = player.x
+ local last_y = player.y
world:update(dt, input)
- local fitness = 0.5
+ local fitness = math.sqrt(math.sqrDist(last_x, last_y, player.x, player.y))
+ fitness = fitness - (player.shot and 1 or 0)
+
+ local enemies_alive = 0
+ for _, v in ipairs(enemies) do
+ if v.alive then
+ enemies_alive = enemies_alive + 1
+ else
+ if not v.__tagged then
+ v.__tagged = true
+ fitness = fitness + 400
+ end
+ end
+ end
- if not player.alive or not enemy.alive then
- world:remove_entity(enemy)
- enemy = Enemy.new(math.random(800), math.random(600))
- world:add_entity(enemy)
+ if not player.alive or enemies_alive == 0 then
+ for _, v in ipairs(enemies) do
+ world:remove_entity(v)
+ end
- player.x = 400
- player.y = 300
+ enemies = {}
- if not enemy.alive then
- fitness = fitness + 1000
+ for _ = 1, math.ceil((pop.generation + 1) / 10) do
+ local enemy = Enemy.new(get_random_pos())
+ world:add_entity(enemy)
+ table.insert(enemies, enemy)
+ end
+
+ if player.alive then
+ fitness = fitness + 2000
+ else
+ player.x = 400
+ player.y = 300
end
end
return fitness, player.alive
end
+local function generation_step(avg_fitness, _, _)
+ table.insert(stored_fitnesses, avg_fitness)
+end
+
function love.update(dt)
if love.keyboard.isDown "escape" then
love.event.quit()
end
- for _ = 1, 30 do
+ if love.keyboard.isDown "z" then
+ update_speed = update_speed - 1
+ if update_speed < 1 then
+ update_speed = 1
+ end
+ end
+
+ if love.keyboard.isDown "x" then
+ update_speed = update_speed + 1
+ if update_speed > 60 then
+ update_speed = 60
+ end
+ end
+
+ for _ = 1, update_speed do
local dists = player:get_distances(world)
- pop_update = pop_update(dists, network_input, dt)
+
+ local inputs = {}
+ for i = 1, 16 do
+ local v1 = dists[i * 2]
+ local v2 = dists[(i * 2 + 1) % 32]
+ local v3 = dists[(i * 2 - 1) % 32]
+
+ inputs[i] = 1 - ((0.5 * v1 + 0.25 * v2 + 0.25 * v3) / (CONF.ENEMY_SIZE * CONF.PLAYER_VISION_DISTANCE))
+ end
+
+ pop_update = pop_update(inputs, network_input, generation_step, dt)
end
end
+local function plot_fitness(x, y, scale)
+ love.graphics.push()
+ love.graphics.translate(x, y)
+ love.graphics.scale(scale, scale)
+
+ love.graphics.setColor(0, 0, 0, 0.4)
+ love.graphics.rectangle("fill", -20, -20, 440, 240)
+
+ love.graphics.setFont(fitness_font)
+ love.graphics.setColor(1, 1, 1)
+
+ love.graphics.printf("Average fitness: " .. math.floor(pop.avg_fitness), 0, 0, 400, "left")
+ love.graphics.printf("Highest fitness: " .. math.floor(pop.high_fitness), 0, 20, 400, "left")
+
+ local highest = 0
+ for _, v in ipairs(stored_fitnesses) do
+ if v > highest then
+ highest = v
+ end
+ end
+
+ local width = 400 / (#stored_fitnesses)
+
+ love.graphics.setColor(0, 0, 1)
+ for i, v in ipairs(stored_fitnesses) do
+ if v < 0 then
+ v = 0
+ end
+ love.graphics.circle("fill", (i - 1) * width, 200 - v * 100 / highest, 8)
+ end
+
+ love.graphics.pop()
+end
+
+local function draw_network(net, x, y, scale)
+ if net == nil then return end
+
+ love.graphics.push()
+ love.graphics.translate(x, y)
+ love.graphics.scale(scale, scale)
+
+ love.graphics.setColor(0, 0, 0, 0.4)
+ love.graphics.rectangle("fill", -20, -20, 680, 600)
+
+ love.graphics.setColor(1, 1, 1)
+
+ for _, v in pairs(net.neurons) do
+ love.graphics.rectangle("fill", v.x, v.y, 24, 24)
+ end
+
+ for _, neuron in pairs(net.neurons) do
+ local ins = neuron.inputs
+
+ local x1 = neuron.x + 12
+ local y1 = neuron.y + 12
+ for _, conn in pairs(ins) do
+ local other = net.neurons[conn.from]
+ local x2 = other.x + 12
+ local y2 = other.y + 12
+
+ local col = { 1, 0, 0 }
+ if conn.weight > 0 then
+ col = { 0, 0, 1 }
+ end
+
+ love.graphics.setColor(col)
+ love.graphics.setLineWidth(math.sigmoid(conn.weight) * 2)
+ love.graphics.line(x1, y1, x2, y2)
+ end
+ end
+
+ love.graphics.setLineWidth(2)
+ love.graphics.pop()
+end
+
function love.draw()
world:draw()
love.graphics.setColor(0, 0, 0)
- --love.graphics.printf(tostring(love.timer.getFPS()) .. " FPS", 0, 0, 800, "center")
+ love.graphics.printf(tostring(love.timer.getFPS()) .. " FPS", 0, 0, 800, "left")
+ love.graphics.printf("Generation: " .. pop.generation, 0, 32, 800, "left")
+ love.graphics.printf("Genome: " .. pop.current_genome, 0, 64, 800, "left")
if pop.genomes[pop.current_genome] ~= nil then
- love.graphics.printf(pop.generation .. " Generation | " .. pop.current_genome .. " Genome | " .. (pop.genomes[pop.current_genome].fitness) .. " Fitness", 0, 0, 800, "center")
+ love.graphics.printf("Fitness: " .. math.floor(pop.genomes[pop.current_genome].fitness), 0, 96, 800, "left")
+
+ draw_network(pop.genomes[pop.current_genome].network, 580, 0, 1 / 3)
end
+
+ plot_fitness(250, 0, 3 / 4)
end
local NN = require "src.neuralnet"
+local conf = require "conf"
local NeuralNetwork = NN.NeuralNetwork
-- Globals
local Starting_Weights_Chance = 0.25
local Starting_Connection_Chance = 2.0
-local Starting_Bias_Chance = 0.4
+local Starting_Bias_Chance = 0.2
local Starting_Split_Chance = 0.5
local Starting_Enable_Chance = 0.2
local Starting_Disable_Chance = 0.4
local Reset_Weight_Chance = 0.9
local Crossover_Chance = 0.75
+local MAX_NEURONS = conf.MAX_NEURONS
+
-- Need a global-ish innovation number, since that depends on the whole training, not just a single genome
local Current_Innovation = 1
-- Genome class --
+
local Genome = {}
local Genome_mt = { __index = Genome }
genes = {};
fitness = 0;
network = {}; -- Neural Network
- high_neuron = inputs + outputs + 1; -- Highest numbered neuron in the genome
+ high_neuron = inputs + 1; -- Highest numbered neuron in the genome
mutations = { -- The different chances of mutating a particular part of the genome
["weights"] = Starting_Weights_Chance; -- Chance of changing the weights
end
function Genome:add_gene(from, to, weight)
+ if from > to then return end
+
local gene = Gene.new()
gene.weight = weight
gene.from = from
for i = 1, #self.genes do
local gene = self.genes[i]
- print("----------------------")
- print(gene.innovation, gene.from, gene.to, gene.weight)
if gene.enabled then
if not net:has_neuron(gene.to) then
for i = 1, #self.genes do
local gene = self.genes[i]
- if (gene.to == to and gene.from == from)
- or (gene.to == from and gene.from == to) then
+ if gene.to == to and gene.from == from then
return true
end
end
return
end
+ -- Must go to a bigger neuron
+ if neuron2 < neuron1 then
+ local tmp = neuron1
+ neuron1 = neuron2
+ neuron2 = tmp
+ end
+
-- Output cant be input
- if (neuron1 > self.num_inputs and neuron1 <= self.num_inputs + self.num_outputs) then
+ if neuron1 >= MAX_NEURONS - self.num_outputs then
return
end
end
local weight = math.random() * 4 - 2
- assert(neuron1 ~= neuron2, "IN MUTATE CONNECTIONS")
self:add_gene(neuron1, neuron2, weight)
end
-- Disable the gene beacause we are about to add other to replace it
gene.enabled = false
+ local n1 = gene.from
+ local n2 = self.high_neuron
+ local n3 = gene.to
+
+ local tmp
+ if n1 > n2 then
+ tmp = n1
+ n1 = n2
+ n2 = tmp
+ end
+
+ if n2 > n3 then
+ tmp = n3
+ n3 = n2
+ n2 = tmp
+ end
+
local gene1 = gene:copy()
- gene1.from = self.high_neuron
+ gene1.from = n2
+ gene1.to = n3
gene1.weight = 1.0
gene1.innovation = Get_Next_Innovation()
gene1.enabled = true
- assert(gene1.from ~= gene1.to, "IN MUTATE NEURON")
-
table.insert(self.genes, gene1)
local gene2 = gene:copy()
- gene2.to = self.high_neuron
+ gene2.from = n1
+ gene2.to = n2
gene2.innovation = Get_Next_Innovation()
gene2.enabled = true
- assert(gene2.from ~= gene2.to, "IN MUTATE NEURON")
-
table.insert(self.genes, gene2)
end
prob = prob - 1
end
- prob = self.mutations["split"]
+ prob = self.mutations["enable"]
while prob > 0 do
if math.random() < prob then
- self:mutate_neuron()
+ self:mutate_enabled(true)
end
prob = prob - 1
end
- prob = self.mutations["enable"]
+ prob = self.mutations["disable"]
while prob > 0 do
if math.random() < prob then
- self:mutate_enabled(true)
+ self:mutate_enabled(false)
end
prob = prob - 1
end
- prob = self.mutations["disable"]
+ prob = self.mutations["split"]
while prob > 0 do
if math.random() < prob then
- self:mutate_enabled(false)
+ self:mutate_neuron()
end
prob = prob - 1
end
for o = 1, self.num_outputs do
- neurons[o + self.num_inputs] = true
+ neurons[MAX_NEURONS - o] = true
end
for i = 1, #genes do
generation = 0;
current_genome = 0;
high_fitness = 0;
+ total_fitness = 0;
avg_fitness = 0;
}
return a.fitness > b.fitness
end)
- local count = math.floor(3 * #self.genomes / 4)
+ local count = math.floor(2 * #self.genomes / 3)
for _ = 1, count do
table.remove(self.genomes) -- This removes the last (worst) genome
end
end
function Population:mate()
- local count = #self.genomes * 3
+ local count = #self.genomes * 2
-- Double the population size
for _ = 1, count do
-- First we need to calculate the fitnesses of every genome
self.current_genome = 0
- function evolve_test(inputs, output_func, ...)
+ function evolve_test(inputs, output_func, _, ...)
if self.current_genome == 0 then
self.current_genome = 1
self.genomes[self.current_genome]:create_network()
if cont then
return evolve_test
else
+ if genome.fitness > self.high_fitness then
+ self.high_fitness = genome.fitness
+ end
+
+ self.total_fitness = self.total_fitness + genome.fitness
+ self.avg_fitness = self.total_fitness / self.current_genome
+
self.current_genome = self.current_genome + 1
+
if self.current_genome <= #self.genomes then
self.genomes[self.current_genome]:create_network()
return evolve_test
-- Then we need to kill off the worst of them
-- Then we breed more
-- Rinse and repeat!
- function finish_evolve()
+ function finish_evolve(_, _, generation_step, ...)
+ generation_step(self.avg_fitness, self.high_fitness, ...)
+
self:kill_worst()
self:mate()
self.current_genome = 0
+ self.high_fitness = 0
+ self.avg_fitness = 0
+ self.total_fitness = 0
return evolve_test
end