From 64a609ebe8b965673c860fdbbcf2440a3629eed0 Mon Sep 17 00:00:00 2001 From: Vylion Date: Tue, 26 Jun 2018 19:20:49 +0200 Subject: [PATCH] Initial commit --- api.lua | 76 +++ api.md | 595 +++++++++++++++++++ api_mobs.lua | 476 +++++++++++++++ depends.txt | 2 + goblin/goblin.lua | 375 ++++++++++++ goblin/goblin_integration.lua | 514 ++++++++++++++++ helper.lua | 135 +++++ helper_mobs.lua | 954 ++++++++++++++++++++++++++++++ init.lua | 22 + kobold/kobold.lua | 369 ++++++++++++ kobold/kobold_integration.lua | 514 ++++++++++++++++ moondrum.lua | 189 ++++++ population.lua | 239 ++++++++ textures/aa_imp_face.png | Bin 0 -> 2989 bytes textures/aa_imp_side.png | Bin 0 -> 2881 bytes textures/aa_imp_top.png | Bin 0 -> 2899 bytes textures/aa_moondrum_mushroom.png | Bin 0 -> 3047 bytes textures/aa_moondrum_roots.png | Bin 0 -> 3015 bytes textures/tnt_smoke.png | Bin 0 -> 202 bytes textures/transparent.png | Bin 0 -> 2808 bytes training/noise.lua | 182 ++++++ training/trained_kobolds.txt | 112 ++++ training/trainer.lua | 300 ++++++++++ training/trainer_goblin.lua | 305 ++++++++++ training/trainer_kobold.lua | 305 ++++++++++ 25 files changed, 5664 insertions(+) create mode 100644 api.lua create mode 100644 api.md create mode 100644 api_mobs.lua create mode 100644 depends.txt create mode 100644 goblin/goblin.lua create mode 100644 goblin/goblin_integration.lua create mode 100644 helper.lua create mode 100644 helper_mobs.lua create mode 100644 init.lua create mode 100644 kobold/kobold.lua create mode 100644 kobold/kobold_integration.lua create mode 100644 moondrum.lua create mode 100644 population.lua create mode 100644 textures/aa_imp_face.png create mode 100644 textures/aa_imp_side.png create mode 100644 textures/aa_imp_top.png create mode 100644 textures/aa_moondrum_mushroom.png create mode 100644 textures/aa_moondrum_roots.png create mode 100644 textures/tnt_smoke.png create mode 100644 textures/transparent.png create mode 100644 training/noise.lua create mode 100644 training/trained_kobolds.txt create mode 100644 training/trainer.lua create mode 100644 training/trainer_goblin.lua create mode 100644 training/trainer_kobold.lua diff --git a/api.lua b/api.lua new file mode 100644 index 0000000..af57136 --- /dev/null +++ b/api.lua @@ -0,0 +1,76 @@ + +--------------------- +-- ADAPTIVE AI API -- +--------------------- + +local random = math.random + +local ai_pathfinding = adaptive_ai.ai_pathfinding +local ai_manager = adaptive_ai.ai_manager +local ai_on_step = adaptive_ai.ai_on_step + +adaptive_ai.breeding = {} + +function adaptive_ai:register_ai(name, def) + adaptive_ai.breeding[name] = { + asexual = def.asexual or false, + breed = def.breed + } + + if not def.do_custom then + if not def.ai_manager then + def.ai_manager = adaptive_ai.ai_manager + end + + def.do_custom = function(self, dtime) + --chat_log("do custom "..()) + if def.action_managers then + -- there's a manager function per action + if self.action then + --chat_log("action managers in action") + def.action_managers[self.action](self, dtime) + end + end + local skip = def.ai_manager(self, dtime) + if skip == false then return false end + ai_on_step(self, dtime) + return false + end + end + + mobs:register_mob(name, def) +end + +--[[ +def (adaptive_ai.api related): +asexual :: bool +breed :: function(parent) if asexual +breed :: function(mother, father) if not asexual +actions :: list of strings +action_managers :: list of functions (index corresponds with action index) +camp :: if there's a node the creature is bound to (like a spawner) + +Notes: +do_custom API functionality from mobs_redo is respected +States from mobs_redo are overwritten with internal adaptive_ai state management +-- stand (stationary idle) +-- wander (equivalent to mob_redo's idle + walk_chance) +-- move (towards or away from a target - with or without pathfinding) +Actions are custom mob states with a different name to avoid conflict with previous states +--]] + +function adaptive_ai:place_ai(def) + local name = def.name + --local setup = def.setup + local pos = def.pos + --local height = minetest.registered_entities[name].jump_height + + local obj = minetest.add_entity(pos, name) + local ent = obj and obj:get_luaentity() or nil + + if def.setup and ent then + def.setup(ent) + end + + return ent or nil +end diff --git a/api.md b/api.md new file mode 100644 index 0000000..b7bf6db --- /dev/null +++ b/api.md @@ -0,0 +1,595 @@ + +Adaptive AI API +=============== +--- +This is a simple platform for development and experimentation of simple adaptive +AI, in the form of a Minetest mod. This mod allows for the creation of mobs that +have some learning algorithm (like a genetic algorithm), as well as bring a +couple examples of learning mobs. + +I decided to build this on top of some of the Mob Redo API. I wanted to have a +custom on_step function and skip some parts of the original API, yet keeping a +few auxiliar functions like do_jump, but since most of those auxiliar functions +are defined as local, many were copied here. These are accesible in +`adaptive_ai.helper.mobs`. + +Registering Adaptive Mobs +------------------------- +--- +To register an adaptive mob there's the following function: + + adaptive_ai:register_ai(name, def) + +This function calls the `mobs:register_mob(name, def)` function from Mobs Redo, +so all the def parameters from Mobs Redo API work here. In addition, there are +the following parameters: + +- **asexual:** type bool. If true, the creature breeds asexually with only one +parent. If false, it breeds with 2 parents. +- **breed:** type `function(parent)` if asexual, or `function(mother, father)` +if not. +- **actions:** list of strings with the name of the possible mob actions. +- **action_managers:** list of `function(self, dtime)` that define the behavior +for each action of the corresponding index. *(More on this later)* +- **ai_manager (Optional):** type `function(self, dtime)` that is in charge of +checking the environment and deciding on the next `self.action` of the creature. +If it returns `false`, it skips the rest of the `on_step` functions (but not the +`action_managers`). +- **Camp (Optional):** if the mob is bound to a node (like a spawner, or a +"town square") this will be the default node if it is not provided in the func- +tion that spawns a new creature of this type. + +Adaptive Mob Behavior +--------------------- +--- +There is no default behavior. Anyone using this API is expected to implement +their own mob behavior, using at least `action` and `action_managers`. However, +there is a default behavior *manager*, and works this way: + +- At each game step, the manager will look at `self.action`, and then call a +specific function from the `action_managers` using the `self.action` value as an +index. This `self.action` of the creature should be set at the end of an +`action_managers` function if one wishes to change the next action taken. These +actions are the mob's "external" states. +- Then it will manage the movement behavior. For that, it uses a set of "inter- +nal" states. The current one is stored in `self.state` as one of `"stand"`, +`"wander"`, `"move"`; they replace Mobs Redo states. They do the following: +- - **Stand:** Equivalent to Mobs Redo `idle` state. The creature stands in +place, doing nothing. +- - **Wander:** Equivalent to Mobs Redo `walk` state. The creature walks aim- +lessly. +- - **Move:** The creature moves respectively to a target. This is used for +pathfinding, and can be used for either following or attacking. If +`self.target.dir` is `"towards"`, the creature will move towards the target. If +it's `"away"`, the creature will try to get away (equivalent to the running away +behavior of Mobs Redo). + +The value stored in `self.action` can be either an integer or a string, but it +must be the index used to access the corresponding `action_managers` function. + +If the pathfinding target is an object (a player or another entity), then it +must be stored in `self.target.obj`; else, the target position must be stored in +`self.target.pos`. And don't forget to store either `"towards"` or `"away"` in +`self.target.dir` whenever you do so. + +Custom Mob Behavior +------------------- +--- +If you wish to make your own behavior, you should implement a function with pa- +rameters `(self, dtime)` and save it in a value called `ai_manager` when regis- +tering the AI. This function should manage the different values of `self.state`, +as well as moving the environment checks to decide the next action from the +`action_managers` to this function. If using pathfinding and following a target +that is an object, it should also update `self.target.pos` with the position of +`self.target.obj` at the very beginning. + +The default `ai_manager` is available in `adaptive_ai.ai_manager` to be called +from your own `ai_manager` function in case you want the default behavior as +part of yours without needing to retype it all. Auxiliar functions like +`do_jump` (checks if it is necessary to jump) in `adaptive_ai.helper.mobs`; and +`ai_pathfinding` (copied and edited from `smart_mobs` in Mobs Redo, does path- +finding from creature to `self.target.pos` if `self.target.dir` is `"towards"`) +are available too. + +Like in Mobs Redo, returning false in your custom `ai_manager` will skip the +rest of the `on_step` auxiliar functions. By default, both `action_managers` and +`ai_manager` are used; if you wish not to do so, make a function called +`do_custom` instead, which will outright skip this API and be passed straight to +the Mobs Redo API. + +To Do +----- +--- +This API is incomplete; I have yet to port all of the features related to com- +bat, mob ownership, etc. In case you wish to use non-implemented Mobs Redo pa- +rameters, like `runaway_from`, you can use your own `ai_manager` that uses them +(together with the default `adaptive_ai.ai_manager`, if you want). + +I also want to make the adaptive AI examples to be optional in the future, maybe +through `minetest.conf` or making a separate mod that uses this API (like Mobs +Redo does with its animals and monsters). + +--- + +Bellow is a copy of the Mobs Redo API (omitting all the features that need to be +but have not yet been reimplemented, and editing the ones modified). + +--- +--- + +Mobs Redo API +============= +--- +Welcome to the world of mobs in minetest and hopefully an easy guide to defining +your own mobs and having them appear in your worlds. + + +Registering Mobs +---------------- +--- +To register a mob and have it ready for use requires the following function: + + mobs:register_mob(name, definition) + +The 'name' of a mob usually starts with the mod name it's running from followed +by it's own name e.g. + + "mobs_monster:sand_monster" or "mymod:totally_awesome_beast" + +... and the 'definition' is a table which holds all of the settings and +functions needed for the mob to work properly which contains the following: + + 'nametag' contains the name which is shown above mob. + 'hp_min' has the minimum health value the mob can spawn with. + 'hp_max' has the maximum health value the mob can spawn with. + 'armor' holds strength of mob, 100 is normal, lower is more powerful + and needs more hits and better weapons to kill. + 'walk_velocity' is the speed that your mob can walk around. + 'run_velocity' is the speed your mob can run with, usually when attacking. + 'walk_chance' has a 0-100 chance value your mob will walk from standing, + set to 0 for jumping mobs only. + 'jump' when true allows your mob to jump updwards. + 'jump_height' holds the height your mob can jump, 0 to disable jumping. + 'stepheight' height of a block that your mob can easily walk up onto, + defaults to 1.1. + 'fly' when true allows your mob to fly around instead of walking. + 'fly_in' holds the node name that the mob flies (or swims) around + in e.g. "air" or "default:water_source". + 'view_range' how many nodes in distance the mob can see. + 'damage' how many health points the mob does to a player or another + mob when melee attacking. + 'knock_back' when true has mobs falling backwards when hit, the greater + the damage the more they move back. + 'fear_height' is how high a cliff or edge has to be before the mob stops + walking, 0 to turn off height fear. + 'fall_speed' has the maximum speed the mob can fall at, default is -10. + 'fall_damage' when true causes falling to inflict damage. + 'water_damage' holds the damage per second infliced to mobs when standing in + water. + 'lava_damage' holds the damage per second inflicted to mobs when standing + in lava or fire. + 'light_damage' holds the damage per second inflicted to mobs when it's too + bright (above 13 light). + 'suffocation' when true causes mobs to suffocate inside solid blocks. + 'floats' when set to 1 mob will float in water, 0 has them sink. + 'follow' mobs follow player when holding any of the items which appear + on this table, the same items can be fed to a mob to tame or + breed e.g. {"farming:wheat", "default:apple"} + + 'reach' is how far the mob will stop from target in pathfinding. + 'blood_amount' contains the number of blood droplets to appear when + mob is hit. + 'blood_texture' has the texture name to use for droplets e.g. + "mobs_blood.png", or table {"blood1.png", "blood2.png"} + 'pathfinding' set to 1 for mobs to use pathfinder feature, set to 2 + so they can also build/break also (only works when + 'mobs_griefing' in minetest.conf is not false). + 'immune_to' is a table that holds specific damage when being hit by + certain items e.g. + {"default:sword_wood", 0} -- causes no damage. + {"default:gold_lump", -10} -- heals by 10 health points. + {"default:coal_block", 20} -- 20 damage when hit on head with coal blocks. + + 'makes_footstep_sound' when true you can hear mobs walking. + 'sounds' this is a table with sounds of the mob + 'distance' maximum distance sounds can be heard, default is 10. + 'random' random sound that plays during gameplay. + 'war_cry' special mob shout. + 'damage' sound heard when mob is hurt. + 'death' played when mob is killed. + 'jump' played when mob jumps. + 'fuse' sound played when mob explode timer starts. + 'explode' sound played when mob explodes. + + 'drops' table of items that are dropped when mob is killed, fields are: + 'name' name of item to drop. + 'chance' chance of drop, 1 for always, 2 for 1-in-2 chance etc. + 'min' minimum number of items dropped. + 'max' maximum number of items dropped. + + 'visual' holds the look of the mob you wish to create: + 'cube' looks like a normal node + 'sprite' sprite which looks same from all angles. + 'upright_sprite' flat model standing upright. + 'wielditem' how it looks when player holds it in hand. + 'mesh' uses separate object file to define mob. + 'visual_size' has the size of the mob, defaults to {x = 1, y = 1} + 'collisionbox' has the box in which mob can be interacted with the + world e.g. {-0.5, -0.5, -0.5, 0.5, 0.5, 0.5} + 'selectionbox' has the box in which player can interact with mob + 'textures' holds a table list of textures to be used for mob, or you + could use multiple lists inside another table for random + selection e.g. { {"texture1.png"}, {"texture2.png"} } + 'child_texture' holds the texture table for when baby mobs are used. + 'gotten_texture' holds the texture table for when self.gotten value is + true, used for milking cows or shearing sheep. + 'mesh' holds the name of the external object used for mob model + e.g. "mobs_cow.b3d" + 'gotten_mesh" holds the name of the external object used for when + self.gotten is true for mobs. + 'rotate' custom model rotation, 0 = front, 90 = side, 180 = back, + 270 = other side. + + 'double_melee_attack' when true has the api choose between 'punch' and + 'punch2' animations. + + 'animation' holds a table containing animation names and settings for use with mesh models: + 'stand_start' start frame for when mob stands still. + 'stand_end' end frame of stand animation. + 'stand_speed' speed of animation in frames per second. + 'walk_start' when mob is walking around. + 'walk_end' + 'walk_speed' + 'run_start' when a mob runs. + 'run_end' + 'run_speed' + 'fly_start' when a mob is flying. + 'fly_end' + 'fly_speed' + 'die_start' death animation + 'die_end' + 'die_speed' + 'die_loop' when set to false stops the animation looping. + + Using '_loop = false' setting will stop any of the above animations from + looping. + + 'speed_normal' is used for animation speed for compatibility with some + older mobs. + + +Node Replacement +---------------- +--- +Mobs can look around for specific nodes as they walk and replace them to mimic +eating. + + 'replace_what' group of items to replace e.g. + {"farming:wheat_8", "farming:carrot_8"} + or you can use the specific options of what, with and + y offset by using this instead: + { + {"group:grass", "air", 0}, + {"default:dirt_with_grass", "default:dirt", -1} + } + 'replace_with' replace with what e.g. "air" or in chickens case "mobs:egg" + 'replace_rate' how random should the replace rate be (typically 10) + 'replace_offset' +/- value to check specific node to replace + + 'on_replace(self, pos, oldnode, newnode)' is called when mob is about to + replace a node. + 'self' ObjectRef of mob + 'pos' Position of node to replace + 'oldnode' Current node + 'newnode' What the node will become after replacing + + If false is returned, the mob will not replace the node. + + By default, replacing sets self.gotten to true and resets the object + properties. + + +Custom Definition Functions +--------------------------- +--- +Along with the above mob registry settings we can also use custom functions to +enhance mob functionality and have them do many interesting things: + + 'on_die' a function that is called when the mob is killed the + parameters are (self, pos) + 'on_rightclick' its same as in minetest.register_entity() + 'on_blast' is called when an explosion happens near mob when using TNT + functions, parameters are (object, damage) and returns + (do_damage, do_knockback, drops) + 'on_spawn' is a custom function that runs on mob spawn with 'self' as + variable, return true at end of function to run only once. + 'after_activate' is a custom function that runs once mob has been activated + with these paramaters (self, staticdata, def, dtime) + 'on_breed' called when two similar mobs breed, paramaters are + (parent1, parent2) objects, return false to stop child from + being resized and owner/tamed flags and child textures being + applied. Function itself must spawn new child mob. + 'on_grown' is called when a child mob has grown up, only paramater is + (self). + 'do_punch' called when mob is punched with paramaters (self, hitter, + time_from_last_punch, tool_capabilities, direction), return + false to stop punch damage and knockback from taking place. + 'custom_attack' when set this function is called instead of the normal mob + melee attack, parameters are (self, to_attack). + 'on_die' a function that is called when mob is killed (self, pos) + 'do_custom' a custom function that is called every tick while mob is + active and which has access to all of the self.* variables + e.g. (self.health for health or self.standing_in for node + status), return with 'false' to skip remainder of mob API. + +Internal Variables +------------------ +--- +The mob api also has some preset variables and functions that it will remember +for each mob. + + 'self.health' contains current health of mob (cannot exceed + self.hp_max) + 'self.texture_list' contains list of all mob textures + 'self.child_texture' contains mob child texture when growing up + 'self.base_texture' contains current skin texture which was randomly + selected from textures list + 'self.gotten' this is used for obtaining milk from cow and wool from + sheep + 'self.horny' when animal fed enough it is set to true and animal can + breed with same animal + 'self.hornytimer' background timer that controls breeding functions and + mob childhood timings + 'self.child' used for when breeding animals have child, will use + child_texture and be half size + 'self.nametag' contains the name of the mob which it can show above + + +Spawning Mobs in World +---------------------- +--- + mobs:register_spawn(name, nodes, max_light, min_light, chance, + active_object_count, max_height, day_toggle) + + mobs:spawn_specfic(name, nodes, neighbors, min_light, max_light, interval, + chance, active_object_count, min_height, max_height, day_toggle, on_spawn) + +These functions register a spawn algorithm for the mob. Without this function +the call the mobs won't spawn. + + 'name' is the name of the animal/monster + 'nodes' is a list of nodenames on that the animal/monster can + spawn on top of + 'neighbors' is a list of nodenames on that the animal/monster will + spawn beside (default is {"air"} for + mobs:register_spawn) + 'max_light' is the maximum of light + 'min_light' is the minimum of light + 'interval' is same as in register_abm() (default is 30 for + mobs:register_spawn) + 'chance' is same as in register_abm() + 'active_object_count' number of this type of mob to spawn at one time inside + map area + 'min_height' is the minimum height the mob can spawn + 'max_height' is the maximum height the mob can spawn + 'day_toggle' true for day spawning, false for night or nil for + anytime + 'on_spawn' is a custom function which runs after mob has spawned + and gives self and pos values. + +A simpler way to handle mob spawns has been added with the mobs:spawn(def) +command which uses above names to make settings clearer: + + mobs:spawn({name = "mobs_monster:tree_monster", + nodes = {"group:leaves"}, + max_light = 7, + }) + +For each mob that spawns with this function is a field in `mobs.spawning_mobs`. +It tells if the mob should spawn or not. Default is true. So other mods can +only use the API of this mod by disabling the spawning of the default mobs in +this mod. + + + mobs:spawn_abm_check(pos, node, name) + +This global function can be changed to contain additional checks for mobs to +spawn e.g. mobs that spawn only in specific areas and the like. By returning +true the mob will not spawn. + + 'pos' holds the position of the spawning mob + 'node' contains the node the mob is spawning on top of + 'name' is the name of the animal/monster + +Spawn Eggs +---------- +--- + mobs:register_egg(name, description, background, addegg, no_creative) + +This function registers a spawn egg which can be used by admin to properly spawn in a mob. + + 'name' this is the name of your new mob to spawn e.g. "mob:sheep" + 'description' the name of the new egg you are creating e.g. "Spawn Sheep" + 'background' the texture displayed for the egg in inventory + 'addegg' would you like an egg image in front of your texture (1 = yes, + 0 = no) + 'no_creative' when set to true this stops spawn egg appearing in creative + mode for destructive mobs like Dungeon Masters. + +Explosion Function +------------------ +--- + mobs:explosion(pos, radius) -- DEPRECATED!!! use mobs:boom() instead +--- + mobs:boom(self, pos, radius) + 'self' mob entity + 'pos' centre position of explosion + 'radius' radius of explosion (typically set to 3) + +This function generates an explosion which removes nodes in a specific radius +and damages any entity caught inside the blast radius. Protection will limit +node destruction but not entity damage. + +Capturing Mobs +-------------- +--- + mobs:capture_mob(self, clicker, chance_hand, chance_net, chance_lasso, + force_take, replacewith) + +This function is generally called inside the `on_rightclick` section of the mob +api code, it provides a chance of capturing the mob by hand, using the net or +lasso items, and can also have the player take the mob by force if tamed and +replace with another item entirely. + + 'self' mob information + 'clicker' player information + 'chance_hand' chance of capturing mob by hand (1 to 100) 0 to disable + 'chance_net' chance of capturing mob using net (1 to 100) 0 to disable + 'chance_lasso' chance of capturing mob using magic lasso (1 to 100) 0 to + disable + 'force_take' take mob by force, even if tamed (true or false) + 'replacewith' once captured replace mob with this item instead (overrides + new mob eggs with saved information) + +Feeding and Taming/Breeding +--------------------------- +--- + mobs:feed_tame(self, clicker, feed_count, breed, tame) + +This function allows the mob to be fed the item inside `self.follow` be it apple, +wheat or whatever a set number of times and be tamed or bred as a result. +Will return true when mob is fed with item it likes. + + 'self' mob information + 'clicker' player information + 'feed_count' number of times mob must be fed to tame or breed + 'breed' true or false stating if mob can be bred and a child created + afterwards + 'tame' true or false stating if mob can be tamed so player can pick + them up + + +Protecting Mobs +--------------- +--- + mobs:protect(self, clicker) + +This function can be used to right-click any tamed mob with `mobs:protector` item, +this will protect the mob from harm inside of a protected area from other +players. Will return true when mob right-clicked with `mobs:protector` item. + + 'self' mob information + 'clicker' player information + + +Riding Mobs +----------- +--- +Mobs can be ridden by players and the following shows its functions and +usage: + + mobs:attach(self, player) + +This function attaches a player to the mob so it can be ridden. + + 'self' mob information + 'player' player information +--- + mobs:detach(player, offset) + +This function will detach the player currently riding a mob to an offset +position. + + 'player' player information + 'offset' position table containing offset values +--- + + mobs:drive(self, move_animation, stand_animation, can_fly, dtime) + +This function allows an attached player to move the mob around and animate it at +same time. + + 'self' mob information + 'move_animation' string containing movement animation e.g. "walk" + 'stand_animation' string containing standing animation e.g. "stand" + 'can_fly' if true then jump and sneak controls will allow mob to fly + up and down + 'dtime' tick time used inside drive function +--- + mobs:fly(self, dtime, speed, can_shoot, arrow_entity, move_animation, stand_animation) + +This function allows an attached player to fly the mob around using directional +controls. + + 'self' mob information + 'dtime' tick time used inside fly function + 'speed' speed of flight + 'can_shoot' true if mob can fire arrow (sneak and left mouse button + fires) + 'arrow_entity' name of arrow entity used for firing + 'move_animation' string containing name of pre-defined animation e.g. "walk" + or "fly" etc. + 'stand_animation' string containing name of pre-defined animation e.g. + "stand" or "blink" etc. + +Note: animation names above are from the pre-defined animation lists inside mob +registry without extensions. +--- + mobs:set_animation(self, name) + +This function sets the current animation for mob, defaulting to "stand" if not +found. + + 'self' mob information + 'name' name of animation + + +Certain variables need to be set before using the above functions: + + 'self.v2' toggle switch used to define below values for the + first time + 'self.max_speed_forward' max speed mob can move forward + 'self.max_speed_reverse' max speed mob can move backwards + 'self.accel' acceleration speed + 'self.terrain_type' integer containing terrain mob can walk on + (1 = water, 2 or 3 = land) + 'self.driver_attach_at' position offset for attaching player to mob + 'self.driver_eye_offset' position offset for attached player view + 'self.driver_scale' sets driver scale for mobs larger than {x=1, y=1} + + +External Settings for "minetest.conf" +------------------------------------ +--- + 'enable_damage' if true monsters will attack players (default is true) + 'only_peaceful_mobs' if true only animals will spawn in game (default is + false) + 'mobs_disable_blood' if false blood effects appear when mob is hit (default + is false) + 'mobs_spawn_protected' if set to false then mobs will not spawn in protected + areas (default is true) + 'remove_far_mobs' if true then untamed mobs that are outside players + visual range will be removed (default is true) + 'mobname' can change specific mob chance rate (0 to disable) and + spawn number e.g. mobs_animal:cow = 1000,5 + 'mob_difficulty' sets difficulty level (health and hit damage + multiplied by this number), defaults to 1.0. + 'mob_show_health' if false then punching mob will not show health status + (true by default) + 'mob_chance_multiplier' multiplies chance of all mobs spawning and can be set + to 0.5 to have mobs spawn more or 2.0 to spawn less. + e.g. 1 in 7000 * 0.5 = 1 in 3500 so better odds of + spawning. + 'mobs_spawn' if false then mobs no longer spawn without spawner or + spawn egg. + 'mobs_drop_items' when false mobs no longer drop items when they die. + 'mobs_griefing' when false mobs cannot break blocks when using either + pathfinding level 2, replace functions or mobs:boom + function. + +Players can override the spawn chance for each mob registered by adding a line +to their minetest.conf file with a new value, the lower the value the more each +mob will spawn e.g. + + mobs_animal:sheep_chance 11000 + mobs_monster:sand_monster_chance 100 diff --git a/api_mobs.lua b/api_mobs.lua new file mode 100644 index 0000000..917898b --- /dev/null +++ b/api_mobs.lua @@ -0,0 +1,476 @@ + +-- Imported and modified API functions from Mobs_Redo +-- WIP; pathfinding is not functional + +-- Localize math functions +local pi = math.pi +local square = math.sqrt +local sin = math.sin +local cos = math.cos +local abs = math.abs +local min = math.min +local max = math.max +local atann = math.atan +local random = math.random +local floor = math.floor +local atan = function(x) + if not x or x ~= x then + --error("atan bassed NaN") + return 0 + else + return atann(x) + end +end + +local helper = adaptive_ai.helper +local set_yaw = helper.mobs.set_yaw +local do_jump = helper.mobs.do_jump +local set_velocity = helper.mobs.set_velocity +local is_at_cliff = helper.mobs.is_at_cliff +local flight_check = helper.mobs.flight_check +local get_distance = helper.mobs.get_distance +local mob_sound = helper.mobs.mob_sound +local do_env_damage = helper.mobs.do_env_damage +local replace = helper.mobs.replace +local flop = helper.mobs.flop +local runaway_from = helper.mobs.runaway_from +local set_animation = helper.mobs.set_animation + +local mobs_griefing = helper.mobs.mobs_griefing +local los_switcher = false +local height_switcher = false + +local chat_log = adaptive_ai.chat_log + +-- Edited from mob_redo's smart_mobs pathfinding function +-- path finding and smart mob routine by rnd, line_of_sight and other edits by Elkien3 +local ai_pathfinding = function(self, s, p, dist, dtime) + chat_log("pathfinding tete") + local s1 = self.path.lastpos + local target_pos = self.target.pos + + -- is it becoming stuck? + if abs(s1.x - s.x) + abs(s1.z - s.z) < .5 then + self.path.stuck_timer = self.path.stuck_timer + dtime + else + self.path.stuck_timer = 0 + end + + self.path.lastpos = {x = s.x, y = s.y, z = s.z} + + local use_pathfind = false + local has_lineofsight = minetest.line_of_sight( + {x = s.x, y = (s.y) + .5, z = s.z}, + {x = target_pos.x, y = (target_pos.y) + 1.5, z = target_pos.z}, + .2) + + -- im stuck, search for path + if not has_lineofsight then + if los_switcher == true then + use_pathfind = true + los_switcher = false + end -- cannot see target! + else + if los_switcher == false then + los_switcher = true + use_pathfind = false + + minetest.after(1, function(self) + if has_lineofsight then self.path.following = false end + end, self) + end -- can see target! + end + + if (self.path.stuck_timer > stuck_timeout and not self.path.following) then + use_pathfind = true + self.path.stuck_timer = 0 + + minetest.after(1, function(self) + if has_lineofsight then self.path.following = false end + end, self) + end + + if (self.path.stuck_timer > stuck_path_timeout and self.path.following) then + use_pathfind = true + self.path.stuck_timer = 0 + + minetest.after(1, function(self) + if has_lineofsight then self.path.following = false end + end, self) + end + + if abs(vector.subtract(s,target_pos).y) > self.stepheight then + if height_switcher then + use_pathfind = true + height_switcher = false + end + else + if not height_switcher then + use_pathfind = false + height_switcher = true + end + end + + if use_pathfind then + -- lets try find a path, first take care of positions + -- since pathfinder is very sensitive + local sheight = self.collisionbox[5] - self.collisionbox[2] + + -- round position to center of node to avoid stuck in walls + -- also adjust height for player models! + s.x = floor(s.x + 0.5) + -- s.y = floor(s.y + 0.5) - sheight + s.z = floor(s.z + 0.5) + + local ssight, sground = minetest.line_of_sight(s, {x = s.x, y = s.y - 4, z = s.z}, 1) + + -- determine node above ground + if not ssight then + s.y = sground.y + 1 + end + + local p1 = self.target.pos + p1.x, p1.y, p1.z = floor(p1.x + 0.5), floor(p1.y + 0.5), floor(p1.z + 0.5) + + local dropheight = 6 + if self.fear_height ~= 0 then dropheight = self.fear_height end + + self.path.way = minetest.find_path(s, p1, self.view_range, self.stepheight, dropheight, "A*") + self.state = "move" + self.target.dir = "towards" + + -- no path found, try something else + if not self.path.way then + self.path.following = false + + -- lets make way by digging/building if not accessible + if self.pathfinding == 2 and mobs_griefing then + + -- is player higher than mob? + if s.y < p1.y then + + -- build upwards + if not minetest.is_protected(s, "") then + local ndef1 = minetest.registered_nodes[self.standing_in] + + if ndef1 and (ndef1.buildable_to or ndef1.groups.liquid) then + minetest.set_node(s, {name = mobs.fallback_node}) + end + end + + local sheight = math.ceil(self.collisionbox[5]) + 1 + + -- assume mob is 2 blocks high so it digs above its head + s.y = s.y + sheight + + -- remove one block above to make room to jump + if not minetest.is_protected(s, "") then + local node1 = node_ok(s, "air").name + local ndef1 = minetest.registered_nodes[node1] + + if node1 ~= "air" + and node1 ~= "ignore" + and ndef1 + and not ndef1.groups.level + and not ndef1.groups.unbreakable + and not ndef1.groups.liquid then + + minetest.set_node(s, {name = "air"}) + minetest.add_item(s, ItemStack(node1)) + end + end + + s.y = s.y - sheight + self.object:setpos({x = s.x, y = s.y + 2, z = s.z}) + + else -- dig 2 blocks to make door toward player direction + local yaw1 = self.object:get_yaw() + pi / 2 + local p1 = {x = s.x + cos(yaw1), y = s.y, z = s.z + sin(yaw1)} + + if not minetest.is_protected(p1, "") then + local node1 = node_ok(p1, "air").name + local ndef1 = minetest.registered_nodes[node1] + + if node1 ~= "air" + and node1 ~= "ignore" + and ndef1 + and not ndef1.groups.level + and not ndef1.groups.unbreakable + and not ndef1.groups.liquid then + + minetest.add_item(p1, ItemStack(node1)) + minetest.set_node(p1, {name = "air"}) + end + + p1.y = p1.y + 1 + node1 = node_ok(p1, "air").name + ndef1 = minetest.registered_nodes[node1] + + if node1 ~= "air" + and node1 ~= "ignore" + and ndef1 + and not ndef1.groups.level + and not ndef1.groups.unbreakable + and not ndef1.groups.liquid then + + minetest.add_item(p1, ItemStack(node1)) + minetest.set_node(p1, {name = "air"}) + end + end + end + end + + -- will try again in 2 second + self.path.stuck_timer = stuck_timeout - 2 + + -- frustration! cant find the damn path :( + mob_sound(self, self.sounds.random) + else + -- yay I found path + mob_sound(self, self.sounds.war_cry) + set_velocity(self, self.walk_velocity) + + -- follow path now that I have it + self.path.following = true + end + end +end + +-- Pieced together from mob_redo's state management functions +-- self.target.pos +-- self.target.dir = "towards" or "away" +local ai_manager = function(self, dtime) + if self.action_managers then + -- there's a manager function per action + if self.action then + --chat_log("action managers in action") + self.action_managers[self.action](self, dtime) + end + end + + --chat_log(self.state == "manager on") + if self.target and self.target.obj then + self.target.pos = self.target.obj:get_pos() + end + local yaw = self.object:get_yaw() or 0 + local s = self.object:get_pos() + local lp = nil + + if self.state == "stand" then + --chat_log("I am idle standing") + if random(1, 4) == 1 then + local objs = minetest.get_objects_inside_radius(s, 3) + for n = 1, #objs do + if objs[n]:is_player() then + lp = objs[n]:get_pos() + break + end + end + + -- look at any players nearby, otherwise turn randomly + if lp then + local vec = {x = lp.x - s.x, z = lp.z - s.z} + yaw = (atan(vec.z / vec.x) + pi / 2) - self.rotate + if lp.x > s.x then yaw = yaw + pi end + else + yaw = yaw + random(-0.5, 0.5) + end + + yaw = set_yaw(self, yaw, 8) + end + + if self.walk_chance ~= 0 + and self.facing_fence ~= true + and random(1, 100) <= self.walk_chance + and is_at_cliff(self) == false then + + set_velocity(self, self.walk_velocity) + self.state = "wander" + set_animation(self, "walk") + else + set_velocity(self, 0) + set_animation(self, "stand") + end + elseif self.state == "wander" then + --chat_log("I am idle wandering") + -- is there something I need to avoid? + local avoid = {} + if self.water_damage > 0 then table.insert(avoid, "group:water") end + if self.lava_damage > 0 then table.insert(avoid, "group:lava") end + + if #avoid > 0 then lp = minetest.find_node_near(s, 1, avoid) end + + if lp then + -- if mob in water or lava then look for land + if (self.lava_damage and minetest.registered_nodes[self.standing_in].groups.lava) + or (self.water_damage and minetest.registered_nodes[self.standing_in].groups.water) then + lp = minetest.find_node_near(s, 5, {"group:soil", "group:stone", "group:sand", node_ice, node_snowblock}) + + -- did we find land? + if lp then + local vec = {x = lp.x - s.x, z = lp.z - s.z} + yaw = (atan(vec.z / vec.x) + pi / 2) - self.rotate + + if lp.x > s.x then yaw = yaw + pi end + + -- look towards land and jump/move in that direction + yaw = set_yaw(self, yaw, 6) + do_jump(self) + set_velocity(self, self.walk_velocity) + else + yaw = yaw + random(-0.5, 0.5) + end + + else + local vec = {x = lp.x - s.x, z = lp.z - s.z} + yaw = (atan(vec.z / vec.x) + pi / 2) - self.rotate + if lp.x > s.x then yaw = yaw + pi end + end + + yaw = set_yaw(self, yaw, 8) + + -- otherwise randomly turn + elseif random(1, 100) <= 30 then + yaw = yaw + random(-0.5, 0.5) + yaw = set_yaw(self, yaw, 8) + end + + -- stand for great fall in front + local temp_is_cliff = is_at_cliff(self) + + if self.facing_fence == true or temp_is_cliff or random(1, 100) <= 30 then + set_velocity(self, 0) + self.state = "stand" + set_animation(self, "stand") + else + set_velocity(self, self.walk_velocity) + + if flight_check(self) + and self.animation + and self.animation.fly_start and self.animation.fly_end then + set_animation(self, "fly") + else + set_animation(self, "walk") + end + end + elseif self.state == "runaway" then + --chat_log("I am moving away") + self.runaway_timer = self.runaway_timer + 1 + + -- stop after 5 seconds or when at cliff + if self.runaway_timer > 5 + or is_at_cliff(self) then + self.runaway_timer = 0 + set_velocity(self, 0) + self.state = "stand" + set_animation(self, "stand") + self.runaway_timer = 0 + else + set_velocity(self, self.run_velocity) + set_animation(self, "walk") + end + elseif self.state == "move" then + -- is there something I need to avoid? + if self.water_damage > 0 + and self.lava_damage > 0 then + lp = minetest.find_node_near(s, 1, {"group:water", "group:lava"}) + elseif self.water_damage > 0 then + lp = minetest.find_node_near(s, 1, {"group:water"}) + elseif self.lava_damage > 0 then + lp = minetest.find_node_near(s, 1, {"group:lava"}) + end + + if lp then + -- if mob in water or lava then look for land + if (self.lava_damage + and minetest.registered_nodes[self.standing_in].groups.lava) + or (self.water_damage + and minetest.registered_nodes[self.standing_in].groups.water) then + + lp = minetest.find_node_near(s, 5, {"group:soil", "group:stone", + "group:sand", node_ice, node_snowblock}) + + -- did we find land? + if lp then + local vec = { + x = lp.x - s.x, + z = lp.z - s.z + } + yaw = (atan(vec.z / vec.x) + pi / 2) - self.rotate + + if lp.x > s.x then yaw = yaw + pi end + + -- look towards land and jump/move in that direction + yaw = set_yaw(self, yaw, 6) + local jumped = do_jump(self) + if jumped then chat_log("Jumping while moving") end + set_velocity(self, self.walk_velocity) + else + yaw = yaw + random(-0.5, 0.5) + end + else + local vec = { + x = lp.x - s.x, + z = lp.z - s.z + } + + yaw = (atan(vec.z / vec.x) + pi / 2) - self.rotate + + if lp.x > s.x then yaw = yaw + pi end + end + + yaw = set_yaw(self, yaw, 8) + end + + -- stand for great fall in front + local temp_is_cliff = is_at_cliff(self) + + if self.facing_fence == true + or temp_is_cliff then + set_velocity(self, 0) + self.state = "stand" + set_animation(self, "stand") + else + set_velocity(self, self.walk_velocity) + + if flight_check(self) + and self.animation + and self.animation.fly_start + and self.animation.fly_end then + set_animation(self, "fly") + else + set_animation(self, "walk") + end + end + end +-- end ai_manager +end + +local ai_on_step = function(self, dtime) + -- To do: add rest of mob_redo functionality, like: + -- Attacking + -- Follow + -- Etc + -- mob plays random sound at times + if random(1, 100) == 1 then + mob_sound(self, self.sounds.random) + end + + -- environmental damage timer (every 1 second) + self.env_damage_timer = self.env_damage_timer + dtime + + if (self.env_damage_timer > 1) then + self.env_damage_timer = 0 + -- check for environmental damage (water, fire, lava etc.) + do_env_damage(self) + -- node replace check (cow eats grass etc.) + replace(self, pos) + end + --flop(self) + do_jump(self) + runaway_from(self) +end + +adaptive_ai.ai_pathfinding = ai_pathfinding +adaptive_ai.ai_manager = ai_manager +adaptive_ai.ai_on_step = ai_on_step diff --git a/depends.txt b/depends.txt new file mode 100644 index 0000000..cc03398 --- /dev/null +++ b/depends.txt @@ -0,0 +1,2 @@ +default +mobs diff --git a/goblin/goblin.lua b/goblin/goblin.lua new file mode 100644 index 0000000..fa89f3b --- /dev/null +++ b/goblin/goblin.lua @@ -0,0 +1,375 @@ + +-- 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 diff --git a/goblin/goblin_integration.lua b/goblin/goblin_integration.lua new file mode 100644 index 0000000..0467e2c --- /dev/null +++ b/goblin/goblin_integration.lua @@ -0,0 +1,514 @@ + +dofile(minetest.get_modpath("adaptive_ai").."/kobold/kobold.lua") + +local kobolds = adaptive_ai.kobolds +local stats = kobolds.stats +local helper = adaptive_ai.helper +local calc = adaptive_ai.calc +local population = adaptive_ai.population + +local random = math.random +local floor = math.floor + +local chat_log = adaptive_ai.chat_log +local ai_on_step = adaptive_ai.ai_on_step + +local sort_nils_out = helper.sort_nils_out +local get_surface = helper.search_surface +local pop = helper.pop +local shuffle = helper.shuffle +local distance = calc.dist_euclidean + +local kobold_from_lines = kobolds.from_lines +local kobold_score = kobolds.score +local kobold_decide = kobolds.decide +local kobold_create = kobolds.create + +local get_from_census = population.get_from_census +local create_tribe = population.create_tribe +local add_to_tribe = population.add_to_tribe +local empty_population = population.empty + +local do_jump = helper.mobs.do_jump +local set_velocity = helper.mobs.set_velocity +local is_at_cliff = helper.mobs.is_at_cliff +local set_yaw = helper.mobs.set_yaw +local flight_check = helper.mobs.flight_check +local set_animation = helper.mobs.set_animation + +local adjacent = helper.adjacent +local file_tag = kobolds.file_tag +local popul_path = population.path +local train_path = adaptive_ai.modpath.."/training/" + +local pi = math.pi +local actions = kobolds.actions +local yaws = { + 0, pi, 3*pi/2, pi/2, -- North, South, East, West + 7*pi/4, 5*pi/4, pi/4, 3*pi/4 -- NE, SE, NW, SW +} +local food_nodes = {"adaptive_ai:moondrum_mushroom"} +local founders = nil +local min_founders = 4 +local timer = 0 + +-- Out of Mobs_Redo +-- Equal to do_states(self, dtime) for self.state == "walk" +-- except without turning to random directions when safe +local function state_move(self, dtime) + local yaw = self.object:get_yaw() or 0 + local s = self.object:get_pos() + local lp = nil + + -- is there something I need to avoid? + if self.water_damage > 0 + and self.lava_damage > 0 then + lp = minetest.find_node_near(s, 1, {"group:water", "group:lava"}) + elseif self.water_damage > 0 then + lp = minetest.find_node_near(s, 1, {"group:water"}) + elseif self.lava_damage > 0 then + lp = minetest.find_node_near(s, 1, {"group:lava"}) + end + + if lp then + -- if mob in water or lava then look for land + if (self.lava_damage + and minetest.registered_nodes[self.standing_in].groups.lava) + or (self.water_damage + and minetest.registered_nodes[self.standing_in].groups.water) then + + lp = minetest.find_node_near(s, 5, {"group:soil", "group:stone", + "group:sand", node_ice, node_snowblock}) + + -- did we find land? + if lp then + local vec = { + x = lp.x - s.x, + z = lp.z - s.z + } + yaw = (atan(vec.z / vec.x) + pi / 2) - self.rotate + + if lp.x > s.x then yaw = yaw + pi end + + -- look towards land and jump/move in that direction + yaw = set_yaw(self, yaw, 6) + local jumped = do_jump(self) + if jumped then chat_log("Jumping while moving") end + set_velocity(self, self.walk_velocity) + else + yaw = yaw + random(-0.5, 0.5) + end + else + local vec = { + x = lp.x - s.x, + z = lp.z - s.z + } + + yaw = (atan(vec.z / vec.x) + pi / 2) - self.rotate + + if lp.x > s.x then yaw = yaw + pi end + end + + yaw = set_yaw(self, yaw, 8) + end + + -- stand for great fall in front + local temp_is_cliff = is_at_cliff(self) + + if self.facing_fence == true + or temp_is_cliff then + set_velocity(self, 0) + self.state = "stand" + set_animation(self, "stand") + else + set_velocity(self, self.walk_velocity) + + if flight_check(self) + and self.animation + and self.animation.fly_start + and self.animation.fly_end then + set_animation(self, "fly") + else + set_animation(self, "walk") + end + end +end + +local function move_creature(self, yaw_id) + if self.state == "stand" or self.state == "walk" or self.state == "move" then + self.object:set_yaw(yaws[yaw_id]) + set_velocity(self, self.walk_velocity) + self.state = "move" + end + return false +end + +local function load_founders() + local file = io.open(train_path..kobolds.trained_tag..".txt", "r") + local counter = 0 + + if file then + founders = {} + local reading, kobold = false, {} + for line in file:lines() do + counter = counter + 1 + if not reading then + reading = line == file_tag[1] + if reading then + kobold = {} + table.insert(kobold,line) + end + else + reading = line ~= file_tag[2] + table.insert(kobold,line) + if not reading then + kobold = kobold_from_lines(kobold) + kobold = kobold_create(kobold) + table.insert(founders, kobold) + chat_log("Founder loaded: "..kobold.name) + end + end + end + file:close() + end + if #founders == 0 then + --error("Founders not found") + end +end + +load_founders() + +local function get_founders(n) + chat_log(#founders.." founders") + founders = founders or {} + local f + while #founders < min_founders do + f = {} + kobold_create(f) + table.insert(founders, f) + end + + if #founders > min_founders then + n = n or min_founders + local tribe_founders = {} + shuffle(founders) + for i=1,n do + table.insert(tribe_founders, founders[i]) + end + + return tribe_founders + else + return founders + end +end + +local function load_kobold(self) + chat_log("Loading from census "..(self.nametag and "true" or "false")) + local old_self = get_from_census(self) + if old_self then + self.kobold = old_self + if not old_self.genes then error("No genes") end + chat_log(old_self.name.." loaded "..#old_self.genes.." genes") + self.timer = self.walk_velocity + return true + else + --self.kobold = nil + --chat_log("Nil kobold") + return false + end +end + +local function set_kobold(self, kobold) + self.kobold = kobold + self.nametag = kobold.name +end + +local function setup_kobold(self, mother, father) + if load_kobold(self) then + return + else + local kobold = {} + kobold_create(kobold, mother, father) + set_kobold(self, kobold) + end + return false +end + +local function breed_kobold(self) + local mother = self.kobold + if not mother.tribe_id then + --error("Breeding with no tribe.") + return + end + + local tribe_id = mother.tribe_id + local tribe = population.tribes[tribe_id] + if tribe then + local popul_size = tribe:popul_size() + + if popul_size < tribe.size * 2 then + local p = mother.pos + p = {x=(p.x), y=(p.y), z=(p.z)} + p.x = p.x + (random(0,1) - 0.5)*2 + p.z = p.z + (random(0,1) - 0.5)*2 + + local father = get_couple(tribe_id, self.kobold) + local child = place_kobold(p) + setup_kobold(child, mother, father) + + child = child.kobold + child.tribe_id = tribe_id + population.add_to_tribe(tribe_id, child) + return true + end + end + return false +end + +kobolds.breed_kobold = breed_kobold + +local kobold_decisions = { + function(self, dtime) -- idle + set_velocity(self, 0) + end, + function(self, dtime) -- wander + local n = random(1,#yaws) + local move = move_creature(self, n) + local kobold = self.kobold + if move then + kobold.hunger = kobold.hunger - 1 + self.state = "walk" + end + end, + function(self, dtime) -- eat + local kobold = self.kobold + if kobold.food and #kobold.food > 0 then + local satiate = pop(kobold.food) + kobold.hunger = kobold.hunger + satiate + kobold.hp = self.object:get_hp() + 1 + self.object:set_hp(kobold.hp) + end + end, + function(self, dtime) -- breed + local kobold = self.kobold + kobold.fitness = kobold.fitness + 2 + kobold.hunger = kobold.hunger - 10 + + kobolds.breed_kobold(self) + end, + function(self, dtime) -- move North + local move = move_creature(self, 1) + self.state = move and "move" or self.state + end, + function(self, dtime) -- move South + local move = move_creature(self, 2) + self.state = move and "move" or self.state + end, + function(self, dtime) -- move East + local move = move_creature(self, 3) + self.state = move and "move" or self.state + end, + function(self, dtime) -- move West + local move = move_creature(self, 4) + self.state = move and "move" or self.state + end, + function(self, dtime) -- move NE + local move = move_creature(self, 5) + self.state = move and "move" or self.state + end, + function(self, dtime) -- move SE + local move = move_creature(self, 6) + self.state = move and "move" or self.state + end, + function(self, dtime) -- move NW + local move = move_creature(self, 7) + self.state = move and "move" or self.state + end, + function(self, dtime) -- move SW + local move = move_creature(self, 8) + self.state = move and "move" or self.state + end, +} + +local manage_kobold = function(self, dtime) + local kobold = self.kobold + if kobold then + if kobold.delete or kobold.hp <= 0 then + self.object:remove() + return + end + kobold.hp = self.object:get_hp() + self.timer = self.timer + dtime + + if kobold.hp > 0 then + local p = self.object:get_pos() + p = {x=floor(p.x+0.5), y=floor(p.y+0.5), z=floor(p.z+0.5)} + kobold.pos = p + + local food_pos = minetest.find_node_near(self.object:get_pos(), self.reach, food_nodes) + + if food_pos then + --chat_log("Pocketing food from "..food_pos.x..","..food_pos.z) + if kobold.closest_food + and kobold.closest_food.x == food_pos.x + and kobold.closest_food.z == food_pos.z then + kobold.closest_food = nil + end + table.insert(kobold.food, random(2,5)) + minetest.set_node(food_pos, {name = "air"}) + end + + food_pos = minetest.find_node_near(self.object:get_pos(), self.view_range, food_nodes) + if food_pos then + --chat_log("Smelling good food") + food_pos = {x=floor(food_pos.x+0.5),y=floor(food_pos.y+0.5),z=floor(food_pos.z+0.5)} + if food_pos and (not kobold.closest_food + or distance(p, food_pos) < distance(p, kobold.closest_food)) then + kobold.closest_food = food_pos + end + end + + if not kobold.action or self.timer >= self.walk_velocity then + self.timer = 0 + --chat_log("Time to decide for "..kobold.name) + kobold.hunger = kobold.hunger - dtime + local neighbors = {} + + local pos, pos2 = {} + for i=1,#adjacent do + pos.x = p.x + adjacent[i].x + pos.y = p.y + pos.z = p.z + adjacent[i].z + pos2 = get_surface(pos) + neighbors[i] = pos2 and {x=pos2.x,y=pos2.y,z=pos2.z} or {x=pos.x, y=self.kobold.climb + 1,z=pos.z} + end + + chat_log("Closest food "..(kobold.closest_food and kobold.closest_food.x-p.x..","..kobold.closest_food.z-p.z or "none")) + + local decision, d_id = kobold_decide(kobold, neighbors) + chat_log(kobold.name.." decides "..(decision and actions[decision] or "nothing").." -- "..(d_id or "error")) + kobold.action = decision + if decision then + kobold_decisions[decision](self, dtime) + else + if not kobold.action then + kobold.action = 1 + kobold_decisions[1](self, dtime) + end + end + kobold.fitness = kobold.fitness + kobold_score(kobold) + end + + if kobold.hunger < 0 then + kobold.hunger = 0 + kobold.hp = self.object:get_hp() - 1 + self.object:set_hp(kobold.hp) + end + end + else + chat_log("No kobold") + end +end + +local function custom_on_step(self, dtime) + manage_kobold(self, dtime) + + if self.state == "move" then + state_move(self, dtime) + end + do_jump(self, dtime) + --ai_on_step(self, dtime) + return false +end + +local function place_kobold(p) + local ent = adaptive_ai:place_ai({ + name = "adaptive_ai:kobold", + pos = p, + }) + + return ent +end + +local function place_tribe(p) + local roots = {x=p.x, y=p.y-2, z=p.z} + local ents = {} + + local popul = create_tribe(p, "adaptive_ai:kobold", get_founders(), kobolds.stats.view_range, kobolds.clone, 5) + local ent, pos, c + c = 0 + + for i,k in ipairs(popul) do + pos = get_surface(k.pos) + ent = place_kobold(pos) + set_kobold(ent, k) + --chat_log("Placing "..ent.nametag.."!") + ent.kobold.pos = pos + end + + chat_log("Placed tribe!") + minetest.set_node(roots, {name = "adaptive_ai:moondrum_roots"}) +end + +adaptive_ai:register_ai("adaptive_ai:kobold", { + stepheight = 0.8, + hp_min = kobolds.stats.hp_max, + hp_max = kobolds.stats.hp_max, + armor = 100, + walk_velocity = 1.5, + run_velocity = 2, + jump = true, + jump_height = kobolds.stats.jump, + view_range = kobolds.stats.view_range, + damage = 2, + knock_back = true, + fear_height = kobolds.stats.fear_height, + floats = 1, + reach = 1, + attack_type = "dogfight", + makes_footstep_sound = true, + visual = "cube", + visual_size = {x=0.8, y=1.6}, + textures = { + "aa_imp_top.png", + "aa_imp_top.png", + "aa_imp_side.png", + "aa_imp_side.png", + "aa_imp_face.png", + "aa_imp_side.png", + }, + collisionbox = {-0.5,-0.8,-0.5, 0.5,0.8,0.5}, + walk_chance = 1, + + actions = kobolds.actions, + action_managers = kobold_decisions, + on_spawn = load_kobold, + do_custom = custom_on_step, + + asexual = false, + breed = kobold_create, +}) + +minetest.register_chatcommand("kobold", { + func = function(name, param) + local player = minetest.get_player_by_name(name) + local pos = player and player:getpos() or nil + if pos then + place_tribe(pos) + end + end +}) + +minetest.register_chatcommand("empty_camps", { + func = function(name, param) + empty_population() + end +}) + +minetest.register_chatcommand("founders", { + func = function(name, param) + for i,f in ipairs(founders) do + chat_log("Founder "..f.name) + end + end +}) diff --git a/helper.lua b/helper.lua new file mode 100644 index 0000000..d91b755 --- /dev/null +++ b/helper.lua @@ -0,0 +1,135 @@ + +--adaptive_ai.chat_log = function() end + +-- Localize math functions +local floor = math.floor +local ceil = math.ceil +local sqrt = math.sqrt +local random = math.random + +-- Helper calculation functions +local calc = {} + +calc.sum = function(vector) + local total = 0 + for i = 1, #vector do + total = total + vector[i] + end + return total +end + +calc.round = function(x) + if x>=0 then + if (x - floor(x)) >= 0.5 then + return ceil(x) + else + return floor(x) + end + else + if (ceil(x) - x) >= 0.5 then + return floor(x) + else + return ceil(x) + end + end +end + +calc.normalize = function(vector) + local total = calc.sum(vector) + + for i = 1, #vector do + vector[i] = vector[i]/total + end +end + +calc.dist_manhattan = function(pos1, pos2) + local dist = abs(pos1.x - pos2.x) + abs(pos1.y - pos2.y) + abs(pos1.z - pos2.z) + return dist +end + +calc.dist_euclidean = function(a, b) + local x, y, z = a.x - b.x, a.y - b.y, a.z - b.z + return sqrt(x * x + y * y + z * z) +end + +calc.average = function(t, to_num) + local avg, cnt = 0, 0 + for _,v in ipairs(t) do + if to_num then + avg = avg + to_num(v) + else + avg = avg + v + end + cnt = cnt + 1 + end + avg = avg/cnt + return avg +end + +calc.truncate = function(n) + return floor(n*100 + 0.5)/100 +end + +adaptive_ai.calc = calc + +-- Helper generic functions +local helper = {} + +helper.adjacent = { + {x=-1, z=-1}, + {x=-1, z= 0}, + {x=-1, z= 1}, + {x= 0, z=-1}, + --{x= 0, z= 0}, -- Center + {x= 0, z= 1}, + {x= 1, z=-1}, + {x= 1, z= 0}, + {x= 1, z= 1}, +} + +helper.sort_nils_out = function(a, b) + if not a then return false end + return true +end + +helper.compare_pos = function(p1, p2) + return p1.x == p2.x and p1.y == p2.y and p1.z == p2.z +end + +helper.pop = function(t, i) + if #t <= 0 then return nil end + local i = i or 1 + local res = t[i] + for i=i,#t do + t[i] = t[i+1] + end + return res +end + +helper.shuffle = function(t, n) + n = n or #t + for i = 1, n+1 do + local j = random(n) + local k = random(n) + t[j], t[k] = t[k], t[j] + end +end + +helper.load_file = function(filename, decompressing) + local file = io.open(filename, "r") + if file then + local t = file:read("*all") + file:close() + if decompressing then + t = minetest.decompress(t) + end + + t = minetest.deserialize(t) + return t + elseif not filename:find("tribe") then + error(filename) + end +end + +--helper.dist_euclidean = get_distance +adaptive_ai.helper = helper diff --git a/helper_mobs.lua b/helper_mobs.lua new file mode 100644 index 0000000..c3b25e8 --- /dev/null +++ b/helper_mobs.lua @@ -0,0 +1,954 @@ + +local helper = adaptive_ai.helper + +local chat_log = function(s) + minetest.chat_send_player("singleplayer", s) +end + +adaptive_ai.chat_log = chat_log + +local function get_node_group(name, group) + if type(group) == "table" then + local check = false + for _,g in ipairs(group) do + check = check or get_node_group(name, g) + end + return check + else + if not minetest.registered_nodes[name] or not minetest.registered_nodes[name].groups[group] then + return nil + end + return minetest.registered_nodes[name].groups[group] + end +end + +local function is_airlike(name, airlike_groups) + airlike_groups = airlike_groups or {} + if name == "air" then return true end + + return get_node_group(name, airlike_groups) +end + +local function search_surface(pos, airlike_groups) + local top = minetest.get_node_or_nil(pos) + if not top then return nil end + + local top_pos = {x=pos.x, y=pos.y, z=pos.z} + top = minetest.get_node_or_nil(top_pos) + --chat_log("Found "..top.name.."...") + + if not is_airlike(top.name) then + repeat + top_pos.y = top_pos.y + 1 + top = minetest.get_node_or_nil(top_pos) + --chat_log("Going up - found "..top.name.."...") + until not top or is_airlike(top.name) + top_pos = not top and nil or top_pos + else + repeat + top_pos.y = top_pos.y - 1 + top = minetest.get_node(top_pos) + --chat_log("Going down - found "..top.name.."...") + until not top or not is_airlike(top.name) + top_pos = not top and nil or top_pos + + top_pos.y = top_pos.y + 1 + end + --chat_log("Found!") + + return top_pos +end + +helper.get_node_group = get_node_group +helper.search_surface = search_surface + +---------------------------------------------- +-- Auxiliar functions copied from mobs_redo -- +---------------------------------------------- + +-- Localize math functions +local pi = math.pi +local square = math.sqrt +local sin = math.sin +local cos = math.cos +local abs = math.abs +local min = math.min +local max = math.max +local atann = math.atan +local floor = math.floor +local random = math.random +local atan = function(x) + if not x or x ~= x then + --error("atan bassed NaN") + return 0 + else + return atann(x) + end +end + +-- config values +local show_health = minetest.settings:get_bool("mob_show_health") ~= false +local use_cmi = minetest.global_exists("cmi") +local mobs_drop_items = minetest.settings:get_bool("mobs_drop_items") ~= false +local mobs_griefing = minetest.settings:get_bool("mobs_griefing") ~= false + +-- default nodes +local node_fire = "fire:basic_flame" +local node_permanent_flame = "fire:permanent_flame" +local node_ice = "default:ice" +local node_snowblock = "default:snowblock" +local node_snow = "default:snow" + +helper.mobs = {} + +local get_distance = function(a, b) + local x, y, z = a.x - b.x, a.y - b.y, a.z - b.z + return square(x * x + y * y + z * z) +end + +local set_animation = function(ent, anim) + mobs:set_animation(ent,anim) +end + +local mob_sound = function(ent, sound) + if sound then + minetest.sound_play(sound, { + object = ent.object, + gain = 1.0, + max_hear_distance = ent.sounds.distance + }) + end +end + +local set_velocity = function(ent, v) + -- do not move if mob has stopped + if ent.state == "stand" then + ent.object:setvelocity({x = 0, y = 0, z = 0}) + return + end + + local yaw = (ent.object:get_yaw() or 0) + ent.rotate + ent.object:setvelocity({ + x = sin(yaw) * -v, + y = ent.object:getvelocity().y, + z = cos(yaw) * v + }) +end + +local get_velocity = function(ent) + local v = ent.object:getvelocity() + return (v.x * v.x + v.z * v.z) ^ 0.5 +end + +local node_ok = function(pos, fallback) + fallback = fallback or mobs.fallback_node + local node = minetest.get_node_or_nil(pos) + if node and minetest.registered_nodes[node.name] then + return node + end + return minetest.registered_nodes[fallback] +end + +local set_yaw = function(ent, yaw, delay) + mobs:yaw(ent, yaw, delay) + return ent.target_yaw +end + +local do_jump = function(self) + if not self.jump + or self.jump_height == 0 + or self.fly + or self.child + or self.order == "stand" then + return false + end + + self.facing_fence = false + -- something stopping us while moving? + if self.state ~= "stand" + and get_velocity(self) > 0.5 + and self.object:getvelocity().y ~= 0 then + return false + end + + local pos = self.object:get_pos() + local yaw = self.object:get_yaw() + + -- what is mob standing on? + pos.y = pos.y + self.collisionbox[2] - 0.2 + + local nod = node_ok(pos) + if minetest.registered_nodes[nod.name].walkable == false then + return false + end + + -- where is front + local dir_x = -sin(yaw) * (self.collisionbox[4] + 0.5) + local dir_z = cos(yaw) * (self.collisionbox[4] + 0.5) + + -- what is in front of mob? + local nod = node_ok({ + x = pos.x + dir_x, + y = pos.y + 0.5, + z = pos.z + dir_z + }) + + -- thin blocks that do not need to be jumped + if nod.name == node_snow then + return false + end + + if minetest.registered_items[nod.name].walkable then + if not nod.name:find("fence") + and not nod.name:find("gate") then + local v = self.object:getvelocity() + v.y = self.jump_height + set_animation(self, "jump") -- only when defined + self.object:setvelocity(v) + + -- when in air move forward + minetest.after(0.3, function(self, v) + if self.object:get_luaentity() then + self.object:set_acceleration({ + x = v.x * 2,--1.5, + y = 0, + z = v.z * 2,--1.5 + }) + end + end, self, v) + + if get_velocity(self) > 0 then + mob_sound(self, self.sounds.jump) + end + else + self.facing_fence = true + end + return true + end + return false +end + +local is_at_cliff = function(ent) + if ent.fear_height == 0 then -- 0 for no falling protection! + return false + end + + local yaw = ent.object:get_yaw() + local dir_x = -sin(yaw) * (ent.collisionbox[4] + 0.5) + local dir_z = cos(yaw) * (ent.collisionbox[4] + 0.5) + local pos = ent.object:get_pos() + local ypos = pos.y + ent.collisionbox[2] -- just above floor + + if minetest.line_of_sight( + {x = pos.x + dir_x, y = ypos, z = pos.z + dir_z}, + {x = pos.x + dir_x, y = ypos - ent.fear_height, z = pos.z + dir_z} + , 1) then + return true + end + return false +end + +local within_limits = function(pos, radius) + if (pos.x - radius) > -30913 + and (pos.x + radius) < 30928 + and (pos.y - radius) > -30913 + and (pos.y + radius) < 30928 + and (pos.z - radius) > -30913 + and (pos.z + radius) < 30928 then + return true -- within limits + end + + return false -- beyond limits +end + +-- update nametag colour +local update_tag = function(ent) + local col = "#00FF00" + local qua = ent.hp_max / 4 + + if ent.health <= floor(qua) then + col = "#FF0000" + elseif ent.health <= floor(qua * 2) then + col = "#FF6600" + elseif ent.health <= floor(qua * 3) then + col = "#FFFF00" + end + + ent.object:set_properties({nametag = ent.nametag, nametag_color = col}) +end + +-- check line of sight (BrunoMine) +local line_of_sight = function(ent, pos1, pos2, stepsize) + + stepsize = stepsize or 1 + + local s, pos = minetest.line_of_sight(pos1, pos2, stepsize) + + -- normal walking and flying mobs can see you through air + if s == true then + return true + end + + -- New pos1 to be analyzed + local npos1 = {x = pos1.x, y = pos1.y, z = pos1.z} + + local r, pos = minetest.line_of_sight(npos1, pos2, stepsize) + + -- Checks the return + if r == true then return true end + + -- Nodename found + local nn = minetest.get_node(pos).name + + -- Target Distance (td) to travel + local td = get_distance(pos1, pos2) + + -- Actual Distance (ad) traveled + local ad = 0 + + -- It continues to advance in the line of sight in search of a real + -- obstruction which counts as 'normal' nodebox. + while minetest.registered_nodes[nn] + and (minetest.registered_nodes[nn].walkable == false + or minetest.registered_nodes[nn].drawtype == "nodebox") do + + -- Check if you can still move forward + if td < ad + stepsize then + return true -- Reached the target + end + + -- Moves the analyzed pos + local d = get_distance(pos1, pos2) + + npos1.x = ((pos2.x - pos1.x) / d * stepsize) + pos1.x + npos1.y = ((pos2.y - pos1.y) / d * stepsize) + pos1.y + npos1.z = ((pos2.z - pos1.z) / d * stepsize) + pos1.z + + -- NaN checks + if d == 0 + or npos1.x ~= npos1.x + or npos1.y ~= npos1.y + or npos1.z ~= npos1.z then + return false + end + + ad = ad + stepsize + + -- scan again + r, pos = minetest.line_of_sight(npos1, pos2, stepsize) + + if r == true then return true end + + -- New Nodename found + nn = minetest.get_node(pos).name + + end + + return false +end + +-- are we flying in what we are suppose to? (taikedz) +local flight_check = function(ent, pos_w) + local nod = ent.standing_in + local def = minetest.registered_nodes[nod] + + if not def then return false end -- nil check + + if type(ent.fly_in) == "string" + and nod == ent.fly_in then + return true + elseif type(ent.fly_in) == "table" then + for _,fly_in in pairs(ent.fly_in) do + if nod == fly_in then + return true + end + end + end + + -- stops mobs getting stuck inside stairs and plantlike nodes + if def.drawtype ~= "airlike" + and def.drawtype ~= "liquid" + and def.drawtype ~= "flowingliquid" then + return true + end + + return false +end + +local effect = function(pos, amount, texture, min_size, max_size, radius, gravity, glow) + radius = radius or 2 + min_size = min_size or 0.5 + max_size = max_size or 1 + gravity = gravity or -10 + glow = glow or 0 + + minetest.add_particlespawner({ + amount = amount, + time = 0.25, + minpos = pos, + maxpos = pos, + minvel = {x = -radius, y = -radius, z = -radius}, + maxvel = {x = radius, y = radius, z = radius}, + minacc = {x = 0, y = gravity, z = 0}, + maxacc = {x = 0, y = gravity, z = 0}, + minexptime = 0.1, + maxexptime = 1, + minsize = min_size, + maxsize = max_size, + texture = texture, + glow = glow, + }) +end + +-- drop items +local item_drop = function(ent, cooked) + -- no drops if disabled by setting + if not mobs_drop_items then return end + + -- no drops for child mobs + if ent.child then return end + + local obj, item, num + local pos = ent.object:get_pos() + ent.drops = ent.drops or {} -- nil check + + for n = 1, #ent.drops do + if random(1, ent.drops[n].chance) == 1 then + num = random(ent.drops[n].min or 1, ent.drops[n].max or 1) + item = ent.drops[n].name + + -- cook items when true + if cooked then + local output = minetest.get_craft_result({ + method = "cooking", width = 1, items = {item}}) + + if output and output.item and not output.item:is_empty() then + item = output.item:get_name() + end + end + + -- add item if it exists + obj = minetest.add_item(pos, ItemStack(item .. " " .. num)) + + if obj and obj:get_luaentity() then + obj:setvelocity({ + x = random(-10, 10) / 9, + y = 6, + z = random(-10, 10) / 9, + }) + elseif obj then + obj:remove() -- item does not exist + end + end + end + + ent.drops = {} +end + + +-- check if mob is dead or only hurt +local check_for_death = function(ent, cause, cmi_cause) + -- has health actually changed? + if ent.health == ent.old_health and ent.health > 0 then + return + end + + ent.old_health = ent.health + -- still got some health? play hurt sound + if ent.health > 0 then + mob_sound(ent, ent.sounds.damage) + -- make sure health isn't higher than max + if ent.health > ent.hp_max then + ent.health = ent.hp_max + end + + -- backup nametag so we can show health stats + if not ent.nametag2 then + ent.nametag2 = ent.nametag or "" + end + + if show_health and (cmi_cause and cmi_cause.type == "punch") then + ent.htimer = 2 + ent.nametag = "♥ " .. ent.health .. " / " .. ent.hp_max + update_tag(ent) + end + + return false + end + + -- dropped cooked item if mob died in lava + if cause == "lava" then + item_drop(ent, true) + else + item_drop(ent, nil) + end + + mob_sound(ent, ent.sounds.death) + local pos = ent.object:get_pos() + + -- execute custom death function + if ent.on_die then + ent.on_die(ent, pos) + + if use_cmi then + cmi.notify_die(ent.object, cmi_cause) + end + + ent.object:remove() + + return true + end + + -- default death function and die animation (if defined) + if ent.animation and ent.animation.die_start and ent.animation.die_end then + local frames = ent.animation.die_end - ent.animation.die_start + local speed = ent.animation.die_speed or 15 + local length = max(frames / speed, 0) + + ent.attack = nil + ent.v_start = false + ent.timer = 0 + ent.blinktimer = 0 + ent.passive = true + ent.state = "die" + set_velocity(ent, 0) + set_animation(ent, "die") + + minetest.after(length, function(ent) + if use_cmi then + cmi.notify_die(ent.object, cmi_cause) + end + + ent.object:remove() + end, ent) + else + if use_cmi then + cmi.notify_die(ent.object, cmi_cause) + end + + ent.object:remove() + end + + effect(pos, 20, "tnt_smoke.png") + return true +end + +local do_env_damage = function(ent) + -- feed/tame text timer (so mob 'full' messages dont spam chat) + if ent.htimer > 0 then + ent.htimer = ent.htimer - 1 + end + + -- reset nametag after showing health stats + if ent.htimer < 1 and ent.nametag2 then + + ent.nametag = ent.nametag2 + ent.nametag2 = nil + + update_tag(ent) + end + + local pos = ent.object:get_pos() + ent.time_of_day = minetest.get_timeofday() + + -- remove mob if beyond map limits + if not within_limits(pos, 0) then + ent.object:remove() + return + end + + -- bright light harms mob + if ent.light_damage ~= 0 +-- and pos.y > 0 +-- and ent.time_of_day > 0.2 +-- and ent.time_of_day < 0.8 + and (minetest.get_node_light(pos) or 0) > 12 then + ent.health = ent.health - ent.light_damage + effect(pos, 5, "tnt_smoke.png") + if check_for_death(ent, "light", {type = "light"}) then return end + end + + local y_level = ent.collisionbox[2] + + if ent.child then + y_level = ent.collisionbox[2] * 0.5 + end + + -- what is mob standing in? + pos.y = pos.y + y_level + 0.25 -- foot level + ent.standing_in = node_ok(pos, "air").name +-- print ("standing in " .. ent.standing_in) + + -- don't fall when on ignore, just stand still + if ent.standing_in == "ignore" then + ent.object:setvelocity({x = 0, y = 0, z = 0}) + end + + local nodef = minetest.registered_nodes[ent.standing_in] + pos.y = pos.y + 1 -- for particle effect position + -- water + if ent.water_damage and nodef.groups.water then + if ent.water_damage ~= 0 then + ent.health = ent.health - ent.water_damage + effect(pos, 5, "bubble.png", nil, nil, 1, nil) + if check_for_death(ent, "water", {type = "environment", + pos = pos, node = ent.standing_in}) then return end + end + + -- lava or fire + elseif ent.lava_damage + and (nodef.groups.lava + or ent.standing_in == node_fire + or ent.standing_in == node_permanent_flame) then + if ent.lava_damage ~= 0 then + ent.health = ent.health - ent.lava_damage + effect(pos, 5, "fire_basic_flame.png", nil, nil, 1, nil) + if check_for_death(ent, "lava", {type = "environment", + pos = pos, node = ent.standing_in}) then return end + end + + -- damage_per_second node check + elseif nodef.damage_per_second ~= 0 then + ent.health = ent.health - nodef.damage_per_second + effect(pos, 5, "tnt_smoke.png") + if check_for_death(ent, "dps", {type = "environment", + pos = pos, node = ent.standing_in}) then return end + end + check_for_death(ent, "", {type = "unknown"}) +end + +-- find and replace what mob is looking for (grass, wheat etc.) +local replace = function(ent, pos) + if not mobs_griefing + or not ent.replace_rate + or not ent.replace_what + or ent.child == true + or ent.object:getvelocity().y ~= 0 + or random(1, ent.replace_rate) > 1 then + return + end + + local what, with, y_offset + if type(ent.replace_what[1]) == "table" then + local num = random(#ent.replace_what) + what = ent.replace_what[num][1] or "" + with = ent.replace_what[num][2] or "" + y_offset = ent.replace_what[num][3] or 0 + else + what = ent.replace_what + with = ent.replace_with or "" + y_offset = ent.replace_offset or 0 + end + + pos.y = pos.y + y_offset + if #minetest.find_nodes_in_area(pos, pos, what) > 0 then + local oldnode = {name = what} + local newnode = {name = with} + local on_replace_return + + if ent.on_replace then + on_replace_return = ent.on_replace(ent, pos, oldnode, newnode) + end + + if on_replace_return ~= false then + minetest.set_node(pos, {name = with}) + + -- when cow/sheep eats grass, replace wool and milk + if ent.gotten == true then + ent.gotten = false + ent.object:set_properties(ent) + end + end + end +end + +-- specific runaway +local specific_runaway = function(list, what) + + -- no list so do not run + if list == nil then + return false + end + + -- found entity on list to attack? + for no = 1, #list do + + if list[no] == what then + return true + end + end + + return false +end + +-- find someone to runaway from +local runaway_from = function(ent) + + if not ent.runaway_from then + return + end + + local s = ent.object:get_pos() + local p, sp, dist + local player, obj, min_player + local type, name = "", "" + local min_dist = ent.view_range + 1 + local objs = minetest.get_objects_inside_radius(s, ent.view_range) + + for n = 1, #objs do + + if objs[n]:is_player() then + + if mobs.invis[ objs[n]:get_player_name() ] + or ent.owner == objs[n]:get_player_name() then + + type = "" + else + player = objs[n] + type = "player" + name = "player" + end + else + obj = objs[n]:get_luaentity() + + if obj then + player = obj.object + type = obj.type + name = obj.name or "" + end + end + + -- find specific mob to runaway from + if name ~= "" and name ~= ent.name + and specific_runaway(ent.runaway_from, name) then + + p = player:get_pos() + sp = s + + -- aim higher to make looking up hills more realistic + p.y = p.y + 1 + sp.y = sp.y + 1 + + dist = get_distance(p, s) + + + -- choose closest player/mpb to runaway from + if dist < min_dist + and line_of_sight(ent, sp, p, 2) == true then + min_dist = dist + min_player = player + end + end + end + + if min_player then + + local lp = player:get_pos() + local vec = { + x = lp.x - s.x, + y = lp.y - s.y, + z = lp.z - s.z + } + + local yaw = (atan(vec.z / vec.x) + 3 * pi / 2) - ent.rotate + + if lp.x > s.x then + yaw = yaw + pi + end + + yaw = set_yaw(ent, yaw, 4) + ent.state = "runaway" + ent.runaway_timer = 3 + end +end + +local flop = function(ent) + -- swimmers flop when out of their element, and swim again when back in + if ent.fly then + local s = ent.object:get_pos() + if not flight_check(ent, s) then + + ent.state = "flop" + ent.object:setvelocity({x = 0, y = -5, z = 0}) + + set_animation(ent, "stand") + + return + elseif ent.state == "flop" then + ent.state = "stand" + end + end +end + +local do_stand = function(ent, dtime) + local yaw = self.object:get_yaw() or 0 + + if random(1, 4) == 1 then + local lp = nil + local s = ent.object:get_pos() + local objs = minetest.get_objects_inside_radius(s, 3) + + for n = 1, #objs do + if objs[n]:is_player() then + lp = objs[n]:get_pos() + break + end + end + + -- look at any players nearby, otherwise turn randomly + if lp then + local vec = { + x = lp.x - s.x, + z = lp.z - s.z + } + + yaw = (atan(vec.z / vec.x) + pi / 2) - ent.rotate + if lp.x > s.x then yaw = yaw + pi end + else + yaw = yaw + random(-0.5, 0.5) + end + yaw = set_yaw(ent, yaw, 8) + end + + set_velocity(ent, 0) + set_animation(ent, "stand") + + -- npc's ordered to stand stay standing + if ent.type ~= "npc" + or ent.order ~= "stand" then + + if ent.walk_chance ~= 0 + and ent.facing_fence ~= true + and random(1, 100) <= ent.walk_chance + and is_at_cliff(ent) == false then + + set_velocity(ent, ent.walk_velocity) + ent.state = "walk" + set_animation(ent, "walk") + + --[[ fly up/down randomly for flying mobs + if ent.fly and random(1, 100) <= ent.walk_chance then + + local v = ent.object:getvelocity() + local ud = random(-1, 2) / 9 + + ent.object:setvelocity({x = v.x, y = ud, z = v.z}) + end--]] + end + end +end + +local do_walk = function(ent, dtime) + local yaw = self.object:get_yaw() or 0 + + local s = self.object:get_pos() + local lp = nil + + -- is there something I need to avoid? + if self.water_damage > 0 + and self.lava_damage > 0 then + lp = minetest.find_node_near(s, 1, {"group:water", "group:lava"}) + elseif self.water_damage > 0 then + lp = minetest.find_node_near(s, 1, {"group:water"}) + elseif self.lava_damage > 0 then + lp = minetest.find_node_near(s, 1, {"group:lava"}) + end + + if lp then + -- if mob in water or lava then look for land + if (self.lava_damage + and minetest.registered_nodes[self.standing_in].groups.lava) + or (self.water_damage + and minetest.registered_nodes[self.standing_in].groups.water) then + + lp = minetest.find_node_near(s, 5, {"group:soil", "group:stone", + "group:sand", node_ice, node_snowblock}) + + -- did we find land? + if lp then + local vec = { + x = lp.x - s.x, + z = lp.z - s.z + } + + yaw = (atan(vec.z / vec.x) + pi / 2) - self.rotate + + if lp.x > s.x then yaw = yaw + pi end + + -- look towards land and jump/move in that direction + yaw = set_yaw(self, yaw, 6) + do_jump(self) + set_velocity(self, self.walk_velocity) + else + yaw = yaw + random(-0.5, 0.5) + end + else + local vec = { + x = lp.x - s.x, + z = lp.z - s.z + } + + yaw = (atan(vec.z / vec.x) + pi / 2) - self.rotate + + if lp.x > s.x then yaw = yaw + pi end + end + + yaw = set_yaw(self, yaw, 8) + + -- otherwise randomly turn + elseif random(1, 100) <= 30 then + yaw = yaw + random(-0.5, 0.5) + yaw = set_yaw(self, yaw, 8) + end + + -- stand for great fall in front + local temp_is_cliff = is_at_cliff(self) + + if self.facing_fence == true + or temp_is_cliff + or random(1, 100) <= 30 then + set_velocity(self, 0) + self.state = "stand" + set_animation(self, "stand") + else + set_velocity(self, self.walk_velocity) + + if flight_check(self) + and self.animation + and self.animation.fly_start + and self.animation.fly_end then + set_animation(self, "fly") + else + set_animation(self, "walk") + end + end +end + +-- Config values +helper.mobs.show_health = show_health +helper.mobs.use_cmi = use_cmi +helper.mobs.mobs_griefing = mobs_griefing + +-- Auxiliar functions +helper.mobs.get_distance = get_distance +helper.mobs.set_animation = set_animation +helper.mobs.mob_sound = mob_sound +helper.mobs.node_ok = node_ok +helper.mobs.set_velocity = set_velocity +helper.mobs.get_velocity = get_velocity +helper.mobs.is_at_cliff = is_at_cliff +helper.mobs.within_limits = within_limits +helper.mobs.update_tag = update_tag +helper.mobs.flight_check = flight_check +helper.mobs.effect = effect +helper.mobs.item_drop = item_drop +helper.mobs.check_for_death = check_for_death +helper.mobs.do_env_damage = do_env_damage +helper.mobs.replace = replace +helper.mobs.flop = flop +helper.mobs.runaway_from = runaway_from +helper.mobs.do_jump = do_jump +helper.mobs.set_yaw = set_yaw +helper.mobs.do_state_stand = do_state_stand + +adaptive_ai.helper = helper diff --git a/init.lua b/init.lua new file mode 100644 index 0000000..b68cbda --- /dev/null +++ b/init.lua @@ -0,0 +1,22 @@ +print("This file will be run at load time!") + +adaptive_ai= {} + +adaptive_ai.path = minetest.get_worldpath().."\\adaptive_ai_" +local modpath = minetest.get_modpath("adaptive_ai") +adaptive_ai.modpath = modpath + +dofile(modpath.."/helper.lua") +dofile(modpath.."/helper_mobs.lua") + +dofile(modpath.."/api_mobs.lua") +dofile(modpath.."/api.lua") + +dofile(modpath.."/population.lua") + +adaptive_ai.population.load_tribes() + +dofile(modpath.."/moondrum.lua") + +-- dofile(modpath.."/imps/imp_integration.lua") +dofile(modpath.."/kobold/kobold_integration.lua") diff --git a/kobold/kobold.lua b/kobold/kobold.lua new file mode 100644 index 0000000..0724e46 --- /dev/null +++ b/kobold/kobold.lua @@ -0,0 +1,369 @@ + +-- 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 diff --git a/kobold/kobold_integration.lua b/kobold/kobold_integration.lua new file mode 100644 index 0000000..0467e2c --- /dev/null +++ b/kobold/kobold_integration.lua @@ -0,0 +1,514 @@ + +dofile(minetest.get_modpath("adaptive_ai").."/kobold/kobold.lua") + +local kobolds = adaptive_ai.kobolds +local stats = kobolds.stats +local helper = adaptive_ai.helper +local calc = adaptive_ai.calc +local population = adaptive_ai.population + +local random = math.random +local floor = math.floor + +local chat_log = adaptive_ai.chat_log +local ai_on_step = adaptive_ai.ai_on_step + +local sort_nils_out = helper.sort_nils_out +local get_surface = helper.search_surface +local pop = helper.pop +local shuffle = helper.shuffle +local distance = calc.dist_euclidean + +local kobold_from_lines = kobolds.from_lines +local kobold_score = kobolds.score +local kobold_decide = kobolds.decide +local kobold_create = kobolds.create + +local get_from_census = population.get_from_census +local create_tribe = population.create_tribe +local add_to_tribe = population.add_to_tribe +local empty_population = population.empty + +local do_jump = helper.mobs.do_jump +local set_velocity = helper.mobs.set_velocity +local is_at_cliff = helper.mobs.is_at_cliff +local set_yaw = helper.mobs.set_yaw +local flight_check = helper.mobs.flight_check +local set_animation = helper.mobs.set_animation + +local adjacent = helper.adjacent +local file_tag = kobolds.file_tag +local popul_path = population.path +local train_path = adaptive_ai.modpath.."/training/" + +local pi = math.pi +local actions = kobolds.actions +local yaws = { + 0, pi, 3*pi/2, pi/2, -- North, South, East, West + 7*pi/4, 5*pi/4, pi/4, 3*pi/4 -- NE, SE, NW, SW +} +local food_nodes = {"adaptive_ai:moondrum_mushroom"} +local founders = nil +local min_founders = 4 +local timer = 0 + +-- Out of Mobs_Redo +-- Equal to do_states(self, dtime) for self.state == "walk" +-- except without turning to random directions when safe +local function state_move(self, dtime) + local yaw = self.object:get_yaw() or 0 + local s = self.object:get_pos() + local lp = nil + + -- is there something I need to avoid? + if self.water_damage > 0 + and self.lava_damage > 0 then + lp = minetest.find_node_near(s, 1, {"group:water", "group:lava"}) + elseif self.water_damage > 0 then + lp = minetest.find_node_near(s, 1, {"group:water"}) + elseif self.lava_damage > 0 then + lp = minetest.find_node_near(s, 1, {"group:lava"}) + end + + if lp then + -- if mob in water or lava then look for land + if (self.lava_damage + and minetest.registered_nodes[self.standing_in].groups.lava) + or (self.water_damage + and minetest.registered_nodes[self.standing_in].groups.water) then + + lp = minetest.find_node_near(s, 5, {"group:soil", "group:stone", + "group:sand", node_ice, node_snowblock}) + + -- did we find land? + if lp then + local vec = { + x = lp.x - s.x, + z = lp.z - s.z + } + yaw = (atan(vec.z / vec.x) + pi / 2) - self.rotate + + if lp.x > s.x then yaw = yaw + pi end + + -- look towards land and jump/move in that direction + yaw = set_yaw(self, yaw, 6) + local jumped = do_jump(self) + if jumped then chat_log("Jumping while moving") end + set_velocity(self, self.walk_velocity) + else + yaw = yaw + random(-0.5, 0.5) + end + else + local vec = { + x = lp.x - s.x, + z = lp.z - s.z + } + + yaw = (atan(vec.z / vec.x) + pi / 2) - self.rotate + + if lp.x > s.x then yaw = yaw + pi end + end + + yaw = set_yaw(self, yaw, 8) + end + + -- stand for great fall in front + local temp_is_cliff = is_at_cliff(self) + + if self.facing_fence == true + or temp_is_cliff then + set_velocity(self, 0) + self.state = "stand" + set_animation(self, "stand") + else + set_velocity(self, self.walk_velocity) + + if flight_check(self) + and self.animation + and self.animation.fly_start + and self.animation.fly_end then + set_animation(self, "fly") + else + set_animation(self, "walk") + end + end +end + +local function move_creature(self, yaw_id) + if self.state == "stand" or self.state == "walk" or self.state == "move" then + self.object:set_yaw(yaws[yaw_id]) + set_velocity(self, self.walk_velocity) + self.state = "move" + end + return false +end + +local function load_founders() + local file = io.open(train_path..kobolds.trained_tag..".txt", "r") + local counter = 0 + + if file then + founders = {} + local reading, kobold = false, {} + for line in file:lines() do + counter = counter + 1 + if not reading then + reading = line == file_tag[1] + if reading then + kobold = {} + table.insert(kobold,line) + end + else + reading = line ~= file_tag[2] + table.insert(kobold,line) + if not reading then + kobold = kobold_from_lines(kobold) + kobold = kobold_create(kobold) + table.insert(founders, kobold) + chat_log("Founder loaded: "..kobold.name) + end + end + end + file:close() + end + if #founders == 0 then + --error("Founders not found") + end +end + +load_founders() + +local function get_founders(n) + chat_log(#founders.." founders") + founders = founders or {} + local f + while #founders < min_founders do + f = {} + kobold_create(f) + table.insert(founders, f) + end + + if #founders > min_founders then + n = n or min_founders + local tribe_founders = {} + shuffle(founders) + for i=1,n do + table.insert(tribe_founders, founders[i]) + end + + return tribe_founders + else + return founders + end +end + +local function load_kobold(self) + chat_log("Loading from census "..(self.nametag and "true" or "false")) + local old_self = get_from_census(self) + if old_self then + self.kobold = old_self + if not old_self.genes then error("No genes") end + chat_log(old_self.name.." loaded "..#old_self.genes.." genes") + self.timer = self.walk_velocity + return true + else + --self.kobold = nil + --chat_log("Nil kobold") + return false + end +end + +local function set_kobold(self, kobold) + self.kobold = kobold + self.nametag = kobold.name +end + +local function setup_kobold(self, mother, father) + if load_kobold(self) then + return + else + local kobold = {} + kobold_create(kobold, mother, father) + set_kobold(self, kobold) + end + return false +end + +local function breed_kobold(self) + local mother = self.kobold + if not mother.tribe_id then + --error("Breeding with no tribe.") + return + end + + local tribe_id = mother.tribe_id + local tribe = population.tribes[tribe_id] + if tribe then + local popul_size = tribe:popul_size() + + if popul_size < tribe.size * 2 then + local p = mother.pos + p = {x=(p.x), y=(p.y), z=(p.z)} + p.x = p.x + (random(0,1) - 0.5)*2 + p.z = p.z + (random(0,1) - 0.5)*2 + + local father = get_couple(tribe_id, self.kobold) + local child = place_kobold(p) + setup_kobold(child, mother, father) + + child = child.kobold + child.tribe_id = tribe_id + population.add_to_tribe(tribe_id, child) + return true + end + end + return false +end + +kobolds.breed_kobold = breed_kobold + +local kobold_decisions = { + function(self, dtime) -- idle + set_velocity(self, 0) + end, + function(self, dtime) -- wander + local n = random(1,#yaws) + local move = move_creature(self, n) + local kobold = self.kobold + if move then + kobold.hunger = kobold.hunger - 1 + self.state = "walk" + end + end, + function(self, dtime) -- eat + local kobold = self.kobold + if kobold.food and #kobold.food > 0 then + local satiate = pop(kobold.food) + kobold.hunger = kobold.hunger + satiate + kobold.hp = self.object:get_hp() + 1 + self.object:set_hp(kobold.hp) + end + end, + function(self, dtime) -- breed + local kobold = self.kobold + kobold.fitness = kobold.fitness + 2 + kobold.hunger = kobold.hunger - 10 + + kobolds.breed_kobold(self) + end, + function(self, dtime) -- move North + local move = move_creature(self, 1) + self.state = move and "move" or self.state + end, + function(self, dtime) -- move South + local move = move_creature(self, 2) + self.state = move and "move" or self.state + end, + function(self, dtime) -- move East + local move = move_creature(self, 3) + self.state = move and "move" or self.state + end, + function(self, dtime) -- move West + local move = move_creature(self, 4) + self.state = move and "move" or self.state + end, + function(self, dtime) -- move NE + local move = move_creature(self, 5) + self.state = move and "move" or self.state + end, + function(self, dtime) -- move SE + local move = move_creature(self, 6) + self.state = move and "move" or self.state + end, + function(self, dtime) -- move NW + local move = move_creature(self, 7) + self.state = move and "move" or self.state + end, + function(self, dtime) -- move SW + local move = move_creature(self, 8) + self.state = move and "move" or self.state + end, +} + +local manage_kobold = function(self, dtime) + local kobold = self.kobold + if kobold then + if kobold.delete or kobold.hp <= 0 then + self.object:remove() + return + end + kobold.hp = self.object:get_hp() + self.timer = self.timer + dtime + + if kobold.hp > 0 then + local p = self.object:get_pos() + p = {x=floor(p.x+0.5), y=floor(p.y+0.5), z=floor(p.z+0.5)} + kobold.pos = p + + local food_pos = minetest.find_node_near(self.object:get_pos(), self.reach, food_nodes) + + if food_pos then + --chat_log("Pocketing food from "..food_pos.x..","..food_pos.z) + if kobold.closest_food + and kobold.closest_food.x == food_pos.x + and kobold.closest_food.z == food_pos.z then + kobold.closest_food = nil + end + table.insert(kobold.food, random(2,5)) + minetest.set_node(food_pos, {name = "air"}) + end + + food_pos = minetest.find_node_near(self.object:get_pos(), self.view_range, food_nodes) + if food_pos then + --chat_log("Smelling good food") + food_pos = {x=floor(food_pos.x+0.5),y=floor(food_pos.y+0.5),z=floor(food_pos.z+0.5)} + if food_pos and (not kobold.closest_food + or distance(p, food_pos) < distance(p, kobold.closest_food)) then + kobold.closest_food = food_pos + end + end + + if not kobold.action or self.timer >= self.walk_velocity then + self.timer = 0 + --chat_log("Time to decide for "..kobold.name) + kobold.hunger = kobold.hunger - dtime + local neighbors = {} + + local pos, pos2 = {} + for i=1,#adjacent do + pos.x = p.x + adjacent[i].x + pos.y = p.y + pos.z = p.z + adjacent[i].z + pos2 = get_surface(pos) + neighbors[i] = pos2 and {x=pos2.x,y=pos2.y,z=pos2.z} or {x=pos.x, y=self.kobold.climb + 1,z=pos.z} + end + + chat_log("Closest food "..(kobold.closest_food and kobold.closest_food.x-p.x..","..kobold.closest_food.z-p.z or "none")) + + local decision, d_id = kobold_decide(kobold, neighbors) + chat_log(kobold.name.." decides "..(decision and actions[decision] or "nothing").." -- "..(d_id or "error")) + kobold.action = decision + if decision then + kobold_decisions[decision](self, dtime) + else + if not kobold.action then + kobold.action = 1 + kobold_decisions[1](self, dtime) + end + end + kobold.fitness = kobold.fitness + kobold_score(kobold) + end + + if kobold.hunger < 0 then + kobold.hunger = 0 + kobold.hp = self.object:get_hp() - 1 + self.object:set_hp(kobold.hp) + end + end + else + chat_log("No kobold") + end +end + +local function custom_on_step(self, dtime) + manage_kobold(self, dtime) + + if self.state == "move" then + state_move(self, dtime) + end + do_jump(self, dtime) + --ai_on_step(self, dtime) + return false +end + +local function place_kobold(p) + local ent = adaptive_ai:place_ai({ + name = "adaptive_ai:kobold", + pos = p, + }) + + return ent +end + +local function place_tribe(p) + local roots = {x=p.x, y=p.y-2, z=p.z} + local ents = {} + + local popul = create_tribe(p, "adaptive_ai:kobold", get_founders(), kobolds.stats.view_range, kobolds.clone, 5) + local ent, pos, c + c = 0 + + for i,k in ipairs(popul) do + pos = get_surface(k.pos) + ent = place_kobold(pos) + set_kobold(ent, k) + --chat_log("Placing "..ent.nametag.."!") + ent.kobold.pos = pos + end + + chat_log("Placed tribe!") + minetest.set_node(roots, {name = "adaptive_ai:moondrum_roots"}) +end + +adaptive_ai:register_ai("adaptive_ai:kobold", { + stepheight = 0.8, + hp_min = kobolds.stats.hp_max, + hp_max = kobolds.stats.hp_max, + armor = 100, + walk_velocity = 1.5, + run_velocity = 2, + jump = true, + jump_height = kobolds.stats.jump, + view_range = kobolds.stats.view_range, + damage = 2, + knock_back = true, + fear_height = kobolds.stats.fear_height, + floats = 1, + reach = 1, + attack_type = "dogfight", + makes_footstep_sound = true, + visual = "cube", + visual_size = {x=0.8, y=1.6}, + textures = { + "aa_imp_top.png", + "aa_imp_top.png", + "aa_imp_side.png", + "aa_imp_side.png", + "aa_imp_face.png", + "aa_imp_side.png", + }, + collisionbox = {-0.5,-0.8,-0.5, 0.5,0.8,0.5}, + walk_chance = 1, + + actions = kobolds.actions, + action_managers = kobold_decisions, + on_spawn = load_kobold, + do_custom = custom_on_step, + + asexual = false, + breed = kobold_create, +}) + +minetest.register_chatcommand("kobold", { + func = function(name, param) + local player = minetest.get_player_by_name(name) + local pos = player and player:getpos() or nil + if pos then + place_tribe(pos) + end + end +}) + +minetest.register_chatcommand("empty_camps", { + func = function(name, param) + empty_population() + end +}) + +minetest.register_chatcommand("founders", { + func = function(name, param) + for i,f in ipairs(founders) do + chat_log("Founder "..f.name) + end + end +}) diff --git a/moondrum.lua b/moondrum.lua new file mode 100644 index 0000000..f7f5f59 --- /dev/null +++ b/moondrum.lua @@ -0,0 +1,189 @@ + +-- Vegetable food for adaptive AI +-- Mushroom + +local helper = adaptive_ai.helper + +local moondrum = {} + +-- local round = adaptive_ai.calc.round +local ceil = math.ceil +local floor = math.floor +local range = adaptive_ai.population.default_range + +local chat_log = adaptive_ai.chat_log + +local spread = {-floor(range/2), floor(range/2)} + +minetest.register_node("adaptive_ai:moondrum_mushroom", { + drawtype = "plantlike", + description = "Moondrum Mushroom", + tiles = {"aa_moondrum_mushroom.png"}, + inventory_image = "aa_moondrum_mushroom.png", + wield_image = "aa_moondrum_mushroom.png", + paramtype = "light", + sunlight_propagates = true, + walkable = false, + buildable_to = true, + groups = {snappy = 3, attached_node = 1, flammable = 1}, + sounds = default.node_sound_leaves_defaults(), + on_use = minetest.item_eat(2), + + selection_box = { + type = "fixed", + fixed = {-4 / 16, -0.5, -4 / 16, 4 / 16, -1 / 16, 4 / 16}, + }, +}) + +minetest.register_node("adaptive_ai:moondrum_roots", { + description = "Moondrum Mycelium", + tiles = {"aa_moondrum_roots.png"}, + groups = {cracky = 2}, + drop = "adaptive_ai:moondrum_roots 1" +}) + +local airlike_groups = {"flora"} +local soil_groups = {"soil", "sand", "wood"} +local soil_nodes = {"group:soil", "group:sand", "group:wood"} + +local search_surface = function(pos) + return helper.search_surface(pos, airlike_groups) +end + +local is_soil = function(name) + return helper.get_node_group(name, soil_groups) +end + +local is_airlike = function(name) + return name == "air" or helper.get_node_group(name, airlike_groups) +end + +local plant_moondrum = function(pos) + if not pos then return end -- skip if nil + + local soil = minetest.get_node(pos) + --chat_log("Planting moondrum on "..soil.name.."...") + + if is_soil(soil.name) then + pos.y = pos.y + 1 + minetest.swap_node(pos, {name = "adaptive_ai:moondrum_mushroom"}) + --chat_log("Planting succesful!") + return true + end + --chat_log("Planting failed!") + return false +end + +local plant_random_moondrum = function(pos) + -- local new_pos = {x = pos.x + math.random(-10, 10), y = pos.y, z = pos.z + math.random(-10, 10)} + local minp = {x = pos.x + spread[1], y = pos.y + spread[1]*2, z = pos.z + spread[1]} + local maxp = {x = pos.x + spread[2], y = pos.y + spread[2]*2, z = pos.z + spread[2]} + + local nodelist = minetest.find_nodes_in_area_under_air(minp, maxp, soil_nodes) + local new_pos = nodelist and nodelist[math.random(#nodelist)] or nil + return plant_moondrum(new_pos) +end + +local spawn_moondrum_mushroom = function(pos) + --chat_log("Spawning mushroom from roots...") + local p = {x = pos.x, y = pos.y, z = pos.z} + p = search_surface(p, airlike_groups) + p.y = p.y-1 + local top = minetest.get_node(p) --{x=p.x, y=p.y-1, z=p.z} + + if not top then + return + elseif top.name == "adaptive_ai:moondrum_mushroom" then + plant_random_moondrum(pos) + return + else -- if is_airlike + if plant_moondrum({x=p.x, y=p.y-1, z=p.z}) then + return + else + plant_random_moondrum(pos) + return + end + end +end + +minetest.register_abm({ + label = "Moondrum sprouting", + nodenames = {"adaptive_ai:moondrum_roots"}, + interval = 5.0, + chance = 10, + action = function(pos, node, active_object_count, active_object_count_wider) + local minp = {x = pos.x + spread[1], y = pos.y + spread[1]*2, z = pos.z + spread[1]} + local maxp = {x = pos.x + spread[2], y = pos.y + spread[2]*2, z = pos.z + spread[2]} + + local nearby_mushrooms = minetest.find_nodes_in_area_under_air(minp, maxp, "adaptive_ai:moondrum_mushroom") + if #nearby_mushrooms < 20 then + for i = 1,20-#nearby_mushrooms do + spawn_moondrum_mushroom(pos) + end + else + spawn_moondrum_mushroom(pos) + end + end +}) + +minetest.register_abm({ + label = "Moondrum spreading", + nodenames = {"adaptive_ai:moondrum_mushroom"}, + interval = 30.0, + chance = 20, + action = function(pos, node, active_object_count, active_object_count_wider) + local minp = {x = pos.x + spread[1], y = pos.y + spread[1]*2, z = pos.z + spread[1]} + local maxp = {x = pos.x + spread[2], y = pos.y + spread[2]*2, z = pos.z + spread[2]} + + local nodelist = minetest.find_nodes_in_area_under_air(minp, maxp, soil_groups) + local new_pos = nodelist and nodelist[math.random(#nodelist)] or nil + plant_moondrum(new_pos) + --chat_log("Spreading moondrum from spores!") + end +}) + +--[[ +minetest.register_abm({ + label = "Moondrum sprouting near water", + nodenames = {"adaptive_ai:moondrum_roots"}, + neighbors = {"default:water_source", "default:water_flowing"}, + interval = 30.0, + chance = 1, + action = spawn_moondrum_mushroom +}) + +minetest.register_abm({ + label = "Moondrum spreading near water" + nodenames = {"adaptive_ai:moondrum_mushroom"}, + neighbors = {"default:water_source", "default:water_flowing"}, + interval = 30.0, + chance = 2, + action = function(pos, node, active_object_count, active_object_count_wider) + new_pos = {x = pos.x + math.random(-2, 2), y = pos.y, z = pos.z + math.random(-2, 2)} + new_pos = search_surface(new_pos, valid_air) + plant_moondrum(new_pos) + --chat_log("Spreading moondrum from fertile mushroom spores!") + end +}) + +minetest.register_abm({ + nodenames = {"adaptive_ai:moondrum_mushroom"}, + interval = 10.0, + chance = 1, + action = function(pos, node, active_object_count, active_object_count_wider) + local objs = minetest.get_objects_inside_radius(pos, imps.vision) + local imp = {} + for _,obj in ipairs(objs) do + imp = obj:get_luaimp() + if imp and imp.name == "adaptive_ai:imp" then + --chat_log("Imp found") + if not imp.stats.closest_food_pos or distance(obj:getpos(), imp.stats.closest_food_pos) > distance(obj:getpos(), pos) then + imp.stats.closest_food_pos = pos + end + end + end + end +}) +--]] + +adaptive_ai.moondrum = moondrum diff --git a/population.lua b/population.lua new file mode 100644 index 0000000..d9823c8 --- /dev/null +++ b/population.lua @@ -0,0 +1,239 @@ + +local chat_log = adaptive_ai.chat_log +local breeding = adaptive_ai.breeding +local random = math.random +local floor = math.floor +local pop = adaptive_ai.helper.pop +local load_file = adaptive_ai.helper.load_file + +local default_size = 10 +local default_range = 20 +local timer = 0 +local dirty = false + +population = {} +tribes = {} +populs = {} +census = {} + +--local lfs = require "lfs" + +--local mkdir = minetest.mkdir or lfs.mkdir + +local mkdir = minetest.mkdir or function(path) + os.execute("mkdir \"" .. path .. "\"") +end + +local path = adaptive_ai.path.."population_" + +local file_tag = {"tribe_list", "census"} + +local function save_census() + local file, to_file, count, gene_copy + count = 0 + for _,c in pairs(census) do + count = count + 1 + gene_copy = c.genes + c.genes = nil + to_file = (minetest.serialize(c)) + file = io.open(path..file_tag[2].."\\"..c.name..".txt", "w") + if not file then + mkdir(path..file_tag[2]) + file = io.open(path..file_tag[2].."\\"..c.name..".txt", "w") + end + file:write(to_file) + file:close() + c.genes = gene_copy + + to_file = (minetest.serialize(gene_copy)) + file = io.open(path..file_tag[2].."\\"..c.name.." genes.txt", "w") + file:write(to_file) + file:close() + end + chat_log("Census: "..count) +end + +local function get_from_census(entity) + if not entity.nametag then return nil end + + local name = entity.nametag--:gsub("%s", "_") + local from_file = load_file(path..file_tag[2].."\\"..name..".txt") + local genes_file = load_file(path..file_tag[2].."\\"..name.." genes.txt") + + local creature = from_file or nil + if creature then + creature.genes = genes_file + table.insert(census, creature) + + local t_id = creature.tribe_id + if not tribes[t_id] then + local tribe = {} + tribe.pos = creature.pos + tribe.name = entity.name + tribe.range = default_range + tribe.size = default_size + tribe.id = t_id + + tribes[t_id] = tribe + end + if not populs[t_id] then + populs[t_id] = {creature} + else + table.insert(populs[t_id], creature) + end + end + + return creature +end + +local function save_to_file() + local file = io.open(path..file_tag[1]..".txt", "w") + local to_file = (minetest.serialize(tribes)) + file:write(to_file) + file:close() + save_census() + + timer = 0 + dirty = false +end + +local function load_tribes() + local from_file = load_file(path..file_tag[1]..".txt")--, true) + tribes = from_file or {} + + --error(#tribes > 0 and "Tribes loaded succesfully" or "Tribes not loaded") + --error(#census > 0 and "Census loaded succesfully" or "Census not loaded") +end + +local function add_to_census(creature) + local c_name, c_tag, disambiguate = creature.name, creature.tag, 1 + while census[creature.name] ~= nil do + creature.name = c_name.." "..disambiguate + disambiguate = disambiguate + 1 + end + census[creature.name] = creature + --chat_log("New censed creature: "..creature.name) +end + +local function fill_tribe(tribe, popul) + local breed, asexual = breeding[tribe.name].breed, breeding[tribe.name].asexual + local pos, t_id, range = tribe.pos, tribe.id, tribe.range + + --math.randomseed(os.time()) + local creature, p, i, j + while #popul < tribe.size do + p = {x=floor(pos.x+0.5), y=floor(pos.y+1), z=floor(pos.z+0.5)} + p.x = p.x + random(0,range) - floor(range/2 + 0.5) + p.z = p.z + random(0,range) - floor(range/2 + 0.5) + + i = random(#popul) + if asexual then + creature = {} + creature = breed(creature, popul[i]) + else + repeat + j = random(#popul) + until i ~= j + creature = {} + creature = breed(creature, popul[i], popul[j]) + end + + creature.pos = p + creature.tribe_id = t_id + + table.insert(popul, creature) + add_to_census(creature) + end + + populs[t_id] = popul +end + +local function create_tribe(pos, name, founders, range, clone, size) + if not founders or #founders == 0 then + error("Not enough creatures in starting population.") + end + + local tribe = {} + tribe.pos = pos + tribe.name = name + tribe.range = range + tribe.size = size or default_size + tribe.id = #tribes + 1 + local popul = {} + + local p + for _,father in ipairs(founders) do + f = clone(father, f) + f.tribe_id = tribe.id + p = {x=floor(pos.x+0.5), y=floor(pos.y+0.5), z=floor(pos.z+0.5)} + p.x = p.x + random(0,range) - floor(range/2 + 0.5) + p.z = p.z + random(0,range) - floor(range/2 + 0.5) + f.pos = p + table.insert(popul, f) + add_to_census(f) + end + + fill_tribe(tribe, popul) + table.insert(tribes, tribe) + save_to_file() + + tribe.popul_size = function(self) + return #populs[tribe.tribe_id] + end + + return popul +end + +local function get_couple(t_id, creature) + local p = populs[creature.tribe_id] + local i + repeat + i = random(#t) + until t[i].name ~= creature.name + + return t[i] +end + +local function empty() + for i,t in ipairs(tribes) do + t = {} + end + for i,c in ipairs(populs) do + c.delete = true + end + tribes = {} + census = {} + + save_to_file() +end + +local function add_to_tribe(t_id, creature) + creature.tribe_id = t_id + table.insert(populs[t_id], creature) + census[creature.name] = creature + dirty = true +end + +minetest.register_globalstep(function(dtime) + timer = timer + dtime; + if timer >= 30 and dirty then + save_to_file() + end +end) + +minetest.register_on_shutdown(function() + adaptive_ai.population.save_to_file() +end) + +population.tribes = tribes +population.load_tribes = load_tribes +population.create_tribe = create_tribe +population.get_from_census = get_from_census +population.add_to_tribe = add_to_tribe +population.empty = empty +population.get_couple = get_couple +population.default_size = default_size +population.default_range = default_range +population.save_to_file = save_to_file + +adaptive_ai.population = population diff --git a/textures/aa_imp_face.png b/textures/aa_imp_face.png new file mode 100644 index 0000000000000000000000000000000000000000..1f5dc2e1a3e2dfbab6c14cb5b1bc5044e696f51c GIT binary patch literal 2989 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 literal 0 HcmV?d00001 diff --git a/textures/aa_imp_side.png b/textures/aa_imp_side.png new file mode 100644 index 0000000000000000000000000000000000000000..43bd76be232ec8bcd427c78d0dac6dff7959c3b8 GIT binary patch literal 2881 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 literal 0 HcmV?d00001 diff --git a/textures/aa_imp_top.png b/textures/aa_imp_top.png new file mode 100644 index 0000000000000000000000000000000000000000..3aee1765e0b87ff098b1fada082399c3221566c5 GIT binary patch literal 2899 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 literal 0 HcmV?d00001 diff --git a/textures/aa_moondrum_roots.png b/textures/aa_moondrum_roots.png new file mode 100644 index 0000000000000000000000000000000000000000..d4cf9d6a090070d0e82e0b4485602d353caf3a1f GIT binary patch literal 3015 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 JPDHLkV1myKou>c* literal 0 HcmV?d00001 diff --git a/textures/tnt_smoke.png b/textures/tnt_smoke.png new file mode 100644 index 0000000000000000000000000000000000000000..488b50fe958d33fa4cd50fa383a4685db045def5 GIT binary patch literal 202 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`Y)RhkE)4%caKYZ?lYt_f1s;*b z3=G`DAk4@xYmNj^Fw4`$F~s6@a>4>ti^@+=>QiQE7$hEQ`G3RY@DXvHh=$u6+S=Ge zd|6pp|9|XU+`ubsmaui^1PiVPOaAGbFCN{bP0l+XkK@H9k* literal 0 HcmV?d00001 diff --git a/textures/transparent.png b/textures/transparent.png new file mode 100644 index 0000000000000000000000000000000000000000..86e8064b4b49be0dd58c8889f12c5991c84271d0 GIT binary patch literal 2808 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 KMNUMnLSTZ0JTa{R literal 0 HcmV?d00001 diff --git a/training/noise.lua b/training/noise.lua new file mode 100644 index 0000000..ac8043e --- /dev/null +++ b/training/noise.lua @@ -0,0 +1,182 @@ +--[[--------------------------------------------- +********************************************************************************** +Simplex Noise Module, Translated by Levybreak +Modified by Jared "Nergal" Hewitt for use with MapGen for Love2D +Original Source: http://staffwww.itn.liu.se/~stegu/simplexnoise/simplexnoise.pdf + The code there is in java, the original implementation by Ken Perlin +********************************************************************************** +--]]--------------------------------------------- + +require "bit" +local LuaBit = bit + +SimplexNoise = {} + +SimplexNoise.Gradients3D = {{1,1,0},{-1,1,0},{1,-1,0},{-1,-1,0}, +{1,0,1},{-1,0,1},{1,0,-1},{-1,0,-1}, +{0,1,1},{0,-1,1},{0,1,-1},{0,-1,-1}} + +for i=1,#SimplexNoise.Gradients3D do + SimplexNoise.Gradients3D[i-1] = SimplexNoise.Gradients3D[i] + SimplexNoise.Gradients3D[i] = nil +end + +function SimplexNoise.seedP(seed) + s2 = seed * 1234567 + + -- reset all the things + SimplexNoise.p = {} + SimplexNoise.Prev2D = {} + SimplexNoise.PrevBlur2D = {} + + local r = 0 + for i=1, 256 do + SimplexNoise.p[i] = (s2+math.floor(s2/i)) % 256 + end + -- To remove the need for index wrapping, double the permutation table length + for i=1,#SimplexNoise.p do + SimplexNoise.p[i-1] = SimplexNoise.p[i] + SimplexNoise.p[i] = nil + end + + SimplexNoise.perm = {} + + for i=0,255 do + SimplexNoise.perm[i] = SimplexNoise.p[i] + SimplexNoise.perm[i+256] = SimplexNoise.p[i] + end +end + +-- just to have some data +--SimplexNoise.seedP(101) + +SimplexNoise.Dot2D = function(tbl, x, y) + return tbl[1]*x + tbl[2]*y +end + +SimplexNoise.Prev2D = {} + +-- 2D simplex noise +SimplexNoise.Noise2D = function(xin, yin) + if SimplexNoise.Prev2D[xin] and SimplexNoise.Prev2D[xin][yin] then return SimplexNoise.Prev2D[xin][yin] end + + local n0, n1, n2 -- Noise contributions from the three corners + -- Skew the input space to determine which simplex cell we're in + local F2 = 0.5*(math.sqrt(3.0)-1.0) + local s = (xin+yin)*F2 -- Hairy factor for 2D + local i = math.floor(xin+s) + local j = math.floor(yin+s) + local G2 = (3.0-math.sqrt(3.0))/6.0 + + local t = (i+j)*G2 + local X0 = i-t -- Unskew the cell origin back to (x,y) space + local Y0 = j-t + local x0 = xin-X0 -- The x,y distances from the cell origin + local y0 = yin-Y0 + + -- For the 2D case, the simplex shape is an equilateral triangle. + -- Determine which simplex we are in. + local i1, j1; -- Offsets for second (middle) corner of simplex in (i,j) coords + if(x0>y0) then + i1=1 + j1=0 -- lower triangle, XY order: (0,0)->(1,0)->(1,1) + else + i1=0 + j1=1 -- upper triangle, YX order: (0,0)->(0,1)->(1,1) + end + + -- A step of (1,0) in (i,j) means a step of (1-c,-c) in (x,y), and + -- a step of (0,1) in (i,j) means a step of (-c,1-c) in (x,y), where + -- c = (3-sqrt(3))/6 + + local x1 = x0 - i1 + G2 -- Offsets for middle corner in (x,y) unskewed coords + local y1 = y0 - j1 + G2 + local x2 = x0 - 1.0 + 2.0 * G2 -- Offsets for last corner in (x,y) unskewed coords + local y2 = y0 - 1.0 + 2.0 * G2 + + -- Work out the hashed gradient indices of the three simplex corners + local ii = LuaBit.band(i , 255) + local jj = LuaBit.band(j , 255) + local gi0 = SimplexNoise.perm[ii+SimplexNoise.perm[jj]] % 12 + local gi1 = SimplexNoise.perm[ii+i1+SimplexNoise.perm[jj+j1]] % 12 + local gi2 = SimplexNoise.perm[ii+1+SimplexNoise.perm[jj+1]] % 12 + + -- Calculate the contribution from the three corners + local t0 = 0.5 - x0*x0-y0*y0 + if t0<0 then + n0 = 0.0; + else + t0 = t0 * t0 + n0 = t0 * t0 * SimplexNoise.Dot2D(SimplexNoise.Gradients3D[gi0], x0, y0) -- (x,y) of Gradients3D used for 2D gradient + end + + local t1 = 0.5 - x1*x1-y1*y1; + if (t1<0) then + n1 = 0.0 + else + t1 = t1*t1 + n1 = t1 * t1 * SimplexNoise.Dot2D(SimplexNoise.Gradients3D[gi1], x1, y1) + end + + local t2 = 0.5 - x2*x2-y2*y2; + if (t2<0) then + n2 = 0.0 + else + t2 = t2*t2 + n2 = t2 * t2 * SimplexNoise.Dot2D(SimplexNoise.Gradients3D[gi2], x2, y2) + end + + + -- Add contributions from each corner to get the final noise value. + -- The result is scaled to return values in the localerval [-1,1]. + + local retval = 70.0 * (n0 + n1 + n2) + + if not SimplexNoise.Prev2D[xin] then SimplexNoise.Prev2D[xin] = {} end + SimplexNoise.Prev2D[xin][yin] = retval + + return retval +end + +SimplexNoise.e = 2.71828182845904523536 + +SimplexNoise.PrevBlur2D = {} + +SimplexNoise.GBlur2D = function(x,y,stdDev) + if SimplexNoise.PrevBlur2D[x] and SimplexNoise.PrevBlur2D[x][y] and SimplexNoise.PrevBlur2D[x][y][stdDev] then return SimplexNoise.PrevBlur2D[x][y][stdDev] end + local pwr = ((x^2+y^2)/(2*(stdDev^2)))*-1 + local ret = ((1/(2*math.pi*(stdDev^2)))*e)^pwr + if not SimplexNoise.PrevBlur2D[x] then PrevBlur2D[x] = {} end + if not SimplexNoise.PrevBlur2D[x][y] then PrevBlur2D[x][y] = {} end + SimplexNoise.PrevBlur2D[x][y][stdDev] = ret + return ret +end + +SimplexNoise.FractalSum2DNoise = function(x,y,itier) --very expensive, much more so that standard 2D noise. + local ret = SimplexNoise.Noise2D(x,y) + for i=1,itier do + local itier = 2^itier + ret = ret + (i/itier)*(Noise2D(x*(itier/i),y*(itier/i))) + end + return ret +end + +SimplexNoise.FractalSumAbs2DNoise = function(x,y,itier) --very expensive, much more so that standard 2D noise. + local ret = math.abs(SimplexNoise.Noise2D(x,y)) + for i=1,itier do + local itier = 2^itier + ret = ret + (i/itier)*(math.abs(SimplexNoise.Noise2D(x*(itier/i),y*(itier/i)))) + end + return ret +end + +SimplexNoise.Turbulent2DNoise = function(x,y,itier) --very expensive, much more so that standard 2D noise. + local ret = math.abs(SimplexNoise.Noise2D(x,y)) + for i=1,itier do + local itier = 2^itier + ret = ret + (i/itier)*(math.abs(SimplexNoise.Noise2D(x*(itier/i),y*(itier/i)))) + end + return math.sin(x+ret) +end + +--return SimplexNoise diff --git a/training/trained_kobolds.txt b/training/trained_kobolds.txt new file mode 100644 index 0000000..bbbe11c --- /dev/null +++ b/training/trained_kobolds.txt @@ -0,0 +1,112 @@ +KOBOLD +genes +{5,11,6,4,12,8,8,4,3,1,2,5,5,2,6,7,11,12,2,11,5,7,2,4,10,3,8,5,4,4,1,8,10,7,12,9,6,11,8,8,8,3,6,1,4,9,6,7,4,8,1,8,5,9,12,10,10,10,1,9,1,6,12,8,11,9,4,1,5,12,2,1,11,8,4,9,5,7,7,2,2,9,8,9,6,3,10,2,5,8,3,1,12,10,11,8,10,7,3,12,4,4,1,2,3,11,12,8,12,10,7,9,10,9,3,10,1,4,11,9,8,10,8,8,6,10,3,6,2,12,9,4,1,7,11,11,7,9,5,12,11,9,12,5,6,7,4,3,11,12,11,5,11,10,12,9,5,11,11,11,2,11,9,10,12,7,7,6,4,6,2,12,8,1,7,10,6,2,3,1,4,8,10,5,12,12,3,9,11,12,4,7,4,8,7,11,2,8,11,7,11,2,9,11,4,12,6,10,10,6,11,4,11,3,7,2,8,12,1,7,6,12,3,2,2,8,6,12,11,11,8,1,11,4,10,5,12,11,11,4,4,5,3,6,5,4,8,11,9,8,7,7,4,3,7,8,7,7,11,2,12,3,2,5,3,2,12,12,8,8,6,8,9,3,9,12,11,8,1,11,5,2,1,2,2,7,3,4,10,6,9,6,10,2,12,1,1,1,12,5,8,4,3,1,9,8,7,8,2,2,4,5,2,6,10,7,12,6,6,5,11,9,8,12,11,8,6,11,12,4,3,5,7,4,6,11,4,9,5,3,8,4,3,1,3,11,1,6,12,12,3,5,1,11,9,12,9,3,8,8,7,6,7,9,11,11,3,11,8,8,5,12,4,6,4,9,1,2,12,8,2,6,5,9,10,3,11,1,3,4,4,7,9,2,2,2,8,7,9,10,9,10,9,9,4,12,8,7,12,2,12,9,2,3,6,8,6,6,8,11,6,2,12,12,6,10,9,9,2,12,8,7,12,3,2,9,6,6,10,2,12,8,5,10,10,3,4,2,10,3,2,5,12,6,8,4,4,5,4,10,1,7,4,4,9,4,1,6,10,12,1,6,5,6,7,7,4,10,7,4,4,3,2,9,10,3,1,6,8,1,10,2,3,2,12,6,7,3,10,6,1,8,10,7,11,5,12,2,9,5,4,5,5,4,11,5,3,1,8,7,8,5,10,12,8,8,12,9,4,4,4,9,3,9,7,2,3,1,6,12,7,9,7,4,6,3,5,12,7,5,3,6,10,5,5,8,9,5,8,3,11,9,12,11,6,3,4,4,6,10,1,8,2,8,12,12,4,5,6,4,9,8,10,1,2,3,12,7,7,9,3,6,12,1,12,5,1,11,3,2,3,4,3,3,4,6,11,10,3,1,8,1,1,1,8,3,6,4,4,5,11,8,8,3,6,4,1,1,8,9,5,2,9,8,6,11,3,12,8,6,12,9,11,11,7,10,7,6,3,12,8,12,2,6,3,8,4,8,7,5,5,7,2,12,2,1,1,4,10,8,5,4,1,1,3,5,1,8,9,1,2,2,1,5,4,5,2,9,1,10,1,11,11,8,5,9,11,3,10,2,11,2,4,7,7,2,7,6,8,9,3,1,11,9,9,9,10,8,11,7,10,8,1,11,5,7,1,6,8,11,6,10,8,4,2,6,4,5,7,2,9,11,3,7,2,12,5,8,7,9,5,11,1,4,8,6,4,5,5,3,1,3,5,11,6,7,9,11,10,8,6,9,4,1,11,11,9,5,4,4,4,12,4,10,3,11,11,1,12,12,5,7,9,4,8,1,12,10,2,11,11,3,3,11,2,3,9,12,1,1,10,5,1,5,6,12,5,12,4,11,5,1,10,10,6,4,12,5,3,7,6,6,11,7,4,4,4,7,5,12,5,12,9,3,11,5,4,6,6,5,5,2,5,4,1,6,3,4,2,10,11,7,1,4,9,11,1,6,8,10,1,9,6,7,5,3,9,1,1,8,4,5,10,1,3,9,12,6,6,7,10,7,4,12,5,5,5,7,9,5,5,11,10,7,4,6,3,12,9,8,3,11,12,11,12,3,5,3,6,1,1,9,11,11,3,7,7,7,10,10,6,12,10,11,9,2,1,2,12,1,1,1,6,4,9,3,11,12,10,2,1,4,2,5,4,3,2,1,3,12,7,5,12,6,7,4,6,11,11,6,10,2,6,3,6,10,12,1,11,10,1,12,2,8,10,7,7,1,9,6,1,8,3,9,4,11,8,4,10,12,1,4,3,6,11,9,9,6,3,8,4,9,2,5,1,5,1,1,7,11,7,8,12,3,10,9,1,1,3,11,4,3,4,8,6,3,12,2,8,8,4,12,4,12,7,10,4,1,9,10,3,3,1,10,5,8,11,10,5,7,4,5,9,10,5,9,5,6,4,10,1,9,8,9,5,3,3,5,2,7,5,1,12,4,2,9,2,1,9,4,5,4,5,2,6,4,8,2,8,7,2,8,2,12,8,1,2,8,7,6,9,8,10,10,11,8,6,10,8,9,9,11,7,12,9,9,2,4,10,4,11,9,7,9,5,2,9,7,4,5,12,12,12,5,9,10,4,11,7,4,6,12,7,3,8,9,6,1,2,8,12,9,12,6,9,2,10,12,3,3,12,2,9,3,1,7,12,9,9,3,7,8,8,6,3,5,12,4,1,7,12,2,5,10,7,2,11,6,1,11,2,9,5,4,3,3,8,5,10,5,9,8,3,1,9,3,4,8,3,10,5,2,6,12,8,12,2,7,9,9,12,6,2,3,3,10,1,10,6,12,6,9,7,6,11,4,4,1,11,7,1,8,9,11,6,8,1,11,10,8,9,5,6,3,10,3,9,11,12,3,7,6,8,1,8,7,10,10,9,12,4,1,6,10,6,7,1,11,11,3,4,2,11,6,5,5,3,6,10,8,4,6,8,10,3,7,5,2,1,7,10,7,9,12,8,10,1,12,4,5,8,9,3,12,4,2,2,2,1,12,2,12,4,1,4,12,1,3,8,6,12,7,1,4,7,9,4,2,9,12,8,6,7,8,10,9,6,8,4,3,1,7,4,6,12,2,10,6,4,10,6,2,9,3,6,9,4,12,8,9,10,8,9,4,1,12,3,12,8,3,9,4,4,7,1,2,7,8,12,8,7,4,5,9,11,2,6,12,11,6,4,8,8,10,3,2,2,8,2,6,11,2,9,11,11,11,3,10,5,7,11,1,12,6,11,2,6,1,4,2,6,10,3,3,2,6,9,6,1,7,9,3,4,1,9,6,4,6,3,3,7,8,10,7,10,1,9,2,12,10,3,9,8,2,5,3,8,2,1,9,9,11,1,3,2,5,9,4,6,7,9,5,3,7,12,2,12,4,6,6,10,5,4,9,8,9,10,1,9,11,8,10,9,7,9,3,7,8,3,9,3,7,4,4,10,11,7,4,6,3,10,10,1,7,6,6,6,1,10,5,12,4,11,2,2,8,8,6,6,10,7,6,7,6,9,6,9,4,7,4,4,9,8,3,6,11,10,10,3,10,5,9,5,12,12,9,6,11,7,2,3,4,10,11,9,9,9,6,3,6,9,12,6,1,4,2,11,8,2,6,7,3,8,3,12,8,9,9,12,9,1,1,3,10,1,4,8,10,9,5,10,10,6,1,12,1,11,6,9,3,6,11,11,2,3,9,1,7,6,2,11,11,6,7,4,12,12,5,7,12,8,9,5,11,3,1,12,1,6,5,7,1,1,7,3,8,3,9,8,5,8,9,11,5,10,3,1,3,1,7,5,10,8,11,11,9,1,7,5,3,7,1,7,5,9,11,2,7,10,3,1,6,8,12,1,5,1,11,5,12,6,7,11,7,1,1,12,8,7,8,8,5,6,9,2,2,5,4,6,1,5,6,2,11,5,5,11,3,9,4,12,7,12,2,3,8,6,10,4,11,6,7,1,6,10,10,4,8,6,1,6,7,1,3,7,9,2,8,4,8,12,5,12,12,2,9,7,7,1,4,8,5,9,3,10,10,11,7,3,2,8,11,6,8,2,3,5,4,6,1,6,5,4,8,2,7,12,2,5,2,1,11,10,6,5,11,8,6,7,10,7,9,11,12,8,4,11,9,4,4,2,2,6,11,5,5,2,10,9,3,8,10,5,8,4,5,1,4,8,2,11,2,6,2,5,1,7,11,5,12,3,10,8,7,3,12,1,12,1,9,8,5,5,5,11,3,8,1,7,8,7,7,7,3,1,4,10,3,6,1,3,12,1,3,7,5,10,12,2,5,2,1,7,8,7,9,2,4,2,2,11,6,6,7,8,10,9,9,1,10,3,5,7,10,4,2,3,7,3,5,10,6,8,1,8,10,8,8,4,11,11,2,4,8,} +focus +0.23404331377891 +firstname +"Yunuta" +midname +"Patachpuanuy" +family +"Kukipuinzik" +food +{} +END_KOBOLD +KOBOLD +genes +{5,5,7,4,7,11,7,11,6,4,6,8,12,2,6,11,3,12,2,5,4,4,2,1,9,7,8,2,11,5,4,8,4,11,11,2,6,11,8,8,4,3,11,2,1,5,2,7,12,2,10,1,11,9,7,10,11,7,12,9,6,6,12,12,6,11,1,6,4,8,3,1,10,6,4,3,3,4,7,1,7,2,6,1,12,9,4,8,5,10,3,5,11,5,11,8,9,10,3,3,4,9,8,11,4,12,5,5,2,8,10,9,11,2,3,12,1,6,10,9,9,1,8,7,10,10,10,6,2,10,11,12,3,1,3,1,9,9,4,12,11,11,12,11,3,12,9,8,11,3,7,5,12,4,5,1,4,2,11,11,2,1,6,4,10,2,7,7,4,1,2,11,2,2,6,1,12,1,11,3,7,8,10,3,7,3,6,11,11,10,4,1,4,8,11,11,8,8,10,7,10,3,2,11,3,1,3,10,11,6,1,12,9,11,11,2,5,6,1,11,4,12,10,2,3,4,10,12,5,11,9,2,4,5,4,6,2,2,2,12,12,5,3,9,8,11,12,11,5,1,12,2,12,3,6,3,4,9,12,11,11,3,2,5,8,6,2,12,1,1,2,10,5,9,12,12,4,12,11,11,5,1,1,2,6,9,3,4,4,6,9,6,3,9,7,1,4,5,5,5,5,2,9,3,10,6,1,9,12,8,12,12,5,5,2,7,7,6,8,5,10,3,8,8,11,12,10,7,12,3,7,2,4,11,2,9,4,3,7,11,2,9,10,12,11,10,6,8,12,12,9,8,2,5,8,1,6,5,3,8,7,7,12,8,10,9,3,10,1,8,2,12,10,12,1,4,11,2,12,12,6,12,4,1,1,1,11,1,3,1,4,2,8,2,3,9,2,4,2,1,4,9,11,11,4,6,6,11,5,7,1,2,10,3,11,8,3,8,9,7,10,12,7,1,11,12,3,5,12,6,8,12,11,9,8,1,5,1,10,4,7,3,5,7,9,2,11,11,10,3,4,8,6,7,11,1,10,6,1,7,9,4,1,8,6,1,1,4,7,2,6,10,7,12,2,7,5,12,7,5,2,12,2,8,11,1,6,6,5,5,4,10,4,2,12,6,5,5,1,9,4,12,10,12,6,4,1,3,1,1,4,2,5,4,9,8,11,5,4,11,4,3,10,6,8,9,9,9,6,7,8,12,6,9,9,3,3,5,2,2,5,4,12,8,1,3,6,12,12,11,7,1,10,5,12,4,5,10,11,7,1,7,6,2,9,10,7,4,2,12,1,12,2,8,7,12,5,1,12,4,4,4,4,10,12,9,10,10,12,5,3,1,10,12,12,12,1,1,2,6,11,2,11,8,12,8,11,10,8,7,8,12,3,8,2,10,9,10,10,7,8,8,10,9,5,5,4,1,9,8,6,4,7,3,2,6,5,7,8,9,4,3,11,12,7,11,7,9,2,12,11,8,3,3,3,1,12,5,4,5,9,9,4,4,2,1,11,3,10,1,8,8,9,3,7,5,12,8,7,10,5,3,5,12,12,6,8,8,11,2,7,11,3,9,5,6,9,3,1,8,11,5,4,5,7,5,11,1,2,1,6,6,3,7,7,12,6,1,9,10,12,7,10,11,5,8,3,6,5,12,6,10,4,4,6,6,4,5,7,3,8,11,3,7,4,6,1,8,11,1,6,11,4,2,8,12,3,4,2,3,5,10,8,8,2,9,9,12,7,9,5,6,1,1,10,6,8,6,3,11,11,9,2,10,4,4,11,1,7,3,2,3,4,11,3,5,7,11,3,9,11,9,5,4,5,3,9,4,1,7,9,5,2,6,1,10,5,5,11,11,2,2,7,10,6,3,4,4,4,2,5,12,9,11,6,4,11,9,5,6,10,3,3,3,11,8,11,6,9,2,1,10,11,5,11,5,11,4,6,11,5,7,5,7,7,2,1,7,7,4,1,10,10,7,6,7,3,2,12,7,4,5,10,8,2,3,6,3,6,11,8,6,1,12,6,1,11,7,7,3,5,8,4,10,6,10,3,7,9,8,9,9,3,3,1,6,10,7,8,2,6,12,11,7,7,8,7,7,2,10,6,10,2,12,12,2,9,5,3,7,8,5,5,4,8,3,11,12,12,1,4,11,2,5,4,12,8,1,5,12,7,1,11,11,6,6,4,1,4,6,1,6,6,4,8,5,2,1,10,12,11,8,12,3,10,1,9,12,2,8,6,8,10,9,6,11,10,6,10,7,11,5,11,6,1,12,4,1,4,3,1,8,5,5,1,12,12,4,7,1,5,8,12,11,8,12,6,8,12,11,4,11,4,7,4,3,12,12,3,12,4,8,3,9,7,10,4,3,2,12,7,1,1,1,2,8,7,7,12,2,12,6,3,10,9,1,11,5,7,10,1,7,9,12,5,3,1,5,11,2,3,1,4,2,5,1,2,9,2,11,10,4,2,6,6,10,6,4,8,7,2,11,9,12,8,5,4,2,9,11,3,1,4,10,8,4,6,5,8,9,9,11,1,12,12,9,6,4,5,3,4,2,6,8,7,5,6,3,4,8,4,12,8,12,3,6,1,11,1,4,6,12,6,12,1,8,10,5,2,3,12,9,3,6,2,12,8,2,4,3,6,8,11,11,1,7,10,9,2,10,1,12,2,6,4,12,2,4,1,12,7,4,5,10,3,11,11,1,6,6,10,9,7,8,3,10,9,6,10,8,12,11,3,9,9,8,3,2,3,10,5,4,5,4,9,12,8,1,8,10,3,6,2,6,3,9,2,10,7,6,2,2,4,11,7,8,6,12,1,10,7,2,11,11,1,3,5,11,9,7,10,5,1,9,12,12,12,11,12,10,4,11,8,2,10,3,2,7,9,2,2,2,6,9,9,11,11,10,3,6,5,12,5,2,1,6,2,10,2,8,10,7,2,10,1,7,12,10,7,2,6,7,12,8,12,11,9,6,11,5,11,8,5,7,5,10,6,4,11,5,2,12,9,7,7,7,5,10,3,5,7,10,8,4,7,9,1,6,8,12,1,4,2,12,2,10,5,1,5,3,6,9,8,3,12,1,6,7,8,10,6,12,9,2,10,4,4,2,8,3,11,9,11,3,1,12,3,12,5,3,9,4,8,7,3,2,6,3,11,4,9,9,4,12,9,6,1,7,11,4,5,1,6,5,3,2,2,4,4,10,8,1,12,2,11,11,12,7,12,9,11,8,2,1,11,1,6,10,6,7,10,9,11,2,12,6,5,6,4,6,9,4,4,4,2,3,9,9,12,5,3,8,5,2,10,10,9,2,4,9,11,10,6,7,3,4,1,2,4,9,11,4,10,4,10,6,4,5,8,7,2,12,10,7,8,2,12,4,9,6,8,5,6,5,1,9,2,11,9,11,1,10,9,7,5,8,6,8,9,11,8,2,2,4,10,5,12,5,2,6,6,12,11,9,4,7,6,1,8,11,12,1,10,2,7,6,11,3,6,3,5,5,12,11,9,12,9,2,7,1,11,9,7,11,6,3,10,5,3,5,5,12,10,10,12,8,3,12,12,7,6,12,10,12,9,11,2,5,9,6,12,4,6,1,9,8,5,8,4,6,5,1,7,3,10,11,8,3,11,9,8,4,6,10,11,10,5,3,6,3,1,6,12,8,6,2,11,1,9,4,1,9,3,11,2,5,2,7,10,2,1,2,6,7,3,8,7,11,1,10,8,8,6,1,8,7,12,11,9,11,6,7,8,4,2,3,12,9,10,7,12,1,3,7,5,10,2,8,7,11,5,11,8,6,9,3,11,1,12,8,12,6,7,5,5,3,2,6,10,1,5,12,2,1,12,6,1,12,9,10,10,5,3,6,9,10,7,5,1,9,6,2,6,4,6,4,6,9,5,6,9,7,4,11,6,5,9,11,11,6,3,11,9,8,3,2,6,3,6,8,4,8,1,1,6,12,4,10,11,8,6,6,8,12,7,8,4,7,5,9,12,5,1,12,9,1,4,10,8,6,12,1,4,3,10,1,10,7,2,3,6,9,6,9,2,3,12,8,1,11,6,6,9,3,1,1,12,9,5,7,6,5,6,11,5,1,2,9,1,1,8,9,10,6,8,8,11,9,10,4,9,5,1,9,9,11,5,12,12,10,12,10,8,9,9,11,10,10,8,5,11,1,9,6,5,1,5,11,9,12,8,6,10,4,3,11,1,2,2,11,3,8,5,8,8,8,11,5,7,9,7,6,1,1,10,4,7,10,12,7,1,5,3,7,2,6,10,9,10,8,8,2,7,6,11,9,9,4,8,7,7,6,4,3,2,10,5,1,2,8,9,10,7,1,4,7,4,11,3,5,12,12,12,1,2,2,12,1,11,7,10,4,1,8,} +focus +0.33772664250454 +firstname +"Kafaki" +midname +"Patachpuanuy" +family +"Kukipuinzik" +food +{4,} +END_KOBOLD +KOBOLD +genes +{5,11,3,6,2,3,12,11,3,11,2,11,10,3,12,9,10,3,9,8,1,3,4,3,10,6,8,4,3,5,10,9,4,10,9,7,12,12,3,5,11,3,12,8,2,9,11,4,12,7,12,7,8,10,10,7,7,1,2,7,10,7,10,4,11,9,12,9,7,1,1,8,1,3,3,10,2,10,2,8,1,5,8,9,11,9,8,3,8,11,4,3,3,12,2,10,5,8,6,3,5,12,7,10,4,11,5,11,8,4,7,11,7,4,8,8,9,10,3,3,6,9,5,5,11,7,12,6,7,1,2,5,12,10,3,4,9,11,6,1,2,7,12,10,3,8,5,3,11,1,10,9,11,5,11,1,8,5,10,7,7,10,5,2,12,10,9,3,7,8,5,1,2,7,5,8,12,9,3,11,5,8,6,4,2,9,10,9,12,7,10,7,3,4,5,3,1,3,10,12,5,12,2,8,4,4,7,7,11,6,1,11,8,11,4,8,5,5,7,10,5,9,8,10,3,10,8,1,8,4,2,9,11,1,2,5,2,10,7,6,12,9,3,5,1,8,4,3,10,6,12,1,6,1,12,8,2,11,4,6,11,3,1,2,1,6,7,10,10,1,10,8,9,8,4,12,11,4,8,6,4,8,10,9,1,9,7,12,6,8,4,12,10,1,9,8,11,11,3,2,6,11,12,3,3,12,9,2,3,12,3,6,8,7,2,4,11,4,11,3,7,4,3,4,11,2,5,10,6,6,11,12,5,9,11,6,12,3,5,3,5,1,9,1,4,9,1,7,8,12,12,10,1,2,5,8,1,3,6,1,7,2,5,2,8,12,1,11,5,7,4,12,6,1,5,5,8,5,6,4,8,1,11,8,8,11,10,8,10,3,10,8,6,5,12,9,6,10,8,1,5,1,9,12,5,9,5,2,10,5,12,1,10,7,7,7,2,3,2,4,11,6,5,12,12,9,1,7,8,10,10,1,7,7,9,5,4,5,3,9,11,1,8,7,9,1,8,11,3,1,7,1,2,11,1,1,12,4,1,10,10,5,3,1,6,2,12,11,2,12,6,2,7,7,3,9,6,11,10,12,6,9,6,11,4,10,3,12,5,7,3,4,3,7,2,8,8,10,12,6,5,3,8,10,12,5,3,12,1,2,5,11,8,4,4,10,3,9,7,8,9,7,10,1,4,1,9,7,6,1,10,9,12,3,3,1,12,11,7,8,12,9,11,9,6,4,6,5,1,11,1,11,9,2,9,8,2,10,10,7,6,5,3,6,4,8,7,8,1,8,6,8,7,4,6,5,2,7,1,8,8,12,2,4,1,4,8,4,12,3,11,11,5,10,3,8,9,8,7,7,2,10,2,8,6,7,6,12,12,1,11,4,6,12,4,10,11,6,10,8,4,4,7,11,8,1,7,3,9,7,4,12,4,12,5,7,4,6,8,10,10,6,6,12,7,11,7,4,9,10,11,11,12,5,5,11,10,12,1,12,1,6,1,5,2,1,3,9,5,4,4,9,1,2,7,1,4,10,7,11,7,2,7,1,12,5,7,7,11,12,5,3,2,1,8,12,4,3,11,7,6,8,12,3,2,2,3,9,3,12,10,8,7,3,12,11,7,10,6,1,3,11,4,3,2,1,6,1,9,6,1,5,9,6,10,2,5,10,5,2,4,6,9,6,10,10,5,4,8,5,5,2,9,2,1,8,10,3,7,5,2,9,10,7,5,2,11,9,8,2,4,2,12,3,4,6,6,11,5,11,6,11,7,3,11,12,4,8,11,9,12,3,10,7,7,2,5,3,3,5,5,11,7,10,11,11,12,5,2,7,9,9,10,10,12,11,2,7,3,11,8,10,2,4,2,2,9,1,3,9,6,5,1,6,10,7,2,10,3,12,4,8,5,12,10,2,12,4,1,12,4,10,2,10,6,2,10,6,2,12,11,9,3,3,10,7,8,7,11,1,1,12,7,5,6,1,12,6,8,6,2,7,4,3,7,3,10,7,12,11,11,12,11,7,2,2,8,11,8,3,7,1,9,8,9,5,10,8,9,1,1,1,8,7,1,5,10,3,5,7,2,3,8,12,6,9,1,6,9,4,3,2,12,9,12,2,9,9,12,7,8,11,11,11,10,5,1,1,5,4,1,12,8,4,4,9,1,6,8,5,8,12,2,7,2,3,7,9,2,12,6,1,12,8,10,11,7,11,6,4,9,6,4,5,6,8,3,1,4,6,12,5,5,5,7,7,7,10,8,6,7,7,8,5,11,10,12,9,9,9,9,11,5,9,1,9,5,6,6,1,10,9,12,6,11,3,12,4,7,10,4,9,10,7,11,8,8,10,11,3,4,8,1,8,1,10,10,9,11,9,5,12,7,1,3,6,4,6,12,10,8,3,8,6,4,1,11,5,8,3,9,11,6,2,10,1,6,1,4,10,5,4,7,12,3,11,10,10,10,11,11,6,10,12,5,3,6,3,2,8,10,11,1,9,7,11,3,1,10,12,2,1,7,6,1,8,11,6,1,8,9,10,4,2,7,10,10,4,7,5,12,4,11,11,6,12,7,12,5,9,9,11,2,11,10,3,1,11,7,9,2,9,4,1,4,11,2,6,4,6,5,5,6,7,9,10,3,1,3,9,8,7,9,10,6,11,5,9,9,12,8,3,6,3,10,7,12,4,11,11,10,10,12,4,4,6,4,3,1,8,9,1,8,8,12,1,9,2,11,4,2,6,8,12,1,6,11,10,2,6,9,11,2,9,5,2,8,5,4,3,6,6,1,6,2,8,2,5,4,6,10,7,7,10,9,6,6,11,1,3,11,1,6,4,9,7,1,8,10,11,8,4,5,6,12,5,7,2,11,1,4,12,1,12,3,7,8,4,9,6,2,5,7,12,4,4,8,2,4,5,4,5,7,6,2,7,11,6,6,8,4,10,3,2,10,10,4,12,4,4,3,5,3,12,9,8,11,5,1,4,6,7,6,3,9,6,6,3,6,10,7,11,3,8,6,2,3,5,6,8,1,10,9,3,11,2,4,4,7,12,3,3,8,7,12,4,6,10,6,1,8,11,10,11,12,8,10,6,8,6,6,5,8,8,10,9,7,4,4,11,8,11,2,5,6,10,3,9,6,11,6,7,9,8,4,12,9,6,11,6,2,6,7,8,11,5,10,8,7,9,5,9,8,11,2,2,7,10,12,8,6,8,5,8,11,11,12,8,2,9,7,2,2,1,6,1,8,1,12,3,4,10,9,2,12,2,10,12,11,7,12,5,2,3,11,5,7,12,3,10,10,1,10,6,7,1,5,9,5,10,1,3,3,1,4,3,11,6,5,11,11,3,11,12,10,6,6,9,4,4,2,10,10,5,11,8,1,1,1,2,4,6,10,10,11,1,7,12,2,1,1,7,10,11,9,10,8,10,6,2,4,6,2,2,1,12,4,11,1,2,12,2,2,2,7,4,1,11,8,11,12,6,3,1,5,1,12,7,10,7,2,12,2,6,2,1,10,2,4,12,1,5,1,6,4,5,3,12,6,11,1,4,8,3,9,10,10,9,2,6,7,11,9,5,3,9,11,9,5,6,4,6,10,10,4,8,7,4,9,11,1,11,7,7,3,8,12,9,2,10,3,2,2,12,6,5,6,8,9,1,6,2,1,1,3,2,1,7,12,11,5,2,4,5,3,4,12,4,5,7,8,4,1,10,6,4,3,2,5,1,2,8,3,1,12,12,10,6,2,1,1,5,10,8,1,8,11,4,4,3,12,4,6,9,9,9,9,2,11,7,4,6,4,5,3,6,10,8,4,3,10,3,3,11,9,11,8,8,1,6,3,10,10,10,5,3,6,9,10,1,7,1,2,3,12,11,6,10,3,2,6,1,6,8,11,1,3,6,2,4,6,12,1,12,6,12,2,11,2,4,11,4,7,11,2,6,6,6,1,4,3,5,1,3,3,1,5,3,3,1,4,7,1,3,10,1,8,2,3,3,12,12,12,12,2,9,9,7,3,3,4,6,1,3,11,2,5,4,1,1,1,9,10,8,7,3,8,5,9,3,4,1,8,2,3,8,10,2,10,10,1,7,7,7,3,5,1,11,9,7,6,5,4,2,6,8,4,11,4,2,2,2,7,6,10,11,2,8,2,3,1,8,5,7,8,2,10,8,11,2,5,8,7,10,5,2,12,8,11,11,8,6,1,5,11,1,2,1,3,6,8,12,5,5,11,4,7,9,10,5,8,12,7,10,1,7,10,2,5,7,2,1,2,12,3,9,11,3,10,8,2,9,1,2,4,12,10,1,4,1,1,6,10,3,8,1,1,2,10,10,12,1,5,8,5,6,6,9,5,7,5,2,11,4,11,3,12,1,5,7,10,11,6,11,} +focus +0.28907365487597 +firstname +"Kisasku" +midname +"Znusasianuy" +family +"Tukipusanzik" +food +{5,} +END_KOBOLD +KOBOLD +genes +{8,2,8,9,1,4,8,10,3,4,1,9,1,7,11,11,1,8,12,6,5,5,9,12,10,11,5,3,3,10,6,3,2,9,9,10,11,5,10,6,3,3,4,3,5,9,5,2,5,11,12,4,5,1,9,9,4,10,5,9,1,10,4,8,6,9,8,1,5,12,6,11,11,12,2,1,5,7,7,6,11,8,4,9,6,3,10,2,1,4,11,7,9,1,11,8,10,10,1,12,4,7,2,1,12,4,5,9,3,8,10,9,4,3,9,3,11,4,7,6,2,6,9,9,9,10,9,6,7,7,9,8,11,12,3,6,7,7,7,1,2,6,5,10,3,7,4,6,12,11,5,4,10,6,10,9,1,5,2,11,2,1,5,3,11,12,7,6,7,8,11,1,5,7,12,8,12,7,12,11,5,8,10,2,11,2,11,7,11,12,8,3,3,8,11,2,2,6,11,10,11,2,2,10,4,3,7,10,1,8,11,11,9,3,4,5,8,11,5,3,4,5,3,1,3,10,12,8,11,7,8,6,3,4,4,2,3,5,11,4,10,7,2,6,8,12,1,5,11,8,10,2,4,10,5,5,7,5,3,1,10,2,10,5,9,2,9,7,12,8,6,8,9,10,1,12,11,8,1,10,8,9,7,2,10,9,7,5,10,1,5,6,10,6,10,4,3,6,10,10,3,4,5,1,4,10,1,4,3,2,7,5,11,10,7,6,7,4,6,4,9,9,4,11,11,1,7,7,12,6,8,5,2,4,4,6,10,2,8,7,8,8,9,1,11,5,2,8,10,3,2,5,10,2,10,2,3,12,8,10,7,1,12,1,12,11,8,11,6,9,5,12,10,12,4,9,2,2,12,7,10,11,4,11,4,11,11,12,3,1,7,4,11,10,2,8,8,11,9,10,6,10,12,6,4,5,5,2,4,4,3,1,2,8,5,5,5,5,7,10,4,6,5,12,5,4,10,9,1,6,10,9,7,4,5,8,8,5,3,5,12,9,2,1,8,10,8,1,5,2,5,11,7,12,9,4,4,4,12,2,12,3,8,7,9,4,7,10,10,11,3,2,7,1,3,2,1,10,11,10,3,1,3,9,3,10,2,10,10,1,6,4,1,11,5,6,12,11,10,5,1,6,10,8,6,2,7,2,5,5,3,12,11,8,12,10,10,6,9,7,12,10,8,12,8,9,5,8,9,6,4,9,2,5,5,10,7,3,5,2,8,5,8,8,11,9,10,11,7,3,3,10,10,11,9,11,9,6,10,7,11,4,12,9,8,3,4,8,6,2,9,6,10,2,12,12,11,9,6,9,9,11,2,2,6,3,6,7,7,1,2,9,3,1,12,3,2,7,3,11,3,1,6,3,4,2,5,10,2,7,8,5,10,12,7,1,3,6,2,5,8,4,11,9,1,1,9,1,2,5,5,4,1,8,3,7,6,1,8,6,12,9,4,3,7,11,11,8,12,12,6,12,4,1,3,8,9,4,7,5,8,6,12,12,9,5,1,4,7,8,10,9,1,1,11,1,1,4,5,10,11,11,6,5,10,5,4,9,6,3,2,4,2,2,3,10,2,7,10,9,6,8,12,10,3,7,4,6,8,4,6,1,7,6,9,9,8,2,6,9,11,8,1,8,5,2,7,10,9,11,8,4,11,4,6,3,8,4,6,8,1,11,3,7,1,12,3,8,10,11,3,11,4,12,11,12,12,5,10,3,7,3,12,3,4,5,8,7,10,5,4,12,4,5,7,5,6,10,8,3,1,9,5,5,11,3,9,4,3,11,7,7,8,10,3,9,12,11,11,6,11,12,11,11,4,12,5,12,5,7,12,7,8,5,6,7,10,9,4,11,11,10,6,12,6,4,3,2,6,8,12,11,11,7,12,7,9,5,5,12,10,9,1,3,8,8,4,2,11,9,3,1,1,4,8,6,5,4,11,11,9,4,12,4,10,7,9,7,9,2,6,10,6,5,3,5,2,1,1,4,4,1,10,12,6,1,9,8,12,7,10,6,11,12,6,2,7,7,8,8,4,4,2,10,2,5,5,4,9,8,9,12,1,3,12,3,10,3,2,8,4,2,8,12,9,7,3,7,1,5,1,10,6,9,9,2,2,10,1,7,10,1,11,8,10,3,1,8,10,5,8,2,2,7,11,7,11,6,3,12,3,10,2,6,12,4,7,11,11,6,5,1,9,3,6,10,10,1,12,2,8,8,8,5,2,7,1,8,4,6,12,8,5,9,4,1,9,4,6,9,2,10,8,1,2,10,6,6,10,10,11,4,6,1,12,4,2,4,5,8,7,12,11,3,6,1,1,12,3,3,4,4,1,10,11,3,3,2,8,8,12,6,4,4,2,11,8,7,4,10,6,8,1,3,7,11,10,12,8,5,4,10,12,1,7,6,4,6,9,5,3,9,8,1,5,3,11,11,4,5,10,1,4,5,3,9,5,7,11,8,5,10,4,2,5,7,5,9,12,7,10,8,5,10,1,3,6,8,4,12,9,7,4,2,3,3,1,1,10,8,4,9,9,12,10,10,9,2,10,4,11,4,1,3,4,4,11,9,5,9,9,2,2,1,8,7,4,5,7,8,6,12,3,1,4,11,6,1,1,8,12,8,12,6,7,3,12,1,3,4,12,3,7,11,1,7,3,9,9,3,9,8,7,3,10,5,4,1,9,9,10,10,5,10,12,9,8,1,1,11,5,2,5,1,6,12,8,5,7,9,1,8,8,1,9,1,7,5,10,10,5,2,2,12,8,8,9,5,9,9,2,6,7,3,1,5,10,9,12,2,1,10,9,9,6,11,7,5,5,12,10,5,9,6,10,8,1,8,3,11,9,5,2,6,2,6,8,12,11,3,8,6,11,4,11,7,8,3,11,7,4,4,6,9,6,8,11,6,2,7,8,2,11,6,12,10,3,6,8,1,2,10,5,10,9,7,5,6,12,7,4,11,11,9,12,5,12,12,1,12,4,9,4,10,9,8,3,10,2,2,11,11,12,12,4,7,6,5,2,8,8,8,3,1,2,8,2,5,9,12,9,4,10,12,12,11,6,12,4,7,2,4,3,6,1,5,11,12,4,12,11,2,8,5,4,9,12,7,2,9,6,3,6,5,11,4,9,10,8,3,5,10,11,8,1,12,8,8,4,9,8,4,5,9,5,10,4,4,10,9,3,2,3,9,2,3,2,6,6,9,3,3,12,11,1,8,5,5,12,3,5,3,5,6,11,2,1,1,12,3,2,10,12,4,7,12,12,6,2,3,11,4,4,11,2,1,2,4,5,2,7,10,9,7,2,1,5,9,12,9,4,2,8,3,11,12,6,2,1,7,9,12,1,3,2,8,2,4,6,4,6,11,2,9,5,5,12,6,7,5,6,11,5,2,11,4,9,10,4,7,3,4,1,7,9,6,2,6,8,2,9,4,6,11,4,11,3,5,8,10,10,12,4,9,11,7,6,1,6,2,3,2,3,1,3,7,4,9,6,7,8,9,3,6,7,9,10,4,3,11,4,10,1,5,2,11,3,6,3,2,1,9,12,9,8,11,11,11,4,2,5,3,12,1,5,7,7,5,10,6,8,10,6,10,2,1,5,8,6,3,12,8,8,6,8,2,5,9,2,10,3,1,2,7,1,5,1,8,7,1,9,8,7,1,1,3,11,2,10,1,5,11,9,2,3,4,5,5,5,2,6,1,2,7,1,12,9,5,5,8,11,4,11,1,8,1,7,1,6,5,8,10,9,5,6,2,2,9,8,8,8,9,10,5,12,1,2,8,5,7,5,1,12,2,4,4,11,9,1,3,4,12,12,10,4,11,12,10,11,3,4,6,8,11,8,9,2,8,12,12,5,5,8,3,4,11,11,9,6,11,4,5,5,9,8,8,10,6,10,1,2,2,10,9,7,2,2,3,2,2,7,6,4,11,5,4,6,7,4,10,10,9,4,6,11,9,3,2,5,1,9,11,1,3,5,7,2,12,7,2,7,5,4,3,1,11,4,10,5,7,8,4,9,3,9,8,3,7,4,5,7,7,7,2,12,3,5,1,4,8,7,5,9,2,3,2,12,10,8,10,12,4,12,6,11,1,1,8,7,4,5,6,4,1,7,7,3,9,4,4,10,7,12,11,5,10,2,2,4,7,6,4,3,8,4,11,6,7,6,2,11,2,12,2,5,6,6,3,2,12,2,5,11,7,7,1,8,8,3,9,9,3,9,11,11,12,7,3,1,8,7,12,3,3,1,12,5,7,3,9,2,5,8,3,8,2,5,7,9,10,12,2,7,6,8,5,12,8,3,10,9,6,9,12,8,8,5,9,1,8,4,8,5,7,7,8,3,5,12,7,10,4,8,9,8,10,2,1,3,4,11,2,1,8,} +focus +0.15512212367605 +firstname +"Tatakaka" +midname +"Napauuanuy" +family +"Kukipuinzik" +food +{4,} +END_KOBOLD +KOBOLD +genes +{5,11,1,12,7,2,11,12,12,9,2,9,10,3,9,8,8,4,3,1,1,11,9,10,4,2,11,4,1,11,6,10,2,3,11,11,4,2,8,1,8,9,1,10,6,4,8,4,4,8,9,9,1,1,8,10,1,1,1,8,5,2,2,2,6,11,4,7,5,2,4,9,4,7,6,3,4,1,3,9,12,2,6,3,3,7,3,2,5,5,1,3,7,7,5,7,8,12,5,4,8,12,9,10,6,7,12,1,4,7,5,12,8,11,12,12,8,10,7,10,4,8,10,11,11,3,3,2,4,11,12,6,12,12,7,6,8,7,6,3,10,1,6,9,12,12,1,10,12,3,6,10,11,12,6,10,5,5,5,2,5,8,5,6,10,3,1,7,2,7,1,5,2,11,5,12,8,4,2,3,9,9,3,6,7,5,10,7,9,1,1,9,9,4,10,7,1,4,2,10,2,5,4,10,7,1,2,10,3,6,9,5,10,6,1,5,2,8,4,4,7,10,5,10,12,9,7,4,7,2,7,4,1,10,6,6,7,2,10,11,12,5,10,1,9,2,11,10,9,12,1,4,11,6,12,5,2,4,1,3,2,4,3,6,1,5,2,3,1,7,9,7,7,4,1,1,9,9,2,8,10,2,5,7,10,9,6,12,9,5,9,8,7,7,4,5,9,2,1,11,3,6,7,10,7,3,2,9,3,9,10,8,5,5,8,8,9,7,11,9,2,4,9,4,5,9,8,3,9,8,11,3,4,3,11,4,8,8,5,4,6,9,2,12,5,4,12,2,11,5,9,3,3,11,12,3,12,2,4,4,6,6,3,1,2,12,1,7,2,2,8,4,7,6,3,7,5,1,6,2,7,10,10,7,6,3,7,3,3,5,11,6,7,6,4,4,10,11,8,10,11,10,5,5,9,4,5,5,12,4,6,8,5,6,2,1,5,12,10,7,11,2,6,10,10,2,1,3,8,1,8,6,4,2,11,11,4,9,4,5,5,2,8,11,11,7,5,5,10,8,6,1,8,6,8,10,4,12,2,11,9,5,12,6,1,4,10,10,7,12,6,10,7,4,11,10,5,3,1,3,4,8,8,12,8,11,2,9,5,6,2,8,1,1,7,5,2,6,11,2,8,3,12,1,1,8,9,3,8,2,1,12,11,10,3,8,8,11,3,8,12,3,7,10,6,6,10,7,1,12,10,7,11,3,6,6,11,3,4,11,3,11,8,11,10,4,11,5,6,3,5,1,3,5,8,10,3,10,10,6,9,5,8,3,8,1,9,1,7,5,1,5,11,11,5,7,9,4,3,4,5,11,3,2,3,2,3,2,6,2,3,5,7,11,12,9,1,11,3,6,3,6,3,10,6,10,1,3,12,1,6,1,8,9,12,4,11,1,2,11,7,2,1,5,7,12,2,4,5,8,4,11,12,1,8,4,10,10,2,4,4,12,4,3,2,2,11,2,9,11,7,6,11,5,7,8,3,8,7,12,4,3,5,7,7,5,6,7,11,2,4,1,3,5,6,4,1,2,8,6,5,8,3,9,7,2,4,5,4,6,9,1,12,6,1,11,1,8,4,7,10,5,5,2,8,11,9,11,1,6,8,12,1,10,1,10,5,7,12,7,12,7,10,6,12,12,11,4,3,5,1,1,12,2,10,6,1,1,6,8,8,5,6,4,4,1,4,5,7,2,11,3,2,7,10,4,10,11,9,9,12,12,8,6,10,12,7,3,7,6,4,3,5,5,2,1,9,9,10,9,12,11,3,9,3,2,9,7,5,9,3,9,12,5,11,1,8,9,2,7,4,8,3,10,4,9,11,5,12,5,10,11,5,3,6,6,1,2,10,10,3,10,11,5,7,2,11,3,1,1,10,2,12,3,6,3,6,3,8,7,2,10,7,10,2,12,4,1,4,8,11,9,3,12,2,9,3,11,9,4,5,11,5,6,8,2,12,6,7,8,10,1,6,8,11,6,6,12,3,7,11,7,10,9,1,10,6,5,3,11,1,8,3,6,10,4,6,12,2,7,4,2,6,12,2,1,2,9,4,3,3,10,4,3,2,1,5,7,10,8,6,5,2,9,4,1,1,1,9,10,11,10,10,3,1,8,4,12,1,9,3,12,9,12,1,11,5,6,2,3,9,6,9,5,4,7,7,11,9,6,4,11,1,11,6,8,5,2,10,9,11,6,5,7,3,9,12,9,1,9,11,8,9,1,11,11,11,9,4,4,9,3,6,1,10,8,5,1,2,6,4,4,10,11,4,4,11,10,11,9,10,12,4,2,9,9,4,2,7,2,11,10,9,4,8,3,4,8,8,3,8,8,1,8,1,2,2,11,5,4,11,7,1,12,3,8,11,4,7,8,1,3,7,8,4,9,5,9,10,7,12,1,5,2,2,11,6,3,9,4,9,5,2,6,4,1,4,9,7,8,7,11,6,2,5,12,9,3,1,10,3,1,8,6,3,3,4,7,10,10,11,5,3,7,7,9,7,3,12,9,9,2,1,11,3,5,2,4,6,10,11,3,8,12,2,8,8,4,7,2,11,9,7,7,3,5,5,12,8,11,12,2,2,3,10,10,5,2,12,12,5,3,4,4,3,2,6,12,2,1,6,8,11,1,6,10,7,3,4,12,5,8,2,3,1,4,5,7,5,8,12,12,1,1,8,2,7,8,7,1,8,3,5,12,11,10,3,12,1,4,1,10,1,10,10,10,10,2,8,7,7,6,6,1,4,5,1,7,8,1,3,10,8,5,4,8,1,5,7,3,9,6,4,9,6,11,4,11,2,10,4,2,7,12,9,8,6,4,9,12,6,7,3,12,8,8,10,9,12,3,11,2,7,5,10,5,4,2,6,3,11,12,2,6,4,11,4,11,1,4,1,4,7,6,12,9,9,7,5,2,11,9,1,10,8,10,7,5,4,8,6,4,4,11,12,4,6,12,5,11,6,3,5,11,11,6,12,8,7,12,3,12,10,1,6,11,3,7,10,4,10,5,3,4,9,10,6,6,8,10,9,4,7,1,11,2,11,3,11,4,2,3,6,8,9,1,3,8,1,6,9,2,6,5,2,5,10,12,9,10,5,8,9,12,5,9,5,12,3,12,8,1,4,4,10,5,11,10,5,7,4,3,3,5,2,7,7,9,6,8,8,10,6,1,6,5,5,5,6,12,1,8,8,4,4,2,5,9,9,7,5,3,9,4,10,2,2,6,1,4,12,5,6,10,3,11,12,2,1,5,8,6,10,7,7,10,2,4,11,3,4,6,1,3,9,6,10,2,4,5,10,9,8,11,6,8,12,3,2,11,8,7,10,5,1,11,9,1,11,4,6,6,12,7,4,8,2,3,8,12,7,6,8,6,5,12,6,6,12,8,1,8,4,6,11,4,8,5,4,5,10,8,12,5,1,2,12,12,8,1,1,6,7,10,8,4,11,10,8,3,7,10,10,9,7,11,7,9,8,3,6,8,10,1,7,11,3,8,11,1,6,4,1,2,2,8,5,2,4,4,3,4,10,4,8,7,2,5,5,7,8,10,1,9,8,5,5,11,4,4,2,7,5,1,9,8,2,6,8,11,1,10,2,3,9,1,2,3,3,3,11,12,10,4,9,1,12,2,9,3,9,8,9,11,11,3,7,7,10,3,3,12,4,4,7,10,7,11,5,7,2,9,5,8,2,9,1,8,5,8,3,2,1,12,1,1,7,3,4,2,12,12,8,4,5,4,12,11,2,7,3,11,1,6,7,4,9,2,1,5,12,7,9,4,10,1,1,6,2,5,8,1,11,3,1,5,6,10,2,1,6,12,8,3,8,9,1,12,8,6,4,9,12,9,8,9,1,8,8,4,10,8,12,3,9,2,11,7,4,7,3,6,12,2,2,11,11,11,4,7,9,9,11,12,2,3,9,2,5,2,4,8,5,10,12,1,4,11,6,2,1,4,12,5,10,12,5,7,11,1,2,4,1,11,3,4,10,1,7,4,1,7,5,1,3,7,8,5,3,4,12,3,6,6,6,1,6,11,10,4,12,1,7,2,2,3,10,5,10,9,6,8,9,11,10,4,12,2,5,9,9,9,10,11,3,10,6,3,9,10,8,12,6,4,12,1,4,1,8,2,12,4,2,6,1,7,10,9,10,9,11,8,1,3,6,8,7,8,9,7,4,5,4,1,6,3,3,5,8,8,6,4,8,7,11,12,10,3,2,3,4,10,1,5,9,5,3,9,2,6,3,2,3,5,8,7,10,1,1,5,7,11,5,8,9,12,9,11,6,11,8,7,12,2,10,10,4,6,9,1,6,10,3,3,8,8,10,9,9,2,8,10,9,10,3,7,10,3,12,6,6,12,7,10,12,7,4,8,2,11,2,4,5,4,} +focus +0.67610705893124 +firstname +"Pusaki" +midname +"Tanysatuanuy" +family +"Ptasfnzik" +food +{} +END_KOBOLD +KOBOLD +genes +{1,7,8,7,8,4,10,8,12,11,11,7,4,3,3,2,7,10,6,3,8,3,9,12,12,8,3,2,2,5,8,2,3,4,2,7,3,4,1,3,5,11,8,8,6,6,9,5,6,9,6,11,3,10,6,3,11,10,7,1,8,6,3,1,7,6,5,1,2,9,11,10,3,4,2,11,2,6,8,10,8,5,6,8,4,7,2,2,12,12,1,3,8,5,7,4,11,4,12,11,10,9,11,2,9,7,6,7,2,12,8,8,11,9,1,2,9,11,1,5,1,8,1,7,3,7,5,3,11,5,4,7,9,1,9,5,9,7,4,12,7,1,11,6,2,2,4,7,6,5,7,4,1,12,10,12,5,10,9,3,5,12,1,6,7,12,10,11,7,2,10,6,2,2,11,3,1,11,1,8,4,2,12,7,3,4,11,11,9,10,10,10,7,9,7,5,4,2,2,8,2,10,1,6,9,10,6,3,4,5,3,4,7,11,7,8,9,10,2,5,1,11,2,12,11,7,6,12,8,8,9,10,11,6,4,11,6,12,9,5,7,8,10,1,11,6,8,12,6,3,9,8,5,8,2,6,5,1,7,8,12,1,10,10,2,10,10,3,7,5,12,8,2,3,1,7,2,3,8,4,1,9,12,8,5,5,1,6,9,11,11,5,2,9,3,5,7,5,7,10,7,6,8,6,7,9,8,8,4,12,7,4,5,12,3,2,12,4,8,8,1,8,2,9,3,9,1,8,1,5,9,2,1,8,2,9,10,9,8,6,3,1,12,7,5,11,3,2,7,8,6,9,4,9,3,5,12,6,10,6,6,10,12,1,11,2,10,3,8,9,7,11,11,4,5,3,4,3,5,10,1,5,3,4,3,6,9,5,6,6,3,4,12,11,12,5,10,10,12,6,9,4,7,9,5,8,12,2,5,3,1,9,9,3,11,3,10,8,2,8,7,10,6,9,10,8,4,3,2,3,7,2,11,8,4,3,10,7,11,9,7,3,4,6,5,12,6,11,11,6,3,10,10,11,3,9,7,10,10,11,2,9,8,1,1,1,2,11,11,6,12,8,9,4,1,6,1,3,10,1,5,12,1,12,12,3,4,7,5,5,5,4,7,2,5,2,7,6,8,3,4,7,11,10,6,11,12,9,2,10,6,10,2,3,12,5,6,12,9,5,12,3,8,10,8,9,10,8,9,7,4,3,5,2,12,2,10,8,3,9,5,12,10,12,3,8,1,3,5,3,7,2,1,7,8,2,10,4,5,4,5,6,2,2,2,8,4,3,12,3,10,3,3,7,5,9,7,3,2,9,3,5,9,6,11,1,10,12,12,12,8,5,10,9,4,9,6,10,4,5,10,5,6,7,7,6,11,12,12,9,12,5,8,6,7,10,10,7,12,4,2,9,6,2,7,2,12,2,6,5,8,3,11,10,3,10,10,11,9,12,10,10,7,1,9,2,11,4,3,7,9,2,10,9,5,7,1,11,4,9,1,5,8,8,9,5,8,6,11,2,10,3,12,5,1,9,1,10,10,9,9,2,7,4,9,7,9,8,8,5,7,5,8,10,2,5,12,4,7,5,9,5,11,7,8,10,1,6,10,5,12,5,2,9,4,1,9,11,10,10,11,1,6,12,10,6,2,2,1,2,6,3,10,8,7,6,9,6,4,11,3,11,4,1,5,10,3,2,4,10,3,11,1,11,7,8,6,10,11,6,12,2,6,6,3,12,4,9,5,10,7,2,8,3,9,6,11,4,1,6,7,6,2,7,10,4,2,11,5,8,10,1,8,12,11,5,7,2,1,3,5,10,12,3,1,5,3,5,11,1,3,11,3,10,3,2,10,5,11,11,10,2,11,2,8,10,1,3,2,11,12,8,11,7,5,2,7,1,1,10,11,8,7,1,8,8,3,1,12,7,7,12,8,1,11,9,12,8,6,2,12,3,4,1,1,1,4,4,4,6,2,4,7,2,5,12,7,9,5,2,5,4,5,3,2,5,3,12,4,8,11,1,12,10,12,6,1,4,8,7,4,5,9,12,12,4,6,3,3,7,6,1,4,7,11,2,9,6,11,3,2,12,11,9,10,5,8,12,8,6,1,1,10,11,2,4,6,1,3,10,8,4,7,5,2,11,11,6,8,10,8,5,10,5,11,3,1,12,11,10,8,2,2,1,9,1,11,9,8,11,12,12,8,7,2,9,1,4,6,12,2,5,9,7,4,11,1,2,12,8,11,10,1,10,5,4,3,8,6,10,9,9,6,10,5,3,3,9,2,8,2,7,5,8,7,2,4,6,11,3,10,7,7,8,5,11,1,1,4,7,1,12,1,2,6,3,6,5,8,3,7,10,6,3,6,6,5,3,11,7,8,2,12,5,3,1,6,8,12,6,7,7,2,9,9,6,9,12,4,10,10,2,1,9,3,3,11,12,6,1,9,4,1,1,4,12,1,12,12,5,11,11,12,3,12,7,7,7,4,3,2,12,9,1,2,5,10,12,8,9,4,12,9,5,7,3,12,8,9,5,7,1,10,6,2,10,6,9,11,5,9,5,8,11,6,3,4,10,10,6,7,8,6,11,11,1,11,5,7,9,2,6,7,10,12,10,1,5,4,5,11,5,9,4,8,10,4,11,3,8,7,9,7,7,4,11,3,11,5,1,5,12,11,3,11,10,7,4,4,6,9,2,7,7,10,5,7,7,2,8,3,7,3,4,6,12,12,2,9,12,7,10,5,4,9,12,7,9,2,1,4,10,9,7,5,2,8,10,4,6,3,6,6,8,2,8,6,4,7,1,3,3,7,10,9,3,5,8,4,11,6,9,9,10,5,8,2,10,9,10,4,6,5,4,5,8,5,4,9,3,4,12,2,9,6,3,12,12,8,1,2,1,3,9,3,1,11,1,1,12,7,11,10,8,3,10,3,8,8,4,7,2,5,1,1,12,11,4,2,12,10,10,4,3,5,8,3,12,9,11,11,4,11,9,6,3,9,10,6,11,9,3,10,5,9,3,7,4,4,3,4,12,4,5,4,4,3,6,11,2,12,7,12,8,5,9,5,8,5,5,2,10,11,4,12,7,5,1,4,3,10,3,10,2,6,2,9,1,3,10,12,8,8,2,4,12,9,8,8,9,7,3,2,6,9,3,12,5,5,12,10,11,9,9,11,11,5,3,2,10,1,7,2,4,4,5,7,7,8,6,11,3,5,8,9,11,9,1,5,8,10,9,5,12,10,2,1,12,1,9,5,12,10,11,8,7,10,6,9,8,3,5,4,2,12,7,2,1,7,2,5,11,10,8,2,8,9,3,2,8,6,4,11,9,12,2,12,6,12,5,4,1,9,4,11,5,1,4,5,5,11,5,6,1,7,10,8,1,4,4,3,12,9,6,12,8,4,10,12,10,5,8,5,8,4,11,3,7,6,4,7,3,10,10,3,6,12,3,4,12,4,11,11,9,2,8,12,3,2,11,6,12,7,10,5,7,4,2,2,4,10,12,12,1,1,11,2,12,10,4,10,8,11,8,5,5,12,10,10,12,9,11,4,5,4,11,10,10,5,8,5,3,4,7,8,2,9,5,8,11,6,2,10,11,7,10,2,6,10,8,3,5,5,3,5,2,1,10,10,7,1,2,11,9,12,8,7,1,7,10,6,1,1,1,6,6,3,1,1,4,2,7,12,7,3,10,5,5,3,11,3,4,9,3,7,7,11,11,6,7,9,8,10,8,6,3,4,10,8,1,9,3,4,11,2,9,6,6,5,9,1,4,3,12,5,6,2,5,9,4,10,4,8,8,1,6,1,7,12,2,9,7,3,5,8,4,9,12,8,11,10,2,3,5,3,8,3,11,7,3,9,1,2,9,6,2,10,4,3,1,12,4,1,3,12,12,7,4,11,10,11,5,2,2,7,11,3,5,11,5,6,10,1,4,11,8,5,4,4,8,5,9,11,5,10,11,8,6,11,4,10,11,4,9,12,7,11,10,3,7,3,2,1,9,8,10,8,4,5,5,7,10,10,7,8,1,3,6,4,11,3,4,5,8,10,11,10,7,2,9,10,6,1,4,2,12,3,2,8,11,7,10,10,3,1,10,12,6,11,6,3,7,12,4,12,5,4,9,11,3,6,12,12,10,12,8,3,8,12,8,9,7,1,6,5,9,3,1,9,1,3,10,10,9,2,1,3,11,11,1,2,1,2,4,10,3,9,3,1,7,9,3,9,8,4,5,10,9,12,11,10,4,6,11,7,12,1,7,11,10,2,3,8,1,12,10,11,11,11,1,9,12,12,11,9,7,8,12,1,3,5,1,8,6,11,11,1,6,11,10,4,12,7,9,3,5,2,2,11,9,12,4,12,12,6,7,5,3,10,6,9,7,11,12,} +focus +0.30954924161504 +firstname +"Psuky" +midname +"Nyanynyanuy" +family +"Kipasannzik" +food +{3,2,3,4,} +END_KOBOLD +KOBOLD +genes +{12,7,2,6,10,1,12,12,10,6,6,4,1,3,1,5,11,12,3,1,10,12,9,8,10,8,12,10,1,12,12,10,11,8,3,11,2,12,6,10,5,5,3,4,1,6,6,6,11,5,9,1,12,5,3,2,1,11,6,1,3,1,12,11,5,4,9,9,12,7,8,5,9,6,3,4,4,10,1,7,7,10,2,9,10,9,1,11,7,1,12,5,1,8,5,11,8,10,5,4,5,11,4,8,6,7,12,9,4,2,7,12,4,1,3,4,8,2,6,3,8,8,2,12,11,11,1,8,10,4,4,11,8,11,12,11,8,3,3,5,9,9,1,3,4,6,7,10,8,1,8,4,1,2,6,8,11,1,4,8,12,8,12,8,1,12,5,5,2,7,2,12,4,5,11,12,9,4,5,6,3,1,12,7,8,4,7,2,8,2,7,2,10,8,9,10,1,6,12,2,9,7,6,6,7,11,6,4,3,7,11,12,11,11,2,12,10,8,3,4,7,4,8,8,6,8,2,9,10,8,10,2,11,5,6,12,11,6,11,8,4,10,2,11,3,10,11,3,1,6,10,9,4,2,10,3,11,5,11,2,11,12,3,2,9,10,5,6,8,3,4,10,1,8,3,5,4,10,5,5,1,2,1,1,6,6,10,2,4,3,9,5,10,7,8,8,11,4,6,11,10,12,12,5,4,10,4,4,3,7,12,6,2,4,6,7,2,8,5,7,2,4,5,5,1,2,6,4,5,4,5,8,4,7,12,9,1,3,5,4,7,10,6,8,12,1,11,4,4,8,12,2,7,3,12,4,5,9,2,8,6,1,1,6,1,8,2,6,5,4,1,11,3,7,2,7,1,7,3,10,1,10,3,7,1,9,5,6,11,3,9,6,9,10,4,2,9,9,7,3,11,1,8,5,4,9,9,5,11,1,6,12,2,10,7,9,5,7,5,2,3,6,1,8,11,3,4,12,12,4,12,2,11,12,6,9,9,10,1,3,11,7,7,1,11,10,11,5,11,8,8,5,12,4,10,6,9,5,10,5,10,2,10,12,7,2,11,3,7,5,11,4,11,5,7,11,9,4,5,12,10,11,10,6,9,4,11,8,8,4,8,3,8,9,9,1,3,5,7,11,6,3,6,12,1,7,9,3,6,9,1,5,10,4,7,11,2,2,8,7,6,2,7,2,5,2,3,5,10,10,5,4,1,4,10,10,11,7,10,9,2,6,4,3,4,10,6,1,4,12,3,1,2,4,10,5,3,11,10,9,9,2,9,10,3,8,4,6,1,6,12,2,6,3,5,11,8,7,3,11,10,11,7,10,12,8,12,1,12,9,7,9,11,3,11,5,1,1,10,7,10,11,1,1,11,10,9,1,3,7,4,11,8,9,7,11,8,7,5,11,7,6,3,12,6,11,7,4,1,1,8,1,9,7,8,8,12,10,6,10,12,2,11,2,5,8,4,3,7,4,8,12,4,3,9,8,3,1,10,6,10,12,11,2,11,4,3,7,1,2,7,10,7,8,6,9,3,9,4,11,6,1,1,12,12,12,9,11,6,11,10,11,12,12,10,9,1,11,5,3,3,4,5,2,8,4,5,6,11,3,8,1,5,6,5,2,11,7,1,7,12,5,4,4,9,11,2,12,8,12,4,12,6,11,10,6,3,8,8,6,9,3,3,1,6,12,4,9,6,2,4,2,4,7,8,7,1,10,10,12,12,9,5,5,12,12,7,9,7,11,9,11,9,11,8,1,7,10,4,4,1,8,9,4,6,9,6,12,5,8,8,4,11,2,6,6,8,12,7,5,2,10,7,4,4,10,11,9,4,8,2,1,1,11,6,6,7,2,6,8,12,12,4,1,12,8,2,11,1,10,11,5,12,7,7,9,9,8,9,8,8,2,6,2,7,9,12,3,11,6,7,5,5,8,10,8,11,12,2,7,4,7,5,7,1,1,7,4,8,10,2,7,1,7,5,5,11,5,2,7,11,7,4,8,9,12,2,7,3,12,3,4,7,2,5,6,2,6,10,11,9,7,2,12,4,4,5,5,4,12,7,1,7,3,5,7,3,8,4,2,1,11,10,11,10,8,2,10,7,10,1,1,5,9,12,8,3,7,10,8,11,6,6,2,7,5,1,4,3,3,2,2,9,11,10,3,5,1,2,6,5,5,3,4,6,7,7,12,9,12,11,6,6,11,5,12,5,4,4,7,12,5,9,12,1,5,5,5,4,1,8,11,5,2,2,1,10,11,6,5,5,2,10,7,8,8,10,12,1,12,9,1,8,7,11,3,1,8,5,7,8,2,8,5,3,9,2,1,10,5,8,2,10,8,9,2,7,1,4,8,9,6,2,3,7,7,7,8,8,9,6,12,7,5,2,10,1,9,1,9,12,11,4,6,9,10,1,11,9,6,10,1,8,7,11,1,7,3,2,6,1,4,6,8,1,3,4,7,4,10,9,8,10,1,7,11,1,9,10,9,9,2,11,6,1,11,5,10,8,8,6,10,7,3,4,11,3,9,4,10,11,12,6,5,6,2,3,11,5,4,2,4,7,10,1,7,11,6,1,10,3,5,10,8,3,1,11,6,5,7,8,12,4,4,8,1,1,6,4,11,8,8,12,4,4,5,10,2,12,2,5,5,7,8,9,3,10,1,7,6,2,12,3,4,9,10,9,6,6,1,9,6,7,8,5,10,11,11,8,6,8,10,2,7,10,2,10,2,9,4,4,1,7,9,1,12,4,2,7,6,12,11,11,11,1,8,6,12,10,5,9,9,11,11,12,11,7,7,2,9,2,12,9,9,9,10,10,6,9,2,12,2,12,3,5,11,2,7,2,3,10,8,9,7,1,2,3,2,2,6,8,6,11,4,5,6,10,1,3,4,10,7,3,9,3,4,12,3,6,1,2,9,7,3,4,7,12,5,12,8,6,8,2,7,12,6,1,10,9,12,6,3,5,11,11,6,9,10,1,2,11,8,11,7,6,8,4,12,2,2,6,5,10,8,4,9,12,4,2,10,9,10,1,12,4,6,11,3,2,12,4,1,2,1,9,4,2,1,11,9,9,11,6,6,2,2,8,1,5,10,2,7,8,11,10,8,10,2,2,10,7,3,9,8,5,5,4,6,4,6,12,1,2,5,5,2,11,3,6,4,5,10,4,9,10,12,3,12,1,6,6,4,6,8,4,2,7,1,4,7,11,6,10,7,7,5,2,4,10,3,7,1,10,7,2,2,6,4,8,3,11,10,12,7,8,4,7,6,11,11,2,5,10,3,1,9,2,2,7,10,7,7,2,3,3,11,4,7,1,2,7,9,6,5,4,7,12,7,5,12,5,9,3,12,1,8,12,1,8,1,12,7,2,4,5,2,7,6,7,4,7,7,12,3,4,4,1,1,4,10,12,3,1,5,6,7,1,3,10,6,5,10,1,4,6,3,12,1,10,7,2,4,8,8,11,11,7,6,10,3,9,2,6,8,8,9,2,7,5,3,10,1,7,11,2,12,1,5,12,10,3,2,8,4,5,2,7,11,10,3,8,6,5,5,9,7,10,8,10,11,9,7,3,1,8,5,10,3,8,3,10,10,3,9,12,8,9,1,8,9,6,11,7,4,4,12,9,7,10,6,1,8,9,5,12,12,3,9,5,5,12,11,5,12,1,2,4,12,3,9,3,12,11,12,2,9,11,11,1,1,10,12,12,4,7,10,8,4,8,9,7,7,10,5,6,4,6,1,12,11,11,9,7,5,6,9,12,10,12,11,10,10,7,10,10,12,8,7,10,11,8,7,10,9,1,11,4,1,10,9,6,3,8,7,8,9,5,2,8,6,11,6,10,9,12,7,8,8,2,4,3,9,3,12,11,7,9,9,1,3,10,5,1,7,2,12,1,10,2,1,12,5,6,8,4,2,4,12,8,7,1,1,7,10,1,11,8,1,10,1,3,5,2,12,3,3,1,6,3,1,1,6,4,4,1,4,12,10,9,1,8,10,10,6,5,8,4,3,4,6,5,1,1,6,10,12,3,2,1,4,2,9,4,8,10,9,5,9,1,6,11,6,1,4,7,3,7,2,10,9,2,9,1,7,1,9,3,7,5,5,3,8,9,1,9,12,8,2,2,2,12,7,4,7,3,10,8,10,4,4,9,2,12,6,6,7,6,3,2,6,7,4,2,7,3,10,9,5,9,8,10,1,4,3,10,9,4,3,9,12,11,1,10,11,7,9,6,5,12,8,6,4,6,5,8,9,2,8,2,10,2,9,8,6,12,12,3,1,7,5,4,7,9,6,12,8,9,1,5,3,5,2,8,6,10,10,4,3,4,5,2,4,7,9,11,7,4,2,11,11,8,9,11,2,2,2,10,5,1,4,12,6,9,8,} +focus +0.094868987074194 +firstname +"Itaaki" +midname +"Natanyianuy" +family +"Ptasfnzik" +food +{} +END_KOBOLD +KOBOLD +genes +{5,7,10,6,4,9,11,3,12,1,4,5,2,12,12,12,5,8,4,2,6,1,3,11,5,9,5,11,4,8,9,1,4,12,1,8,2,10,6,12,5,9,8,6,2,1,10,11,5,9,10,5,6,6,7,10,5,4,7,12,8,7,9,6,7,7,8,6,6,7,2,10,10,10,4,10,11,5,12,1,7,9,5,6,3,6,7,1,7,12,4,8,1,1,11,6,1,8,12,11,2,2,12,12,9,7,7,11,7,8,6,4,8,9,5,5,9,5,10,1,1,6,5,4,2,11,8,7,3,11,6,11,12,10,1,11,6,12,11,6,5,9,3,5,8,10,7,9,9,3,9,11,8,3,4,2,6,5,9,3,10,1,5,3,8,1,5,5,7,9,10,7,12,8,9,1,1,6,3,1,3,3,2,12,3,4,2,10,9,10,9,8,9,11,2,11,8,3,1,8,7,11,5,7,6,8,6,10,10,8,1,2,10,12,11,8,3,11,8,10,6,3,1,5,4,1,7,4,1,1,10,4,1,10,4,8,6,12,10,4,8,11,6,7,6,1,3,6,7,2,5,10,12,8,6,4,12,6,5,7,10,12,5,5,7,3,9,2,10,3,4,1,6,9,2,8,12,1,10,9,1,7,12,9,10,2,2,12,10,8,10,7,1,7,3,11,3,3,3,6,10,8,3,6,12,9,2,11,4,6,12,9,5,8,12,5,1,1,8,9,1,1,2,10,7,6,7,2,8,12,3,9,8,8,3,6,12,4,4,11,2,2,8,3,8,11,3,12,4,1,9,7,10,8,3,3,7,9,3,2,7,2,10,5,8,1,11,10,2,2,7,11,9,2,8,5,12,1,11,2,7,3,3,8,12,3,10,9,11,6,11,6,2,4,2,7,6,10,10,9,10,11,2,7,7,8,11,3,7,10,11,6,6,4,10,7,6,4,8,10,5,10,10,7,9,12,10,4,5,8,11,3,4,5,5,9,10,4,7,12,11,7,5,2,4,8,1,4,6,2,4,11,11,10,4,6,5,12,12,11,10,6,8,4,5,9,1,9,10,4,8,6,9,6,10,5,2,12,1,9,7,5,5,5,4,8,1,8,12,6,7,5,7,9,5,9,6,6,6,8,12,3,7,9,1,8,9,3,5,5,2,9,7,7,10,5,12,2,9,12,12,6,11,2,12,8,10,8,5,9,11,2,10,9,6,12,4,10,5,8,8,3,1,9,1,11,12,2,9,11,2,6,10,11,1,11,5,11,8,3,9,2,5,9,7,6,5,6,11,5,1,3,11,10,12,7,5,9,6,7,9,1,11,9,3,7,6,6,8,10,3,9,10,1,10,11,10,10,7,3,8,10,8,11,7,12,11,10,1,12,10,5,4,3,12,4,9,1,12,1,12,7,9,4,10,4,6,2,2,7,1,5,3,1,11,5,4,4,10,8,2,2,9,9,2,12,9,4,5,1,1,1,8,4,4,11,8,8,1,8,1,6,12,2,6,4,4,11,9,9,6,9,8,9,7,1,3,10,7,4,12,2,5,12,7,5,2,11,1,7,11,1,2,1,6,3,12,10,1,6,9,12,4,9,5,8,5,6,3,5,2,2,5,10,2,10,2,3,4,7,7,11,8,1,4,5,8,8,6,8,3,8,6,9,4,2,5,8,7,2,6,10,5,3,4,9,6,1,9,10,8,9,12,9,5,6,10,6,7,8,6,2,9,8,3,7,12,1,10,3,8,3,4,5,11,10,6,4,4,2,5,6,11,5,12,7,11,10,8,3,3,11,9,12,3,1,4,7,5,1,12,5,9,1,2,11,6,12,2,1,4,10,7,7,12,5,11,7,9,2,11,4,8,3,10,11,9,6,8,5,12,6,4,2,10,9,12,11,3,7,4,3,3,10,9,2,9,3,10,4,8,3,11,9,8,8,12,8,9,8,5,2,11,8,9,8,7,6,12,8,4,2,7,12,9,12,5,9,3,12,5,5,4,10,10,12,2,12,1,9,1,8,10,12,7,3,4,8,3,9,2,12,6,2,1,12,11,2,12,2,4,8,10,4,3,2,7,12,6,1,7,10,8,8,3,8,10,1,7,12,10,8,1,4,11,12,7,6,11,2,1,4,5,3,2,12,2,7,12,11,5,5,5,2,12,2,7,3,9,12,2,12,2,8,5,8,10,6,4,9,12,2,1,6,12,2,7,11,1,12,4,7,11,4,2,4,8,7,11,11,1,7,5,3,3,2,3,6,7,8,7,9,12,2,7,8,4,5,7,8,5,11,8,1,5,5,2,1,3,4,5,11,3,1,10,2,8,1,8,3,6,8,3,4,8,11,10,4,12,10,7,6,7,1,5,3,5,10,4,8,7,2,10,8,10,5,9,5,7,9,11,3,9,2,10,5,10,12,1,12,1,3,6,5,10,8,6,6,8,4,9,12,11,4,1,1,9,8,7,3,5,11,9,10,12,9,8,4,4,4,3,6,1,2,2,10,5,8,1,1,1,2,1,3,11,4,5,2,12,11,12,7,10,6,5,1,9,6,5,10,4,12,5,7,3,11,3,12,1,1,3,5,10,3,6,10,5,11,12,2,3,8,1,9,4,4,8,8,1,5,7,9,9,9,2,10,1,6,8,8,3,8,12,7,7,10,7,1,5,3,10,9,11,2,7,4,3,6,9,1,9,9,6,5,7,2,8,8,11,7,1,3,11,12,6,1,1,5,6,2,5,6,5,11,8,8,7,9,7,12,9,8,7,9,6,5,1,10,9,12,12,12,3,7,2,7,12,5,4,2,9,3,1,9,1,5,7,12,2,8,10,6,2,7,10,8,6,9,5,5,9,4,8,6,11,6,11,9,3,11,9,9,5,6,2,5,8,2,7,1,11,10,9,12,2,8,6,2,7,2,9,10,7,1,6,10,6,10,2,1,8,4,6,4,6,4,9,1,5,1,10,3,8,1,6,9,9,11,7,1,3,12,1,9,12,5,7,12,7,1,1,6,1,9,2,1,11,9,3,6,6,10,11,11,5,10,10,4,6,12,8,1,12,3,5,3,4,7,1,9,5,4,3,11,5,8,3,4,1,12,12,7,8,2,3,9,9,5,6,12,3,11,5,9,1,9,5,5,8,10,2,3,5,9,7,9,11,9,2,2,1,1,12,3,10,8,5,9,2,2,4,2,2,1,1,12,7,6,4,8,9,11,1,8,11,12,1,7,9,2,1,1,6,3,1,4,9,2,2,7,7,6,10,8,5,12,2,5,11,8,8,12,7,12,1,3,10,8,12,7,12,6,12,7,6,6,9,1,2,6,3,7,12,10,9,5,11,10,1,10,11,9,9,5,6,1,6,4,3,11,5,8,10,6,11,2,4,10,12,7,11,12,9,5,7,4,12,7,5,3,9,10,3,10,8,4,10,4,4,4,12,1,2,5,2,7,9,1,12,1,2,7,6,6,5,8,7,8,2,9,8,10,12,8,2,11,3,8,9,6,10,3,12,2,2,2,10,5,5,8,10,4,1,4,6,1,10,12,2,8,7,11,5,9,9,3,5,6,8,6,7,4,1,5,8,5,2,8,12,7,5,1,5,10,12,8,3,2,9,4,4,11,3,7,11,3,6,4,9,12,1,3,11,7,3,2,2,12,7,10,5,5,10,7,10,5,12,11,1,2,4,4,8,7,5,7,5,4,4,12,4,7,10,7,8,10,5,7,11,6,12,11,8,4,7,12,5,1,5,7,7,3,7,7,11,5,10,8,7,2,5,5,4,10,11,2,8,12,4,7,11,7,12,6,11,4,1,5,7,11,11,7,2,10,5,8,9,10,3,2,6,6,8,3,5,2,2,12,12,4,11,9,2,2,7,11,2,11,1,7,1,5,9,1,10,10,8,6,6,12,12,5,4,5,1,11,12,3,10,1,5,7,7,10,7,10,10,5,3,12,2,5,2,5,2,10,1,2,5,5,5,6,1,3,5,4,4,1,2,9,7,7,5,9,8,4,3,6,3,10,7,5,6,7,5,12,1,8,10,10,3,9,12,6,12,3,1,11,1,2,1,6,7,5,8,9,7,6,10,6,8,12,11,7,8,8,8,1,2,9,5,5,7,3,4,1,1,7,5,9,11,3,3,12,6,1,11,1,4,1,8,7,4,7,12,9,9,8,7,4,4,12,12,12,9,12,1,11,6,1,11,6,4,6,9,5,11,1,4,3,8,9,9,11,10,10,10,1,1,3,10,2,3,5,9,3,9,2,4,11,7,5,10,10,1,5,9,9,4,8,2,9,6,7,9,5,10,10,5,3,1,3,10,3,4,11,11,8,3,9,11,12,7,1,3,12,7,11,2,11,2,6,4,9,3,1,9,11,5,1,5,1,9,4,} +focus +0.081013066505835 +firstname +"Zkuki" +midname +"Nykukuzanuy" +family +"Kipasannzik" +food +{} +END_KOBOLD diff --git a/training/trainer.lua b/training/trainer.lua new file mode 100644 index 0000000..06b6b1d --- /dev/null +++ b/training/trainer.lua @@ -0,0 +1,300 @@ + +adaptive_ai = not adaptive_ai and {} or adaptive_ai + +dofile("../helper.lua") +dofile("./noise.lua") + +trainer = {} + +math.randomseed(os.time()) + +local random = math.random +local helper = adaptive_ai.helper +local calc = adaptive_ai.calc + +local ceil = math.ceil +local avg = calc.average +local round = calc.round + +local adjacent = helper.adjacent +local conditions = {} +local gen_stats, popul, top_size -- condition values +local fill_creature, reset_creature, manage_creature, fill_cell -- condition functions + +require "socket" + +local function sleep(sec) + socket.select(nil, nil, sec) +end + +local function console_log(s, delay) + if conditions.output and s then + print(s) + if delay then sleep(delay) end + end +end + +-- TERRAIN CONTROL +local function valid(pos) + if pos.x and pos.x < 1 or pos.x > conditions.dims.x then return false + elseif pos.z and pos.z < 1 or pos.z > conditions.dims.z then return false + else return true + end +end + +-- Fill the training area +-- Cell[1] stores the height. Cell[2] is the space to store custom metadata +local function fill_terrain(map) + console_log("Filling terrain...") + + local p + local max_x, max_y, max_z = conditions.dims.x, conditions.dims.y, conditions.dims.z + local x_dimming, z_dimming = max_x/4, max_z/4 + + for i=1,max_x do + for j=1,max_z do + if map[i][j][1] == -1 then + p = {x=i, z=j} + p.y = round((SimplexNoise.Noise2D(p.x/x_dimming, p.z/z_dimming) + 1)*5) + p.y = p.y + round(SimplexNoise.Noise2D(p.x/2, p.z/2)) + p.y = p.y + round(SimplexNoise.Noise2D(p.x, p.z)/1.5) + if valid(p) then + if p.y > max_y then + p.y = max_y + elseif p.y < 1 then + p.y = 1 + end + end + map[i][j][1] = p.y + + fill_cell(map[i][j]) + end + end + end + + console_log("Success filling terrain.\n") +end + +local function gen_terrain() + console_log("Prepare new terrain...") + local map = {} + for i=1,40 do + map[i] = {} + for j=1,40 do + map[i][j] = {-1} + end + end + + SimplexNoise.seedP(os.time()) + fill_terrain(map) + return map +end + +-- POPULATION CONTROL +local function fill_popul(popul, cull) + console_log("Populating...") + + math.randomseed(os.time()) + local creature + if cull then + creature = {} + local j = random(1,cull) + fill_creature(creature, popul[1], popul[j]) + table.insert(popul, creature) + end + + local popul_size = conditions.popul_max + while #popul <= popul_size do + creature = {} + if conditions.champ_breed + and (not cull or random() < 0.05) then + fill_creature(creature) + else + local i = random(1,cull) + local j + repeat + j = random(1,cull) + until i ~= j + fill_creature(creature, popul[i], popul[j]) + end + table.insert(popul, creature) + end + + console_log("Success populating.\n") +end + +local function fitness_sort(a, b) + return a.fitness > b.fitness +end + +-- TURN MANAGEMENT +local function move_creature(creature, n) + local p = {} + p.x = creature.pos.x + adjacent[n].x + p.z = creature.pos.z + adjacent[n].z + if valid(p) then p.y = terrain[p.x][p.z][1] end + if p.y and p.y - creature.pos.y < creature.climb then + creature.pos = p + if creature.pos.y - p.y > creature.fear_height then + creature.hp = creature.hp - ceil(creature.pos.y - p.y) + end + end +end + +local function manage_turn(creature, map, turn) + creature.lifespan = turn + local last = manage_creature(creature, map, conditions.dtime) + + console_log(conditions.turn_log(creature, map, gen_stats, turn)) + conditions.map_checks(creature, map, turn) + + return last +end + +local function manage_lifetime(creature, map) + local last, turn, max_turn = false, 0, conditions.max_turn + + while turn <= max_turn and not last do + turn = turn + 1 + last = manage_turn(creature, map, turn) + end + + if conditions.fitness_modifier then + creature.fitness = creature.fitness * conditions.fitness_modifier(turn, max_turn) + end +end + +local function manage_gen(num) + gen_stats.num = num + + if not output and not minetest then + print("-- Start of generation "..num.." --") + end + + local s = string.rep("-", 42).."\n"..string.rep("-", 42).."\n\n" + s = s.."*** START OF GEN "..num.." ***".."\n" + console_log(s) + + local map = gen_terrain() + terrain = map + + local creature + for i=1,#popul do + creature = popul[i] + creature.id = i + + reset_creature(creature, map) + manage_lifetime(creature, map) + end + + table.sort(popul, fitness_sort) + + local champ = popul[1] + + gen_stats.avg = avg(popul, function(creature) + return creature.fitness + end) + gen_stats.peak = champ.fitness + + local i, elite = 1, gen_stats.top[1] + while elite and i <= top_size do + if champ.fitness > elite.fitness then + gen_stats.top[i] = clone_creature(champ) + elite = nil + else + i = i + 1 + end + end + if not gen_stats.top[i] then + gen_stats.top[i] = clone_creature(champ) + end + + if champ.fitness < conditions.fitness_threshold then + popul = {} + fill_popul(popul) + else + if champ.fitness > gen_stats.max_peak then + gen_stats.gen = num + gen_stats.max_peak = champ.fitness + conditions.custom_stats(champ, gen_stats) + end + + local cull = ceil(#popul*conditions.cull_threshold) + for i=cull,#popul do + popul[i] = nil + end + + fill_popul(popul, cull) + end + + console_log(conditions.gen_log(num, gen_stats)) +end + +-- TRAINING PROCESS +local function train(options, offset) + conditions = { + dims = options.dims or {x=40, y=40, z=40}, + output = options.output or false, + gens = options.gens or 100, + dtime = options.dtime or 1, + turn_log = options.turn_log or function() end, + gen_log = options.gen_log or function() end, + training_log = options.training_log or function() end, + map_checks = options.map_checks or function() end, + fitness_threshold = options.fitness_threshold or 0, + cull_threshold = options.cull_threshold or 3/5, + champ_breed = options.champ_breed or true, + max_turn = options.max_turn or 200, + fitness_modifier = options.fitness_modifier, + custom_stats = options.custom_stats, + popul_max = options.popul_max or 150, + } + + gen_stats = options.gen_stats or {} + gen_stats.num = 0 + gen_stats.avg = 0 + gen_stats.peak = 0 + gen_stats.max_peak = 0 + gen_stats.top = {} + + popul = options.popul or {} + fill_cell = options.fill_cell + fill_creature = options.fill_creature + reset_creature = options.reset_creature + manage_creature = options.manage_creature + clone_creature = options.clone or function(creature) + return creature + end + top_size = options.top_size > 0 and options.top_size or 1 + + fill_popul(popul) + + local last_gen = ofsset or 0 + + for i=1,conditions.gens do + manage_gen(i + last_gen) + end + last_gen = gen_stats.num + console_log(conditions.training_log(gen_stats)) + + if options.to_file then + for i=1,top_size do + s = popul[i]:to_lines() + file = io.open(options.to_file..".txt", "a") + for _,line in ipairs(s) do + file:write(line, "\n") + end + file:close() + end + end + + return popul, last_gen +end + +trainer.train = train +trainer.valid = valid +trainer.console_log = console_log +trainer.fitness_sort = fitness_sort +trainer.move_creature = move_creature + +adaptive_ai.trainer = trainer diff --git a/training/trainer_goblin.lua b/training/trainer_goblin.lua new file mode 100644 index 0000000..5e1dd16 --- /dev/null +++ b/training/trainer_goblin.lua @@ -0,0 +1,305 @@ + +dofile("./trainer.lua") +dofile("../goblin/goblin.lua") + +local trainer = adaptive_ai.trainer +local helper = adaptive_ai.helper +local calc = adaptive_ai.calc +local goblins = adaptive_ai.goblins +local file_tag = goblins.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_goblin(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_goblin(goblin, mother, father) + goblin.fear_height = goblins.stats.fear_height + goblin.pos = {x=random(1,40), z=random(1,40)} + + goblins.setup(goblin, mother, father) + + goblin.fitness = 0 + trainer.console_log("Created new goblin: "..goblin.name) +end + +local function reset_goblin(goblin, map) + goblin.food = {} + goblin.fitness = 0 + goblin.hp = goblins.stats.hp_max + goblin.hunger = goblins.stats.hunger_max + + + goblin.pos.y = map[goblin.pos.x][goblin.pos.z][1] + while not goblin.pos.y do + goblin.pos = {x=random(1,40), z=random(1,40)} + goblin.pos.y = map[goblin.pos.x][goblin.pos.z][1] + end +end + +local goblin_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.hunger = self.hunger + satiate + self.hp = self.hp + 1 + end + end, + function(self, dtime) -- breed + self.fitness = self.fitness + 2 + 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_goblin(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] + 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 then + player.action = decision + player.last_gene = gene + goblin_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 modifier = function(turn, max_turn) return (1 + turn/max_turn) end + +local function turn_log_goblin(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 average: "..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: "..goblins.actions[player.action].."\t\tFocus: "..player.focus.."\n" + s = s.."Gene: "..(player.last_gene or "None").."\t\tFitness: "..truncate(player.fitness * modifier(turn, turn_max)).."\n\n" + + s = s..print_terrain(map, player) + return s, 0.008 +end + +local function gen_log_goblin(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_goblin(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 10000 + 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_goblin, + fill_creature = fill_goblin, + reset_creature = reset_goblin, + decision_managers = goblin_decisions, + manage_creature = manage_goblin, + clone = goblins.clone, + map_checks = map_checks_goblin, + turn_log = turn_log_goblin, + gen_log = gen_log_goblin, + training_log = global_log, + fitness_threshold = 40, + fitness_modifier = modifier, + 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() diff --git a/training/trainer_kobold.lua b/training/trainer_kobold.lua new file mode 100644 index 0000000..820985a --- /dev/null +++ b/training/trainer_kobold.lua @@ -0,0 +1,305 @@ + +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.hunger = self.hunger + satiate + self.hp = self.hp + 1 + end + end, + function(self, dtime) -- breed + self.fitness = self.fitness + 2 + 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] + 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 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 modifier = function(turn, max_turn) return (1 + turn/max_turn) 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 average: "..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(player.fitness * modifier(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 = true + local option = input and 0 or 10000 + 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_modifier = modifier, + 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()