Погоня с препятствиями

     
               Какая медлительная страна! — сказала Королева. — Ну, а здесь,
    знаешь ли, приходится бежать со всех ног, чтобы только остаться
    на том же месте! Если же хочешь попасть в другое место, тогда
    нужно бежать по меньшей мере вдвое быстрее! 
     
                            Льюис Кэрролл "Алиса в Зазеркалье


    Сегодня, я хочу рассказать об удивительной и недооценённой игре, с которой я познакомился чуть менее двух лет назад. В каком-то смысле, именно с этой игры, а также с Ура, началось моё знакомство с Дмитрием Скирюком. В те дни я только начинал интересоваться настольными играми. Мои познания были скудны и, во многом, наивны. Такие игры как "Чейз", буквально открыли для меня новый необъятный мир. Даже сейчас, работа над этой игрой, в большой степени, напоминает детективную историю. В этом отношении, игра "Chase" полностью оправдала как своё название так и сходство с псевдонимом известного американского писателя.

    Игра была разработана Томом Крушевски и выпущена в продажу компанией «TSR» в 1986 году. Помимо специальной доски, у каждого из игроков имеется по 10 шестигранных игральных кубиков, но несмотря на это игра не является азартной. Кубики бросаются всего один раз, для определения очерёдности хода и в дальнейшем используются лишь в качестве фигур. Количество очков на верхней грани показывает число шагов, на которое может быть передвинут кубик. Так кубик с одним очком может быть перемещён на соседнее поле, в любом из шести направлений, с двумя очками — на два поля по прямой, с тремя — на три и т.д. Кубик должен перемещаться ровно на указанное число шагов, не больше и не меньше. В процессе перемещения, кубик не поворачивается другой стороной (количество очков на верхней грани не изменяется). Начальная расстановка показана ниже:


    Подробнее о правилах
    Для каждого из игроков, общее количество очков, на верхних гранях, составляет 25. Игрок обязан поддерживать эту сумму до конца игры. Игроки ходят по очереди и если один из них забирает одну (или две, такое тоже возможно) фигуры, его противник обязан добавить сумму очков, выбывших из игры, к своему кубику с минимальным количеством очков (в начале игры, это одна из единичек). Если после этого распределены не все очки, остаток распределяется далее, всегда начиная с кубика с наименьшим количеством очков. Игрок, у которого остаётся менее 5 кубиков — проигрывает, поскольку не может распределить необходимое количество очков по оставшимся на доске кубикам.


    Границы доски не препятствуют движению фигур. Левая и правая границы доски «склеены» между собой, а от верхней и нижней границ фигуры отскакивают «рикошетом». Разумеется, это не означает, что фигуры движутся беспрепятственно. Фигуры не могут «перепрыгивать» друг друга, а также центральное поле "Chamber". Для взятия фигуры противника, фигура должна «встать» на неё (шахматное взятие), выполнив полное количество шагов по прямой. Ход может закончится и на фигуре своего цвета. В этом случае происходит "bumping" — фигура оказавшаяся на целевом поле смещается на один шаг, продолжая направление движения (с учётом склеенности доски и рикошетов). Если следующее поле также оказалось занято своей фигурой, "bumping" распространяется далее, до первого пустого поля или поля занятого фигурой противника (вражеская фигура забирается). Только одно препятствие может сделать такой ход невозможным — запрещено «задвигать» фигуры в центральную клетку, используя bumping.

    Можно заметить, что из начальной позиции, каждый из игроков может циклически сдвинуть все свои фигуры, сходив любой из единичек в сторону двойки. Подобный ход разрешён правилами. Также допускается «обмен» очками между фигурами одного цвета, находящимися на соседних полях. Так пара из 5 и 2 может превратиться в 4 и 3 или даже в 1 и 6. Такое действие считается ходом. Не рассмотренным остался всего лишь один тип хода. Ни одна из фигур не может пройти сквозь центральное поле доски (Chamber), но она может закончить своё движение на этом поле. Если это произошло, фигура «расщепляется» на две, с сохранением суммарного количества очков. Фигура всегда разделяется таким образом, чтобы очки одной из полученных фигур превышали очки другой не более чем на 1. Общее количество фигур, у каждого из игроков, не может превысить 10 (именно на этот случай, в начале игры, каждый из игроков имеет 1 кубик в резерве).


    Направления «разлёта» осколков напоминают остриё стрелы. Кубик с большим числом очков (если такой есть) всегда уходит в левую сторону. В двух особых случаях «расщепление» невозможно. Во первых, как я уже сказал выше, количество кубиков одного цвета не может превышать 10. Кроме того, совершенно очевидно, что расщепить кубик с 1 очком не удастся. В обоих этих случаях, кубик, вошедший в Chamber, выходит неизменным, по левому направлению. Каждая из фигур, покинувших Chamber, может инициировать bumping, попав на свою фигуру или взять фигуру противника (только таким способом можно взять две вражеских фигуры одновременно).

    Должен сказать, что Tom Kruszewski и «TSR» сильно переоценили возможности своей потенциальной аудитории. Для массового потребителя, игра оказалась слишком сложной (шахматы не менее сложны, но к ним все привыкли). Производитель прекратил выпуск продукции и, в настоящее время, «Чейз» можно приобрести лишь с рук, на различных ярмарках, аукционах и распродажах. Тем не менее, эта игра по праву считается одной из лучших игр 20-го столетия.

    Простая работа


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

    Вот как это выглядит
    (board
       (image "../Images/Chase/board.bmp")
       (grid
         (start-rectangle 48 32 108 82)
         (dimensions
             ("a/b/c/d/e/f/g/h/i/j/k/l/m" (60 0))
             ("1/2/3/4/5/6/7/8/9" (-30 52))
         )
         (directions (se 1 1) (w 1 0) (sw 0 1)
                     (nw -1 -1) (e -1 0) (ne 0 -1))
       )
       (kill-positions
          j1 k1 l1 m1 j2 k2 l2 m2 a3 k3 l3 m3 
          a4 k4 l4 m4 a5 b5 l5 m5 a6 b6 l6 m6 
          a7 b7 c7 m7 a8 b8 c8 m8 a9 b9 c9 d9
       )
    )
    

    Я не сторонник того, чтобы детали модели смешивались с вопросами визуализации, но до тех пор, пока не требуется отделить одно от другого (например отобразить доску в «честном» 3D, а не изометрии) такой подход вполне работает. Рассмотрим это описание подробнее:

    • Неотъемлемой частью описания является файл, содержащий изображение доски. Все геометрические размеры и позиции фигур привязаны к нему (именно по этой причине, большую часть дистрибутива моей реализации "Сокобана" составляют чёрные прямоугольники различных форм и размеров). Файл содержащий изображение доски в BMP-формате (ZoG понимает только этот формат) определяется ключевым словом image. Здесь можно определить сразу несколько файлов (для обеспечения возможности переключения между скинами), но лишь с идентичными геометрическими пропорциями.
    • Ключевое слово grid позволяет описать n-мерный массив позиций. В большинстве случаев, это привычная двумерная доска, но также можно определять и доски другой размерности (вплоть до пяти). Доска может состоять из нескольких grid-ов, при условии того, что обеспечивается уникальное именование отдельных позиций. При большом желании, можно даже размещать один grid поверх другого, наподобие того как это сделано в "Квантовых крестиках-ноликах".
    • Размер «ячейки» и расположение сетки определяются ключевым словом start-rectangle. Две пары целых чисел задают экранные координаты (x, y) левого верхнего и правого нижнего угла самой первой (левой верхней) ячейки.
    • Далее следует описание «измерений» (dimensions). Каждое описание содержит строку имён (из которых декартовым произведением комбинируются имена позиций), а также два целых числа. В этих числах и заключается «магия», позволяющая описывать гексагональные и изометрические доски. Это ни что иное как сдвиги, на которые смещаются очередные ячейки сетки. Обычно (для двумерных досок), в одном из измерений, ячейки смещаются на ширину ячейки по x, а в другом — на высоту ячейки по y, но дополнительно смещая эти ячейки на половину ширины по x, можно получить превосходную основу для гексагональной доски.
    • Вторая составляющая «магии» grid-ов — направления (directions). Доска — это не только позиции, но и связи (именованные и однонаправленные) между ними. Конечно, никто не мешает определить каждую связь индивидуально, задав имя и пару позиций для каждого соединения, но при определении досок больших размеров, этот процесс не будет весел. Ключевое слово directions позволяет манипулировать не именами позиций, а направлениями внутри сетки.
    • Чтобы получить доску требуемой формы, мы берём «прямоугольную» доску большего размера, а затем смещаем ряды на половину ячейки друг относительно друга. В результате остаются «лишние» позиции, которые необходимо «отрезать» от доски. Ключевое слово kill-positions позволяет объявить ранее определённое имя позиции недействительным. Разумеется, вместе с удаляемыми позициями разрываются и соответствующие им соединения.


    Использование ключевого слова grid позволяет существенно снизить объём ручной работы при описании «типовых» досок, но такой подход не лишён определённых недостатков. Во первых, если изображение доски не рисовалось под выбранные геометрические размеры специально, оперируя лишь целочисленными координатами и смещениями, бывает сложно выровнять расположение всех позиций доски идеально. Индивидуальное описание позиций менее лаконично, но позволяет корректировать их расположение независимо друг от друга. Вместе с тем, оно требует просто убийственного объёма ручной работы (с учётом необходимости исправления всех допущенных опечаток). Чтобы как-то облегчить этот процесс, я использую grid для «чернового» описания, после чего получаю индивидуальное описание позиций, при помощи небольшого скрипта:

    Скрипт
    my @grid;
    my %kp;
    my $sx, $sy, $dx, $dy;
    my $dm = 0;
    
    while (<>) {
      if (/\(start-rectangle\s+(\d+)\s+(\d+)\s+(\d+)\s+(\d+)\)/) {
         $sx = $1; 
         $sy = $2;
         $dx = $3 - $1;
         $dy = $4 - $2;
      }
      if (/\(\"([^\"]+)\"\s+\((-?\d+)\s+(-?\d+)\)\)/) {
         my @a = split(/\//, $1);
         $grid[$dm]->{ix} = \@a;
         $grid[$dm]->{x}  = $2;
         $grid[$dm]->{y}  = $3;
         $dm++;
      }
      if (/\(kill-positions/) {
         $fl = 1;
      }
      if ($fl) {
         if (/\s(([a-z0-9]{1,2}\s+)+)/i) {
            my @a = split(/\s+/, $1);
            foreach my $p (@a) {
               $kp{$p} = 1;
            }
         }
         if (/\)/) {
            $fl = 0;
         }
      }
    }
    
    sub try {
      my ($ix, $pos, $x, $y) = @_;
      if ($ix < $dm) {
         my $i = 0;
         foreach my $p (@{$grid[$ix]->{ix}}) {
            try($ix + 1, $pos . $p, $x + $i * $grid[$ix]->{x}, $y + $i * $grid[$ix]->{y});
            $i++;
         }
      } else {
         if (!$kp{$pos}) {
             my $a = $sx + $x;
             my $b = $sy + $y;
             my $c = $a + $dx;
             my $d = $b + $dy;
             print "             ";
             printf "($pos %3d %3d %3d %3d)\n", $a, $b, $c, $d;
         }
      }
    }
    
    try(0, '', 0, 0);
    


    Результат
          (positions  
                 (a1  48  32 108  82)
                 (a2  18  84  78 134)
                 (b1 108  32 168  82)
                 (b2  78  84 138 134)
                 (b3  48 136 108 186)
                 (b4  18 188  78 238)
                 (c1 168  32 228  82)
                 (c2 138  84 198 134)
                 (c3 108 136 168 186)
                 (c4  78 188 138 238)
                 (c5  48 240 108 290)
                 (c6  18 292  78 342)
                 (d1 228  32 288  82)
                 (d2 198  84 258 134)
                 (d3 168 136 228 186)
                 (d4 138 188 198 238)
                 (d5 108 240 168 290)
                 (d6  78 292 138 342)
                 (d7  48 344 108 394)
                 (d8  18 396  78 446)
                 (e1 288  32 348  82)
                 (e2 258  84 318 134)
                 (e3 228 136 288 186)
                 (e4 198 188 258 238)
                 (e5 168 240 228 290)
                 (e6 138 292 198 342)
                 (e7 108 344 168 394)
                 (e8  78 396 138 446)
                 (e9  48 448 108 498)
                 (f1 348  32 408  82)
                 (f2 318  84 378 134)
                 (f3 288 136 348 186)
                 (f4 258 188 318 238)
                 (f5 228 240 288 290)
                 (f6 198 292 258 342)
                 (f7 168 344 228 394)
                 (f8 138 396 198 446)
                 (f9 108 448 168 498)
                 (g1 408  32 468  82)
                 (g2 378  84 438 134)
                 (g3 348 136 408 186)
                 (g4 318 188 378 238)
                 (g5 288 240 348 290)
                 (g6 258 292 318 342)
                 (g7 228 344 288 394)
                 (g8 198 396 258 446)
                 (g9 168 448 228 498)
                 (h1 468  32 528  82)
                 (h2 438  84 498 134)
                 (h3 408 136 468 186)
                 (h4 378 188 438 238)
                 (h5 348 240 408 290)
                 (h6 318 292 378 342)
                 (h7 288 344 348 394)
                 (h8 258 396 318 446)
                 (h9 228 448 288 498)
                 (i1 528  32 588  82)
                 (i2 498  84 558 134)
                 (i3 468 136 528 186)
                 (i4 438 188 498 238)
                 (i5 408 240 468 290)
                 (i6 378 292 438 342)
                 (i7 348 344 408 394)
                 (i8 318 396 378 446)
                 (i9 288 448 348 498)
                 (j3 528 136 588 186)
                 (j4 498 188 558 238)
                 (j5 468 240 528 290)
                 (j6 438 292 498 342)
                 (j7 408 344 468 394)
                 (j8 378 396 438 446)
                 (j9 348 448 408 498)
                 (k5 528 240 588 290)
                 (k6 498 292 558 342)
                 (k7 468 344 528 394)
                 (k8 438 396 498 446)
                 (k9 408 448 468 498)
                 (l7 528 344 588 394)
                 (l8 498 396 558 446)
                 (l9 468 448 528 498)
                 (m9 528 448 588 498)
          )
    


    Это лишь половина дела! Имена позиций доски необходимо поправить, чтобы привести их в соответствие с общепринятой нотацией. Кроме того, требуется связать пары позиций направлениями, не забыв «зациклить» доску по краям. Всё вместе вылилось в немаленький объём ручной работы, но я не стал писать под это дело скрипт (хотя возможно и стоило).

    Сон разума


    Хоть я и познакомился с «Чейзом» довольно давно, поиграть в него, до последнего времени, никак не удавалось. Очень уж причудливая для этого требуется доска. При некоторой сноровке, можно играть на доске Сёги (9x9), но её у меня тоже не было. Обычная шахматная доска (8x8) для этой игры непригодна совершенно. Доску для «Чейза» удалось приобрести на прошлом "Зилантконе", но кубики в комплект не входили. Своё приобретение я забросил на дальнюю полку и там бы оно вероятно и провалялось, если бы в дело не вмешался случай.

    Случайности не случайны
    Мою дочку пригласила на день рожденья семья, с которой мы давно и крепко дружим. В качестве подарка, была выбрана настольная игра, а поскольку нескольким взрослым предстояло просидеть около трёх часов в детском кафе, их (и меня) тоже требовалось чем-то занять. В качестве возможного варианта, была предложена другая игра, но поскольку я предпочитаю игры более абстрактные, то решил с собой тоже что нибудь принести. Первоначально, я подумал об Уре, но в имевшемся у меня комплекте, его D2 «кости», выполненные в форме полукруглых палочек (более характерных для Сенета), были довольно неудобны, при броске производили много шума и могли помешать окружающим.

    Тут-то я и вспомнил про «Чейз». Предстояло пополнить его комплект двадцатью игральными кубиками, но поскольку я всё равно направлялся в магазин настольных игр (за подарком), это (как мне тогда казалось) не являлось проблемой. На сайте, я присмотрел себе замечательные полупрозрачные кубики (по 70 рублей за штуку), но жизнь внесла коррективы. В магазине выяснилось, что присмотренные мной кубики имеются лишь в одном экземпляре. Что я могу сказать, Казань — не Москва, пришлось удовольствоваться бюджетным вариантом и набирать вожделенные кубики из предложенной продавцом россыпи ико-, доде- и прочих -аэдров. Красный или зелёный комплект собрать не удалось, но синие и белые (ладно, ладно, один слегка желтоватый) кубики в наличии имелись.

    Правила я, разумеется, переврал (рассказывал о памяти). В моём изложении, траектории разлёта «осколков», на выходе из «репликатора», напоминали не наконечник стрелы, а скорее латинскую букву 'Y'. По всей видимости, определённую роль сыграло её сходство со схемами распада элементарных частиц. «Осколки» двигались не на одну клетку (как в оригинальном варианте правил), а в соответствии с их «номиналом». Кроме того, такой ход было гораздо легче заблокировать. Любые препятствия (будь то фигура, стоящая на пути разлёта «осколков» или наличие на доске десяти фигур) трактовались как невозможность выполнения хода. В оригинальной версии правил, заблокировать "Chamber" можно лишь установив фигуру на пути входа в него.

    Другим звеном "испорченного телефона" послужил сам Дмитрий. В своём описании «Чейза» он упомянул, что фигура, выполнившая взятие, имеет право на повторный ход (по аналогии с Шашками). В первоисточнике не было ни слова об этом (о чём ему не преминул сообщить уважаемый Гест), но я, в тот момент, не обратил на это внимания. Надо сказать, идея скрестить «Чейз» с «Шашками» уже тогда вызывала много вопросов. Следовало ли распространять правило повторного хода на случай bumping-а? На «осколки», полученные при разделении фигуры? Что следовало делать если взятие выполнял каждый из осколков? А если то же с bumping-ом? Но, нет таких сложностей, которых мы не могли бы себе создать! Я с энтузиазмом принялся за работу…

    Закат Солнца вручную
    Разумеется, в первую очередь, я попытался использовать механизм частичных ходов, используемый в ZoG для игр, наподобие шашек. Совсем недавно он мне здорово пригодился, в процессе создания очень непростой игры. До сих пор, мне не приходилось использовать его в Axiom, но всё когда-то бывает в первый раз. Суть частичного хода в том, что сложный, составной ход разбивается на мелкие шажки. В шашках, взятие фигуры противника реализовано именно таким частичным ходом. При этом, используются ещё и, так называемые, «режимы» выполнения хода, позволяющие указать, что следующий частичный ход также обязан выполнить взятие.

    Я не в восторге от реализации составных ходов в ZoG и вот почему. Прежде всего, в понимании ZoG частичные ходы — это именно отдельные, независимые действия. По сути, это просто набор ходов, выполняемых одним и тем же игроком, друг за другом. Мы не можем передавать какую либо промежуточную информацию между частичными ходами! Глобальные и позиционные флаги автоматически сбрасываются, в начале каждого хода. Это дьявольски неудобно, но это лишь часть беды! ZoG не может рассматривать составной ход как единую сущность (в частности, именно по этой причине пришлось вводить хардкодную опцию "maximal captures", для реализации «правила большинства». Какие-то другие идеи, не укладывающиеся в этот хардкод, реализовать уже не удастся!



    Это фрагмент партии из игры "Mana", придуманной Клодом Лероем. Количество чёрточек, на каждой позиции, показывает, на сколько шагов может переместиться фигура. Должно быть выполнено точное число шагов и, при этом, в процессе движения нельзя поворачиваться назад. Тут-то нас и поджидает засада! Очень редко, но бывает так, что фигура, выполнив два шага, загоняет себя «в тупик». Она не может продолжить движение, поскольку ей мешают другие фигуры и обязана сделать ещё один шаг, поскольку должна завершить ход! А ZoG, в свою очередь, не предоставляет ровно никаких средств, чтобы решить эту проблему!

    Другим ограничением является то, что составной ход может продолжать лишь та же самая фигура, которая перемещалась предыдущим частичным ходом. Именно так всё и происходит в шашках, но в «Чейзе» ситуация немного сложнее. Например, взятие может быть осуществлено при помощи bumping-а, то есть не той фигурой, которая выполняла ход! С Chamber-ходом всё ещё сложнее. Оба осколка могут взять фигуры противника и, по логике, имеют право выполнить следующий частичный ход. И обе они не являются той фигурой которая заходила в Chamber (той фигуры, на доске, вообще уже нет)!

    Меньше слов - больше кода
    : val ( -- n )
    	piece-type mark -
    ;
    
    : mirror ( 'dir  -- 'dir )
    	DUP ['] nw = IF
    		DROP ['] sw
    	ELSE
    		DUP ['] ne = IF
    			DROP ['] se
    		ELSE
    			DUP ['] sw = IF
    				DROP ['] nw
    			ELSE
    				['] se = verify
    				['] ne
    			ENDIF
    		ENDIF
    	ENDIF
    ;
    
    : step ( 'dir  -- 'dir )
    	DUP EXECUTE NOT IF
    		mirror
    		DUP EXECUTE verify
    	ENDIF
    ;
    
    : bump ( 'dir -- )
    	BEGIN
    		here E5 <> verify
    		friend? here from <> AND IF
    			piece-type SWAP step SWAP
    			create-piece-type
    			FALSE
    		ELSE
    			TRUE
    		ENDIF
    	UNTIL DROP
    ;
    
    : slide ( 'dir n -- )
    	alloc-path !
    	val SWAP BEGIN
    		step
    		SWAP 1- DUP 0= IF
    			TRUE
    		ELSE
    			my-empty? verify
    			SWAP FALSE
    		ENDIF
    	UNTIL DROP
    	from here move
    +	enemy? IF
    +		cont-type partial-move-type
    +	ENDIF
    	bump enemy? IF
    		alloc-all
    	ELSE
    		alloc-path @ 0= verify
    	ENDIF
    	add-move
    ;
    


    В конечном счёте, всё сводится к добавлению вызова partial-move-type при взятии вражеской фигуры (до выполнения bumping-а). Ограничения, о которых я говорил выше, остаются в силе. Мы не можем выполнить частичный ход, если взятие было осуществлено не той фигурой которая начала ход (в результате bumping-а или «расщепления» в Chamber), но даже в таком виде, этот код был бы неплохим решением. Если бы он заработал:


    Я так и не смог расшифровать этот ребус и просто отослал код разработчику Axiom. Грег пока не ответил, но вроде бы работает над выпуском патча, который, я надеюсь, решит проблему. Странно здесь то, что частичные ходы в Axiom действительно работают! Более того, они существенно расширяют функциональность ZRF. Всё это хорошо описано в документации и используется в нескольких приложениях. Видимо, мне просто не повезло.

    Поскольку частичные ходы не работали, пришлось искать другой способ решения проблемы. Если не удаётся выполнить все действия в рамках одного хода, можно попробовать растянуть их на несколько ходов! Я уже делал так в других играх, создавая на доске специальную невидимую позицию, на которой размещалась фигура-флаг. Если фигура принадлежала противнику, игрок знал, что должен пропустить свой ход. Это небольшое изменение, но оно потянуло за собой другие. Мне пришлось помечать фигуры, продолжающие ход (теперь это могли быть не только фигуры, начавшие ход), а также усложнить порядок передачи хода. В целом, это было довольно громоздкое и очень неуклюжее решение.

    Результатом моих усилий стала весьма оригинальная модификация игры, к сожалению имевшая слишком мало общего с оригиналом. Кроме того, использование «сложного» порядка передачи ходов (turn-order) наотмашь било по «интеллекту» AI. Используемый им минимаксный алгоритм крайне негативно реагирует на подобные вольности, а в «иммунном» к ним search-engine (альтернативном варианте построения Axiom AI) невероятно сложно реализовать поиск в глубину.

    По хлебным крошкам


    Хорошо, допустим мы, своим ходом, забираем одну (или даже две фигуры) противника, после чего, распределяем полученные очки по оставшимся его фигурам, обязательно начиная с младших. Но как быть, если младших фигур несколько? Например, в самом начале игры, у каждого из игроков имеется по две «единички». Взяв любую фигуру номиналом от одного до пяти очков, мы получим два варианта распределения очков и ход игры может серьёзным образом измениться, в зависимости от того, какой из них мы выберем.

    Те же и комбинаторика
    Здесь, практически на ровном месте, возникает интересная комбинаторная задача. Для того, чтобы понять, какими способами (при взятии фигуры) могут распределяться очки, необходимо представлять себе все сочетания фигур (на стороне одного из игроков), способные появиться в игре. Есть всего три условия:

    1. Каждая фигура может иметь номинал от 1 до 6 очков
    2. Количество фигур не может превышать 10
    3. Суммарное количество очков всегда равно 25

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

    Скрипт
    my @d;
    my %s;
    
    sub out {
      my ($deep) = @_;
      for (my $i = 0; $i < $deep; $i++) {
          print "$d[$i]";
      }
      print "\n";
    }
    
    sub dice {
      my ($start, $deep, $sum) = @_;
      if ($sum == 25) {
          out($deep);
      }
      if ($deep < 10 && $sum < 25) {
         for (my $i = $start; $i <= 6; $i++) {
             $d[$deep] = $i;
             dice($i, $deep + 1, $sum + $i);
         }
      }
    }
    
    dice(1);
    


    Результат
    1111111666
    1111112566
    1111113466
    1111113556
    1111114456
    1111114555
    1111122466
    1111122556
    1111123366
    1111123456
    1111123555
    1111124446
    1111124455
    111112666
    1111133356
    1111133446
    1111133455
    1111134445
    111113566
    1111144444
    111114466
    111114556
    111115555
    1111222366
    1111222456
    1111222555
    1111223356
    1111223446
    1111223455
    1111224445
    111122566
    1111233346
    1111233355
    1111233445
    1111234444
    111123466
    111123556
    111124456
    111124555
    1111333336
    1111333345
    1111333444
    111133366
    111133456
    111133555
    111134446
    111134455
    11113666
    111144445
    11114566
    11115556
    1112222266
    1112222356
    1112222446
    1112222455
    1112223346
    1112223355
    1112223445
    1112224444
    111222466
    111222556
    1112233336
    1112233345
    1112233444
    111223366
    111223456
    111223555
    111224446
    111224455
    11122666
    1112333335
    1112333344
    111233356
    111233446
    111233455
    111234445
    11123566
    111244444
    11124466
    11124556
    11125555
    1113333334
    111333346
    111333355
    111333445
    111334444
    11133466
    11133556
    11134456
    11134555
    11144446
    11144455
    1114666
    1115566
    1122222256
    1122222346
    1122222355
    1122222445
    1122223336
    1122223345
    1122223444
    112222366
    112222456
    112222555
    1122233335
    1122233344
    112223356
    112223446
    112223455
    112224445
    11222566
    1122333334
    112233346
    112233355
    112233445
    112234444
    11223466
    11223556
    11224456
    11224555
    1123333333
    112333336
    112333345
    112333444
    11233366
    11233456
    11233555
    11234446
    11234455
    1123666
    11244445
    1124566
    1125556
    113333335
    113333344
    11333356
    11333446
    11333455
    11334445
    1133566
    11344444
    1134466
    1134556
    1135555
    1144456
    1144555
    115666
    1222222246
    1222222255
    1222222336
    1222222345
    1222222444
    122222266
    1222223335
    1222223344
    122222356
    122222446
    122222455
    1222233334
    122223346
    122223355
    122223445
    122224444
    12222466
    12222556
    1222333333
    122233336
    122233345
    122233444
    12223366
    12223456
    12223555
    12224446
    12224455
    1222666
    122333335
    122333344
    12233356
    12233446
    12233455
    12234445
    1223566
    12244444
    1224466
    1224556
    1225555
    123333334
    12333346
    12333355
    12333445
    12334444
    1233466
    1233556
    1234456
    1234555
    1244446
    1244455
    124666
    125566
    133333333
    13333336
    13333345
    13333444
    1333366
    1333456
    1333555
    1334446
    1334455
    133666
    1344445
    134566
    135556
    1444444
    144466
    144556
    145555
    16666
    2222222236
    2222222245
    2222222335
    2222222344
    222222256
    2222223334
    222222346
    222222355
    222222445
    2222233333
    222223336
    222223345
    222223444
    22222366
    22222456
    22222555
    222233335
    222233344
    22223356
    22223446
    22223455
    22224445
    2222566
    222333334
    22233346
    22233355
    22233445
    22234444
    2223466
    2223556
    2224456
    2224555
    223333333
    22333336
    22333345
    22333444
    2233366
    2233456
    2233555
    2234446
    2234455
    223666
    2244445
    224566
    225556
    23333335
    23333344
    2333356
    2333446
    2333455
    2334445
    233566
    2344444
    234466
    234556
    235555
    244456
    244555
    25666
    33333334
    3333346
    3333355
    3333445
    3334444
    333466
    333556
    334456
    334555
    344446
    344455
    34666
    35566
    444445
    44566
    45556
    55555
    


    Всего 294 возможных варианта. Впрочем, это только половина дела. Нам более интересны не сами расклады, а то, какими способами мы сможем разместить очки взятой фигуры в каждом из них. Не буду утомлять читателя подробными рассуждениями, покажу лишь скрипт и окончательный результат его работы:

    Скрипт
    my @d;
    my %s;
    
    sub out {
      my ($deep) = @_;
      for (my $i = 0; $i < $deep; $i++) {
          print "$d[$i]";
      }
      print "\n";
    }
    
    sub proc {
      my ($x, $r, $m) = @_;
      if ($x == 0) {
          $s{$r}++;
      } else {
         my $n = $x % 10;
         for (my $i = 0; $i < $n; $i++) {
            proc(int($x / 10), $r + $i * $m, $m * 10);
         }
      }
    }
    
    sub alloc {
      my ($x, $deep, $res) = @_;
      if ($x == 0) {
          proc($res, 0, 1);
      } else {
          my $vl = 6;
          for (my $i = 0; $i < $deep; $i++) {
             if ($d[$i] < $vl) {
                 $vl = $d[$i];
             }
          }
          if ($vl < 6) {
             my $cn = 0;
             my $ix = 0;
             for (my $i = 0; $i < $deep; $i++) {
                 if ($d[$i] == $vl) {
                    $cn++;
                    $ix = $i;
                 }
             }
             my $y = $d[$ix]; $d[$ix] = 6;
             $x -= 6 - $vl;
             if ($x < 0) {
                 $x = 0;
             }
             alloc($x, $deep, $res * 10 + $cn);
             $d[$ix] = $y;
          }
      }
    }
    
    sub dice {
      my ($start, $deep, $sum) = @_;
      if ($sum == 25) {
         for (my $i = 0; $i < $deep; $i++) {
             my $x = $d[$i]; $d[$i] = 6;
             alloc($x, $deep, 0);
             $d[$i] = $x;
         }
      }
      if ($deep < 10 && $sum < 25) {
         for (my $i = $start; $i <= 6; $i++) {
             $d[$deep] = $i;
             dice($i, $deep + 1, $sum + $i);
         }
      }
    }
    
    dice(1, 0, 0);
    
    my $all;
    
    foreach my $k (sort { $s{$a} <=> $s{$b} } keys %s) {
      $all += $s{$k};
      print "$k\t=> $s{$k}\n";
    }
    
    print "\n$all\n";

    Результат
    102	=> 1
    331	=> 1
    200	=> 1
    ...
    22	=> 93
    5	=> 106
    21	=> 152
    20	=> 152
    11	=> 152
    10	=> 220
    4	=> 259
    3	=> 584
    2	=> 1061
    1	=> 1677
    0	=> 2407
    
    7954
    


    Слева — цепочки цифр, управляющие порядком распределения взятых очков. Например, «20» означает, что мы начинаем распределение с первой попавшейся фигуры (мы начинаем их подсчёт с 0), затем, распределяем в третью из оставшихся фигур с минимальным номиналом. Очевидно, что такая схема распределения возможна лишь для раскладов, не менее чем с четырьмя «минимальными» фигурами, например «3333445» (причём, распределить таким образом получится только «четвёрку» или «пятёрку»). Результат работы скрипта показывает, что распределяя очки, каждый раз в первую попавшуюся «минимальную» фигуру, мы покроем 30% (2407/7954) всех возможных ситуаций, а используя всего лишь три схемы распределения, уже более 64%!

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

    За Гензель и Гретель!
    Суть идеи проста — для того, чтобы ядро ZoG сочло ходы разными, достаточно, чтобы они имели разное ZSG-представление. Попросту говоря, эти ходы должны делать различные вещи. Добиться этого не сложно, необходимо, всего навсего управлять тем, к каким из фигур будут добавляться очки. Тот факт, что количество фигур (с каждой стороны) не может превышать 10, позволяет использовать удобную десятичную систему счисления. Мы уже встречались с этими числами в предыдущей врезке. Каждая отдельная цифра означает ту фигуру (с нуля, по порядку), к которой будут добавлены очки. После каждого использования, от числа отрезается один десятичный разряд. В конечном итоге остаётся 0, означающий использование первой попавшейся фигуры.

    Ещё немного кода
    VARIABLE	alloc-path
    VARIABLE	alloc-val
    VARIABLE	alloc-target
    VARIABLE	alloc-pos
    
    : alloc-to ( pos -- )
    	DUP add-pos
    	DUP val-at 6 SWAP -
    	DUP alloc-val @ > IF
    		DROP alloc-val @
    		0 alloc-val !
    	ELSE
    		alloc-val @ OVER - alloc-val !
    	ENDIF
    	my-next-player ROT ROT
    	OVER piece-type-at + SWAP
    	create-player-piece-type-at
    ;
    
    : alloc ( -- )
    	6 0 BEGIN
    		DUP enemy-at? OVER not-in-pos? AND IF
    			SWAP OVER val-at MIN SWAP
    		ENDIF
    		1+ DUP A9 >
    	UNTIL DROP
    	DUP 6 < IF
    		alloc-target !
    		alloc-path @ 10 MOD alloc-pos !
    		0 BEGIN
    			DUP enemy-at? OVER not-in-pos? AND IF
    				DUP val-at alloc-target @ = IF
    					alloc-pos @ 0= IF
    						DUP alloc-to
    						0 alloc-target !
    						DROP A9
    					ELSE
    						alloc-pos --
    					ENDIF
    				ENDIF
    			ENDIF
    			1+ DUP A9 >
    		UNTIL DROP
    		alloc-target @ 0= verify
    		alloc-val @ 0> IF
    			alloc-path @ 10 / alloc-path !
    			RECURSE
    		ENDIF
    	ELSE
    		DROP
    	ENDIF
    ;
    
    : alloc-all ( -- )
    	0 pos-count !
    	here add-pos
    	alloc
    ;
    


    Переменная alloc-path содержит нашу последовательность «хлебных крошек». Разумеется, было бы совершенно слишком расточительно определять в коде все 105 возможных управляющих последовательностей, но мы уже выяснили, что они не равнозначны. Большинство из них будут использоваться крайне редко, а всего 4 из них покроют большую часть возможных случаев. К сожалению, даже этим воспользоваться не удалось:

    Хлебные крошки
    : eat ( 'dir n -- )
    	LITE-VERSION NOT IF
    		check-pass
    		check-neg
    	ENDIF
    +	alloc-path !
    	val SWAP BEGIN
    		step
    		SWAP 1- DUP 0= IF
    			TRUE
    		ELSE
    			my-empty? verify
    			SWAP FALSE
    		ENDIF
    	UNTIL DROP
    	from here move
    	LITE-VERSION NOT enemy? AND IF
    		from piece-type-at mark - ABS
    		mark SWAP - create-piece-type
    	ENDIF
    	bump DROP
    	here E5 <> verify
    	enemy? verify
    	LITE-VERSION NOT IF
    		clear-neg
    		set-pass
    	ENDIF
    +	val alloc-val !
    +	alloc-all
    	add-move
    ;
    
    : eat-nw-0 ( -- ) ['] nw 0 eat ;
    : eat-sw-0 ( -- ) ['] sw 0 eat ;
    : eat-ne-0 ( -- ) ['] ne 0 eat ;
    : eat-se-0 ( -- ) ['] se 0 eat ;
    : eat-w-0  ( -- ) ['] w  0 eat ;
    : eat-e-0  ( -- ) ['] e  0 eat ;
    
    : eat-nw-1 ( -- ) ['] nw 1 eat ;
    : eat-sw-1 ( -- ) ['] sw 1 eat ;
    : eat-ne-1 ( -- ) ['] ne 1 eat ;
    : eat-se-1 ( -- ) ['] se 1 eat ;
    : eat-w-1  ( -- ) ['] w  1 eat ;
    : eat-e-1  ( -- ) ['] e  1 eat ;
    
    : eat-nw-2 ( -- ) ['] nw 2 eat ;
    : eat-sw-2 ( -- ) ['] sw 2 eat ;
    : eat-ne-2 ( -- ) ['] ne 2 eat ;
    : eat-se-2 ( -- ) ['] se 2 eat ;
    : eat-w-2  ( -- ) ['] w  2 eat ;
    : eat-e-2  ( -- ) ['] e  2 eat ;
    
    : eat-nw-3 ( -- ) ['] nw 3 eat ;
    : eat-sw-3 ( -- ) ['] sw 3 eat ;
    : eat-ne-3 ( -- ) ['] ne 3 eat ;
    : eat-se-3 ( -- ) ['] se 3 eat ;
    : eat-w-3  ( -- ) ['] w  3 eat ;
    : eat-e-3  ( -- ) ['] e  3 eat ;
    
    {moves p-moves
    	{move} split-nw-0	{move-type} normal-priority
    	{move} split-ne-0	{move-type} normal-priority
    	{move} split-sw-0	{move-type} normal-priority
    	{move} split-se-0	{move-type} normal-priority
    	{move} split-w-0	{move-type} normal-priority
    	{move} split-e-0	{move-type} normal-priority
    	{move} split-nw-1	{move-type} normal-priority
    	{move} split-ne-1	{move-type} normal-priority
    	{move} split-sw-1	{move-type} normal-priority
    	{move} split-se-1	{move-type} normal-priority
    	{move} split-w-1	{move-type} normal-priority
    	{move} split-e-1	{move-type} normal-priority
    +	{move} eat-nw-0		{move-type} normal-priority
    +	{move} eat-ne-0		{move-type} normal-priority
    +	{move} eat-sw-0		{move-type} normal-priority
    +	{move} eat-se-0		{move-type} normal-priority
    +	{move} eat-w-0		{move-type} normal-priority
    +	{move} eat-e-0		{move-type} normal-priority
    +	{move} eat-nw-1		{move-type} normal-priority
    +	{move} eat-ne-1		{move-type} normal-priority
    +	{move} eat-sw-1		{move-type} normal-priority
    +	{move} eat-se-1		{move-type} normal-priority
    +	{move} eat-w-1		{move-type} normal-priority
    +	{move} eat-e-1		{move-type} normal-priority
    +	{move} eat-nw-2		{move-type} normal-priority
    +	{move} eat-ne-2		{move-type} normal-priority
    +	{move} eat-sw-2		{move-type} normal-priority
    +	{move} eat-se-2		{move-type} normal-priority
    +	{move} eat-w-2		{move-type} normal-priority
    +	{move} eat-e-2		{move-type} normal-priority
    +	{move} eat-nw-3		{move-type} normal-priority
    +	{move} eat-ne-3		{move-type} normal-priority
    +	{move} eat-sw-3		{move-type} normal-priority
    +	{move} eat-se-3		{move-type} normal-priority
    +	{move} eat-w-3		{move-type} normal-priority
    +	{move} eat-e-3		{move-type} normal-priority
    	{move} slide-nw		{move-type} normal-priority
    	{move} slide-ne		{move-type} normal-priority
    	{move} slide-sw		{move-type} normal-priority
    	{move} slide-se		{move-type} normal-priority
    	{move} slide-w		{move-type} normal-priority
    	{move} slide-e		{move-type} normal-priority
    -(	{move} exchange-1-nw	{move-type} normal-priority
    -	{move} exchange-1-ne	{move-type} normal-priority
    -	{move} exchange-1-sw	{move-type} normal-priority
    -	{move} exchange-1-se	{move-type} normal-priority
    -	{move} exchange-1-w	{move-type} normal-priority
    -	{move} exchange-1-e	{move-type} normal-priority
    -	{move} exchange-2-nw	{move-type} normal-priority
    -	{move} exchange-2-ne	{move-type} normal-priority
    -	{move} exchange-2-sw	{move-type} normal-priority
    -	{move} exchange-2-se	{move-type} normal-priority
    -	{move} exchange-2-w	{move-type} normal-priority
    -	{move} exchange-2-e	{move-type} normal-priority
    -	{move} exchange-3-nw	{move-type} normal-priority
    -	{move} exchange-3-ne	{move-type} normal-priority
    -	{move} exchange-3-sw	{move-type} normal-priority
    -	{move} exchange-3-se	{move-type} normal-priority
    -	{move} exchange-3-w	{move-type} normal-priority
    -	{move} exchange-3-e	{move-type} normal-priority
    -	{move} exchange-4-nw	{move-type} normal-priority
    -	{move} exchange-4-ne	{move-type} normal-priority
    -	{move} exchange-4-sw	{move-type} normal-priority
    -	{move} exchange-4-se	{move-type} normal-priority
    -	{move} exchange-4-w	{move-type} normal-priority
    -	{move} exchange-4-e	{move-type} normal-priority
    -	{move} exchange-5-nw	{move-type} normal-priority
    -	{move} exchange-5-ne	{move-type} normal-priority
    -	{move} exchange-5-sw	{move-type} normal-priority
    -	{move} exchange-5-se	{move-type} normal-priority
    -	{move} exchange-5-w	{move-type} normal-priority
    -	{move} exchange-5-e	{move-type} normal-priority )
    moves}
    


    По всей видимости, в Axiom имеется ограничение на количество определяемых ходов (никак не отражённое в документации). Как я это определил? Очень просто! Когда я добавляю в код все определения, программа крэшится при старте. Если я убираю часть определений (например exchange-ходы), всё работает нормально. К сожалению, от идеи вариативного распределения очков пришлось отказаться.

    Строго говоря, это не вполне корректное решение. По правилам «Чейза», распределять очки должен не тот игрок, который выполнил ход, а его противник. Я не имею ни малейшего представления, о том, как этого можно добиться, используя ZoG, но есть очень простой обходной путь. Интерфейс ZoG предоставляет удобную интерфейсную возможность редактирования доски. Используя команды всплывающего меню, игрок может удалить любую фигуру на доске или создать другую. Эта возможность незаменима при отладке и я часто ей пользуюсь. В общем, игрок которому не понравилось автоматическое распределение очков, может легко перераспределить их вручную (очерёдность хода, при этом, не нарушается). Необходимо соблюдать лишь минимальную осторожность. В процессе редактирования не следует допускать ситуации, когда у одного из игроков остаётся менее 5 фигур, поскольку в этом случае, ему будет немедленно засчитано поражение и игра будет остановлена.

    … считай до одного!


    Поскольку идея «вариативного» распределения съеденных очков провалилась, я вернулся к разработке игры, посредством ZRF. Axiom-реализация, в принципе, тоже работала, но ей всё ещё не хватало AI (штатным ZoG-овским Аксиома пользоваться не умеет). В целом, эта задача сводится к правильному кодированию оценочной функции (для эстетов есть ещё и "Custom Engine"), но и это — весьма не просто! Во всяком случае, стандартная оценочная функция, учитывающая мобильность и материальный баланс, в «Чейзе» проявила себя не лучшим образом.

    Немножко деталей
    Оценочная функция, о которой я говорю, выглядит так:
    : OnEvaluate ( -- score ) 
    	mobility
    	current-player material-balance KOEFF * +
    ;
    

    Самый хитрый зверь здесь — mobility. Фактически — это количество всех возможных ходов игрока, из которого вычитается количество всех возможных ходов противника. Все ходы игрока, на момент оценки позиции, уже сгенерированы — подсчитать их не сложно, а вот чтобы сгенерировать ходы противника, приходится использовать немножко аксиомовской магии:
    : mobility ( -- score )
    	move-count
    	current-player TRUE 0 $GenerateMoves
    	move-count -
    	$DeallocateMoves
    ;
    

    Далее, полученная «мобильность» складывается с «материальным балансом», умноженным на некоторый константный коэффициент. Материальный баланс — это просто суммарная стоимость всех своих фигур, за вычетом стоимости фигур противника. Кстати, это объясняет, почему для фигур в Axiom я выбрал такие странные числовые значения:
    {pieces
    	{piece}		p1	{moves} p-moves 6   {value}
    	{piece}		p2	{moves} p-moves 5   {value}
    	{piece}		p3	{moves} p-moves 4   {value}
    	{piece}		p4	{moves} p-moves 3   {value}
    	{piece}		p5	{moves} p-moves 2   {value}
    	{piece}		p6	{moves} p-moves 1   {value}
    pieces}
    

    Я стремился сделать «мелкие» фигуры более значимыми, поскольку игроку действительно выгодно держать на доске как можно больше мелких фигур. В общем, в таком виде, всё это не сработало! AI вёл себя просто ужасно. Иногда складывалось впечатление, что он целенаправленно стремиться проиграть. Я думал о том как улучшить оценочную функцию, включив в неё бонусы/штрафы за взаимные угрозы фигур, образование кластеров (из фигур, стоящих вплотную друг к другу), достижимости Chamber и пр., но решил не тратить на это время, а просто переключиться на ZRF. Штатный AI ZoG-а традиционно показывает себя сильным, в подобных играх.

    Оставалась всего одна мелочь — в ZRF напрочь отсутствовала арифметика! «Чейз» — такая игра, в которой постоянно приходится считать! В некоторых случаях можно выкрутится. Например, при определении поражения игрока, вместо подсчёта очков (до 25-ти) на всех фигурах, можно ограничиться стандартной проверкой количества фигур. Поскольку 25 очков заведомо невозможно разместить на 4 фигурах, и всегда можно распределить по большему количеству фигур, следующих условий завершения игры вполне достаточно:

    (loss-condition (Red White) (pieces-remaining 4) )
    (loss-condition (Red White) (pieces-remaining 3) )
    

    Вторая проверка необходима, поскольку в игре возможна ситуация, когда одним ходом забираются сразу две фигуры (после расщепления фигуры в Chamber). К сожалению, есть одна задача, в которой целочисленная арифметика необходима! Разумеется, это распределение «съеденных» очков. В ZRF я не пытаюсь предложить несколько возможных вариантов распределения, на выбор. Мне необходимо просто обойти все фигуры, начиная с младших, и правильно добавить к ним ещё не распределённые очки. Вот как я это делаю:

    В основном, из палок
    Целые числа будем делать из булевских флагов (просто потому что больше не из чего). В ZRF-приложении их можно создать не больше тридцати двух, но нам вполне хватит четырёх (чтобы уметь считать до десяти). Макросы обеспечат (более менее) комфортную работу. Для начала, совершенно необходимо уметь обнулять «число», а также прибавлять (и отнимать) единичку:

    Ноль плюс/минус один
    (define clear
       (set-flag $1-8 false) (set-flag $1-4 false)
       (set-flag $1-2 false) (set-flag $1-1 false)
    )
    
    (define inc
       (if (flag? $1-1)
           (set-flag $1-1 false)
           (if (flag? $1-2)
               (set-flag $1-2 false)
               (if (flag? $1-4)
                   (set-flag $1-4 false)
                   (if (flag? $1-8)
                       (set-flag $1-8 false)
                    else
                       (set-flag $1-8 true)
                   )
                else
                   (set-flag $1-4 true)
               )
            else
               (set-flag $1-2 true)
           )
        else
           (set-flag $1-1 true)
       )
    )
    
    (define dec
       (if (not-flag? $1-1)
           (set-flag $1-1 true)
           (if (not-flag? $1-2)
               (set-flag $1-2 true)
               (if (not-flag? $1-4)
                   (set-flag $1-4 true)
                   (if (not-flag? $1-8)
                       (set-flag $1-8 true)
                    else
                       (set-flag $1-8 false)
                   )
                else
                   (set-flag $1-4 false)
               )
            else
               (set-flag $1-2 false)
           )
        else
           (set-flag $1-1 false)
       )
    )
    


    Пользоваться этим — совсем просто:

    Не больше десяти!
    (define not-10?
       (or (not-flag? $1-8)
           (flag? $1-4)
           (not-flag? $1-2)
           (flag? $1-1)
       )
    )
    
    (define calc
       (clear x)
       mark START
       (while (on-board? next) 
          next
          (if friend?
              (inc x)
          )
       )
       (verify (not-10? x))
       back
    )
    


    Главный цирк, как и предполагалось, начинается когда дело доходит до распределения очков по фигурам. Для начала, эти очки необходимо получить из съедаемой фигуры. Здесь подход совершенно прямолинейный. ZRF — не знает чисел, но мы-то знаем!

    Инициализация
    (define init
       (clear $1)
       (if (or (piece? p1) (piece? p3) (piece? p5))
           (set-flag $1-1 true)
       )
       (if (or (piece? p2) (piece? p3) (piece? p6))
           (set-flag $1-2 true)
       )
       (if (or (piece? p4) (piece? p5) (piece? p6))
           (set-flag $1-4 true)
       )
    )
    


    Здесь, нас подстерегает маленькая засада. Если съедаемых фигур две (такое редко, но бывает), такой код совершенно не подходит, поскольку, в самом начале, обнуляет «число». Надо научиться складывать числа! Это просто:

    Отнимаем от одного - добавляем к другому
    (define sum
       (while (not-0? $2)
           (inc $1)
           (dec $2)
       )
    )
    


    Осталось немного, но главное. Как добавить часть «числа» к количеству очков на фигуре? Причём, не абы как, а начиная с младших фигур?

    Здесь пришлось немного подумать
    (define try-alloc
       (if (is-0? x)
           (inc y)
        else
           (dec x)
       )
    )
    
    (define set-piece
       (if (am-i-red?)
           (create White $1)
        else
           (create Red $1)
       )
    )
    
    (define alloc-to
       (clear y)
       (if (piece? p1)
           (try-alloc) (try-alloc) (try-alloc) (try-alloc) (try-alloc)
       )
       (if (piece? p2)
           (try-alloc) (try-alloc) (try-alloc) (try-alloc)
       )
       (if (piece? p3)
           (try-alloc) (try-alloc) (try-alloc)
       )
       (if (piece? p4)
           (try-alloc) (try-alloc)
       )
       (if (piece? p5)
           (try-alloc)
       )
       (if (is-0? y)
           (set-piece p6)
        else
           (if (is-1? y)
               (set-piece p5)
            else
               (if (is-2? y)
                   (set-piece p4)
                else
                   (if (is-3? y)
                       (set-piece p3)
                    else
                       (set-piece p2)
                   )
               )
           )
       )
    )
    
    (define alloc
       (if (not-0? x)
           mark ST
           (while (on-board? next) 
               next
               (if (and enemy? (piece? $1) (not-0? x) 
                        (not-position-flag? is-captured?))
                   (alloc-to)
               )
           )
           back
       )
    )
    
    (define alloc-all
       (alloc p1) (alloc p2) (alloc p3) (alloc p4) (alloc p5)
    )
    


    При выполнении alloc-all, в x находится количество ещё не распределённых очков (максимум — 12, если съели две шестёрки). Пока в x не 0, пытаемся его распределить, начиная с p1 и до p5 (в шестёрки, очевидно, распределить уже ничего не удастся). Ищем фигуру требуемого номинала на доске и вызываем alloc-to. Здесь и начинается магия. Распределяем очки по одной единичке, в зависимости от типа фигуры (в p1 лезет 5 единичек. в p2 — 4 и т.д.). Не пытаемся анализировать, хватает ли в x единичек, а просто добавляем все распределяемые единички к ещё одной переменной — y. Это и есть переполнение (очевидно оно не может превышать 4), если оно не нулевое, просто корректируем тип фигуры.




    В результате, вся наша «ненормальная арифметика» работает с вполне приемлемой производительностью и AI ничуть не страдает. Надо сказать, что не всегда подобные эксперименты бывают столь же удачны. Например, эту версию калькулятора (напомню, что никакой арифметики в ZRF нет) можно рассматривать исключительно как шутку. Его производительность просто ужасна! Но в нашем случае, «ненормальное программирование» показало себя лучшим из возможных решений.
    • +15
    • 8,1k
    • 6
    Поделиться публикацией
    Комментарии 6
      +1
      Как любителю абстрактных настолок мне игра понравилась, спасибо, что показали)
      На картинке показывающей начальную позицию склеенность боковой границы не наблюдается… Проблемка…
        0
        Да, меня самого это сначала вводило в ступор. В общем, есть несколько вариантов досок. В одних одинаковое количество ячеек в каждой строке, другие, как эта, на фотографии. В них фигуры ставятся сбоку от доски (как бы на отсутствующую ячейку). Иначе траектории движения фигур совсем ломаются.
        Глянул на фото внимательнее и понял что немного наврал. На том варианте доски, что на фотографии, обе боковые позиции считаются одной и той же позицией. Фигуру можно ставить на любую из них.
          +1
          Мне кажется оптимальный вариант расстановки — на видео.
          Но там ходы, которые через "разорванную" границу отображаются неверно (кубик движется сразу к конечному полю, а должен двигаться к границе, а потом "перескакивать" на другую сторону).
          В этой границе — особая сложность для игрока-человека (как в цилиндрических шахматах). Нужно много играть, чтобы начать автоматом видеть такие ходы…
          Эх… надо кубики купить...
            0
            Да, есть проблемы с анимацией хода, в ZoG это традиционно. В принципе, можно было сделать нормальную анимацию, используя частичные ходы, но я побоялся усложнять жизнь AI.
        0
        Как я понял, у ZoG есть большие проблемы(неудобства) с реализацией этой игры.
        А как справляется Dagaz с этим?
          0
          Сложности две:
          1. Главная — вариативное распределение очков (в Dagaz до этого пока не дошёл, даже мыслей нет как можно нормально сделать)
          2. Хитрые составные ходы, когда ход продолжается другой фигурой или даже несколькими фигурами (здесь я хочу отказаться от частичных ходов и рассматривать ход как единое целое, такой подход используется в Jocly, о которой я буду писать в следующий раз)

        Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

        Самое читаемое