}
return {
+ -- GENERAL PROPERTIES
+ LOAD_FILE = "";
+ SAVE_FILE = "./saved/POPULATION";
+
WINDOW_WIDTH = WINDOW_WIDTH;
WINDOW_HEIGHT = WINDOW_HEIGHT;
KEYS = KEYMAP;
+ -- COLOR PROPERTIES
+
BACK_COLOR = { 0.1, 0.1, 0.15 };
FONT_COLOR = { 1.0, 1.0, 1.0 };
ENEMY_COLOR = { 1.0, 0.0, 0.0 };
BULLET_COLOR = { 0.6, 0.6, 1.0 };
+ -- BEHAVIOR PROPERTIES
+
PLAYER_VISION_SEGMENTS = 32;
PLAYER_VISION_DISTANCE = 20;
ENEMY_SIZE = 14;
- MAX_NEURONS = 1024;
+ -- GENETIC PROPERTIES
- -- How many of the genomes tested survive
+ MAX_NEURONS = 1024;
GENOME_THRESHOLD = 1 / 5;
+ POPULATION_SIZE = 100;
Starting_Weights_Chance = 0.25;
Starting_Connection_Chance = 2.0;
Reset_Weight_Chance = 0.9;
Crossover_Chance = 0.75;
+
+ -- REWARD / PUNISHMENT PROPERTIES
+
+ POINTS_PER_KILL = 100;
+ POINTS_PER_ROUND_END = 1000;
+ POINTS_PER_BULLET = -1;
+ POINTS_PER_MOVEMENT = 1;
}
Things to fix tonight...
-* Need to be able to save and load the generations
+\, Need to be able to save and load the generations
\, main.lua has a lot of logic that can be split up
\, Should have a "Tester" class that encapsulates the updating and handling population growth
- Should have a Statistics class that calculates basic stats on a list of numbers
-- Way to "manually" play the game
-
- Need to be able to train the AI without running the visuals
- - Separate logic
+ \, Separate logic
local world_mod = require "src.world"
local Input = require "src.input"
local Gen = require "src.genetics"
+require "src.data"
local Trainer = (require "src.trainer").Trainer
local World = world_mod.World
input = Input:new()
- pop = Population.new()
- pop:create_genomes(96, 16, 8)
+ if CONF.LOAD_FILE == "" then
+ pop = Population.new()
+ pop:create_genomes(CONF.POPULATION_SIZE, 16, 8)
+ else
+ pop = Population.load(CONF.LOAD_FILE)
+ end
trainer = Trainer.new(pop, world, input)
trainer:initialize_training()
input:keyup(key)
end
-
function love.update(dt)
if love.keyboard.isDown "escape" then
love.event.quit()
--- /dev/null
+local CONF = require "conf"
+local gen_mod = require "src.genetics"
+local Population = gen_mod.Population
+
+local save_file_safe = false
+
+local function file_exists(path)
+ local file = io.open(path, "r")
+ if file ~= nil then
+ file:close()
+ return true
+ else
+ return false
+ end
+end
+
+function Population:save(path)
+ local real_path = path .. "_GEN_" .. tostring(self.generation)
+
+ if file_exists(real_path) and not save_file_safe then
+ local e = 1
+ local tmp
+
+ repeat
+ tmp = path .. "_" .. tostring(e)
+ e = e + 1
+ until not file_exists(tmp .. "_GEN_1")
+
+ real_path = tmp .. "_GEN_" .. tostring(self.generation)
+
+ -- Override the configured save file since it already exists
+ CONF.SAVE_FILE = tmp
+ save_file_safe = true
+ end
+
+ local file = io.open(real_path, "w")
+
+ file:write(self.genome_count .. "\n")
+ file:write(self.generation .. "\n")
+ file:write("0\n")
+
+ for _, genome in ipairs(self.genomes) do
+ file:write((genome.num_inputs - 1).. " " .. genome.num_outputs .. " " .. genome.high_neuron .. "\n")
+ file:write(genome.mutations["weights"] .. " ")
+ file:write(genome.mutations["connection"] .. " ")
+ file:write(genome.mutations["bias"] .. " ")
+ file:write(genome.mutations["split"] .. " ")
+ file:write(genome.mutations["enable"] .. " ")
+ file:write(genome.mutations["disable"] .. "\n")
+
+ file:write(#genome.genes .. "\n")
+
+ for _, gene in ipairs(genome.genes) do
+ file:write(gene.weight .. " " .. gene.from .. " " .. gene.to .. " " .. gene.innovation .. "\n")
+ end
+ end
+
+ file:close()
+end
+
+function Population.load(path)
+ local file = io.open(path, "r")
+
+ local pop = Population.new()
+
+ pop.genome_count = file:read("*n")
+ pop.generation = file:read("*n")
+ pop.current_genome = file:read("*n")
+
+ for i = 1, pop.genome_count do
+ local ins, outs = file:read("*n", "*n")
+ pop:create_empty_genome(ins, outs)
+
+ local genome = pop.genomes[i]
+ genome.high_neuron = file:read("*n")
+ genome.mutations["weights"] = file:read("*n")
+ genome.mutations["connection"] = file:read("*n")
+ genome.mutations["bias"] = file:read("*n")
+ genome.mutations["split"] = file:read("*n")
+ genome.mutations["enable"] = file:read("*n")
+ genome.mutations["disable"] = file:read("*n")
+
+ local num_genes = file:read("*n")
+
+ for _ = 1, num_genes do
+ local from, to, weight, innov
+ weight = file:read("*n")
+ from = file:read("*n")
+ to = file:read("*n")
+ innov = file:read("*n")
+
+ genome:add_gene(from, to, weight, innov)
+ end
+ end
+
+ file:close()
+
+ return pop
+end
return o
end
-function Genome:add_gene(from, to, weight)
+function Genome:add_gene(from, to, weight, innov)
if from > to then return end
local gene = Gene.new()
gene.weight = weight
gene.from = from
gene.to = to
- gene.innovation = Get_Next_Innovation()
+ gene.innovation = innov or Get_Next_Innovation()
table.insert(self.genes, gene)
end
genomes = {};
genome_count = 0;
generation = 0;
- max_innovations = 0;
current_genome = 0;
high_fitness = 0;
total_fitness = 0;
return o
end
+function Population:create_empty_genome(ins, outs)
+ table.insert(self.genomes, Genome.new(ins, outs))
+end
+
function Population:create_genomes(num, inputs, outputs)
local genomes = self.genomes
self.genome_count = num
end
end
-function Population:evolve(_, _, generation_step, ...)
- generation_step(self.avg_fitness, self.high_fitness, ...)
+function Population:evolve(_, _, generation_steps, ...)
+ generation_steps[1](self.avg_fitness, self.high_fitness, ...)
self:kill_worst()
self:mate()
self.avg_fitness = 0
self.total_fitness = 0
+ generation_steps[2](...)
+
return self.training_step
end
return self:after_inputs(...)
end
- self.generation_step_func = function(...)
- return self:generation_step(...)
+ self.pre_evolution_func = function(...)
+ return self:pre_evolution(...)
+ end
+
+ self.post_evolution_func = function(...)
+ return self:post_evolution(...)
end
end
self.world:update(dt, self.input)
- local fitness = math.sqrt(math.sqrDist(last_x, last_y, self.player.x, self.player.y))
+ local fitness = 0
+
+ local dist = math.sqrt(math.sqrDist(last_x, last_y, self.player.x, self.player.y))
+ fitness = fitness + dist * CONF.POINTS_PER_MOVEMENT
- fitness = fitness - (self.player.shot and 1 or 0)
+ fitness = fitness + (self.player.shot and CONF.POINTS_PER_BULLET or 0)
self.player.shot = false
if self.player.kills ~= last_kills then
- fitness = fitness + 400 * (self.player.kills - last_kills)
+ fitness = fitness + CONF.POINTS_PER_KILL * (self.player.kills - last_kills)
end
if not self.player.alive or self.world:get_count{ "Enemy" } == 0 then
self.world:kill_all{ "Bullet", "Enemy" }
if self.player.alive then
- fitness = fitness + 2000
+ fitness = fitness + CONF.POINTS_PER_ROUND_END
self.world:next_round()
else
self.world:reset()
return fitness, self.player.alive
end
-function Trainer:generation_step(avg, high, _)
- print "PROCEEDING TO NEXT GENERATION"
+function Trainer:pre_evolution(_, _, _)
+end
+
+function Trainer:post_evolution(_)
+ self.population:save(CONF.SAVE_FILE)
end
function Trainer:update(dt)
self.population,
inputs,
self.after_inputs_func,
- self.generation_step_func,
+ { self.pre_evolution_func, self.post_evolution_func },
dt
)
end