Приручаем ZoG (Часть 5: Сбор маны)
Сегодня я завершаю цикл статей, рассказывающих о возможностях языка ZRF, используемого для разработки игр в Zillions of Games. Поскольку я намеренно двигался от простого к сложному, логично предположить, что сегодняшняя игра (в плане реализации) будет сложнее всех предыдущих. Это действительно так. Дело в том, что ZRF ни в коем случае нельзя отнести к универсальным языкам программирования. Он предназначен для описания игр похожих на Шахматы. Чем более «шахматоподобна» игра, тем более очевидно её описание (если, конечно, не обращать внимание на описание правил таких хитрых ходов как рокировка или взятие на проходе). Описание такой игры может быть довольно большим по объему (в этом случае может помочь PreZRF, о котором я писал ранее), но довольно тривиальным по содержанию.Все меняется когда приходится делать что-то на Шахматы совсем не (или не совсем) похожее. Создание таких приложений как Game of Life или Mine Finder является серьезным вызовом, в случае использования чистого ZRF, без каких либо расширений. Сегодня я постараюсь показать с какими сложностями может быть связана подобная разработка.Источником вдохновения, на этот раз, мне послужила миниигра «Сбор маны» из уже упоминавшейся мной ранее Battle vs Chess. Игроку предоставляется одна или несколько шахматных фигур, которыми он должен ставить вилки, убирая с поля расставляемые случайным образом «кристаллы»:
[embedded content]
Попробуем разобраться, с какими сложностями мы столкнемся, пытаясь реализовать аналогичную миниигру, используя ZRF. Первая сложность — это подсчет количества фигур, находящихся под боем, необходимый для определения наличия «вилки». Как я уже говорил, с разного рода подсчетами в ZRF туго, но, в частных случаях, задача вполне решаема:
Определение «вилок» (define common-check mark (if (on-board? $1) $1 (while (and empty? (on-board? $1)) $1) (if enemy? (if (flag? is-first) (set-flag is-second true) else (set-flag is-first true) ) ) ) back )
(define common-capture mark (if (on-board? $1) $1 (while (and empty? (on-board? $1)) $1) (if enemy? capture) ) back )
(define queen-slide ($1 (while empty? (set-flag is-first false) (set-flag is-second false) (common-check n) (common-check s) (common-check w) (common-check e) (common-check nw) (common-check ne) (common-check sw) (common-check se) (if (flag? is-second) (common-capture n) (common-capture s) (common-capture w) (common-capture e) (common-capture nw) (common-capture ne) (common-capture sw) (common-capture se) ) add $1 ) ) ) Поскольку нам не обязательно знать точное количество фигур, находящихся «под боем», для «подсчета», достаточно двух булевских флагов. Если, после серии проверок common-check в различных направлениях, флаг is-second взведен, необходимо снять все фигуры, находящиеся «под боем».Важным (и не очевидным для понимания) моментом является то, что все эти проверки и взятия должны быть выполнены до формирования команды add, завершающей возможный ход (поскольку они являются частью этого хода). Кроме того, в отличии от обычных шахмат, на занятое (фигурой противника) поле ходить мы не можем.
Уже в этом месте, в полной мере, проявляется «коварство» ZRF. Поиграв немного, можно заметить следующий баг:
[embedded content]
Можно заметить, что фигура находящаяся за стартовой позицией, в противоположном от хода направлении, не рассматривается как находящаяся «под боем». Это связано с тем, что, в момент рассчета хода, начальное поле не считается пустым. Поняв причину ошибки, легко её исправить:
Исправленное определение «вилок» (define common-check mark (if (on-board? $1) $1 (while (and (or (position-flag? is-start) empty?) (on-board? $1)) $1) (if enemy? (if (flag? is-first) (set-flag is-second true) else (set-flag is-first true) ) ) ) back )
(define common-capture mark (if (on-board? $1) $1 (while (and (or (position-flag? is-start) empty?) (on-board? $1)) $1) (if enemy? capture) ) back )
(define queen-slide ((set-position-flag is-start true) $1 (while empty? (set-flag is-first false) (set-flag is-second false) (common-check n) (common-check s) (common-check w) (common-check e) (common-check nw) (common-check ne) (common-check sw) (common-check se) (if (flag? is-second) (common-capture n) (common-capture s) (common-capture w) (common-capture e) (common-capture nw) (common-capture ne) (common-capture sw) (common-capture se) ) add $1 ) ) ) Мы просто помечаем начальное поле позиционным флагом и рассматриваем его далее как пустое.Обобщение сформулированных правил на ходы остальных фигур является делом техники. Это решение прекрасно работает, в случае игры одним ферзём, но уже при игре, например, двумя ладьями оно не полно.
[embedded content]
Программа не учитывает, что одна фигура, перемещаясь, может «открыть» вилку другой фигурой. Это как раз тот случай, когда вполне понятно, что надо делать для исправления ошибки, но (пока) не вполне понятно как. Очевидно, требуется перебрать все свои фигуры и проверить возможные вилки от каждой из них. Для автоматизации поиска всех фигур, находящихся на доске, полезно создать «направление», связывающее все поля в цепочку:
Связывание всех полей (define Next-Definitions (dummy offboard) (links next (a1 b1) (b1 c1) (c1 d1) (d1 e1) (e1 f1) (f1 g1) (g1 h1) (h1 a2) (a2 b2) (b2 c2) (c2 d2) (d2 e2) (e2 f2) (f2 g2) (g2 h2) (h2 a3) (a3 b3) (b3 c3) (c3 d3) (d3 e3) (e3 f3) (f3 g3) (g3 h3) (h3 a4) (a4 b4) (b4 c4) (c4 d4) (d4 e4) (e4 f4) (f4 g4) (g4 h4) (h4 a5) (a5 b5) (b5 c5) (c5 d5) (d5 e5) (e5 f5) (f5 g5) (g5 h5) (h5 a6) (a6 b6) (b6 c6) (c6 d6) (d6 e6) (e6 f6) (f6 g6) (g6 h6) (h6 a7) (a7 b7) (b7 c7) (c7 d7) (d7 e7) (e7 f7) (f7 g7) (g7 h7) (h7 a8) (a8 b8) (b8 c8) (c8 d8) (d8 e8) (e8 f8) (f8 g8) (g8 h8) (h8 offboard) ) )
(game … (board (Board-Definitions) (Next-Definitions)) ) Помимо направления next, мы также определяем фиктивное поле offboard, используемое для завершения перебора. Это достаточно распространенная идиома ZRF. Кроме того, помимо возможности связывания всех полей, команда links может использоваться для создания разнообразных досок с «измененной топологией». С её помощью, можно, например, «склеить» края доски, превратив её в цилиндр, тор или ленту Мебиуса.Теперь можно использовать созданное направление. К сожалению, попытка «перебрать» свои фигуры непосредственно в рамках хода одной из них сталкивается с техническими сложностями. Перед началом перебора, требуется запомнить текущую позицию (для того, чтобы, впоследствии, сформировать завершающую ход команду add). Обычно, для этого используется пара команд mark/back, но мы уже используем их в common-check и common-capture, а стэк сохраняемых позиций командой mark не поддерживается.
Для того, чтобы решить эту проблему, создадим фиктивного игрока ? Clean:
(game … (players Black? White? Clean) (turn-order? White? White? White? White? White? White? White? White Black? Clean) ) Как я уже рассказывал в предыдущей статье, знак вопроса в начале имени игрока означает, что непосредственно он в игре не участвует и будет выполнять свои ходы случайным образом. Но как именно будет ходить ? Clean? Ходы этого игрока нужны нам исключительно ради побочного эффекта (во время его хода будет выполняться проверка наличия на доске вилок и снятие попавших под них фигуры). Очевидно, не стоит ? Clean-ом двигать фигуры, значит придется выставлять (drop) их на доску: Поиск всех вилок (define common-check mark (if (on-board? $1) $1 (while (and (or (position-flag? is-start) (or (piece? Cleaner) empty?)) (on-board? $1)) $1) (if (piece? Stone) (if (flag? is-first) (set-flag is-second true) else (set-flag is-first true) ) ) ) back )
(define common-capture mark (if (on-board? $1) $1 (while (and (or (position-flag? is-start) (or (piece? Cleaner) empty?)) (on-board? $1)) $1) (if (piece? Stone) capture) ) back )
(define clean-queen ((verify empty?) (set-position-flag is-cleaner true) a1 (while (not-position? offboard) (if (piece? Queen) (set-flag is-first false) (set-flag is-second false) (common-check n)(common-check nw) (common-check s)(common-check ne) (common-check w)(common-check sw) (common-check e)(common-check se) (if (flag? is-second) (common-capture n)(common-capture nw) (common-capture s)(common-capture ne) (common-capture w)(common-capture sw) (common-capture e)(common-capture se) ) ) next ) a1 (while (not-position? offboard) (if (position-flag? is-cleaner) add ) next ) ) )
(define slide ((set-position-flag is-start true) $1 (while empty? add $1 ) ) )
(game …
(players Black? White? Clean) (turn-order? White? White? White? White? White? White? White? White Black? Clean) (board (Board-Definitions) (Next-Definitions))
(board-setup (? Clean (Cleaner off 1) ) (? White (Stone off 8) ) (Black (Queen e4) ) )
(piece (name Stone) (image? White «images\Chess\SHaag\wpawn.bmp») (help » ») (drops (add-to-empty) ) ) (piece (name Cleaner) (image? Clean «images\DarkChess\Invisible.bmp») (drops (clean-queen) ) ) (piece (name Queen) (image Black «images\Chess\SHaag\bqueen.bmp») (help «Queen: can slide any number of squares in any direction») (moves (slide n)(slide ne) (slide e)(slide nw) (slide s)(slide se) (slide w)(slide sw) ) ) ) Здесь довольно много изменений, но общий смысл, я думаю, понятен. Мы разрешили игроку ? Clean ставить на поле его фигуру (сразу после хода Black), проверяя, в процессе, наличие вилок. Поскольку эта фигура не имеет отношения к игровому процессу, желательно сделать ее невидимой. Ресурс полностью прозрачной фигуры можно взять, например, из этой забавной игры.? Clean прекрасно справляется со своей работой по обнаружению вилок, но как быть с добавляемой им фигурой? Будь эта фигура хоть трижды невидима, чтобы она не мешала игровому процессу, желательно снимать её с доски. Будем делать это на ходе? White:
Чистка мусора (define add-to-empty ((verify empty?) (set-position-flag is-cleaner true) a1 (while (not-position? offboard) (if (piece? Cleaner) capture ) next ) a1 (while (not-position? offboard) (if (position-flag? is-cleaner) add ) next ) ) ) Чтобы ? Clean мог использовать свою фигуру многократно, включим соответствующую опцию: (option «recycle captures» true) В принципе, все это работает, но если Black во время хода не поставит вилку, ? Clean потратит свою фигуру, а ? White не сможет ее очистить, поскольку не сможет сделать ход (так как все его 8 фигур уже на доске). Очевидно, мы должны давать возможность завершить ход ? Clean только при условии, что ему удалось найти хотя-бы одну вилку: Исправленный поиск вилок (define clean-queen ((verify empty?) (set-flag is-succeed false) (set-position-flag is-cleaner true) a1 (while (not-position? offboard) (if (piece? Queen) (set-flag is-first false) (set-flag is-second false) (common-check n)(common-check nw) (common-check s)(common-check ne) (common-check w)(common-check sw) (common-check e)(common-check se) (if (flag? is-second) (set-flag is-succeed true) (common-capture n)(common-capture nw) (common-capture s)(common-capture ne) (common-capture w)(common-capture sw) (common-capture e)(common-capture se) ) ) next ) a1 (while (not-position? offboard) (if (and (flag? is-succeed) (position-flag? is-cleaner)) add ) next ) ) ) Для того, чтобы игра не завершалась внезапно, при невозможности хода одним из игроков, нам поможет следующая опция: (option «pass turn» 2) Уффф… Теперь все работает (правда не так красиво как в оригинале, но зато мы сами решаем, каким набором фигур будем играть). Конечно, вся эта конструкция сильно напоминает сооружение из подпирающих друг друга костылей, но такова уж плата за то, чтобы сделать в ZRF что либо не тривиальное.В качестве десерта, предлагаю насладиться следующим вариантом Шахмат:
[embedded content]
Все фигуры ходят также, как и в обычных Шахматах. Я внес всего лишь одно изменение, но оно кардинально изменило весь ход игры. Поскольку я субъективен, мне сложно оценить её достоинства, но могу сказать, что, мне лично, выиграть в неё у компьютера гораздо сложнее, чем в обычные Шахматы. Игра очень динамична и редко продолжается дольше 10 ходов. Ставьте вилки, чтобы победить!