Воронополис

На июньском Базарном Дне я рассказывал о своём процессе подготовки и генерации карты города на основе ключевых локаций, которые сгенерировали игроки. Поскольку результат мне понравился, хочу поделиться плодами своих экспериментов, ну и, собственно, инструментами.

Задача

Я очень люблю механику Клинков, а в особенности огромный инструментарий, облегчающий жизнь ведущего. Не знаешь, как много смогли герои унести добра? Посмотри описание района, кинь столько кубов, сколько отмечено на шкале богатства. Как быстро прибудет стража? Посмотри описание района, кинь столько кубов, сколько отмечено на шкале закона, продвинь таймер. Насколько много здесь злых духов? Посмотри описание района… вы поняли. Добавим сюда проекты и конфликты банд, которые разворачиваются на фоне и продвигаются между сессиями, генератор улиц и их обитателей, и получаем отличный живой город, наполненный интересными событиями, местами и личностями.

Именно этого не хватает некоторым FitD-хакам, да и играм в городском сеттинге в целом. Проблема «оживления» города не нова, и можно найти кучу средств для этого. Я же сфокусировался на создании районов города. Хотелось получить что-то среднее между вот этими двумя вариантами:

Взято отсюда
Сгенерировано вот тут
1 of 2
Взято отсюда

Учитывая, что играть я хотел в супергероику, в частности, Shards in the Dark, мне нужен был современный мегаполис, разбитый на различные районы. Для каждого из них я хочу задать несколько параметров, описывающих эти районы с игромеханической точки зрения.

Альтернативы

У моего запроса было несколько изящных, но не очень подходящих мне вариантов решения, которые, тем не менее, могут отлично сработать у вас.

Настоящий город
Вполне годным решением было бы взять карту какого-нибудь Миннеаполиса и играть по ней. Snazzy Maps в этом очень помогает:

И это только один из стилей. Залипнуть можно надолго
И это только один из стилей. Залипнуть можно надолго

Ненастоящий город
Диаметральная противоположность первому решению — воспользоваться нейросетью и сгенерировать город, которого никогда не существовало. Вот только несколько примеров:

Воронополис, image #4
Воронополис, image #5
Воронополис, image #6
Воронополис, image #7
Воронополис, image #8
1 of 5

Оба эти варианта хороши, когда вам нужен город вот прямо сейчас, срочно. Тогда вы просто берёте один из вариантов, и буквально в любом графическом редакторе накидываете поверх карту районов.

5 минут в paint.net
5 минут в paint.net

Проблема такого подхода в том, что вы сначала имеете на руках готовый город, и только потом начинаете создавать в нём точки интереса. Что, если мои игроки хотят добавить пляж? Тюрьму? Огромный парк?

Поэтому я решил идти прямым путём: сначала определяем точки интереса, потом разбиваем холст на районы, и в конце натягиваем на районы карту города.

Разбиение Вороного

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

Диаграмма Вороного конечного множества точек S на плоскости представляет такое разбиение плоскости, при котором каждая область этого разбиения образует множество точек, более близких к одному из элементов множества S, чем к любому другому элементу множества — Wikipedia

Конечно, в идеале стоило бы написать процесс разбиения самостоятельно, тогда можно и Манхэттенскую метрику использовать, и весовые коэффициенты добавить, но это в будущем.

Сравните: Евклидова метрика
Сравните: Манхэттенская метрика
1 of 2
Сравните: Евклидова метрика

В итоге, я вооружился связкой python+scipy+numpy+cv2+matplotlib и начал кодить.

После нуля

Мы провели нулевую сессию с игроками, и в Miro накидали вот такую карту:

Некоторые названия будут изменены
Некоторые названия будут изменены

Уже можно представить себе будущую карту. Но сперва сделаем дубликат, уберём стрелочки, спрячем надписи и заменим пиктограммы на, собственно, точки. В итоге, чуть более причёсанная версия карты выглядит так:

Я решил удвоить океан. чтобы уменьшить будущий размер тюремного острова
Я решил удвоить океан. чтобы уменьшить будущий размер тюремного острова

Теперь пользуемся методами компьютерного зрения и прочими прелестями раннего киберпанка. Размоем изображение и сделаем маску, отделяющую цвет от фона:

Воронополис, image #14

У каждого из белых сегментов теперь можно найти координаты нижнего левого и верхнего правого углов. Отсюда вытащим координаты центров пятен и поместим их поверх оригинальной картинки:

Воронополис, image #15

Помимо этого, вытащим координаты центров на экран, они нам пригодятся позднее.

По-хорошему сохранить бы их в файлик, но у меня что-то питон на этом моменте раскапризничался, и я оставил как есть
По-хорошему сохранить бы их в файлик, но у меня что-то питон на этом моменте раскапризничался, и я оставил как есть

И всё, теперь добавляем несколько фэйковых точек за пределами экрана, scipy вместе с matplotlib делают свою магию, и мы получаем вот такой вот паттерн:

Цвета выглядят более бледными, т.к. я специально скрутил полупрозрачность до 0.4
Цвета выглядят более бледными, т.к. я специально скрутил полупрозрачность до 0.4

В принципе, этого уже достаточно, чтобы стартовать. Разбиение готово, цвета соблюдены, можно вставлять в любимый VT и восстанавливать названия районов.

Но иногда хочется большего.

Дополни-отбрось

На всё том же сервере SHG в процессе обсуждений с Сашей Брегановым и Рансвиндом возникла идея, что клетки Вороного не подойдут для некоторых типов игр. В частности, любой city crawl будет требовать равномерного разбиения на клетки, а лучше — на гексы. Разные площади ячеек здесь сыграют плохую службу и игрокам, и ведущему. Что ж, решим и эту проблему, переразбив нашу карту снова:

Остаётся только взять любимый генератор и добавить объектов в гексы. Размер гексов можно менять
Остаётся только взять любимый генератор и добавить объектов в гексы. Размер гексов можно менять

На том же сервере InstaHeat#3912 и Ани#7436 подкинули идею тепловой карты. Замечательно, сделаем и её. Вернее, несколько тепловых карт.

Будем определять районы по цвету, т.к. некоторые из них могут содержать более одной точки в разбиении. Каждому цвету поставим в соответствие своё значение каждого из трёх параметров: влияние криминала, богатство района, активность полиции. Подберём красивые цветовые карты из набора matplotlib и получим вот такие паттерны:

Криминал
Богатство
Полиция
1 of 3
Криминал

В принципе, всё готово, не хватает только одного.

Города.

Мучения

Я не уверен, что вообще взялся бы за этот проект, если бы noisyflasher не подкинул мне генератор карты города.

Вот он.

Вроде бы, всё понятно. Берём заготовку, добавляем тензорных полей, щёлкаем «сгенерировать», готово!

Проблема заключается в том, как мало контроля мы имеем над этим генератором на самом деле. Функция wrap в Medieval City Generator позволяет натянуть сову на глобус подогнать город под желаемые параметры, хоть и, порой, с трудом. Тут можно: двигать и изменять тензорные поля, изменять среднее расстояние между дорогами, изменять количество парков. Всё.

Всё остальное — только генерация за генерацией. Снова и снова, пока не выпадет то, что надо. К счастью, можно генерировать поэтапно, к чему я и призываю.

Помните набор координат, которые выводились на экран? Тут-то они нам и пригодятся. Используем их как координаты центров тензорных полей. Обратите внимание, что карта при этом не будет совпадать с тем, что мы видим на рисунке. Это возникает из-за того, что веб-страницы и графики в matplotlib по-разному направляют ось ординат. Благо, это решается простым flip vertical.

Часть сетки пропала, т.к. я уже нагенерировал воду. Получилось хорошо примерно с сотой попытки
Часть сетки пропала, т.к. я уже нагенерировал воду. Получилось хорошо примерно с сотой попытки

Подкручиваем углы поворота сетки, генерируем всё, пока нам не начнёт нравиться, и вуаля.

Воронополис, image #23

Жуть, правда?

Благо, нам не нужна вся эта карта, а только центральная её часть. Наложим поверх маску из районов, отразим по вертикали и получим вполне себе годный результат.

Воронополис, image #24

И не забываем про тепловые карты:

Воронополис, image #25
Воронополис, image #26
Воронополис, image #27
1 of 3

Итог

Картой я остался доволен, равно как и программой для её генерации. Нужно будет немного добавить деталей, например, поместить госпиталь Святой Элеаноры в Даунтауне и вернуть тюремный остров из небытия, но это как раз легко сделать, вручную скопировав подходящий объект с Google Maps.

Любые комментарии, как всегда, приветствуются.

Если захочется попользоваться программкой, то можно либо написать мне, либо заглянуть вот сюда. Я не профессиональный человек-программист, поэтому не судите строго.

P.S.

Пока писал статью, жена нашла на полке вот такую вот прелесть

Оцените разбиение Москвы по Манхэттенской метрике
Оцените разбиение Москвы по Манхэттенской метрике
671 views·9 shares