Приручаем ZoG (Часть 3: Футбол Кумской долины)

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

Приручаем ZoG (Часть 3: Футбол Кумской долины)
         Но мы говорим: здесь в этой пещере на краю света, дварфы и тролли заключили мир, чтобы рука об руку пройти под дланью Смерти.

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

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

В предыдущей статье, я рассказывал об оригинальной настольной игре, разработанной Тревором Трураном, по мотивам произведений знаменитого английского писателя Терри Пратчетта. В 2005 году, Труран разработал новую игру, использующую тот же набор фигур, на той же доске. Название этой игры — "Koom Valley Thud" и, сегодня, я постараюсь ее реализовать, попутно рассказав о тех возможностях языка ZRF, о которых не успел рассказать ранее.

Напомню, что ZRF — это язык описания игровых правил (напоминающий Lisp), используемый Zillions of Games. Несмотря на то, что в нём имеются определенные сложные моменты, в целом, он довольно прост и может быть освоен любым человеком, даже очень далеким от программирования. Главным достоинством игрового ядра ZoG является его универсальность. Описав правила мы, фактически сразу, получаем новую игру. Хотя AI ZoG уступает специализированным игровым движкам, играет он, на удивление, сильно. К сожалению, бесплатная демонстрационная версия приложения позволяет запускать лишь ограниченный набор игр и не позволяет загружать ZRF-описания собственной разработки.

Как обычно, начну с правил игры. Доска для Koom Valley Thud имеет единственное отличие от оригинальной доски Thud — центральное поле доступно для ходов фигур. «Скала», расположенная на нем в начале игры, может перемещаться по полю. Цель игры гномов — перетащить «скалу» на последнюю горизонталь доски. Троллям, для того чтобы победить, необходимо окружить «Cкалу» как минимум тремя фигурами, причем, сделать это они должны на своем ходу.

Ходы фигур претерпели более серьезные изменения. Тролли, в этой игре, могут перемещаться на одну, две или три клетки в любом направлении (по вертикали, горизонтали или диагоналям). Взятие фигур противника осуществляется по правилам Шашек — тролль перепрыгивает через гнома (расположенного на соседней клетке) и снимает его фигуру с доски, останавливаясь на следующей клетке. Также как в Шашках, цепочка взятий может быть продолжена, но, в отличии от них, взятие осуществляется по желанию игрока. Также как перемещения, взятия могут осуществляться в любом направлении. Через свои фигуры и «Скалу» тролли перепрыгивать не могут.

Гномы, как и раньше, могут ходить на любое количество клеток по вертикали, горизонтали или диагоналям (как Ферзь в Шахматах). Для того чтобы снять с поля фигуру тролля, гномы должны «окружить» его с двух сторон (в этом отношении, игра напоминает Тафл, с тем отличием, что «окружать» можно и по диагоналям). За ход, с доски могут быть сняты несколько троллей (если все они были «окружены» гномами). Вместо хода фигурой, гномы могут переместить «Скалу» на одну клетку в любом направлении. Обязательным условием для такого перемещения является соседство с фигуркой гнома как в начальной, так и в конечной точке перемещения (соседом не обязательно должен быть один и тот же гном). Гномы не могут перепрыгивать через фигуры.

Осмыслив правила, можно приступить к их описанию на ZRF. Я не буду приводить здесь макросы описания доски и игры. Отличия их от Thud минимальны, кроме того, полное описание игры всегда можно посмотреть на GitHub. Сосредоточимся на менее очевидных вещах. Со стороны троллей, нам требуется описать возможности двух принципиально разных ходов (со взятием и без). При этом, если выполняется взятие фигуры, следующим ходом может быть только взятие. Делается это следующим образом:

Вначале, описываем обычный ход тролля
( define kv-troll 
  ( $1 
    (verify empty?) 
    add
    $1
    (verify empty?) 
    add
    $1
    (verify empty?) 
    add
  )
)

Здесь сказано, что фигура может ходить по пустым клеткам на одну, две или три клетки, в указанном направлении.

Далее, описываем ход со взятием

( define kv-troll-capturing
  ( $1
    (verify (piece? Dwarf) )
    capture
    $1
    (verify empty?)
    (add-partial capturing-type)
  )
)

Мы двигаемся в указанном направлении и, если на клетке расположен гном, снимаем его с доски. Двигаемся далее, если следующая клетка пустая, и завершаем ход новой директивной add-partial. В отличии от add, она позволяет выполнить частичный ход. В качестве параметра, директиве add-partial передается тип хода, к которому должно принадлежать продолжение. Типы ходов определяются в описании фигуры:

Типы ходов

   (piece
      (name Troll)
      (image Trolls "imagesglukT.bmp")
      (description "Troll")
      (moves
         (move-type capturing-type)
            (kv-troll-capturing n) (kv-troll-capturing ne)
            (kv-troll-capturing e) (kv-troll-capturing nw)
            (kv-troll-capturing s) (kv-troll-capturing se)
            (kv-troll-capturing w) (kv-troll-capturing sw)
         (move-type non-capturing-type)
            (kv-troll n) (kv-troll ne)
            (kv-troll e) (kv-troll nw)
            (kv-troll s) (kv-troll se)
            (kv-troll w) (kv-troll sw)
      )
   )

На уровне описания игры (game), директивой move-priorities можно задать приоритеты для типов ходов, сделав взятие обязательным (как в Шашках), но мы этого делать не будем, предоставляя возможность выбора игроку. Ход гнома описывается просто:

Ход гнома
( define check-troll
  ( if (enemy? $1)
    mark
    $1
    ( if (piece? Dwarf $1) 
      capture
    )
    back
  )
)

( define kv-dwarf
  ( $1 
    ( while empty? 
      ( if on-board?
        (check-troll n) (check-troll nw)
        (check-troll s) (check-troll sw)
        (check-troll w) (check-troll ne)
        (check-troll e) (check-troll se)
        add
      )
      $1 
    )
  )
)

Завершая ход, мы проверяем все направления, на предмет наличия тролля, «окруженного» гномом с противоположной стороны и снимаем с доски найденные фигуры. Стоит обратить внимание на то, что предикат piece? может принимать два параметра — тип фигуры и направление (что позволяет выполнить проверку не перемещаясь на соседнюю клетку). Осталось описать перемещение «Скалы»:

Двигаем "Скалу"

( define rock
  ( (verify (or (piece? Dwarf n) (piece? Dwarf nw) 
                (piece? Dwarf s) (piece? Dwarf sw) 
                (piece? Dwarf w) (piece? Dwarf ne) 
                (piece? Dwarf e) (piece? Dwarf se) ) )
    $1
    (verify empty?)
    (verify (or (piece? Dwarf n) (piece? Dwarf nw) 
                (piece? Dwarf s) (piece? Dwarf sw) 
                (piece? Dwarf w) (piece? Dwarf ne) 
                (piece? Dwarf e) (piece? Dwarf se) ) )
    add
  )
)

Главная сложность, как обычно, связана с подсчетом. Нам необходимо определить условие победы троллей, как соседство «Скалы» с тремя их фигурами. Этого можно было бы добиться, используя директиву relative-config, аналогично тому, как это сделано в реализации крестиков-ноликов, но нам потребовалось бы описать все взаимные расположения четырех соседствующих фигур (что само по себе является довольно интересной комбинаторной задачей) и, что самое главное, такое условие срабатывало бы как на ходе троллей, так и на ходе гномов (что противоречит условиям игры).

Есть другой способ

( define check-proximity
  ( if (on-board? $1)
    ( if (friend? $1)
        ( if (flag? is-second)
            change-owner
          else
            ( set-flag is-second true )
        )
    )
  )
)

( define check-rock-direction
  ( set-flag is-second false )
  ( if (on-board? $1)
    $1
    ( if (and enemy? (piece? Rock) ) 
      ( check-proximity $2 )
      ( check-proximity $3 )
      ( check-proximity $4 )
      ( check-proximity $5 )
      ( check-proximity $6 )
      ( check-proximity $7 )
      ( check-proximity $8 )
    )
    $5
  )
)

( define check-rock
  ( check-rock-direction n  ne e  se s  sw w  nw)
  ( check-rock-direction ne e  se s  sw w  nw n)
  ( check-rock-direction e  se s  sw w  nw n  ne)
  ( check-rock-direction se s  sw w  nw n  ne e)
  ( check-rock-direction s  sw w  nw n  ne e  se)
  ( check-rock-direction sw w  nw n  ne e  se s)
  ( check-rock-direction w  nw n  ne e  se s  sw)
  ( check-rock-direction nw n  ne e  se s  sw w)
)

Этот макрос позволяет, выполнив ход троллем, осуществить проверку, не соседствует ли с конечной точкой хода «Скала», соседствующая также с двумя другими троллями. Используется он следующим образом:

Измененные ходы троллей

( define kv-troll 
  ( $1 
    (verify empty?) 
    (check-rock)
    add
    $1
    (verify empty?) 
    (check-rock)
    add
    $1
    (verify empty?) 
    (check-rock)
    add
  )
)

( define kv-troll-capturing
  ( $1
    (verify (piece? Dwarf) )
    capture
    $1
    (verify empty?)
    (check-rock)
    (add-partial capturing-type)
  )
)

Если условие соседства с тремя троллями выполняется, мы меняем владельца «Скалы» командой change-owner. Теперь мы можем легко сформулировать условие поражения гномов, как потерю всех фигур типа «Скала»:

(loss-condition (Dwarfs) (pieces-remaining 0 Rock))

В реализации макроса, также стоит обратить внимание на работу с флагом командами flag? и set-flag. Флаги позволяют хранить состояние в процессе генерации хода. Помимо глобальных флагов, имеется возможность связывать булевские значения с позициями доски (командой set-position-flag), а также с фигурами (командой set-attribute). Сразу замечу, что любые попытки использовать флаги для хранения глобальных состояний, наподобие этой, обречены на неудачу.

В первую очередь, это связано с тем, что флаги и флаги позиций сбрасываются в начале каждого хода, но главная причина заключается в том, что использовать их можно только в процессе расчета ходов, а порядок вызова макросов при этом не определен (что приводит к нежелательным побочным эффектам). Помимо этого, разработчики ZoG рекомендуют не злоупотреблять позиционными флагами, так как их использование негативно влияет на производительность. В принципе, для хранения состояния между ходами, можно использовать атрибуты фигур, но этот способ весьма трудоемок.

Осталось описать игру. Поскольку мы добавляем новый вариант игры к уже существующему ZRF-описанию, используется ключевое слово variant:

Koom Valley Thud

(variant
   (title "Koom Valley Thud")
   (description "...")
   (history "...")
   (strategy "") 
   (players Dwarfs Trolls)
   (turn-order Dwarfs Trolls)
   (kv-game-defs)
   (piece
      (name Rock)
      (image Dwarfs "imagesglukR.bmp"
             Trolls "imagesglukR.bmp" )
      (description "Rock")
      (moves
          (rock w) (rock sw)
          (rock e) (rock se)
          (rock n) (rock nw)
          (rock s) (rock ne)
      )
   )
   (piece
      (name Dwarf)
      (image Dwarfs "imagesglukd.bmp")
      (description "Dwarf")
      (moves
         (kv-dwarf ne) (kv-dwarf n)
         (kv-dwarf nw) (kv-dwarf s)
         (kv-dwarf se) (kv-dwarf e)
         (kv-dwarf sw) (kv-dwarf w)
      )
   )
   (piece
      (name Troll)
      (image Trolls "imagesglukT.bmp")
      (description "Troll")
      (moves
         (move-type capturing-type)
         (kv-troll-capturing n) (kv-troll-capturing ne)
         (kv-troll-capturing e) (kv-troll-capturing nw)
         (kv-troll-capturing s) (kv-troll-capturing se)
         (kv-troll-capturing w) (kv-troll-capturing sw)
         (move-type non-capturing-type)
         (kv-troll n) (kv-troll ne)
         (kv-troll e) (kv-troll nw)
         (kv-troll s) (kv-troll se)
         (kv-troll w) (kv-troll sw)
      )
   )
)

Мы можем добавить в меню вариантов разделители, используя следующую конструкцию:

(variant
   (title "-")
)

Запустив это игру на выполнение, можно заметить, что гномы заняты, в основном, беготней от троллей (как и в предыдущей игре, тролли получились слишком сильными) и времени на перемещение «Скалы» у них не остается. Поскольку я, как и старина Моркоу, выступаю за мир между гномами и троллями, я воспользовался гибкостью ZoG и внес в правила небольшие изменения:

Я запретил все взятия вражеских фигур

( define shift 
  ( $1 
    ( while empty? 
      add 
      $1
    )
  )
)

( define kvb-troll 
  ( $1 
    (verify empty?) 
    (verify (on-board? $1) ) 
    (check-rock)
    add
    $1
    (verify empty?) 
    (verify (on-board? $1) ) 
    (check-rock)
    add
    $1
    (verify empty?) 
    (verify (on-board? $1) ) 
    (check-rock)
    add
  )
)

И разрешил двигать мяч (пусть это будет мяч) более чем на одну клетку

( define ball
  ( ( verify (or (piece? Dwarf n) (piece? Dwarf nw) 
                 (piece? Dwarf s) (piece? Dwarf sw) 
                 (piece? Dwarf w) (piece? Dwarf ne) 
                 (piece? Dwarf e) (piece? Dwarf se) ) 
    )
    $1
    ( while empty?
      ( if (or (piece? Dwarf n) (piece? Dwarf nw) 
               (piece? Dwarf s) (piece? Dwarf sw) 
               (piece? Dwarf w) (piece? Dwarf ne) 
               (piece? Dwarf e) (piece? Dwarf se) )
        add
      )
      $1
    )
  )
)

Кроме того, как можно заметить, я запретил троллям подходить к краю доски. Если этого не сделать, у троллей появляется беспроигрышная стратегия — заполнить последнюю горизонталь своими фигурами, устроив оставшимися троллями (их как раз будет три) охоту за мячом. В Koom Valley Thud это также возможно, но там троллей можно есть (к слову сказать, компьютер, под управлением ZoG, до такой тактики ни разу не додумался).

Результат получился, на мой взгляд, неплохим. Игра осталась достаточно сложной и позиционной, став более динамичной. Гномы (под управлением компьютера) теперь легко справляются с задачей (что говорит о том, что я слишком уж облегчил им жизнь) и, после некоторого количества головокружительных пасов через все поле, закатывают мяч в ворота. Желающие могут посмотреть, как это выглядит, на видео:

Автор: GlukKazan

Источник

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


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