Initial commit
76
api.lua
Normal file
|
@ -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
|
595
api.md
Normal file
|
@ -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
|
476
api_mobs.lua
Normal file
|
@ -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
|
2
depends.txt
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
default
|
||||||
|
mobs
|
375
goblin/goblin.lua
Normal file
|
@ -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
|
514
goblin/goblin_integration.lua
Normal file
|
@ -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
|
||||||
|
})
|
135
helper.lua
Normal file
|
@ -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
|
954
helper_mobs.lua
Normal file
|
@ -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
|
22
init.lua
Normal file
|
@ -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")
|
369
kobold/kobold.lua
Normal file
|
@ -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
|
514
kobold/kobold_integration.lua
Normal file
|
@ -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
|
||||||
|
})
|
189
moondrum.lua
Normal file
|
@ -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
|
239
population.lua
Normal file
|
@ -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
|
BIN
textures/aa_imp_face.png
Normal file
After Width: | Height: | Size: 2.9 KiB |
BIN
textures/aa_imp_side.png
Normal file
After Width: | Height: | Size: 2.8 KiB |
BIN
textures/aa_imp_top.png
Normal file
After Width: | Height: | Size: 2.8 KiB |
BIN
textures/aa_moondrum_mushroom.png
Normal file
After Width: | Height: | Size: 3 KiB |
BIN
textures/aa_moondrum_roots.png
Normal file
After Width: | Height: | Size: 2.9 KiB |
BIN
textures/tnt_smoke.png
Normal file
After Width: | Height: | Size: 202 B |
BIN
textures/transparent.png
Normal file
After Width: | Height: | Size: 2.7 KiB |
182
training/noise.lua
Normal file
|
@ -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
|
112
training/trained_kobolds.txt
Normal file
|
@ -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
|
300
training/trainer.lua
Normal file
|
@ -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
|
305
training/trainer_goblin.lua
Normal file
|
@ -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()
|
305
training/trainer_kobold.lua
Normal file
|
@ -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()
|