В предыдущей статье я рассказывал об уникальном, на мой взгляд, проекте Zillions of Games. Как я и обещал, я начинаю небольшой цикл учебных статей по описанию возможностей декларативного языка, предназначенного для разработки новых (и описания уже существующих) игр, в рамках этого проекта.
Для того чтобы не загромождать изложение излишними (на этом этапе) подробностями, я выбрал для реализации очень простую игру. Я играл в нее в детстве. Она называется «Волки и Козленок». Правила следующие: Волки (черные фигуры) — ходят на одну клетку по диагонали, только вперед. Козленок (белая фигура) — также ходит на одну клетку по диагонали, но в любую сторону. Задача белых — пройти на любую из четырех клеток своего цвета последней горизонтали. Задача черных — лишить белых возможности хода.
Поскольку на стандартной шахматной доске 8x8 черные побеждают элементарно, используем для игры доску 9x9 клеток. Эта игра очень простая (и нравится детям). При правильной игре, белые всегда побеждают.
Язык описания правил (ZRF) очень напоминает LISP. Как и в LISP-е, очень важно следить за балансом открывающихся и закрывающихся скобок (в противном случае, игра просто не загрузится). Также как и в LISP-е, комментарии начинаются с символа точки с запятой и продолжаются до конца строки.
; *****************************************************************
; *** Волки и Козленок
; *****************************************************************
(version "2.0")
Здесь, в комментарии, мы кратко описываем игру и указываем версию Zillions of Games, на которой она должна работать. На более младшей версии «движка» игра не запустится.
В ZRF, активно используются макросы. Они довольно просты, но могут использовать параметры. Следующая запись, представляет собой макрос, описывающий единственно возможный в нашей игре ход — на одну клетку в указанном направлении:
(define checker-shift ($1 (verify empty?) add))
При использовании в коде записи:
(checker-shift ???)
Макрос раскрывается следующим образом:
(??? (verify empty?) add)
Смысл передаваемого параметра и всей этой записи я объясню ниже. Пока, нам важно понимать, что макросы спасают нас от большого количества писанины и связанной с ней возможностью ошибок в описании.
Далее описывается доска. Ее описание также принято выносить в макрос:
(define board-defs
(image "imagesglukBoard.bmp")
(grid
(start-rectangle 2 2 48 48)
(dimensions
("a/b/c/d/e/f/g/h/i" (48 0)) ; files
("9/8/7/6/5/4/3/2/1" (0 48)) ; ranks
)
(directions (nw -1 -1) (ne 1 -1) (se 1 1) (sw -1 1))
)
(symmetry Black (ne sw) (sw ne) (nw se) (se nw))
(zone (name goals) (players White) (positions b9 d9 f9 h9) )
)
Смысл описания image понятен — это имя файла рисунка, загружаемого для отображения доски. Далее следует описание квадратной доски (grid). Следует заметить, что возможности ZRF не ограничиваются описанием прямоугольных досок. Используя этот язык, можно описывать треугольные и шестиугольные доски, можно описывать многомерные доски, вплоть до 5-ти измерений, можно «склеивать» края доски, определяя ее топологию и т.д. Мы не будем сейчас на этом останавливаться. Подробности таких описаний можно найти в chm-файле, описывающем язык ZRF (поставляемом вместе с игрой), а также в огромном количестве уже реализованных игр на всевозможных досках.
Для нашей игры важны три ключевые фразы описания grid: start-rectangle описывает каким образом доска «накладывается» на загруженный рисунок. Фраза dimensions — описывает измерения (у нас их два). Строки в начале описания каждого измерения важны — они описывают порядок нумерации клеток (определяя, таким образом, систему координат). Следующий далее параметр определяет, каким образом «линейка» измерения накладывается на доску. Следует сказать, что при использовании изображения доски, разработанного самостоятельно, могут возникнуть сложности с подбором этих числовых значений, а также значений, указанных в start-rectangle. Может потребоваться множество попыток, для того, чтобы изображения фигур попадали в нужные места на изображении доски.
Следующая, очень важная фраза (directions) определяет направления, в которых могут двигаться фигуры. Мы определяем четыре направления — северо-запад (nw), северо-восток (ne), юго-запад (sw) и юго-восток (se). Фразой symmetry, мы определяем правила, по которым эти направления преобразуются для черного игрока.
Последняя строка (zone) — определяет набор клеток, который мы будет использовать далее, при определении условия победы Белых.
(define game-defs
(board
(board-defs)
)
(board-setup
(White (WC e2) )
(Black (BC b9 d9 f9 h9) )
)
(win-condition (White) (absolute-config WC (goals)))
(loss-condition (White Black) stalemated)
)
В этом макросе, мы определяем условия игры. Фразой board задается ранее определенная нами доска (макрос board-defs раскрывается). В board-setup определяются начальные позиции игроков, после чего, фразами win-condition и loss-condition определяются условия победы и поражения для игроков. Условием победы для Белых мы определяем прохождение в ранее определенную нами зону, а условием поражения, для обеих сторон, отсутствие возможности сделать очередной ход.
Осталось определить игру:
(game
(title "Волки и Козленок")
(description "Провести белую фишку на последнюю горизонталь")
(players White Black)
(turn-order White Black)
(game-defs)
(piece
(name WC)
(image White "imagesglukW.bmp")
(description "Козленок")
(help "Ходит на 1 клетку по диагонали вперед и назад")
(moves
(checker-shift ne)
(checker-shift nw)
(checker-shift se)
(checker-shift sw)
)
)
(piece
(name BC)
(image Black "imagesglukB.bmp")
(description "Волк")
(help "Ходит на 1 клетку по диагонали только вперед")
(moves
(checker-shift ne)
(checker-shift nw)
)
)
)
Большинство из определений здесь интуитивно понятны. Стоит остановиться лишь на нескольких моментах. Фраза players описывает тех самых игроков White и Black, которых мы уже использовали ранее. Игроков может быть больше двух, а в головоломках может быть определен один игрок (имеется даже возможность определить игрока делающего случайные ходы, но это тема для отдельного разговора). Фраза turn-order определяет порядок очередности хода для игроков (он также может отличаться от простого чередования хода двух игроков).
Далее, после описания настроек игры (game-defs), следует описание фигур, используемых в игре (piece). Большинство описаний в них также понятны. В разделе moves перечисляются все возможные для фигуры ходы. Именно здесь мы используем макрос checker-shift, о котором говорили в самом начале. Как легко видеть, в качестве параметра, ему передается возможное направление движения фигуры. В результате развертывания макроса, получается что-то наподобие следующего:
( ne
(verify empty?)
add
)
Мы выполняем три шага — двигаемся в указанном направлении, проверяем, пуста ли клетка и ставим фигуру (здесь имеется сложный для понимания момент. Для себя, я считаю, что в начале хода фигура снимается с доски, а в конце хода возвращается на доску). Подробнее, о возможностях описания правил ходов я планирую рассказать в следующих статьях, на примере более сложных игр.
Новая игра готова. Желающие могут загрузить ее из репозитория на GitHub.
Автор: GlukKazan