Starbound/assets/devel/scripts/questgen/plannerTests.lua
2025-03-21 22:23:30 +11:00

484 lines
12 KiB
Lua

-- This script can be run either in starbound or in an external lua interpreter.
-- Run in starbound:
-- /spawnitem questgentest
-- Place the questgentest object down, point at it with the cursor and run:
-- /entityeval blocksWorld()
-- /entityeval campfire()
-- /entityeval recipes()
-- To run in an external interpreter:
-- cd assets/packed
-- lua -i ../devel/scripts/questgen/plannerTests.lua
-- > setupTests()
-- > blocksWorld()
-- > campfire()
-- > recipes()
function setupTests()
if sb then
require("/scripts/util.lua")
require("/scripts/questgen/util.lua")
require("/scripts/questgen/planner.lua")
else
require("scripts.util")
require("scripts.questgen.util")
require("scripts.questgen.planner")
sb = {}
sb.logInfo = function (...)
print(string.format(...))
end
end
end
function generatePlan(planner, initialState, goalState)
local co = coroutine.create(function () return planner:generatePlan(initialState, goalState) end)
local status, result
while coroutine.status(co) ~= "dead" do
status, result = coroutine.resume(co)
if not status then
sb.logInfo("Planner broke: %s", debug.traceback(co, result))
result = nil
end
end
return result
end
function blocksWorld()
local planner = Planner.new(20)
planner.debug = true
planner:setConstants({
Table = "Table",
A = "A",
B = "B",
C = "C"
})
function isBlock(entity)
return entity == "A" or entity == "B" or entity == "C"
end
planner:addRelations({
defineQueryRelation("Block", true) {
[case(1, NonNil)] = function (self, entity)
if xor(isBlock(entity), self.negated) then
return {{entity}}
else
return Relation.empty
end
end,
default = Relation.some
},
createRelation("Handempty"),
createRelation("Clear"),
defineRelation("On") {
contradicts = function (self, condition)
if self.negated then return false end
return condition:containsTerm(self.new(false, {self.predicands[2], self.predicands[1]}, self.context))
end
},
createRelation("Holding")
})
planner:addOperators({
pickup_from_table = {
preconditions = {
{"Block", "b"},
{"Handempty"},
{"Clear", "b"},
{"On", "b", "Table"}
},
postconditions = {
{"Holding", "b"},
{"!Handempty"},
{"!On", "b", "Table"}
}
},
pickup_from_block = {
preconditions = {
{"Block", "b"},
{"Handempty"},
{"Clear", "b"},
{"On", "b", "c"},
{"Block", "c"},
{"!=", "b", "c"}
},
postconditions = {
{"Holding", "b"},
{"Clear", "c"},
{"!Handempty"},
{"!On", "b", "c"}
}
},
putdown_on_table = {
preconditions = {
{"Block", "b"},
{"Holding", "b"}
},
postconditions = {
{"Handempty"},
{"On", "b", "Table"},
{"!Holding", "b"}
}
},
putdown_on_block = {
preconditions = {
{"Block", "b"},
{"Holding", "b"},
{"Block", "c"},
{"Clear", "c"},
{"!=", "b", "c"}
},
postconditions = {
{"Handempty"},
{"On", "b", "c"},
{"!Holding", "b"},
{"!Clear", "c"}
}
}
})
local initialState = planner:parseConjunction({
{"Handempty"},
{"Clear", "A"},
{"Clear", "C"},
{"On", "A", "B"},
{"On", "B", "Table"},
{"On", "C", "Table"}
})
local goalState = planner:parseConjunction({
{"On", "A", "B"},
{"On", "B", "C"}
})
generatePlan(planner, initialState, goalState)
planner:clearVariables()
end
function campfire()
local planner = Planner.new(20)
planner.debug = true
planner:setConstants({
Log = "Log"
})
planner:addRelations({
createRelation("Holding"),
createRelation("Handempty"),
createRelation("AtWoods"),
createRelation("AtCampfire"),
createRelation("Warm")
})
planner:addOperators({
collect = {
preconditions = {
{"AtWoods", "origCount", "x"},
{"Handempty"},
{"+", "newCount", 1, "origCount"}
},
postconditions = {
{"!Handempty"},
{"Holding", "x"},
{"!AtWoods", "origCount", "x"},
{"AtWoods", "newCount", "x"}
}
},
drop = {
preconditions = {
{"Holding", "y"},
{"AtCampfire", "origCount", "y"},
{"+", "origCount", 1, "newCount"}
},
postconditions = {
{"!AtCampfire", "origCount", "y"},
{"AtCampfire", "newCount", "y"},
{"!Holding", "y"},
{"Handempty"}
}
},
lightFire = {
preconditions = {
{"AtCampfire", "count", "Log"},
{">=", "count", 2}
},
postconditions = {
{"Warm"}
}
},
resetCampfire = {
preconditions = {},
postconditions = {
{"AtCampfire", 0, "y"}
}
}
})
local initialState = planner:parseConjunction({
{"AtWoods", 2, "Log"},
{"Handempty"}
})
local goalState = planner:parseConjunction({
{"Warm"}
})
generatePlan(planner, initialState, goalState)
planner:clearVariables()
end
function recipes()
local planner = Planner.new(20)
planner.debug = true
planner:setConstants({
Cake = "Cake",
Pancakes = "Pancakes",
Wheat = "Wheat",
Egg = "Egg",
Milk = "Milk",
Sugar = "Sugar",
Pearlpeas = "Pearlpeas",
Eggshoot = "Eggshoot"
})
local recipes = {
Cake = PrintableTable.new({
Wheat = 3,
Sugar = 1,
Milk = 1,
Egg = 2
}),
Pancakes = PrintableTable.new({
Wheat = 3,
Milk = 1,
Egg = 1,
Pearlpeas = 1,
Eggshoot = 2
})
}
local delicious = {
Cake = true,
Pancakes = true
}
local ingredients = {
Wheat = true,
Sugar = true,
Milk = true,
Egg = true,
Pearlpeas = true,
Eggshoot = true
}
planner:addRelations({
defineQueryRelation("have") {
[case(0, Any, 0)] = function (self)
return {self.predicands}
end,
default = Relation.empty
},
defineQueryRelation("isRecipe", true) {
[case(0, NonNil, NonNil)] = function (self, food, ingredients)
if xor(self.negated, recipes[food] == ingredients) then
return {{food, ingredients}}
end
return Relation.empty
end,
[case(1, NonNil, Nil)] = function (self, food)
if xor(self.negated, recipes[food]) then
return {{food, recipes[food]}}
end
return Relation.empty
end,
[case(2, Nil, Nil)] = function (self)
if self.negated then return Relation.some end
local results = {}
for food, ingredients in pairs(recipes) do
results[#results+1] = {food, ingredients}
end
return results
end,
[case(3, Nil, NonNil)] = Relation.some,
default = Relation.empty
},
defineRelation("haveRecipe") {
satisfiable = function (self)
return true
end,
implications = function (self)
return self:unpackPredicands {
[case(0, NonNil, Any)] = function (self, ingredients, vars)
local terms = {}
if vars == nil or vars == Nil then
vars = PrintableTable.new({ old = PrintableTable.new(), new = PrintableTable.new()})
self.predicands[2]:bindToValue(vars)
end
function variable(table, ingredient)
if not table[ingredient] then
table[ingredient] = planner:newVariable(ingredient)
end
return table[ingredient]
end
function addTerm(term)
if term.static then
term:applyConstraints()
end
terms[#terms+1] = term
end
if not self.negated then
-- We're in the preconditions.
-- Create a variable for the counts of each ingredient we
-- currently have that is at least the amount needed by the
-- recipe. The variable used is stashed in the magic predicand
-- so that it can be constrained against later.
--
-- For each ingredient, the implications are:
-- have(ingredient, oldCount)
-- >=(oldCount, amountNeededByRecipe)
-- +(newCount, amountNeededByRecipe, oldCount)
for ingredient, amountNeeded in pairs(ingredients) do
local oldCount = variable(vars.old, ingredient)
local newCount = variable(vars.new, ingredient)
addTerm(planner.relations.have.new(false, {ingredient, oldCount}, self.context))
addTerm(planner.relations[">="].new(false, {oldCount, amountNeeded}, self.context))
addTerm(planner.relations["+"].new(false, {newCount, amountNeeded, oldCount}, self.context))
end
else
-- We're in the postconditions, and the results of crafting
-- are being applied for each ingredient:
-- !have(ingredient, oldCount)
-- have(ingredient, newCount)
for ingredient, amountNeeded in pairs(ingredients) do
local oldCount = variable(vars.old, ingredient)
local newCount = variable(vars.new, ingredient)
addTerm(planner.relations.have.new(true, {ingredient, oldCount}, self.context))
addTerm(planner.relations.have.new(false, {ingredient, newCount}, self.context))
end
end
return Conjunction.new(terms)
end,
default = Conjunction.new()
}
end
},
defineQueryRelation("isDelicious", true) {
[case(0, NonNil)] = function (self, food)
if xor(self.negated, delicious[food]) then
return {food}
end
return Relation.empty
end,
[case(1, Nil)] = function (self)
if self.negated then return Relation.some end
local results = {}
for food,_ in pairs(delicious) do
results[#results+1] = {food}
end
return results
end,
default = Relation.some
},
defineQueryRelation("isIngredient", true) {
[case(0, NonNil)] = function (self, item)
if xor(self.negated, ingredients[item]) then
return {item}
end
return Relation.empty
end,
[case(1, Nil)] = function (self)
if self.negated then return Relation.some end
local results = {}
for item,_ in pairs(ingredients) do
results[#results+1] = {item}
end
return results
end,
default = Relation.some
},
createRelation("yum")
})
planner:addOperators({
fetch = {
preconditions = {
{">=", "count", 1},
{"isIngredient", "item"}
},
postconditions = {
{"have", "item", "count"}
},
objectives = {
{"have", "item", "count"}
}
},
bake = {
preconditions = {
{"have", "food", "foodCount"},
{"isRecipe", "food", "ingredients"},
{"haveRecipe", "ingredients", "magic"},
{"+", "foodCount", 1, "newFoodCount"},
{">=", "foodCount", 0}
},
postconditions = {
{"!haveRecipe", "ingredients", "magic"},
{"have", "food", "newFoodCount"}
},
objectives = {
{"have", "food", "newFoodCount"}
}
},
eat = {
preconditions = {
{"have", "foodA", "countA"},
{"isDelicious", "foodA"},
{"+", "newCountA", 1, "countA"},
{">=", "countA", 1},
{"have", "foodB", "countB"},
{"isDelicious", "foodB"},
{"+", "newCountB", 1, "countB"},
{">=", "countB", 1},
{"!=", "foodA", "foodB"}
},
postconditions = {
{"!have", "foodA", "countA"},
{"have", "foodA", "newCountA"},
{"!have", "foodB", "countB"},
{"have", "foodB", "newCountB"},
{"yum"}
},
objectives = {
{"yum"}
}
}
})
local initialState = planner:parseConjunction({})
local goalState = planner:parseConjunction({
{"yum"}
})
generatePlan(planner, initialState, goalState)
planner:clearVariables()
end