309 lines
9.6 KiB
Lua
309 lines
9.6 KiB
Lua
|
|
dofile("./trainer.lua")
|
|
dofile("../kobold/kobold.lua")
|
|
|
|
local trainer = adaptive_ai.trainer
|
|
local helper = adaptive_ai.helper
|
|
local calc = adaptive_ai.calc
|
|
local kobolds = adaptive_ai.kobolds
|
|
local file_tag = kobolds.trained_tag
|
|
|
|
local random = math.random
|
|
--local mod = math.fmod or math.mod
|
|
local move_creature = trainer.move_creature
|
|
local truncate = calc.truncate
|
|
local distance = calc.dist_euclidean
|
|
local pop = helper.pop
|
|
local valid = trainer.valid
|
|
|
|
local adjacent = helper.adjacent
|
|
local turn_max = 300
|
|
local popul_size = 100
|
|
|
|
local function fill_cell_kobold(cell, y)
|
|
if random() <= 0.01 then
|
|
-- Putting impassable obstacle
|
|
cell[1] = nil
|
|
else
|
|
if random() <= 0.02 then
|
|
-- Planting mushroom
|
|
cell[2] = true
|
|
else
|
|
cell[2] = false
|
|
end
|
|
end
|
|
end
|
|
|
|
local function fill_kobold(kobold, mother, father)
|
|
kobold.fear_height = kobolds.stats.fear_height
|
|
kobold.pos = {x=random(1,40), z=random(1,40)}
|
|
|
|
kobolds.setup(kobold, mother, father)
|
|
|
|
kobold.fitness = 0
|
|
trainer.console_log("Created new kobold: "..kobold.name)
|
|
end
|
|
|
|
local function reset_kobold(kobold, map)
|
|
kobold.food = {}
|
|
kobold.fitness = 0
|
|
kobold.hp = kobolds.stats.hp_max
|
|
kobold.hunger = kobolds.stats.hunger_max
|
|
|
|
|
|
kobold.pos.y = map[kobold.pos.x][kobold.pos.z][1]
|
|
while not kobold.pos.y do
|
|
kobold.pos = {x=random(1,40), z=random(1,40)}
|
|
kobold.pos.y = map[kobold.pos.x][kobold.pos.z][1]
|
|
end
|
|
end
|
|
|
|
local kobold_decisions = {
|
|
function(self, dtime) -- idle
|
|
end,
|
|
function(self, dtime) -- wander
|
|
local n = random(1,#adjacent)
|
|
move_creature(self, n)
|
|
self.hunger = self.hunger - 1
|
|
end,
|
|
function(self, dtime) -- eat
|
|
if self.food and #self.food > 0 then
|
|
local satiate = pop(self.food)
|
|
self:eat(satiate)
|
|
end
|
|
end,
|
|
function(self, dtime) -- breed
|
|
self.fitness = self.fitness + 1
|
|
self.hunger = self.hunger - 10
|
|
end,
|
|
function(self, dtime) -- move North
|
|
move_creature(self, 5)
|
|
end,
|
|
function(self, dtime) -- move South
|
|
move_creature(self, 4)
|
|
end,
|
|
function(self, dtime) -- move East
|
|
move_creature(self, 7)
|
|
end,
|
|
function(self, dtime) -- move West
|
|
move_creature(self, 2)
|
|
end,
|
|
function(self, dtime) -- move NE
|
|
move_creature(self, 8)
|
|
end,
|
|
function(self, dtime) -- move SE
|
|
move_creature(self, 6)
|
|
end,
|
|
function(self, dtime) -- move NW
|
|
move_creature(self, 3)
|
|
end,
|
|
function(self, dtime) -- move SW
|
|
move_creature(self, 1)
|
|
end,
|
|
}
|
|
|
|
local function manage_kobold(player, map, dtime)
|
|
if player.hp > 0 then
|
|
local neighbors = {}
|
|
local p, pos = {}, player.pos
|
|
for i=1,#adjacent do
|
|
p.x = pos.x + adjacent[i].x
|
|
p.z = pos.z + adjacent[i].z
|
|
if valid(p) and map[p.x][p.z][1] then
|
|
p.y = map[p.x][p.z][1] - player.pos.y
|
|
else
|
|
p.y = pos.y + player.climb + 1
|
|
end
|
|
neighbors[i] = {x=p.x, y=p.y, z=p.z}
|
|
--print("Neighbor "..i.." = "..p.y)
|
|
end
|
|
|
|
player.hunger = player.hunger - dtime
|
|
local decision, gene = player.decide(player, neighbors)
|
|
if decision and decision ~= -1 then
|
|
player.action = decision
|
|
player.last_gene = gene
|
|
kobold_decisions[decision](player, dtime)
|
|
else
|
|
if not player.action then player.action = 1 end
|
|
end
|
|
|
|
pos = player.pos
|
|
|
|
if map[pos.x][pos.z][2] then
|
|
table.insert(player.food, random(2,5))
|
|
map[pos.x][pos.z][2] = false
|
|
if player.closest_food
|
|
and player.closest_food.x == pos.x
|
|
and player.closest_food.z == pos.z then
|
|
player.closest_food = nil
|
|
end
|
|
end
|
|
|
|
if player.hunger < 0 then
|
|
player.hunger = 0
|
|
player.hp = player.hp - 1
|
|
end
|
|
player.fitness = player.fitness + player:score()
|
|
return false
|
|
else
|
|
return true
|
|
end
|
|
end
|
|
|
|
local function print_terrain(map, player)
|
|
local s = string.rep("#",42).."\n"
|
|
local line, cell
|
|
local p = player.pos
|
|
|
|
for i=1,40 do
|
|
line = "#"
|
|
for j=1,40 do
|
|
if p.x == i and p.z == j then
|
|
cell = "i"
|
|
elseif map[i][j][1] == nil then
|
|
cell = "O"
|
|
elseif map[i][j][1] == p.y and map[i][j][2] == true then
|
|
cell = "T"
|
|
elseif map[i][j][1] == p.y then
|
|
cell = "▒"
|
|
elseif map[i][j][1] - p.y == -1 then
|
|
cell = "▓"
|
|
elseif map[i][j][1] - p.y == 1 then
|
|
cell = "░"
|
|
elseif map[i][j][1] - p.y < -1 then
|
|
cell = "█"
|
|
elseif map[i][j][1] - p.y > 1 then
|
|
cell = " "
|
|
else
|
|
cell = "?"
|
|
end
|
|
line = line..cell
|
|
end
|
|
s = s..line.."#\n"
|
|
end
|
|
s = s..string.rep("#",42)
|
|
return s
|
|
end
|
|
|
|
local function final_fitness(player, turn, max_turn)
|
|
local c1 = turn
|
|
local c2 = player.fitness
|
|
local m = 3 -- * max_turn
|
|
return (m*c1 + c2)/100
|
|
end
|
|
|
|
local function turn_log_kobold(player, map, gen_stats, turn)
|
|
local s = "\n\nBest creature: "..gen_stats.champ.."\tBest creature fitness: "..truncate(gen_stats.max_peak).."\n"
|
|
s = s.."Best creature lifespan: "..(gen_stats.lifespan or "None").."\tBest creature gen: "..(gen_stats.gen or "None").."\n"
|
|
s = s.."Last gen avg: "..truncate(gen_stats.avg).."\t\tLast gen peak: "..truncate(gen_stats.peak).."\n\n"
|
|
s = s.."Current gen: "..gen_stats.num.."\t\tCurrent creature: "..player.id.."\n"
|
|
s = s.."Turn: "..(turn < 10 and "0" or "")..turn.."\t\tCreature name: "..player.name.."\n"
|
|
s = s.."Action: "..kobolds.actions[player.action].."\t\tFocus: "..player.focus.."\n"
|
|
s = s.."Gene: "..(player.last_gene or "None").."\t\tFitness: "..truncate(final_fitness(player, turn, turn_max)).."\n\n"
|
|
|
|
s = s..print_terrain(map, player)
|
|
return s, 0.008
|
|
end
|
|
|
|
local function gen_log_kobold(num, gen_stats)
|
|
local s = "\n\nBest creature: "..gen_stats.champ.."\tBest creature fitness: "..truncate(gen_stats.max_peak).."\n"
|
|
s = s.."Best creature lifespan: "..(gen_stats.lifespan or "None").."\tBest creature gen: "..(gen_stats.gen or "None").."\n"
|
|
s = s.."Last gen average: "..truncate(gen_stats.avg).."\t\tLast gen peak: "..truncate(gen_stats.peak).."\n\n"
|
|
end
|
|
|
|
local function map_checks_kobold(player, map)
|
|
if random() <= 0.05 then
|
|
map[random(1,40)][random(1,40)][2] = true
|
|
end
|
|
|
|
for i=1,40 do
|
|
for j=1,40 do
|
|
if map[i][j][1] and map[i][j][2] == true then
|
|
local pos = {x=i, y=map[i][j][1], z=j}
|
|
if not pos.y then error("No pos.y in "..i..","..j) end
|
|
if not player.pos.y then error("No player.pos.y") end
|
|
if player.closest_food and not player.closest_food.y then error("No player.closest_food.y") end
|
|
if not player.closest_food
|
|
or distance(player.pos, pos) < distance(player.pos, player.closest_food) then
|
|
player.closest_food = pos--{x=pos.x, y=pos.y, z=pos.z}
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
local function gen_stats_update(champ, gen_stats)
|
|
gen_stats.champ = champ.name
|
|
gen_stats.peak = champ.fitness
|
|
gen_stats.lifespan = champ.lifespan
|
|
end
|
|
|
|
local function global_log(gen_stats)
|
|
local s = string.rep("-", 42).."\n"..string.rep("-", 42).."\n\n"
|
|
s = s.."Last champion: "..gen_stats.champ.."\n"
|
|
s = s.."From generation "..gen_stats.gen.."\tTotal generations: "..gen_stats.num.."\n"
|
|
s = s.."Fitnes score: "..gen_stats.max_peak.."\tLifespan: "..(gen_stats.lifespan or "None").."\n"
|
|
s = s.."\n"..string.rep("-", 42).."\n"..string.rep("-", 42).."\n"
|
|
|
|
return s
|
|
end
|
|
|
|
local function train()
|
|
local input = false
|
|
local option = input and 0 or 300
|
|
local offset = 0
|
|
local population = {}
|
|
repeat
|
|
local gens_option = tonumber(option)
|
|
if gens_option and gens_option > 0 then
|
|
local elapsed = os.clock()
|
|
local stats = {
|
|
champ = "None",
|
|
lifespan = 0,
|
|
gen = 0,
|
|
}
|
|
|
|
population, offset = trainer.train({
|
|
dims = {x=40, y=10, z=40},
|
|
output = input,
|
|
gens = gens_option,
|
|
fill_cell = fill_cell_kobold,
|
|
fill_creature = fill_kobold,
|
|
reset_creature = reset_kobold,
|
|
decision_managers = kobold_decisions,
|
|
manage_creature = manage_kobold,
|
|
clone = kobolds.clone,
|
|
map_checks = map_checks_kobold,
|
|
turn_log = turn_log_kobold,
|
|
gen_log = gen_log_kobold,
|
|
training_log = global_log,
|
|
fitness_threshold = 40,
|
|
fitness_func = final_fitness,
|
|
max_turn = turn_max,
|
|
popul = population,
|
|
custom_stats = gen_stats_update,
|
|
gen_stats = stats,
|
|
to_file = file_tag,
|
|
top_size = 4,
|
|
}, offset)
|
|
|
|
if not minetest then
|
|
elapsed = os.clock() - elapsed
|
|
|
|
print(global_log(stats))
|
|
print()
|
|
print("Elapsed time: ", elapsed)
|
|
end
|
|
end
|
|
|
|
if minetest or not input then
|
|
option = "q"
|
|
else
|
|
print("Please enter number of gens to simulate:")
|
|
option = io.read()
|
|
end
|
|
until option == "q" or option == "Q"
|
|
end
|
|
|
|
train()
|