DemonScript: Волк, коза и капуста
Постановка задачи
Решаем стандартную головоломку о перевозке голодных волка, козы и капусты из места $1 в место $2. Если волк и коза находятся в одном месте без фермера, то волк съедает козу. Аналогичная, ситуация для козы и капусты. Первоначально все объекты находятся в месте $1. Фермеру необходимо перевезти волка, козу и капусту в место $2 без потерь. Для перевозки он может взять только один объект.
Мы рассмотрим обобщение этой задачи, в котором места $1 и $2 могут быть связаны через граф других мест, число фермеров и животных произвольно и кто кого может съесть определяется начальными условиями задачи.
Знания об объектах
Будем использовать следующие отношения:
| X on Y | означает, что объект X расположен в месте Y; |
| X hold Y | - объект X (это всегда будет человек) имеет ("держит" в руках) объект Y (это будет животное); |
| X want Y | - истинно, если животное X готово съесть животное Y; |
| X link Y | - истинно, если из места X есть дорога в место Y. |
edges on, hold, want // находится в месте, иметь объект, хотеть съесть edges link // два места связаны дорогой
Для удобства введём три абстрактных класса: людей, животных (к которым будем относить и капусту) и класс мест. Эти классы храним в графе Sen:
var Sen = GRAPH("Sen") // создаём граф абстрактных классов
Sen.nodes $human, $animal, $place // человек, животное, место
global Sen // граф будет доступен в демонах
Создадим граф начального состояния Start с узлами, соответствующими фермеру, животным и двум местам:
var Start = GRAPH("Start") // граф начального состояния
GRAPH = Start // делаем текущим и добавляем узлы:
nodes f, w, g, c // фермер, волк, коза, капуста
nodes $1, $2 // различные места мира
Start.verbose(0) // не будет вывода информации об изменении рёбер
Зададим "знания" о сущностях мира (принадлежность к классам, кто кого готов съесть и наличие дорог между местами):
[f] isa Sen=>$human; // принадлежность к классам isa [w,g,c] isa Sen=>$animal; [$1,$2] isa Sen=>$place; w want g; g want c; // кто кого хочет $1 link $2; $2 link $1; // геометрия мира: 1<->2Дальнейший код универсален и работа скрипта будет полностью зависеть от сделанных выше определений. Можно увеличить число мест, животных или фермеров, определив отношения между ними.
Поместим всех людей и животных в начальное положение:
for X in X.isa(Sen=>$human) | X.isa(Sen=>$animal): X on $1; // все в месте $1
Основные действия
Определим вспомогательного демона, который возвращает положение (место) объекта X в состоянии, описываемом графом G. В нём используется функция exists, которая ищет первое Z для которого условие, записанное во втором аргументе функции, будет истинным. Если такого Z нет, демон вернёт логическое значение Undef (все демоны так поступают, если не сработал ни один return):
def place(G, X)
{
GRAPH = G
if exists(Z, X on Z) : return Z
}
Основным действуюшим агентом в задаче является человек (фермер в базовой постановке). Он может взять животное, положить его и перейти в другое место. Каждое действие опишем демоном, который будет возвращать логическое значение: True - если действие может быть выполнено (и оно тогда совершается) или False - если действие выполнено быть не может (оно тогда и не делается)
Демон взятия животного A человеком H в состоянии G имеет вид:
def take(G, H, A)
{
GRAPH = G
if exists(Z, H hold Z) : return False // H уже кого-то носит
var P = place(G, H) // место, где находится человек H
if (A on P) != True : return False // H и A в разных местах
H hold A; // человек берёт A
A on P = Undef // A уже не в P (а у человека)
return True
}
Демон, который избавляет человека H от его ноши в состоянии G имеет вид:
def put(G, H)
{
GRAPH = G
if !exists(Z, H hold Z) : return False // никого не носит
H hold Z = Undef // человек теперь не имеет Z
var P = place(G, H)
Z on P; // Z там же, где человек
return True
}
Обратим внимание на строку H hold Z = Undef.
Можно было бы написать False, но тогда в графе начнут появляться False
рёбра hold. Это приведёт к росту различных состояний, которые по факту являются одним и тем же состоянием.
Поясним это на примере. Создадим копию G начального состояния Start.
Пусть фермер f берёт и сразу кладёт волка w:
var G = Start.copy() take(G, f, w); put(G, f) out G == StarОчевидно, что ничего не поменялось и при наличии Undef графы будут равны (G==Start истинно). Однако, если Undef заменить на False, то сравнение вернёт ложь. Стоит вывести структуру графов Start и G в обоих случаях.
Демон перемещения человека H в место P2 чуть более сложный. Он не допускает этого действия, если в покидаемом месте остаются животные, готовые друг друга съесть и там нет других людей:
def move(G, H, P2)
{
GRAPH = G
var P1 = place(G, H) // где сейчас находится человек H
if P1 == P2 : return False // уже тут и находится
if (P1 link P2) != True : return False // нет пути P1 -> P2
// если людей больше в P1 нет:
if !exists(F, F.isa(Sen=>$human) & (F on P1) & F != H) :
for A in A.isa(Sen=>$animal){ // по всем носимым животным
if (A on P1) != True : continue
for B in (A want B) & (B on P1) :
return False // нельзя оставлять животных и их жертв
}
H on P1 = Undef; // покидаем P1
H on P2; // переезжаем в P2
return True
}
Генератор переходов
При поиске решения нам потребуется создавать все возможные состояния (графы) в которые можно перейти из состояния G. Этим занимается демон all_actions(G), который возвращает массив графов, совершая последовательно все разрешённые в состоянии G действия. После каждого успешного действия по .copy создаётся граф Next в котором осуществляется очередное действие. Этот граф добавляется в массив Grpahs, который возвращает демон. В имени графа Next (метод .name) мы сохраняем действие, накапливая их последовательность:
def all_actions(G)
{
GRAPH = G
var Graphs = []
var Next = G.copy();
for H in H.isa(Sen=>$human) { // по всем людям
if put(Next, H) { // может положить, что имеет
Next.name(Next.name()+";"+H.string()+".p");
Graphs.push(Next)
Next = G.copy()
}
for A in A.isa(Sen=>$animal) : // по всем животным
if take(Next, H, A) { // его можно взять
Next.name(Next.name()+";"+H.string()+".t."+A.string());
Graphs.push(Next)
Next = G.copy()
}
for P in P.isa(Sen=>$place) : // по всем местам
if move(Next, H, P) { // туда можно перейти
Next.name(Next.name()+";"+H.string()+".m."+P.string());
Graphs.push(Next)
Next = G.copy()
}
}
return Graphs
}
Последний вспомогательный демон будет выяснять, является ли состояние G целевым. Это происходит, если все люди и животные собрались на финальном месте $2 и люди совершили действие put (выложили животное)
def target_state(G)
{
GRAPH = G
for X in X.isa(Sen=>$human) | X.isa(Sen=>$animal) {
if (X on $2) != True : return False // не все на финальном месте
if exists(Z, X hold Z): return False // некоторые не положены
}
return True
}
Поиск решения
Поиск решения головоломки проведём в ширину. Это не самый эффективный метод, однако он гарантированно найдёт наилучшее решение. Как обычно, при помощи массивов, вводим две очереди. В очереди Open будем хранить состояния в которых дальше планируется совершить действия. В очереди Close сохраняем все состояния в которых мы уже были. Начинаем поиск со стартового состояния Start:
def solver(Start)
{
var Open = [] // очередь состояний для обследования
var Close = [] // состояния в которых уже были
Open.push(Start) // помещаем стартовое
Close.push(Start)
while !Open.empty() { // пока очередь не пустая
var G = Open.pop() // извлекаем из конца
var Gs = all_actions(G) // графы после всех возможных действий
for Gi in Gs { // бежим по ним
if target_state(Gi) { // это целевое состояние
out "Close:",Close.size()
return Gi // конец поиска
}
if Close.is(Gi) : // в этом состоянии уже были
continue
Open.unshift(Gi) // в начало очереди (поиск в ширину)
Close.push(Gi) // запоминаем, что уже были
}
}
}
Тестирование
Запустим решатель для начального состояния:
var Finish = solver(Start) out Finish==Undef ? "Can't find solution" : Finish.name()На экране появится:
f.t.g;f.m.$2;f.p;f.m.$1;f.t.w;f.m.$2;f.p; f.t.g;f.m.$1;f.p;f.t.c;f.m.$2;f.p;f.m.$1;f.t.g;f.m.$2;f.pЭто означает, что фермер берёт козу , едет в место $2, кладёт её и т.д.
Можно добавить ещё одного фермера:
GRAPH = Start // делаем текущим Start nodes s // добавляем в него узел $s isa Sen=>$human; // объявляем его человеком $s on $1; // отправляем на первое место Finish = solver(Start) // запускаем решатель out Finish.name()Результат будет несколько короче:
f.t.w;f.m.$2;f.p;f.m.$1;f.t.g;f.m.$2;f.p;$s.t.c;$s.m.$2;$s.p
Если стоит цель найти не одно, решение, можно в цикле for демона solver заменить return Gi на
out Gi.name(); Close.push(Gi); continueВпрочем, все возможные решения не будут найдены, так как в Close мы сохраняем состояния в которых мы уже были. Однако, попасть в данное состояние можно различным образом. Если мы уже попадали в состояние, то все другие способы попадания в него будут блокированы. Ситуацию можно несколько изменить, если упомянутые выше Undef заменить на False (но не в демоне put, который приводит к слишком большому ветвлению). Впрочем, классическая задача имеет только одно решение.
Полный код этого примера находится в файле wolf.ds.