Jump to content
Cookie Ninja

How to make custom AI

Recommended Posts

Opening statement. In my quest to make the most intelligent foes possible I have run into some limitations, although there are some AI scripts out there they allow you to list a condition, an action and a target of that action. But what if I want to list multiple conditions and add some random variance to the enemys behavior while still ensuring that the best course of action is performed? I will try to adress it in this tutorial, hopefully it won't be to hard to follow and I also hope that it will prove useful to some of you aspiering developers. I will end this opening statement by saying that this is the first tutorial I've ever written so apologies for any upcoming bad explenations.
 
For simpler AI(s) I suggest the use of these 2 scripts because they are more user friendly. My script is compatible with either of them but I'm not sure if they are compatible with eachother.

Please read their respective TOU before use, these scripts are NOT mine! I'm marely pointing you in the right direction. ;) The use of my script is mainly to use if you want, either to bypass any underlying game mechanics or to write very complex AI.

 

Shall we get started?

 

Table contents:

  • Setup, stuff needed for the AI to work
     
  • Guide for writing AIs
     
  • Screenshots
     
  • Math, dealing with probability use in said AI (optional)

Setup: CTRL C+V this little scriplet below materials above main at the bottom of your scripts.

################vCookie_brainv################
class Scene_Battle < Scene_Base
  alias rearrange_invoke_item invoke_item
  def invoke_item(target, item)
    @subject.last_target_index = target.index
    rearrange_invoke_item(target, item)
  end
end
 
# Thanks to Shiggy the battle log messages doesn't go haywire 
 
class Window_BattleLog < Window_Selectable
  def display_use_item(subject, item)
    if item.is_a?(RPG::Skill)
      return if item.note.include?("shiggy")
      add_text(subject.name + item.message1)
      unless item.message2.empty?
        wait
        add_text(item.message2)
      end
    else
      add_text(sprintf(Vocab::UseItem, subject.name, item.name))
    end
  end
  def display_action_results(target, item)
    if target.result.used
      if item.note.include?("cookie")
        last_line_number = line_number
        display_affected_status(target, item)
        display_failure(target, item)
        wait if line_number > last_line_number
        back_to(last_line_number)
      else
        return if item.note.include?("shiggy")
        last_line_number = line_number
        display_critical(target, item)
        display_damage(target, item) 
        display_affected_status(target, item)
        display_failure(target, item)
        wait if line_number > last_line_number
        back_to(last_line_number)
      end
    end
  end
end 
 
class Game_Battler < Game_BattlerBase
  attr_accessor :cookie
  def cookie_of_choice(skill_id, target)
    cookie = Game_Action.new(self, true)
    cookie.set_skill(skill_id)
    if target == 0
      cookie.target_index = last_target_index
    end
    @actions.push(cookie)
  end
end
################^Cookie_brain^################

 
class Game_Battler < Game_BattlerBase
  # We will write our AI(s) here
end


Easy enough? :P
 
 
Guide:
Before we start I want to mention that the way we write these AI, they will bypass any game mechanics unless we specifically tell them not to. The benefit being that we have ultimate freedom to design their behaviour, the downside is that we have to tell them to respect restrictions if we want them to do so. Time to write our AI!
 
First we want to name the AI. I usually  name them EnemyName_brain(a, B) so it's easy to keep track of them.

  def Jelly_brain(a, 
    # Here goes what we want it to do!
  end


 
Next we decide how many conditional branches we want, in this case 2. I'll also write under what condition they will trigger.

  def Jelly_brain(a, 
    if a.state?(4) == false
      # This means that unless the Jellyfish is silenced it will perform these actions
    else
 
    end
  end


 
Now that I have set the initial condition I can go ahead and decide exactly what it is supposed to do. I want it to use 1 of 4 elemental spells, the catch being that I want it use the one that its target is most vulnerable to. First we need to figure out what that greatest weakness is and then choose what Spell to use. Glasses showd me an awsome way to accomplish this.

  def Jelly_brain(a, 
    if a.state?(4) == false
      # A big thank you to Glasses for an awsome way to to compare element rates!
      max = [3, 4, 5, 6].max_by { |e| b.element_rate(e) }
      case max
      when 3 # When target is weak to fire (element ID)
        return 51 # Use Fire spell (skill ID)
      when 4
        return 55
      when 5
        return 59
      else
        return 63
      end
    else
      return 1
    end
  end


At this point the whole script should look like this

################vCookie_brainv################
class Scene_Battle < Scene_Base
  alias rearrange_invoke_item invoke_item
  def invoke_item(target, item)
    @subject.last_target_index = target.index
    rearrange_invoke_item(target, item)
  end
end
 
# Thanks to Shiggy the battle log messages doesn't go haywire 
 
class Window_BattleLog < Window_Selectable
  def display_use_item(subject, item)
    if item.is_a?(RPG::Skill)
      return if item.note.include?("shiggy")
      add_text(subject.name + item.message1)
      unless item.message2.empty?
        wait
        add_text(item.message2)
      end
    else
      add_text(sprintf(Vocab::UseItem, subject.name, item.name))
    end
  end
  def display_action_results(target, item)
    if target.result.used
      if item.note.include?("cookie")
        last_line_number = line_number
        display_affected_status(target, item)
        display_failure(target, item)
        wait if line_number > last_line_number
        back_to(last_line_number)
      else
        return if item.note.include?("shiggy")
        last_line_number = line_number
        display_critical(target, item)
        display_damage(target, item) 
        display_affected_status(target, item)
        display_failure(target, item)
        wait if line_number > last_line_number
        back_to(last_line_number)
      end
    end
  end
end 
 
class Game_Battler < Game_BattlerBase
  attr_accessor :cookie
  def cookie_of_choice(skill_id, target)
    cookie = Game_Action.new(self, true)
    cookie.set_skill(skill_id)
    if target == 0
      cookie.target_index = last_target_index
    end
    @actions.push(cookie)
  end
end
################^Cookie_brain^################

 
 
class Game_Battler < Game_BattlerBase
  def Jelly_brain(a, B)
    if a.state?(4) == false
      # A big thank you to Glasses for an awsome way to to compare element rates!
      max = [3, 4, 5, 6].max_by { |e| b.element_rate(e) }
      case max

      when 3 #When fire is the greatest weakness
        return 51 #Use "Fire" spell

      when 4
        return 55
      when 5
        return 59
      else
        return 63
      end
    else
      return 1
    end
  end
# In skill formula: a.cookie_of_choice(a.Jelly_brain(a, B), 0)
end


In order to now make use of this simplistic AI save and close the script. Press F9 and open the 'Skills' tab and make a new skill that you name whatever you want. Set it to HP Damage, put a.cookie_of_choice(a.Jelly_brain(a, B), 0) in the formula box and lastly put <shiggy> in the notebox. We are now done with the skill.

Head over to the 'Enemies' tab and select the enemy that will make use of the AI, empty out its skill list and only give it the skill we just made! Voila! We have just made our first "smart" enemy.

This was a very simple AI we just made, and now I want to make a smarter AI boss that has these smart jellyfishes(contradiction in terms lol) as minions. So where do we start?

Right where we left off^^ I'll try to go abit faster here since we need to cover more ground

class Game_Battler < Game_BattlerBase
  def Jelly_brain(a,
    if rand(5) >= 1 && a.state?(4) == false
      # A big thank you to Glasses for an awsome way to to compare element rates!
      max = [3, 4, 5, 6].max_by { |e| b.element_rate(e) }
      case max
      when 3 #When fire is the greatest weakness
        return 51 #Use "Fire" spell
      when 4
        return 55
      when 5
        return 59
      else
        return 63
      end
    else
      return 1
    end
  end
  # In skill formula: a.cookie_of_choice(a.Jelly_brain(a, , 0)
  def Angel_brain(a,
    if rand(3) == 2 && a.hp < a.mhp*0.2 && a.mp != 0 && a.state?(4) == false
      return 27
    elsif rand(2) == 1
 
    else
 
    end
  end
  # In skill formula: a.cookie_of_choice(a.Angel_brain(a, , 0)
end


Now what did I do here? Let me explain, first I put down the random conditions so that each of the 3 branches has exactly 1/3 chance of coming up. (rand(x) == y)
Then I made it so that the first branch would return the skill ID of Heal2
But I don't want the boss to heal at every turn of the battle so I put down some extra conditions (&& = and)
The way it works now, the boss needs to have less than 20% HP and not 0 MP and not be silenced to use Heal2

Since the Angel and the Jellyfish go together I don't mind them using the same skills. So I'll add in the parts we already made into the second branch of the Angel AI.

   def Angel_brain(a, 
    if rand(3) == 2 && a.hp < a.mhp*0.2 && a.mp != 0 && a.state?(4) == false
      return 27
    elsif rand(2) == 1
      if a.state?(4) == true
        return 997
      else
        max = [3, 4, 5, 6].max_by { |e| b.element_rate(e) }
        case max
        when 3
          return 51
        when 4
          return 55
        when 5
          return 59
        else
          return 63
        end
      end
    else
 
    end
  end


Please note that I added a sub condition to the magic branch. Now every time the player tries to silence the boss in an atempt to prevent it from using heals/spells, it will use Judgement (a very nasty skill) every time the magic branch gets chosen. Wich is now 50% of the time since the boss is silent.

Lets play with states in the final branch.

  def Angel_brain(a, 
    if rand(3) == 2 && a.hp < a.mhp*0.2 && a.mp != 0 && a.state?(4) == false
      return 27
    elsif rand(2) == 1
      if a.state?(4) == true
        return 997
      else
        max = [3, 4, 5, 6].max_by { |e| b.element_rate(e) }
        case max
        when 3
          return 51
        when 4
          return 55
        when 5
          return 59
        else
          return 63
        end
      end
    else
      if b.atk > b.mat && b.state?(3) == false
        return 127 
        #Inflict Blind
      elsif b.atk < b.mat && b.state?(4) == false
        return 128 
        #Inflict Silence
      elsif b.mhp > a.hp && b.state?(2) == false
        return 129 
        #Inflict Poison
      else
        return 4
      end
    end
  end
  # In skill formula: a.cookie_of_choice(a.Angel_brain(a, , 0)


In this last branch I set it up like so:

  • If target is ATK heavy and not blinded it will use blind attack
  • If target is MAT heavy and not silenced it will use silence attack
  • If target Max HP is higher than the bosses HP and not poisoned it will use Poison attack
  • If none of the above the boss will use dubble attack

Screenshots of setup: Red arrows point to the important stuff. All else is entirely up to you, note that I placed the skill(AI) at the bottom of the skill list. This is not a nessecity, I just do it so I can easily overlook my AI and enemy unique skills.

 

Installing script:

BPvDYF9.png

 

Skill setup:

wurZChI.png

 

Enemy setup:

4klhbKN.png

 

 

So our AIs are done. What now? I know, the fun stuff, math^^ This portion covers the use of probability when writing conditional branches:D
 
Maths:

I briefly mentioned that,
if rand(3) == 2
elsif rand(2) == 1
else
equates to 33% chance for each branch to come up. How can that be when they are rolling different dices?
 
If we consider the individual dice rolls, the first dice rolls 0-2 and needs 2 to succeed, the second dice rolls 0-1 and needs 1 to succeed while the else statement picks up the remainder.
 
Then:
00 = else (neither dice succeded)
01 = elsif (the second dice succeded)
10 = else (neither dice succeded)
11 = elsif (the second dice succeded)
20 = if (the first dice succeded)
21 = if (both succeded but since the if statement comes first it wins out)
 
As you can see each one comes up 1/3 of the time, but if we want to make many more branches with random chances to come up, then writing down every possible outcome to assess the probability gets rather tedious. Lucky for you there is an equation for this. To show you how it works I'll make an example with multiple dices. Uppercase letter is the true likelyhood of comming up while lowercase letter represents the individual dice roll.
 
If:
a = 1/3
b = 1/3
c = 1/3
d = picks up the remainder (else statement)
 
Then:
A = a
B = b - b*A
C = c - c*B - c*A
D = 1 - A - B - C
 
It works by first calculating the succes rate of the dice roll, and then subracting the number of times it is blocked by all previous dice rolls.
 
The previous example would result in:
A = 1/3    or 33.33%
B = 2/9    or 22.22%
C = 4/27  or 14.81%
D = 8/27  or 29.63%
 
If you want to make each of your branches to have the same likelyhood of coming up then there is 2 ways to accomplish this.

  • Use a different code for your branches.

    n = rand(4)
    case n
    when 0
    # do this
    when 1
    # do this
    when 2
    # do this
    else
    #do this

  • Or you could drop the nominator by 1 in every diceroll
    a = 1/4
    b = 1/3
    c = 1/2
    d = (1/1 is taken up by the else statement)

Each one of these statements will have exactly 25% chance of coming up no matter what approach you take.
 
That will be it for the math section, hope I managed to shed some light on the situation.

 

 

The tutorial can now be considered complete, I may update the script in the future but don't hold your breath^^ (unless super bug is found that needs dealing with)
 
TOU for script

  • No need for credit, I would be happy to know that it has come to use for someone else than me.
     
  • If you really want to credit someone, credit the community I could never have done this without them.

Special thanks to Shiggy, the AIs are less awkward thanks to your help!
 
So what do you think for my first ever tutorial? Is something missing? I'll try to answer any questions you have to the best of my capability! If you found it useful or worthless, pls leave a comment^^

Edited by Cookie Ninja

Share this post


Link to post
Share on other sites

Glad you liked it!^^ I prefer weak enemies that are tricky to beat because they are smart, as opposed to powerhouses that are dumb as bricks. Sadly the latter is alot more prevalent in games, understandibly so, they are ALOT easier to make. I would appreciate if more developers saw the enemies as an opportunity to add depth to combat. Instead of making them into punching bags. :P

Share this post


Link to post
Share on other sites

Couldn't agree more! In my experience I like to have all fights a challenge, neither side can really soak up unrealistic amounts of damage and, on top of them (now!) not sticking to a predictable pattern, fights could be damn near impossible :P

Share this post


Link to post
Share on other sites

First we want to name the AI. I usually  name them EnemyName_brain(a, B) so it's easy to keep track of them.

Do you mean EnemyName_moveset. A moveset is like knowing a list of moves.

 

And Brain is depending how smart you are, like quizzes and trivia.

 

If moveset doesn't make sense, try strategy.

Share this post


Link to post
Share on other sites

I went quickly through the tutorial just to check what it was about (without opening spoilers) and was surprised to see I was in the credits. I'm not sure if I have memory problems or if I just help too many people .

 

What did I do exactly ?

Edited by Shiggy

Share this post


Link to post
Share on other sites

Do you mean EnemyName_moveset. A moveset is like knowing a list of moves.

 

 

 

And Brain is depending how smart you are, like quizzes and trivia.

 

If moveset doesn't make sense, try strategy.

In a way you are right, what we actually are doing is constructing a FI (Fake Intelligence). The enemy is actually as dumb as a brick but behaves as if it is smart, it only have a complicated set of rules for what to do in certain situations. If you feel that the method name is inacurrate, then feel free to change it to a more suitable name when writing your own :P

 

 

I went quickly through the tutorial just to check what it was about (without opening spoilers) and was surprised to see I was in the credits. I'm not sure if I have memory problems or if I just help too many people .

 

What did I do exactly ?

You helped me to fix the battle messages silly, before your aid it would say something like this.

 

-Jellyfish uses Jelly_brain

-No dmg

-Jellyfish uses Fire

-Eric took 246 dmg

 

You helped me to remove the first 2 lines, and for that I'm grateful :wub:

Share this post


Link to post
Share on other sites

Thanks for this tutorial on enemy AI. It's a nice way of having enemies be smarter than rmvxa normally can provide.

I also found the battle system quite limited and very lacking in setting up conditions for enemies' AI.

Thankfully, I found TDS Custom AI (Skill Use AI) a long while ago, which I have been using to set up the "smarter" enemies' AI.

When I don't feel like going all out, or when I don't deem it necessary, since not all enemies need good AI in my eyes, I too use Hime Enemy Action Conditions.

 

This is an example of how I have an enemy's AI set up with TDS Custom AI. It might seem tricky at first, since it has so many options, but it's not that bad once you get the hang of it.

 

t.state?(20) is reflect btw, so that the enemy doesn't cast magic that will just reflect back to itself.

you can make enemy AI really really smart, but it can a) make the enemies too tough and B) take too much time to program ^^''

 

 

<Skill_Use_AI: 66> #pyroburst
Remove_If: t.state?(20)
Require: rand(100) > 85
Select_Default
</Skill_Use_AI>

<Skill_Use_AI: 67> #incinerate1
Remove_If: t.state?(20)
Require: rand(100) > 85 && u.tp > 999
Select_Default
</Skill_Use_AI>

<Skill_Use_AI: 298> #incinerateall
Remove_If: t.state?(20)
Require: rand(100) > 90 && u.tp > 999
Select_Default
</Skill_Use_AI>

<Skill_Use_AI: 71> #icespear
Remove_If: t.state?(20)
Require: rand(100) > 90
Select_Default
</Skill_Use_AI>

<Skill_Use_AI: 72> #deepfreeze1
Remove_If: t.state?(20)
Require: rand(100) > 90 && u.tp > 999
Select_Default
</Skill_Use_AI>

<Skill_Use_AI: 331> #deepfreezeall
Remove_If: t.state?(20)
Require: rand(100) > 90 && u.tp > 999
Select_Default
</Skill_Use_AI>

<Skill_Use_AI: 152> #dispel
Select_If: t.states.any? {|state| [14,20,21,22,23,24,28,29,35,36,39,46,60,61,62,63,64,65,66,67].include?(state.id) }
Require: rand(100) > 55
Select_Default
</Skill_Use_AI>

<Skill_Use_AI: 139> #custudio
Remove_If: t.state?(46) || t.state?(20)
Require: rand(100) > 85
Select_Default
</Skill_Use_AI>

<Skill_Use_AI: 117> #baffle
Remove_If: t.state?(5)
Require: rand(100) > 90
Select_Default
</Skill_Use_AI>

<Skill_Use_AI: 116> #amnesia
Remove_If: t.state?(4) || t.state?(27)
Require: rand(100) > 90
Select_Default
</Skill_Use_AI>

<Skill_Use_AI: 267> #delayall
Remove_If: t.state?(34)
Require: rand(100) > 90
Select_Default
</Skill_Use_AI>

<Skill_Use_AI:120> #delay1
Remove_If: t.state?(34)
Require: rand(100) > 90
Select_Default
</Skill_Use_AI>

<Skill_Use_AI: 122> #suspend
Remove_If: t.state?(7)
Require: rand(100) > 80
Select_Default
</Skill_Use_AI>

<Skill_Use_AI: 138> #tunica
Remove_If: t.state?(39) || t.state?(20)
Require: rand(100) > 70
Select_Default
</Skill_Use_AI>

<Skill_Use_AI: 121> #rev
Remove_If: t.state?(35) || t.state?(20)
Require: rand(100) > 80
Select_Default
</Skill_Use_AI>

<Skill_Use_AI: 140> #reflect
Remove_If: t.state?(20)
Require: rand(100) > 60
Select_Default
</Skill_Use_AI>

<Skill_Use_AI: 107> #capto
Remove_If: t.mp >= 200
Require: rand(100) > 50
Select_Default
</Skill_Use_AI>

<Skill_Use_AI: 106> #transvoro
Remove_If: t.state?(20)
Require: rand(100) > 60 && u.hp_rate < 0.4
Select_Default
</Skill_Use_AI>

<Skill_Use_AI: 131> #might
Require: rand(100) > 50
Remove_If: t.state?(20) || t.state?(23) || t.id == 241
Sort_by: t.atk
Select_First
Select_Default
</Skill_Use_AI>

<Skill_Use_AI: 1>
Select_Default
</Skill_Use_AI> 

 

 

Edited by Jolt Android

Share this post


Link to post
Share on other sites

Create an account or sign in to comment

You need to be a member in order to leave a comment

Create an account

Sign up for a new account in our community. It's easy!

Register a new account

Sign in

Already have an account? Sign in here.

Sign In Now

  • Recently Browsing   0 members

    No registered users viewing this page.

×