Добрый день, при написании программы в Prologe появляется строчка getSpiral(_, _, _, _) = []. Но данный предикат нигде не описан и естественно программа выдае ошибку. Можно уточнить, что это за предикат и где и как его необходимо описать |
Игра "Жизнь"
Описание игры. Создание клеточного поля
Ниже описываются правила игры "Жизнь" и создается клеточное поле.
Правила игры
Игра "Жизнь" ведется на клеточном поле. Каждая клетка поля находится в одном из двух состояний — живая или пустая. Игра состоит в итеративной смене одного поколения живых клеток другим по определенным правилам, которые называются правилами Конвея. На нулевом шаге итерации имеется некоторая начальная конфигурация живых клеток, которые составляют первое поколение. На последующих шагах состояние каждой клетки определяется количеством ее живых соседей. Соседними считаются клетки, имеющие хотя бы одну общую вершину.
Правила Конвея имеют вид:
- если пустая клетка имеет ровно три живых соседа, то она становится живой;
- если живая клетка имеет менее двух или более трех живых соседей, то она становится пустой;
- в остальных случаях состояние клетки не изменяется.
Исследователи игры "Жизнь" обычно занимаются созданием и изучением поведения конфигураций, которые обладают теми или иными свойствами: не изменяются с течением времени; периодически воспроизводят самих себя, в том же самом месте или в некотором другом месте; периодически порождают новые живые клетки, и так далее.
Ниже создается клеточное поле, состоящее из 30 строк и 50 столбцов и, таким образом, содержащее 1500 клеток. Рассматриваются два варианта поля — ограниченное клеточное поле, и "тор". Тором называется поле, в котором противолежащие границы отождествляются. Если, например, живая клетка движется влево, то, после достижения границы окна, она появится справа.
Создадим проект lifeGame (MDI). Для игры необходимо создать окно, в котором размещается игровое поле, состоящее из клеток. Соответственно, ниже будут созданы диалоговое окно (Dialog) и два элемента управления — клетка (Draw Control) и игровое поле (Control). Клетки размещаются на игровом поле.
Создание клетки
Выделим корень дерева проекта, откроем с помощью команды меню New In New Package диалоговое окно CreateProject Item, выберем элемент Draw Control и назовем его cellControl.
Клетка обладает следующими свойствами:
- состояние — пустая или живая;
- номер строки;
- номер столбца.
Соответственно, в интерфейсе cellControl следует объявить эти свойства, а также предикат, который устанавливает состояние клетки.
domains state = alive; empty. properties i : integer. % номер строки j : integer. % номер столбца state : state. % состояние predicates setCellState: (state).Листинг 8.1. Свойства клетки
В имплементации класса cellControl следует определить объявленные свойства и предикат, а также выбрать цвет для различных состояний клетки.
constants borderColor : color = color_WhiteSmoke. % цвет границы emptyCellColor : color = color_Lavender. aliveCellColor : color = color_VioletRed. facts i : integer := 0. j : integer := 0. state : state := empty. currentColor : color := emptyCellColor. clauses setCellState(State):- state := State, currentColor := getColor(State), invalidate(). predicates getColor: (state) -> color. clauses getColor(alive) = aliveCellColor:- !. getColor(_) = emptyCellColor.Листинг 8.2. Определение основных параметров
Клетка создается пустой.
Добавим в редакторе окна cellControl обработчик событий PaintResponder. Ниже приведено его определение.
clauses onPaint(_Source, Rectangle, GDI):- GDI:setPen(pen(1, ps_Solid, borderColor)), GDI:setBrush(brush(pat_Solid, currentColor)), GDI:drawRect(Rectangle).Листинг 8.3. Определение предиката onPaint
Создание диалогового окна
Подготовим поле для игры. С помощью диалогового окна CreateProject Item cоздадим элемент Control и назовем его gameControl. Закроем редактор окна gameControl.
Теперь создадим диалоговое окно gameDialog. В поле Title укажем название игры: "Игра Жизнь".
Поместим в окно следующие элементы управления (рис. 8.1 рис. 8.1):
- пользовательский элемент управления (Custom Control):
- Class: gameControl; Width: 500; Height: 300;
- три кнопки (Push Button):
- Name: step_ctl; Text: >;
- Name: start_ctl; Text: Старт;
- Name: clear_ctl; Text: Очистить;
- флажок (Check Box):
- Name: torus_ctl; Text: Тор.
Размер окна следует увеличить (растянуть его с помощью мыши), для того чтобы игровое поле с указанными размерами помещалось в него полностью. Далее следует закрыть редактор окна.
Изменим определение предиката onShow в имплементации класса taskWindow так, как показано ниже. В результате при запуске приложения сразу откроется окно gameDialog. По умолчанию оно является модальным. Приложение будет закрываться одновременно с окном gameDialog.
clauses onShow(_Source, _CreationData):- _MessageForm = messageForm::display(This), _ = gameDialog::display(This).Листинг 8.4. Определение предиката onShow класса taskWindow
Закрыть окно можно с помощью нажатия кнопки "Закрыть" ("крестик" в правом верхнем углу окна) или с помощью нажатия на кнопку Cancel.
Добавим в редакторе окна gameDialog обработчик события DestroyListener и обработчик нажатия на кнопку Cancel (Click Responder). Определяются они одинаково (см. ниже).
clauses onDestroy(_Source):- getParent():close().Листинг 8.5. Определение предиката onDestroy
clauses onCancelClick(_Source) = button::defaultAction:- getParent():close().Листинг 8.6. Определение предиката onCancelClick
Создание клеточного поля
Клеточное поле создается в классе gameControl (рис. 8.2 рис. 8.2).
Изменим раздел open имплементации класса gameControl следующим образом:
open core, vpiDomains, cellControl
Ниже приведено определение основных параметров.
constants m : integer = 30. % число строк n : integer = 50. % число столбцов width : integer = 10. % ширина ячейки height : integer = 10. % высота ячейки facts gameDlg : gameDialog := erroneous. previousState : cellControl* := []. % предыдущее состояние cellTree : redBlackTree::tree{tuple{integer, integer}, cellControl} := redBlackTree::emptyUnique().Листинг 8.7. Объявление основных параметров
Факт-переменная gameDlg хранит указатель на объект окна gameDialog. Факт-переменная previousState используется для хранения списка клеток, которые были живыми на предыдущем шаге. Факт-переменная cellTree предназначена для хранения индексов (номеров строк и столбцов) и указателей на объекты клеток поля. Для хранения используется красно-черное дерево.
Красно-черное дерево — это двоичное дерево поиска, которое пополнено пустыми листьями, так что каждая непустая вершина имеет двух потомков, и которое обладает следующими свойствами:
- каждая вершина имеет цвет — красный или черный;
- корень дерева черный;
- все листья черные;
- оба потомка красной вершины черные;
- ветви, исходящие из одной вершины, содержат одно и то же количество черных вершин, т. е. имеют одинаковую черную высоту.
Операция поиска элементов, хранящихся в вершинах красно-черного дерева, а также операции добавления и удаления вершин обладают логарифмической сложностью.
В языке Visual Prolog имеются классы redBlackTree, redBlackSet и другие, в которых реализованы средства, необходимые для хранения множеств в красно-черных деревьях.
В классе redBlackTree красно-черное дерево определяется как функция из множества ключей в множество значений. Вершины дерева хранят как ключи, так и значения. Поиск значений ведется по ключам. Когда создается дерево, то сначала берется пустое дерево, затем в него последовательно добавляются вершины так, что дерево всегда остается красно-черным (выполняются все необходимые свойства). Предикат empty/0 создает пустое дерево, в котором для одного ключа может храниться несколько значений в разных вершинах. Предикат emptyUnique/0 создает дерево, в котором разным ключам соответствуют разные значения; если для одного ключа добавляется несколько значений, то в дереве остается последнее.
В класcе redBlackSet дерево реализуется как множество уникальных элементов (ключи в нем отсутствуют).
Ключами в дереве cellTree являются пары индексов ячеек, а значениями — указатели на объекты этих ячеек. Создается дерево с уникальными ключами, в котором каждому ключу соответствует не более одной вершины (см. объявление факта-переменной cellTree).
Определение конструктора new/1 следует изменить так, как показано ниже.
clauses new(Parent):- new(), setContainer(Parent), % gameDlg := uncheckedConvert(gameDialog, Parent).Листинг 8.8. Изменение определения конструктора new
Клеточное поле создает конструктор new/0. Определение этого конструктора также следует изменить (см. ниже).
clauses new():- userControlSupport::new(), generatedInitialize(), % foreach I = std::cIterate(m), J = std::cIterate(n) do Cell = cellControl::new(This), Cell:i := I, Cell:j := J, cellTree := redBlackTree::insert(cellTree, tuple(I, J), Cell), Cell:setSize(width, height), Cell:setPosition(J * width, I * height) end foreach.Листинг 8.9. Создание прямоугольного клеточного поля
Предикат cIterate/1 недетерминированно возвращает для целого аргумента N значения от 0 до N – 1 включительно. Предикат insert/3 добавляет в красно-черное дерево новую вершину с заданными ключом и значением.
Правила Конвея
Начальная конфигурация живых клеток создается с помощью мыши. При клике по клетке ее состояние изменяется: пустая клетка становится живой, живая — пустой.
Создание начальной конфигурации живых клеток
Добавим в редакторе окна cellControl обработчик событий MouseDownListener. Ниже приведено его определение.
clauses onMouseDown(_Source, _Point, _ShiftControlAlt, _Button):- empty = state, !, setCellState(alive). onMouseDown(_Source, _Point, _ShiftControlAlt, _Button):- setCellState(empty).Листинг 8.10. Изменение состояния клетки с помощью мыши
Далее в интерфейсе gameControl следует объявить предикаты clear и step. Предикат clear очищает клеточное поле, так что все клетки становятся пустыми. Предикат step выполняет один шаг итерации в соответствии с правилами Конвея.
predicates clear: (). step: ().Листинг 8.11. Объявление предикатов в интерфейсе gameControl
Ниже приведено определение предиката clear.
clauses clear():- tuple(_, Cell) = redBlackTree::getAll_nd(cellTree), alive = Cell:state, Cell:setCellState(empty), fail. clear().Листинг 8.12. Очищение поля
Предикат getAll_nd/1 недетерминированно возвращает вершины красно-черного дерева в виде пар "ключ – значение". Предикат step будет определен позднее.
Поиск соседних клеток
Состояние клетки на очередном шаге итерации определяется количеством ее живых соседей. Ниже описывается предикат neighbor/1, который недетерминированно возвращает клетки, являющиеся соседними для заданной клетки. В определении этого предиката используется предикат isTorus, который будет определен позднее в классе gameDialog. Предикат isTorus возвращает одно из двух значений — true, в случае "тора", и false, в случае обычного ограниченного поля. Если его значение равно true, то количество соседей каждой клетки равно восьми. В противном случае клетки, лежащие на границе поля, имеют меньшее количество соседей.
predicates neighbor: (cellControl) -> cellControl nondeterm. neighbor: (integer I, integer J, boolean IsTorus, integer NeighborI [out], integer NeighborJ [out]) nondeterm. f: (integer I, boolean IsTorus, integer N) -> integer nondeterm. clauses neighbor(Cell) = Neighbor:- neighbor(Cell:i, Cell:j, gameDlg:isTorus(), I, J), Neighbor = redBlackTree::tryLookUp(cellTree, tuple(I, J)). neighbor(I, J, IsTorus, I, f(J, IsTorus, n)). neighbor(I, J, IsTorus, f(I, IsTorus, m), J). neighbor(I, J, IsTorus, f(I, IsTorus, m), f(J, IsTorus, n)). f(X, true, N) = (X + std::fromToInStep(-1, 1, 2)) mod N. f(X, false, _) = X - 1:- X > 0. f(X, false, N) = X + 1:- X < N - 1.Листинг 8.13. Вычисление соседних клеток
Предикат tryLookUp/2 находит в дереве вершину с заданным ключом и возвращает значение. В данном случае по индексам клетки возвращается указатель на объект этой клетки.
Реализация шага итерации
Ниже реализуется один шаг итерации в соответствии с правилами Конвея.
clauses step():- % собираем в список живые клетки AliveCells = [C || tuple(_, C) = redBlackTree::getAll_nd(cellTree), alive = C:state], % проверяем, что он не совпадает со списком на пред. шаге AliveCells <> previousState, !, % находим соседей живых клеток Neighbors = [NC || Cell in AliveCells, NC = neighbor(Cell)], % разбиваем на группы одинаковых Groups = list::decompose(Neighbors, {(X) = X}), % находим для каждой клетки количество живых соседей NList = list::map(Groups, {(tuple(X, L)) = tuple(X, list::length(L))}), % применяем правила Конвея list::forAll(NList, {(tuple(Cell, N)):- if empty = Cell:state, 3 = N then Cell:setCellState(alive) elseif alive = Cell:state, (N < 2; N > 3), ! then Cell:setCellState(empty) end if}), % находим список клеток, у которых нет живых соседей RestCells = list::difference(AliveCells, Neighbors), % изменяем их состояние на пустое list::forAll(RestCells, {(Cell):- Cell:setCellState(empty)}), % запоминаем новое состояние previousState := AliveCells. step():- % если ничего не изменилось, то выключаем таймер gameDlg:stop().Листинг 8.14. Реализация правил Конвея
Шаг итерации выполняется следующим образом. Формируется список AliveCells, содержащий живые клетки. Если он совпадает со списком живых клеток, полученных на предыдущем шаге (previousState), то таймер останавливается (см. второе предложение). Если не совпадает, то формируется список Neighbors клеток, соседних с живыми клетками. Каждая клетка попадает в него столько раз, сколько живых соседей она имеет. Далее с помощью предиката decompose/2 полученный список разбивается на группы — преобразуется в список Groups элементов вида tuple(Cell, [Cell, Cell, Cell]). Затем с помощью предиката map/2 списки одинаковых элементов заменяются их количеством, и получается список NList элементов вида tuple(Cell, N), которые содержат указатель на объект клетки и количество граничащих с ней живых клеток. Для элементов этого списка применяются правила Конвея.
Если живая клетка не граничит с другими живыми клетками, то она в список Neighbors не попадает. Такие клетки образуют список RestCells. В соответствии с правилами Конвея все они становятся пустыми.
Добавим для окна gameDialog обработчики событий нажатия на кнопки ">" ("шаг"), "Старт" и "Очистить", а также обработчик событий TimerListener.
При нажатии на кнопку ">" выполняется один шаг итерации.
clauses onStepClick(_Source) = button::defaultAction:- gameControl_ctl:step().Листинг 8.15. Определение предиката onStepClick
При нажатии на кнопку "Очистить" поле очищается и снимается флажок из поля "Тор".
clauses onClearClick(_Source) = button::defaultAction:- gameControl_ctl:clear(), torus_ctl:setChecked(false).Листинг 8.16. Определение предиката onClearClick
Запуск и остановка таймера
Таймер запускается при нажатии на кнопку "Старт". Надпись на этой кнопке при этом меняется на надпись "Стоп". При повторном нажатии на кнопку таймер останавливается, возвращается прежняя надпись.
clauses onStartClick(_Source) = button::defaultAction:- "Старт" = start_ctl:getText(), !, timer := timerSet(100), start_ctl:setText("Стоп"). onStartClick(_Source) = button::defaultAction:- stop().Листинг 8.17. Определение предиката onStartClick
Шаг итерации выполняется на каждый тик таймера.
clauses onTimer(_Source, _Timer):- gameControl_ctl:step().Листинг 8.18. Определение предиката onTimer
Объявим в интерфейсе gameDialog предикаты isTorus и stop. Предикат isTorus возвращает состояние флажка (true или false). Предикат stop останавливает таймер.
predicates isTorus: () -> boolean. stop: ().Листинг 8.19. Объявление предикатов в интерфейсе gameDialog
Остается объявить факт-переменную timer и определить предикаты isTorus и stop в имплементации класса gameDialog.
facts timer : timerHandle := erroneous. clauses isTorus() = torus_ctl:getChecked(). stop():- isErroneous(timer), !. stop():- timerKill(timer), timer := erroneous, start_ctl:setText("Старт").Листинг 8.20. Определение в имплементации класса gameDialog
Флажок в поле "Тор" можно ставить или снимать в любой момент времени. Это сразу повлияет на поведение конфигурации живых клеток.
На рис. 8.3 рис. 8.3 показаны конфигурации живых клеток, которые сходятся через несколько шагов к известным пульсарам — фигурам, которые периодически воспроизводят самих себя. Эти фигуры были показаны на рис. 8.2 рис. 8.2 слева (кембриджский пульсар с периодом 3) и справа вверху (пульсар с периодом 15). Исходные конфигурации нельзя получить одну из другой.
Рис. 8.3. Фигуры (a) и (b) сходятся к пульсару, показанному слева на рис. 8.2; фигуры (c) и (d) сходятся к пульсару, показанному на рис. 8.2 справа вверху
Упражнения
8.1. Определите предикат, который по заданной конфигурации живых клеток находит возможные конфигурации живых клеток (не содержащие изолированных клеток), из которых она могла появиться за один шаг.
8.2. Определите операцию вычисления периода, через который конфигурация живых клеток повторяется, и отображение его в строке заголовка окна.
8.3. Определите операции сохранения поколений и прокручивания поколений в обратном порядке.
8.4. Реализуйте игру "Жизнь" на поле, составленном из правильных шестиугольников.
8.5. Добавьте кнопку и числовое поле для задания случайной начальной конфигурации живых клеток. В поле вводится число живых клеток. По нажатию на кнопку живые клетки случайным образом расставляются на поле.