- PVSM.RU - https://www.pvsm.ru -

Работа над ошибками

Работа над ошибками - 1 — Люди забыли эту истину, — сказал Лис,
— но ты не забывай:
       ты навсегда в ответе за всех, кого приручил…
 
       Антуан де Сент-Экзюпери "Маленький принц [1]"

Не ошибается только тот, кто ничего не делает. Со временем, ошибки накапливаются. Да что там говорить, прямо сейчас у меня на руках имеется с десяток use case-ов, приводящих к различным ошибкам, в одних только "Квантовых крестиках-ноликах [2]" и совершенно не хватает духу, чтобы ими заняться (код там действительно адовый [3]). Но иногда я нахожу время, чтобы что-то исправить. И знаете что? Исправлять старые ошибки ничуть не менее интересно, чем делать новые!

Старый Ур в новых одёжках

Ур [4] — одна из моих самых первых игр (если не на Zillions of Games [5], то на Axiom [6] уж точно [7]). Это игра, с которой началось моё увлечение настольными играми! Разумеется, речь идёт не о реконструкциях [8] Белла [9] или Мюррея [10]. При всех своих достоинствах, эти учёные мужи, по всей видимости, редко играли в настольные игры (во всяком случае, по своим правилам). Единственный, на мой взгляд, играбельный вариант [11] правил, для этой игры, был разработан Дмитрием Скирюком [12].

Выглядела игра страшненько

К счастью, в творческий процесс практически сразу включился Nomad1 [13], разработав превосходную реализацию игры под Android и iOS [14]. Следует заметить, что в разработке принимал участие ещё один из авторов Хабра. Без превосходных музыкальных тем, созданных 1eqinfinity [15], игра и вполовину не получилась бы столь атмосферной. Ресурсы Android-версии позволили значительно улучшить внешний вид [16] ZoG-версии, но один из важных моментов не давался мне ни в какую.

Всё дело в ''стопках'' фигур

По правилам предложенным Дмитрием Скирюком, на доске имеется несколько позиций разрешающих устанавливать до четырёх игровых фишек «друг на друга». Работает такая стопка по принципу LIFO [17] — фишка зашедшая на позицию первой уходит последней. Этот механизм позволяет задерживать продвижение фишек противника, «прижимая» их своими. Именно с этим возникли проблемы. В рамках модели Zillions of Games [18], каждое поле доски может содержать не более одной фигуры.

Я не в первый раз сталкиваюсь с этим ограничением и знаю как с ним бороться. В некоторых играх, таких как "Focus [19]", можно использовать «составные» фигуры, представляющие собой «стопки» фигур поставленных друг на друга. Аналогичный подход используется в манкалах [20]. К сожалению, для Ура этот способ неприменим, пришлось бы определить слишком много типов фигур. Другой подход заключается в незаметном перемещении фигур «стопки» на специально выделенные позиции вне доски (как это сделано в Ритмомахии [21]).

В первой версии Ура, я прятал заблокированные фигуры, незаметно перемещая их на невидимые поля. Когда верхняя фишка уходила, я возвращал фигуры из стопки, по одной, на прежнее место. К сожалению, наглядность страдала. При таком «взгляде сверху», требовалось хорошо разбираться в правилах, чтобы отличать ситуации «сбивания» фишек от их «накрывания». Совсем недавно, мне удалось эту проблему решить:

Начал я с Пулука

Давно хотел сделать эту игру [22]. В «Пулуке» фишки могут ставиться друг на друга, но, в отличии от «Фокуса», нет ограничения на размер стопки. Поскольку «составные» фигуры использовать было нельзя, появилась идея отрисовывать тайлы фишек с небольшим наложением друг на друга. И здесь мне здорово помогли наработки от ещё одной игры [23]. Дело в том, что доску в ZRF можно определять двумя способами. Проще всего определить grid:

(grid
  (start-rectangle 0 54 50 104)
  (dimensions
     ("a/b/c/d/e/f/g" (18 0)) ; files
     ("10/9/8/7/6/5/4/3/2" (0 50)) ; ranks
  )
)

Число 18 (в скобках) — это смещение, на которое сдвигается тайл фигуры при изменении позиции по горизонтали. Поскольку ширина самого тайла равняется 30, фигуры «накладываются» друг на друга. Выглядит это не слишком хорошо:

Работа над ошибками - 2

К счастью, есть способ исправить ситуацию! Этот небольшой скрипт [24] уже один раз помог мне с «Чейзом», поможет и теперь. Он не очень правильный, некорректно работает с отрицательными числами и множественными описаниями grid-ов, но экономит мне кучу времени. Вот как выглядит игра после его применения:

Работа над ошибками - 3

Секрет прост — теперь, каждая позиция описывается индивидуально. Порядок описания позиций определяет z-последовательность «наложения» тайлов друг на друга:

(positions
 ...
 (g5 108 304 158 354)
 (f5  90 304 140 354)
 (e5  72 304 122 354)
 (d5  54 304 104 354)
 (c5  36 304  86 354)
 (b5  18 304  68 354)
 (a5   0 304  50 354)
 ...
)

К сожалению, из за специфичного для «Пулука» группового перемещения фигур, картинка «ломается». Накрытая фигура перемещается второй и отрисовывается позже фигуры перемещаемой игроком. Сделать с этим ничего нельзя, так работает Zillions of Games:

Работа над ошибками - 4

Пулук просто не подходит для такого способа отображения фигур. А вот Ур — подходит!

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

Работа над ошибками - 5

Подход работает, но в некоторых случаях картинка ломается:

Работа над ошибками - 6В общем-то, здесь всё понятно. Мы имеем дело с прямоугольными тайлами, частично нарисованными «прозрачным» цветом, но когда ZoG отрисовывает «прозрачный» цвет то берёт часть изображения доски (фонового рисунка), а не фигуры перекрывшей то же место. Я уже сталкивался с подобным когда рисовал фигуры для "MarGo [25]" и знаю как с этим бороться. Требуется всего лишь нарисовать дополнительные тайлы с закрашенными правыми-нижними уголками. Цвет заливки должен совпадать с цветом лежащей ниже фигуры. 

К сожалению, одной только подготовкой графических ресурсов дело не ограничивается. Дело в том, что оболочка Zillions of Games «не любит» выполнять «лишнюю» работу. При перемещении одной фигуры, другие не перерисовываются. После выполнения анимации хода, фигура просто отрисовывается на новой позиции, а старая позиция заполняется (опять же) прямоугольным фрагментом фонового изображения доски. Способ «обмануть систему» есть. Необходимо «всего лишь» инициировать перерисовку всех фигур, начиная с низа стопки:

Вот как это выглядит

(define refresh
   (if (im-white?)
       (if friend?
           (create White $1)
        else
           (create Black $1)
       )
    else
       (if friend?
           (create Black $1)
        else
           (create White $1)
       )
   )
)

(define check-refresh
   (if (on-board? down)
       (if (or (and (flag? is-enemy?) not-enemy?)
               (and (not-flag? is-enemy?) enemy?)
           )
           (refresh $3)
        else
           (refresh $2)
       )
    else
       (refresh $1)
   )
)

(define pre-action
   ...
   (if (on-board? down)
       mark down
       (while (and (on-board? up) (not-empty? up))
            (if (or (piece? King) (piece? KingE) (piece? KingF))
                (check-refresh King KingF KingE)
             else
                (check-refresh Man ManF ManE)
            )
            (set-flag is-enemy? enemy?)
            up
       )
       back
   )
)

Фактически, все фигуры «стопки» пересоздаются с теми же типами, в рамках выполнения хода снимающего верхнюю фишку. Это уже вполне работоспособное решение [26]. Разумеется, если внимательно следить за анимацией, можно заметить кратковременное разрушение картинки при снятии фишки или анимацию перемещения фигуры с залитым «уголком», но по завершении хода картинка восстанавливается во вполне приемлемое состояние. Пожалуй, это лучшее, чего можно добиться средствами Zillions of Games.

К сожалению, есть ещё одна проблема

Работа над ошибками - 7

Жёлтые прямоугольники — это регионы связанные с фигурами. Можно видеть, что в «стопках» верхние регионы перекрывают нижние. В этом и кроется проблема. Для того, чтобы «кликнуть» по фишке лежащей в основании стопки, требуется попасть мышью в узенькую полоску шириной в пару пикселов. В этом немного помогает курсор мыши — он изменяет форму когда наводится на фигуру способную выполнить ход, но здесь есть большая засада.

Оболочка Zillions of Games поддерживает довольно хитрую опцию, связанную с UI. При включении «smart moves», игроку уже не обязательно перетаскивать фигуры по доске мышью. Если мы кликаем по фигуре, способной выполнить единственный ход — ход будет выполнен автоматически, без всякого перетаскивания. В случае Ура, это очень удобно (а вот в Шахматах, иногда, бывает не очень). Но у опции есть и обратная сторона — если мы кликаем по пустому полю, на которое может быть выполнен единственный ход, то этот ход будет выполнен! Теперь представим себе стопку из трёх фигур. Верхний прямоугольник существенно превосходит по площади двухпиксельные уголки и, если в него возможен ход, то чуть дрогнувшая рука может запросто привести к поражению во всей партии.

В качестве бонуса, я получил неожиданный подарок. Вообще, я планировал переписать Axiom-версию вслед за ZRF, но это не понадобилось! Из за упрощения логики игры, AI стал справляться с игрой гораздо лучше. Достаточно сказать, что иногда он у меня выигрывает, а в большинстве случаев, не успевает провести всего одну фишку. Он всё ещё хуже Android-версии, но с ним уже вполне приятно играть.

Охота на «Слона»

Это [27] была не моя ошибка, но уж очень чесались руки её исправить. Не знаю почему. Речь идёт об одной довольно оригинальной шахмато-подобной игре [27], якобы популярной среди пигмеев Итури [28]. Со слов автора [29], описание игры было обнаружено его другом в дневниках французского миссионера Maurice Morceau, датированных 1821 годом:

The first written reference to the game of Elephant Hunt was found in the diaries of father Maurice Morceau, a French missionary who disappeared without a trace in 1821 while on a mission to the Ituri forest. His personal effects, including the diaries, were later found in a cannibal village by an anthropological expedition.

К сожалению, мы имеем дело лишь с авторской реконструкцией. Описание игры было очевидно неполным, доступа к первоисточнику нет, а контакт с другом был впоследствии потерян. В частности, вызывает сомнение использование в игре перемещения "ходом коня [30]" — слишком абстрактного для подобной традиционной игры:

The author did mention that the Elephant moved on the 5x5 field on which the 10x10 field for the Pygmies was 'overlaid by halving', and that the Pygmies moved 'by hopping about, like our chess-knight' but I personally doubt they actually made a Knight-move, which is sort of abstract. However, other possible alternatives (like D and/or A) seem to me to be out of the question, as the Pygmies cannot possibly win if colorbound.

Кстати, в этой цитате используется "Ralph Betza's ''funny notation'' [31]", о которой я уже писал ранее [32]. Почему меня так заинтересовала эта игра? Попробуем разобраться.

Работа над ошибками - 8

Мы видим одну из редких игр, в которых используются фигуры двух разных размеров. Слон — большая фигура, занимающая сразу четыре поля доски. Он может перемещаться на один «большой» шаг по любой ортогонали и «давит» всё, оказавшееся на четырёх целевых клетках. Ему противостоят «пигмеи», перемещающиеся ходом «Коня», а также один «шаман», двигающийся на одну или две клетки, в любую из восьми сторон.

Разумеется, «малые» фигуры не могут просто так перемещаться на клетки занятые «слоном» (это было бы слишком просто). Слон может быть «съеден» лишь при условии, что все находящиеся под ним поля атакованы! Сам автор проводит параллели со средневековой европейской игрой, под названием "Лиса и гуси [33]". Неплохая задумка, но, к сожалению, реализация подкачала.

Немного кода

(define Ele-shift (
   $1
   (verify not-friend?)
   (verify (not-friend? ul)) 
   (verify (not-friend? ur)) 
   (verify (not-friend? dl)) 
   (verify (not-friend? dr))
   (set-attribute vulnerable?
            (and (attacked? ul) (attacked? ur) (attacked? dl) (attacked? dr)))
   (capture ul) (capture ur) (capture dl) (capture dr)
   add
))

(define Shaman-shift2 (
   $1 (if empty? add $2)
   (verify (and not-friend? (or (empty? c) (vulnerable? c))))
   (if (not-empty? c) (capture c))
   add
))

Здесь сразу две ошибки. Одна обидная, зато другая — фундаментальная! Во первых, совершенно неожиданно, шаман может атаковать одиноко-стоящего слона (при этом, вполне ожидаемо «ломая» картинку). При этом, самого слона он не ест, а просто занимает клетку под ним:

Работа над ошибками - 9

Корень зла здесь:

(if empty? add ...)

Если клетка пустая — то мы на неё встаём (и двигаемся дальше, но это в другой реальности). Фокус в том, что клетка «под слоном» действительно пустая. Она в другом grid-е, маленькая и «слон» не поместился бы в ней, при всём желании. Исправить это довольно легко:

(define my-empty?
   (and not-friend? (or (empty? c) (vulnerable? c)))
)
...
(if (my-empty?)
    (if (not-empty? c) (capture c))
    add 
$2)
...

Дальше — больше:

Работа над ошибками - 10

Вот таким нехитрым способом, используя шамана в качестве наживки, одинокий пигмей может одолеть целого слона! Разумеется, это немного не то, что задумывалось автором. Но почему так получилось? На самом деле, всё просто. Слон может быть взят, если все четыре клетки, на которых он стоит, находятся под боем. Теперь посмотрим код:

(set-attribute vulnerable?
      (and (attacked? ul) (attacked? ur) (attacked? dl) (attacked? dr)))

Всё достаточно прозрачно и это бы даже работало, если бы в игре участвовали одни только «пигмеи». Выполняя бой «ходом коня», они не атакуют поля квадрата 2x2, на котором находятся. Шаман — другое дело! Он «покрывает» 3 из 4 полей квадрата, на котором стоит. Остаётся «покрыть» то поле на котором расположился сам шаман и западня готова. И не важно, что шаман будет съеден — атрибут уязвимости будет уже установлен и будет действовать весь следующий ход!

Поняв ошибку, можно легко её исправить. Сам патч [34] довольно многословен, но его суть проста. Мы отказываемся от использования предиката attacked? в этой игре (он здесь просто не подходит) и заменяем его ручными проверками. Более гибкий подход позволяет предусмотреть особую обработку для «шамана». С внешним видом игры, к сожалению, сделать ничего нельзя. Картинка постоянно «ломается». Эта игра не очень подходит для Zillions of Games (но сама идея, безусловно, интересна).

Не оглядывайся!

В процессе работы над новыми играми, я узнаю платформу Zillions of Games лучше. В голову приходят совершенно новые решения, до которых я не мог додуматься раньше. Часто они оказываются столь хороши, что позволяют исправить другие, более старые игры. Так, в одной из статей [35] Дмитрия Скирюка, описывается весьма оригинальная игра "Поединок [36]", в которой вместо шахматных фигур используются игральные кубики.

При выполнении хода, кубик перекатывается по доске на то количество шагов, которое было изображено на его верхней грани в начале хода. Движение выполняется не по прямой, направление может изменяться произвольным образом. Это важно, поскольку грань, которой кубик окажется обращён вверх по завершении хода будет зависеть не только от начальной и конечной точки, но и от выбранного пути перемещения.

Подобная разновидность перемещения (с возможным изменением направления на каждом шаге) в настольных играх встречается довольно часто. Это и Thunderclap в "Ko Shogi [37]" и тихие ходы фигур в "Nine-Tile Cyvasse [38]" и многое другое. Главная связанная с ним проблема — необходимость запрета возвращения на ранее пройденные поля. Если максимальная дистанция не превышает трёх шагов (как в игре "Mana [39]" Клода Лероя), достаточно запретить изменение направления движения на строго противоположное (для этого в ZRF имеется замечательный предикат not-last-from?).

Строго говоря, он не всегда спасает

В реализации Mana [40] на ZRF этот приём работает, но может привести к аварийному завершению игры. В игре возможна (хотя и маловероятна) ситуация, в которой один или два первых частичных хода заведут фигуру в тупик, из которого нет выхода. Это происходит из за того, что каждый частичный ход считается самостоятельным, независимым от других. Zillions of Games [18] не может увидеть проблему, пока не «упрётся в неё носом». Jocly [41], рассматривающая весь составной ход целиком, позволяет построить более корректную реализацию [42].

Если фигура может перемещаться более чем на 3 шага, not-last-from? уже не спасает. Необходимо как-то помечать все ранее пройденные поля. В ZRF, для привязки к полям булевских значений, используются так называемые «позиционные флаги». К сожалению, они не являются частью игрового состояния и автоматически очищаются в начале каждого частичного хода.

Выход, разумеется, есть

(define clear-mark
   mark a0
   (while (on-board? next) 
      next
      (if (and enemy? (piece? Mark))
          capture
      )
   )
   back
)

(define step (
   (create Mark)
   $1 (verify (my-empty?))
   (clear-mark)
   (add-partial $2 $3)
))

На посещённых полях можно размещать невидимые фигуры (уж они то безусловно являются частью состояния). Главное не забывать их вовремя удалять! Этот приём настолько универсален, что может пригодиться во множестве совершенно непохожих игр. В первую очередь, в голову приходят всевозможные игры-переходы, такие как "Salta [43]" или "Traverse [44]", но одними лишь играми семейства "Халма [45]" дело не ограничивается. Тот же самый приём позволил довести до «рабочего» состояния "Luzhanqi [46]" и давным давно написанные "Осетинские шашки [47]".

Вместо послесловия...

Продолжая разговор о шашках, хочу заметить, что «работа над ошибками» далеко не всегда сводится к чисто техническим моментам. Очень часто, в процессе работы над игрой, я узнаю что-то новое. Иногда такие «подробности» буквально переворачивают все мои представления об игре, заставляя взглянуть на неё по новому. В качестве примера, хочу рассказать об одной из интереснейших разновидностей "Турецких шашек [48]".

В эту игру играют в Бахрейне [49]. Все правила «Турецких шашек» выполняются. Вводится всего лишь одно новое правило, отсутствующее в оригинальной игре. Это правило очень просто формулируется и, в некоторых случаях, существенно влияет на характер игры, делая её более комбинационной. Для меня оно было крайне неожиданной и интересной находкой. Сможете ли вы определить, в чём оно заключается, по записи игры?

Ответ

Правило стало сюрпризом для меня. В той разновидности «Турецких шашек», в которую играют в Бахрейне, вводится понятие шаха [50], очень похожее на шахматное. Ситуация, когда на одну из фигур осуществляется нападение, называется «Kish» (شك). Правила запрещают отвечать нападением, если предыдущим ходом противник напал на одну из ваших фигур (то есть делать «kish...kish»). При этом, устранять угрозу не обязательно. Можно выполнить любой ход, при условии, что он не угрожает противнику.

Поскольку тактика «ответного» нападения широко используется в «Турецких шашках», подобное нововведение весьма пикантно разнообразит игру. В свою очередь, для меня было весьма интересно узнать о шашках ещё что-то новое, а также попытаться выразить это посредством ZRF [51].

Автор: GlukKazan

Источник [52]


Сайт-источник PVSM.RU: https://www.pvsm.ru

Путь до страницы источника: https://www.pvsm.ru/razrabotka-igr/198805

Ссылки в тексте:

[1] Маленький принц: http://lib.ru/EKZUPERY/mprinc.txt

[2] Квантовых крестиках-ноликах: https://habrahabr.ru/post/276329/

[3] адовый: https://github.com/GlukKazan/ZoG/blob/master/Axiom/QXO/Code.4th

[4] Ур: https://habrahabr.ru/post/224661/

[5] Zillions of Games: http://www.zillions-of-games.com/

[6] Axiom: http://www.zillions-of-games.com/cgi-bin/zilligames/submissions.cgi?do=show;id=1452

[7] уж точно: https://habrahabr.ru/post/226235/

[8] реконструкциях: http://skyruk.livejournal.com/211326.html

[9] Белла: https://en.wikipedia.org/wiki/Robert_Charles_Bell

[10] Мюррея: https://en.wikipedia.org/wiki/H._J._R._Murray

[11] вариант: http://skyruk.livejournal.com/231444.html?thread=4494868

[12] Дмитрием Скирюком: https://ru.wikipedia.org/wiki/%D0%A1%D0%BA%D0%B8%D1%80%D1%8E%D0%BA,_%D0%94%D0%BC%D0%B8%D1%82%D1%80%D0%B8%D0%B9_%D0%98%D0%B3%D0%BE%D1%80%D0%B5%D0%B2%D0%B8%D1%87

[13] Nomad1: https://habrahabr.ru/users/nomad1/

[14] Android и iOS: https://habrahabr.ru/post/225631

[15] 1eqinfinity: https://habrahabr.ru/users/1eqinfinity/

[16] внешний вид: http://www.zillions-of-games.com/cgi-bin/zilligames/submissions.cgi?do=show;id=2262

[17] LIFO: https://ru.wikipedia.org/wiki/LIFO

[18] Zillions of Games: http://www.zillions-of-games.com

[19] Focus: http://www.zillions-of-games.com/cgi-bin/zilligames/submissions.cgi?do=show;id=2369

[20] манкалах: https://habrahabr.ru/post/272119/

[21] Ритмомахии: https://habrahabr.ru/post/234587/

[22] эту игру: http://www.zillions-of-games.com/cgi-bin/zilligames/submissions.cgi?do=show;id=2525

[23] игры: https://habrahabr.ru/post/278853/

[24] скрипт: https://github.com/GlukKazan/ZoG/blob/master/utils/perl/grid.pl

[25] MarGo: https://habrahabr.ru/post/274043/

[26] решение: https://github.com/GlukKazan/ZoG/blob/master/Rules/Ur.zrf

[27] Это: http://www.chessvariants.com/other.dir/elephant_hunt.html

[28] Итури: https://ru.wikipedia.org/wiki/%D0%98%D1%82%D1%83%D1%80%D0%B8

[29] автора: http://www.chessvariants.com/who/Freederick

[30] ходом коня: https://ru.wikipedia.org/wiki/%D0%9A%D0%BE%D0%BD%D1%8C_(%D1%88%D0%B0%D1%85%D0%BC%D0%B0%D1%82%D1%8B)

[31] Ralph Betza's ''funny notation'': https://en.wikipedia.org/wiki/Fairy_chess_piece#Ralph_Betza.27s_.22funny_notation.22

[32] ранее: https://habrahabr.ru/post/309096/

[33] Лиса и гуси: https://ru.wikipedia.org/wiki/%D0%9B%D0%B8%D1%81%D0%B0_%D0%B8_%D0%B3%D1%83%D1%81%D0%B8

[34] патч: https://github.com/GlukKazan/ZoG/commit/c78cab2fc6c32ca8eaf1c0f270ceed935b4bbf7d

[35] статей: http://skyruk.livejournal.com/493105.html

[36] Поединок: http://www.zillions-of-games.com/cgi-bin/zilligames/submissions.cgi?do=show;id=2524

[37] Ko Shogi: https://geektimes.ru/post/269152/

[38] Nine-Tile Cyvasse: https://habrahabr.ru/post/283502/

[39] Mana: http://www.zillions-of-games.com/cgi-bin/zilligames/submissions.cgi?do=show;id=2462

[40] Mana: https://github.com/GlukKazan/ZoG/blob/master/Rules/Mana.zrf

[41] Jocly: https://www.jocly.com/#/about

[42] реализацию: https://www.jocly.com/#/game/mana

[43] Salta: http://skyruk.livejournal.com/424973.html

[44] Traverse: http://skyruk.livejournal.com/488033.html

[45] Халма: https://ru.wikipedia.org/wiki/%D0%A5%D0%B0%D0%BB%D0%BC%D0%B0

[46] Luzhanqi: http://www.zillions-of-games.com/cgi-bin/zilligames/submissions.cgi?do=show;id=2515

[47] Осетинские шашки: http://www.zillions-of-games.com/cgi-bin/zilligames/submissions.cgi?do=show;id=2351

[48] Турецких шашек: https://ru.wikipedia.org/wiki/%D0%A2%D1%83%D1%80%D0%B5%D1%86%D0%BA%D0%B8%D0%B5_%D1%88%D0%B0%D1%88%D0%BA%D0%B8

[49] Бахрейне: https://ru.wikipedia.org/wiki/%D0%91%D0%B0%D1%85%D1%80%D0%B5%D0%B9%D0%BD

[50] шаха: https://ru.wikipedia.org/wiki/%D0%A8%D0%B0%D1%85_(%D1%88%D0%B0%D1%85%D0%BC%D0%B0%D1%82%D1%8B)

[51] ZRF: https://github.com/GlukKazan/ZoG/commit/c09e518984ddf79d9ccbd4a89edc38e51c620815

[52] Источник: https://habrahabr.ru/post/310292/?utm_source=habrahabr&utm_medium=rss&utm_campaign=best