Age of Empires II (2013)

Age of Empires II (2013)

40 ratings
AI Scripting
By redmechanic
A guide to scripting your own AI for AOEII HD.
 
Rate  
Favorite
Favorited
Unfavorite
Getting Started
First, you will need to locate your AI folder. This is where we will put our files so the game can detect them. Mine is located at:
C:\Program Files (x86)\Steam\SteamApps\common\Age2HD\resources\_common\ai

A basic AI is composed of two elements, a AINAME.ai file and a AINAME.per file. The .ai file tells the game that this ai is run with the personality file of the same name. Example:

stupid.ai stupid.per

The .ai file remains blank, and is simply points to the personality file. Here, I've decided to call the AI "stupid".

Inside every .per file, rules and constants are defined, both of which are covered in the next section.
Rules and Constants
Constants

In AIs, usually we like to use names rather than numbers. This does not help the computer, but us, so we can understand what it does.

If we want to store a number as a name, we can use defconst:

(defconst my-number 5)

This means that whenever we type my-number, the game will interpret it as 5.

Rules

To make the AI do things, we need to provide it with rules. The format of a rule is simple:

(defrule (CONDITION 1) (CONDITION 2) => (ACTION 1) (ACTION 2) )

The rule will perform each action after the other when all of the conditions are true. Every action must be contained within a rule. Here is an example rule to produce villagers constantly:

(defrule (can-train villager) => (train villager) )

can-train checks whether we can train the specified unit (we have the building, the resources...). can-build is the same, but for buildings.

If we want a rule to only run once, we can append (disable-self) to the actions.
Boolean Operators
You may have noticed that in rules, we can only do AND, the rule will only perform the actions when every condition is true.

  • OR takes two conditions. A condition can be another or statement.
  • AND also takes two conditions, and it's only real purpose is to be included in ORs.
  • NOT only takes one condition, and is true if the condition is false.

Examples:

(defrule (or (condition 1) (condition 2) ) => (action) ) (defrule (or (condition 1) (and (condition 2) (condition 3) ) ) => (action) ) (defrule (not (condition) ) => (action) )

Note that all the indentation is completely optional. It has only been included so that each rule is easier to read.
Goals
If we want to do anything substantial with our AIs, then we are going to have to remember certain numbers. defconst cannot do that for us, since we cannot change it. This is where the goal system comes in.

The format of a goal action is as follows:

(set-goal GOAL_NUMBER VALUE)

We use an action to set a goal so we can use it in a condition. Example:

(defrule (true) => (set-goal 1 0) )

This sets goal 1 to have a value of 0. We can check the value of goal 1 using a condition:

(defrule (goal 1 1) => (chat-to-all "Goal 1 is 1!") ) (defrule (goal 1 0) => (chat-to-all "Goal 1 is 0!") )

When reading through an AI, referencing goal by number can be hard or impossible to understand. This is why we throw constants into the mix (note: anything after a semicolon is a comment):

(defconst gl-train-militia 1) ;this is the goal number (defrule ;set the goal's initial value for the sake of clarity (true) => (set-goal gl-train-militia 0) (disable-self) ) (defrule (goal gl-train-militia 1) (can-train militiaman-line) => (train militiaman-line) )
Timers
If we want to perform a set of actions after a certain amount of time, we can use timers. To set a timer, we can use the enable-timer action:

(enable-timer TIMER_NUMBER SECONDS)

To unset a timer, we can use the disable-timer action:

(disable-timer TIMER_NUMBER)

This will start the timer of TIMER_NUMBER to count down to SECONDS in seconds. We can check if the timer has finished using the condition timer-triggered:

(timer-triggered TIMER_NUMBER)

If we want the AI to shout exactly 30 seconds in, we can do it pretty easily combining all the above:

(defconst timer-shout 1) ;this is the timer number (defrule ;start the timer initially (true) => (enable-timer timer-shout 30) (disable-self) ) (defrule (timer-triggered timer-shout) => (chat-to-all "YAHH") (disable-timer timer-shout) )

If we wanted our AI to shout every 30 seconds on loop, we just follow our disable-timer with enable-timer directly afterwards.
load-if Statement
A tool given to us is the load-if statement. It is formatted like so:

#load-if-defined CONSTANT_NAME RULES #end-if

This will check if a constant exists. If it does, then activate all the rules enclosed between the load-if-defined and the end-if (the body of the if). This is very useful for check what civ the AI is:

#load-if-defined AZTEC-CIV (defrule (true) => (chat-to-all "I am the Aztecs.") (disable-self) ) #end-if

AZTEC-CIV is a constant defined for you if you are the Aztecs. Otherwise, it will not exist. This of course means we can check if constants we've defined exist:

#load-if-defined blargle ;nothing in here will load because blargle is not defined. #end-if

In a similar way, we can check the opposite with load-if-not-defined. We could check if we were not the Aztecs.
Strategic Numbers
Strategic number tell the AI how to behave at the most basic level. They are mostly used to task villagers to different resources:

(defrule (current-age == dark-age) => (set-strategic-number sn-food-gatherer-percentage 85) (set-strategic-number sn-wood-gatherer-percentage 15) (set-strategic-number sn-gold-gatherer-percentage 0) (set-strategic-number sn-stone-gatherer-percentage 0) (disable-self) )

In english, this rule is: "If the current age is the dark age, then task 85% of villagers to food, and 15% to wood. Do this only once."
Escrow
Sometimes we will want to make the AI save up for something. We can save a percentage of all income by setting an escrow percentage.

Let's suppose that we want to save 90% of all our food. This means that if a villager drops off 10 food, 9 of it cannot be used until we explicitally release it, and 1 will be used as normal.

To set the escrow percentage, we can use the following action:

(set-escrow-percentage food 90)

This will do what I described above. And to release it:

(release-escrow food)

This will take everything we've saved up and throw it in the common resource pool.
Combining everything gives us a nice way to ensure that we train/build/research certain things:

(defrule (game-time >= 900) (building-type-count-total barrracks == 0) => (set-escrow-percentage wood 100) ) (defrule (can-build-with-escrow barracks) => (release-escrow wood) (set-escrow-percentage wood 0) (build barracks) )

This allows us to save for a barracks if we don't have one by 15 minutes, if we for some reason wanted to do that. can-build-with-escrow, can-train-with-escrow, or anything similar, adds the escrowed amounts to the non-escrowed amounts to check.
Buildings
To build any kind of building we can use the condition:

(can-build BUILDING)

and the action:

(build BUILDING)

For example, to build a castle we could do:

(defrule (can-build castle) => (build castle) )

It is also important to note that we can build a building forwards using build-forward instead of just build.

We can check if we how many buildings of each type we have using this condition:

(building-type-count-total BUILDING > NUMBER)

Note that the difference between building-type-count and building-type-count-total is that the latter includes buildings queued for construction. (this is the same story for unit counts too)

Also note that instead of the > we can use any comparator. Valid comparators:
  • > greater than
  • < less than
  • >= greater than or equal to
  • <= less than of equal to
  • == equal to
Units + Researching
Same as buildings except with (can-train UNIT) and (train UNIT), (can-research RESEARCH) and (research RESEARCH). Example:

(defrule (can-research ri-ballistics) => (research ri-ballistics) )

This example will require the university building, of course.
Houses
I feel this deserves it's own section. To make the AI build houses we can build the house building as normal, but we might also want to check for a few things:

(housing-headroom < 5)

This checks if we are less than 5 units away from being housed. We want this so that the AI doesn't just build houses on loop. Something else that might be a good idea:

(population-headroom != 0)

This checks if we have enough houses to support the max population. Tying this all together gives us something like this:

(defrule (housing-headroom < 5) (population-headroom != 0) (can-build house) => (build house) )
Dropoff points
Camps are built like any other building, but like houses, we only want to build them when certain conditions are met. This condition is useful:

(dropsite-min-distance RESOURCE > NUMBER_TILES)

This checks if the closest RESOURCE is more than NUMBER_TILES away from a dropoff point. Another useful condition is

(resource-found RESOURCE)

This checks if the AI has scouted a particular resource. We don't want to build dropoff points for a resource that doesn't exist!

Here is an example for a lumber camp that should make things clear:

(defrule (dropsite-min-distance wood > 3) (resource-found wood) (can-build lumber-camp) => (build lumber-camp) )
Basic Example
This will try to show how all these components work with each other with an AI that will try an archer rush, and will stay in an endless feudal war. (I'm not very good at the game so the build will be off, probably.).

Files:
basic.ai basic.per

basic.ai:

basic.per:
;ATTACKING ======================================== (defrule (military-population >= 10) ;when we have 10 military, attack! => (attack-now) ) ;STRATEGIC NUMBERS ======================================== (defrule ;initial numbers (true) => (set-strategic-number sn-percent-civilian-explorers 0) ;don't scout with villagers (set-strategic-number sn-total-number-explorers 1) ;scout with our scout! (set-strategic-number sn-number-explore-groups 1) (set-strategic-number sn-enable-boar-hunting 1) ;take the boar! (disable-self) ) (defrule (current-age == dark-age) => (set-strategic-number sn-food-gatherer-percentage 80) (set-strategic-number sn-wood-gatherer-percentage 20) (set-strategic-number sn-gold-gatherer-percentage 0) (set-strategic-number sn-stone-gatherer-percentage 0) (disable-self) ) (defrule (current-age == feudal-age) => (set-strategic-number sn-food-gatherer-percentage 50) (set-strategic-number sn-wood-gatherer-percentage 35) (set-strategic-number sn-gold-gatherer-percentage 15) (set-strategic-number sn-stone-gatherer-percentage 0) (disable-self) ) ;RESEARCH ======================================== ;Notice that this section is above units, this is so we can research stuff before we queue stuff. (defrule (can-research ri-loom) => (research ri-loom) ) (defrule (can-research ri-fletching) => (research ri-fletching) ) (defrule (civilian-population >= 21) (can-research feudal-age) => (research feudal-age) ) ;UNITS ======================================== (defrule (civilian-population < 130) ;if we have less than 130 villagers, train a villager. (can-train villager) => (train villager) ) (defrule (can-train archer-line) => (train archer-line) ) ;BUILDINGS ======================================== (defrule ;we need houses! (housing-headroom < 5) ;if we are nearly housed (population-headroom != 0) ;if we are not population blocked (can-build house) => (build house) ) (defrule ;lumber camps (resource-found wood) ;if we have scouted wood (dropsite-min-distance wood > 3) ;if the closest tree is more than 3 tiles away from a dropoff point (can-build lumber-camp) => (build lumber-camp) ) (defrule ;mills (resource-found food) (dropsite-min-distance food > 3) (can-build mill) => (build mill) ) (defrule ;farms (building-type-count-total farm < 6) (can-build farm) => (build farm) ) (defrule ;mining camps (current-age >= feudal-age) (resource-found gold) ;if we have scouted gold (dropsite-min-distance gold > 3) ;if the closest gold pile is more than 3 tiles away from a dropoff point (can-build mining-camp) => (build mining-camp) ) (defrule (building-type-count-total blacksmith == 0) ;if no blacksmith, build a blacksmith. (can-build blacksmith) => (build blacksmith) ) (defrule (current-age >= feudal-age) ; >= is greater than or equal to, so do this in the castle age as well. (building-type-count-total barracks == 0) (can-build barracks) => (build barracks) ) (defrule (current-age >= feudal-age) (building-type-count-total archery-range < 2) ;construct two archery ranges. (can-build archery-range) => (build archery-range) )

This AI is very bad and requires a lot of tuning, but as an example, it will suffice.
Finally
Have a look through your CPSB.doc. Mine is located at:

C:\Program Files (x86)\Steam\SteamApps\common\Age2HD\Docs\All\AoK CP Strategy Builder.doc

Hopefully it will all make sense, and will give you most things you can do. Pay careful attention to the strategic numbers, they will control how the AIs micro will work.

Do also take a look at Saladin on the workshop (not made by me), it is an excellent example.
Miscellaneous Tips
  • You may want to assign more builders to build a type of building. This can be done with up-assign-builders
    (up-assign-builders c: castle c: 4)
    Remember when doing this to also raise the builders cap:
    (set-strategic-number sn-cap-civilian-builders 100)

  • You can load personality files. This does not go in a rule, and can't.
    (load "stupid\darkage")
    (this is assuming that there is a file called darkage.per in a folder called stupid)
    Further details can be obtained from the CPSB.

  • The camp-max-distance is always a pain to work with, if the AI gains map control, it will not take the resources because they are too far away, even if they have run out of the resources at home leading to a horrible shanty town being built. An easy fix that I like to employ is modifying the sn-camp-max-distance whenever I build a camp.
    (defrule (dropsite-min-distance wood > 3) (resource-found wood) (can-build lumber-camp) => (build lumber-camp) (up-modify-sn sn-camp-max-distance c:+ 3) )

  • Important Strategic Numbers!
    (defrule (true) => (set-strategic-number sn-initial-exploration-required 0) (set-strategic-number sn-defer-dropsite-update 1) (disable-self) )
    I consider these two to be very important if sn-initial-exploration-required is not set to 0, then the AI cannot build anything until a certain percentage of the map is explored. This leads to the AI sometimes not building houses at the start of the game. If sn-defer-dropsite-update is set to 0, then if the ai wants to build a mining camp in enemy territory, it will send a ton of villagers to their deaths. If 1, then only the one sent to build it may die.
< >
11 Comments
Skaldr Jan 2 @ 8:50am 
Hi I'm back with a little question:
How do you define a unit line such as "archer-line" or "militaman-line"?
redmechanic  [author] Dec 27, 2019 @ 11:01pm 
@eXtreme Angel

Commands and various constants can be found in the CPSB (Age2HD\Docs\All\AoK CP Strategy Builder.doc).

I had a look at max-trade-pop, and that seems to just be a constant that defines how many trade carts the AI can make, depending on difficulty.

Would this not do what you specified?

(defconst
(unit-type-count-total trade-cart < 2)
(can-train trade-cart)
=>
(train trade-cart)
)

If a trade cart is killed, the number of trade carts drops below 2, and the AI queues another one. A problematic part is setting the player to trade with. You can set the preferred distance with "sn-preferred-trade-distance", but I don't think you can specify a player in HD.
eXtreme Angel Dec 27, 2019 @ 7:19pm 
I still can't understand where I can found these commands. Okay, I understood that "civilian-population" and "military-population" we can represent as "units type"-"population", but where exactly I can search what types of "units type" variables I can use?

For example, I want to make AI to train trading carts when he has only 1 Marketplace and give him a rule to train maximum of 2 carts, rebuild carts if any of them are destroyed and set marketplace targets to which player he should trade with. I don't understand how to make this.

I opened standart AI (HD Version) file and tried to understand those cariables and what he uses but whenever I use "trade-carts" (it also contains "trade-cogs") and check for it's population with "max-trade-pop", the game says that it doesn't know those variables when the original file seemed to just have numbers with "defconst" and that's all.

So my main question is - where can I check all commands, all variables so I could use them to write my AI.
Skaldr Dec 10, 2019 @ 3:21pm 
@redmechanic Thanks a lot for your quick answer! You guide is very helpful!
redmechanic  [author] Dec 10, 2019 @ 2:40pm 
@Skaldr I would advise copying the AI to a different name and then modifying it. It's file can be found at resources/_common\drs/gamedata_x2/Promisory.per2
Skaldr Dec 10, 2019 @ 2:38pm 
Hi! Is it possible to just modify the existing AI?
Jez Oct 14, 2019 @ 12:32pm 
Great explanation. Thanks for writing it!
Solar Prophet Aug 7, 2019 @ 12:09pm 
my brain hurts, great effort, i mean none of this is stuff a eleven yr old needs, but great effort
Valkyrie May 25, 2018 @ 9:31am 
can i like give you a high five?
Trota12 Dec 31, 2017 @ 3:30pm 
+10 prro :ftlslug: