Modularity is a thing not very common among the world of roguelike source code. Most of it is based upon medieval designs such as Nethack or Angband, plus the community itself only few years ago started embracing more open minded languages such as python (how python can survive in a fridge?!) abandoning ye olde senile C. Still, I have seen many outdated things written in Python I would like to forget.
It's bad since many, many roguelike projects get abandoned because the author (who's probably more of a programmer than designer) gets bored by the spagetti.
Neural Nets
What the industry calls "Neural nets" are really pattern recognizing primitives, no sentience involved. We take some inputs, perform something akin to weighted average on them, and if the output is > 0, we have recognized or liked something.
input1 input2 input3
| | |
weight1 weight2 weight3
\ | /
\ | /
\ | /
\ | /
\ | /
Sum
|
|
f()
|
\|/
output
Standard issue neuron
Schematic of neuron in work is below:
1) Sum = 0
2) for each input <> weight pair
3) sum = sum + input*weight
4) output = F(Sum)
f() is a special function used almost exclusively during net leatning, so we don't need to concern ourselves with it now. Weights can be of any range, but for net learning purposes they range from -1.0 to 1.0. We ignore that now and use any range we find suitable.
How to understand them intuitively? How do you rank cars, for example? You probably take colour, speed, silhouette, cost and age into consideration. The higher the attribute, the more desirable it is for the 'general' perspective. So, high value for the age input is a young car, high for colour is a red car while a low (negative) value for speed is a slow car.
If you like 'best of the best' models and are a spoiled rich kid that doesn't care about money and changes his car for newer every year, your net weights might look like this:
colour speed silhouette cost age
10 15 10 0 20
(weight of 0 means 'we are indifferent')
This guy values most the fastest and youngest cars with good appearance, but doesn't care if the car is expensive.
Or maybe you're an eccentric who likes old, ugly cars bought in retail prices, and..
colour speed silhouette cost age
0 0 -5 10 -10
..you don't care about car's colour or quality and the older the car, the better.
If we take a average, but expensive car like this:
colour speed silhouette cost age
3 2 5 10 2
Our millionaire will react to it like this:
colour speed silhouette cost age
10 15 10 0 20 weights
* * * * *
3 2 5 10 2 inputs
+ + + + = 150
Since the output is more than zero, we like the car.
For an old, cheap, painted in pink amphibia we get a reaction of:
colour speed silhouette cost age
10 15 10 0 20 weights
* * * * *
-6 1 -2 1 -5 inputs
+ + + + = -165
The vehicle is repulsing.
Important thing in all of this is that inputs can vary a bit and the spoiled kid will still react negatively to .
That technique can be used for making npc's a tad smarter by making them evaluate their targets. Lets say we want a goblin to avoid strong enemies. How do we define 'strong'? Are tigers strong? How strong exactly? If we make all goblins avoid tigers, all goblins will run from all tigers, so this will get old quickly. If the goblins are cunning, they may run away from adult tigers, but pick on baby tigers or even malnourished adult ones if they have good weapons and significant numbers advantage.
Humans (and goblins by extension) can only approximate if somebody is strong enough. Using neurons, we can emulate this behaviour.
Let me see.. Goblins are cowards who dislike attacking anyone taller and with more friends than them. They also know how to backstab - attacking creatures not hostile to them. They're not stupid or sociable enough to sacrifice themselves for the greater good and will value their own health very much. If the output is positive, there's more motivation to attack than not to. Input values are obtained solely by functions, to be more general than direct variable checking.
input weight
IsHostile(Enemy) -2
WeaponValue(Goblin) 5
NumberOfFightingAlliesOf(Goblin) 10
HeightDifference(Goblin,Enemy) 4
HPDifference(Goblin,Enemy) 5
Difference(HP,MaxHP) 10
This way, we designed an 'Attack' neuron, that will call a mystic InitiateAttack() function if there are no better actions to take (no other neuron has higher output). What sets this apart from other AI methods is that the goblin can act a little erratic - sometimes it runs away when heavily wounded and surrounded by friends, sometimes it's wounded heavily, but so is the surrounded enemy so the goblin keeps fighting. We can have either species weights (all goblins use the same weight vector) or custom weights (every goblin gets his slightly altered set of weights). Adjusting the weights to change a coward into a berserker is very easy from both the programmer and the monster designer's perspective. Some gobbos are afraid of everything, others are overconfident and there are some dirty cowards mugging on the weak and alone. The player will never be sure.
Those are the most basic things in neural net theory, but enough for us to understand what's going on below.
Modular decision maker.
struct Neuron{
VectorOfInputGettingFunctions I; // those are something like int Func(Creature Self, Object Target)
VectorOfWeights W;
FunctionToCallAfterGettingOutput F; // void Func(Creature Self, Object Target)
int output;
};
struct Brain{
VectorOfNeurons N;
};
void RunNeuron(Neuron N, Creature Owner, Object Target)
{
call all those Input Getters, building the I vector, multipy I vector by W vector, obtaining output
}
void ContestOfNeurons(Brain B, Creature Owner, Object Target)
{
call RunNeuron for all Neurons in vector N
choose the neuron with maximum output
return it
}
void UseTheBrain(Brain B, Creature Owner, ListOfTargets L)
{
for every target in L:
call ContestOfNeurons(B, Owner, Target)
store the returned neuron somewhere
choose a neuron from the stored ones with the highest output
call that neuron's F function
Fin
}
There's more pseudocode than C here, but almost everybody will have their own ideas of what a Creature and Object is. In my book, Creatures are Npc's, Objects are Creatures, items and possibly (if we go for very general design) doors, trees, anything we can interact with. Since we use value obtaining inputs instead of directly wiring the input vector to some raw values, the neurons can obtain a sensible value if we try to obtain a weapon value of a door.
Why should I use it?
Remember our goblin? What if we want to teach him something like casting spells? While playing the game? Using states, this requires some magic with flags and ifs which is nice, but a bitch to modify once you've written all the state code.
With the modular design of neuron method, Goblins have Move, Attack, Flee, PickUp, Wear and other neurons. We just add water, or the CastMinorSpell neuron and that's all. Great thing is that we can add and remove those neurons at run time, so we can have a spell named Evolve which grants spellcasting abilities to any humanoid we cast it upon. Or Touch Of Idiocy, which retards everyone into zombie who only StandBy, CloseWith and Attack.
We can also add some unorthodox behaviours like rust monsters hunting for swords in the dungeon, orc pyromaniacs blowing up every powder barrel they spot (if they have too much mana on their hands), kobold skirmishers who keep 1 tile distance to the player when wounded and moving for a strike at full health, or Confusion spell which gives a temporary neuron with a very high output and a function which makes the monster move randomly. Maybe incorporate a phobia system in your game, so every mundane monster has a chance of being blessed with arachnophobia (reacts very negatively to spiders), hydrophobia (hates water elementals) or paranoia (has an insanity counter incremented every time hears a weird noise, starts panic after a certain number of them). Some minor changes in neuron themselves could be done by Fear spell, which alters some kind of EstimetePower function to give much higher value for the caster.
As I said, altering the brain is a trivial task - we can design dozens of neurons and construct many different brains from those blocks. Also storing the weights themselves in a file is nice and clean, so is making minor changes to them.
Why shouldn't I use it?
Since we have to use indirect function calls and calculate every neuron, it's extremely slow in comparison with most other roguelike AI's. Multipy that by the number of targets we want to check with the brain. Turn time roguelikes (most of them) probably won't even notice since you have the user slowing down the game, but this system is rather overkill for real time. I think the reuse and ease factor far oughtweights time and memory consumption and I intend to use this in a general roguelike engine.
Some neurons can frequently alternate between negative and positive outputs - some monsters may exhibit unbalanced behaviour, quickly changing from escaping to attacking or simillar flavour. Could be a problem when we want monsters who can be relied to run at least N turns when scared.
Plus, having unbalanced weight ranges for various neurons can result in the lower ranged ones being seldomly called, if at all. If we assign CastSpell neuron with min/max weight ranges of -10/10 to brain with neurons of average range of -100/100 will probably exclude that neuron from being useful. Additional tests need to be made, if there's really a problem it is solved by restricting the weights to a global range, -10000/10000 for example with normalizing functions when loading the weights from a file.
There are other methods like this one that are more lightweight, like Need driven AI by Bjorn Bergstrom
http://www.roguebasin.com/index.php?title=Need_driven_AI.