adaptive-ai/kobold/kobold.lua

392 lines
11 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 ceil = math.ceil
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 directions = {
"northeast", "northwest", "southeast", "southwest", "north", "south", "east", "west"
}
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 eat(self, food)
local ate = false
if self.hp < stats.hp_max then
self.hp = self.hp + ceil(food/2)
if self.hp > stats.hp_max then self.hp = stats.hp_max end
ate = ate or true
end
if self.hunger < stats.hunger_max then
self.hunger = self.hunger + food
if self.hunger > stats.hunger_max then self.hunger = stats.hunger_max end
ate = ate or true
end
return ate
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.1
or self.hp < stats.hp_max * 0.8 then
hunger_level = 2 -- Critical
elseif self.hunger < stats.hunger_max * 0.6 then
hunger_level = 1 -- Bad
else
hunger_level = 0 -- Good
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
chat_log("Close food at "..directions[dir_level+1])
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)
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:len() > 0 and " "..kobold.midname or "").." "..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.eat = eat
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
kobolds.eat = eat
adaptive_ai.kobolds = kobolds