Planner Reasoner: Quickstart
Defining a simple game
Before we begin implementing AI logic, we have to have something to work on. Let’s create a simple item crafting game!
The game consists of four types of objects:
-
Items
-
Recipes - combinations of items that result in other items
-
Factories, characterized by lists of Recipes they make available
-
Agents - characters acting in the world by picking up Items and creating new ones using the Recipes in Factories
Classes that have their representation in abstract planning space implement IPlannerObject
interface and provide translation logic.
-
C++
-
C#
class Object
{
public:
Object(const std::string& name) : name(name) {}
std::string name;
};
class Item : public Object, public grail::planner::IPlannerObject
{
public:
Item(const std::string& name) : Object(name) {}
// Inherited via IPlannerObject;
virtual grail::planner::WorldObject ToWorldObject(grail::planner::Domain& domain, const grail::planner:ObjectIndexBinding&) const override
{
grail::planner::WorldObject obj = domain.CreateObjectOfType("item");
obj.SetParameter("name", name);
return obj;
}
};
class Recipe
{
public:
//[item name, required item count] pairs
std::vector<std::pair<std::string, int>> components;
std::string productName;
};
class Factory : public Object, public grail::planner::IPlannerObject
{
public:
Factory(const std::string& name, std::vector<std::shared_ptr<Item>>& items) : Object(name), worldItems(items) {}
void MakeRecipe(const Recipe& recipe, Agent& maker) const
{
//...item creation logic omitted for brevity
}
// Inherited via IPlannerObject
virtual grail::planner::WorldObject ToWorldObject(grail::planner::Domain& domain, const grail::planner::ObjectIndexBinding&) const override
{
return domain.CreateObjectOfType(name);
}
std::vector<Recipe*> recipes;
std::vector<std::shared_ptr<Item>>& worldItems;
};
class Agent
{
public:
void PickUp(Item* item)
{
//..pick up logic omitted for brevity
}
std::vector<std::shared_ptr<Item>>* worldItems; //a reference to all items present in the game world
std::vector<std::shared_ptr<Factory>>* worldFactories; //a reference to all factories present in the game world
Object* nearbyObject = nullptr;
std::vector<std::pair<std::string, int>> inventory;
};
public class Object
{
public Object(string name)
{
this.name = name;
}
public string name;
};
public class Item : Object, Grail.IPlannerObject
{
public Item(string name) : base(name) {}
public Grail.WorldObject ToWorldObject(Grail.Domain domain, Grail.ObjectIndexBinding)
{
Grail.WorldObject obj = domain.CreateObjectOfType("item");
obj.SetParameter("name", name);
return obj;
}
};
public class Recipe
{
//[item name, required item count] pairs
public Dictionary<string, int> components = Dictionary<string, int>();
public string productName;
};
public class Factory : Object, Grail.IPlannerObject
{
public Factory(string name, List<Item> items) : base(name)
{
worldItems = items;
}
public void MakeRecipe(Recipe recipe, Agent maker)
{
//...item creation logic omitted for brevity
}
// Inherited via IPlannerObject
public Grail.WorldObject ToWorldObject(Grail.Domain domain, Grail.ObjectIndexBinding binding)
{
return domain.CreateObjectOfType(name);
}
private List<Recipe> recipes;
private List<Item> worldItems;
};
public class Agent
{
public void PickUp(Item item)
{
//..pick up logic omitted for brevity
}
public List<Item> worldItems; //a reference to all items present in the game world
public List<Factory> worldFactories; //a reference to all factories present in the game world
public Object nearbyObject = null;
public List<(string name, int count)> inventory;
};
Defining behaviors
Next, we proceed to define behaviors that our agent can perform:
-
Approaching items and factories in the game world (
GoTo
behavior) -
Crafting items in factories (
MakeRecipe
behavior) -
Picking up existing items (
PickUp
behavior)
For better readability, Update
, IsFinished
and IsLegal
method implementations are omitted in this example.
-
C++
-
C#
class GoTo : public grail::Behavior
{
public:
GoTo(Object* obj) : target(obj) {}
// Inherited via Behavior
virtual void Start(grail::AIEntity& entity) override
{
//the agent is stored on the entity's blackboard
auto agent = entity.GetBlackboard().GetValue<std::shared_ptr<Agent>>("controlled_agent");
agent->nearbyObject = target;
}
private:
Object* target;
};
class MakeRecipe : public grail::Behavior
{
public:
MakeRecipe(Factory* fac, const Recipe* rec) : target(fac), recipe(rec) {}
// Inherited via Behavior
virtual void Start(grail::AIEntity& entity) override
{
auto agent = entity.GetBlackboard().GetValue<std::shared_ptr<Agent>>("controlled_agent");
target->MakeRecipe(*recipe, *agent);
}
private:
Factory* target;
const Recipe* recipe;
};
class PickUp : public grail::Behavior
{
public:
PickUp() = default;
PickUp(Item* obj) : target(obj) {}
// Inherited via Behavior
virtual void Start(grail::AIEntity& entity, float) override
{
auto agent = entity.GetBlackboard().GetValue<std::shared_ptr<Agent>>("controlled_agent");
agent->PickUp(target);
}
private:
Item* target;
};
public class GoTo : Grail.Behavior
{
public GoTo(Object obj)
{
target = obj
}
// Inherited via Behavior
public override void Start(Grail.AIEntity entity)
{
//the agent is stored on the entity's blackboard
var agent = entity.Blackboard.GetValue<Agent>("controlled_agent");
agent.nearbyObject = target;
}
private Object target;
};
public class MakeRecipe : Grail.Behavior
{
public MakeRecipe(Factory fac, Recipe rec)
{
target = fac;
recipe = rec;
}
// Inherited via Behavior
public override void Start(Grail.AIEntity entity)
{
var agent = entity.Blackboard.GetValue<Agent>("controlled_agent");
target.MakeRecipe(recipe, agent);
}
private Factory target;
private Recipe recipe;
};
public class PickUp : Grail.Behavior
{
public PickUp(Item obj)
{
target = obj;
}
// Inherited via Behavior
public override void Start(Grail.AIEntity entity, float deltaTime)
{
var agent = entity.Blackboard.GetValue<Agent>("controlled_agent");
agent.PickUp(target);
}
private Item target;
};
Planner domain definition
In this section, an example of domain definition is presented.
We start by defining WorldObject
types and then proceed to action template creation.
For these tasks, DomainTranslator class is used. A basic outline of our translator looks like this:
-
C++
-
C#
class Translator : public grail::planner::DomainTranslator
{
public:
Translator(grail::MemoryPool& memory, const std::vector<Factory>& allFactories, const std::map<const Recipe*, std::string>& recipeNames)
: grail::planner::DomainTranslator{ memory }, factoryDatabase(allFactories), recipeNames(recipeNames)
{
for (const auto& pair : recipeNames)
{
nameRecipes.insert({ pair.second, pair.first });
}
}
// Inherited via DomainTranslator; the methods below will be implemented in the following sections
virtual std::vector<grail::planner::WorldObjectType> CreateObjectTypes(const grail::AIEntity&, const grail::planner::Goal&) override
{
}
virtual void AddActionTemplates(const grail::AIEntity&, const std::shared_ptr<grail::planner::Domain>& inDomain, const grail::planner::Goal&) override
{
}
//the implementation of these three methods will be described in the
//'State Translation' section
virtual grail::Plan TranslatePlan(const grail::AIEntity&, const grail::planner::Planner::AbstractPlan& plan, const grail::planner::Goal&) const override
{
}
virtual std::vector<grail::planner::IPlannerObject*> GeneratePlannerObjects(grail::AIEntity& entity, const grail::planner::Goal&) override
{
}
virtual void SetWorldStateParameters(const grail::AIEntity& entity, grail::planner::WorldState& state, const grail::planner::Goal& goal) const
{
}
private:
const std::vector<Factory>& factoryDatabase; //a list of all possible factory types
std::map<const Recipe*, std::string> recipeNames; //recipe to name mapping
std::map<std::string, const Recipe*> nameRecipes; //name to recipe mapping
};
public class Translator : Grail.Planning.DomainTranslator
{
public Translator(List<Factory> allFactories, Dictionary<Recipe, string> recipeNames)
{
factoryDatabase = allFactories;
this.recipeNames = recipeNames;
for (var recipe : recipeNames)
{
nameRecipes.Add(recipe.Value, recipe.Key)
}
}
// Inherited via DomainTranslator; the methods below will be implemented in the following sections
protected override List<Grail.Planning.WorldObjectType> CreateObjectTypes(Grail.AIEntity, Grail.Planning.Goal goal)
{
}
protected override void AddActionTemplates(Grail.AIEntity, Grail.Planning.Domain inDomain, Grail.Planning.Goal goal)
{
}
//the implementation of these three methods will be described in the
//'State Translation' section
public override Grail.Planning.Plan TranslatePlan(Grail.AIEntity, Grail.Planning.Planner.AbstractPlan plan, Grail.Planning.Goal goal)
{
}
protected override List<Grail.Planning.IPlannerObject> GeneratePlannerObjects(Grail.AIEntity entity, Grail.Planning.Goal)
{
}
protected override void SetWorldStateParameters(Grail.AIEntity entity, Grail.Planning.WorldState state, Grail.Planning.Goal goal)
{
}
private List<Factory> factoryDatabase; //a list of all possible factory types
private Dictionary<Recipe, string> recipeNames; //recipe to name mapping
private Dictionary<string, Recipe> nameRecipes = new Dictionary<string, Recipe>(); //name to recipe mapping
};
Object type definition
Planner WorldObject
types are declared inside the CreateObjectTypes
method.
-
C++
-
C#
virtual std::vector<grail::planner::WorldObjectType> CreateObjectTypes(const grail::AIEntity&, const grail::planner::Goal&) override
{
//the object types created inside this method will be automatically added
//to the planning domain
std::vector<grail::planner::WorldObjectType> result;
result.push_back(grail::planner::WorldObjectType(memory, "item")); //here, we create an object type for items
//each factory is a separate object type in the plan space
for (const auto& factory : factoryDatabase)
{
result.push_back(grail::planner::WorldObjectType(memory, factory.name));
}
return result;
}
protected override List<Grail.Planning.WorldObjectType> CreateObjectTypes(Grail.AIEntity, Grail.Planning.Goal goal)
{
//the object types created inside this method will be automatically added
//to the planning domain
List<WorldObjectType> result = new List<WorldObjectType>
{
new WorldObjectType("item") //here, we create an object type for items
};
//each factory is a separate object type in the plan space
foreach (var factory in factoryDatabase)
{
result.Add(new WorldObjectType(factory.name));
}
return result;
}
Planner action template definition
Action templates are defined inside AddActionTemplate
method.
-
C++
-
C#
virtual void AddActionTemplates(const grail::AIEntity&, const std::shared_ptr<grail::planner::Domain>& inDomain, const grail::planner::Goal&) override
{
//foreach factory type
for (const auto& factory : factoryDatabase)
{
//generate a new plan space action for each recipe
for (const auto recipe : factory.recipes)
{
//to create the action and add it to the domain, pass its name and its parameter type names
//in this case, there's only one parameter of a type corresponding to the factory name
auto& action = inDomain->AddActionTemplate(recipeNames.at(recipe), { factory.name });
//set action preconditions as a lambda function
action.SetPreconditions([=](const std::vector<const grail::planner::WorldObject*>&, const grail::planner::WorldState& state)
{
//return false if any of the recipe components is missing
for(const auto& compPair : recipe->components)
{
if (state.GetParameterValue<int>(compPair.first) < compPair.second)
{
return false;
}
}
return true;
});
//analogously, set action effect function
action.SetEffects([=](const std::vector<grail::planner::WorldObject*>&, grail::planner::WorldState& state)
{
//inventory items are stored as world state parameters
//when this action is executed, the corresponding item counter gets incremented
int itemCount = state.GetParameterValue<int>(recipe->productName);
state.SetParameter(recipe->productName, itemCount + 1);
//the recipe ingredients get consumed during the production process
for (const auto& compPair : recipe->components)
{
itemCount = state.GetParameterValue<int>(compPair.first);
state.SetParameter(compPair.first, itemCount - compPair.second);
}
});
}
}
//pick up action definition, accepting objects of type 'item' as parameters
auto& action = inDomain->AddActionTemplate("pick_up", { "item" });
action.SetPreconditions([](const std::vector<const grail::planner::WorldObject*>& args, const grail::planner::WorldState&)
{
//the action is legal only if the item is not picked
return args[0]->GetParameterValue<bool>("is_picked") == false;
});
action.SetEffects([](const std::vector<grail::planner::WorldObject*>& args, grail::planner::WorldState& state)
{
//increment the item counter, mark the object as picked
std::string itemName = args[0]->GetParameterValue<std::string>("name");
int itemCount = state.GetParameterValue<int>(itemName);
state.SetParameter(itemName, itemCount + 1);
args[0]->SetParameter("is_picked", true);
});
}
protected override void AddActionTemplates(AIEntity entity, Domain domain, Goal goal)
{
//foreach factory type
foreach (var factory in factoryDatabase)
{
//generate a new plan space action for each recipe
foreach (var recipe in factory.recipes)
{
//to create the action and add it to the domain, pass its name and its parameter type names
//in this case, there's only one parameter of a type corresponding to the factory name
var action = domain.AddActionTemplate(recipeNames[recipe], new List<string>() {factory.name});
//set action preconditions as a lambda function
action.Preconditions = (args, state) =>
{
//return false if any of the recipe components is missing
foreach (var componentPair in recipe.components)
{
if (state.GetParameterValue<int>(componentPair.Key) < componentPair.Value)
{
return false;
}
}
return true;
};
//analogously, set action effect function
action.Effects = (args, state) =>
{
//inventory items are stored as world state parameters
//when this action is executed, the corresponding item counter gets incremented
int itemCount = state.GetParameterValue<int>(recipe.productName);
state.SetParameter(recipe.productName, itemCount + 1);
//the recipe ingredients get consumed during the production process
foreach (var componentPair in recipe.components)
{
if (componentPair.Key != "axe" && componentPair.Key != "pickaxe")
{
itemCount = state.GetParameterValue<int>(componentPair.Key);
state.SetParameter(componentPair.Key, itemCount - componentPair.Value);
}
}
};
}
}
//pick up action definition, accepting objects of type 'item' as parameters
var pickUpAction = domain.AddActionTemplate("pick_up", new List<string>() {"item"});
pickUpAction.Preconditions = (args, state) => !args[0].GetParameterValue<bool>("is_picked");
pickUpAction.Effects = (args, state) =>
{
//increment the item counter, mark the object as picked
string itemName = args[0].GetParameterValue<string>("name");
int itemCount = state.GetParameterValue<int>(itemName);
state.SetParameter(itemName, itemCount + 1);
args[0].SetParameter("is_picked", true);
};
}
State translation
In this section, we describe the implementation of DomainTranslator
methods responsible for producing abstract state representations from 'real' game states and translating abstract action sequences to Behaviors.
Creating World Objects
GeneratePlannerObjects
method should return all objects implementing IPlannerObject
interface that we want to be taken into account in a particular planning domain.
In our case, it means all items and factories.
-
C++
-
C#
virtual std::vector<grail::planner::IPlannerObject*> GeneratePlannerObjects(grail::AIEntity& entity, const grail::planner::Goal&) override
{
std::vector<grail::planner::IPlannerObject*> result;
auto agent = entity.GetBlackboard().GetValue<std::shared_ptr<Agent>>("controlled_agent");
for (auto& item : *agent->worldItems)
{
result.push_back(item.get());
}
for (auto& factory : *agent->worldFactories)
{
result.push_back(factory.get());
}
return result;
}
protected override List<IPlannerObject> GeneratePlannerObject(AIEntity entity, Goal goal)
{
List<IPlannerObject> result = new List<IPlannerObject>();
var agent = entity.Blackboard.GetValue<Agent>("controlled_agent");
foreach (var item in agent.worldItems)
{
result.Add(item);
}
foreach (var factory in agent.worldFactories)
{
result.Add(factory);
}
return result;
}
Setting World State parameters
-
C++
-
C#
virtual void SetWorldStateParameters(const grail::AIEntity& entity, grail::planner::WorldState& state, const grail::planner::Goal& goal) const
{
auto agent = entity.GetBlackboard().GetValue<std::shared_ptr<Agent>>("controlled_agent");
for (const auto& inventoryEntry : agent->inventory)
{
state.SetParameter(inventoryEntry.first, inventoryEntry.second); //item name and count
}
}
protected override void SetWorldStateParameters(Grail.AIEntity entity, Grail.Planning.WorldState state, Grail.Planning.Goal goal)
{
var agent = entity.Blackboard.GetValue<Agent>("controlled_agent");
for (var (itemName, count) : agent.inventory)
{
state.SetParameter(itemName, count);
}
}
Translation from abstract actions to behaviors
After a plan is found, it has to be converted to a behavior sequence.
To do this, we implement TranslatePlan
method.
Inside the method we use the binding
field (defined in DomainTranslator
base class) to retrieve object references, then we construct behaviors to be executed based
-
C++
-
C#
virtual grail::Plan TranslatePlan(const grail::AIEntity&, const grail::planner::Planner::AbstractPlan& plan, const grail::planner::Goal&) const override
{
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));
}
else
{
auto factory = binding.GetObjectByIndex<Factory>(action.GetArgumentIndices()[0]);
result.PushBehavior(std::make_unique<GoTo>(factory));
result.PushBehavior(std::make_unique<MakeRecipe>(factory, nameRecipes.at(action.GetName())));
}
}
return result;
}
public override Plan TranslatePlan(AIEntity entity, Planner.AbstractPlan plan, Goal goal)
{
Plan result = new Plan();
foreach (var action in plan.actions)
{
if (action.Name == "pick_up")
{
var item = binding.GetObjectByIndex<Item>(action.ArgumentIndices[0]);
result.PushBehavior(new GoTo(item));
result.PushBehavior(new PickUp(item));
}
else
{
var factory = binding.GetObjectByIndex<Factory>(action.ArgumentIndices[0]);
result.PushBehavior(new GoTo(factory));
result.PushBehavior(new MakeRecipe(factory, nameRecipes[action.Name]));
}
}
return result;
}
Defining goals
Grail provides the Goal
class to handle goal logic and its interaction with the reasoner.
For more details on this class, see Goal page.
In this tutorial, we focus on a concrete implementation of a goal: creating an item.
-
C++
-
C#
class ItemCreated : public grail::planner::Goal
{
public:
ItemCreated() = default;
ItemCreated(const std::string& name) : itemName(name) {} //the class is initialized with the requested item name
// Inherited via Goal
virtual bool IsReached(const grail::AIEntity& entity) const override
{
auto agent = entity.GetBlackboard().GetValue<std::shared_ptr<Agent>>("controlled_agent");
//return true if there is one or more specified items in the game world
auto itemIter = std::find_if(agent->inventory.begin(), agent->inventory.end(), [&](const auto& itemPair)
{
return itemPair.first == itemName;
});
return itemIter != agent.inventory.end() && itemIter->second > 0;
}
//this method specifies goal conditions in the abstract plan space
virtual void SetPlanSpaceConditions(grail::planner::Planner& planner, const grail::planner::ObjectIndexBinding&) const override
{
std::string name = itemName;
planner.PushCondition([name](const grail::planner::WorldState& state)
{
return state.GetParameterValue<int>(name) > 0;
});
}
private:
std::string itemName;
};
public class ItemCreated : Goal
{
private string itemName;
public ItemCreated(string name)
{
itemName = name;
}
public override bool IsReached(AIEntity entity)
{
var agent = entity.Blackboard.GetValue<Agent>("controlled_agent");
return agent.inventory.ContainsKey(itemName) && agent.inventory[itemName] > 0;
}
public override void SetPlanSpaceConditions(Planner planner, ObjectIndexBinding binding)
{
string name = itemName;
planner.PushCondition(state => state.GetParameterValue<int>(name) > 0);
}
}
Defining a goal selector
To give our Reasoner a proper goal, we need to implement a Goal Selector. In this example, we’ll limit ourselves to the simplest possible selection logic: setting a single, predefined goal.
-
C++
-
C#
class DummyGoalSelector : public grail::planner::GoalSelector
{
public:
DummyGoalSelector(const std::string& itemName) : itemToCraft{ itemName } {}
virtual void UpdateGoal(grail::planner::IGoalAcceptor& goalAcceptor, grail::AIEntity& entity) override
{
//if no goal is currently chosen, set one
if (goalAcceptor.GetCurrentGoal() == nullptr)
{
goalAcceptor.SetNewGoal(std::make_unique<ItemCreated>(itemToCraft), entity);
}
}
private:
std::string itemToCraft;
};
class DummyGoalSelector : IGoalSelector
{
public DummyGoalSelector(string itemName)
{
this.itemToCraft = itemName;
}
public void UpdateGoal(IGoalAcceptor goalAcceptor, AIEntity entity)
{
//if no goal is currently chosen, set one
if (goalAcceptor.CurrentGoal == null)
{
goalAcceptor.SetNewGoal(new ItemCreated(itemToCraft), entity);
}
}
private readonly string itemToCraft;
};
Reasoner creation and assignment
In this section, we show how to create and use a Planner Reasoner that uses the Domain Translator that we defined above.
-
C++
-
C#
//create a shared memory pool for world state data
//this pointer should NOT be released before the reasoner
auto pool = std::make_unique<grail::planner::MemoryPool>();
//create domain translator. 'allFactories' and 'recipeNames' arguments will be made clear in the next section
auto translator = std::make_unique<Translator>(pool, allFactories, recipeNames);
auto entity = std::make_shared<grail::AIEntity>();
//set planner reasoner configuration and create a reasoner instance
grail::planner::PlannerReasoner::Config config;
config.iterationsPerFrame = 20;
config.maxIterations = 50000;
config.maxPlanLength = 20;
config.usePartialPlans = true;
auto plannerReasoner = std::make_unique<grail::planner::PlannerReasoner>(pool, std::make_shared<DummyGoalSelector>("grail"), std::move(translator), config);
//assign the reasoner
entity->SetReasoner(std::move(plannerReasoner));
//create domain translator. 'allFactories' and 'recipeNames' arguments will be made clear in the next section
var translator = new Translator(pool, allFactories, recipeNames);
var entity = new Grail.AIEntity();
//set planner reasoner configuration and create a reasoner instance
Grail.PlannerReasoner.Config config
{
MaxIterations = 500000,
MaxPlanLength = 20,
IterationPerUpdate = 20,
UsePartialPlans = true
}
var plannerReasoner = new Grail.PlannerReasoner(new DummyGoalSelector("grail"), translator, config);
//assign the reasoner
entity.Reasoner = plannerReasoner;
Putting it all together
In this section, we show a complete implementation of our simple crafting game, along with AIEntity data setup.
-
C++
-
C#
//GAME SETUP START
//definition of starting game world items
std::vector<std::shared_ptr<Item>> worldItems;
worldItems.push_back(std::make_shared<Item>("wood"));
worldItems.push_back(std::make_shared<Item>("ore"));
worldItems.push_back(std::make_shared<Item>("ore"));
//definition of all available recipes
//to produce an axe, you need 1 wood and 1 metal
Recipe axeRecipe;
axeRecipe.components = { { "wood", 1} , { "metal", 1} };
axeRecipe.productName = "axe";
Recipe pickaxeRecipe;
pickaxeRecipe.components = { { "wood", 1 },{ "metal", 1 } };
pickaxeRecipe.productName = "pickaxe";
Recipe woodRecipe;
woodRecipe.components = { { "axe", 1} };
woodRecipe.productName = "wood";
Recipe oreRecipe;
oreRecipe.components = { { "pickaxe", 1 }};
oreRecipe.productName = "ore";
Recipe metalRecipe;
metalRecipe.components = { { "ore", 1 } };
metalRecipe.productName = "metal";
Recipe grailRecipe;
grailRecipe.components = { { "wood", 3 }, { "metal", 3} };
grailRecipe.productName = "grail";
//factory types and their corresponding recipes
Factory workshop("workshop", worldItems);
workshop.recipes = { &axeRecipe };
Factory forest("forest", worldItems);
forest.recipes = { &woodRecipe };
Factory forge("forge", worldItems);
forge.recipes = { &pickaxeRecipe, &metalRecipe, &grailRecipe };
Factory mine("mine", worldItems);
mine.recipes = { &oreRecipe };
//definition of game world factories
std::vector<std::shared_ptr<Factory>> worldFactories =
{
std::make_shared<Factory>(workshop),
std::make_shared<Factory>(forest),
std::make_shared<Factory>(forge),
std::make_shared<Factory>(mine)
};
//create the agent to be controlled by our AI and initialize it with game world info
auto agent = std::make_shared<Agent>();
agent->worldItems = &worldItems;
agent->worldFactories = &worldFactories;
//GAME SETUP END
//AI SETUP START
grail::AIManager manager; //create an AI Manager responsible for updating AI Entities
//create a shared memory pool for world state data
//this pointer should NOT be released before the reasoner
auto pool = std::make_unique<grail::planner::MemoryPool>();
//define a mapping from recipes to action names and factory type list
//use them to create a domain translator
std::map<const Recipe*, std::string> recipeNames =
{
{&axeRecipe, "make_axe"},
{&pickaxeRecipe, "make_pickaxe"},
{&woodRecipe, "make_wood"},
{&oreRecipe, "make_ore"},
{&metalRecipe, "make_metal"},
{&grailRecipe, "make_grail"}
};
std::vector<Factory> allFactories = { workshop, forest, forge, mine };
auto translator = std::make_unique<Translator>(pool, allFactories, recipeNames);
//create an AI Entity to control our agent
auto entity = std::make_shared<grail::AIEntity>();
//set planner reasoner configuration and create a reasoner instance
grail::planner::PlannerReasoner::Config config;
config.iterationsPerFrame = 20;
config.maxIterations = 50000;
config.maxPlanLength = 20;
config.usePartialPlans = true;
auto plannerReasoner = std::make_unique<grail::planner::PlannerReasoner>(pool, std::make_shared<DummyGoalSelector>("grail"), std::move(translator), config);
//assign the reasoner
entity->SetReasoner(std::move(plannerReasoner));
//set up entity data and register the entity in the AI Manager
entity->GetBlackboard().SetValue("controlled_agent", agent);
manager.RegisterAIEntity(agent, 0);
//AI SETUP END
//a simple game loop
float deltaTime = 0.05f;
for (int i = 0; i < 1000; ++i)
{
manager.UpdateReasoners();
manager.UpdateEntities(deltaTime);
}
//if everything went as expected, this iterator should point to a valid object
auto itemIter = std::find_if(agent->inventory.begin(), agent->inventory.end(), [&](const auto& itemPair)
{
return itemPair.first == "grail";
});
//GAME SETUP START
//definition of starting game world items
List<Item> worldItems = new List<Item>();
worldItems.Add(new Item("wood"));
worldItems.Add(new Item("ore"));
worldItems.Add(new Item("ore"));
//definition of all available recipes
//to produce an axe, you need 1 wood and 1 metal
Recipe axeRecipe = new Recipe();
axeRecipe.components = new Dictionary<string, int>();
axeRecipe.components["wood"] = 1;
axeRecipe.components["metal"] = 1;
axeRecipe.productName = "axe";
Recipe pickaxeRecipe = new Recipe();
pickaxeRecipe.components = new Dictionary<string, int>();
pickaxeRecipe.components["wood"] = 1;
pickaxeRecipe.components["metal"] = 1;
pickaxeRecipe.productName = "pickaxe";
Recipe woodRecipe = new Recipe();
woodRecipe.components = new Dictionary<string, int>();
woodRecipe.components["axe"] = 1;
woodRecipe.productName = "wood";
Recipe oreRecipe = new Recipe();
oreRecipe.components = new Dictionary<string, int>();
oreRecipe.components["pickaxe"] = 1;
oreRecipe.productName = "ore";
Recipe metalRecipe = new Recipe();
metalRecipe.components = new Dictionary<string, int>();
metalRecipe.components["ore"] = 1;
metalRecipe.productName = "metal";
Recipe grailRecipe = new Recipe();
grailRecipe.components = new Dictionary<string, int>();
grailRecipe.components["wood"] = 3;
grailRecipe.components["metal"] = 3;
grailRecipe.productName = "grail";
//factory types and their corresponding recipes
Factory workshop = new Factory("workshop", worldItems);
workshop.recipes = new List<Recipe>() {axeRecipe};
Factory forest = new Factory("forest", worldItems);
forest.recipes = new List<Recipe>() {woodRecipe};
Factory forge = new Factory("forge", worldItems);
forge.recipes = new List<Recipe>() { pickaxeRecipe, metalRecipe, grailRecipe};
Factory mine = new Factory("mine", worldItems);
mine.recipes = new List<Recipe>() {oreRecipe};
List<Factory> factories = new List<Factory>() {workshop, forest, forge, mine};
//definition of game world factories
//this is a just copy of the list above, but we keep them separated, because they represent different things
List<Factory> worldFactories = new List<Factory>(factories);
//create the agent to be controlled by our AI and initialize it with game world info
var agent = new Agent();
agent.worldItems = worldItems;
agent.worldFactories = worldFactories;
//GAME SETUP END
//AI SETUP START
Grail.AIManager manager; //create an AI Manager responsible for updating AI Entities
//define a mapping from recipes to action names and factory type list
//use them to create a domain translator
var recipeNames = new Dictionary<Recipe, string>();
recipeNames[axeRecipe] = "make_axe";
recipeNames[pickaxeRecipe] = "make_pickaxe";
recipeNames[woodRecipe] = "make_wood";
recipeNames[oreRecipe] = "make_ore";
recipeNames[metalRecipe] = "make_metal";
recipeNames[grailRecipe] = "make_grail";
List<Factory> allFactories = new List<Factory>() {workshop, forest, forge, mine};
var translator = new Translator(allFactories, recipeNames);
//create an AI Entity to control our agent
var entity = new Grail.AIEntity();
//set planner reasoner configuration and create a reasoner instance
Grail.PlannerReasoner.Config config
{
MaxIterations = 500000,
MaxPlanLength = 20,
IterationPerUpdate = 20,
UsePartialPlans = true
};
var plannerReasoner = new PlannerReasoner(new DummyGoalSelector("grail"), translator, config);
//assign the reasoner
entity.Reasoner = plannerReasoner;
//set up entity data and register the entity in the AI Manager
entity.Blackboard.SetValue("controlled_agent", agent);
manager.RegisterAIEntity(agent, 0);
//AI SETUP END
//a simple game loop
float deltaTime = 0.05f;
for (int i = 0; i < 1000; ++i)
{
manager.UpdateReasoners();
manager.UpdateEntities(deltaTime);
}
//if everything went as expected, this flag should be true after the loop
bool grailCreated = false;
foreach (var item in agent.inventory)
{
if (item.Key == "grail" && item.Value > 0)
{
grailCreated = true;
break;
}
}