Приручаем ZoG (Часть 2: Бац!)

в 5:10, , рубрики: Без рубрики

Приручаем ZoG (Часть 2: Бац!)
Бац…

…с этим звуком тяжелая дубинка соприкоснулась с чьей то головой. Тело дернулось и завалилось назад. Дело было сделано, никем неуслышанное, неувиденное: идеальный конец, идеальное решение, идеальная история.

Но, как говорят гномы, за любой бедой стоит тролль.

сэр Терри Праттчетт

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 клетки, я делаю следующее:

  1. Запоминаю текущую позицию командой mark
  2. Дважды двигаюсь в направлении противоположном указанному, проверяя наличие на клетке дружественной фигуры
  3. Возвращаюсь на исходную клетку, командой back
  4. Дважды двигаюсь в направлении хода, проверяя, что клетка пуста
  5. Двигаюсь еще раз, если на целевой клетке находится вражеская фигура
  6. Если все условия соблюдены, завершаю ход командой 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
  )
)

Осталось собрать игру воедино и убедиться, что все работает:

Thud!

(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

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js