AI actions

You can find all the information from this page in this PDF.

There's also the old notes about AI in this other PDF. They're still mostly valid, and might help you understand how the AI works with a practical/visual example.

Overview of the AI system

Intelligent entities in Stonehearth run a hierarchical task network to perform activities by making, comparing, and executing plans made up of actions. Many potential plans are typically being considered at a time, most of which are not ready to execute, or are suboptimal, and the AI constantly selects the one best plan to execute at any given time.

Let's unpack that formal definition and map it to the terms used in the implementation:

Below is a screenshot of how a typical AI tree looks in the AI inspector. Activities (=frames) are presented in angle brackets. The rest of the nodes are actions. Leaf nodes are Leaf Actions. Inner nodes are compound, task group, or task actions. The root is always the stonehearth:top activity.

ai_inspector

Writing actions

Actions are Lua scripts registered in the mod's manifest and "injected" into a character's AI. This section explores how to declare actions. The next section explores how to inject actions into into a character's AI.

Leaf action example

(hypothetical) draw_animal_action.lua

  local DrawAnimalAction = class()

  DrawAnimalAction.name = 'draw an animal' -- required
  DrawAnimalAction.does = 'stonehearth:paint' -- required
  DrawAnimalAction.args = { -- required
     subject = Entity,
     effect_name = {
        type = 'string',
        default = 'draw_in_notebook',
     }
  }
  DrawAnimalAction.think_output = {}
  DrawAnimalAction.priority = {0.2, 0.4} -- required
  DrawAnimalAction.sunk_cost_boost = 0.2
  DrawAnimalAction.weight = 1
  DrawAnimalAction.scheduler_priority = 4
  DrawAnimalAction.status_text_key = 'stonehearth:ai.actions.status_text.drawing'

  function DrawAnimalAction:start_thinking(ai, entity, args)
     if is_animal(args.subject) then
        ai:set_utility(get_photogenicity(args.subject))
        self._listener = radiant.events.listen(args.subject, 'stonehearth:ready_to_pose',
                                                function()
                                                   ai:set_think_output({})
                                                end)
     else
        ai:reject('not an animal!')
     end
  end

  function DrawAnimalAction:stop_thinking(ai, entity, args)
     if self._listener then
        self._listener:destroy()
        self._listener = nil
     end
  end

  function DrawAnimalAction:start(ai, entity, args)
     if not is_ready_to_pose(args.subject)
        ai:abort('animal stopped being ready to pose by the time the action started')
     end
  end

  function DrawAnimalAction:run(ai, entity, args)
     if not is_ready_to_pose(args.subject)
        ai:abort('animal stopped being ready to pose by the time the action ran')
     end

     freeze_animal(args.subject)
     ai:execute('stonehearth:run_effect', {effect = args.effect_name})
     radiant.entities.add_thought(entity, 'stonehearth:thoughts:drew_an_animal')
  end

  function DrawAnimalAction:stop(ai, entity, args)
     if is_frozen(args.subject) then
        unfreeze_animal(args.subject)
     end
  end

  return DrawAnimalAction

Let's take a look at each part of that action:

Here's a simplified diagram of the state machine of an AI action: ai_state_machine

Compound action example

(hypothetical) find_and_paint_subject_action.lua

  local FindAndPaintSubjectAction = class()

  FindAndPaintSubjectAction.name = 'find and paint subject'
  FindAndPaintSubjectAction.does = 'stonehearth:free_time'
  FindAndPaintSubjectAction.args = {}
  FindAndPaintSubjectAction.priority = {0.3, 0.7}

  function FindAndPaintSubjectAction:start_thinking(ai, entity, args)
     local traits = entity:get_component('stonehearth:traits')
     if traits and traits:has_trait('stonehearth:traits:artist') then
        ai:set_utility(0.5)
     else
        ai:set_utility(0.0)
     end
     ai:set_think_output()
  end

  function FindAndPaintSubjectAction:compose_utility(entity, self_utility, child_utilities, current_activity)
     return self_utility + child_utilities:get('stonehearth:follow_path') * 0.1 + child_utilities:get('stonehearth:paint') * 0.4
  end

  local ai = stonehearth.ai
  return ai:create_compound_action(FindAndPaintSubjectAction)
              :execute('stonehearth:find_willing_painting_subject')
              :execute('stonehearth:find_path_to_entity', { destination = ai.PREV.subject })
              :execute('stonehearth:follow_path', { path = ai.PREV.path })
              :execute('stonehearth:paint', { subject = ai.BACK(3).subject })

  return FindAndPaintSubjectAction

Let's take a look at each part of that action:

Task group example

solo_basic_needs_task_group.lua

  local SoloBasicNeedsTaskGroup = class()

  SoloBasicNeedsTaskGroup.name = 'solo basic needs'
  SoloBasicNeedsTaskGroup.does = 'stonehearth:top'
  SoloBasicNeedsTaskGroup.priority = {0, 0.3}

  return stonehearth.ai:create_task_group(SoloBasicNeedsTaskGroup)
                          :declare_task('stonehearth:sleep', 0.6)
                          :declare_multiple_tasks('stonehearth:eat', {0.4, 1.0})
                          :declare_permanent_task('stonehearth:rest_when_injured', {}, 0.2)

Let's take a look at each part of that action:

AI contexts

All Action methods get passed an "AI Context" as described above, which is used to communicate with the AI system about that action. Here are the methods available on AI Contexts:

Utility

When deciding which action to select to perform a given activity or task group, the actions are judged by comparing their utility, a value between 0 and 1, which represents how good that action is at performing its activity in the current situation.

Actions can specify their utility as a number if the action always has the same utility, or as a range like {0.2, 0.6} if the action will at some point change its utility depending on state or input. In the latter case, the minimum is assumed by default, and the action can adjust it while thinking or running by calling ai:set_utility() with a number from 0 to 1 (which will be rescaled to the declared range), or for compound and task group actions by returning the new utility value from their compose_utility() method as explained above.

Note that an action's utility value is only meaningful in the context of its activity. The utility values of actions that perform different activities cannot be compared directly.

In addition to the utility that the action reports, we also have a "sunk cost boost" feature, which increases the utility of an action after it has run for some time. By default, it takes the game time equivalent of 500ms of real time before it kicks in, and by default it adds 0.05 utility, though the latter number can be overridden by the action's metadata.

Injecting actions

Even after an action is written and registered in the manifest in the "aliases" section, it is of no use if no character in the game "knows" that action. For a character to "know" an action, it has to be injected into their AI using an AI pack. AI packs are JSON files, usually under stonehearth/ai/packs, which have "type" : "ai_pack" and contain lists of:

Sometimes the AI Packs are inlined in other JSON files instead of being referenced.

Entities inject AI packs in several different ways:

So after you create an action, you should add it to an AI pack and make sure the characters who should know this action have that AI pack injected somewhere.

Tasks

While the simplest way to get a character to perform some behavior is to create an action, add it to an AI pack, and have it fulfill the top level activity, stonehearth:top, that does have a few limitations:

Tasks are how you surpass these limitations. A task is a structure that schedules an activity; think of it as a personal assistant whose sole job is to nag someone (or a group of someones) to do something.

In order to match tasks up with people, we house tasks inside Task Groups. A task group instance contains a set of pending tasks, and a set of people to give those tasks to. Each task belongs to exactly one task group, while each character can belong to any number of task groups. When a character is added to a task group, its Task Group Action is injected into their AI. When a new task is created, the Task Action is injected into the AI of every character belonging to the group, and runs under the group's Task Group Action.

Personal task groups

Each character can (and usually does) have "personal" task groups that only include the character themself. For example, this is used to issue basic need tasks (eating / sleeping / recovery) as shown above, or to issue move orders to military units. By convention, task groups that are intended to be personal are prefixed with "solo".

Personal task groups are declared in AI packs, as described above, and are accessed from the stonehearth:ai component via its get_task_group() method.

Town task groups

In addition to personal task groups, there are task groups managed by the town, which potentially include multiple characters. Building and farming task groups are examples of this. Other systems can get these task groups from the town and issue tasks in them. These task groups are declared in a task groups JSON file (e.g. player_task_groups.json) and referenced in the population faction JSON (e.g. ascendancy_population.json). Characters are added to these task groups if their job description specifies the group under task_groups (e.g. farmer_description.json).

Ad hoc task groups

Certain systems create temporary task groups to dispatch tasks to temporary groupings of hearthlings. An example for this is combat parties, which creates a task group to dispatch commands to everyone in the party.

Issuing tasks

Systems that want to issue tasks need to get the task group (a personal one from the AI component, a town one from Town, or an ad hoc one created elsewhere), then call create_task(), passing the activity and arguments. Only activities declared by that task group action (as described earlier in this page) are allowed, and if the task group does not declare the task as a "multiple", only one instance of the task can be issued at a time. When a task is created, it starts off in a paused state, and the caller can configure it before calling start(), which dispatches it to the characters belonging to its group. Configuration methods include:

Tasks can be added to task groups at any time, even when the task group doesn't have people in it yet.

For example, the shepherd pasture component will create a task for all shepherds in the town to feed animals when the animals become hungry. From shepherd_pasture_component.lua:

  local feed_task = town:get_task_group('stonehearth:task_groups:herding')
                          :create_task('stonehearth:feed_pasture_animals', {pasture = self._entity})
                          :set_source(self._entity)
                          :start()

Remember to declare your task groups in the "aliases" sections of your manifest.

Persistence

None of the AI state is persisted into savegames, which means several things, including: