DomainTranslator

DomainTranslator is the class that’s an intermediary between the 'real' game state and the planner state space. It is used to define the plan domain (action templates and object types), create a binding between in-game objects and planner World Objects and - finally - to translate abstract plans to behavior sequences.

IPlannerObject

Grail provides an interface that can be implemented by in-game objects (or wrappers implemented specifically for AI). This interface is called IPlannerObject. It is used by the domain translator to create bindings between 'real' and abstract (i. e. belonging to abstract state space).

The role of IPlannerObject is to produce a WorldObject associated with the original object. Here’s an example implementation:

  • C++

  • C#

class Item : public grail::IPlannerObject
{
private:
  std::string name;
public:
  Item(const std::string& name) : name(name) {}

  virtual grail::WorldObject ToWorldObject(grail::Domain& domain, const grail::ObjectIndexBinding&) const override
  {
    grail::WorldObject obj = domain.CreateObjectOfType("item");
    obj.SetParameter("name", name);
    return obj;
  }
};
public class Item : Grail.IPlannerObject
{
  private string name;

  public Item(const std::string& name)
  {
    this.name = name;
  }

  public Grail.WorldObject ToWorldObject(Grail.Domain domain, Grail.ObjectIndexBinding binding)
  {
    WorldObject obj = domain.CreateObjectOfType("item");
    obj.SetParameter("name", name);
    return obj;
  }
};

Example implementation

Now that we have our IPlannerObject sorted out, we can proceed with the translator. Here’s an example implementation of a complete domain translator:

  • C++

  • C#

class MyTranslator : public grail::DomainTranslator
{
public:
  MyTranslator(grail::MemoryPool& memory)
    : grail::DomainTranslator{ memory }
  {
  }

  // Inherited via DomainTranslator
  virtual std::vector<grail::IPlannerObject*> GeneratePlannerObjects(grail::AIEntity& entity, const grail::Goal&) override
  {
    //In this method we assume that Item class implements IPlannerObject interface
    //the returned vector will be used to create the initial world state
    //and create a mapping between real in-game objects and abstract planner objects

    std::vector<grail::IPlannerObject*> result;

    //A list of considered items is stored on the entity's blackboard
    auto worldItems = entity.GetBlackboard().GetValue<std::vector<std::shared_ptr<Item>>>("worldItems");
    for (auto& item : worldItems)
    {
      result.push_back(item.get());
    }

    //more object types follow
    //...

    return result;
  }

  virtual std::vector<grail::WorldObjectType> CreateObjectTypes(const grail::AIEntity&, const grail::Goal&) override
  {
    std::vector<grail::WorldObjectType> result;
    result.push_back(grail::WorldObjectType(memory, "item"));
    //... more item types follow
    return result;
  }

  virtual void AddActionTemplates(const grail::AIEntity&, const std::shared_ptr<grail::Domain>& inDomain, const grail::Goal&) override
  {
    //this action accepts one argument of type "item"
    auto& action = inDomain->AddActionTemplate("pick_up", { "item" });
    action.SetPreconditions([](const std::vector<const grail::WorldObject*>& args, const grail::WorldState&)
    {
      return true;
    });

    action.SetEffects([](const std::vector<grail::WorldObject*>& args, grail::WorldState& state)
    {
      std::string itemName = args[0]->GetParameter<string>("name");
      int itemCount = state.GetParameter<int>(itemName);
      state.SetParameter(itemName, itemCount + 1);
      state.RemoveObject(args[0]->GetObjectIndex());
    });

    //more actions follow
    //...
  }

  virtual grail::Plan TranslatePlan(const grail::AIEntity&, const grail::Planner::AbstractPlan& plan, const grail::Goal&) const override
  {
    //when translating the plan, we use action names to identify
    //needed behaviors and created index-to-object binding to recover in-game objects
    grail::Plan result;
    for (const auto& action : plan.actions)
    {
      if (action.GetName() == "pick_up")
      {
        auto item = binding.GetObjectByIndex<Item>(action.GetArgumentIndices()[0]);
        result.PushBehavior(std::make_unique<GoTo>(item));
        result.PushBehavior(std::make_unique<PickUp>(item));
      }

      //... more actions can be interpreted in a similar fashion
    }
    return result;
  }
};
public class MyTranslator : public Grail.DomainTranslator
{
  // Inherited via DomainTranslator
  public override List<Grail.IPlannerObject> GeneratePlannerObjects(Grail.AIEntity entity, Grail.Goal goal)
  {
    //In this method we assume that Item class implements IPlannerObject interface
    //the returned vector will be used to create the initial world state
    //and create a mapping between real in-game objects and abstract planner objects

    var result = new List<Grail.IPlannerObject>();

    //A list of considered items is stored on the entity's blackboard
    var worldItems = entity.Blackboard.GetValue<List<Item>>("worldItems");
    foreach (var item in worldItems)
    {
      result.Add(item);
    }

    //more object types follow
    //...

    return result;
  }

  public override List<Grail.WorldObjectType> CreateObjectTypes(Grail.AIEntity entity, Grail.Goal goal)
  {
    var result = new List<Grail.WorldObjectType>();
    result.Add(new Grail.WorldObjectType("item"));
    //more item types follow
    //...
    return result;
  }

  public override void AddActionTemplates(Grail.AIEntity entity, Grail.Domain inDomain, Grail.Goal goal)
  {
    //this action accepts one argument of type "item"
    var action = inDomain.AddActionTemplate("pick_up", new List<string>() { "item" });
    action.Preconditions = (args, state) =>
    {
      return true;
    };

    action.Effects = (args, state) =>
    {
      string itemName = args[0].GetParameter<string>("name");
      int itemCount = state.GetParameter<int>(itemName);
      state.SetParameter(itemName, itemCount + 1);
      state.RemoveObject(args[0].ObjectIndex);
    };

    //more actions follow
    //...
  }

  public override Grail.Plan TranslatePlan(Grail.AIEntity entity, Grail.Planner.AbstractPlan plan, Grail.Goal goal)
  {
    //when translating the plan, we use action names to identify
    //needed behaviors and created index-to-object binding to recover in-game objects
    Grail.Plan result = new Grail.Plan();
    foreach (var action : plan.actions)
    {
      if (action.Name == "pick_up")
      {
        auto item = binding.GetObjectByIndex<Item>(action.ArgumentIndices[0]);

        //as you can see, one abstract action can correspond to more than one behavior
        result.PushBehavior(new GoTo(item));
        result.PushBehavior(new PickUp(item));
      }

      //... more actions can be interpreted in a similar fashion
    }
    return result;
  }
};