369 lines
10 KiB
Lua
369 lines
10 KiB
Lua
|
|
-- Example of Creature with Genetic Algorithm
|
|
|
|
-- Structure:
|
|
-- n possible actions
|
|
-- Single vector 1 value for each combination of inputs
|
|
-- Each value in vector is 1..n to decide an action
|
|
|
|
local kobolds = {}
|
|
|
|
local distance = adaptive_ai.calc.dist_euclidean
|
|
local chat_log = adaptive_ai.chat_log
|
|
local random = math.random
|
|
local abs = math.abs
|
|
local floor = math.floor
|
|
local min = math.min
|
|
local max = math.max
|
|
local round = adaptive_ai.calc.round
|
|
local chat_log = adaptive_ai.chat_log or function(s) end--print(s) end
|
|
|
|
local file_tag = {"KOBOLD", "END_KOBOLD"}
|
|
local trained_tag = "trained_kobolds"
|
|
|
|
local actions = {
|
|
"idle", "wander",
|
|
"eat", "breed",
|
|
--"get_food",
|
|
"move_N", "move_S", "move_E", "move_W",
|
|
"move_NE", "move_SE", "move_NW", "move_SW"
|
|
}
|
|
|
|
-- Considerations:
|
|
-- Hunger/life = Good, bad, critical (3 values)
|
|
-- Direction of food = N, S, E, W, NE, NW, SE, SW (8 values)
|
|
-- Height of 4 directions = leveled, drop, climb (3^4 values = 81)
|
|
-- Total values = 3*8*81 = 1944 possible values
|
|
|
|
-- Global stats
|
|
local stats = {
|
|
total_genes = 1944,
|
|
mutation_chance = 0.2,
|
|
view_range = 20,
|
|
jump = 5,
|
|
climb = 1,
|
|
hunger_max = 100,
|
|
hp_max = 10,
|
|
fear_height = 2
|
|
}
|
|
|
|
local function get_genes(kobold)
|
|
genes = {}
|
|
for i = 1, stats.total_genes do
|
|
genes[i] = random(1, #actions)
|
|
end
|
|
kobold.genes = genes
|
|
end
|
|
|
|
local score = function(self)
|
|
local total = 0
|
|
total = total + (stats.hunger_max - self.hunger)/stats.hunger_max
|
|
total = total + (stats.hp_max - self.hp)/stats.hp_max
|
|
return floor(total + 0.5)
|
|
end
|
|
|
|
local function decide(self, neighbor_list)
|
|
if random() < self.focus then return nil end
|
|
local pos = self.pos
|
|
|
|
local hunger_level -- Hunger level
|
|
if self.hunger > stats.hunger_max * 0.6 then
|
|
hunger_level = 0 -- Good
|
|
--chat_log("Hunger: good")
|
|
elseif self.hunger > stats.hunger_max * 0.1 then
|
|
hunger_level = 1 -- Bad
|
|
--chat_log("Hunger: bad")
|
|
else
|
|
hunger_level = 2 -- Critical
|
|
--chat_log("Hunger: critical")
|
|
end
|
|
--chat_log("Hunger "..hunger_level)
|
|
hunger_level = hunger_level * 648 -- stats.total_genes/4
|
|
|
|
local dir_level
|
|
local closest_food = self.closest_food
|
|
if closest_food then
|
|
if pos.z < closest_food.z then -- North
|
|
if pos.x < closest_food.x then
|
|
dir_level = 0 -- North-East
|
|
elseif pos.x > closest_food.x then
|
|
dir_level = 1 -- North-West
|
|
else
|
|
dir_level = 4 -- North
|
|
end
|
|
elseif pos.z < closest_food.z then
|
|
if pos.x < closest_food.x then
|
|
dir_level = 2 -- South-East
|
|
elseif pos.x > closest_food.x then
|
|
dir_level = 3 -- South-West
|
|
else
|
|
dir_level = 5 -- South
|
|
end
|
|
elseif pos.x < closest_food.x then
|
|
dir_level = 6 -- East
|
|
elseif pos.x > closest_food.x then
|
|
dir_level = 7 -- West
|
|
else
|
|
dir_level = 4
|
|
end
|
|
else
|
|
dir_level = 5
|
|
--chat_log("No close food")
|
|
end
|
|
--chat_log("Dir "..dir_level)
|
|
dir_level = dir_level * 81
|
|
|
|
local current_neighbor, node_height, neighbors_total = 0, 0, 0
|
|
--chat_log("Current: "..current_neighbor.."\tNeighbors "..neighbors_total)
|
|
for i=1,3 do -- North
|
|
node_height = (neighbor_list[i].y) + node_height
|
|
end
|
|
node_height = round(node_height/3)
|
|
if node_height > self.climb then
|
|
current_neighbor = 2 -- Climb
|
|
elseif node_height < self.fear_height then
|
|
current_neighbor = 1 -- Drop
|
|
else
|
|
current_neighbor = 0
|
|
end
|
|
neighbors_total = current_neighbor
|
|
--chat_log("Height: "..node_height.."\tCurrent: "..current_neighbor.."\tNeighbors "..neighbors_total)
|
|
|
|
node_height = 0
|
|
for _,i in ipairs({1,4,6}) do -- West
|
|
node_height = (neighbor_list[i].y) + node_height
|
|
end
|
|
node_height = round(node_height/3)
|
|
if node_height > self.climb then
|
|
current_neighbor = 2 -- Climb
|
|
elseif node_height < self.fear_height then
|
|
current_neighbor = 1 -- Drop
|
|
else
|
|
current_neighbor = 0
|
|
end
|
|
neighbors_total = neighbors_total * 3 + current_neighbor
|
|
--chat_log("Height: "..node_height.."\tCurrent: "..current_neighbor.."\tNeighbors "..neighbors_total)
|
|
|
|
node_height = 0
|
|
for _,i in ipairs({3,5,8}) do -- East
|
|
node_height = (neighbor_list[i].y) + node_height
|
|
end
|
|
node_height = round(node_height/3)
|
|
if node_height > self.climb then
|
|
current_neighbor = 2 -- Climb
|
|
elseif node_height < self.fear_height then
|
|
current_neighbor = 1 -- Drop
|
|
else
|
|
current_neighbor = 0
|
|
end
|
|
neighbors_total = neighbors_total * 3 + current_neighbor
|
|
--chat_log("Height: "..node_height.."\tCurrent: "..current_neighbor.."\tNeighbors "..neighbors_total)
|
|
|
|
node_height = 0
|
|
for i=6,8 do -- South
|
|
node_height = (neighbor_list[i].y) + node_height
|
|
end
|
|
node_height = round(node_height/3)
|
|
if node_height > self.climb then
|
|
current_neighbor = 2 -- Climb
|
|
elseif node_height < self.fear_height then
|
|
current_neighbor = 1 -- Drop
|
|
else
|
|
current_neighbor = 0
|
|
end
|
|
neighbors_total = neighbors_total * 3 + current_neighbor
|
|
--chat_log("Height: "..node_height.."\tCurrent: "..current_neighbor.."\tNeighbors "..neighbors_total)
|
|
|
|
local gene = hunger_level + dir_level + neighbors_total + 1
|
|
chat_log("Gene: "..gene.."\tHunger id: "..hunger_level.."\tDirection id: "..dir_level.."\tNeighbors id: "..neighbors_total)
|
|
|
|
return self.genes[gene], gene
|
|
end
|
|
|
|
local function breed(mother, father)
|
|
local genes = {}
|
|
for i = 1, stats.total_genes do
|
|
if random() <= stats.mutation_chance then
|
|
genes[i] = random(1, #actions)
|
|
else
|
|
genes[i] = random() <= 0.5 and mother.genes[i] or father.genes[i]
|
|
end
|
|
end
|
|
|
|
local focus
|
|
if random() <= stats.mutation_chance then
|
|
focus = random()
|
|
else
|
|
local fmax = max(mother.focus, father.focus)
|
|
local fmin = min(mother.focus, father.focus)
|
|
focus = fmin + random() * (fmax - fmin)
|
|
end
|
|
|
|
return genes, focus
|
|
end
|
|
|
|
-- Sugar
|
|
local name_glyphs = {
|
|
"a", "ka", "ta", "pa", "na", "sa",
|
|
"i", "ki", "ch", "p", "ny", "si",
|
|
"y", "k", "z", "f", "n", "s",
|
|
"u", "ku", "tu", "pu", "nu", "su"
|
|
}
|
|
|
|
local function new_name(count)
|
|
local s = name_glyphs[math.random(1,#name_glyphs)]
|
|
if math.random() < 0.2 and (count > 2) then return s end
|
|
local n = count + 1
|
|
if n > 3 then return s end
|
|
return s..new_name(n)
|
|
end
|
|
|
|
local function uppercase(s)
|
|
return s:sub(1,1):upper()..s:sub(2)
|
|
end
|
|
|
|
local function correct_name(name)
|
|
name = name:gsub("zs", "ts")
|
|
name = name:gsub("pn", "m")
|
|
name = name:gsub("kn", "ng")
|
|
name = name:gsub("yy", "yi")
|
|
name = name:gsub("ks", "kh")
|
|
name = name:gsub("sch", "sh")
|
|
|
|
return name
|
|
end
|
|
|
|
local function get_name(kobold, mother)
|
|
if kobold.firstname and kobold.midname and kobold.family and not kobold.name then
|
|
kobold.name = kobold.firstname.." "..kobold.midname.." "..kobold.family
|
|
return
|
|
end
|
|
if mother then
|
|
if mother.family == "Sisanzik" then
|
|
kobold.family = correct_name(mother.firstname.."nzik")
|
|
else
|
|
kobold.family = mother.family
|
|
end
|
|
kobold.midname = mother.firstname.."anuy"
|
|
else
|
|
kobold.midname = ""
|
|
kobold.family = "Sisanzik"
|
|
end
|
|
|
|
local name = correct_name(new_name(0))
|
|
kobold.firstname = uppercase(name)
|
|
|
|
kobold.name = kobold.firstname.." "..kobold.midname.." "..kobold.family
|
|
end
|
|
|
|
local function create(self, mother, father)
|
|
if not self.name then
|
|
get_name(self, mother)
|
|
end
|
|
if not self.genes then
|
|
if mother and father then
|
|
local genes, focus = breed(mother, father)
|
|
self.genes = genes
|
|
self.focus = focus
|
|
else
|
|
get_genes(self)
|
|
self.focus = random()
|
|
end
|
|
end
|
|
|
|
self.food = {}
|
|
self.climb = stats.climb
|
|
|
|
self.hunger = self.hunger or stats.hunger_max
|
|
self.hp = self.hp or stats.hp_max
|
|
self.fear_height = self.fear_height or stats.fear_height
|
|
self.fitness = self.fitness or 0
|
|
|
|
return self
|
|
end
|
|
|
|
local function clone(kobold, cloned)
|
|
cloned = cloned or {}
|
|
|
|
for k, v in pairs(kobold) do
|
|
if k ~= "genes" then cloned[k] = v end
|
|
end
|
|
cloned.genes = {}
|
|
for i, v in ipairs(kobold.genes) do
|
|
cloned.genes[i] = v
|
|
end
|
|
|
|
return cloned
|
|
end
|
|
|
|
local function to_lines(self)
|
|
local s = {}
|
|
s[1] = file_tag[1]
|
|
s[2] = "genes"
|
|
s[3] = "{"
|
|
for _,gene in ipairs(self.genes) do
|
|
s[3] = s[3]..gene..","
|
|
end
|
|
s[3] = s[3].."}"
|
|
s[4] = "focus"
|
|
s[5] = tostring(self.focus)
|
|
s[6] = "firstname"
|
|
s[7] = "\""..self.firstname.."\""
|
|
s[8] = "midname"
|
|
s[9] = "\""..self.midname.."\""
|
|
s[10] = "family"
|
|
s[11] = "\""..self.family.."\""
|
|
if self.tribe_id then
|
|
table.insert(s, "tribe_id")
|
|
table.insert(s, self.tribe_id)
|
|
end
|
|
if self.food then
|
|
table.insert(s, "food")
|
|
local s_food = "{"
|
|
for _,f in ipairs(self.food) do
|
|
s_food = s_food..f..","
|
|
end
|
|
table.insert(s, s_food.."}")
|
|
end
|
|
table.insert(s, file_tag[2])
|
|
|
|
return s
|
|
end
|
|
|
|
local function setup(self, mother, father)
|
|
create(self, mother, father)
|
|
self.decide = decide
|
|
self.score = score
|
|
self.to_lines = to_lines
|
|
end
|
|
|
|
local function from_lines(s)
|
|
kobold = {}
|
|
if s[1] ~= file_tag[1] or s[#s] ~= file_tag[2] then
|
|
error("Kobold expected. Not a kobold")
|
|
end
|
|
for i=2,#s - 1,2 do
|
|
loadstring("kobold[\""..s[i].."\"] = "..s[i+1])()
|
|
end
|
|
if type(kobold.genes) ~= "table" then
|
|
error("Kobold genes are not a table, but a "..type(kobold.genes))
|
|
end
|
|
|
|
create(kobold)
|
|
|
|
return kobold
|
|
end
|
|
|
|
kobolds.stats = stats
|
|
kobolds.actions = actions
|
|
kobolds.create = create
|
|
kobolds.setup = setup
|
|
kobolds.clone = clone
|
|
kobolds.file_tag = file_tag
|
|
kobolds.trained_tag = trained_tag
|
|
kobolds.from_lines = from_lines
|
|
kobolds.decide = decide
|
|
kobolds.score = score
|
|
kobolds.to_lines = kobolds.to_lines
|
|
|
|
adaptive_ai.kobolds = kobolds
|