-- 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 goblins = {} 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 = {"GOBLIN", "END_GOBLIN"} local trained_tag = "trained_goblins" 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 (2 values) -- Direction of food = N, S, E, W, NE, NW, SE, SW (8 values) -- Has pocketed food = Yes, No (2 values) -- Walkable neighbors = walkable, non-walkable (2^4 values) -- Possible actions (12 values) -- Total values: matrix of (4*8*2*2^4) x 12 = 3072 possible values -- For each action: -- idle, wander, breed --> small reward at high satiation, punishment at low satiation -- eat --> reward in increase of hunger -- move --> punishment if can't move, reward if closer to food, punishment if drop too high -- x*12 = 1944 -- Global stats local stats = { total_states = 64, gamma = 0.6, view_range = 20, jump = 5, hunger_max = 100, hp_max = 10, fear_height = 2 } local function get_genes(goblin) genes = {} for i = 1, stats.total_genes do genes[i] = random(1, #actions) end goblin.genes = genes end local score = function(self) local total = 0 total = total + (stats.hunger_max - self.hunger)/stats.hunger_max total = total + 2 * (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(goblin, mother) if goblin.firstname and goblin.midname and goblin.family and not goblin.name then goblin.name = goblin.firstname.." "..goblin.midname.." "..goblin.family return end if mother then if mother.family == "Sisanzik" then goblin.family = correct_name(mother.firstname.."nzik") else goblin.family = mother.family end goblin.midname = mother.firstname.."anuy" else goblin.midname = "" goblin.family = "Sisanzik" end local name = correct_name(new_name(0)) goblin.firstname = uppercase(name) goblin.name = goblin.firstname.." "..goblin.midname.." "..goblin.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 = 2 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(goblin, cloned) cloned = cloned or {} for k, v in pairs(goblin) do if k ~= "genes" then cloned[k] = v end end cloned.genes = {} for i, v in ipairs(goblin.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) goblin = {} if s[1] ~= file_tag[1] or s[#s] ~= file_tag[2] then error("Kobold expected. Not a goblin") end for i=2,#s - 1,2 do loadstring("goblin[\""..s[i].."\"] = "..s[i+1])() end if type(goblin.genes) ~= "table" then error("Kobold genes are not a table, but a "..type(goblin.genes)) end create(goblin) return goblin end goblins.stats = stats goblins.actions = actions goblins.create = create goblins.setup = setup goblins.clone = clone goblins.file_tag = file_tag goblins.trained_tag = trained_tag goblins.from_lines = from_lines goblins.decide = decide goblins.score = score goblins.to_lines = goblins.to_lines adaptive_ai.goblins = goblins