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

В этой небольшой статье мы нарисуем игровое поле для Колонизаторов с помощью SQL.

Для начала, заглянем в правила.
В игре используется шестиугольное игровое поле. Игровое поле - это остров. Естественно, остров окружен морем. Поле заполняется 19-ю шестиугольными плитками "суши", которые должны быть распределены о полю случайным образом. Плитки "суши" могут быть пяти типов, соответственно типам ресурсов, которые на них находятся: "дерево", "глина", "шерсть", "зерно" и "руда" (3 + 4 + 5 + 4 + 3).
Кроме того, на 18 из этих плиток надо разложить карточки с очками. 19-я плитка - это "Пустыня".
Наконец, поскольку остров окружен морем, по краям надо расположить 9 плиток с гаванями. Плитки с гаванями должны быть случайным образом распределены по своим местам на поле.

Плитки суши

Каждая плитка будет определена своей позицией на двух осях, где оси расположены под 60 градусов по отношению друг к другу, с началом в левом верхнем углу. Мы будем называть это трапециевидным кодированием для плиток. Ось х будет диагональной (из левого нижнего угла в правый верхний угол), ось у будет горизонтальной.

Напишем маленькое табличное выражение, чтобы определить координаты каждой плитки суши.

CTE для координат суши:
WITH    tiles (rn, x, y) AS
        (
        VALUES
                (1, 0, 0),
                (2, 1, 0),
                (3, 2, 0),
                (4, 3, 1),
                (5, 4, 2),
                (6, 4, 3),
                (7, 4, 4),
                (8, 3, 4),
                (9, 2, 4),
                (10, 1, 3),
                (11, 0, 2),
                (12, 0, 1),
                (13, 1, 1),
                (14, 2, 1),
                (15, 3, 2),
                (16, 3, 3),
                (17, 2, 3),
                (18, 1, 2),
                (19, 2, 2)
        )
SELECT  *
FROM    tiles
Вывод СTE:
rn      x       y
1       0       0
2       1       0
3       2       0
4       3       1
5       4       2
6       4       3
7       4       4
8       3       4
9       2       4
10      1       3
11      0       2
12      0       1
13      1       1
14      2       1
15      3       2
16      3       3
17      2       3
18      1       2
19      2       2

Ресурсы

Каждой плитке суши надо присвоить тип ресурсов - один из пяти доступных.
Закодируем ресурсы символами, по одному символу на каждый тип ресурсов.
Мы будем использовать / для зерна, # для дерева, > для руды, " для шерсти и . для глины. Пустыню обозначим, как пустое место.
Хотя эти символы и не очень наглядные, они хорошо подходят для ASCII рисования.

Мы сделаем просто: запишем все доступные ресурсы в одну строку, а затем перемешаем символы в строке с помощью ORDER BY RANDOM().

Запрос для таблицы ресурсов
WITH    tiles (rn, x, y) AS
        (
        VALUES
                (1, 0, 0),
                (2, 1, 0),
                (3, 2, 0),
                (4, 3, 1),
                (5, 4, 2),
                (6, 4, 3),
                (7, 4, 4),
                (8, 3, 4),
                (9, 2, 4),
                (10, 1, 3),
                (11, 0, 2),
                (12, 0, 1),
                (13, 1, 1),
                (14, 2, 1),
                (15, 3, 2),
                (16, 3, 3),
                (17, 2, 3),
                (18, 1, 2),
                (19, 2, 2)
        ),
        resources AS
        (
        SELECT  '////####^^^' || CHR(34) || CHR(34) || CHR(34) || CHR(34) || '... '::TEXT tile_resources
        )
SELECT  *
FROM    tiles
JOIN    (
        SELECT  ROW_NUMBER() OVER (ORDER BY RANDOM()) rn,
                resource
        FROM    resources
        CROSS JOIN
                LATERAL
                REGEXP_SPLIT_TO_TABLE(tile_resources, '') q (resource)
        ) tr
USING   (rn)
Вывод таблицы ресурсов:
rn      x       y       resource
1       0       0       ^
2       1       0       #
3       2       0        
4       3       1       #
5       4       2       #
6       4       3       "
7       4       4       "
8       3       4       /
9       2       4       .
10      1       3       "
11      0       2       ^
12      0       1       .
13      1       1       #
14      2       1       ^
15      3       2       /
16      3       3       /
17      2       3       "
18      1       2       /
19      2       2       .

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

Очки

Каждая плитка суши (кроме пустыни) должна иметь карточку очков. Количество очков находится в промежутке от 2 до 12 (так чтобы можно было выбросить двумя игральными костями). Число 7 не используется; очкам от 2 до 12 соответствуют по одной карточке, а остальным очкам по две карточки. Получается 18 карточек с очками в сумме.

Хитрость в том, что по правилам плитки с очками 6 и 8 не могут находиться рядом. Это означает, что чисто случайное распределение (такое же, как мы использовали для плиток с ресурсами) не работает.

Если верить правилам, то "нам возможно потребуется поменять карточки очков местами так, чтобы никакие два красных номера не были рядом". Однако соответствующие алгоритмы, если не аккуратно проработаны, известны тем, что вносят предвзятость, так что некоторые перестановки могут быть более вероятны, чем другие.

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

Для того чтобы это сделать, мы сначала сгенерируем случайный массив из значений очков. Затем мы проверим этот массив и соединим (join) с плитками суши и таблицей ресурсов, которые мы сгенерировали на предыдущих шагах. Одна хитрость состоит в том, что у нас 19 плиток суши, но 18 карточек очков, поэтому нам нужно будет соединять их, пропуская плитку пустыни. Мы обойдём эту проблему с помощью простой аналитической функции, которая будет пропускать число после плитки пустыни.

Когда мы соединим плитки и очки, мы должны будем убедиться, что 6 и 8 не находяться рядом. Для этого мы сделаем SELF JOIN таблиц с очками и плитками, используя некоторые свойства шестиугольной сетки.

Как нам понять, что две плитки являются соседними? В первую очередь, они должны быть на расстоянии 1 друг от друга по каждой из осей. Это значит, что разница значений координат по каждой из осей должна быть от -1 до 1. Во-вторых, координаты не должны полностью совпадать, т.е. по крайней мере одна координата должна отличаться. В-третьих, (и это главное отличие шестиугольной сетки от прямоугольной) в шестиугольной сетке каждая плитка имеет 6 соседей, тогда как в прямоугольной восемь. Это означает, что даже если две координаты отличаются не более чем на единицу, плитки всё равно могут не быть соседями. Это действительно так для плиток (-1, 1) и (1, -1).

Таким образом, для кадой плитки список возможных соседей следующий (-1, -1), (-1, 0), (0, -1), (0, 1), (1, 0), (1, 1). Естественно, здесь имеются ввиду сдвиги относительной текущей координаты. Список довольно небольшой, поэтому мы будем использовать парные предикаты в условии соединения. Мы будем отбраковывать раскладку плиток, где встретятся две 6-ки или две 8-ки, разницы между координатами плиток входят в список соседей выше.

"Отбраковывать" раскладку - означает перетасовать массив очков и дать его на вход следующей итерации рекурсивного табличного выражения (CTE).

Создаем таблицу распределения очков:
SELECT  SETSEED(0.201703);
 
WITH    RECURSIVE
        resources AS
        (
        SELECT  '////####^^^' || CHR(34) || CHR(34) || CHR(34) || CHR(34) || '... '::TEXT tile_resources
        ),
        tiles (rn, x, y) AS
        (
        VALUES
                (1, 0, 0),
                (2, 1, 0),
                (3, 2, 0),
                (4, 3, 1),
                (5, 4, 2),
                (6, 4, 3),
                (7, 4, 4),
                (8, 3, 4),
                (9, 2, 4),
                (10, 1, 3),
                (11, 0, 2),
                (12, 0, 1),
                (13, 1, 1),
                (14, 2, 1),
                (15, 3, 2),
                (16, 3, 3),
                (17, 2, 3),
                (18, 1, 2),
                (19, 2, 2)
        ),
        layout AS
        (
        SELECT  *,
                CASE resource
                WHEN ' ' THEN
                        NULL
                ELSE
                        rn + SUM(CASE resource WHEN ' ' THEN -1 ELSE 0 END) OVER (ORDER BY rn)
                END score_rn
        FROM    tiles
        JOIN    (
                SELECT  ROW_NUMBER() OVER (ORDER BY RANDOM()) rn,
                        resource
                FROM    resources
                CROSS JOIN
                        LATERAL
                        REGEXP_SPLIT_TO_TABLE(tile_resources, '') q (resource)
                ) tr
        USING   (rn)
        ),
        score AS
        (
        SELECT  1 attempt,
                ARRAY_AGG(s ORDER BY RANDOM()) score_array,
                NULL::BIGINT desert
        FROM    generate_series(2, 12) s
        CROSS JOIN
                generate_series(1, 2) r
        WHERE   s <> 7
                AND NOT (r = 2 AND s IN (2, 12))
        UNION ALL
        SELECT  attempt + 1 attempt,
                sa.score_array,
                (
                SELECT  rn
                FROM    layout
                WHERE   score_rn IS NULL
                ) desert
        FROM    (
                SELECT  *
                FROM    score
                WHERE   EXISTS
                        (
                        SELECT  NULL
                        FROM    (
                                SELECT  *
                                FROM    UNNEST(score_array) WITH ORDINALITY q(s1, score_rn)
                                JOIN    layout
                                USING   (score_rn)
                                ) sc1
                        JOIN    (
                                SELECT  *
                                FROM    UNNEST(score_array) WITH ORDINALITY q(s2, score_rn)
                                JOIN    layout
                                USING   (score_rn)
                                ) sc2
                        ON      s1 IN (6, 8)
                                AND s2 IN (6, 8)
                                AND ((sc1.x - sc2.x), (sc1.y - sc2.y)) IN ((-1, -1), (-1, 0), (0, -1), (0, 1), (1, 0), (1, 1))
                        )
                ) s
        CROSS JOIN
                LATERAL
                (
                SELECT  ARRAY_AGG(score ORDER BY RANDOM()) score_array
                FROM    UNNEST(score_array) WITH ORDINALITY q(score, score_rn)
                ) sa
        ),
        score_good AS
        (
        SELECT  score, score_rn
        FROM    (
                SELECT  *
                FROM    score
                ORDER BY
                        attempt DESC
                LIMIT 1
                ) s
        CROSS JOIN
                LATERAL
                UNNEST(score_array) WITH ORDINALITY q (score, score_rn)
        )
SELECT  *
FROM    score_good
Вывод таблицы очков:
score   score_rn
8       1
2       2
6       3
10      4
11      5
11      6
9       7
3       8
4       9
8       10
9       11
4       12
12      13
5       14
10      15
5       16
3       17
6       18

Мы добавили вызов SETSEED для того чтобы результаты были воспроизводимы.

Можно заметить, что запросу понадобилось 7 попыток на то,чтобы сгенерировать подходящую раскладку очков. В первой перестановке 6 и 8 были на соседних плитках 7 и 16; во второй дыло две 8-ки рядом на плитках 17 и 18 и т.д.

Как только мы получили корректную расстановку для карточек очков мы можем соединить её с остальными таблицами:

Совмещаем очки, координаты и ресурсы:
SELECT  SETSEED(0.201703);
 
WITH    RECURSIVE
        resources AS
        (
        SELECT  '////####^^^' || CHR(34) || CHR(34) || CHR(34) || CHR(34) || '... '::TEXT tile_resources
        ),
        tiles (rn, x, y) AS
        (
        VALUES
                (1, 0, 0),
                (2, 1, 0),
                (3, 2, 0),
                (4, 3, 1),
                (5, 4, 2),
                (6, 4, 3),
                (7, 4, 4),
                (8, 3, 4),
                (9, 2, 4),
                (10, 1, 3),
                (11, 0, 2),
                (12, 0, 1),
                (13, 1, 1),
                (14, 2, 1),
                (15, 3, 2),
                (16, 3, 3),
                (17, 2, 3),
                (18, 1, 2),
                (19, 2, 2)
        ),
        layout AS
        (
        SELECT  *,
                CASE resource
                WHEN ' ' THEN
                        NULL
                ELSE
                        rn + SUM(CASE resource WHEN ' ' THEN -1 ELSE 0 END) OVER (ORDER BY rn)
                END score_rn
        FROM    tiles
        JOIN    (
                SELECT  ROW_NUMBER() OVER (ORDER BY RANDOM()) rn,
                        resource
                FROM    resources
                CROSS JOIN
                        LATERAL
                        REGEXP_SPLIT_TO_TABLE(tile_resources, '') q (resource)
                ) tr
        USING   (rn)
        ),
        score AS
        (
        SELECT  1 attempt,
                ARRAY_AGG(s ORDER BY RANDOM()) score_array,
                NULL::BIGINT desert
        FROM    generate_series(2, 12) s
        CROSS JOIN
                generate_series(1, 2) r
        WHERE   s <> 7
                AND NOT (r = 2 AND s IN (2, 12))
        UNION ALL
        SELECT  attempt + 1 attempt,
                sa.score_array,
                (
                SELECT  rn
                FROM    layout
                WHERE   score_rn IS NULL
                ) desert
        FROM    (
                SELECT  *
                FROM    score
                WHERE   EXISTS
                        (
                        SELECT  NULL
                        FROM    (
                                SELECT  *
                                FROM    UNNEST(score_array) WITH ORDINALITY q(s1, score_rn)
                                JOIN    layout
                                USING   (score_rn)
                                ) sc1
                        JOIN    (
                                SELECT  *
                                FROM    UNNEST(score_array) WITH ORDINALITY q(s2, score_rn)
                                JOIN    layout
                                USING   (score_rn)
                                ) sc2
                        ON      s1 IN (6, 8)
                                AND s2 IN (6, 8)
                                AND ((sc1.x - sc2.x), (sc1.y - sc2.y)) IN ((-1, -1), (-1, 0), (0, -1), (0, 1), (1, 0), (1, 1))
                        )
                ) s
        CROSS JOIN
                LATERAL
                (
                SELECT  ARRAY_AGG(score ORDER BY RANDOM()) score_array
                FROM    UNNEST(s.score_array) WITH ORDINALITY q(score, score_rn)
                ) sa
        ),
        score_good AS
        (
        SELECT  score, score_rn, attempt
        FROM    (
                SELECT  *
                FROM    score
                ORDER BY
                        attempt DESC
                LIMIT 1
                ) s
        CROSS JOIN
                LATERAL
                UNNEST(score_array) WITH ORDINALITY q (score, score_rn)
        )
SELECT  *
FROM    layout
LEFT JOIN
        score_good
USING   (score_rn)
ORDER BY
        rn;
Вывод соответствующей таблицы:
score_rn        rn      x       y       resource        score   attempt
1                1      0       0       #               11      5
2                2      1       0       "               11      5
3                3      2       0       .               8       5
4                4      3       1       #               10      5
5                5      4       2       /               8       5
6                6      4       3       "               3       5
7                7      4       4       ^               3       5
None             8      3       4                       None    None
8                9      2       4       /               4       5
9               10      1       3       /               5       5
10              11      0       2       #               9       5
11              12      0       1       #               6       5
12              13      1       1       "               9       5
13              14      2       1       ^               4       5
14              15      3       2       .               2       5
15              16      3       3       ^               5       5
16              17      2       3       "               10      5
17              18      1       2       /               12      5
18              19      2       2       .               6       5

Гавани

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

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

Запрос для расположения гаваней:
WITH    RECURSIVE
        resources AS
        (
        SELECT  '////####^^^' || CHR(34) || CHR(34) || CHR(34) || CHR(34) || '... '::TEXT tile_resources,
                '/#^' || CHR(34) || '.????'::TEXT harbor_resources
        ),
        harbors (rn, x, y, pier1, pier2) AS
        (
        VALUES
                (1, -1, -1, 0, 1),
                (2, 1, -1, 1, 2),
                (3, 3, 0, 1, 2),
                (4, 5, 2, 2, 3),
                (5, 5, 4, 3, 4),
                (6, 4, 5, 3, 4),
                (7, 2, 5, 4, 5),
                (8, 0, 3, 5, 0),
                (9, -1, 1, 5, 0)
        ),
        harbor_resources AS
        (
        SELECT  '/#>".????'::TEXT harbor_resources
        )
SELECT  resource, rn, x, y, pier1, pier2
FROM    harbors
CROSS JOIN
        resources
JOIN    LATERAL
        (
        SELECT  resource, ROW_NUMBER() OVER (ORDER BY RANDOM()) rn
        FROM    REGEXP_SPLIT_TO_TABLE(harbor_resources, '') q (resource)
        ) q
USING   (rn)
ORDER BY
        RANDOM()
Вывод таблицы с расположением гаваней:
resource        rn      x       y       pier1   pier2
#               3       3       0       1       2
"               5       5       4       3       4
?               1       -1      -1      0       1
?               8       0       3       5       0
^               4       5       2       2       3
/               9       -1      1       5       0
?               7       2       5       4       5
?               6       4       5       3       4
.               2       1       -1      1       2

Собираем всё это вместе

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

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

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

Для шестиугольников мы сначала сгенерируем квадрат с помощью перекрестного соединения (cross-join) двух таблиц GENERATE_SERIES, а затем отфильтруем результат, чтобы получился шестиугольник.

Центры каждого шестиугольника рассчитываются из их координат х и у: х берется как есть, а у сдвигается на половину соответствующего значения х (поскольку координатные оси находятся по углом 60 градусов друг к другу).

Условие фильтра для шестиугольника следующее: если символ находится в промежутке -1/4 до 1/4 высоты, считая от центра, мы выводим этот символ; если символ в промежутке -1/2 до -1/4 (или 1/4 до 1/2 с другой стороны), мы выводим его только в том случае, если его горизонтальная координата не больше чем два расстояния с верху до низу, соответственно. Первая часть формирует прямоугольник внутри шестиугольника; вторая часть формирует два треугольника - сверху и снизу.

Маленькие шестиугольники строятся по тому же принципу, только их высота и широта меньше.

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

Нам также надо как-то вывести гавани и причалы. Символы для гавани выводятся по соответствующим координатам на сетке (учитывая предыдущие рассуждения). Причалы будут сдвинуты на приблизительные значения синусов и косинусов их соответствующих углов (записанные как множители 60 градусов)ю Мы не будем считать синусы и косинусами, а вместо это воспользуемся таблицами приближенных значений.

Некоторые из символов будут накладываться друг на друга: например, центр каждой плитки будет содержать символ из большого шестиугольника, символ из маленького шестиугольника и символ ресурса. Чтобы решить эту проблему, каждому символу мы присвоим номер слоя, на котором он находится. Большие шестиугольники будут на слое № 1, маленькие шестиугольники на слое № 2, а символы очков на самом верхнем 3 слое. Если несколько символов имеют одни координаты, выводится символ с наибольшим номером слоя. Мы будем выводить их все в одном запросе, поэтому гаваням и причалам тоже понадобится слой, даже если они ни с чем не пересекаются. Мы дадим им 4 слой.

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

Вот, что получается:

Запрос для построения поля целиком:
SELECT  SETSEED(0.201704);
 
WITH    RECURSIVE
        resources AS
        (
        SELECT  '////####^^^' || CHR(34) || CHR(34) || CHR(34) || CHR(34) || '... '::TEXT tile_resources,
                '/#^' || CHR(34) || '.????'::TEXT harbor_resources
        ),
        tiles (rn, x, y) AS
        (
        VALUES
                (1, 0, 0),
                (2, 1, 0),
                (3, 2, 0),
                (4, 3, 1),
                (5, 4, 2),
                (6, 4, 3),
                (7, 4, 4),
                (8, 3, 4),
                (9, 2, 4),
                (10, 1, 3),
                (11, 0, 2),
                (12, 0, 1),
                (13, 1, 1),
                (14, 2, 1),
                (15, 3, 2),
                (16, 3, 3),
                (17, 2, 3),
                (18, 1, 2),
                (19, 2, 2)
        ),
        harbors (rn, x, y, pier1, pier2) AS
        (
        VALUES
                (1, -1, -1, 0, 1),
                (2, 1, -1, 1, 2),
                (3, 3, 0, 1, 2),
                (4, 5, 2, 2, 3),
                (5, 5, 4, 3, 4),
                (6, 4, 5, 3, 4),
                (7, 2, 5, 4, 5),
                (8, 0, 3, 5, 0),
                (9, -1, 1, 5, 0)
        ),
        score AS
        (
        SELECT  1 attempt,
                ARRAY_AGG(s ORDER BY RANDOM()) score_array
        FROM    generate_series(2, 12) s
        CROSS JOIN
                generate_series(1, 2) r
        WHERE   s <> 7
                AND NOT (r = 2 AND s IN (2, 12))
        UNION ALL
        SELECT  attempt + 1 attempt,
                sa.score_array
        FROM    (
                SELECT  *
                FROM    score
                WHERE   EXISTS
                        (
                        SELECT  NULL
                        FROM    (
                                SELECT  *
                                FROM    UNNEST(score_array) WITH ORDINALITY q(s1, rn)
                                JOIN    tiles
                                USING   (rn)
                                ) sc1
                        JOIN    (
                                SELECT  *
                                FROM    UNNEST(score_array) WITH ORDINALITY q(s2, rn)
                                JOIN    tiles t
                                USING   (rn)
                                ) sc2
                        ON      s1 IN (6, 8)
                                AND s2 IN (6, 8)
                                AND ((sc1.x - sc2.x), (sc1.y - sc2.y)) IN ((-1, -1), (-1, 0), (0, -1), (0, 1), (1, 0), (1, 1))
                        )
                ) s
        CROSS JOIN
                LATERAL
                (
                SELECT  ARRAY_AGG(score ORDER BY RANDOM()) score_array
                FROM    UNNEST(score_array) WITH ORDINALITY q(score, score_rn)
                ) sa
        ),
        score_good AS
        (
        SELECT  score, score_rn
        FROM    (
                SELECT  *
                FROM    score
                ORDER BY
                        attempt DESC
                LIMIT 1
                ) s
        CROSS JOIN
                LATERAL
                UNNEST(score_array) WITH ORDINALITY q (score, score_rn)
        ),
        layout AS
        (
        SELECT  *
        FROM    (
                SELECT  *,
                        CASE resource
                        WHEN ' ' THEN
                                NULL
                        ELSE
                                rn + SUM(CASE resource WHEN ' ' THEN -1 ELSE 0 END) OVER (ORDER BY rn)
                        END score_rn
                FROM    tiles
                JOIN    (
                        SELECT  ROW_NUMBER() OVER (ORDER BY RANDOM()) rn,
                                resource
                        FROM    resources
                        CROSS JOIN
                                LATERAL
                                REGEXP_SPLIT_TO_TABLE(tile_resources, '') q (resource)
                        ) tr
                USING   (rn)
                ) t
        LEFT JOIN
                score_good
        USING   (score_rn)
        ORDER BY
                rn
        )
SELECT  row
FROM    (
        SELECT  r,
                STRING_AGG(COALESCE(letter, ' '), '' ORDER BY c) AS row
        FROM    generate_series(0, 70) r
        CROSS JOIN
                generate_series(0, 89) c
        LEFT JOIN
                (
                SELECT  *
                FROM    (
                        SELECT  *,
                                ROW_NUMBER() OVER (PARTITION BY r, c ORDER BY layer DESC) rn
                        FROM    (
                                SELECT  10 height,
                                        16 width
                                ) d
                        CROSS JOIN
                                LATERAL
                                (
                                SELECT  letter, r, c, layer
                                FROM    layout
                                CROSS JOIN
                                        LATERAL
                                        (
                                        SELECT  height * x + 15 center_r,
                                                width * y - (width / 2)::INT * x + 24 center_c
                                        ) c
                                CROSS JOIN
                                        LATERAL
                                        (
                                        SELECT  *
                                        FROM    (
                                                SELECT  1 layer, resource letter, center_r + rs r, center_c + cs c
                                                FROM    (
                                                        SELECT  height * 1.5 * 0.8 th, width * 0.9 tw
                                                        ) t
                                                CROSS JOIN
                                                        generate_series(-(th / 2)::INT, (th / 2)::INT) rs
                                                CROSS JOIN
                                                        generate_series(-(tw / 2)::INT, (tw / 2)::INT ) cs
                                                CROSS JOIN
                                                        LATERAL
                                                        (
                                                        SELECT  rs::FLOAT / th rsf, cs::FLOAT / tw csf
                                                        ) f
                                                WHERE   rsf BETWEEN -0.25 AND 0.25
                                                        OR
                                                        ABS(csf) BETWEEN 0 AND 1 - ABS(rsf * 2)
                                                UNION ALL
                                                SELECT  2 layer, ' ', center_r + rs r, center_c + cs c
                                                FROM    (
                                                        SELECT  height * 1.5 * 0.35 th, width * 0.35 tw
                                                        ) t
                                                CROSS JOIN
                                                        generate_series(-(th / 2)::INT, (th / 2)::INT) rs
                                                CROSS JOIN
                                                        generate_series(-(tw / 2)::INT, (tw / 2)::INT ) cs
                                                CROSS JOIN
                                                        LATERAL
                                                        (
                                                        SELECT  rs::FLOAT / th rsf, cs::FLOAT / tw csf
                                                        ) f
                                                WHERE   rsf BETWEEN -0.25 AND 0.25
                                                        OR
                                                        ABS(csf) BETWEEN 0 AND 1 - ABS(rsf * 2)
                                                UNION ALL
                                                SELECT  3 layer, score_letter letter, center_r r, center_c + pos - 1 c
                                                FROM    REGEXP_SPLIT_TO_TABLE(score::TEXT, '') WITH ORDINALITY l(score_letter, pos)
                                                ) q
                                        ) q2
                                UNION ALL
                                SELECT  letter, r, c, 4 layer
                                FROM    harbors
                                JOIN    LATERAL
                                        (
                                        SELECT  resource, ROW_NUMBER() OVER (ORDER BY RANDOM()) rn
                                        FROM    resources
                                        CROSS JOIN
                                                LATERAL
                                                REGEXP_SPLIT_TO_TABLE(harbor_resources, '') q (resource)
                                        ) q2
                                USING   (rn)
                                CROSS JOIN
                                        LATERAL
                                        (
                                        SELECT  height * x + 15 center_r,
                                                width * y - (width / 2)::INT * x + 25 center_c
                                        ) c
                                CROSS JOIN
                                        LATERAL
                                        (
                                        SELECT  resource letter, center_r r, center_c c
                                        UNION ALL
                                        SELECT  letter, r, c
                                        FROM    (
                                                SELECT  pier1
                                                UNION ALL
                                                SELECT  pier2
                                                ) p (pier)
                                        CROSS JOIN
                                                LATERAL
                                                (
                                                SELECT  SUBSTRING('|\/|\/', (pier + 1), 1) letter,
                                                        center_r + ((ARRAY[0.4, 0.2, -0.2, -0.4, -0.2, 0.2])[pier + 1] * height * 1.5 * 0.8)::INT r,
                                                        center_c + ((ARRAY[0, 0.3, 0.3, 0, -0.3, -0.3])[pier + 1] * width * 0.9)::INT c
                                                ) pl
                                        ) p2
                                ) q3
                        ) l
                        WHERE   rn = 1
                ) t
        USING   (r, c)
        GROUP BY
                r
        ) q
ORDER BY
        r
Игровое поле:
                 .                               ^                                        
                                                                                          
                     \                       /                                            
                                                                                          
                        /               .               "                                 
                 |    /////           .....      |    """""                               
                    /////////       .........       """""""""                             
                 /////////////// ............... """""""""""""""                          
                 //////   ////// ......   ...... """"""   """"""                          
                 ////       //// ....       .... """"       """"                          
                 ////   5   //// ....   11  .... """"   8   """"         "                
                 ////       //// ....       .... """"       """"                          
                 //////   ////// ......   ...... """"""   """"""     /                    
                 /////////////// ............... """""""""""""""                          
                "   /////////   "   .........   ^   """""""""   "                         
              """""   /////   """""   .....   ^^^^^   """""   """""      |                
            """""""""   /   """""""""   .   ^^^^^^^^^   "   """""""""                     
         """"""""""""""" """"""""""""""" ^^^^^^^^^^^^^^^ """""""""""""""                  
     /   """"""   """""" """"""   """""" ^^^^^^   ^^^^^^ """"""   """"""                  
         """"       """" """"       """" ^^^^       ^^^^ """"       """"                  
 ?       """"   9   """" """"   5   """" ^^^^   6   ^^^^ """"   4   """"                  
         """"       """" """"       """" ^^^^       ^^^^ """"       """"                  
     \   """"""   """""" """"""   """""" ^^^^^^   ^^^^^^ """"""   """"""                  
         """"""""""""""" """"""""""""""" ^^^^^^^^^^^^^^^ """""""""""""""                  
        ^   """""""""       """""""""   /   ^^^^^^^^^   .   """""""""   #                 
      ^^^^^   """""           """""   /////   ^^^^^   .....   """""   #####               
    ^^^^^^^^^   "               "   /////////   ^   .........   "   #########             
 ^^^^^^^^^^^^^^^                 /////////////// ............... ###############          
 ^^^^^^   ^^^^^^                 //////   ////// ......   ...... ######   ######     \    
 ^^^^       ^^^^                 ////       //// ....       .... ####       ####          
 ^^^^   11  ^^^^                 ////   3   //// ....   9   .... ####   12  ####         ?
 ^^^^       ^^^^                 ////       //// ....       .... ####       ####          
 ^^^^^^   ^^^^^^                 //////   ////// ......   ...... ######   ######     /    
 ^^^^^^^^^^^^^^^                 /////////////// ............... ###############          
    ^^^^^^^^^   #               /   /////////   #   .........   #   #########             
      ^^^^^   #####           /////   /////   #####   .....   #####   #####               
        ^   #########       /////////   /   #########   .   #########   #                 
         ############### /////////////// ############### ###############                  
     /   ######   ###### //////   ////// ######   ###### ######   ######                  
         ####       #### ////       //// ####       #### ####       ####                  
 /       ####   6   #### ////   10  //// ####   4   #### ####   2   ####                  
         ####       #### ////       //// ####       #### ####       ####                  
     \   ######   ###### //////   ////// ######   ###### ######   ######                  
         ############### /////////////// ############### ###############                  
            #########   /   /////////   .   #########   ^   #########                     
              #####   /////   /////   .....   #####   ^^^^^   #####      |                
                #   /////////   /   .........   #   ^^^^^^^^^   #                         
                 /////////////// ............... ^^^^^^^^^^^^^^^                          
                 //////   ////// ......   ...... ^^^^^^   ^^^^^^     \                    
                 ////       //// ....       .... ^^^^       ^^^^                          
                 ////   10  //// ....   8   .... ^^^^   3   ^^^^         #                
                 ////       //// ....       .... ^^^^       ^^^^                          
                 //////   ////// ......   ...... ^^^^^^   ^^^^^^                          
                 /////////////// ............... ^^^^^^^^^^^^^^^                          
                    /////////       .........       ^^^^^^^^^                             
                 |    /////           .....      |    ^^^^^                               
                        /               .               ^                                 
                                                                                          
                     /                       \                                            
                                                                                          
                 ?                               ?                                        

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

Новый Год уже не за горами, поэтому пусть он принесет всем читателям много приятных часов, проведенных за этой игрой с друзьями или семьёй!

Комментарии (2)


  1. Metotron0
    14.12.2024 14:22

    Какие же разные люди. Кому-то дано суметь так погрузиться в тему, а кому-то дано только по чуть-чуть отовсюду, но везде кое-как.


  1. atues
    14.12.2024 14:22

    Внушает. Тут не пол-литры, тут ящик нужен, чтобы разобраться