Бац…
…с этим звуком тяжелая дубинка соприкоснулась с чьей то головой. Тело дернулось и завалилось назад. Дело было сделано, никем неуслышанное, неувиденное: идеальный конец, идеальное решение, идеальная история.
Но, как говорят гномы, за любой бедой стоит тролль.
сэр Терри Праттчетт
Thud! Разумеется, я не мог пройти мимо этой игры. Не только потому, что мне очень нравятся произведения Терри Пратчета, но, главным образом, по той причине, что игра эта ни на что не похожа. Начнем с того, что играется она на восьмиугольной доске. Гномы сражаются с троллями (и последних существенно меньше). Как тролли могут победить, пребывая в меньшинстве? Очень просто — за один ход тролль может снять с доски несколько гномов. А гномам, даже действуя сообща, чтобы снять с доски одного тролля приходится попотеть.
Настольная игра, по произведению Пратчетта, была разработана в 2002 году Тревором Трураном и выпущена в продажу. С того самого момента, как я узнал про эту игру, мне очень хотелось в нее сыграть. Я долго и безуспешно искал ее компьютерную реализацию, но все, что я нашел, это рассуждения о том, что игра эта слишком сложная, чтобы компьютер мог в нее играть. Теперь, у меня есть возможность проверить это утверждение.
Для начала разберемся с правилами игры.
Как я уже сказал ранее, игра ведется на восьмиугольной доске, в центре которой расположена «Скала», занимающая одну клетку. В игре участвуют 8 троллей, с одной стороны и 32 гнома (Dwarfs) с другой. Задача каждой стороны — максимально уменьшить численность противника.
Игра ведется в два этапа (этот пункт правил существенным образом используется в сюжете одноименной книги). Каждый из игроков должен сыграть как за гномов, так и за троллей. Как правило, снять с доски все фигуры противника не удается. Партия заканчивается по договоренности игроков, после чего считаются очки. Каждый оставшийся на доске гном оценивается в 1 очко, тролль — 4 очка. Результаты двух сыгранных партий складываются. Кто набрал больше очков — тот и победил.
Гномы ходят на любое количество свободных клеток по вертикали, горизонтали или диагоналям (как ферзь в Шахматах), но, для того, чтобы снять с доски тролля, гном должен на него «запрыгнуть». Несколько гномов, образующих линию (по вертикали, горизонтали или диагонали), позволяют крайнему гному прыгнуть (в противоположном от линии направлении) на количество клеток меньшее или равное количеству гномов в линии (при условии того, что в конечной точке прыжка располагается тролль). Одиночно стоящий гном также образует линию и, таким образом, может прыгнуть на 1 клетку в любом направлении, сняв тролля (если ему конечно удалось подобраться к нему вплотную).
Тролли могут ходить на одну клетку в любом направлении (как король в Шахматах). Все гномы, оказавшиеся на расстоянии одной клетки от точки завершения хода, снимаются с доски. Таким образом, за один ход, тролль может снять с доски до 7 гномов (если очень сильно повезет). Для того, чтобы снять гнома с доски, тролль должен сходить (даже если вплотную к нему уже стоит гном). Тролли также могут выстраиваться в линии (хотя им это нужно в гораздо меньшей степени чем гномам). Линия из N троллей (по вертикали, горизонтали или диагонали) может «толкнуть» крайнего тролля на N (или менее) клеток, при условии, что, в результате этого хода, с доски будет снят хотя бы один гном.
Фигуры не могут перепрыгивать друг через друга или через «Скалу» в центре доски.
Можно заметить, что нам требуется подсчитывать количество фигур в «линии». Поскольку с подсчетом чего-бы то ни было в ZoG все очень непросто (скорее всего, я еще неоднократно буду говорить об этом), реализация этих правил станет, для нас, в некотором роде, испытанием, а заодно, позволит более полно проиллюстрировать возможности языка описаний ZRF. Начнем, впрочем, с простого:
(define board-defs
(image "imagesglukThud.bmp")
(grid
(start-rectangle 45 35 60 58)
(dimensions
("a/b/c/d/e/f/g/h/i/j/k/l/m/n/o" (35 0)) ; files
("15/14/13/12/11/10/9/8/7/6/5/4/3/2/1" (0 35)) ; ranks
)
(directions (n 0 -1) (e 1 0) (s 0 1) (w -1 0)
(nw -1 -1) (ne 1 -1) (se 1 1) (sw -1 1))
)
(kill-positions a15 b15 c15 d15 e15 k15 l15 m15 n15 o15
a14 b14 c14 d14 l14 m14 n14 o14
a13 b13 c13 m13 n13 o13
a12 b12 n12 o12
a11 o11
h8
a5 o5
a4 b4 n4 o4
a3 b3 c3 m3 n3 o3
a2 b2 c2 d2 l2 m2 n2 o2
a1 b1 c1 d1 e1 k1 l1 m1 n1 o1 )
(symmetry Black (n s)(s n) (ne sw) (sw ne) (nw se) (se nw))
)
В этом описании, по сравнению с тем, о чем я говорил в предыдущей статье, добавилось новое ключевое слова kill-positions. Этот раздел описания позволяет запретить ряд позиций доски, превратив ее, таким образом, из четырехугольной в восьмиугольную. В центре, как я и обещал, находится «Скала». Фигуры через нее проходить не могут. Фактически, это не часть игрового поля.
( define game-defs
( board
(board-defs)
)
( board-setup
(White (Dwarf f1 g1 i1 j1 e2 k2 d3 l3 c4 m4 b5
n5 a6 o6 a7 o7 a9 o9 a10 o10 b11 n11
c12 m12 d13 l13 e14 k14 f15 g15 i15 j15) )
(Black (Troll g7 h7 i7 g8 i8 g9 h9 i9) )
)
(loss-condition (White Black) stalemated)
(draw-condition (White Black) repetition)
)
Здесь ничего нового. Расставляем фигуры, определяем условия проигрыша (для каждой из сторон — невозможность сделать очередной ход) и ничьей (повторение позиции). Теперь, начинается самое интересное. Нам необходимо придумать способ подсчитывать фигуры в линии.
( define dwarf-3
( mark
(opposite $1) (verify friend?)
(opposite $1) (verify friend?)
back
$1 (verify empty?)
$1 (verify empty?)
$1 (verify enemy?)
add
)
)
Для того, чтобы прыгнуть фигурой на 3 клетки, я делаю следующее:
- Запоминаю текущую позицию командой mark
- Дважды двигаюсь в направлении противоположном указанному, проверяя наличие на клетке дружественной фигуры
- Возвращаюсь на исходную клетку, командой back
- Дважды двигаюсь в направлении хода, проверяя, что клетка пуста
- Двигаюсь еще раз, если на целевой клетке находится вражеская фигура
- Если все условия соблюдены, завершаю ход командой add (вражеская фигура убирается с доски автоматически)
Теперь гномы умеют прыгать на три клетки (при наличии подходящей линии), вынося с доски тролля. Чтобы помимо трех, гномы могли прыгать и на другое количество клеток, необходимо доопределить макросы dwarf-1, dwarf-2 и так далее, до dwarf-7 включительно. Далее макросы можно не определять, поскольку на большее количество клеток, на нашей доске, прыгнуть не получится (даже для того, чтобы имелась возможность прыгнуть на 7 клеток, гномам должно очень сильно повезти, так что, скорее всего, этот макрос никогда не будет использоваться). Это решение несколько многословно, но оно работает.
Помимо взятия, гномы должны уметь делать обычный ход (на любое количество клеток по указанному направлению). Здесь все просто:
( define shift
( $1
( while empty?
add
$1
)
)
)
Мы используем цикл. Пока клетки пустые, двигаемся в указанном направлении, добавляя возможные ходы. Это не означает, что мы будем ходить на все эти клетки. Мы просто сообщаем ядру ZoG, что на эти клетки можно ходить, остальное — его дело.
Реализация хода троллей несколько осложнена тем обстоятельством, что в конце хода они могут снимать с доски фигурки гномов, находящиеся на соседних клетках. Вот как выглядит реализация хода тролля на две клетки:
( define troll-2
( mark
(opposite $1) (verify friend?)
back
$1 (verify empty?)
$1 (verify empty?)
(verify (or (enemy? n) (enemy? nw) (enemy? s) (enemy? ne)
(enemy? w) (enemy? sw) (enemy? e) (enemy? se)))
(if (enemy? n) (capture n)) (if (enemy? nw) (capture nw))
(if (enemy? s) (capture s)) (if (enemy? ne) (capture ne))
(if (enemy? w) (capture w)) (if (enemy? sw) (capture sw))
(if (enemy? e) (capture e)) (if (enemy? se) (capture se))
add
)
)
Помимо проверки наличия подходящей линии, здесь осуществляется проверка того, что, в результате хода, будет взят хотя бы один гном. Также, перед завершением хода, добавлено взятие соседей по всем восьми направлениям (при условии того, что это вражеские фигуры). Следует отметить, что такие предикаты как enemy? или empty? допускают форму вызова с передачей параметра направления, что позволяет выполнить проверку, не перемещаясь на другую клетку. Зачастую, это очень удобно. В реализации хода на одну клетку, просто убираем лишние проверки:
( define troll-1
( $1
(verify empty?)
(if (enemy? n) (capture n)) (if (enemy? nw) (capture nw))
(if (enemy? s) (capture s)) (if (enemy? ne) (capture ne))
(if (enemy? w) (capture w)) (if (enemy? sw) (capture sw))
(if (enemy? e) (capture e)) (if (enemy? se) (capture se))
add
)
)
Осталось собрать игру воедино и убедиться, что все работает:
(game
(title "Thud")
(description "...")
(history "...")
(strategy "...")
(players White Black)
(turn-order White Black)
(game-defs)
(piece
(name Dwarf)
(image White "imagesglukd.bmp")
(description "d")
(moves
(dwarf-1 n) (dwarf-1 ne) (dwarf-2 n) (dwarf-2 ne)
(dwarf-1 e) (dwarf-1 nw) (dwarf-2 e) (dwarf-2 nw)
(dwarf-1 s) (dwarf-1 se) (dwarf-2 s) (dwarf-2 se)
(dwarf-1 w) (dwarf-1 sw) (dwarf-2 w) (dwarf-2 sw)
(dwarf-3 n) (dwarf-3 ne) (dwarf-4 n) (dwarf-4 ne)
(dwarf-3 e) (dwarf-3 nw) (dwarf-4 e) (dwarf-4 nw)
(dwarf-3 s) (dwarf-3 se) (dwarf-4 s) (dwarf-4 se)
(dwarf-3 w) (dwarf-3 sw) (dwarf-4 w) (dwarf-4 sw)
(dwarf-5 n) (dwarf-5 ne) (dwarf-6 n) (dwarf-6 ne)
(dwarf-5 e) (dwarf-5 nw) (dwarf-6 e) (dwarf-6 nw)
(dwarf-5 s) (dwarf-5 se) (dwarf-6 s) (dwarf-6 se)
(dwarf-5 w) (dwarf-5 sw) (dwarf-6 w) (dwarf-6 sw)
(dwarf-7 n) (dwarf-7 ne) (shift n) (shift ne)
(dwarf-7 e) (dwarf-7 nw) (shift e) (shift nw)
(dwarf-7 s) (dwarf-7 se) (shift s) (shift se)
(dwarf-7 w) (dwarf-7 sw) (shift w) (shift sw)
)
)
(piece
(name Troll)
(image Black "imagesglukT.bmp")
(description "T")
(moves
(troll-1 n) (troll-1 ne) (troll-2 n) (troll-2 ne)
(troll-1 e) (troll-1 nw) (troll-2 e) (troll-2 nw)
(troll-1 s) (troll-1 se) (troll-2 s) (troll-2 se)
(troll-1 w) (troll-1 sw) (troll-2 w) (troll-2 sw)
(troll-3 n) (troll-3 ne) (troll-4 n) (troll-4 ne)
(troll-3 e) (troll-3 nw) (troll-4 e) (troll-4 nw)
(troll-3 s) (troll-3 se) (troll-4 s) (troll-4 se)
(troll-3 w) (troll-3 sw) (troll-4 w) (troll-4 sw)
(troll-5 n) (troll-5 ne) (troll-6 n) (troll-6 ne)
(troll-5 e) (troll-5 nw) (troll-6 e) (troll-6 nw)
(troll-5 s) (troll-5 se) (troll-6 s) (troll-6 se)
(troll-5 w) (troll-5 sw) (troll-6 w) (troll-6 sw)
(troll-7 n) (troll-7 ne)
(troll-7 e) (troll-7 nw)
(troll-7 s) (troll-7 se)
(troll-7 w) (troll-7 sw)
)
)
)
Следует заметить, что тролли, в этой игре, получились очень сильными фигурами. Будь доска побольше, гномы успели бы убежать подальше и собраться в компактный блок, подойти к которому было бы уже не так просто. Но, на нашей доске, благодаря возможности прыжка, тролли добираются до гномов уже на 2-3 ходу. Я запускал эту игру в режиме управления компьютером за обе стороны на продолжительное время. В конечном итоге, с обоих сторон оставалось по 5 фигур, после чего тролли уже не могли догнать гномов, в силу мобильности последних. Допускаю, что человек мог бы сыграть за гномов лучше. С другой стороны, играя за троллей, компьютер с поставленной задачей безусловно справляется.
Исходники, как всегда, можно забрать на GitHub.
На всякий случай напоминаю, что запустить их на Demo-версии Zillions of Games не получится. Можете даже не пытаться.
Автор: GlukKazan