Sunday, August 4, 2013

Programming Bravery, Building Personality Traits Into Game AI (Part 1)

Hello there ladies and gentlemen!  Today I am going to explain how to program bravery into AI!

Take a look at this function I wrote:
        public Boolean BraveEnoughToAttack(AI_SingleUnit enemy)
        {

            //how many attacks it would take to kill the enemy
            float howManyAttacksToKill = (float)Math.Ceiling((float)enemy.Health / DamageDealing.MeleeDamage);

            //how long would it take to kill the enemy
            TimeSpan timeTillKill = TimeSpan.FromSeconds(howManyAttacksToKill * (1f / Attacking.AttackSpeed));


            //how many attacks from the enemy it would take to kill me
            float howManyAttacksTillDeath = (float)Math.Ceiling((float)Health / enemy.DamageDealing.MeleeDamage);

            //how long would it take for the enemy to kill me
            TimeSpan timeTillDeath = TimeSpan.FromSeconds(howManyAttacksTillDeath * (1f / enemy.Attacking.AttackSpeed));

            if (timeTillKill < timeTillDeath)
            {
                //should win
                return (true);
            }
            else //should lose or tie
            {
                //How many of me would it take to kill them?

                //given this equation:
                //newTimeToKill = timeTillKill * 1/numberOfMe
                //newTimeTillDeath = timeTillDeath * numberOfMe

                //newTimeToKill = newTimeTillDeath
                //timeTillKill * 1/numberOfMe = timeTillDeath * numberOfMe
                //timeTillKill = timeTillDeath * numberOfMe^2
                //timeTillKill / timeTillDeath = numberOfMe^2
                //sqrt(timeTillKill/timeTillDeath) = numberOfMe

                //because timeTillKill > timeTillDeath, numberOfMe will always be greater than sqrt(1)
                float numberOfMeItWouldTakeToKill = (float)Math.Sqrt(timeTillKill.TotalSeconds/timeTillDeath.TotalSeconds);

                if (1 - (1f / numberOfMeItWouldTakeToKill) <= Personality.Bravery / 100f)
                {
                    return (true);
                }
                else
                {
                    return (false);
                }
            }
        }

So let's get explaining!  The purpose of this function is to determine whether or not a unit is brave enough to consider attacking an enemy.  A unit is either brave enough or it isn't, so the function returns either true or false.  True being brave enough; false being not brave enough.

Let's explain some variables!

howManyAttacksToKill
//how many attacks it would take to kill the enemy
float howManyAttacksToKill = (float)Math.Ceiling((float)enemy.Health / DamageDealing.MeleeDamage);

This is the number of attacks it would take the unit to kill the enemy. This is calculated by dividing the amount of health the enemy has by the amount of damage the unit can do in one hit. We round the value up because, when it comes to hitting, there are no halfsies. A unit either hits the enemy or it doesn't.

timeTillKill
//how long would it take to kill the enemy
TimeSpan timeTillKill = TimeSpan.FromSeconds(howManyAttacksToKill * (1f / Attacking.AttackSpeed));

This is how long it would take the unit to kill the enemy.  This is calculated by multiplying the number of attacks it would take the unit to kill the enemy by the attack speed of the unit.

howManyAttacksTillDeath
//how many attacks from the enemy it would take to kill me
float howManyAttacksTillDeath = (float)Math.Ceiling((float)Health / enemy.DamageDealing.MeleeDamage);

This is the number of attacks it would take the enemy to kill the unit. This is calculated by dividing the amount of health the unit has by the amount of damage the enemy can do in one hit. Once again, we round the value up because we don't hit like little sissies.

timeTillDeath
 //how long would it take for the enemy to kill me
TimeSpan timeTillDeath = TimeSpan.FromSeconds(howManyAttacksTillDeath * (1f / enemy.Attacking.AttackSpeed));

This is how long it would take the enemy to kill the unit.  This is calculated by multiplying the number of attacks it would take the enemy to kill the unit by the attack speed of the enemy.

Let's look at the first conditional statement:
if (timeTillKill < timeTillDeath)
{
     //should win
     return (true);
}

In this case, the unit has calculated that it will be able to kill the enemy faster than the enemy can kill the unit, so we say that the unit is brave enough to attack the enemy.

In order to really explain this, I think I need to define "bravery" first.
Google says that bravery is "courageous behavior or character." 
So what exactly does "courageous" mean then? 
Google says that courageous means "not deterred by danger or pain." 
Those are some pretty "human" definitions. They need to be translated so that they are more relevant to my AI. My AI is not programmed to sense pain, so let's talk about danger. To the unit, a dangerous enemy would be an enemy that, given a fair fight, would kill the unit if it were to engage in combat. As long as our unit thinks it will be able to kill the enemy, then it's not really being "brave" because it doesn't think there is any danger. So, given a supposedly guaranteed victory, the unit is brave enough to engage because it doesn't even have to consider being brave. To the unit, there is nothing to worry about. We return true.

Let's look at the next part of the conditional statement:
else //should lose or tie
{
     //How many of me would it take to kill them?

     //given this equation:
     //newTimeToKill = timeTillKill * 1/numberOfMe
     //newTimeTillDeath = timeTillDeath * numberOfMe

     //newTimeToKill = newTimeTillDeath
     //timeTillKill * 1/numberOfMe = timeTillDeath * numberOfMe
     //timeTillKill = timeTillDeath * numberOfMe^2
     //timeTillKill / timeTillDeath = numberOfMe^2
     //sqrt(timeTillKill/timeTillDeath) = numberOfMe

     //because timeTillKill > timeTillDeath, numberOfMe will always be greater than sqrt(1)
     float numberOfMeItWouldTakeToKill = (float)Math.Sqrt(timeTillKill.TotalSeconds/timeTillDeath.TotalSeconds);

     if (1 - (1f / numberOfMeItWouldTakeToKill) <= Personality.Bravery / 100f)
     {
          return (true);
     }
     else
     {
          return (false);
     }
}

In this case, the unit has calculated that the enemy would kill the unit faster than the unit would kill the enemy (a losing situation) or that they would both kill each other at the same time (a neutral situation).  This is where I really need to define "bravery".  I need to make bravery measurable.  So how do I do that?  I think it's fair to say that, in a fight with a stronger opponent, the stronger the opponent is, the more brave it would be to engage in combat.  So it would be more brave to fight someone who is three times stronger than you, rather than someone who is two times stronger than you.  I need to quantify how much stronger the enemy is in comparison to the unit.  Let me break it down for you.

How many units would it take to kill the enemy faster than the enemy could kill the units?

This problem can be solved with either an equation or through trial and error.  With the amount of times this code would be executing in my game, trial and error is simply not an option.  I need to come up with an equation.

Since the conditional compares how long it would take the unit to kill the enemy versus how long it would take the enemy to kill the unit, I am going to keep the comparison between kill times.

To start, I've already defined when the unit will kill the enemy faster than the enemy will kill the unit: when timeTillKill is greater than timeTillDeath.  I need to figure out when the new timeTillKill will be greater than the new timeTillDeath. For the sake of simplicity, it's okay to just figure out when the new timeTillKill will equal the new timeTillDeath because that is where the threshold of victory lies.

So, the equation can be defined as: 
newTimeTillKill = newTimeTillDeath

Now I need to define newTimeTillKill and newTimeTillDeath.  This is what I came up with:

newTimeTillKill = oldTimeTillKill / numberOfMe
newTimeTillDeath = oldTimeTillDeath * numberOfMe

I realize that these definitions are not perfect and are only an approximation, but these are the only definitions I could come up with that returned a solvable equation.  If you can think of a more accurate solution, please let me know.  On a side note, we do not need a perfectly accurate definition because bravery is a relatively subjective matter.

Solving for numberOfMe:
newTimeTillKill = newTimeTillDeath

oldTimeTillKill / numberOfMe = oldTimeTillDeath * numberOfMe

oldTimeTillKill = oldTimeTillDeath * numberOfMe2

oldTimeTillKill / oldTimeTillDeath  = numberOfMe2

sqrt(oldTimeTillKill / oldTimeTillDeath) = numberOfMe

Final equation:
numberOfMe = sqrt(timeTillKill / timeTillDeath)

So, now that we have an idea of how much stronger the enemy is, we need to evaluate whether or not a unit is brave enough to consider attacking the enemy.  In my game, DATABITS, the bravery attribute is a number between 1 and 100 and could be considered a percentage.  50 would mean 50% bravery.  Because I want the bravery attribute to account for enemies that are between (inclusive) 1 and (exclusive) infinity times stronger, I need to have some kind of parabolic relationship in my evaluation of bravery.  This is where the next conditional statement comes in.

if (1 - (1f / numberOfMeItWouldTakeToKillEnemy) <= Personality.Bravery / 100f)
{
     return (true);
}
else
{
     return (false);
}

By taking the inverse of numberOfMeItWouldTakeToKillEnemy, we're returning how strong the unit is in comparison to the enemy.  So if it takes three units to kill the enemy, the unit can be considered to be as strong as 33.3% of the enemy.  This is a nice number to have, but because a higher bravery attribute means that a unit is more brave, we need to subtract the calculated value from 100% when comparing the value to a unit's bravery attribute.  This way, it takes 77.7% bravery to fight an enemy that is three times stronger than the unit.  Because we know that this code will only be reached in neutral or losing conditions, we know that the variable numberOfMeItWouldTakeToKillEnemy will always be greater than or equal to one.

That's all I have for today!  I know that there are various other ways this could be done, so feel free to share any of your thoughts!

...Part 2...

4 comments:

  1. I enjoyed this post as it was extremely educational to me. Thank you for taking the time to be so thorough.

    ReplyDelete
  2. I like the idea , imagine a combination with stats or skills in a rpg , lets say if a unit has a particular level of intelligence it can get a better evaluation of the the enemy and correctly determine if it can kill it or not , another ability is to recalculate his chance to win based on the number of allies and their damage output. Imagine a mob that can determine correctly that you (the hero) are more stronger than him and will try to escape and it will go in search of allies so he can take you down. imagine a more advanced scenario in which the mobs coordinate themselves so that they can take you down (something in the lines of a party trying to take a boss down , in which u are the boss)

    ReplyDelete
    Replies
    1. That's exactly where I'm going with this! In terms of mob mentality, I have two traits in mind: cohesion and subservience. Cohesion determining how likely units are to stick together; subservience determining how likely units are to respond to their allies' cries for help. Keep an eye out for those posts. I should be tackling them after I handle aggressiveness.

      And you can simulate intelligence in two ways:

      A)Provide less accurate data for lower intelligence. Aka randomly modify the data by a certain threshold before passing it into the relevant functions.

      B)Don't provide as much data for lower intelligence. Do calculations based on the data that is available. More data means more accuracy or "intelligence".

      Sent from my phone. Please excuse any brevity.

      Delete