Most real-time strategy games are built around a two-layered AI system. From a low-level perspective, individual units are controlled by simple rule systems or state machines, which control their behavior, simple interactions with the environment, and so on. But how about tactics? How do armies get built, and how do they command troops on the battlefield? Most times, a higher-abstraction, complex rule system is used, which becomes an expert system that holds a significant body of knowledge regarding the art of war. In this section we will explore this tactical layer.
For our simple AI example, we will use classic rules in the form:
FACT > ACTION
Our fact list will need to store all those items we need to test for. A similar system was used in Microsoft's Age of Kings. Here are, for example, two rules that control the frequency of the computer attacks:
(defrule .(game-time > 1100) => .(attack-now) .(enable-timer 7 1100) .(disable-self) .(chat-local-to-self "first attack") ) (defrule .(timer-triggered 7) .(defend-soldier-count >= 12) => .(attack-now) .(disable-timer 7) .(enable-timer 7 1400) .(chat-local-to-self "other attacks") )
The first rule is triggered after 1,100 seconds have elapsed (approximately 20 minutes into the gameplay) and once every 23 minutes from that initial wave. Notice how timers are used to control the timing of these second-wave attacks, and how subsequent attacks are only started if 12 or more soldiers are available to defend our positions.
Also notice the fact that action language is highly symbolic. We need to implement each and every call in our programming language so AI programmers can work from a comfortable, high-abstraction level.
Imagine that we need to create a rule system for a Roman army battle that starts with the configuration shown in figure 8.8. I'll first provide some brief background information on the Roman army on the battlefield. In Roman battles, troops were deployed on a frontline with less-trained infantry in the first ranks and more experienced soldiers behind them. Some artillery and archer support were also available. The cavalry was kept to a minimum and was located at the wings to perform flanking moves around enemy troops. As the battle started, the first ranks of infantry (the lesser-trained soldiers) advanced while archers and ballistae rained projectiles on the enemy. As the distance between the first ranks of the infantry to the enemy reached a threshold, soldiers threw their spears or pila toward the enemy and kept advancing until the final clash actually took place. At that moment, the highly experienced legionnaires from the second wave started to advance toward the enemy as well. Then, after a short battle, the lesser-trained infantry would retreat through the gaps in the formations, and the experienced legionnaires would appear from the back, engaging in close combat with the enemy. At this point, the cavalry from the wings would charge until the battle was resolved.
Figure 8.8. Roman army disposition on the battlefield.
Now, let's build a rule system for such a battle. We will begin with some general rules that start the confrontation and follow the syntax from the Age of Kings AI system for simplicity:
(defrule .(true) => .(shoot-catapults) .(shoot-arrows) .(disable-self) ) (defrule .(true) => .(enable-timer 1 30) .(disable-self) )
These first two rules start the battle. The first rule states that both arrows and catapults must continue to shoot throughout the battle. The second rule is just used to delay the advance of the first wave by 30 seconds. We set a timer so it triggers after 30 seconds, signaling the start of the infantry advance. Notice how we use the fact true whenever we want to express a rule that is always available for execution. Also notice how we need a construct like disable-self to manually delete a rule, so it is not inspected in further execution loops:
(defrule .(timer-triggered 1) => .(attack-first-wave) .(disable-timer 1) .(disable-self) ) (defrule .(first-wave-distance < 100) .(have-pilum) => .(throw-pilum) ) (defrule .(first-wave-distance < 20) => .(engage-combat) ) (defrule .(first-wave-distance < 20) => .(attack-second-wave) .(disable-self) )
These four rules implement the advance, pilum-throwing, and engagement in combat of the first wave. The last two rules allow us to begin fighting (rule 3, which can be activated repeatedly) and to notify the second wave that we are requesting their help (rule four, which is disabled right after its first activation). Now, these rules should provide you with a practical example of how tactical AIs are designed.
But this example only shows part of the picture. After all, a choreographed battle has a defined sequential pattern, and rule systems are great when parallel behaviors are to be modeled. Now, imagine the power of such a system where rules apply not to the army as a whole, but to specific squadrons. Each squad would evaluate the expert system, and complex behaviors would emerge from apparent simplicity.
Here is when a choice must be made or a balance must be found. Will we implement our tactic as rules and hence as a reactive, highly emergent system, or will we choose to sequence actions as in the preceding Roman example? Sequential systems are all about memorizing the CPU's actions and learning how to defeat them. Emergent gameplay is somehow harder to master because it requires skill more than memory.