Думаю, почти все читатели хотя бы раз играли в Колонизаторов.
Настольная игра "Колонизаторы" стала одним из лучших новогодних подарков для автора текста. Мы с друзьями провели много времени, играя в эту игру, и, должен сказать, нам было довольно весело.
В этой небольшой статье мы нарисуем игровое поле для Колонизаторов с помощью 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 ^^^^ #
//// //// .... .... ^^^^ ^^^^
////// ////// ...... ...... ^^^^^^ ^^^^^^
/////////////// ............... ^^^^^^^^^^^^^^^
///////// ......... ^^^^^^^^^
| ///// ..... | ^^^^^
/ . ^
/
? ?
Итак, мы построили игровое поле для игры Колонизаторы, учитывая, что карточки суши могут иметь пять типов ресурсов, очки надо распределять рандомно по всему игровому полю, одну карточку суши надо оставить пустой, потому что там пустыня. Так же мы учли карточки гаваней и расположение портов в каждой из гаваней. И всё это на диагональной сетке, поскольку поле и карточки шестиугольные.
Новый Год уже не за горами, поэтому пусть он принесет всем читателям много приятных часов, проведенных за этой игрой с друзьями или семьёй!
Автор: altors