Age of Empires II (2013) Guide

AI Scripting for Age of Empires II (2013)

AI Scripting

Overview

A guide to scripting your own AI for AOEII HD.

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)SteamSteamAppscommonAge2HDresources_commonai

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)SteamSteamAppscommonAge2HDDocsAllAoK 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 “stupiddarkage”)

    (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.

SteamSolo.com