Battle AI
Overview
Scripting Language
Basic Syntax
Subject Composition
General Subjects
Specific Subjects
Conditions
Conditional Functions
Comparisons
Multiple Conditions (C 16216c218q lauses)
Using <OR> with Conditions
Actions
Functions
Random Actions
APE Gamevars
Nested Blocks
Special Behavior of Nested Default Blocks
The Battle AI system uses text scripts to define behavior for monsters in battle. The scripting language is very specific and just powerful enough to do what it needs to do. There are some seemingly reasonable things which can't be done with it, but these things are by no means necessary in creating capable and challenging monster brains. However, in general the language is easily expandable from within the Anachronox source.
Any given monster that exists in the Game Data Base should have a key that specifies which script is used for it:
.
level int 5
xp int 60
aiscript file "battle/ai/scriptname.txt"
.
When battle starts, the game will load the script file for each monster, examine them and report any errors found. When the monster's timer is up, the game will stop time and execute the script. The script then decides what the best action(s) is(are) and makes it(them) happen. When the monster is done doing everything it decided to do, it's turn is over and battle time resumes.
In an abstract sense, the script files are simply a series of condition-action blocks. (A block is everything between a left bracket and a right bracket; there will be more info on syntax later). Each block begins with one or more conditions to be met, and one or more actions to be performed if all the conditions are true. The game will proceed through the script one block at a time until a true condition is found. Then it will execute all the actions in that block and quit.
First, here is an example of a very simple script:
# Test script, just basic
default
Or, in a conceptualized version:
# comment
condition
First of all, everything on a line that comes after a pound sign (#) is a comment and will be ignored by the game. You can use them anywhere you want, but everything after it on that line is a comment.
In the example above, "default" is the only condition. Default is a special conditional which is always true. It's used to tell the AI what the default behavior is in case none of the other conditions are true. In this case, we use default because we want the monster to do the same thing every time.
Notice the brackets. Every condition must be followed by a block of actions, which must exist entirely between an open and a close bracket. To be totally clear: only comments and conditions can be outside the brackets. Only actions can be inside the brackets (with one exception, which we will talk about in a later section). Also, each individual action must be in parentheses.
"Attack nelohp" will make the monster try to attack, in any way possible, it's nearest enemy with the lowest hitpoints. What's that mean?
"In any way possible" means that it will think about each attack it's capable of, starting with the most powerful, until it finds of one that would work. Also, if any of the attack functions can't find an attack to use, it automatically makes the monster move toward the target and/or toward a position where an attack might be possible.
"It's nearest enemy with the lowest hitpoints" is specified by "nelohp." First of all, distances are measured in terms of how many turns it would take to walk there. There will be more information about understanding nelohp in the next section, but for now just understand that "nearest" is more important than "lowest hitpoints." In other words, if the nearest enemy is three steps away then no enemies more than three steps away will be considered.
The most complex part of writing these scripts is referring to specific entities in battle. After you get the hang of it though, it's very intuitive and fairly easy.
A subject is a short string of characters that refers to a single entity in battle. It may refer to the exact entity, or it may specify a random individual from a group of entities. The basic structure of a subject looks like this:
distance + team [ + stat range + stat type ]
Stat range and stat type are in brackets because they are not required. However, if you specify one you must specify the other as well. Here's a breakdown of what each of those things mean, and what the valid values are:
Distance |
|
N |
Nearest |
F |
Farthest |
A |
Adjacent |
* |
Whatever |
Team |
|
E |
Enemy |
A |
Ally |
* |
Whatever |
Stat Range |
|
LO |
Lowest |
HI |
Highest |
Stat Type |
|
HP |
Hitpoints |
Special Subjects |
|
SL |
Self |
All you have to do is put the letters together, and case doesn't matter. Here are some examples to help solidify it for you:
Nahihp |
Nearest ally highest hitpoints |
sl |
Myself |
*a |
Any ally |
n*hihp |
The nearest whatever with the highest hitpoints |
ae |
Any adjacent enemy |
Aelohp |
Adjacent enemy with the lowest hitpoints |
** |
Some random lucky bastard |
You can also specify specific nodes in battle by using a predetermined field created in the editor. For example, say there is a node in your battle that you want everyone to try to attack. You could say:
(attack &targetname=deathnode)
And in BED (the battle editor), you would have to give that node a key/value pair of "targetname=deathnode". Also, if you have an APE gamevar or gamestring that holds the value you want to search for, you can use it as well. However, if you use a numeric variable, you must prefix it with a percent sign ("&uid=%next_uid") or use a dollar sign prefix for a string variable ("&message=$node_message").
Just for completeness' sake, here is the general syntax for it:
&keyname=[%,$]value
Conditions control the flow of your script. If there is a prime place for elusive errors and bugs to creep in, this is it. Conditions consist of operations, or functions, which are performed on a subject and result in an integer or boolean value. If the particular function in use results in an integer value, you must provide a comparison (more detail coming up). If the function is boolean, you don't need to compare it to anything. The "default" function, from the example script above, is a boolean function that always results in TRUE.
Here is the format for a conditional statement:
subject.function [ operator value[%] ]
or
gamevar:name [ operator value[%] ]
The subject is a string of characters as just specified in the last section. Next, there must be a dot/period. Then comes the function. Here is a list of the possible functions:
These five evaluate to an integer value.
Hp |
Hitpoints |
Naden |
Number of adjacent enemies |
Nadal |
Number of adjacent allies |
Nen |
Total number of enemies |
Nal |
Total number of allies |
And these are binary.
Isburn |
Is subject burning? |
Isnuts |
. nuts? |
Ispois |
. poisoned? |
Isfroz |
. frozen? |
Isslow |
. slowed? |
Ishast |
. hasted? |
Iswink |
. winky? |
Issnoo |
. snoozing? |
Ispsys |
. a psy slave? |
Isvuln |
is subject vulnerable to attack from any enemy? |
Isvis |
is subject visible to any enemy? |
Canburn |
Am I capable of inflicting this status on subject? |
Cannuts | |
Canpois | |
Canfroz | |
Canslow | |
Canhast | |
Canwink | |
Cansnoo | |
Canpsys | |
Canbeat | |
Iamon | |
Isoccupied | |
Canhurt |
Am I capable of using hurt mystech on subject? |
Canheal |
Am I capable of using heal mystech on subject? |
CanFixburn |
Am I capable of fixing this status on subject? |
CanFixnuts | |
CanFixpois | |
CanFixfroz | |
CanFixwink | |
CanFixsnoo | |
CanFixpsys |
If you're using a boolean function, then that's it. However, if you're using one of the functions that result in a value, you will need to provide a comparison. For example:
sl.hp < 30%
(Note that the spaces on either side of the operator are currently required. This is a limitation that may or may not be fixed depending on time restraints and demand.) This tests if the current entity's hitpoints are less than 30% of it's maximum. If it's max hitpoints are 200 and it's current hitpoints are 50, this will be TRUE. If it's current hitpoints are 60, it will be false because 60 is 30% of 200, and thus is not less than 30%. If you wanted it to be true, you would need to use the "less than or equal to" operator (see below. that kind of thing shouldn't ever really be a problem or source of stress, but you should be aware of it anyway).
Here is yet another table, this particular one being the list of comarison operators you can use:
< |
is less than |
( |
is less than or equal to |
= |
is equal to |
) |
is greater than or equal to |
> |
is greater than |
! |
is not equal to |
Now we finally get to the point where conditions become a bit more powerful. You can put up to sixteen separate conditions, each of which would be called a clause, on a single line. Just separate each one with a comma, like so:
sl.hp < 30%, sl.canheal
This says "if my hitpoints are less than 30 percent of my max, and I have the ability to heal myself." For the entire line, or statement, to be true, each individual clause must be true.
By default, each clause in a condition has an AND relationship with the other. That is, "A, B" is true only if A and B are both true. By putting
<OR>
as the first thing in a conditional, everything in that conditional will have an OR relationship. "<OR> A, B" is true if either A or B are true.
If the conditions are the brains of a script, the actions are the brawn. They just get it done. Each individual action must be in parentheses, and there can be only one action on a line. However, you can have up to sixteen individual actions inside a block. When you have multiple actions inside a block, they will be carried out in the order they appear in the block, and each one will not happen until the one before it completes. If you happen to need two separate actions to occur at exactly the same time, too bad. But that functionality could be added with a little effort if it becomes desirable so say something to the programmers if you find yourself needing it.
Here is the syntax for an action:
(function [parameter 1 [parameter 2] . [parameter N]])
Function is one of the following:
Beat (subject) (gdb item) |
Hit target with beat attack, or move closer to target if beat is not possible |
Range (subject) (gdb item) |
Hit target with ranged attack, or move toward a spot where target is visible from |
Attack (subject) (gdb item) |
Attack target with anything, or move toward a spot where attack is possible |
Invoke (sequence) |
Invoke an APE sequence |
Gamevar (var)(value) |
Assigns a value to an APE gamevar |
Sound (name) |
Play a sound |
Anim (name) |
Play an animation |
Goto (subject) |
Teleport to a specific node |
Spawn (GDB classname) (target node) (script name) |
Used to call other objects into battle, e.g. Stone Sentinel boulders or Hive Queen drones. |
Effect (subject) "effect string" |
Used to add hit points, poison, fire, etc to the subject. |
Setinvuln (true,false) |
Become invulerable or not |
Become (script name) |
Switch to using a different AI script |
Burn (subject) |
Set subject on fire |
Nuts (subject) |
Make subject nuts |
Pois (subject) |
Poison subject |
Froz (subject) |
Freeze subject |
Wink (subject) |
Make subject winky |
Snoo (subject) |
Make subject snoozin |
Psys (subject) |
Make subject a psy slave |
Slow (subject) |
Slow subject |
Hast (subject) |
Haste subject |
Hurt (subject) |
Hurt subject |
Heal (subject) |
Heal subject |
Fixburn (subject) |
Put out fire on subject |
Fixnuts (subject) |
Make subject un-nuts |
Fixpois (subject) |
Un-poison subject |
Fixfroz (subject) |
Thaw subject |
Fixwink (subject) |
Unwinkify subject |
Fixsnoo (subject) |
Wake subject |
Fixpsys (subject) |
Make subject not a psy slave |
Fixany (subject) |
Undo any negative status modifiers if possible |
Hide |
Move to a place where the enemy cannot see |
Pass |
Skip this turn - do nothing |
Moverandom |
Just move to wherever |
It's hard to create unpredictable behaviour without using some kind of random selection. If you want to have a monster select one action from a list of possible actions, prefix each action to be selected from with a number representing it's probability of being selected, followed by a colon. For example:
5: (attack nelohp)
1: (heal sl)
This will cause the monster to usually attack nelohp, but 1 out of 5 times (randomly, not precisely) it will heal itself.
In order to ease interaction between APE and the Battle AI scripts, you can check and set the value of gamevars directly from the AI script. To set it, use the gamevar action, just as any other action, followed by the gamevar name, then a space, then the value to assign to it. You can also add and subtract values for counters. Prefix the number with '+' to add the value, or '-' to subtract.
(gamevar variable [+,-]value)
Now we get to the part of writing scripts that takes a little thought. You can put blocks of conditions inside conditions to streamline and simplify your scripts. The only thing that is different inside a nested block is that when the end of a nested block is reached, the script continues at the first condition after the block (with the exception of default block, which is explained below).
To help explain how to use nested blocks, I'm going to use a real example of something you might want to do with a script. Let's say you want a monster to:
Attack, or move toward, it's nearest enemy with the lowest hitpoints
Occasionally attack it's nearest enemy with the highest hitpoints
When it's health gets less than 1/3 of it's max, it will try to get in a place where it can't be attacked
After it gets to a place where it can't be attacked, it will heal itself
You can do this like this:
sl.hp < 33%, sl.isvuln
sl.hp < 33%, sl.canheal
default
Or like this:
sl.hp < 33%
sl.canheal
}
default
In this example, the only advantage you get by nesting the blocks is that it's a bit more comprehendible. In order to read the conditionals like a book, you have to learn to read the comma as the word "and".
The first one is "If my health is low and I can be attacked, get to a place where I can't be attacked. Or if my health is low and I can heal myself, then heal myself."
The second one is "If my health is low, get to a place where I can't be attacked, then heal myself if I can."
But beyond comprehension, if you start writing large complex scripts you'll find that it will be much easier to separate common conditions into a nested block rather than typing the same clause over and over for each block. Also, if you wanted to change the above example to 50% instead of 33%, it's much easier to change at the top of a block than it is on every 5th line.
As we learned in section 2.3, the default block will always execute when it's reached. This applies in nested blocks also. Check it out:
sl.naden > 1
sl.hp < 20
}
sl.nadal > 1, nalohp.hp < 50%
default
This says
If I have more than one enemy near me, attack it if my health over 100 or hide if my health is below 20
Or, if I have more than one ally near me and it's hitpoints are less than half, heal it.
Or, pass.
Now, let's say that sl.naden > 1 and sl.nadal > 1, nadal.hp < 50% are true, but that sl.hp is 50. Neither of the blocks under sl.naden > 1 will be true and the script will just go on to sl.nadal > 1, nadal.hp < 50%. But what if we don't want the script to do anything else if there are enemies around? We stick a default in there, and the script will stop at that point:
sl.naden > 1
sl.hp < 20
default
}
sl.nadal > 1, nadal.hp < 50%
default
|