Building Exospecies AI: Hierarchical Task Networks Overview
I got all wrapped up in trying to write the blog post for my survey of AI algorithms, how I classified them, etc. and even I got bored. So, I’m switching to the topic I actually wanted to write about: Hierarchical Task Networks (HTNs, sometimes called HTN Planners).
I used them in the first Exospecies AI prototype. I started there for a few reasons: It is a proven algorithm that had already been used in published games, it builds an AI that behaves like a human, and it appeared to be one of the fastest algorithms due to its hierarchical, depth-first, human-scripted approach.
Turns out there’s lots of information out there on HTNs but I couldn’t find enough to understand them to the level where I could build one from scratch. I also didn’t find any good examples of using one for strategy game AI. The next few posts will try to correct that. I may come back to writing up the survey once I get some distance from it.
What I did find was two buckets of stuff on the Internet:
- Bucket 1 was a bunch of PhD theses and college-level AI coursework about HTNs that was quite theoretical. The best I found were:
- Bucket 2 was some very practical how-to information. It was tough to find a concrete example here. The best I found was:
The confusing part was that, if you digest the Bucket 2 stuff, HTNs seem like a relatively simple tool (along the lines of state machines) for helping you to organize a giant script that just tells the system what to do. I guess I was expecting something more magical here. So, I figured something must have gotten lost in translation from the really theoretical proofs, logic and scientific sounding stuff in the “literature”. I dug deep into that and I think I understand it now. Let me take you on the journey…
HTN Overview
In broad strokes, an HTN solves a problem using a very human planning approach that centers on abstraction and alternatives.
Alternatives: Let’s use a travel example from the famous SHOP HTN Planner. Imagine you asked me how to get from downtown to the park. I might say, “Well, it is 2Km away but, if it is nice out, I would just walk it. However, if it is raining and you’ve got taxi fare, I’d take the taxi. Otherwise, if you have bus fare, just take a bus.” A abstract thing you accomplish like “travelling from one place to another” is known in HTNs as a “Task”. The 3 alternatives I gave for accomplishing it are known as “Methods” in HTNs. Note that each Method had a “Condition” that went with it: walk if it is nice out, take a taxi if you have the fare, etc.
Abstraction: Note that these 3 Methods are at a pretty high level of abstraction. You might further ask me how to accomplish the “TakeABus” Method. I’d say something like, “Well, wait for the #1 bus at the ‘Downtown’ bus stop, pay the driver $1, and ride it to the ‘Park’ stop.” These three tasks are known in HTNs as “Subtasks” of the “TakeABus” Method. In HTNs, Methods do what they do by decomposing into Subtasks. Now, this is a pretty complete answer for a human but maybe not if we were talking to a robot. In that case we might need to further decompose the task of “PayTheDriver$1” into something like “Reach into your pocket, grab a $1 bill, hand to driver”. Each of those could further be decomposed until we finally have something a robot could do. The final step of an HTN, the part that actually gets done, is called an “Operator”. How detailed you need to get before you are done (and have an Operator) depends on the kind of system you are building. A big part of designing an HTN model is getting your head around what the right abstractions are.
So, HTNs have these parts:
- State of the world:
You are downtown. Taxi fare is $2. Bus fare is $1. Distance from downtown to park is 2Km.
- Tasks: What can be done abstractly in the world (there may be several ways to do it!):
Travel from A to B. Walk from A to B. Pay a Driver X$. Wait for bus A at stop B.
- Operators: How to accomplish abstract Tasks when they can “just be done” without further explanation. They actually modify the State of the world. If you are human these might be Operators:
Pay the driver $1, Ride #1 bus to the 'Park' stop
. If you are a robot, these might be:Move stepper motor forward by 100 steps, turn on spotlight
- Methods: How to accomplish abstract Tasks when they involve several steps or more detailed plans. They have a Condition for when they can be used and a set of Subtasks for how to accomplish them:
METHOD: "Travel from A to B" CONDITION: the weather is bad and you have bus fare, SUBTASKS: Wait for the bus at the bus stop, pay the driver, and ride it to the stop
The algorithm itself is pretty simple. You start with a Task or Goal list like Go from downtown to the park
and an empty Plan list:
- Remove and start with the first goal in the Goal List
- See if there is an Operator that knows how to do it. If so, add the Operator to the end of the Plan List (since Operators can “just be done”) and continue at #1
- See if there are Methods that know how to do it. If so, add their Subtasks to the front of our Goal List and continue at #1 If there are multiple Methods, duplicate the current state of the world and run the algorithm with each, independently. Each that completes successfully will be a different solution
- Repeat until the Goal List is empty
- When done, what you have in the Plan List is the solution
An Example
Let’s see how the above travel example would work more formally. I’ve made up a little pseudo-computer-language just to help with the example. It is described as it is used:
State: State can be looked up using syntax like
State[ThingYouWantToLookup]
and is modified usingState.Add()
andState.Remove()
Location: downtown. Weather: good. Cash: $12. TaxiFare: $2. DistanceFrom(Downtown,Park): 2Km. TaxiLocation(Taxi1) : TaxiBase.
Operators: Anything that has a
?
in front of it is a variable that works just like you’d expectWalk(?From, ?To) : State.Remove(Location, ?From), State.Add(Location, ?To) Ride(?From, ?To) : State.Remove(Location, ?From), State.Add(Location, ?To) HailTaxi(?Location) : State.Remove(TaxiLocation(Taxi1), TaxiBase), State.Add(TaxiLocation, ?Location) SetCash(?Old, ?New) : State.Remove(Cash, ?Old), State.Add(Cash, ?New)
Methods: Conditions are just English expressions that should be self explanatory
TravelTo(?X) : CONDITION: State[Weather] is good and DistanceFrom(State[Location], ?X) < 3 SUBTASKS: Walk(State[Location], ?X) TravelTo(?X) : CONDITION: State[YourCash] >= State[TaxiFare] and State[TaxiLocation(Taxi1)] = Taxi Base SUBTASKS: HailTaxi(State[Location]), Ride(State[Location], ?X), PayDriver(State[TaxiFare]) PayDriver(?Amount) : CONDITION: if ?Amount <= State[Cash] SUBTASKS: SetCash(State[Cash], State[Cash] - ?Amount)
Note a few things about this HTN model:
- There are no “Abstract Tasks” listed anywhere. This might be confusing since they are supposedly part of the HTN model. Task names are just the union of all the Operator and Method names: Walk, Ride, HailTaxi, SetCash, TravelTo, PayDriver, etc.
- There are two Methods (i.e. ways) to travel to a place, but they have different times (i.e. Conditions) when they are appropriate. This is a key feature of an HTN model.
- Even though it seems like a “toy example”, HTN models tend to be very concise. The entire AI for my strategy game is almost finished at 350 lines and over half of those are comments.
- You may think it is totally bogus to make “Walk from A to B” as simple as changing some string called “Location” in the state. Think about it like this: HTNs allow you to build a simplified model of what you are working on and operate on it in an abstract way. The State is there to make sure your Tasks are doing things that are legal in your “real world”. You will probably ignore the State when it is done and only pay attention to the Operators, which you will map to functions in your “real” code, which have real effects on your real game.
Now let’s do the algorithm:
Start with the State, Operators and Methods above, a Goal List:
TravelTo(Park)
, and an empty Plan List
- Remove and Start with the first (and only) goal
TravelTo(Park)
in the Goal ListTravelTo(Park)
- There are no Operators called
TravelTo
- There are two Methods called
TravelTo
, thus we might have more than one solution. Let’s go through the first one first: The ConditionState[Weather] is good and DistanceFrom(State[Location], ?X) < 3
is true in the current state Therefore: Add its Subtasks to the Goal List after substituting variables:Walk(downtown, park)
- Remove and Start with the first goal
Walk(downtown, park)
from the new goal list:Walk(downtown, park)
- There is a
Walk(?From, ?To)
Operator Fill in variables and add it to the Plan List, which is now:Walk(downtown, park)
Allow it to modify the State in case we keep going. ChangesLocation: downtown
toLocation: park
- There are no more goals so the Plan is
Walk(downtown, park)
We found two TravelTo
methods that might be applicable above. So, we need to run the algorithm on the other option too if it is valid:
The Condition
State[YourCash] >= State[TaxiFare] and State[TaxiLocation(Taxi1)] = Taxi Base
is also true Therefore: Add its Subtasks to the Goal List after substituting variables:HailTaxi(downtown), Ride(downtown, park), PayDriver(2$)
- Remove and Start with the first goal
HailTaxi(downtown)
in the new Goal ListHailTaxi(downtown), Ride(downtown, park), PayDriver(2$)
- There is a
HailTaxi(?Location)
Operator Fill in variables and add it to the Plan List which is now:HailTaxi(downtown)
Allow it to modify the state in case we keep going: changesTaxiLocation(Taxi1) : Taxi Base.
toTaxiLocation(Taxi1) : downtown
- Grab the next goal:
Ride(downtown, park)
in the Goal ListRide(downtown, park), PayDriver(2$)
- There is a
Ride(?From, ?To)
Operator Fill in variables and add it to the Plan List which is now:HailTaxi(downtown), Ride(downtown, park)
Allow it to modify the state in case we keep going: changesLocation: downtown
toLocation: park
- Grab the next goal:
PayDriver(2$)
in the Goal ListPayDriver(2$)
- There is not an Operator for
PayDriver
- There is a single Method for
PayDriver(?Amount)
The condition?Amount <= State[Cash]
is true in the current state Therefore: Add its Subtasks to the Goal List after substituting variables:SetCash(12$, 12$ - $2)
- Grab the next goal:
SetCash(12$, 10$)
- There is a
SetCash(?Old, ?New)
Operator Fill in variables and add it to the Plan List which is now:HailTaxi(downtown), Ride(downtown, park), SetCash(12$, 10$)
Allow it to modify the State in case we keep going: changesCash: $12
toCash: $10
- No more goals: We are done.
So, we found two solutions. Depending on what you are doing, you could stop with the first solution and just use it, or generate all of them and somehow evaluate which is the best:
Walk(downtown, park)
HailTaxi(downtown), Ride(downtown, park), SetCash(12$, 10$)
You can see how the HTN algorithm starts with a high-level goal and breaks it down into more detailed plans until you have a set of Operators that your system can “just do”, ordered in the proper order. This all works because a human has carefully crafted the Methods and Operators to make sense for your problem. In practice, you don’t end up “thinking about the algorithm” so much. Really, you just end up thinking about the base operations your system can do and about how you’d teach someone to do the things you need done.
Final Thoughts
HTNs are a natural way to describe how to solve a planning problem where you start at a high level and refine your way down to a solution. They have some great features:
- Simple to understand and debug
- Methods and Operators are relatively simple to write and understand
- Plans like a human since Tasks are written by humans and not “learned”. This is a great for game AI since users want their opponent to act “naturally”
- Final plan contains an ordered set of Operators that are guaranteed to work (if a solution can be found)
And they have some downsides:
- They won’t produce surprising behavior, which can be good or bad. The system isn’t going to do something it wasn’t told to do.
- You have to know the solutions and encode them. I.e. the AI for Exospecies will only be as good a strategist as I am. Other approaches that explore the whole game tree or learn don’t have this issue (but they have other ones…)
Coming Next
In the future posts I’ll talk about how I actually implemented an HTN and different approaches you could take, as well as how I actually implemented the Exospecies AI as an HTN.