Three ways to get the AI to attack

As someone working on custom campaign AI scripts, I thought I could share with you the three non-DUC way to have the AI attack on a loop.

Method 1: Attack-Now
In the code snippet below, the AI will attack every 60 seconds:

(defrule
	(true)
=>
	(set-strategic-number sn-number-explore-groups 1)
	(set-strategic-number sn-relic-return-distance 0)
	(set-strategic-number sn-wall-targeting-mode 1)
	(disable-self)
)

;attack now loop
(defrule
	(up-timer-status 1 != timer-running)
=>
	(attack-now)
	(enable-timer 1 60)
)

Of the three attack methods, this one uses the fewest lines. However, only a fraction of the AI’s soldiers will attack, even if sn-percent-attack-soldiers is set to 100. If you’re trying to get warships to attack, though, you should use the attack-now method, because the other two methods (attack-groups and town size attack) only apply to land units.

Method 2: Attack-Groups
In the code snippet below, the AI will not attack for 40 seconds and then attack for 20 seconds.

(defrule
	(true)
=>
	(set-strategic-number sn-number-explore-groups 1)
	(set-strategic-number sn-relic-return-distance 0)
	(set-strategic-number sn-wall-targeting-mode 1)
	(disable-self)
)

;attack groups
(defrule	
	(true)
=>
	(set-strategic-number sn-number-attack-groups 0)
	(enable-timer 1 0)
	(disable-self)
)
(defrule
	(timer-triggered 1)
=>
	(disable-timer 1)
	(set-strategic-number sn-number-attack-groups 0)
	(chat-to-all "sn-number-attack-groups 0")
	(enable-timer 2 40)
)

(defrule
	(timer-triggered 2)
=>
	(set-strategic-number sn-number-attack-groups 200)
	(chat-to-all "sn-number-attack-groups 200")
	(disable-timer 2)	
	(enable-timer 1 20)
)

Using this method, the AI will attack when sn-number-attack-groups is set to a non-zero value. Unlike with attack-now, attack-groups sends all its the soldiers to attack. I also found that soldiers attacking with attack-groups get less stuck attacking walls than attack-now. This is why, of these three methods, I prefer using attack-groups for land units.

Method 3:Town Size Attack
The AI is programmed to defend their town. The AI’s definition of what counts as their town is determined the strategic number sn-maximum-town-size. If any enemy units are inside the area defined by sn-maximum-town-size, the AI will attack said enemy units. By changing the size of sn-maximum-town-size to cover the whole map, we can get the AI to attack enemy bases far away. This attack method is known as town size attack.

In the code snippet below, the AI increases its town size every 5 seconds. When it finds an enemy in its town, the AI will attack and reset its town size to 24.

(defrule
	(true)
=>
	(set-strategic-number sn-relic-return-distance 0)
	(set-strategic-number sn-maximum-town-size 24)
	(set-strategic-number sn-number-explore-groups 1)
	(set-strategic-number sn-wall-targeting-mode 1)
	(set-goal 1 0)
	(enable-timer 2 5)
	(disable-self)
)

(defrule
	(timer-triggered 2)
	(not (enemy-buildings-in-town))
	(strategic-number sn-maximum-town-size == 124)
=>
	(set-strategic-number sn-maximum-town-size 144)
	(disable-timer 2)
	(enable-timer 2 5)
)

(defrule
	(timer-triggered 2)
	(not (enemy-buildings-in-town))
	(strategic-number sn-maximum-town-size == 104)
=>
	(set-strategic-number sn-maximum-town-size 124)
	(disable-timer 2)
	(enable-timer 2 5)
)

(defrule
	(timer-triggered 2)
	(not (enemy-buildings-in-town))
	(strategic-number sn-maximum-town-size == 84)
=>
	(set-strategic-number sn-maximum-town-size 104)
	(disable-timer 2)
	(enable-timer 2 5)
)

(defrule
	(timer-triggered 2)
	(not (enemy-buildings-in-town))
	(strategic-number sn-maximum-town-size == 64)
=>
	(set-strategic-number sn-maximum-town-size 84)
	(disable-timer 2)
	(enable-timer 2 5)
)

(defrule
	(timer-triggered 2)
	(not (enemy-buildings-in-town))
	(strategic-number sn-maximum-town-size == 44)
=>
	(set-strategic-number sn-maximum-town-size 64)
	(disable-timer 2)
	(enable-timer 2 5)
)

(defrule
	(timer-triggered 2)
	(not (enemy-buildings-in-town))
	(strategic-number sn-maximum-town-size == 24)
=>
	(set-strategic-number sn-maximum-town-size 44)
	(disable-timer 2)
	(enable-timer 2 5)
)

(defrule
	(goal 1 0)
	(enemy-buildings-in-town)
=>
	(enable-timer 1 14)
	(disable-timer 2)
	(set-goal 1 1)
)

(defrule
	(goal 1 1)
	(timer-triggered 1)
=>
	(disable-timer 1)
	(enable-timer 2 5)
	(set-goal 1 0)
	(set-strategic-number sn-maximum-town-size 24)
)

Town size attack is great if you want the AI to prioritize attacking the closest buildings. However, as you can see, town size attack requires the most lines among the three methods.

For all three methods, the AI will only attack explored enemy objects. Therefore, make sure sn-number-explore-groups is set to at least 1.

Bonus Section: Randomizing Attack Intervals
Say you want to make the attack intervals less predictable. Perhaps you want the AI attack every 20 to 40 seconds, instead of exactly 40 seconds every time. To do this, you will need to use goals, UserPatch commands and be familiar with mathOp, compareOp and typeOp operators.

In the code snippet below, the AI, using the attack-groups method, will wait 20 to 40 seconds, and then attack for 20 seconds:

(defconst gl-attack-time-range 6)
(defconst gl-attack-interval 7)
(defconst gl-game-time 8)

(defrule
	(true)
=>
	(set-strategic-number sn-number-explore-groups 1)
	(set-strategic-number sn-relic-return-distance 0)
	(set-strategic-number sn-wall-targeting-mode 1)
	(set-goal gl-attack-interval 20)
	(disable-self)
)

;attack groups
(defrule	
	(true)
=>
	(set-strategic-number sn-number-attack-groups 0)
	(enable-timer 1 0)
	(disable-self)
)
(defrule
	(timer-triggered 1)
=>
	(disable-timer 1)
	(set-strategic-number sn-number-attack-groups 0)
	(up-get-fact game-time 0 gl-game-time)
	(up-chat-data-to-self "sn-number-attack-groups 0 at %d seconds" g: gl-game-time)
	(generate-random-number 20)
	(up-get-fact random-number 0 gl-attack-time-range)
	(up-modify-goal gl-attack-interval g:+ gl-attack-time-range)
	(up-chat-data-to-self "gl-attack interval is %d" g: gl-attack-interval)
	(up-set-timer c: 2 g: gl-attack-interval)
	(set-goal gl-attack-interval 20)
)

(defrule
	(timer-triggered 2)
=>
	(set-strategic-number sn-number-attack-groups 200)
	(up-get-fact game-time 0 gl-game-time)
	(up-chat-data-to-self "Attack at %d seconds" g: gl-game-time)	
	(disable-timer 2)	
	(enable-timer 1 20)
)

In the first block of code after the defconst stuff, (set-goal gl-attack-interval 20) initially sets goal gl-attack-interval to 20, which is the lower bound of the attack interval (goals can be thought of as variables). When timer 1 is triggered, the AI goes into non-attack mode and determines how long timer 2 will be triggered. First, a random number between 1 and 20 is generated with the command (generate-random-number 20). (up-get-fact random-number 0 gl-attack-time-range) then places the number generated by (generate-random-number 20) into the goal gl-attack-time-range. (up-modify-goal gl-attack-interval g:+ gl-attack-time-range) adds gl-attack-time-range to gl-attack-interval; the goal value of gl-attack-interval should now be a number between 20 and 40. (up-set-timer c: 2 g: gl-attack-interval) then sets timer 2 to trigger at gl-attack-interval’s goal value, in seconds. (set-goal gl-attack-interval 20) resets gl-attack-interval back to 20, so that it can be resued when timer 1 is re-triggered. When timer 2 is triggered, the AI will attack.

And one more thing: you’ll see in that code that I use up-chat-data-to-self commands for debugging purposes. This up-chat-data-to-self command, however, will only work if you play as an AI in a skirmish match. If you start the match as a human player or use the scenario editor to test your scenario, the AI will not say anything even if you use CTRL-Shift-Fnumber to take control of them. The only up-chat commands that will work if you don’t play as an AI are up-chat-data-to-all or up-chat-data-to-player 1. Obviously you don’t want the AI giving away its strategy to you, so those two commands aren’t exactly the what we want in a “serious” AI.

2 Likes

I’m capable of making really good maps, and doing a great many triggers, but AI is my big thing creating issues for custom campaign making. Any hints on how to start really? There are lots of resources and stuff, I need relatively capable AIs, preferably without too much work. As of right now, I’ve hijacked one of the AIs from the final level of the Sicilian campaign, but it’s very long and I don’t understand all of it.

Here’s an AI script that keeps all units still with strategic numbers. This code snippet was taken form ADVANCED Immobile Units AI v1.1:

(defrule	;this rule has FACTS and ACTIONS to keep VILLAGERS still
	(true)
=>
	(set-strategic-number sn-maximum-food-drop-distance 0)
	(set-strategic-number sn-maximum-wood-drop-distance 0)
	(set-strategic-number sn-maximum-gold-drop-distance 0)
	(set-strategic-number sn-maximum-hunt-drop-distance 0)
	(set-strategic-number sn-maximum-stone-drop-distance 0)
	(set-strategic-number sn-food-gatherer-percentage 0)
	(set-strategic-number sn-wood-gatherer-percentage 0)
	(set-strategic-number sn-gold-gatherer-percentage 0)
	(set-strategic-number sn-stone-gatherer-percentage 0)
	(set-strategic-number sn-cap-civilian-explorers 0)
	(set-strategic-number sn-percent-civilian-explorers 0) 
	(disable-self)
)
(defrule	;this rule has FACTS and ACTIONS to keep TROOPS still
	(true)
=>
	(set-strategic-number sn-percent-enemy-sighted-response 100)
	(set-strategic-number sn-hits-before-alliance-change 25)
	(set-strategic-number sn-number-explore-groups 0)
	(set-strategic-number sn-percent-attack-soldiers 0)
	(set-strategic-number sn-task-ungrouped-soldiers 0)
	(set-strategic-number sn-number-attack-groups 0)
	(set-strategic-number sn-enemy-sighted-response-distance 10)
	(set-strategic-number sn-total-number-explorers 0)
	(set-strategic-number sn-relic-return-distance 0)
	(disable-self)
)

Here’s a block of code that tells the AI to train 10 knights, 10 villagers and 2 rams:

(defrule
	(unit-type-count-total knight-line < 10)
	(can-train knight-line)
=>
	(train knight-line)
)
(defrule
	(unit-type-count-total villager < 10)
	(can-train villager)
=>
	(train villager)
)
(defrule
	(unit-type-count-total battering-ram-line < 2)
	(can-train battering-ram-line)
=>
	(train battering-ram-line)
)

For the above code to work, make sure you already have Stables, Siege Workshops and adequate headroom. If you want unit counts to vary by difficulty level, check out this post I made.

If your scenario already has villagers and you don’t want the AI to get housed, add this snippet:

(defrule
	(can-build house)
	(housing-headroom < 4) 
	(population-headroom > 3)
=>
	(build house)
)

As for other resources, the AI Scripting Encyclopedia is a great reference. You can also ask the AI Scripting Discord if you have any questions.

I just took a look at an AI script ffrom that scenario and yeah, that script is pretty complex, with a lot of constants, UP commands and references to other AI files. You definitely shouldn’t use those scripts as a reference if you’re a beginner.

A much simpler AI to look at would be the Greek AI in the Lepanto scenario (Conquerors-scn6 player 3). All that AI does are your basic train units and build buildings stuff. The Catalaunian Fields AIs are much longer, but they should be good reference if you want a make a relatively simple AI for a deathmatch-like scenario.